From cff9dab650b8bc0e453be6b312a96b778fdcbc93 Mon Sep 17 00:00:00 2001 From: apollo-dw <83023433+apollo-dw@users.noreply.github.com> Date: Tue, 14 Dec 2021 03:11:04 +0000 Subject: [PATCH 0001/2493] Remove combo scaling and change miss penalty --- .../Difficulty/OsuPerformanceCalculator.cs | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index 8d45c7a8cc..6de6572b93 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -101,13 +101,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty aimValue *= lengthBonus; - // Penalize misses by assessing # of misses relative to the total # of objects. Default a 3% reduction for any # of misses. if (effectiveMissCount > 0) - aimValue *= 0.97 * Math.Pow(1 - Math.Pow((double)effectiveMissCount / totalHits, 0.775), effectiveMissCount); - - // Combo scaling. - if (Attributes.MaxCombo > 0) - aimValue *= Math.Min(Math.Pow(scoreMaxCombo, 0.8) / Math.Pow(Attributes.MaxCombo, 0.8), 1.0); + aimValue *= Math.Pow(0.97, effectiveMissCount); double approachRateFactor = 0.0; if (Attributes.ApproachRate > 10.33) @@ -151,13 +146,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty (totalHits > 2000 ? Math.Log10(totalHits / 2000.0) * 0.5 : 0.0); speedValue *= lengthBonus; - // Penalize misses by assessing # of misses relative to the total # of objects. Default a 3% reduction for any # of misses. if (effectiveMissCount > 0) - speedValue *= 0.97 * Math.Pow(1 - Math.Pow((double)effectiveMissCount / totalHits, 0.775), Math.Pow(effectiveMissCount, .875)); - - // Combo scaling. - if (Attributes.MaxCombo > 0) - speedValue *= Math.Min(Math.Pow(scoreMaxCombo, 0.8) / Math.Pow(Attributes.MaxCombo, 0.8), 1.0); + speedValue *= Math.Pow(0.97, effectiveMissCount); double approachRateFactor = 0.0; if (Attributes.ApproachRate > 10.33) @@ -238,9 +228,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty if (mods.Any(h => h is OsuModHidden)) flashlightValue *= 1.3; - // Penalize misses by assessing # of misses relative to the total # of objects. Default a 3% reduction for any # of misses. if (effectiveMissCount > 0) - flashlightValue *= 0.97 * Math.Pow(1 - Math.Pow((double)effectiveMissCount / totalHits, 0.775), Math.Pow(effectiveMissCount, .875)); + flashlightValue *= Math.Pow(0.97, effectiveMissCount); // Combo scaling. if (Attributes.MaxCombo > 0) From 86ad42a7440ec0a1478e2030209c2675985e3220 Mon Sep 17 00:00:00 2001 From: apollo-dw <83023433+apollo-dw@users.noreply.github.com> Date: Tue, 14 Dec 2021 17:47:41 +0000 Subject: [PATCH 0002/2493] Nerf length bonus --- osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index 6de6572b93..6ade8c1732 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -96,7 +96,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty double aimValue = Math.Pow(5.0 * Math.Max(1.0, rawAim / 0.0675) - 4.0, 3.0) / 100000.0; // Longer maps are worth more. - double lengthBonus = 0.95 + 0.4 * Math.Min(1.0, totalHits / 2000.0) + + double lengthBonus = 0.95 + 0.25 * Math.Min(1.0, totalHits / 2000.0) + (totalHits > 2000 ? Math.Log10(totalHits / 2000.0) * 0.5 : 0.0); aimValue *= lengthBonus; @@ -142,7 +142,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty double speedValue = Math.Pow(5.0 * Math.Max(1.0, Attributes.SpeedStrain / 0.0675) - 4.0, 3.0) / 100000.0; // Longer maps are worth more. - double lengthBonus = 0.95 + 0.4 * Math.Min(1.0, totalHits / 2000.0) + + double lengthBonus = 0.95 + 0.25 * Math.Min(1.0, totalHits / 2000.0) + (totalHits > 2000 ? Math.Log10(totalHits / 2000.0) * 0.5 : 0.0); speedValue *= lengthBonus; From 489aa43b1bab6efeb0ed05e97ec66181dfe3b4f4 Mon Sep 17 00:00:00 2001 From: apollo-dw <83023433+apollo-dw@users.noreply.github.com> Date: Tue, 14 Dec 2021 19:57:36 +0000 Subject: [PATCH 0003/2493] Make miss penalty harsher --- .../Difficulty/OsuPerformanceCalculator.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index 6ade8c1732..0459c64c0e 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -96,13 +96,13 @@ namespace osu.Game.Rulesets.Osu.Difficulty double aimValue = Math.Pow(5.0 * Math.Max(1.0, rawAim / 0.0675) - 4.0, 3.0) / 100000.0; // Longer maps are worth more. - double lengthBonus = 0.95 + 0.25 * Math.Min(1.0, totalHits / 2000.0) + + double lengthBonus = 0.95 + 0.4 * Math.Min(1.0, totalHits / 2000.0) + (totalHits > 2000 ? Math.Log10(totalHits / 2000.0) * 0.5 : 0.0); aimValue *= lengthBonus; if (effectiveMissCount > 0) - aimValue *= Math.Pow(0.97, effectiveMissCount); + aimValue *= Math.Pow(0.96, effectiveMissCount); double approachRateFactor = 0.0; if (Attributes.ApproachRate > 10.33) @@ -142,12 +142,12 @@ namespace osu.Game.Rulesets.Osu.Difficulty double speedValue = Math.Pow(5.0 * Math.Max(1.0, Attributes.SpeedStrain / 0.0675) - 4.0, 3.0) / 100000.0; // Longer maps are worth more. - double lengthBonus = 0.95 + 0.25 * Math.Min(1.0, totalHits / 2000.0) + + double lengthBonus = 0.95 + 0.4 * Math.Min(1.0, totalHits / 2000.0) + (totalHits > 2000 ? Math.Log10(totalHits / 2000.0) * 0.5 : 0.0); speedValue *= lengthBonus; if (effectiveMissCount > 0) - speedValue *= Math.Pow(0.97, effectiveMissCount); + speedValue *= Math.Pow(0.96, effectiveMissCount); double approachRateFactor = 0.0; if (Attributes.ApproachRate > 10.33) @@ -229,7 +229,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty flashlightValue *= 1.3; if (effectiveMissCount > 0) - flashlightValue *= Math.Pow(0.97, effectiveMissCount); + flashlightValue *= Math.Pow(0.96, effectiveMissCount); // Combo scaling. if (Attributes.MaxCombo > 0) From bac4cfed50be4e3d1e0264ce0cd9099c3132d4b5 Mon Sep 17 00:00:00 2001 From: apollo-dw <83023433+apollo-dw@users.noreply.github.com> Date: Thu, 16 Dec 2021 18:37:57 +0000 Subject: [PATCH 0004/2493] Use frost's miss count penalty --- .../Difficulty/OsuPerformanceCalculator.cs | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index 0459c64c0e..bba53c92fc 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -102,7 +102,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty aimValue *= lengthBonus; if (effectiveMissCount > 0) - aimValue *= Math.Pow(0.96, effectiveMissCount); + aimValue *= calculateMissPenalty(effectiveMissCount); double approachRateFactor = 0.0; if (Attributes.ApproachRate > 10.33) @@ -147,7 +147,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty speedValue *= lengthBonus; if (effectiveMissCount > 0) - speedValue *= Math.Pow(0.96, effectiveMissCount); + speedValue *= calculateMissPenalty(effectiveMissCount); double approachRateFactor = 0.0; if (Attributes.ApproachRate > 10.33) @@ -229,7 +229,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty flashlightValue *= 1.3; if (effectiveMissCount > 0) - flashlightValue *= Math.Pow(0.96, effectiveMissCount); + flashlightValue *= calculateMissPenalty(effectiveMissCount); // Combo scaling. if (Attributes.MaxCombo > 0) @@ -265,6 +265,21 @@ namespace osu.Game.Rulesets.Osu.Difficulty return Math.Max(countMiss, (int)Math.Floor(comboBasedMissCount)); } + private double calculateMissPenalty(double missCount) + { + double leniency = 2.0; + + if (missCount > totalHits - leniency) + return 0; + + double missApprox = erfInvApprox((totalHits - leniency - missCount) / totalHits); + double fcApprox = erfInvApprox((totalHits - leniency) / totalHits); + + return Math.Pow(missApprox / fcApprox, 1.5); + } + + private double logit(double x) => Math.Log(x / (1 - x)); + private double erfInvApprox(double x) => (Math.Sqrt(Math.PI) / 4) * logit((x + 1) / 2); private int totalHits => countGreat + countOk + countMeh + countMiss; private int totalSuccessfulHits => countGreat + countOk + countMeh; } From 60e2a8ed4b2f0f38dafe0cc9bc6ab8c55128b189 Mon Sep 17 00:00:00 2001 From: apollo-dw <83023433+apollo-dw@users.noreply.github.com> Date: Tue, 21 Dec 2021 17:54:26 +0000 Subject: [PATCH 0005/2493] Use MathNet for miss penalty calculation, and use old penalty formula for Flashlight --- .../Difficulty/OsuPerformanceCalculator.cs | 14 +++++++------- osu.Game.Rulesets.Osu/osu.Game.Rulesets.Osu.csproj | 4 ++++ 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index bba53c92fc..90caa64512 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -9,6 +9,7 @@ using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; +using MathNet.Numerics; namespace osu.Game.Rulesets.Osu.Difficulty { @@ -228,8 +229,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty if (mods.Any(h => h is OsuModHidden)) flashlightValue *= 1.3; + // Penalize misses by assessing # of misses relative to the total # of objects. Default a 3% reduction for any # of misses. if (effectiveMissCount > 0) - flashlightValue *= calculateMissPenalty(effectiveMissCount); + flashlightValue *= 0.97 * Math.Pow(1 - Math.Pow((double)effectiveMissCount / totalHits, 0.775), Math.Pow(effectiveMissCount, .875)); // Combo scaling. if (Attributes.MaxCombo > 0) @@ -267,19 +269,17 @@ namespace osu.Game.Rulesets.Osu.Difficulty private double calculateMissPenalty(double missCount) { - double leniency = 2.0; + double leniency = 4.3; if (missCount > totalHits - leniency) return 0; - double missApprox = erfInvApprox((totalHits - leniency - missCount) / totalHits); - double fcApprox = erfInvApprox((totalHits - leniency) / totalHits); + double missApprox = SpecialFunctions.ErfInv((totalHits - leniency - missCount) / totalHits); + double fcApprox = SpecialFunctions.ErfInv((totalHits - leniency) / totalHits); - return Math.Pow(missApprox / fcApprox, 1.5); + return Math.Pow(missApprox / fcApprox, 3.5); } - private double logit(double x) => Math.Log(x / (1 - x)); - private double erfInvApprox(double x) => (Math.Sqrt(Math.PI) / 4) * logit((x + 1) / 2); private int totalHits => countGreat + countOk + countMeh + countMiss; private int totalSuccessfulHits => countGreat + countOk + countMeh; } diff --git a/osu.Game.Rulesets.Osu/osu.Game.Rulesets.Osu.csproj b/osu.Game.Rulesets.Osu/osu.Game.Rulesets.Osu.csproj index 98f1e69bd1..018bdb93df 100644 --- a/osu.Game.Rulesets.Osu/osu.Game.Rulesets.Osu.csproj +++ b/osu.Game.Rulesets.Osu/osu.Game.Rulesets.Osu.csproj @@ -15,4 +15,8 @@ + + + + \ No newline at end of file From 5640918c8c7c8af2e29feec4c7ad6d501619dafc Mon Sep 17 00:00:00 2001 From: apollo-dw <83023433+apollo-dw@users.noreply.github.com> Date: Sun, 26 Dec 2021 23:51:49 +0000 Subject: [PATCH 0006/2493] New miss penalty formula, using relevant difficult notes in each skill (targets diffspikes) --- .../Difficulty/OsuDifficultyAttributes.cs | 6 ++++++ .../Difficulty/OsuDifficultyCalculator.cs | 9 +++++++++ .../Difficulty/OsuPerformanceCalculator.cs | 16 ++++------------ .../Difficulty/Skills/OsuStrainSkill.cs | 7 +++++++ 4 files changed, 26 insertions(+), 12 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs index 4b2e54da17..6102f4a8b2 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs @@ -24,6 +24,12 @@ namespace osu.Game.Rulesets.Osu.Difficulty [JsonProperty("slider_factor")] public double SliderFactor { get; set; } + [JsonProperty("aim_difficult_strain_count")] + public int AimDifficultStrainCount { get; set; } + + [JsonProperty("speed_difficult_strain_count")] + public int SpeedDifficultStrainCount { get; set; } + [JsonProperty("approach_rate")] public double ApproachRate { get; set; } diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs index ed42f333c0..ac228b0a32 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs @@ -40,6 +40,13 @@ namespace osu.Game.Rulesets.Osu.Difficulty double sliderFactor = aimRating > 0 ? aimRatingNoSliders / aimRating : 1; + int aimDifficultyStrainCount = ((OsuStrainSkill)skills[0]).RelevantDifficultStrains(); + int speedDifficultyStrainCount = ((OsuStrainSkill)skills[2]).RelevantDifficultStrains(); + + // Total number of strains in a map can vary by clockrate, and this needs to be corrected for. + aimDifficultyStrainCount = (int)(aimDifficultyStrainCount * clockRate); + speedDifficultyStrainCount = (int)(aimDifficultyStrainCount * clockRate); + if (mods.Any(h => h is OsuModRelax)) speedRating = 0.0; @@ -78,6 +85,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty SpeedStrain = speedRating, FlashlightRating = flashlightRating, SliderFactor = sliderFactor, + AimDifficultStrainCount = aimDifficultyStrainCount, + SpeedDifficultStrainCount = speedDifficultyStrainCount, ApproachRate = preempt > 1200 ? (1800 - preempt) / 120 : (1200 - preempt) / 150 + 5, OverallDifficulty = (80 - hitWindowGreat) / 6, DrainRate = drainRate, diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index 90caa64512..3efb8951b3 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -103,7 +103,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty aimValue *= lengthBonus; if (effectiveMissCount > 0) - aimValue *= calculateMissPenalty(effectiveMissCount); + aimValue *= calculateMissPenalty(effectiveMissCount, Attributes.AimDifficultStrainCount); double approachRateFactor = 0.0; if (Attributes.ApproachRate > 10.33) @@ -148,7 +148,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty speedValue *= lengthBonus; if (effectiveMissCount > 0) - speedValue *= calculateMissPenalty(effectiveMissCount); + speedValue *= calculateMissPenalty(effectiveMissCount, Attributes.SpeedDifficultStrainCount); double approachRateFactor = 0.0; if (Attributes.ApproachRate > 10.33) @@ -267,17 +267,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty return Math.Max(countMiss, (int)Math.Floor(comboBasedMissCount)); } - private double calculateMissPenalty(double missCount) + private double calculateMissPenalty(double missCount, double strainCount) { - double leniency = 4.3; - - if (missCount > totalHits - leniency) - return 0; - - double missApprox = SpecialFunctions.ErfInv((totalHits - leniency - missCount) / totalHits); - double fcApprox = SpecialFunctions.ErfInv((totalHits - leniency) / totalHits); - - return Math.Pow(missApprox / fcApprox, 3.5); + return 0.95 / ((missCount / (3 * Math.Sqrt(strainCount))) + 1); } private int totalHits => countGreat + countOk + countMeh + countMiss; diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs index e47edc37cc..43e39482a7 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs @@ -57,5 +57,12 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills return difficulty * DifficultyMultiplier; } + + public int RelevantDifficultStrains() + { + List strains = GetCurrentStrainPeaks().OrderByDescending(d => d).ToList(); + + return strains.Count(s => s > strains[0] * 0.66); + } } } From e9589e57a662bc47297fec2429d7bfce1256db68 Mon Sep 17 00:00:00 2001 From: apollo-dw <83023433+apollo-dw@users.noreply.github.com> Date: Mon, 27 Dec 2021 02:23:03 +0000 Subject: [PATCH 0007/2493] Fix logical error --- osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs index ac228b0a32..87ab12248f 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs @@ -45,7 +45,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty // Total number of strains in a map can vary by clockrate, and this needs to be corrected for. aimDifficultyStrainCount = (int)(aimDifficultyStrainCount * clockRate); - speedDifficultyStrainCount = (int)(aimDifficultyStrainCount * clockRate); + speedDifficultyStrainCount = (int)(speedDifficultyStrainCount * clockRate); if (mods.Any(h => h is OsuModRelax)) speedRating = 0.0; From 8ce6e3c573104d55956c9a3c4cd3c8beffd41b09 Mon Sep 17 00:00:00 2001 From: apollo-dw <83023433+apollo-dw@users.noreply.github.com> Date: Wed, 29 Dec 2021 18:49:13 +0000 Subject: [PATCH 0008/2493] Remove mathnet --- osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs | 4 ---- osu.Game.Rulesets.Osu/osu.Game.Rulesets.Osu.csproj | 4 ---- 2 files changed, 8 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index d7d294df47..f6e5481feb 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -99,8 +99,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty if (effectiveMissCount > 0) aimValue *= 0.97 * Math.Pow(1 - Math.Pow((double)effectiveMissCount / totalHits, 0.775), effectiveMissCount); - aimValue *= getComboScalingFactor(); - double approachRateFactor = 0.0; if (Attributes.ApproachRate > 10.33) approachRateFactor = 0.3 * (Attributes.ApproachRate - 10.33); @@ -146,8 +144,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty if (effectiveMissCount > 0) speedValue *= 0.97 * Math.Pow(1 - Math.Pow((double)effectiveMissCount / totalHits, 0.775), Math.Pow(effectiveMissCount, .875)); - speedValue *= getComboScalingFactor(); - double approachRateFactor = 0.0; if (Attributes.ApproachRate > 10.33) approachRateFactor = 0.3 * (Attributes.ApproachRate - 10.33); diff --git a/osu.Game.Rulesets.Osu/osu.Game.Rulesets.Osu.csproj b/osu.Game.Rulesets.Osu/osu.Game.Rulesets.Osu.csproj index 018bdb93df..98f1e69bd1 100644 --- a/osu.Game.Rulesets.Osu/osu.Game.Rulesets.Osu.csproj +++ b/osu.Game.Rulesets.Osu/osu.Game.Rulesets.Osu.csproj @@ -15,8 +15,4 @@ - - - - \ No newline at end of file From 4f257d6987b1144912f561d5a3360593598be834 Mon Sep 17 00:00:00 2001 From: apollo-dw <83023433+apollo-dw@users.noreply.github.com> Date: Wed, 29 Dec 2021 18:59:17 +0000 Subject: [PATCH 0009/2493] Clean up unsuccessful merge --- .../Difficulty/OsuPerformanceCalculator.cs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index f6e5481feb..793f9e790f 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -95,9 +95,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty (totalHits > 2000 ? Math.Log10(totalHits / 2000.0) * 0.5 : 0.0); aimValue *= lengthBonus; - // Penalize misses by assessing # of misses relative to the total # of objects. Default a 3% reduction for any # of misses. if (effectiveMissCount > 0) - aimValue *= 0.97 * Math.Pow(1 - Math.Pow((double)effectiveMissCount / totalHits, 0.775), effectiveMissCount); + aimValue *= calculateMissPenalty(effectiveMissCount, Attributes.AimDifficultStrainCount); double approachRateFactor = 0.0; if (Attributes.ApproachRate > 10.33) @@ -140,9 +139,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty (totalHits > 2000 ? Math.Log10(totalHits / 2000.0) * 0.5 : 0.0); speedValue *= lengthBonus; - // Penalize misses by assessing # of misses relative to the total # of objects. Default a 3% reduction for any # of misses. if (effectiveMissCount > 0) - speedValue *= 0.97 * Math.Pow(1 - Math.Pow((double)effectiveMissCount / totalHits, 0.775), Math.Pow(effectiveMissCount, .875)); + speedValue *= calculateMissPenalty(effectiveMissCount, Attributes.SpeedDifficultStrainCount); double approachRateFactor = 0.0; if (Attributes.ApproachRate > 10.33) @@ -259,6 +257,12 @@ namespace osu.Game.Rulesets.Osu.Difficulty } private double getComboScalingFactor() => Attributes.MaxCombo <= 0 ? 1.0 : Math.Min(Math.Pow(scoreMaxCombo, 0.8) / Math.Pow(Attributes.MaxCombo, 0.8), 1.0); + + private double calculateMissPenalty(double missCount, double strainCount) + { + return 0.95 / ((missCount / (3 * Math.Sqrt(strainCount))) + 1); + } + private int totalHits => countGreat + countOk + countMeh + countMiss; private int totalSuccessfulHits => countGreat + countOk + countMeh; } From fd1028f3bb5c0f19ac2154974fafa528ea6e52e9 Mon Sep 17 00:00:00 2001 From: apollo-dw <83023433+apollo-dw@users.noreply.github.com> Date: Wed, 29 Dec 2021 23:49:07 +0000 Subject: [PATCH 0010/2493] Use clockrate in the difficult strain count method --- .../Difficulty/OsuDifficultyCalculator.cs | 8 ++------ .../Difficulty/Skills/OsuStrainSkill.cs | 9 +++++++-- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs index 44ba6c6a58..27d97e9d75 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs @@ -40,12 +40,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty double sliderFactor = aimRating > 0 ? aimRatingNoSliders / aimRating : 1; - int aimDifficultyStrainCount = ((OsuStrainSkill)skills[0]).RelevantDifficultStrains(); - int speedDifficultyStrainCount = ((OsuStrainSkill)skills[2]).RelevantDifficultStrains(); - - // Total number of strains in a map can vary by clockrate, and this needs to be corrected for. - aimDifficultyStrainCount = (int)(aimDifficultyStrainCount * clockRate); - speedDifficultyStrainCount = (int)(speedDifficultyStrainCount * clockRate); + int aimDifficultyStrainCount = ((OsuStrainSkill)skills[0]).CountDifficultStrains(clockRate); + int speedDifficultyStrainCount = ((OsuStrainSkill)skills[2]).CountDifficultStrains(clockRate); if (mods.Any(h => h is OsuModRelax)) speedRating = 0.0; diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs index 43e39482a7..fd751727a9 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs @@ -58,11 +58,16 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills return difficulty * DifficultyMultiplier; } - public int RelevantDifficultStrains() + /// + /// Returns the number of difficult strains. + /// A strain is considered difficult if it's higher than 66% of the highest strain. + /// + public int CountDifficultStrains(double clockRate) { List strains = GetCurrentStrainPeaks().OrderByDescending(d => d).ToList(); - return strains.Count(s => s > strains[0] * 0.66); + // Total number of strains in a map can vary by clockrate, and this needs to be corrected for. + return (int)(strains.Count(s => s > strains[0] * 0.66) * clockRate); } } } From d2b815b745b1cfaef241488c436b26fede000571 Mon Sep 17 00:00:00 2001 From: apollo-dw <83023433+apollo-dw@users.noreply.github.com> Date: Sun, 2 Jan 2022 19:20:20 +0000 Subject: [PATCH 0011/2493] Add miss penalty comment --- .../Difficulty/OsuPerformanceCalculator.cs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index 793f9e790f..d8fe85d645 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -256,13 +256,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty return Math.Max(countMiss, (int)Math.Floor(comboBasedMissCount)); } + // Miss penalty assumes that a player will miss on the relatively hard parts of a map, not the easy parts, hence the strain count. + private double calculateMissPenalty(double missCount, double strainCount) => 0.95 / ((missCount / (3 * Math.Sqrt(strainCount))) + 1); + private double getComboScalingFactor() => Attributes.MaxCombo <= 0 ? 1.0 : Math.Min(Math.Pow(scoreMaxCombo, 0.8) / Math.Pow(Attributes.MaxCombo, 0.8), 1.0); - - private double calculateMissPenalty(double missCount, double strainCount) - { - return 0.95 / ((missCount / (3 * Math.Sqrt(strainCount))) + 1); - } - private int totalHits => countGreat + countOk + countMeh + countMiss; private int totalSuccessfulHits => countGreat + countOk + countMeh; } From 75be4e83d6a44adc7016c150b5cbd1c74d1caf34 Mon Sep 17 00:00:00 2001 From: Luminiscental Date: Mon, 3 Jan 2022 22:22:32 +0000 Subject: [PATCH 0012/2493] Remove abusable 0.66 threshold by averaging --- .../Difficulty/Skills/OsuStrainSkill.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs index fd751727a9..1513befad5 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs @@ -59,15 +59,15 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills } /// - /// Returns the number of difficult strains. - /// A strain is considered difficult if it's higher than 66% of the highest strain. + /// Returns the number of strains above a threshold averaged as the threshold varies. + /// The result is scaled by clock rate as it affects the total number of strains. /// public int CountDifficultStrains(double clockRate) { - List strains = GetCurrentStrainPeaks().OrderByDescending(d => d).ToList(); - - // Total number of strains in a map can vary by clockrate, and this needs to be corrected for. - return (int)(strains.Count(s => s > strains[0] * 0.66) * clockRate); + List strains = GetCurrentStrainPeaks().ToList(); + // This is the average value of strains.Count(s => s > p * strains.Max()) for p between 0 and 1. + double realtimeCount = strains.Sum() / strains.Max(); + return (int)(clockRate * realtimeCount); } } } From 132079004ca5e6df33a2bc646a52f3036edff85e Mon Sep 17 00:00:00 2001 From: Luminiscental Date: Tue, 4 Jan 2022 12:30:05 +0000 Subject: [PATCH 0013/2493] Remove unnecessary truncation --- osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs | 4 ++-- osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs | 4 ++-- osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs index 51842f65d5..7ab4232a19 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs @@ -25,10 +25,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty public double SliderFactor { get; set; } [JsonProperty("aim_difficult_strain_count")] - public int AimDifficultStrainCount { get; set; } + public double AimDifficultStrainCount { get; set; } [JsonProperty("speed_difficult_strain_count")] - public int SpeedDifficultStrainCount { get; set; } + public double SpeedDifficultStrainCount { get; set; } [JsonProperty("approach_rate")] public double ApproachRate { get; set; } diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs index 27d97e9d75..3c039c9b7e 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs @@ -40,8 +40,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty double sliderFactor = aimRating > 0 ? aimRatingNoSliders / aimRating : 1; - int aimDifficultyStrainCount = ((OsuStrainSkill)skills[0]).CountDifficultStrains(clockRate); - int speedDifficultyStrainCount = ((OsuStrainSkill)skills[2]).CountDifficultStrains(clockRate); + double aimDifficultyStrainCount = ((OsuStrainSkill)skills[0]).CountDifficultStrains(clockRate); + double speedDifficultyStrainCount = ((OsuStrainSkill)skills[2]).CountDifficultStrains(clockRate); if (mods.Any(h => h is OsuModRelax)) speedRating = 0.0; diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs index 1513befad5..46dc9c683b 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs @@ -62,12 +62,12 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills /// Returns the number of strains above a threshold averaged as the threshold varies. /// The result is scaled by clock rate as it affects the total number of strains. /// - public int CountDifficultStrains(double clockRate) + public double CountDifficultStrains(double clockRate) { List strains = GetCurrentStrainPeaks().ToList(); // This is the average value of strains.Count(s => s > p * strains.Max()) for p between 0 and 1. double realtimeCount = strains.Sum() / strains.Max(); - return (int)(clockRate * realtimeCount); + return clockRate * realtimeCount; } } } From 443640a48c8bed53510947240e8337c9350d8d6d Mon Sep 17 00:00:00 2001 From: apollo <83023433+apollo-dw@users.noreply.github.com> Date: Tue, 4 Jan 2022 16:39:30 +0000 Subject: [PATCH 0014/2493] Revert "Remove abusable 0.66 threshold by averaging" --- .../Difficulty/OsuDifficultyAttributes.cs | 4 ++-- .../Difficulty/OsuDifficultyCalculator.cs | 4 ++-- .../Difficulty/Skills/OsuStrainSkill.cs | 14 +++++++------- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs index 7ab4232a19..51842f65d5 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs @@ -25,10 +25,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty public double SliderFactor { get; set; } [JsonProperty("aim_difficult_strain_count")] - public double AimDifficultStrainCount { get; set; } + public int AimDifficultStrainCount { get; set; } [JsonProperty("speed_difficult_strain_count")] - public double SpeedDifficultStrainCount { get; set; } + public int SpeedDifficultStrainCount { get; set; } [JsonProperty("approach_rate")] public double ApproachRate { get; set; } diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs index 3c039c9b7e..27d97e9d75 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs @@ -40,8 +40,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty double sliderFactor = aimRating > 0 ? aimRatingNoSliders / aimRating : 1; - double aimDifficultyStrainCount = ((OsuStrainSkill)skills[0]).CountDifficultStrains(clockRate); - double speedDifficultyStrainCount = ((OsuStrainSkill)skills[2]).CountDifficultStrains(clockRate); + int aimDifficultyStrainCount = ((OsuStrainSkill)skills[0]).CountDifficultStrains(clockRate); + int speedDifficultyStrainCount = ((OsuStrainSkill)skills[2]).CountDifficultStrains(clockRate); if (mods.Any(h => h is OsuModRelax)) speedRating = 0.0; diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs index 46dc9c683b..fd751727a9 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs @@ -59,15 +59,15 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills } /// - /// Returns the number of strains above a threshold averaged as the threshold varies. - /// The result is scaled by clock rate as it affects the total number of strains. + /// Returns the number of difficult strains. + /// A strain is considered difficult if it's higher than 66% of the highest strain. /// - public double CountDifficultStrains(double clockRate) + public int CountDifficultStrains(double clockRate) { - List strains = GetCurrentStrainPeaks().ToList(); - // This is the average value of strains.Count(s => s > p * strains.Max()) for p between 0 and 1. - double realtimeCount = strains.Sum() / strains.Max(); - return clockRate * realtimeCount; + List strains = GetCurrentStrainPeaks().OrderByDescending(d => d).ToList(); + + // Total number of strains in a map can vary by clockrate, and this needs to be corrected for. + return (int)(strains.Count(s => s > strains[0] * 0.66) * clockRate); } } } From dcb969316dd5a7e2bd1b06ebd1450a0ab56ef831 Mon Sep 17 00:00:00 2001 From: apollo-dw <83023433+apollo-dw@users.noreply.github.com> Date: Tue, 4 Jan 2022 17:33:23 +0000 Subject: [PATCH 0015/2493] Weight difficult strain count against the top strain --- osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs index 46dc9c683b..3e0fccb6c4 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs @@ -59,14 +59,15 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills } /// - /// Returns the number of strains above a threshold averaged as the threshold varies. + /// Returns the number of strains weighted against the top strain. /// The result is scaled by clock rate as it affects the total number of strains. /// public double CountDifficultStrains(double clockRate) { List strains = GetCurrentStrainPeaks().ToList(); - // This is the average value of strains.Count(s => s > p * strains.Max()) for p between 0 and 1. - double realtimeCount = strains.Sum() / strains.Max(); + double topStrain = strains.Max(); + + double realtimeCount = strains.Sum(s => Math.Pow(s / topStrain, 4)); return clockRate * realtimeCount; } } From 400abc147b1f3742061125d91e80a62f5e1333f7 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 6 Jan 2022 16:28:04 +0900 Subject: [PATCH 0016/2493] Add attribute ids to mapping functions --- osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs | 4 ++++ osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs | 2 ++ 2 files changed, 6 insertions(+) diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs index 7ab4232a19..7041acc16c 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs @@ -60,6 +60,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty yield return (ATTRIB_ID_FLASHLIGHT, FlashlightDifficulty); yield return (ATTRIB_ID_SLIDER_FACTOR, SliderFactor); + yield return (ATTRIB_ID_AIM_DIFFICULT_STRAIN_COUNT, AimDifficultStrainCount); + yield return (ATTRIB_ID_SPEED_DIFFICULT_STRAIN_COUNT, SpeedDifficultStrainCount); } public override void FromDatabaseAttributes(IReadOnlyDictionary values) @@ -74,6 +76,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty StarRating = values[ATTRIB_ID_DIFFICULTY]; FlashlightDifficulty = values.GetValueOrDefault(ATTRIB_ID_FLASHLIGHT); SliderFactor = values[ATTRIB_ID_SLIDER_FACTOR]; + AimDifficultStrainCount = values[ATTRIB_ID_AIM_DIFFICULT_STRAIN_COUNT]; + SpeedDifficultStrainCount = values[ATTRIB_ID_SPEED_DIFFICULT_STRAIN_COUNT]; } #region Newtonsoft.Json implicit ShouldSerialize() methods diff --git a/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs b/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs index 991b567f57..803a5bdac7 100644 --- a/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs +++ b/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs @@ -24,6 +24,8 @@ namespace osu.Game.Rulesets.Difficulty protected const int ATTRIB_ID_SCORE_MULTIPLIER = 15; protected const int ATTRIB_ID_FLASHLIGHT = 17; protected const int ATTRIB_ID_SLIDER_FACTOR = 19; + protected const int ATTRIB_ID_AIM_DIFFICULT_STRAIN_COUNT = 21; + protected const int ATTRIB_ID_SPEED_DIFFICULT_STRAIN_COUNT = 23; /// /// The mods which were applied to the beatmap. From 598946737f07c0f2a3ac7523666a868722906db1 Mon Sep 17 00:00:00 2001 From: apollo-dw <83023433+apollo-dw@users.noreply.github.com> Date: Wed, 12 Jan 2022 14:38:53 +0000 Subject: [PATCH 0017/2493] Reword comment and rename argument --- .../Difficulty/OsuPerformanceCalculator.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index d8fe85d645..52658dfe38 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -256,9 +256,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty return Math.Max(countMiss, (int)Math.Floor(comboBasedMissCount)); } - // Miss penalty assumes that a player will miss on the relatively hard parts of a map, not the easy parts, hence the strain count. - private double calculateMissPenalty(double missCount, double strainCount) => 0.95 / ((missCount / (3 * Math.Sqrt(strainCount))) + 1); - + // Miss penalty assumes that a player will miss on the hardest parts of a map, + // so we use the amount of relatively difficult sections to adjust miss penalty + // to make it more punishing on maps with lower amount of hard sections. + private double calculateMissPenalty(double missCount, double difficultStrainCount) => 0.95 / ((missCount / (3 * Math.Sqrt(difficultStrainCount))) + 1); private double getComboScalingFactor() => Attributes.MaxCombo <= 0 ? 1.0 : Math.Min(Math.Pow(scoreMaxCombo, 0.8) / Math.Pow(Attributes.MaxCombo, 0.8), 1.0); private int totalHits => countGreat + countOk + countMeh + countMiss; private int totalSuccessfulHits => countGreat + countOk + countMeh; From da31ca17e7c12a2922ab83657df4d8d9b6826bbd Mon Sep 17 00:00:00 2001 From: apollo-dw <83023433+apollo-dw@users.noreply.github.com> Date: Mon, 14 Feb 2022 01:53:03 +0000 Subject: [PATCH 0018/2493] Use note strains instead of sectional strains --- osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs | 2 ++ osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs | 7 ++++--- osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs | 6 +++++- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs index a6301aed6d..3486db04af 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs @@ -159,6 +159,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills currentStrain *= strainDecay(current.DeltaTime); currentStrain += strainValueOf(current) * skillMultiplier; + objectStrains.Add(currentStrain); + return currentStrain; } } diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs index 3e0fccb6c4..94e2c9d774 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs @@ -28,6 +28,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills /// protected virtual double DifficultyMultiplier => 1.06; + protected List objectStrains = new List(); + protected OsuStrainSkill(Mod[] mods) : base(mods) { @@ -64,10 +66,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills /// public double CountDifficultStrains(double clockRate) { - List strains = GetCurrentStrainPeaks().ToList(); - double topStrain = strains.Max(); + double topStrain = objectStrains.Max(); - double realtimeCount = strains.Sum(s => Math.Pow(s / topStrain, 4)); + double realtimeCount = objectStrains.Sum(s => Math.Pow(s / topStrain, 4)); return clockRate * realtimeCount; } } diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs index 06d1ef7346..108edc6f2d 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs @@ -173,7 +173,11 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills currentRhythm = calculateRhythmBonus(current); - return currentStrain * currentRhythm; + double totalStrain = currentStrain * currentRhythm; + + objectStrains.Add(totalStrain); + + return totalStrain; } } } From 94a46ab640b36c3ca58f03eaf2644ab51c3abd51 Mon Sep 17 00:00:00 2001 From: apollo-dw <83023433+apollo-dw@users.noreply.github.com> Date: Mon, 14 Feb 2022 02:02:46 +0000 Subject: [PATCH 0019/2493] Rescale miss penalty for note strains --- osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index cf2116cc5d..cf1af18d39 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -259,7 +259,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty // Miss penalty assumes that a player will miss on the hardest parts of a map, // so we use the amount of relatively difficult sections to adjust miss penalty // to make it more punishing on maps with lower amount of hard sections. - private double calculateMissPenalty(double missCount, double difficultStrainCount) => 0.95 / ((missCount / (3 * Math.Sqrt(difficultStrainCount))) + 1); + private double calculateMissPenalty(double missCount, double difficultStrainCount) => 0.94 / ((missCount / (2 * Math.Sqrt(difficultStrainCount))) + 1); private double getComboScalingFactor() => Attributes.MaxCombo <= 0 ? 1.0 : Math.Min(Math.Pow(scoreMaxCombo, 0.8) / Math.Pow(Attributes.MaxCombo, 0.8), 1.0); private int totalHits => countGreat + countOk + countMeh + countMiss; private int totalSuccessfulHits => countGreat + countOk + countMeh; From c18df86720244a7d5bba22bc502afd4f959c9082 Mon Sep 17 00:00:00 2001 From: apollo-dw <83023433+apollo-dw@users.noreply.github.com> Date: Sat, 19 Feb 2022 15:33:28 +0000 Subject: [PATCH 0020/2493] Remove clockrate factor --- osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs | 4 ++-- osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs | 5 ++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs index 3c039c9b7e..788b515d7f 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs @@ -40,8 +40,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty double sliderFactor = aimRating > 0 ? aimRatingNoSliders / aimRating : 1; - double aimDifficultyStrainCount = ((OsuStrainSkill)skills[0]).CountDifficultStrains(clockRate); - double speedDifficultyStrainCount = ((OsuStrainSkill)skills[2]).CountDifficultStrains(clockRate); + double aimDifficultyStrainCount = ((OsuStrainSkill)skills[0]).CountDifficultStrains(); + double speedDifficultyStrainCount = ((OsuStrainSkill)skills[2]).CountDifficultStrains(); if (mods.Any(h => h is OsuModRelax)) speedRating = 0.0; diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs index 94e2c9d774..1124c4466f 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs @@ -64,12 +64,11 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills /// Returns the number of strains weighted against the top strain. /// The result is scaled by clock rate as it affects the total number of strains. /// - public double CountDifficultStrains(double clockRate) + public double CountDifficultStrains() { double topStrain = objectStrains.Max(); - double realtimeCount = objectStrains.Sum(s => Math.Pow(s / topStrain, 4)); - return clockRate * realtimeCount; + return objectStrains.Sum(s => Math.Pow(s / topStrain, 4)); } } } From 2f335a76dca77b0304f5ff21ed7ca2c8e1130757 Mon Sep 17 00:00:00 2001 From: apollo-dw <83023433+apollo-dw@users.noreply.github.com> Date: Thu, 17 Mar 2022 22:08:56 +0000 Subject: [PATCH 0021/2493] Switch to using osuAttributes --- osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index 48b4f53a3e..964bd73e81 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -92,7 +92,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty aimValue *= lengthBonus; if (effectiveMissCount > 0) - aimValue *= calculateMissPenalty(effectiveMissCount, Attributes.AimDifficultStrainCount); + aimValue *= calculateMissPenalty(effectiveMissCount, attributes.AimDifficultStrainCount); double approachRateFactor = 0.0; if (attributes.ApproachRate > 10.33) @@ -136,7 +136,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty speedValue *= lengthBonus; if (effectiveMissCount > 0) - speedValue *= calculateMissPenalty(effectiveMissCount, Attributes.SpeedDifficultStrainCount); + speedValue *= calculateMissPenalty(effectiveMissCount, attributes.SpeedDifficultStrainCount); double approachRateFactor = 0.0; if (attributes.ApproachRate > 10.33) From c907751a5cf4e53c7910212e9ff0643a3f9c36f5 Mon Sep 17 00:00:00 2001 From: mk56-spn Date: Mon, 16 Jan 2023 16:57:18 +0100 Subject: [PATCH 0022/2493] Set up test scene and basic LeaderBoardScoreV2.cs shape --- .../SongSelect/TestSceneLeaderboardScoreV2.cs | 26 +++++++++++ .../Online/Leaderboards/LeaderBoardScoreV2.cs | 43 +++++++++++++++++++ 2 files changed, 69 insertions(+) create mode 100644 osu.Game.Tests/Visual/SongSelect/TestSceneLeaderboardScoreV2.cs create mode 100644 osu.Game/Online/Leaderboards/LeaderBoardScoreV2.cs diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneLeaderboardScoreV2.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneLeaderboardScoreV2.cs new file mode 100644 index 0000000000..2c631e943d --- /dev/null +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneLeaderboardScoreV2.cs @@ -0,0 +1,26 @@ +// 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.Game.Online.Leaderboards; + +namespace osu.Game.Tests.Visual.SongSelect +{ + public partial class TestSceneLeaderboardScoreV2 : OsuTestScene + { + [BackgroundDependencyLoader] + private void load() + { + Child = new Container + { + Width = 900, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + AutoSizeAxes = Axes.Y, + Child = new LeaderBoardScoreV2() + }; + } + } +} diff --git a/osu.Game/Online/Leaderboards/LeaderBoardScoreV2.cs b/osu.Game/Online/Leaderboards/LeaderBoardScoreV2.cs new file mode 100644 index 0000000000..07184b8474 --- /dev/null +++ b/osu.Game/Online/Leaderboards/LeaderBoardScoreV2.cs @@ -0,0 +1,43 @@ +// 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.Shapes; +using osu.Game.Graphics.Containers; +using osuTK; + +namespace osu.Game.Online.Leaderboards +{ + public partial class LeaderBoardScoreV2 : OsuClickableContainer + { + private const int HEIGHT = 60; + private const int corner_radius = 10; + + private static readonly Vector2 shear = new Vector2(0.15f, 0); + + private Container content = null!; + + [BackgroundDependencyLoader] + private void load() + { + Shear = shear; + RelativeSizeAxes = Axes.X; + Height = HEIGHT; + Child = content = new Container + { + Masking = true, + CornerRadius = corner_radius, + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + } + } + }; + } + } +} From b7584b4a02471c5d8894f30189da61075b8e46ec Mon Sep 17 00:00:00 2001 From: mk56-spn Date: Mon, 16 Jan 2023 17:15:37 +0100 Subject: [PATCH 0023/2493] Add grid container with sections, add background colours and hover logic --- .../SongSelect/TestSceneLeaderboardScoreV2.cs | 10 ++- .../Online/Leaderboards/LeaderBoardScoreV2.cs | 78 ++++++++++++++++++- 2 files changed, 85 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneLeaderboardScoreV2.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneLeaderboardScoreV2.cs index 2c631e943d..377bc79605 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneLeaderboardScoreV2.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneLeaderboardScoreV2.cs @@ -5,6 +5,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Online.Leaderboards; +using osuTK; namespace osu.Game.Tests.Visual.SongSelect { @@ -13,13 +14,18 @@ namespace osu.Game.Tests.Visual.SongSelect [BackgroundDependencyLoader] private void load() { - Child = new Container + Child = new FillFlowContainer { Width = 900, Anchor = Anchor.Centre, Origin = Anchor.Centre, + Spacing = new Vector2(0, 10), AutoSizeAxes = Axes.Y, - Child = new LeaderBoardScoreV2() + Children = new Drawable[] + { + new LeaderBoardScoreV2(), + new LeaderBoardScoreV2(true) + } }; } } diff --git a/osu.Game/Online/Leaderboards/LeaderBoardScoreV2.cs b/osu.Game/Online/Leaderboards/LeaderBoardScoreV2.cs index 07184b8474..7a56657365 100644 --- a/osu.Game/Online/Leaderboards/LeaderBoardScoreV2.cs +++ b/osu.Game/Online/Leaderboards/LeaderBoardScoreV2.cs @@ -5,23 +5,43 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Framework.Input.Events; using osu.Game.Graphics.Containers; +using osu.Game.Overlays; using osuTK; namespace osu.Game.Online.Leaderboards { public partial class LeaderBoardScoreV2 : OsuClickableContainer { + private readonly bool isPersonalBest; private const int HEIGHT = 60; private const int corner_radius = 10; + private const int transition_duration = 200; + + private Colour4 foregroundColour; + private Colour4 backgroundColour; private static readonly Vector2 shear = new Vector2(0.15f, 0); + [Cached] + private OverlayColourProvider colourProvider { get; set; } = new OverlayColourProvider(OverlayColourScheme.Aquamarine); + private Container content = null!; + private Box background = null!; + private Box foreground = null!; + + public LeaderBoardScoreV2(bool isPersonalBest = false) + { + this.isPersonalBest = isPersonalBest; + } [BackgroundDependencyLoader] private void load() { + foregroundColour = isPersonalBest ? colourProvider.Background1 : colourProvider.Background5; + backgroundColour = isPersonalBest ? colourProvider.Background2 : colourProvider.Background4; + Shear = shear; RelativeSizeAxes = Axes.X; Height = HEIGHT; @@ -32,12 +52,68 @@ namespace osu.Game.Online.Leaderboards RelativeSizeAxes = Axes.Both, Children = new Drawable[] { - new Box + background = new Box { RelativeSizeAxes = Axes.Both, + Colour = backgroundColour + }, + new GridContainer + { + RelativeSizeAxes = Axes.Both, + ColumnDimensions = new[] + { + new Dimension(GridSizeMode.Absolute, 65), + new Dimension(), + new Dimension(GridSizeMode.Absolute, 176) + }, + Content = new[] + { + new[] + { + Empty(), + createCentreContent(), + Empty(), + } + } } } }; } + + private Container createCentreContent() => + new Container + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Masking = true, + CornerRadius = corner_radius, + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + foreground = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = foregroundColour + } + } + }; + + protected override bool OnHover(HoverEvent e) + { + updateState(); + return base.OnHover(e); + } + + protected override void OnHoverLost(HoverLostEvent e) + { + updateState(); + base.OnHoverLost(e); + } + + private void updateState() + { + foreground.FadeColour(IsHovered ? foregroundColour.Lighten(0.2f) : foregroundColour, transition_duration, Easing.OutQuint); + background.FadeColour(IsHovered ? backgroundColour.Lighten(0.2f) : backgroundColour, transition_duration, Easing.OutQuint); + } } } From 9ea151539670ad5bb8175026c8fcaeba6059e695 Mon Sep 17 00:00:00 2001 From: mk56-spn Date: Mon, 16 Jan 2023 17:35:27 +0100 Subject: [PATCH 0024/2493] setup up rank and score logic flow --- .../SongSelect/TestSceneLeaderboardScoreV2.cs | 60 ++++++++++++++++++- .../Online/Leaderboards/LeaderBoardScoreV2.cs | 45 +++++++++++++- 2 files changed, 100 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneLeaderboardScoreV2.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneLeaderboardScoreV2.cs index 377bc79605..13eaf5417d 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneLeaderboardScoreV2.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneLeaderboardScoreV2.cs @@ -4,7 +4,13 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Leaderboards; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.Osu.Mods; +using osu.Game.Scoring; +using osu.Game.Users; using osuTK; namespace osu.Game.Tests.Visual.SongSelect @@ -14,6 +20,56 @@ namespace osu.Game.Tests.Visual.SongSelect [BackgroundDependencyLoader] private void load() { + var scores = new[] + { + new ScoreInfo + { + Position = 999, + Rank = ScoreRank.XH, + Accuracy = 1, + MaxCombo = 244, + TotalScore = 1707827, + Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), }, + Ruleset = new OsuRuleset().RulesetInfo, + User = new APIUser + { + Id = 6602580, + Username = @"waaiiru", + CountryCode = CountryCode.ES, + }, + }, + new ScoreInfo + { + Position = 110000, + Rank = ScoreRank.X, + Accuracy = 1, + MaxCombo = 244, + TotalScore = 1707827, + Ruleset = new OsuRuleset().RulesetInfo, + User = new APIUser + { + Id = 4608074, + Username = @"Skycries", + CountryCode = CountryCode.BR, + }, + }, + new ScoreInfo + { + Position = 22333, + Rank = ScoreRank.S, + Accuracy = 1, + MaxCombo = 244, + TotalScore = 1707827, + Ruleset = new OsuRuleset().RulesetInfo, + User = new APIUser + { + Id = 1541390, + Username = @"Toukai", + CountryCode = CountryCode.CA, + }, + } + }; + Child = new FillFlowContainer { Width = 900, @@ -23,8 +79,8 @@ namespace osu.Game.Tests.Visual.SongSelect AutoSizeAxes = Axes.Y, Children = new Drawable[] { - new LeaderBoardScoreV2(), - new LeaderBoardScoreV2(true) + new LeaderBoardScoreV2(scores[0], 1), + new LeaderBoardScoreV2(scores[2], 3, true) } }; } diff --git a/osu.Game/Online/Leaderboards/LeaderBoardScoreV2.cs b/osu.Game/Online/Leaderboards/LeaderBoardScoreV2.cs index 7a56657365..d526172861 100644 --- a/osu.Game/Online/Leaderboards/LeaderBoardScoreV2.cs +++ b/osu.Game/Online/Leaderboards/LeaderBoardScoreV2.cs @@ -4,21 +4,32 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; +using osu.Framework.Localisation; +using osu.Game.Graphics; using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Sprites; using osu.Game.Overlays; +using osu.Game.Scoring; +using osu.Game.Utils; using osuTK; namespace osu.Game.Online.Leaderboards { public partial class LeaderBoardScoreV2 : OsuClickableContainer { - private readonly bool isPersonalBest; + private readonly ScoreInfo score; + private const int HEIGHT = 60; private const int corner_radius = 10; private const int transition_duration = 200; + private readonly int? rank; + + private readonly bool isPersonalBest; + private Colour4 foregroundColour; private Colour4 backgroundColour; @@ -31,8 +42,10 @@ namespace osu.Game.Online.Leaderboards private Box background = null!; private Box foreground = null!; - public LeaderBoardScoreV2(bool isPersonalBest = false) + public LeaderBoardScoreV2(ScoreInfo score, int? rank, bool isPersonalBest = false) { + this.score = score; + this.rank = rank; this.isPersonalBest = isPersonalBest; } @@ -70,7 +83,14 @@ namespace osu.Game.Online.Leaderboards { new[] { - Empty(), + new RankLabel(rank) + { + Shear = -shear, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Y, + Width = 35 + }, createCentreContent(), Empty(), } @@ -115,5 +135,24 @@ namespace osu.Game.Online.Leaderboards foreground.FadeColour(IsHovered ? foregroundColour.Lighten(0.2f) : foregroundColour, transition_duration, Easing.OutQuint); background.FadeColour(IsHovered ? backgroundColour.Lighten(0.2f) : backgroundColour, transition_duration, Easing.OutQuint); } + + private partial class RankLabel : Container, IHasTooltip + { + public RankLabel(int? rank) + { + if (rank >= 1000) + TooltipText = $"#{rank:N0}"; + + Child = new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Font = OsuFont.GetFont(size: 20, weight: FontWeight.SemiBold, italics: true), + Text = rank == null ? "-" : rank.Value.FormatRank().Insert(0, "#") + }; + } + + public LocalisableString TooltipText { get; } + } } } From 9178e3fd7dba9d8a2fde74ecbd8c691b9f646f47 Mon Sep 17 00:00:00 2001 From: mk56-spn Date: Mon, 16 Jan 2023 18:03:38 +0100 Subject: [PATCH 0025/2493] Add right side content --- .../Online/Leaderboards/LeaderBoardScoreV2.cs | 128 +++++++++++++++++- osu.Game/Rulesets/UI/ModSwitchTiny.cs | 16 +-- 2 files changed, 132 insertions(+), 12 deletions(-) diff --git a/osu.Game/Online/Leaderboards/LeaderBoardScoreV2.cs b/osu.Game/Online/Leaderboards/LeaderBoardScoreV2.cs index d526172861..ed8536a2bf 100644 --- a/osu.Game/Online/Leaderboards/LeaderBoardScoreV2.cs +++ b/osu.Game/Online/Leaderboards/LeaderBoardScoreV2.cs @@ -1,24 +1,34 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Collections.Generic; +using System.Linq; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; using osu.Framework.Localisation; +using osu.Framework.Platform; +using osu.Game.Database; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; using osu.Game.Overlays; +using osu.Game.Resources.Localisation.Web; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.UI; using osu.Game.Scoring; +using osu.Game.Screens.Select; using osu.Game.Utils; using osuTK; namespace osu.Game.Online.Leaderboards { - public partial class LeaderBoardScoreV2 : OsuClickableContainer + public partial class LeaderBoardScoreV2 : OsuClickableContainer, IHasContextMenu { private readonly ScoreInfo score; @@ -38,10 +48,25 @@ namespace osu.Game.Online.Leaderboards [Cached] private OverlayColourProvider colourProvider { get; set; } = new OverlayColourProvider(OverlayColourScheme.Aquamarine); + [Resolved] + private SongSelect? songSelect { get; set; } + + [Resolved] + private IDialogOverlay? dialogOverlay { get; set; } + + [Resolved] + private Storage storage { get; set; } = null!; + private Container content = null!; private Box background = null!; private Box foreground = null!; + protected Container RankContainer { get; private set; } = null!; + private FillFlowContainer modsContainer = null!; + + private OsuSpriteText scoreText = null!; + private Drawable scoreRank = null!; + public LeaderBoardScoreV2(ScoreInfo score, int? rank, bool isPersonalBest = false) { this.score = score; @@ -50,7 +75,7 @@ namespace osu.Game.Online.Leaderboards } [BackgroundDependencyLoader] - private void load() + private void load(ScoreManager scoreManager) { foregroundColour = isPersonalBest ? colourProvider.Background1 : colourProvider.Background5; backgroundColour = isPersonalBest ? colourProvider.Background2 : colourProvider.Background4; @@ -81,7 +106,7 @@ namespace osu.Game.Online.Leaderboards }, Content = new[] { - new[] + new Drawable[] { new RankLabel(rank) { @@ -92,12 +117,15 @@ namespace osu.Game.Online.Leaderboards Width = 35 }, createCentreContent(), - Empty(), + createRightSideContent(scoreManager) } } } } }; + + modsContainer.Spacing = new Vector2(modsContainer.Children.Count > 5 ? -20 : 2, 0); + modsContainer.Padding = new MarginPadding { Top = modsContainer.Children.Count > 0 ? 4 : 0 }; } private Container createCentreContent() => @@ -118,6 +146,63 @@ namespace osu.Game.Online.Leaderboards } }; + private FillFlowContainer createRightSideContent(ScoreManager scoreManager) => + new FillFlowContainer + { + Padding = new MarginPadding { Left = 11, Right = 15 }, + Y = -5, + AutoSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.X, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Direction = FillDirection.Vertical, + Spacing = new Vector2(13, 0f), + Children = new Drawable[] + { + new Container + { + AutoSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.X, + Children = new Drawable[] + { + scoreText = new OsuSpriteText + { + Shear = -shear, + Current = scoreManager.GetBindableTotalScoreString(score), + + //Does not match figma, adjusted to allow 8 digits to fit comfortably + Font = OsuFont.GetFont(size: 28, weight: FontWeight.SemiBold, fixedWidth: false), + }, + RankContainer = new Container + { + BypassAutoSizeAxes = Axes.Both, + Y = 2, + Shear = -shear, + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + AutoSizeAxes = Axes.Both, + Children = new[] + { + scoreRank = new UpdateableRank(score.Rank) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(32) + } + } + } + } + }, + modsContainer = new FillFlowContainer + { + Shear = -shear, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + ChildrenEnumerable = score.Mods.Select(mod => new ColouredModSwitchTiny(mod) { Scale = new Vector2(0.375f) }) + } + } + }; + protected override bool OnHover(HoverEvent e) { updateState(); @@ -154,5 +239,40 @@ namespace osu.Game.Online.Leaderboards public LocalisableString TooltipText { get; } } + + private partial class ColouredModSwitchTiny : ModSwitchTiny + { + [Resolved] + private OsuColour colours { get; set; } = null!; + + public ColouredModSwitchTiny(IMod mod) + : base(mod) + { + } + + protected override void UpdateState() + { + AcronymText.Colour = Colour4.FromHex("#555555"); + Background.Colour = colours.Yellow; + } + } + + public MenuItem[] ContextMenuItems + { + get + { + List items = new List(); + + if (score.Mods.Length > 0 && modsContainer.Any(s => s.IsHovered) && songSelect != null) + items.Add(new OsuMenuItem("Use these mods", MenuItemType.Highlighted, () => songSelect.Mods.Value = score.Mods)); + + if (score.Files.Count <= 0) return items.ToArray(); + + items.Add(new OsuMenuItem("Export", MenuItemType.Standard, () => new LegacyScoreExporter(storage).Export(score))); + items.Add(new OsuMenuItem(CommonStrings.ButtonsDelete, MenuItemType.Destructive, () => dialogOverlay?.Push(new LocalScoreDeleteDialog(score)))); + + return items.ToArray(); + } + } } } diff --git a/osu.Game/Rulesets/UI/ModSwitchTiny.cs b/osu.Game/Rulesets/UI/ModSwitchTiny.cs index a5cf75bd07..df7722761d 100644 --- a/osu.Game/Rulesets/UI/ModSwitchTiny.cs +++ b/osu.Game/Rulesets/UI/ModSwitchTiny.cs @@ -24,8 +24,8 @@ namespace osu.Game.Rulesets.UI private readonly IMod mod; - private readonly Box background; - private readonly OsuSpriteText acronymText; + protected Box Background; + protected OsuSpriteText AcronymText; private Color4 activeForegroundColour; private Color4 inactiveForegroundColour; @@ -44,11 +44,11 @@ namespace osu.Game.Rulesets.UI Masking = true, Children = new Drawable[] { - background = new Box + Background = new Box { RelativeSizeAxes = Axes.Both }, - acronymText = new OsuSpriteText + AcronymText = new OsuSpriteText { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -78,14 +78,14 @@ namespace osu.Game.Rulesets.UI { base.LoadComplete(); - Active.BindValueChanged(_ => updateState(), true); + Active.BindValueChanged(_ => UpdateState(), true); FinishTransforms(true); } - private void updateState() + protected virtual void UpdateState() { - acronymText.FadeColour(Active.Value ? activeForegroundColour : inactiveForegroundColour, 200, Easing.OutQuint); - background.FadeColour(Active.Value ? activeBackgroundColour : inactiveBackgroundColour, 200, Easing.OutQuint); + AcronymText.FadeColour(Active.Value ? activeForegroundColour : inactiveForegroundColour, 200, Easing.OutQuint); + Background.FadeColour(Active.Value ? activeBackgroundColour : inactiveBackgroundColour, 200, Easing.OutQuint); } } } From cfbf6672e51bba0bad7f23d0d245aeba7c9a9a09 Mon Sep 17 00:00:00 2001 From: mk56-spn Date: Mon, 16 Jan 2023 18:12:59 +0100 Subject: [PATCH 0026/2493] Add flag and user name --- .../Online/Leaderboards/LeaderBoardScoreV2.cs | 94 ++++++++++++++++++- 1 file changed, 90 insertions(+), 4 deletions(-) diff --git a/osu.Game/Online/Leaderboards/LeaderBoardScoreV2.cs b/osu.Game/Online/Leaderboards/LeaderBoardScoreV2.cs index ed8536a2bf..f5c002446a 100644 --- a/osu.Game/Online/Leaderboards/LeaderBoardScoreV2.cs +++ b/osu.Game/Online/Leaderboards/LeaderBoardScoreV2.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 osu.Framework.Allocation; @@ -13,16 +14,19 @@ using osu.Framework.Input.Events; using osu.Framework.Localisation; using osu.Framework.Platform; using osu.Game.Database; +using osu.Game.Extensions; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; +using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays; using osu.Game.Resources.Localisation.Web; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.UI; using osu.Game.Scoring; using osu.Game.Screens.Select; +using osu.Game.Users.Drawables; using osu.Game.Utils; using osuTK; @@ -61,7 +65,14 @@ namespace osu.Game.Online.Leaderboards private Box background = null!; private Box foreground = null!; + private Drawable avatar = null!; + private ClickableAvatar innerAvatar = null!; + + private OsuSpriteText nameLabel = null!; + protected Container RankContainer { get; private set; } = null!; + + private FillFlowContainer flagBadgeAndDateContainer = null!; private FillFlowContainer modsContainer = null!; private OsuSpriteText scoreText = null!; @@ -77,6 +88,8 @@ namespace osu.Game.Online.Leaderboards [BackgroundDependencyLoader] private void load(ScoreManager scoreManager) { + var user = score.User; + foregroundColour = isPersonalBest ? colourProvider.Background1 : colourProvider.Background5; backgroundColour = isPersonalBest ? colourProvider.Background2 : colourProvider.Background4; @@ -116,7 +129,7 @@ namespace osu.Game.Online.Leaderboards RelativeSizeAxes = Axes.Y, Width = 35 }, - createCentreContent(), + createCentreContent(user), createRightSideContent(scoreManager) } } @@ -124,11 +137,13 @@ namespace osu.Game.Online.Leaderboards } }; + innerAvatar.OnLoadComplete += d => d.FadeInFromZero(200); + modsContainer.Spacing = new Vector2(modsContainer.Children.Count > 5 ? -20 : 2, 0); modsContainer.Padding = new MarginPadding { Top = modsContainer.Children.Count > 0 ? 4 : 0 }; } - private Container createCentreContent() => + private Container createCentreContent(APIUser user) => new Container { Anchor = Anchor.CentreLeft, @@ -136,13 +151,63 @@ namespace osu.Game.Online.Leaderboards Masking = true, CornerRadius = corner_radius, RelativeSizeAxes = Axes.Both, - Children = new Drawable[] + Children = new[] { foreground = new Box { RelativeSizeAxes = Axes.Both, Colour = foregroundColour - } + }, + avatar = new MaskedWrapper( + innerAvatar = new ClickableAvatar(user) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Scale = new Vector2(1.1f), + Shear = -shear, + RelativeSizeAxes = Axes.Both, + }) + { + RelativeSizeAxes = Axes.None, + Size = new Vector2(HEIGHT) + }, + new FillFlowContainer + { + Position = new Vector2(HEIGHT + 9, 9), + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Children = new Drawable[] + { + flagBadgeAndDateContainer = new FillFlowContainer + { + Shear = -shear, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(5f, 0f), + Size = new Vector2(87, 16), + Masking = true, + Children = new Drawable[] + { + new UpdateableFlag(user.CountryCode) + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Size = new Vector2(24, 16), + }, + new DateLabel(score.Date) + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + } + } + }, + nameLabel = new OsuSpriteText + { + Shear = -shear, + Text = user.Username, + Font = OsuFont.GetFont(size: 24, weight: FontWeight.SemiBold) + } + } + }, } }; @@ -221,6 +286,17 @@ namespace osu.Game.Online.Leaderboards background.FadeColour(IsHovered ? backgroundColour.Lighten(0.2f) : backgroundColour, transition_duration, Easing.OutQuint); } + private partial class DateLabel : DrawableDate + { + public DateLabel(DateTimeOffset date) + : base(date) + { + Font = OsuFont.GetFont(size: 16, weight: FontWeight.Medium, italics: true); + } + + protected override string Format() => Date.ToShortRelativeTime(TimeSpan.FromSeconds(30)); + } + private partial class RankLabel : Container, IHasTooltip { public RankLabel(int? rank) @@ -240,6 +316,16 @@ namespace osu.Game.Online.Leaderboards public LocalisableString TooltipText { get; } } + private partial class MaskedWrapper : DelayedLoadWrapper + { + public MaskedWrapper(Drawable content, double timeBeforeLoad = 500) + : base(content, timeBeforeLoad) + { + CornerRadius = corner_radius; + Masking = true; + } + } + private partial class ColouredModSwitchTiny : ModSwitchTiny { [Resolved] From 6b889c2c534fa61f3e0e09bcdcefe1e373b99da9 Mon Sep 17 00:00:00 2001 From: mk56-spn Date: Mon, 16 Jan 2023 18:21:19 +0100 Subject: [PATCH 0027/2493] add score component label and animate component --- .../Online/Leaderboards/LeaderBoardScoreV2.cs | 146 ++++++++++++++++++ 1 file changed, 146 insertions(+) diff --git a/osu.Game/Online/Leaderboards/LeaderBoardScoreV2.cs b/osu.Game/Online/Leaderboards/LeaderBoardScoreV2.cs index f5c002446a..d9a47914a3 100644 --- a/osu.Game/Online/Leaderboards/LeaderBoardScoreV2.cs +++ b/osu.Game/Online/Leaderboards/LeaderBoardScoreV2.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; +using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; @@ -19,16 +20,19 @@ using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; +using osu.Game.Localisation; using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays; using osu.Game.Resources.Localisation.Web; using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI; using osu.Game.Scoring; using osu.Game.Screens.Select; using osu.Game.Users.Drawables; using osu.Game.Utils; using osuTK; +using CommonStrings = osu.Game.Resources.Localisation.Web.CommonStrings; namespace osu.Game.Online.Leaderboards { @@ -69,6 +73,7 @@ namespace osu.Game.Online.Leaderboards private ClickableAvatar innerAvatar = null!; private OsuSpriteText nameLabel = null!; + private List statisticsLabels = null!; protected Container RankContainer { get; private set; } = null!; @@ -93,6 +98,8 @@ namespace osu.Game.Online.Leaderboards foregroundColour = isPersonalBest ? colourProvider.Background1 : colourProvider.Background5; backgroundColour = isPersonalBest ? colourProvider.Background2 : colourProvider.Background4; + statisticsLabels = GetStatistics(score).Select(s => new ScoreComponentLabel(s, score)).ToList(); + Shear = shear; RelativeSizeAxes = Axes.X; Height = HEIGHT; @@ -268,6 +275,50 @@ namespace osu.Game.Online.Leaderboards } }; + protected (CaseTransformableString, LocalisableString DisplayAccuracy)[] GetStatistics(ScoreInfo model) => new[] + { + (EditorSetupStrings.ComboColourPrefix.ToUpper(), model.MaxCombo.ToString().Insert(model.MaxCombo.ToString().Length, "x")), + (BeatmapsetsStrings.ShowScoreboardHeadersAccuracy.ToUpper(), model.DisplayAccuracy), + (getResultNames(score).ToUpper(), getResults(score).ToUpper()) + }; + + public override void Show() + { + foreach (var d in new[] { avatar, nameLabel, scoreText, scoreRank, flagBadgeAndDateContainer, modsContainer }.Concat(statisticsLabels)) + d.FadeOut(); + + Alpha = 0; + + content.MoveToY(75); + avatar.MoveToX(75); + nameLabel.MoveToX(150); + + this.FadeIn(200); + content.MoveToY(0, 800, Easing.OutQuint); + + using (BeginDelayedSequence(100)) + { + avatar.FadeIn(300, Easing.OutQuint); + nameLabel.FadeIn(350, Easing.OutQuint); + + avatar.MoveToX(0, 300, Easing.OutQuint); + nameLabel.MoveToX(0, 350, Easing.OutQuint); + + using (BeginDelayedSequence(250)) + { + scoreText.FadeIn(200); + scoreRank.FadeIn(200); + + using (BeginDelayedSequence(50)) + { + var drawables = new Drawable[] { flagBadgeAndDateContainer, modsContainer }.Concat(statisticsLabels).ToArray(); + for (int i = 0; i < drawables.Length; i++) + drawables[i].FadeIn(100 + i * 50); + } + } + } + } + protected override bool OnHover(HoverEvent e) { updateState(); @@ -297,6 +348,51 @@ namespace osu.Game.Online.Leaderboards protected override string Format() => Date.ToShortRelativeTime(TimeSpan.FromSeconds(30)); } + private partial class ScoreComponentLabel : Container + { + private readonly (LocalisableString Name, LocalisableString Value) statisticInfo; + private readonly ScoreInfo score; + + private FillFlowContainer content = null!; + public override bool Contains(Vector2 screenSpacePos) => content.Contains(screenSpacePos); + + public ScoreComponentLabel((LocalisableString Name, LocalisableString Value) statisticInfo, ScoreInfo score) + { + this.statisticInfo = statisticInfo; + this.score = score; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours, OverlayColourProvider colourProvider) + { + AutoSizeAxes = Axes.Both; + OsuSpriteText value; + Child = content = new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Padding = new MarginPadding { Right = 25 }, + Children = new Drawable[] + { + new OsuSpriteText + { + Colour = colourProvider.Content2, + Text = statisticInfo.Name, + Font = OsuFont.GetFont(size: 12, weight: FontWeight.Bold), + }, + value = new OsuSpriteText + { + Text = statisticInfo.Value, + Font = OsuFont.GetFont(size: 19, weight: FontWeight.Medium), + } + } + }; + + if (score.Combo == score.MaxCombo && statisticInfo.Name == EditorSetupStrings.ComboColourPrefix.ToUpper()) + value.Colour = colours.Lime1; + } + } + private partial class RankLabel : Container, IHasTooltip { public RankLabel(int? rank) @@ -360,5 +456,55 @@ namespace osu.Game.Online.Leaderboards return items.ToArray(); } } + + private LocalisableString getResults(ScoreInfo score) + { + string resultString = score.GetStatisticsForDisplay().Where(s => s.Result.IsBasic()).Aggregate(string.Empty, (current, result) => + current.Insert(current.Length, $"{result.Count}/")); + + return resultString.Remove(resultString.Length - 1); + } + + private LocalisableString getResultNames(ScoreInfo score) + { + string resultName = string.Empty; + + foreach (var hitResult in score.GetStatisticsForDisplay().Where(s => s.Result.IsBasic())) + { + switch (hitResult.Result) + { + case HitResult.Perfect: + appendToString("320/"); + break; + + case HitResult.Great: + appendToString("300/"); + break; + + case HitResult.Good: + appendToString("200/"); + break; + + case HitResult.Ok: + appendToString("100/"); + break; + + case HitResult.Meh: + appendToString("50/"); + break; + + case HitResult.Miss: + appendToString("X"); + break; + + default: + throw new ArgumentOutOfRangeException(); + } + } + + void appendToString(string appendedString) => resultName = resultName.Insert(resultName.Length, appendedString); + + return resultName.Remove(resultName.Length); + } } } From 5a68b3062a5cbee236f2c875dca3b30974dc76c4 Mon Sep 17 00:00:00 2001 From: mk56-spn Date: Mon, 16 Jan 2023 18:23:13 +0100 Subject: [PATCH 0028/2493] add statistics to main content --- osu.Game/Online/Leaderboards/LeaderBoardScoreV2.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/osu.Game/Online/Leaderboards/LeaderBoardScoreV2.cs b/osu.Game/Online/Leaderboards/LeaderBoardScoreV2.cs index d9a47914a3..ccf48f4f74 100644 --- a/osu.Game/Online/Leaderboards/LeaderBoardScoreV2.cs +++ b/osu.Game/Online/Leaderboards/LeaderBoardScoreV2.cs @@ -215,6 +215,15 @@ namespace osu.Game.Online.Leaderboards } } }, + new FillFlowContainer + { + Shear = -shear, + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Children = statisticsLabels + } } }; From 83b10d61f493ec4029d1967890220eef0d8b44ad Mon Sep 17 00:00:00 2001 From: mk56-spn Date: Mon, 16 Jan 2023 19:03:17 +0100 Subject: [PATCH 0029/2493] Adjust widths in statistic labels --- .../SongSelect/TestSceneLeaderboardScoreV2.cs | 21 ++------------ .../Online/Leaderboards/LeaderBoardScoreV2.cs | 28 ++++++++++++++----- 2 files changed, 24 insertions(+), 25 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneLeaderboardScoreV2.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneLeaderboardScoreV2.cs index 13eaf5417d..9a6f52a487 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneLeaderboardScoreV2.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneLeaderboardScoreV2.cs @@ -39,26 +39,11 @@ namespace osu.Game.Tests.Visual.SongSelect }, }, new ScoreInfo - { - Position = 110000, - Rank = ScoreRank.X, - Accuracy = 1, - MaxCombo = 244, - TotalScore = 1707827, - Ruleset = new OsuRuleset().RulesetInfo, - User = new APIUser - { - Id = 4608074, - Username = @"Skycries", - CountryCode = CountryCode.BR, - }, - }, - new ScoreInfo { Position = 22333, Rank = ScoreRank.S, - Accuracy = 1, - MaxCombo = 244, + Accuracy = 0.1f, + MaxCombo = 2404, TotalScore = 1707827, Ruleset = new OsuRuleset().RulesetInfo, User = new APIUser @@ -80,7 +65,7 @@ namespace osu.Game.Tests.Visual.SongSelect Children = new Drawable[] { new LeaderBoardScoreV2(scores[0], 1), - new LeaderBoardScoreV2(scores[2], 3, true) + new LeaderBoardScoreV2(scores[1], null, true) } }; } diff --git a/osu.Game/Online/Leaderboards/LeaderBoardScoreV2.cs b/osu.Game/Online/Leaderboards/LeaderBoardScoreV2.cs index ccf48f4f74..a9d86d75d7 100644 --- a/osu.Game/Online/Leaderboards/LeaderBoardScoreV2.cs +++ b/osu.Game/Online/Leaderboards/LeaderBoardScoreV2.cs @@ -40,7 +40,7 @@ namespace osu.Game.Online.Leaderboards { private readonly ScoreInfo score; - private const int HEIGHT = 60; + private const int height = 60; private const int corner_radius = 10; private const int transition_duration = 200; @@ -102,7 +102,7 @@ namespace osu.Game.Online.Leaderboards Shear = shear; RelativeSizeAxes = Axes.X; - Height = HEIGHT; + Height = height; Child = content = new Container { Masking = true, @@ -176,11 +176,11 @@ namespace osu.Game.Online.Leaderboards }) { RelativeSizeAxes = Axes.None, - Size = new Vector2(HEIGHT) + Size = new Vector2(height) }, new FillFlowContainer { - Position = new Vector2(HEIGHT + 9, 9), + Position = new Vector2(height + 9, 9), AutoSizeAxes = Axes.Both, Direction = FillDirection.Vertical, Children = new Drawable[] @@ -217,6 +217,7 @@ namespace osu.Game.Online.Leaderboards }, new FillFlowContainer { + Spacing = new Vector2(5, 0), Shear = -shear, Anchor = Anchor.CentreRight, Origin = Anchor.CentreRight, @@ -346,6 +347,8 @@ namespace osu.Game.Online.Leaderboards background.FadeColour(IsHovered ? backgroundColour.Lighten(0.2f) : backgroundColour, transition_duration, Easing.OutQuint); } + #region Subclasses + private partial class DateLabel : DrawableDate { public DateLabel(DateTimeOffset date) @@ -374,13 +377,12 @@ namespace osu.Game.Online.Leaderboards [BackgroundDependencyLoader] private void load(OsuColour colours, OverlayColourProvider colourProvider) { - AutoSizeAxes = Axes.Both; + AutoSizeAxes = Axes.Y; OsuSpriteText value; Child = content = new FillFlowContainer { AutoSizeAxes = Axes.Both, Direction = FillDirection.Vertical, - Padding = new MarginPadding { Right = 25 }, Children = new Drawable[] { new OsuSpriteText @@ -397,8 +399,18 @@ namespace osu.Game.Online.Leaderboards } }; - if (score.Combo == score.MaxCombo && statisticInfo.Name == EditorSetupStrings.ComboColourPrefix.ToUpper()) + if (statisticInfo.Name == EditorSetupStrings.ComboColourPrefix.ToUpper()) + { + Width = 45; + + if (score.Combo != score.MaxCombo) return; + value.Colour = colours.Lime1; + + return; + } + + Width = statisticInfo.Name == BeatmapsetsStrings.ShowScoreboardHeadersAccuracy.ToUpper() ? 60 : 120; } } @@ -448,6 +460,8 @@ namespace osu.Game.Online.Leaderboards } } + #endregion + public MenuItem[] ContextMenuItems { get From 6c30ba25bc0b58a7870284f36fbe5d76cbe46b32 Mon Sep 17 00:00:00 2001 From: mk56-spn Date: Mon, 16 Jan 2023 19:16:35 +0100 Subject: [PATCH 0030/2493] Add shading to mod pills --- .../Visual/SongSelect/TestSceneLeaderboardScoreV2.cs | 3 ++- osu.Game/Online/Leaderboards/LeaderBoardScoreV2.cs | 10 ++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneLeaderboardScoreV2.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneLeaderboardScoreV2.cs index 9a6f52a487..f9c3248791 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneLeaderboardScoreV2.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneLeaderboardScoreV2.cs @@ -29,7 +29,7 @@ namespace osu.Game.Tests.Visual.SongSelect Accuracy = 1, MaxCombo = 244, TotalScore = 1707827, - Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), }, + Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), new OsuModAlternate(), new OsuModFlashlight(), new OsuModFreezeFrame() }, Ruleset = new OsuRuleset().RulesetInfo, User = new APIUser { @@ -44,6 +44,7 @@ namespace osu.Game.Tests.Visual.SongSelect Rank = ScoreRank.S, Accuracy = 0.1f, MaxCombo = 2404, + Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), new OsuModAlternate(), new OsuModFlashlight(), new OsuModFreezeFrame(), new OsuModClassic() }, TotalScore = 1707827, Ruleset = new OsuRuleset().RulesetInfo, User = new APIUser diff --git a/osu.Game/Online/Leaderboards/LeaderBoardScoreV2.cs b/osu.Game/Online/Leaderboards/LeaderBoardScoreV2.cs index a9d86d75d7..396fa7708b 100644 --- a/osu.Game/Online/Leaderboards/LeaderBoardScoreV2.cs +++ b/osu.Game/Online/Leaderboards/LeaderBoardScoreV2.cs @@ -9,6 +9,7 @@ using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; +using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; @@ -451,6 +452,15 @@ namespace osu.Game.Online.Leaderboards public ColouredModSwitchTiny(IMod mod) : base(mod) { + Masking = true; + EdgeEffect = new EdgeEffectParameters + { + Roundness = 15, + Type = EdgeEffectType.Shadow, + Colour = Colour4.Black.Opacity(0.15f), + Radius = 3, + Offset = new Vector2(-2, 0) + }; } protected override void UpdateState() From 3ccecc2cb5ebc1ee6cea13d4f6ccea9d44bdf2d6 Mon Sep 17 00:00:00 2001 From: mk56-spn Date: Mon, 16 Jan 2023 19:24:03 +0100 Subject: [PATCH 0031/2493] Add back tooltip --- osu.Game/Online/Leaderboards/LeaderBoardScoreV2.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/osu.Game/Online/Leaderboards/LeaderBoardScoreV2.cs b/osu.Game/Online/Leaderboards/LeaderBoardScoreV2.cs index 396fa7708b..08986f200e 100644 --- a/osu.Game/Online/Leaderboards/LeaderBoardScoreV2.cs +++ b/osu.Game/Online/Leaderboards/LeaderBoardScoreV2.cs @@ -37,7 +37,7 @@ using CommonStrings = osu.Game.Resources.Localisation.Web.CommonStrings; namespace osu.Game.Online.Leaderboards { - public partial class LeaderBoardScoreV2 : OsuClickableContainer, IHasContextMenu + public partial class LeaderBoardScoreV2 : OsuClickableContainer, IHasContextMenu, IHasCustomTooltip { private readonly ScoreInfo score; @@ -77,13 +77,15 @@ namespace osu.Game.Online.Leaderboards private List statisticsLabels = null!; protected Container RankContainer { get; private set; } = null!; - private FillFlowContainer flagBadgeAndDateContainer = null!; private FillFlowContainer modsContainer = null!; private OsuSpriteText scoreText = null!; private Drawable scoreRank = null!; + public ITooltip GetCustomTooltip() => new LeaderboardScoreTooltip(); + public virtual ScoreInfo TooltipContent => score; + public LeaderBoardScoreV2(ScoreInfo score, int? rank, bool isPersonalBest = false) { this.score = score; From 1df049294701844bd880400ac465bc26a073fe3d Mon Sep 17 00:00:00 2001 From: mk56-spn Date: Mon, 16 Jan 2023 19:30:50 +0100 Subject: [PATCH 0032/2493] Add tooltip to new mod pills --- osu.Game/Online/Leaderboards/LeaderBoardScoreV2.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/osu.Game/Online/Leaderboards/LeaderBoardScoreV2.cs b/osu.Game/Online/Leaderboards/LeaderBoardScoreV2.cs index 08986f200e..22c5db1a99 100644 --- a/osu.Game/Online/Leaderboards/LeaderBoardScoreV2.cs +++ b/osu.Game/Online/Leaderboards/LeaderBoardScoreV2.cs @@ -446,14 +446,17 @@ namespace osu.Game.Online.Leaderboards } } - private partial class ColouredModSwitchTiny : ModSwitchTiny + private partial class ColouredModSwitchTiny : ModSwitchTiny, IHasTooltip { + private readonly IMod mod; + [Resolved] private OsuColour colours { get; set; } = null!; public ColouredModSwitchTiny(IMod mod) : base(mod) { + this.mod = mod; Masking = true; EdgeEffect = new EdgeEffectParameters { @@ -470,6 +473,8 @@ namespace osu.Game.Online.Leaderboards AcronymText.Colour = Colour4.FromHex("#555555"); Background.Colour = colours.Yellow; } + + public virtual LocalisableString TooltipText => (mod as Mod)?.IconTooltip ?? mod.Name; } #endregion From 7c550e534005bd12811e90c5c38a124a2050a24d Mon Sep 17 00:00:00 2001 From: MK56 <74463310+mk56-spn@users.noreply.github.com> Date: Mon, 16 Jan 2023 22:37:06 +0100 Subject: [PATCH 0033/2493] fix capitalisation issue in class name Co-authored-by: Joseph Madamba --- osu.Game/Online/Leaderboards/LeaderBoardScoreV2.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Online/Leaderboards/LeaderBoardScoreV2.cs b/osu.Game/Online/Leaderboards/LeaderBoardScoreV2.cs index 22c5db1a99..6c16b8a9da 100644 --- a/osu.Game/Online/Leaderboards/LeaderBoardScoreV2.cs +++ b/osu.Game/Online/Leaderboards/LeaderBoardScoreV2.cs @@ -37,7 +37,7 @@ using CommonStrings = osu.Game.Resources.Localisation.Web.CommonStrings; namespace osu.Game.Online.Leaderboards { - public partial class LeaderBoardScoreV2 : OsuClickableContainer, IHasContextMenu, IHasCustomTooltip + public partial class LeaderboardScoreV2 : OsuClickableContainer, IHasContextMenu, IHasCustomTooltip { private readonly ScoreInfo score; From d73ce1ddb24b197b83a25e111d9afab63651bd2c Mon Sep 17 00:00:00 2001 From: mk56-spn Date: Mon, 16 Jan 2023 22:51:46 +0100 Subject: [PATCH 0034/2493] Actually fix issue with naming of LeaderboardScoreV2.cs class --- .../Visual/SongSelect/TestSceneLeaderboardScoreV2.cs | 4 ++-- .../{LeaderBoardScoreV2.cs => LeaderboardScoreV2.cs} | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) rename osu.Game/Online/Leaderboards/{LeaderBoardScoreV2.cs => LeaderboardScoreV2.cs} (99%) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneLeaderboardScoreV2.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneLeaderboardScoreV2.cs index f9c3248791..4db2012733 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneLeaderboardScoreV2.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneLeaderboardScoreV2.cs @@ -65,8 +65,8 @@ namespace osu.Game.Tests.Visual.SongSelect AutoSizeAxes = Axes.Y, Children = new Drawable[] { - new LeaderBoardScoreV2(scores[0], 1), - new LeaderBoardScoreV2(scores[1], null, true) + new LeaderboardScoreV2(scores[0], 1), + new LeaderboardScoreV2(scores[1], null, true) } }; } diff --git a/osu.Game/Online/Leaderboards/LeaderBoardScoreV2.cs b/osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs similarity index 99% rename from osu.Game/Online/Leaderboards/LeaderBoardScoreV2.cs rename to osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs index 6c16b8a9da..0532d6e51b 100644 --- a/osu.Game/Online/Leaderboards/LeaderBoardScoreV2.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs @@ -86,7 +86,7 @@ namespace osu.Game.Online.Leaderboards public ITooltip GetCustomTooltip() => new LeaderboardScoreTooltip(); public virtual ScoreInfo TooltipContent => score; - public LeaderBoardScoreV2(ScoreInfo score, int? rank, bool isPersonalBest = false) + public LeaderboardScoreV2(ScoreInfo score, int? rank, bool isPersonalBest = false) { this.score = score; this.rank = rank; From c44891d42775d56022a5cf0616f516aa3662758d Mon Sep 17 00:00:00 2001 From: mk56-spn Date: Tue, 17 Jan 2023 20:13:50 +0100 Subject: [PATCH 0035/2493] clean up linQ result formatting. Replace numbers with hitresult displaynames. Make adjustments to statistics to allow them to work with autosizing --- .../SongSelect/TestSceneLeaderboardScoreV2.cs | 2 +- .../Online/Leaderboards/LeaderboardScoreV2.cs | 76 +++++-------------- 2 files changed, 20 insertions(+), 58 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneLeaderboardScoreV2.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneLeaderboardScoreV2.cs index 4db2012733..04846d7f19 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneLeaderboardScoreV2.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneLeaderboardScoreV2.cs @@ -43,7 +43,7 @@ namespace osu.Game.Tests.Visual.SongSelect Position = 22333, Rank = ScoreRank.S, Accuracy = 0.1f, - MaxCombo = 2404, + MaxCombo = 32040, Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), new OsuModAlternate(), new OsuModFlashlight(), new OsuModFreezeFrame(), new OsuModClassic() }, TotalScore = 1707827, Ruleset = new OsuRuleset().RulesetInfo, diff --git a/osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs b/osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs index 0532d6e51b..093037e6d1 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs @@ -21,7 +21,6 @@ using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; -using osu.Game.Localisation; using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays; using osu.Game.Resources.Localisation.Web; @@ -33,7 +32,6 @@ using osu.Game.Screens.Select; using osu.Game.Users.Drawables; using osu.Game.Utils; using osuTK; -using CommonStrings = osu.Game.Resources.Localisation.Web.CommonStrings; namespace osu.Game.Online.Leaderboards { @@ -220,7 +218,8 @@ namespace osu.Game.Online.Leaderboards }, new FillFlowContainer { - Spacing = new Vector2(5, 0), + Margin = new MarginPadding { Right = 40 }, + Spacing = new Vector2(25, 0), Shear = -shear, Anchor = Anchor.CentreRight, Origin = Anchor.CentreRight, @@ -290,7 +289,7 @@ namespace osu.Game.Online.Leaderboards protected (CaseTransformableString, LocalisableString DisplayAccuracy)[] GetStatistics(ScoreInfo model) => new[] { - (EditorSetupStrings.ComboColourPrefix.ToUpper(), model.MaxCombo.ToString().Insert(model.MaxCombo.ToString().Length, "x")), + (BeatmapsetsStrings.ShowScoreboardHeadersCombo.ToUpper(), model.MaxCombo.ToString().Insert(model.MaxCombo.ToString().Length, "x")), (BeatmapsetsStrings.ShowScoreboardHeadersAccuracy.ToUpper(), model.DisplayAccuracy), (getResultNames(score).ToUpper(), getResults(score).ToUpper()) }; @@ -380,7 +379,7 @@ namespace osu.Game.Online.Leaderboards [BackgroundDependencyLoader] private void load(OsuColour colours, OverlayColourProvider colourProvider) { - AutoSizeAxes = Axes.Y; + AutoSizeAxes = Axes.Both; OsuSpriteText value; Child = content = new FillFlowContainer { @@ -396,24 +395,17 @@ namespace osu.Game.Online.Leaderboards }, value = new OsuSpriteText { + // We don't want the value setting the horizontal size, since it leads to wonky accuracy container length, + // since the accuracy is sometimes longer than its name. + BypassAutoSizeAxes = Axes.X, Text = statisticInfo.Value, Font = OsuFont.GetFont(size: 19, weight: FontWeight.Medium), } } }; - if (statisticInfo.Name == EditorSetupStrings.ComboColourPrefix.ToUpper()) - { - Width = 45; - - if (score.Combo != score.MaxCombo) return; - + if (score.Combo != score.MaxCombo && statisticInfo.Name == BeatmapsetsStrings.ShowScoreboardHeadersCombo) value.Colour = colours.Lime1; - - return; - } - - Width = statisticInfo.Name == BeatmapsetsStrings.ShowScoreboardHeadersAccuracy.ToUpper() ? 60 : 120; } } @@ -446,7 +438,7 @@ namespace osu.Game.Online.Leaderboards } } - private partial class ColouredModSwitchTiny : ModSwitchTiny, IHasTooltip + private sealed partial class ColouredModSwitchTiny : ModSwitchTiny, IHasTooltip { private readonly IMod mod; @@ -474,7 +466,7 @@ namespace osu.Game.Online.Leaderboards Background.Colour = colours.Yellow; } - public virtual LocalisableString TooltipText => (mod as Mod)?.IconTooltip ?? mod.Name; + public LocalisableString TooltipText => (mod as Mod)?.IconTooltip ?? mod.Name; } #endregion @@ -499,52 +491,22 @@ namespace osu.Game.Online.Leaderboards private LocalisableString getResults(ScoreInfo score) { - string resultString = score.GetStatisticsForDisplay().Where(s => s.Result.IsBasic()).Aggregate(string.Empty, (current, result) => - current.Insert(current.Length, $"{result.Count}/")); + string resultString = score.GetStatisticsForDisplay() + .Where(s => s.Result.IsBasic()) + .Aggregate(string.Empty, (current, result) => + current.Insert(current.Length, $"{result.Count}/")); return resultString.Remove(resultString.Length - 1); } private LocalisableString getResultNames(ScoreInfo score) { - string resultName = string.Empty; + string resultName = score.GetStatisticsForDisplay() + .Where(s => s.Result.IsBasic()) + .Aggregate(string.Empty, (current, hitResult) => + current.Insert(current.Length, $"{hitResult.DisplayName.ToString().ToUpperInvariant()}/")); - foreach (var hitResult in score.GetStatisticsForDisplay().Where(s => s.Result.IsBasic())) - { - switch (hitResult.Result) - { - case HitResult.Perfect: - appendToString("320/"); - break; - - case HitResult.Great: - appendToString("300/"); - break; - - case HitResult.Good: - appendToString("200/"); - break; - - case HitResult.Ok: - appendToString("100/"); - break; - - case HitResult.Meh: - appendToString("50/"); - break; - - case HitResult.Miss: - appendToString("X"); - break; - - default: - throw new ArgumentOutOfRangeException(); - } - } - - void appendToString(string appendedString) => resultName = resultName.Insert(resultName.Length, appendedString); - - return resultName.Remove(resultName.Length); + return resultName.Remove(resultName.Length - 1); } } } From 4623c04f4676961f09dbbb5378d9db030bbb972c Mon Sep 17 00:00:00 2001 From: mk56-spn Date: Mon, 23 Jan 2023 11:35:42 +0100 Subject: [PATCH 0036/2493] Add mania score to leaderboard test scene --- .../SongSelect/TestSceneLeaderboardScoreV2.cs | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneLeaderboardScoreV2.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneLeaderboardScoreV2.cs index 04846d7f19..0823da2248 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneLeaderboardScoreV2.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneLeaderboardScoreV2.cs @@ -6,6 +6,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Leaderboards; +using osu.Game.Rulesets.Mania; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Mods; @@ -53,7 +54,23 @@ namespace osu.Game.Tests.Visual.SongSelect Username = @"Toukai", CountryCode = CountryCode.CA, }, - } + }, + + new ScoreInfo + { + Position = 110000, + Rank = ScoreRank.X, + Accuracy = 1, + MaxCombo = 244, + TotalScore = 17078279, + Ruleset = new ManiaRuleset().RulesetInfo, + User = new APIUser + { + Id = 4608074, + Username = @"Skycries", + CountryCode = CountryCode.BR, + }, + }, }; Child = new FillFlowContainer @@ -66,7 +83,8 @@ namespace osu.Game.Tests.Visual.SongSelect Children = new Drawable[] { new LeaderboardScoreV2(scores[0], 1), - new LeaderboardScoreV2(scores[1], null, true) + new LeaderboardScoreV2(scores[1], null, true), + new LeaderboardScoreV2(scores[2], null, true) } }; } From 244ca4e8c75293d7f00c62aeae010316f95cdfc4 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Mon, 8 May 2023 11:54:33 +0200 Subject: [PATCH 0037/2493] Remove dependency on SamplesBindable --- .../Edit/Compose/Components/Timeline/SamplePointPiece.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs index b02cfb505e..1f673a7b10 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs @@ -26,12 +26,12 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline { public readonly HitObject HitObject; - private readonly BindableList samplesBindable; + [Resolved(canBeNull: true)] + private EditorBeatmap editorBeatmap { get; set; } = null!; public SamplePointPiece(HitObject hitObject) { HitObject = hitObject; - samplesBindable = hitObject.SamplesBindable.GetBoundCopy(); } protected override Color4 GetRepresentingColour(OsuColour colours) => colours.Pink; @@ -39,7 +39,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline [BackgroundDependencyLoader] private void load() { - samplesBindable.BindCollectionChanged((_, _) => updateText(), true); + HitObject.DefaultsApplied += _ => updateText(); + updateText(); } protected override bool OnClick(ClickEvent e) @@ -50,7 +51,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline private void updateText() { - Label.Text = $"{GetBankValue(samplesBindable)} {GetVolumeValue(samplesBindable)}"; + Label.Text = $"{GetBankValue(HitObject.Samples)} {GetVolumeValue(HitObject.Samples)}"; } public static string? GetBankValue(IEnumerable samples) From b447018e5b54009a297495d7ef96a48bbc5a1d18 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Mon, 8 May 2023 11:55:58 +0200 Subject: [PATCH 0038/2493] remove editor beatmap --- .../Edit/Compose/Components/Timeline/SamplePointPiece.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs index 1f673a7b10..4f3526b531 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs @@ -26,9 +26,6 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline { public readonly HitObject HitObject; - [Resolved(canBeNull: true)] - private EditorBeatmap editorBeatmap { get; set; } = null!; - public SamplePointPiece(HitObject hitObject) { HitObject = hitObject; From cb7b747d52dec03881c056cbc6545777a5c5dc47 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Mon, 8 May 2023 12:38:53 +0200 Subject: [PATCH 0039/2493] create NodeSamplePointPiece --- .../Timeline/NodeSamplePointPiece.cs | 54 +++++++++++++++++++ .../Components/Timeline/SamplePointPiece.cs | 10 ++-- 2 files changed, 61 insertions(+), 3 deletions(-) create mode 100644 osu.Game/Screens/Edit/Compose/Components/Timeline/NodeSamplePointPiece.cs diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/NodeSamplePointPiece.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/NodeSamplePointPiece.cs new file mode 100644 index 0000000000..dc91401c13 --- /dev/null +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/NodeSamplePointPiece.cs @@ -0,0 +1,54 @@ +// 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.Graphics.UserInterface; +using osu.Game.Audio; +using osu.Game.Graphics; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; +using osuTK.Graphics; + +namespace osu.Game.Screens.Edit.Compose.Components.Timeline +{ + public partial class NodeSamplePointPiece : SamplePointPiece + { + public readonly int NodeIndex; + + public NodeSamplePointPiece(HitObject hitObject, int nodeIndex) + : base(hitObject) + { + if (hitObject is not IHasRepeats) + throw new System.ArgumentException($"HitObject must implement {nameof(IHasRepeats)}", nameof(hitObject)); + + NodeIndex = nodeIndex; + } + + protected override Color4 GetRepresentingColour(OsuColour colours) => colours.Purple; + + protected override IList GetSamples() + { + var hasRepeats = (IHasRepeats)HitObject; + return NodeIndex < hasRepeats.NodeSamples.Count ? hasRepeats.NodeSamples[NodeIndex] : HitObject.Samples; + } + + public override Popover GetPopover() => new NodeSampleEditPopover(HitObject); + + public partial class NodeSampleEditPopover : SampleEditPopover + { + private readonly int nodeIndex; + + protected override IList GetSamples(HitObject ho) + { + var hasRepeats = (IHasRepeats)ho; + return nodeIndex < hasRepeats.NodeSamples.Count ? hasRepeats.NodeSamples[nodeIndex] : ho.Samples; + } + + public NodeSampleEditPopover(HitObject hitObject, int nodeIndex = 0) + : base(hitObject) + { + this.nodeIndex = nodeIndex; + } + } + } +} diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs index 4f3526b531..064e96c255 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs @@ -48,7 +48,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline private void updateText() { - Label.Text = $"{GetBankValue(HitObject.Samples)} {GetVolumeValue(HitObject.Samples)}"; + Label.Text = $"{GetBankValue(GetSamples())} {GetVolumeValue(GetSamples())}"; } public static string? GetBankValue(IEnumerable samples) @@ -61,7 +61,9 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline return samples.Count == 0 ? 0 : samples.Max(o => o.Volume); } - public Popover GetPopover() => new SampleEditPopover(HitObject); + protected virtual IList GetSamples() => HitObject.Samples; + + public virtual Popover GetPopover() => new SampleEditPopover(HitObject); public partial class SampleEditPopover : OsuPopover { @@ -70,6 +72,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline private LabelledTextBox bank = null!; private IndeterminateSliderWithTextBoxInput volume = null!; + protected virtual IList GetSamples(HitObject ho) => ho.Samples; + [Resolved(canBeNull: true)] private EditorBeatmap beatmap { get; set; } = null!; @@ -112,7 +116,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline // if the piece belongs to a currently selected object, assume that the user wants to change all selected objects. // if the piece belongs to an unselected object, operate on that object alone, independently of the selection. var relevantObjects = (beatmap.SelectedHitObjects.Contains(hitObject) ? beatmap.SelectedHitObjects : hitObject.Yield()).ToArray(); - var relevantSamples = relevantObjects.Select(h => h.Samples).ToArray(); + var relevantSamples = relevantObjects.Select(GetSamples).ToArray(); // even if there are multiple objects selected, we can still display sample volume or bank if they all have the same value. string? commonBank = getCommonBank(relevantSamples); From 32f945d304509a4e3f359f667caa4ccb3d473e19 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Mon, 8 May 2023 13:05:53 +0200 Subject: [PATCH 0040/2493] fix updating wrong samples --- .../Components/Timeline/NodeSamplePointPiece.cs | 4 ++-- .../Components/Timeline/SamplePointPiece.cs | 14 +++++++++----- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/NodeSamplePointPiece.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/NodeSamplePointPiece.cs index dc91401c13..437d3c3d3a 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/NodeSamplePointPiece.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/NodeSamplePointPiece.cs @@ -32,7 +32,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline return NodeIndex < hasRepeats.NodeSamples.Count ? hasRepeats.NodeSamples[NodeIndex] : HitObject.Samples; } - public override Popover GetPopover() => new NodeSampleEditPopover(HitObject); + public override Popover GetPopover() => new NodeSampleEditPopover(HitObject, NodeIndex); public partial class NodeSampleEditPopover : SampleEditPopover { @@ -44,7 +44,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline return nodeIndex < hasRepeats.NodeSamples.Count ? hasRepeats.NodeSamples[nodeIndex] : ho.Samples; } - public NodeSampleEditPopover(HitObject hitObject, int nodeIndex = 0) + public NodeSampleEditPopover(HitObject hitObject, int nodeIndex) : base(hitObject) { this.nodeIndex = nodeIndex; diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs index 064e96c255..50b1ec80ff 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs @@ -158,9 +158,11 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline foreach (var h in objects) { - for (int i = 0; i < h.Samples.Count; i++) + var samples = GetSamples(h); + + for (int i = 0; i < samples.Count; i++) { - h.Samples[i] = h.Samples[i].With(newBank: newBank); + samples[i] = samples[i].With(newBank: newBank); } beatmap.Update(h); @@ -171,7 +173,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline private void updateBankPlaceholderText(IEnumerable objects) { - string? commonBank = getCommonBank(objects.Select(h => h.Samples).ToArray()); + string? commonBank = getCommonBank(objects.Select(GetSamples).ToArray()); bank.PlaceholderText = string.IsNullOrEmpty(commonBank) ? "(multiple)" : string.Empty; } @@ -184,9 +186,11 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline foreach (var h in objects) { - for (int i = 0; i < h.Samples.Count; i++) + var samples = GetSamples(h); + + for (int i = 0; i < samples.Count; i++) { - h.Samples[i] = h.Samples[i].With(newVolume: newVolume.Value); + samples[i] = samples[i].With(newVolume: newVolume.Value); } beatmap.Update(h); From e945846759e16fb51d589d665accae195ab9957a Mon Sep 17 00:00:00 2001 From: OliBomby Date: Mon, 8 May 2023 13:10:24 +0200 Subject: [PATCH 0041/2493] Add node sample pieces to timeline blueprint --- .../Timeline/TimelineHitObjectBlueprint.cs | 28 ++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs index ea063e9216..e7c14fc53d 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs @@ -32,6 +32,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline private const float circle_size = 38; private Container? repeatsContainer; + private Container? nodeSamplesContainer; public Action? OnDragHandled = null!; @@ -49,6 +50,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline private readonly Border border; private readonly Container colouredComponents; + private readonly Container sampleComponents; private readonly OsuSpriteText comboIndexText; [Resolved] @@ -101,10 +103,15 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline }, } }, + sampleComponents = new Container + { + RelativeSizeAxes = Axes.Both, + }, new SamplePointPiece(Item) { Anchor = Anchor.BottomLeft, - Origin = Anchor.TopCentre + Origin = Anchor.TopCentre, + X = Item is IHasRepeats ? -10 : 0 }, }); @@ -233,6 +240,25 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline X = (float)(i + 1) / (repeats.RepeatCount + 1) }); } + + // Add node sample pieces + nodeSamplesContainer?.Expire(); + + sampleComponents.Add(nodeSamplesContainer = new Container + { + RelativeSizeAxes = Axes.Both, + }); + + for (int i = 0; i < repeats.RepeatCount + 2; i++) + { + nodeSamplesContainer.Add(new NodeSamplePointPiece(Item, i) + { + X = (float)i / (repeats.RepeatCount + 1), + RelativePositionAxes = Axes.X, + Anchor = Anchor.BottomLeft, + Origin = Anchor.TopCentre, + }); + } } protected override bool ShouldBeConsideredForInput(Drawable child) => true; From 3b5bae774259451c62c16a4202c6f4e963cb9fde Mon Sep 17 00:00:00 2001 From: OliBomby Date: Mon, 8 May 2023 13:16:30 +0200 Subject: [PATCH 0042/2493] Abbreviate common bank names on timeline --- .../Compose/Components/Timeline/SamplePointPiece.cs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs index 50b1ec80ff..1f3f5305b8 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs @@ -48,7 +48,18 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline private void updateText() { - Label.Text = $"{GetBankValue(GetSamples())} {GetVolumeValue(GetSamples())}"; + Label.Text = $"{abbreviateBank(GetBankValue(GetSamples()))} {GetVolumeValue(GetSamples())}"; + } + + private static string? abbreviateBank(string? bank) + { + return bank switch + { + "normal" => "N", + "soft" => "S", + "drum" => "D", + _ => bank + }; } public static string? GetBankValue(IEnumerable samples) From 88d840a60d143cd404c6f033380653abc7550055 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Mon, 8 May 2023 14:42:15 +0200 Subject: [PATCH 0043/2493] fix assigned hitsounds dont have bank or volume --- osu.Game/Rulesets/Objects/HitObject.cs | 4 ++-- .../Screens/Edit/Compose/Components/EditorSelectionHandler.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Rulesets/Objects/HitObject.cs b/osu.Game/Rulesets/Objects/HitObject.cs index a4cb976d50..352ee72962 100644 --- a/osu.Game/Rulesets/Objects/HitObject.cs +++ b/osu.Game/Rulesets/Objects/HitObject.cs @@ -210,10 +210,10 @@ namespace osu.Game.Rulesets.Objects /// /// The name of the sample. /// A populated . - protected HitSampleInfo GetSampleInfo(string sampleName = HitSampleInfo.HIT_NORMAL) + public HitSampleInfo GetSampleInfo(string sampleName = HitSampleInfo.HIT_NORMAL) { var hitnormalSample = Samples.FirstOrDefault(s => s.Name == HitSampleInfo.HIT_NORMAL); - return hitnormalSample == null ? new HitSampleInfo(sampleName) : hitnormalSample.With(newName: sampleName); + return hitnormalSample == null ? new HitSampleInfo(sampleName, SampleControlPoint.DEFAULT_BANK, volume: 100) : hitnormalSample.With(newName: sampleName); } } diff --git a/osu.Game/Screens/Edit/Compose/Components/EditorSelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/EditorSelectionHandler.cs index 357cc940f2..694b24c567 100644 --- a/osu.Game/Screens/Edit/Compose/Components/EditorSelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/EditorSelectionHandler.cs @@ -122,7 +122,7 @@ namespace osu.Game.Screens.Edit.Compose.Components if (h.Samples.Any(s => s.Name == sampleName)) return; - h.Samples.Add(new HitSampleInfo(sampleName)); + h.Samples.Add(h.GetSampleInfo(sampleName)); EditorBeatmap.Update(h); }); } From 4c365304351fdc3d9f62c6cab40d11e421be4ae3 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Mon, 8 May 2023 15:08:40 +0200 Subject: [PATCH 0044/2493] allow editing additions in sample point piece --- .../Components/ComposeBlueprintContainer.cs | 4 +- .../Compose/Components/SelectionHandler.cs | 2 +- .../Components/Timeline/SamplePointPiece.cs | 146 ++++++++++++++++++ 3 files changed, 149 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs index 453e4b9130..0a87314a2a 100644 --- a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs @@ -206,10 +206,10 @@ namespace osu.Game.Screens.Edit.Compose.Components yield return new TernaryButton(NewCombo, "New combo", () => new SpriteIcon { Icon = FontAwesome.Regular.DotCircle }); foreach (var kvp in SelectionHandler.SelectionSampleStates) - yield return new TernaryButton(kvp.Value, kvp.Key.Replace("hit", string.Empty).Titleize(), () => getIconForSample(kvp.Key)); + yield return new TernaryButton(kvp.Value, kvp.Key.Replace("hit", string.Empty).Titleize(), () => GetIconForSample(kvp.Key)); } - private Drawable getIconForSample(string sampleName) + public static Drawable GetIconForSample(string sampleName) { switch (sampleName) { diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs index 9e4fb26688..93d51c849e 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs @@ -279,7 +279,7 @@ namespace osu.Game.Screens.Edit.Compose.Components /// /// Given a selection target and a function of truth, retrieve the correct ternary state for display. /// - protected static TernaryState GetStateFromSelection(IEnumerable selection, Func func) + public static TernaryState GetStateFromSelection(IEnumerable selection, Func func) { if (selection.Any(func)) return selection.All(func) ? TernaryState.True : TernaryState.Indeterminate; diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs index 1f3f5305b8..0cac914e2c 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; +using Humanizer; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions; @@ -14,11 +15,14 @@ using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; using osu.Game.Audio; using osu.Game.Graphics; +using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Rulesets.Objects; +using osu.Game.Screens.Edit.Components.TernaryButtons; using osu.Game.Screens.Edit.Timing; using osuTK; using osuTK.Graphics; +using osuTK.Input; namespace osu.Game.Screens.Edit.Compose.Components.Timeline { @@ -83,6 +87,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline private LabelledTextBox bank = null!; private IndeterminateSliderWithTextBoxInput volume = null!; + private FillFlowContainer togglesCollection = null!; + protected virtual IList GetSamples(HitObject ho) => ho.Samples; [Resolved(canBeNull: true)] @@ -108,6 +114,13 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline Spacing = new Vector2(0, 10), Children = new Drawable[] { + togglesCollection = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(5, 5), + }, bank = new LabelledTextBox { Label = "Bank Name", @@ -149,6 +162,10 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline bank.OnCommit += (_, _) => bank.Current.Value = getCommonBank(relevantSamples); volume.Current.BindValueChanged(val => updateVolumeFor(relevantObjects, val.NewValue)); + + createStateBindables(relevantObjects); + updateTernaryStates(relevantObjects); + togglesCollection.AddRange(createTernaryButtons().Select(b => new DrawableTernaryButton(b) { RelativeSizeAxes = Axes.None, Size = new Vector2(40, 40) })); } protected override void LoadComplete() @@ -209,6 +226,135 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline beatmap.EndChange(); } + + #region hitsound toggles + + private readonly Dictionary> selectionSampleStates = new Dictionary>(); + + private void createStateBindables(IEnumerable objects) + { + foreach (string sampleName in HitSampleInfo.AllAdditions) + { + var bindable = new Bindable + { + Description = sampleName.Replace("hit", string.Empty).Titleize() + }; + + bindable.ValueChanged += state => + { + switch (state.NewValue) + { + case TernaryState.False: + removeHitSampleFor(objects, sampleName); + break; + + case TernaryState.True: + addHitSampleFor(objects, sampleName); + break; + } + }; + + selectionSampleStates[sampleName] = bindable; + } + } + + private void updateTernaryStates(IEnumerable objects) + { + foreach ((string sampleName, var bindable) in selectionSampleStates) + { + bindable.Value = SelectionHandler.GetStateFromSelection(objects, h => GetSamples(h).Any(s => s.Name == sampleName)); + } + } + + private IEnumerable createTernaryButtons() + { + foreach ((string sampleName, var bindable) in selectionSampleStates) + yield return new TernaryButton(bindable, string.Empty, () => ComposeBlueprintContainer.GetIconForSample(sampleName)); + } + + private void addHitSampleFor(IEnumerable objects, string sampleName) + { + if (string.IsNullOrEmpty(sampleName)) + return; + + beatmap.BeginChange(); + + foreach (var h in objects) + { + var samples = GetSamples(h); + + // Make sure there isn't already an existing sample + if (samples.Any(s => s.Name == sampleName)) + return; + + samples.Add(h.GetSampleInfo(sampleName)); + beatmap.Update(h); + } + + beatmap.EndChange(); + } + + private void removeHitSampleFor(IEnumerable objects, string sampleName) + { + if (string.IsNullOrEmpty(sampleName)) + return; + + beatmap.BeginChange(); + + foreach (var h in objects) + { + var samples = GetSamples(h); + + for (int i = 0; i < samples.Count; i++) + { + if (samples[i].Name == sampleName) + samples.RemoveAt(i--); + } + + beatmap.Update(h); + } + + beatmap.EndChange(); + } + + protected override bool OnKeyDown(KeyDownEvent e) + { + if (e.ControlPressed || e.AltPressed || e.SuperPressed || e.ShiftPressed || !checkRightToggleFromKey(e.Key, out int rightIndex)) + return base.OnKeyDown(e); + + var item = togglesCollection.ElementAtOrDefault(rightIndex); + + if (item is not DrawableTernaryButton button) return base.OnKeyDown(e); + + button.Button.Toggle(); + return true; + } + + private bool checkRightToggleFromKey(Key key, out int index) + { + switch (key) + { + case Key.W: + index = 0; + break; + + case Key.E: + index = 1; + break; + + case Key.R: + index = 2; + break; + + default: + index = -1; + break; + } + + return index >= 0; + } + + #endregion } } } From bb8285e2ef99cba43b0a95e2082841fc1a6af7a5 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Mon, 8 May 2023 15:14:25 +0200 Subject: [PATCH 0045/2493] cleanup code duplication --- .../Components/Timeline/SamplePointPiece.cs | 62 +++++++------------ 1 file changed, 23 insertions(+), 39 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs index 0cac914e2c..69fff8a8a7 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.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 Humanizer; @@ -177,28 +178,34 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline private static string? getCommonBank(IList[] relevantSamples) => relevantSamples.Select(GetBankValue).Distinct().Count() == 1 ? GetBankValue(relevantSamples.First()) : null; private static int? getCommonVolume(IList[] relevantSamples) => relevantSamples.Select(GetVolumeValue).Distinct().Count() == 1 ? GetVolumeValue(relevantSamples.First()) : null; - private void updateBankFor(IEnumerable objects, string? newBank) + private void updateFor(IEnumerable objects, Action> updateAction) { - if (string.IsNullOrEmpty(newBank)) - return; - beatmap.BeginChange(); foreach (var h in objects) { var samples = GetSamples(h); - - for (int i = 0; i < samples.Count; i++) - { - samples[i] = samples[i].With(newBank: newBank); - } - + updateAction(h, samples); beatmap.Update(h); } beatmap.EndChange(); } + private void updateBankFor(IEnumerable objects, string? newBank) + { + if (string.IsNullOrEmpty(newBank)) + return; + + updateFor(objects, (_, samples) => + { + for (int i = 0; i < samples.Count; i++) + { + samples[i] = samples[i].With(newBank: newBank); + } + }); + } + private void updateBankPlaceholderText(IEnumerable objects) { string? commonBank = getCommonBank(objects.Select(GetSamples).ToArray()); @@ -210,21 +217,13 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline if (newVolume == null) return; - beatmap.BeginChange(); - - foreach (var h in objects) + updateFor(objects, (_, samples) => { - var samples = GetSamples(h); - for (int i = 0; i < samples.Count; i++) { samples[i] = samples[i].With(newVolume: newVolume.Value); } - - beatmap.Update(h); - } - - beatmap.EndChange(); + }); } #region hitsound toggles @@ -277,21 +276,14 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline if (string.IsNullOrEmpty(sampleName)) return; - beatmap.BeginChange(); - - foreach (var h in objects) + updateFor(objects, (h, samples) => { - var samples = GetSamples(h); - // Make sure there isn't already an existing sample if (samples.Any(s => s.Name == sampleName)) return; samples.Add(h.GetSampleInfo(sampleName)); - beatmap.Update(h); - } - - beatmap.EndChange(); + }); } private void removeHitSampleFor(IEnumerable objects, string sampleName) @@ -299,22 +291,14 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline if (string.IsNullOrEmpty(sampleName)) return; - beatmap.BeginChange(); - - foreach (var h in objects) + updateFor(objects, (_, samples) => { - var samples = GetSamples(h); - for (int i = 0; i < samples.Count; i++) { if (samples[i].Name == sampleName) samples.RemoveAt(i--); } - - beatmap.Update(h); - } - - beatmap.EndChange(); + }); } protected override bool OnKeyDown(KeyDownEvent e) From 7260dcac60f00b48489d3a100c907355d45f38f3 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Mon, 8 May 2023 15:57:30 +0200 Subject: [PATCH 0046/2493] fix crash on multiselect and node sample piece popup --- .../Edit/Compose/Components/Timeline/NodeSamplePointPiece.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/NodeSamplePointPiece.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/NodeSamplePointPiece.cs index 437d3c3d3a..de43e16e55 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/NodeSamplePointPiece.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/NodeSamplePointPiece.cs @@ -40,7 +40,9 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline protected override IList GetSamples(HitObject ho) { - var hasRepeats = (IHasRepeats)ho; + if (ho is not IHasRepeats hasRepeats) + return ho.Samples; + return nodeIndex < hasRepeats.NodeSamples.Count ? hasRepeats.NodeSamples[nodeIndex] : ho.Samples; } From dd0fceaec69eb322b75851ccfd17a0ddc97e1346 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Mon, 8 May 2023 16:12:03 +0200 Subject: [PATCH 0047/2493] add addition bank --- .../Components/EditorSelectionHandler.cs | 3 +- .../Components/Timeline/SamplePointPiece.cs | 53 ++++++++++++++++++- 2 files changed, 53 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/EditorSelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/EditorSelectionHandler.cs index 694b24c567..71a01d9988 100644 --- a/osu.Game/Screens/Edit/Compose/Components/EditorSelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/EditorSelectionHandler.cs @@ -122,7 +122,8 @@ namespace osu.Game.Screens.Edit.Compose.Components if (h.Samples.Any(s => s.Name == sampleName)) return; - h.Samples.Add(h.GetSampleInfo(sampleName)); + var relevantSample = h.Samples.FirstOrDefault(s => s.Name != HitSampleInfo.HIT_NORMAL); + h.Samples.Add(relevantSample?.With(sampleName) ?? h.GetSampleInfo(sampleName)); EditorBeatmap.Update(h); }); } diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs index 69fff8a8a7..a6a7a59793 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs @@ -69,7 +69,12 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline public static string? GetBankValue(IEnumerable samples) { - return samples.FirstOrDefault()?.Bank; + return samples.FirstOrDefault(o => o.Name == HitSampleInfo.HIT_NORMAL)?.Bank; + } + + public static string? GetAdditionBankValue(IEnumerable samples) + { + return samples.FirstOrDefault(o => o.Name != HitSampleInfo.HIT_NORMAL)?.Bank ?? GetBankValue(samples); } public static int GetVolumeValue(ICollection samples) @@ -86,6 +91,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline private readonly HitObject hitObject; private LabelledTextBox bank = null!; + private LabelledTextBox additionBank = null!; private IndeterminateSliderWithTextBoxInput volume = null!; private FillFlowContainer togglesCollection = null!; @@ -126,6 +132,10 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline { Label = "Bank Name", }, + additionBank = new LabelledTextBox + { + Label = "Addition Bank", + }, volume = new IndeterminateSliderWithTextBoxInput("Volume", new BindableInt(100) { MinValue = 0, @@ -136,6 +146,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline }; bank.TabbableContentContainer = flow; + additionBank.TabbableContentContainer = flow; volume.TabbableContentContainer = flow; // if the piece belongs to a currently selected object, assume that the user wants to change all selected objects. @@ -148,6 +159,10 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline if (!string.IsNullOrEmpty(commonBank)) bank.Current.Value = commonBank; + string? commonAdditionBank = getCommonAdditionBank(relevantSamples); + if (!string.IsNullOrEmpty(commonAdditionBank)) + additionBank.Current.Value = commonAdditionBank; + int? commonVolume = getCommonVolume(relevantSamples); if (commonVolume != null) volume.Current.Value = commonVolume.Value; @@ -162,6 +177,14 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline // this ensures that committing empty text causes a revert to the previous value. bank.OnCommit += (_, _) => bank.Current.Value = getCommonBank(relevantSamples); + updateAdditionBankPlaceholderText(relevantObjects); + additionBank.Current.BindValueChanged(val => + { + updateAdditionBankFor(relevantObjects, val.NewValue); + updateAdditionBankPlaceholderText(relevantObjects); + }); + additionBank.OnCommit += (_, _) => additionBank.Current.Value = getCommonAdditionBank(relevantSamples); + volume.Current.BindValueChanged(val => updateVolumeFor(relevantObjects, val.NewValue)); createStateBindables(relevantObjects); @@ -176,6 +199,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline } private static string? getCommonBank(IList[] relevantSamples) => relevantSamples.Select(GetBankValue).Distinct().Count() == 1 ? GetBankValue(relevantSamples.First()) : null; + private static string? getCommonAdditionBank(IList[] relevantSamples) => relevantSamples.Select(GetAdditionBankValue).Distinct().Count() == 1 ? GetAdditionBankValue(relevantSamples.First()) : null; private static int? getCommonVolume(IList[] relevantSamples) => relevantSamples.Select(GetVolumeValue).Distinct().Count() == 1 ? GetVolumeValue(relevantSamples.First()) : null; private void updateFor(IEnumerable objects, Action> updateAction) @@ -201,6 +225,24 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline { for (int i = 0; i < samples.Count; i++) { + if (samples[i].Name != HitSampleInfo.HIT_NORMAL) continue; + + samples[i] = samples[i].With(newBank: newBank); + } + }); + } + + private void updateAdditionBankFor(IEnumerable objects, string? newBank) + { + if (string.IsNullOrEmpty(newBank)) + return; + + updateFor(objects, (_, samples) => + { + for (int i = 0; i < samples.Count; i++) + { + if (samples[i].Name == HitSampleInfo.HIT_NORMAL) continue; + samples[i] = samples[i].With(newBank: newBank); } }); @@ -212,6 +254,12 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline bank.PlaceholderText = string.IsNullOrEmpty(commonBank) ? "(multiple)" : string.Empty; } + private void updateAdditionBankPlaceholderText(IEnumerable objects) + { + string? commonAdditionBank = getCommonAdditionBank(objects.Select(GetSamples).ToArray()); + additionBank.PlaceholderText = string.IsNullOrEmpty(commonAdditionBank) ? "(multiple)" : string.Empty; + } + private void updateVolumeFor(IEnumerable objects, int? newVolume) { if (newVolume == null) @@ -282,7 +330,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline if (samples.Any(s => s.Name == sampleName)) return; - samples.Add(h.GetSampleInfo(sampleName)); + var relevantSample = samples.FirstOrDefault(s => s.Name != HitSampleInfo.HIT_NORMAL) ?? samples.FirstOrDefault(); + samples.Add(relevantSample?.With(sampleName) ?? h.GetSampleInfo(sampleName)); }); } From 114f12a79056cc20951d06aec1ade073899d684a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 30 May 2023 14:04:02 +0900 Subject: [PATCH 0048/2493] Adjust `CreateHitSampleInfo` to handle additions correctly, rather than implementing locally --- osu.Game/Rulesets/Objects/HitObject.cs | 12 ++++++++++-- .../Compose/Components/EditorSelectionHandler.cs | 6 ++---- .../Compose/Components/Timeline/SamplePointPiece.cs | 3 +-- 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/osu.Game/Rulesets/Objects/HitObject.cs b/osu.Game/Rulesets/Objects/HitObject.cs index ed3d3a6eb2..e87fb56b73 100644 --- a/osu.Game/Rulesets/Objects/HitObject.cs +++ b/osu.Game/Rulesets/Objects/HitObject.cs @@ -216,8 +216,16 @@ namespace osu.Game.Rulesets.Objects /// A populated . public HitSampleInfo CreateHitSampleInfo(string sampleName = HitSampleInfo.HIT_NORMAL) { - if (Samples.FirstOrDefault(s => s.Name == HitSampleInfo.HIT_NORMAL) is HitSampleInfo existingSample) - return existingSample.With(newName: sampleName); + // As per stable, all non-normal "addition" samples should use the same bank. + if (sampleName != HitSampleInfo.HIT_NORMAL) + { + if (Samples.FirstOrDefault(s => s.Name != HitSampleInfo.HIT_NORMAL) is HitSampleInfo existingAddition) + return existingAddition.With(newName: sampleName); + } + + // Fall back to using the normal sample bank otherwise. + if (Samples.FirstOrDefault(s => s.Name == HitSampleInfo.HIT_NORMAL) is HitSampleInfo existingNormal) + return existingNormal.With(newName: sampleName); return new HitSampleInfo(sampleName); } diff --git a/osu.Game/Screens/Edit/Compose/Components/EditorSelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/EditorSelectionHandler.cs index 07622e4385..9bf6cfffe6 100644 --- a/osu.Game/Screens/Edit/Compose/Components/EditorSelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/EditorSelectionHandler.cs @@ -222,12 +222,10 @@ namespace osu.Game.Screens.Edit.Compose.Components if (h.Samples.Any(s => s.Name == sampleName)) return; - var existingNonNormalSample = h.Samples.FirstOrDefault(s => s.Name != HitSampleInfo.HIT_NORMAL); - var sampleToAdd = h.CreateHitSampleInfo(sampleName); - h.Samples.Add(existingNonNormalSample?.With(sampleName) ?? h.GetSampleInfo(sampleName)); - h.Samples.Add(h.CreateHitSampleInfo(sampleName)); + h.Samples.Add(sampleToAdd); + EditorBeatmap.Update(h); }); } diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs index a6a7a59793..64330e354a 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs @@ -330,8 +330,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline if (samples.Any(s => s.Name == sampleName)) return; - var relevantSample = samples.FirstOrDefault(s => s.Name != HitSampleInfo.HIT_NORMAL) ?? samples.FirstOrDefault(); - samples.Add(relevantSample?.With(sampleName) ?? h.GetSampleInfo(sampleName)); + samples.Add(h.CreateHitSampleInfo(sampleName)); }); } From 7a46b7b96177a234622031c77f51b632c816750d Mon Sep 17 00:00:00 2001 From: OliBomby Date: Wed, 31 May 2023 14:33:06 +0200 Subject: [PATCH 0049/2493] Invert colors --- .../Edit/Compose/Components/Timeline/NodeSamplePointPiece.cs | 4 ---- .../Edit/Compose/Components/Timeline/SamplePointPiece.cs | 4 +++- .../Components/Timeline/TimelineHitObjectBlueprint.cs | 5 +++-- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/NodeSamplePointPiece.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/NodeSamplePointPiece.cs index de43e16e55..f168fb791f 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/NodeSamplePointPiece.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/NodeSamplePointPiece.cs @@ -4,10 +4,8 @@ using System.Collections.Generic; using osu.Framework.Graphics.UserInterface; using osu.Game.Audio; -using osu.Game.Graphics; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; -using osuTK.Graphics; namespace osu.Game.Screens.Edit.Compose.Components.Timeline { @@ -24,8 +22,6 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline NodeIndex = nodeIndex; } - protected override Color4 GetRepresentingColour(OsuColour colours) => colours.Purple; - protected override IList GetSamples() { var hasRepeats = (IHasRepeats)HitObject; diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs index 64330e354a..61004aae88 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs @@ -36,7 +36,9 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline HitObject = hitObject; } - protected override Color4 GetRepresentingColour(OsuColour colours) => colours.Pink; + public bool AlternativeColor { get; init; } + + protected override Color4 GetRepresentingColour(OsuColour colours) => AlternativeColor ? colours.Purple : colours.Pink; [BackgroundDependencyLoader] private void load() diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs index e7c14fc53d..ddac3bb667 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs @@ -111,7 +111,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline { Anchor = Anchor.BottomLeft, Origin = Anchor.TopCentre, - X = Item is IHasRepeats ? -10 : 0 + X = Item is IHasRepeats ? -10 : 0, + AlternativeColor = Item is IHasRepeats }, }); @@ -256,7 +257,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline X = (float)i / (repeats.RepeatCount + 1), RelativePositionAxes = Axes.X, Anchor = Anchor.BottomLeft, - Origin = Anchor.TopCentre, + Origin = Anchor.TopCentre }); } } From b7bc49b1f4ea438daa1e313b7cface55f62c84a7 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Wed, 31 May 2023 16:28:43 +0200 Subject: [PATCH 0050/2493] Fix regressed bank inheriting behaviour on node samples --- .../Edit/Compose/Components/Timeline/SamplePointPiece.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs index 61004aae88..b52463d8f3 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs @@ -332,7 +332,9 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline if (samples.Any(s => s.Name == sampleName)) return; - samples.Add(h.CreateHitSampleInfo(sampleName)); + // First try inheriting the sample info from the node samples instead of the samples of the hitobject + var relevantSample = samples.FirstOrDefault(s => s.Name != HitSampleInfo.HIT_NORMAL) ?? samples.FirstOrDefault(); + samples.Add(relevantSample?.With(sampleName) ?? h.CreateHitSampleInfo(sampleName)); }); } From fede432969fbf202f0a0d702ebedc02ab1e66a34 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Wed, 31 May 2023 20:00:19 +0200 Subject: [PATCH 0051/2493] Make relevantObject and relevantSamples instance variables --- .../Components/Timeline/SamplePointPiece.cs | 100 ++++++++++-------- 1 file changed, 57 insertions(+), 43 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs index b52463d8f3..bbc5380e70 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs @@ -98,6 +98,9 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline private FillFlowContainer togglesCollection = null!; + private HitObject[] relevantObjects = null!; + private IList[] relevantSamples = null!; + protected virtual IList GetSamples(HitObject ho) => ho.Samples; [Resolved(canBeNull: true)] @@ -153,44 +156,41 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline // if the piece belongs to a currently selected object, assume that the user wants to change all selected objects. // if the piece belongs to an unselected object, operate on that object alone, independently of the selection. - var relevantObjects = (beatmap.SelectedHitObjects.Contains(hitObject) ? beatmap.SelectedHitObjects : hitObject.Yield()).ToArray(); - var relevantSamples = relevantObjects.Select(GetSamples).ToArray(); + relevantObjects = (beatmap.SelectedHitObjects.Contains(hitObject) ? beatmap.SelectedHitObjects : hitObject.Yield()).ToArray(); + relevantSamples = relevantObjects.Select(GetSamples).ToArray(); // even if there are multiple objects selected, we can still display sample volume or bank if they all have the same value. - string? commonBank = getCommonBank(relevantSamples); + string? commonBank = getCommonBank(); if (!string.IsNullOrEmpty(commonBank)) bank.Current.Value = commonBank; - string? commonAdditionBank = getCommonAdditionBank(relevantSamples); - if (!string.IsNullOrEmpty(commonAdditionBank)) - additionBank.Current.Value = commonAdditionBank; - - int? commonVolume = getCommonVolume(relevantSamples); + int? commonVolume = getCommonVolume(); if (commonVolume != null) volume.Current.Value = commonVolume.Value; - updateBankPlaceholderText(relevantObjects); + updateBankPlaceholderText(); bank.Current.BindValueChanged(val => { - updateBankFor(relevantObjects, val.NewValue); - updateBankPlaceholderText(relevantObjects); + updateBank(val.NewValue); + updateBankPlaceholderText(); }); // on commit, ensure that the value is correct by sourcing it from the objects' samples again. // this ensures that committing empty text causes a revert to the previous value. - bank.OnCommit += (_, _) => bank.Current.Value = getCommonBank(relevantSamples); + bank.OnCommit += (_, _) => bank.Current.Value = getCommonBank(); - updateAdditionBankPlaceholderText(relevantObjects); + updateAdditionBankPlaceholderText(); + updateAdditionBankText(); additionBank.Current.BindValueChanged(val => { - updateAdditionBankFor(relevantObjects, val.NewValue); - updateAdditionBankPlaceholderText(relevantObjects); + updateAdditionBank(val.NewValue); + updateAdditionBankPlaceholderText(); }); - additionBank.OnCommit += (_, _) => additionBank.Current.Value = getCommonAdditionBank(relevantSamples); + additionBank.OnCommit += (_, _) => updateAdditionBankText(); - volume.Current.BindValueChanged(val => updateVolumeFor(relevantObjects, val.NewValue)); + volume.Current.BindValueChanged(val => updateVolume(val.NewValue)); - createStateBindables(relevantObjects); - updateTernaryStates(relevantObjects); + createStateBindables(); + updateTernaryStates(); togglesCollection.AddRange(createTernaryButtons().Select(b => new DrawableTernaryButton(b) { RelativeSizeAxes = Axes.None, Size = new Vector2(40, 40) })); } @@ -200,15 +200,15 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline ScheduleAfterChildren(() => GetContainingInputManager().ChangeFocus(volume)); } - private static string? getCommonBank(IList[] relevantSamples) => relevantSamples.Select(GetBankValue).Distinct().Count() == 1 ? GetBankValue(relevantSamples.First()) : null; - private static string? getCommonAdditionBank(IList[] relevantSamples) => relevantSamples.Select(GetAdditionBankValue).Distinct().Count() == 1 ? GetAdditionBankValue(relevantSamples.First()) : null; - private static int? getCommonVolume(IList[] relevantSamples) => relevantSamples.Select(GetVolumeValue).Distinct().Count() == 1 ? GetVolumeValue(relevantSamples.First()) : null; + private string? getCommonBank() => relevantSamples.Select(GetBankValue).Distinct().Count() == 1 ? GetBankValue(relevantSamples.First()) : null; + private string? getCommonAdditionBank() => relevantSamples.Select(GetAdditionBankValue).Distinct().Count() == 1 ? GetAdditionBankValue(relevantSamples.First()) : null; + private int? getCommonVolume() => relevantSamples.Select(GetVolumeValue).Distinct().Count() == 1 ? GetVolumeValue(relevantSamples.First()) : null; - private void updateFor(IEnumerable objects, Action> updateAction) + private void update(Action> updateAction) { beatmap.BeginChange(); - foreach (var h in objects) + foreach (var h in relevantObjects) { var samples = GetSamples(h); updateAction(h, samples); @@ -218,12 +218,12 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline beatmap.EndChange(); } - private void updateBankFor(IEnumerable objects, string? newBank) + private void updateBank(string? newBank) { if (string.IsNullOrEmpty(newBank)) return; - updateFor(objects, (_, samples) => + update((_, samples) => { for (int i = 0; i < samples.Count; i++) { @@ -234,12 +234,12 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline }); } - private void updateAdditionBankFor(IEnumerable objects, string? newBank) + private void updateAdditionBank(string? newBank) { if (string.IsNullOrEmpty(newBank)) return; - updateFor(objects, (_, samples) => + update((_, samples) => { for (int i = 0; i < samples.Count; i++) { @@ -250,24 +250,34 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline }); } - private void updateBankPlaceholderText(IEnumerable objects) + private void updateBankPlaceholderText() { - string? commonBank = getCommonBank(objects.Select(GetSamples).ToArray()); + string? commonBank = getCommonBank(); bank.PlaceholderText = string.IsNullOrEmpty(commonBank) ? "(multiple)" : string.Empty; } - private void updateAdditionBankPlaceholderText(IEnumerable objects) + private void updateAdditionBankPlaceholderText() { - string? commonAdditionBank = getCommonAdditionBank(objects.Select(GetSamples).ToArray()); + string? commonAdditionBank = getCommonAdditionBank(); additionBank.PlaceholderText = string.IsNullOrEmpty(commonAdditionBank) ? "(multiple)" : string.Empty; } - private void updateVolumeFor(IEnumerable objects, int? newVolume) + private void updateAdditionBankText() + { + if (additionBank.Current.Disabled) return; + + string? commonAdditionBank = getCommonAdditionBank(); + if (string.IsNullOrEmpty(commonAdditionBank)) return; + + additionBank.Current.Value = commonAdditionBank; + } + + private void updateVolume(int? newVolume) { if (newVolume == null) return; - updateFor(objects, (_, samples) => + update((_, samples) => { for (int i = 0; i < samples.Count; i++) { @@ -280,7 +290,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline private readonly Dictionary> selectionSampleStates = new Dictionary>(); - private void createStateBindables(IEnumerable objects) + private void createStateBindables() { foreach (string sampleName in HitSampleInfo.AllAdditions) { @@ -294,11 +304,11 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline switch (state.NewValue) { case TernaryState.False: - removeHitSampleFor(objects, sampleName); + removeHitSample(sampleName); break; case TernaryState.True: - addHitSampleFor(objects, sampleName); + addHitSample(sampleName); break; } }; @@ -307,11 +317,11 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline } } - private void updateTernaryStates(IEnumerable objects) + private void updateTernaryStates() { foreach ((string sampleName, var bindable) in selectionSampleStates) { - bindable.Value = SelectionHandler.GetStateFromSelection(objects, h => GetSamples(h).Any(s => s.Name == sampleName)); + bindable.Value = SelectionHandler.GetStateFromSelection(relevantObjects, h => GetSamples(h).Any(s => s.Name == sampleName)); } } @@ -321,12 +331,12 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline yield return new TernaryButton(bindable, string.Empty, () => ComposeBlueprintContainer.GetIconForSample(sampleName)); } - private void addHitSampleFor(IEnumerable objects, string sampleName) + private void addHitSample(string sampleName) { if (string.IsNullOrEmpty(sampleName)) return; - updateFor(objects, (h, samples) => + update((h, samples) => { // Make sure there isn't already an existing sample if (samples.Any(s => s.Name == sampleName)) @@ -336,14 +346,16 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline var relevantSample = samples.FirstOrDefault(s => s.Name != HitSampleInfo.HIT_NORMAL) ?? samples.FirstOrDefault(); samples.Add(relevantSample?.With(sampleName) ?? h.CreateHitSampleInfo(sampleName)); }); + + updateAdditionBankText(); } - private void removeHitSampleFor(IEnumerable objects, string sampleName) + private void removeHitSample(string sampleName) { if (string.IsNullOrEmpty(sampleName)) return; - updateFor(objects, (_, samples) => + update((_, samples) => { for (int i = 0; i < samples.Count; i++) { @@ -351,6 +363,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline samples.RemoveAt(i--); } }); + + updateAdditionBankText(); } protected override bool OnKeyDown(KeyDownEvent e) From 9e78a6b34e0deb25a4933b14d14f16cd91165bfd Mon Sep 17 00:00:00 2001 From: OliBomby Date: Wed, 31 May 2023 20:00:45 +0200 Subject: [PATCH 0052/2493] hide addition bank field when no additions active --- .../Compose/Components/Timeline/SamplePointPiece.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs index bbc5380e70..37a6fa8a22 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs @@ -180,6 +180,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline updateAdditionBankPlaceholderText(); updateAdditionBankText(); + updateAdditionBankActivated(); additionBank.Current.BindValueChanged(val => { updateAdditionBank(val.NewValue); @@ -262,6 +263,15 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline additionBank.PlaceholderText = string.IsNullOrEmpty(commonAdditionBank) ? "(multiple)" : string.Empty; } + private void updateAdditionBankActivated() + { + bool anyAdditions = relevantSamples.Any(o => o.Any(s => s.Name != HitSampleInfo.HIT_NORMAL)); + if (anyAdditions) + additionBank.Show(); + else + additionBank.Hide(); + } + private void updateAdditionBankText() { if (additionBank.Current.Disabled) return; @@ -347,6 +357,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline samples.Add(relevantSample?.With(sampleName) ?? h.CreateHitSampleInfo(sampleName)); }); + updateAdditionBankActivated(); updateAdditionBankText(); } @@ -365,6 +376,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline }); updateAdditionBankText(); + updateAdditionBankActivated(); } protected override bool OnKeyDown(KeyDownEvent e) From 8acfe6b58b1de2de3466315fc74b000a428860c1 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Thu, 1 Jun 2023 07:41:30 +0200 Subject: [PATCH 0053/2493] Add PopoverContainer to TimelineTestScene --- .../Visual/Editing/TimelineTestScene.cs | 36 +++++++++++-------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TimelineTestScene.cs b/osu.Game.Tests/Visual/Editing/TimelineTestScene.cs index cb45ad5a07..afec75e948 100644 --- a/osu.Game.Tests/Visual/Editing/TimelineTestScene.cs +++ b/osu.Game.Tests/Visual/Editing/TimelineTestScene.cs @@ -9,6 +9,7 @@ using osu.Framework.Audio; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Shapes; using osu.Framework.Testing; using osu.Game.Beatmaps; @@ -51,28 +52,35 @@ namespace osu.Game.Tests.Visual.Editing Composer.Alpha = 0; - Add(new OsuContextMenuContainer + Add(new PopoverContainer { RelativeSizeAxes = Axes.Both, Children = new Drawable[] { - EditorBeatmap, - Composer, - new FillFlowContainer + new OsuContextMenuContainer { - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Vertical, - Spacing = new Vector2(0, 5), + RelativeSizeAxes = Axes.Both, Children = new Drawable[] { - new StartStopButton(), - new AudioVisualiser(), + EditorBeatmap, + Composer, + new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0, 5), + Children = new Drawable[] + { + new StartStopButton(), + new AudioVisualiser(), + } + }, + TimelineArea = new TimelineArea(CreateTestComponent()) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + } } - }, - TimelineArea = new TimelineArea(CreateTestComponent()) - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, } } }); From 2812b2345779e6d9af397b0af0f4f30dff4e864b Mon Sep 17 00:00:00 2001 From: OliBomby Date: Thu, 1 Jun 2023 08:03:41 +0200 Subject: [PATCH 0054/2493] Add sample point piece test --- .../TestSceneTimelineHitObjectBlueprint.cs | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneTimelineHitObjectBlueprint.cs b/osu.Game.Tests/Visual/Editing/TestSceneTimelineHitObjectBlueprint.cs index 08e036248b..61bfaad64c 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneTimelineHitObjectBlueprint.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneTimelineHitObjectBlueprint.cs @@ -8,10 +8,14 @@ using NUnit.Framework; using osu.Framework.Graphics; using osu.Framework.Graphics.UserInterface; using osu.Framework.Testing; +using osu.Game.Audio; using osu.Game.Graphics.UserInterface; +using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Screens.Edit.Components.TernaryButtons; using osu.Game.Screens.Edit.Compose.Components.Timeline; +using osu.Game.Screens.Edit.Timing; using osuTK; using osuTK.Input; using static osu.Game.Screens.Edit.Compose.Components.Timeline.TimelineHitObjectBlueprint; @@ -111,5 +115,73 @@ namespace osu.Game.Tests.Visual.Editing AddAssert("object has zero repeats", () => EditorBeatmap.HitObjects.OfType().Single().RepeatCount == 0); } + + [Test] + public void TestSamplePointPiece() + { + SamplePointPiece samplePointPiece; + SamplePointPiece.SampleEditPopover popover = null!; + + AddStep("add circle", () => + { + EditorBeatmap.Clear(); + EditorBeatmap.Add(new HitCircle + { + Position = new Vector2(256, 256), + StartTime = 2700, + Samples = + { + new HitSampleInfo(HitSampleInfo.HIT_NORMAL) + } + }); + }); + + AddStep("open hitsound popover", () => + { + samplePointPiece = this.ChildrenOfType().Single(); + InputManager.MoveMouseTo(samplePointPiece); + InputManager.PressButton(MouseButton.Left); + InputManager.ReleaseButton(MouseButton.Left); + }); + + AddStep("add whistle addition", () => + { + popover = this.ChildrenOfType().First(); + var whistleTernaryButton = popover.ChildrenOfType().First(); + InputManager.MoveMouseTo(whistleTernaryButton); + InputManager.PressButton(MouseButton.Left); + InputManager.ReleaseButton(MouseButton.Left); + }); + + AddAssert("has whistle sample", () => EditorBeatmap.HitObjects.First().Samples.Any(o => o.Name == HitSampleInfo.HIT_WHISTLE)); + + AddStep("change bank name", () => + { + var bankTextBox = popover.ChildrenOfType().First(); + bankTextBox.Current.Value = "soft"; + }); + + AddAssert("bank name changed", () => + EditorBeatmap.HitObjects.First().Samples.Where(o => o.Name == HitSampleInfo.HIT_NORMAL).All(o => o.Bank == "soft") + && EditorBeatmap.HitObjects.First().Samples.Where(o => o.Name != HitSampleInfo.HIT_NORMAL).All(o => o.Bank == "normal")); + + AddStep("change addition bank name", () => + { + var bankTextBox = popover.ChildrenOfType().ToArray()[1]; + bankTextBox.Current.Value = "drum"; + }); + + AddAssert("addition bank name changed", () => + EditorBeatmap.HitObjects.First().Samples.Where(o => o.Name == HitSampleInfo.HIT_NORMAL).All(o => o.Bank == "soft") + && EditorBeatmap.HitObjects.First().Samples.Where(o => o.Name != HitSampleInfo.HIT_NORMAL).All(o => o.Bank == "drum")); + + AddStep("change volume", () => + { + var bankTextBox = popover.ChildrenOfType>().Single(); + bankTextBox.Current.Value = 30; + }); + + AddAssert("volume changed", () => EditorBeatmap.HitObjects.First().Samples.All(o => o.Volume == 30)); + } } } From aa52d86ea3304c0cf6922ef1e584fc4a6244cb39 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Thu, 1 Jun 2023 08:12:39 +0200 Subject: [PATCH 0055/2493] add nodesample piece test --- .../TestSceneTimelineHitObjectBlueprint.cs | 77 +++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneTimelineHitObjectBlueprint.cs b/osu.Game.Tests/Visual/Editing/TestSceneTimelineHitObjectBlueprint.cs index 61bfaad64c..6524722687 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneTimelineHitObjectBlueprint.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneTimelineHitObjectBlueprint.cs @@ -3,6 +3,7 @@ #nullable disable +using System.Collections.Generic; using System.Linq; using NUnit.Framework; using osu.Framework.Graphics; @@ -11,6 +12,7 @@ using osu.Framework.Testing; using osu.Game.Audio; using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterfaceV2; +using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Screens.Edit.Components.TernaryButtons; @@ -183,5 +185,80 @@ namespace osu.Game.Tests.Visual.Editing AddAssert("volume changed", () => EditorBeatmap.HitObjects.First().Samples.All(o => o.Volume == 30)); } + + [Test] + public void TestNodeSamplePointPiece() + { + Slider slider = null!; + SamplePointPiece samplePointPiece; + SamplePointPiece.SampleEditPopover popover = null!; + + AddStep("add slider", () => + { + EditorBeatmap.Clear(); + EditorBeatmap.Add(slider = new Slider + { + Position = new Vector2(256, 256), + StartTime = 2700, + Path = new SliderPath(new[] { new PathControlPoint(Vector2.Zero), new PathControlPoint(new Vector2(250, 0)) }), + Samples = + { + new HitSampleInfo(HitSampleInfo.HIT_NORMAL) + }, + NodeSamples = + { + new List { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) }, + new List { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) }, + } + }); + }); + + AddStep("open slider end hitsound popover", () => + { + samplePointPiece = this.ChildrenOfType().Last(); + InputManager.MoveMouseTo(samplePointPiece); + InputManager.PressButton(MouseButton.Left); + InputManager.ReleaseButton(MouseButton.Left); + }); + + AddStep("add whistle addition", () => + { + popover = this.ChildrenOfType().First(); + var whistleTernaryButton = popover.ChildrenOfType().First(); + InputManager.MoveMouseTo(whistleTernaryButton); + InputManager.PressButton(MouseButton.Left); + InputManager.ReleaseButton(MouseButton.Left); + }); + + AddAssert("has whistle sample", () => slider.NodeSamples[1].Any(o => o.Name == HitSampleInfo.HIT_WHISTLE)); + + AddStep("change bank name", () => + { + var bankTextBox = popover.ChildrenOfType().First(); + bankTextBox.Current.Value = "soft"; + }); + + AddAssert("bank name changed", () => + slider.NodeSamples[1].Where(o => o.Name == HitSampleInfo.HIT_NORMAL).All(o => o.Bank == "soft") + && slider.NodeSamples[1].Where(o => o.Name != HitSampleInfo.HIT_NORMAL).All(o => o.Bank == "normal")); + + AddStep("change addition bank name", () => + { + var bankTextBox = popover.ChildrenOfType().ToArray()[1]; + bankTextBox.Current.Value = "drum"; + }); + + AddAssert("addition bank name changed", () => + slider.NodeSamples[1].Where(o => o.Name == HitSampleInfo.HIT_NORMAL).All(o => o.Bank == "soft") + && slider.NodeSamples[1].Where(o => o.Name != HitSampleInfo.HIT_NORMAL).All(o => o.Bank == "drum")); + + AddStep("change volume", () => + { + var bankTextBox = popover.ChildrenOfType>().Single(); + bankTextBox.Current.Value = 30; + }); + + AddAssert("volume changed", () => slider.NodeSamples[1].All(o => o.Volume == 30)); + } } } From e70939005257e6b853117edbf3f7aa504f3d273e Mon Sep 17 00:00:00 2001 From: OliBomby Date: Thu, 1 Jun 2023 09:00:59 +0200 Subject: [PATCH 0056/2493] reset popover at the end of sample tests --- .../TestSceneTimelineHitObjectBlueprint.cs | 22 +++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneTimelineHitObjectBlueprint.cs b/osu.Game.Tests/Visual/Editing/TestSceneTimelineHitObjectBlueprint.cs index 6524722687..4410ae4112 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneTimelineHitObjectBlueprint.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneTimelineHitObjectBlueprint.cs @@ -121,7 +121,6 @@ namespace osu.Game.Tests.Visual.Editing [Test] public void TestSamplePointPiece() { - SamplePointPiece samplePointPiece; SamplePointPiece.SampleEditPopover popover = null!; AddStep("add circle", () => @@ -140,7 +139,7 @@ namespace osu.Game.Tests.Visual.Editing AddStep("open hitsound popover", () => { - samplePointPiece = this.ChildrenOfType().Single(); + var samplePointPiece = this.ChildrenOfType().Single(); InputManager.MoveMouseTo(samplePointPiece); InputManager.PressButton(MouseButton.Left); InputManager.ReleaseButton(MouseButton.Left); @@ -184,13 +183,20 @@ namespace osu.Game.Tests.Visual.Editing }); AddAssert("volume changed", () => EditorBeatmap.HitObjects.First().Samples.All(o => o.Volume == 30)); + + AddStep("close popover", () => + { + InputManager.MoveMouseTo(popover, new Vector2(200, 0)); + InputManager.PressButton(MouseButton.Left); + InputManager.ReleaseButton(MouseButton.Left); + popover = null; + }); } [Test] public void TestNodeSamplePointPiece() { Slider slider = null!; - SamplePointPiece samplePointPiece; SamplePointPiece.SampleEditPopover popover = null!; AddStep("add slider", () => @@ -215,7 +221,7 @@ namespace osu.Game.Tests.Visual.Editing AddStep("open slider end hitsound popover", () => { - samplePointPiece = this.ChildrenOfType().Last(); + var samplePointPiece = this.ChildrenOfType().Last(); InputManager.MoveMouseTo(samplePointPiece); InputManager.PressButton(MouseButton.Left); InputManager.ReleaseButton(MouseButton.Left); @@ -259,6 +265,14 @@ namespace osu.Game.Tests.Visual.Editing }); AddAssert("volume changed", () => slider.NodeSamples[1].All(o => o.Volume == 30)); + + AddStep("close popover", () => + { + InputManager.MoveMouseTo(popover, new Vector2(200, 0)); + InputManager.PressButton(MouseButton.Left); + InputManager.ReleaseButton(MouseButton.Left); + popover = null; + }); } } } From 63d9be9523a547c0c87d4e08cda6e79483ba359a Mon Sep 17 00:00:00 2001 From: OliBomby Date: Thu, 1 Jun 2023 09:27:04 +0200 Subject: [PATCH 0057/2493] merge updateAdditionBankPlaceholderText and updateAdditionBankActivated --- .../Components/Timeline/SamplePointPiece.cs | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs index 37a6fa8a22..997e342dad 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs @@ -178,13 +178,12 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline // this ensures that committing empty text causes a revert to the previous value. bank.OnCommit += (_, _) => bank.Current.Value = getCommonBank(); - updateAdditionBankPlaceholderText(); updateAdditionBankText(); - updateAdditionBankActivated(); + updateAdditionBankVisual(); additionBank.Current.BindValueChanged(val => { updateAdditionBank(val.NewValue); - updateAdditionBankPlaceholderText(); + updateAdditionBankVisual(); }); additionBank.OnCommit += (_, _) => updateAdditionBankText(); @@ -257,14 +256,11 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline bank.PlaceholderText = string.IsNullOrEmpty(commonBank) ? "(multiple)" : string.Empty; } - private void updateAdditionBankPlaceholderText() + private void updateAdditionBankVisual() { string? commonAdditionBank = getCommonAdditionBank(); additionBank.PlaceholderText = string.IsNullOrEmpty(commonAdditionBank) ? "(multiple)" : string.Empty; - } - private void updateAdditionBankActivated() - { bool anyAdditions = relevantSamples.Any(o => o.Any(s => s.Name != HitSampleInfo.HIT_NORMAL)); if (anyAdditions) additionBank.Show(); @@ -274,8 +270,6 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline private void updateAdditionBankText() { - if (additionBank.Current.Disabled) return; - string? commonAdditionBank = getCommonAdditionBank(); if (string.IsNullOrEmpty(commonAdditionBank)) return; @@ -357,7 +351,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline samples.Add(relevantSample?.With(sampleName) ?? h.CreateHitSampleInfo(sampleName)); }); - updateAdditionBankActivated(); + updateAdditionBankVisual(); updateAdditionBankText(); } @@ -376,7 +370,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline }); updateAdditionBankText(); - updateAdditionBankActivated(); + updateAdditionBankVisual(); } protected override bool OnKeyDown(KeyDownEvent e) From 1eb9b8e135ff2ae31f65bde9b7d5c01dae2a9afc Mon Sep 17 00:00:00 2001 From: OliBomby Date: Thu, 1 Jun 2023 09:34:21 +0200 Subject: [PATCH 0058/2493] added xmldoc and renamed GetSamples --- .../Components/Timeline/NodeSamplePointPiece.cs | 2 +- .../Components/Timeline/SamplePointPiece.cs | 14 ++++++++++---- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/NodeSamplePointPiece.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/NodeSamplePointPiece.cs index f168fb791f..ae3838bc41 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/NodeSamplePointPiece.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/NodeSamplePointPiece.cs @@ -34,7 +34,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline { private readonly int nodeIndex; - protected override IList GetSamples(HitObject ho) + protected override IList GetRelevantSamples(HitObject ho) { if (ho is not IHasRepeats hasRepeats) return ho.Samples; diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs index 997e342dad..26cdf87d02 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs @@ -101,7 +101,13 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline private HitObject[] relevantObjects = null!; private IList[] relevantSamples = null!; - protected virtual IList GetSamples(HitObject ho) => ho.Samples; + /// + /// Gets the sub-set of samples relevant to this sample point piece. + /// For example, to edit node samples this should return the samples at the index of the node. + /// + /// The hit object to get the relevant samples from. + /// The relevant list of samples. + protected virtual IList GetRelevantSamples(HitObject ho) => ho.Samples; [Resolved(canBeNull: true)] private EditorBeatmap beatmap { get; set; } = null!; @@ -157,7 +163,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline // if the piece belongs to a currently selected object, assume that the user wants to change all selected objects. // if the piece belongs to an unselected object, operate on that object alone, independently of the selection. relevantObjects = (beatmap.SelectedHitObjects.Contains(hitObject) ? beatmap.SelectedHitObjects : hitObject.Yield()).ToArray(); - relevantSamples = relevantObjects.Select(GetSamples).ToArray(); + relevantSamples = relevantObjects.Select(GetRelevantSamples).ToArray(); // even if there are multiple objects selected, we can still display sample volume or bank if they all have the same value. string? commonBank = getCommonBank(); @@ -210,7 +216,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline foreach (var h in relevantObjects) { - var samples = GetSamples(h); + var samples = GetRelevantSamples(h); updateAction(h, samples); beatmap.Update(h); } @@ -325,7 +331,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline { foreach ((string sampleName, var bindable) in selectionSampleStates) { - bindable.Value = SelectionHandler.GetStateFromSelection(relevantObjects, h => GetSamples(h).Any(s => s.Name == sampleName)); + bindable.Value = SelectionHandler.GetStateFromSelection(relevantObjects, h => GetRelevantSamples(h).Any(s => s.Name == sampleName)); } } From eb8ac8951361cf8ba77cad6f69f09fc371209a67 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Thu, 1 Jun 2023 09:50:14 +0200 Subject: [PATCH 0059/2493] rename sample update logic and add xmldoc for clarity --- .../Components/Timeline/SamplePointPiece.cs | 65 ++++++++++--------- 1 file changed, 35 insertions(+), 30 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs index 26cdf87d02..2060a8ddd3 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs @@ -99,7 +99,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline private FillFlowContainer togglesCollection = null!; private HitObject[] relevantObjects = null!; - private IList[] relevantSamples = null!; + private IList[] allRelevantSamples = null!; /// /// Gets the sub-set of samples relevant to this sample point piece. @@ -163,7 +163,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline // if the piece belongs to a currently selected object, assume that the user wants to change all selected objects. // if the piece belongs to an unselected object, operate on that object alone, independently of the selection. relevantObjects = (beatmap.SelectedHitObjects.Contains(hitObject) ? beatmap.SelectedHitObjects : hitObject.Yield()).ToArray(); - relevantSamples = relevantObjects.Select(GetRelevantSamples).ToArray(); + allRelevantSamples = relevantObjects.Select(GetRelevantSamples).ToArray(); // even if there are multiple objects selected, we can still display sample volume or bank if they all have the same value. string? commonBank = getCommonBank(); @@ -206,19 +206,24 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline ScheduleAfterChildren(() => GetContainingInputManager().ChangeFocus(volume)); } - private string? getCommonBank() => relevantSamples.Select(GetBankValue).Distinct().Count() == 1 ? GetBankValue(relevantSamples.First()) : null; - private string? getCommonAdditionBank() => relevantSamples.Select(GetAdditionBankValue).Distinct().Count() == 1 ? GetAdditionBankValue(relevantSamples.First()) : null; - private int? getCommonVolume() => relevantSamples.Select(GetVolumeValue).Distinct().Count() == 1 ? GetVolumeValue(relevantSamples.First()) : null; + private string? getCommonBank() => allRelevantSamples.Select(GetBankValue).Distinct().Count() == 1 ? GetBankValue(allRelevantSamples.First()) : null; + private string? getCommonAdditionBank() => allRelevantSamples.Select(GetAdditionBankValue).Distinct().Count() == 1 ? GetAdditionBankValue(allRelevantSamples.First()) : null; + private int? getCommonVolume() => allRelevantSamples.Select(GetVolumeValue).Distinct().Count() == 1 ? GetVolumeValue(allRelevantSamples.First()) : null; - private void update(Action> updateAction) + /// + /// Applies the given update action on all samples of + /// and invokes the necessary update notifiers for the beatmap and hit objects. + /// + /// The action to perform on each element of . + private void updateAllRelevantSamples(Action> updateAction) { beatmap.BeginChange(); - foreach (var h in relevantObjects) + foreach (var relevantHitObject in relevantObjects) { - var samples = GetRelevantSamples(h); - updateAction(h, samples); - beatmap.Update(h); + var relevantSamples = GetRelevantSamples(relevantHitObject); + updateAction(relevantHitObject, relevantSamples); + beatmap.Update(relevantHitObject); } beatmap.EndChange(); @@ -229,13 +234,13 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline if (string.IsNullOrEmpty(newBank)) return; - update((_, samples) => + updateAllRelevantSamples((_, relevantSamples) => { - for (int i = 0; i < samples.Count; i++) + for (int i = 0; i < relevantSamples.Count; i++) { - if (samples[i].Name != HitSampleInfo.HIT_NORMAL) continue; + if (relevantSamples[i].Name != HitSampleInfo.HIT_NORMAL) continue; - samples[i] = samples[i].With(newBank: newBank); + relevantSamples[i] = relevantSamples[i].With(newBank: newBank); } }); } @@ -245,13 +250,13 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline if (string.IsNullOrEmpty(newBank)) return; - update((_, samples) => + updateAllRelevantSamples((_, relevantSamples) => { - for (int i = 0; i < samples.Count; i++) + for (int i = 0; i < relevantSamples.Count; i++) { - if (samples[i].Name == HitSampleInfo.HIT_NORMAL) continue; + if (relevantSamples[i].Name == HitSampleInfo.HIT_NORMAL) continue; - samples[i] = samples[i].With(newBank: newBank); + relevantSamples[i] = relevantSamples[i].With(newBank: newBank); } }); } @@ -267,7 +272,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline string? commonAdditionBank = getCommonAdditionBank(); additionBank.PlaceholderText = string.IsNullOrEmpty(commonAdditionBank) ? "(multiple)" : string.Empty; - bool anyAdditions = relevantSamples.Any(o => o.Any(s => s.Name != HitSampleInfo.HIT_NORMAL)); + bool anyAdditions = allRelevantSamples.Any(o => o.Any(s => s.Name != HitSampleInfo.HIT_NORMAL)); if (anyAdditions) additionBank.Show(); else @@ -287,11 +292,11 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline if (newVolume == null) return; - update((_, samples) => + updateAllRelevantSamples((_, relevantSamples) => { - for (int i = 0; i < samples.Count; i++) + for (int i = 0; i < relevantSamples.Count; i++) { - samples[i] = samples[i].With(newVolume: newVolume.Value); + relevantSamples[i] = relevantSamples[i].With(newVolume: newVolume.Value); } }); } @@ -346,15 +351,15 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline if (string.IsNullOrEmpty(sampleName)) return; - update((h, samples) => + updateAllRelevantSamples((h, relevantSamples) => { // Make sure there isn't already an existing sample - if (samples.Any(s => s.Name == sampleName)) + if (relevantSamples.Any(s => s.Name == sampleName)) return; // First try inheriting the sample info from the node samples instead of the samples of the hitobject - var relevantSample = samples.FirstOrDefault(s => s.Name != HitSampleInfo.HIT_NORMAL) ?? samples.FirstOrDefault(); - samples.Add(relevantSample?.With(sampleName) ?? h.CreateHitSampleInfo(sampleName)); + var relevantSample = relevantSamples.FirstOrDefault(s => s.Name != HitSampleInfo.HIT_NORMAL) ?? relevantSamples.FirstOrDefault(); + relevantSamples.Add(relevantSample?.With(sampleName) ?? h.CreateHitSampleInfo(sampleName)); }); updateAdditionBankVisual(); @@ -366,12 +371,12 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline if (string.IsNullOrEmpty(sampleName)) return; - update((_, samples) => + updateAllRelevantSamples((_, relevantSamples) => { - for (int i = 0; i < samples.Count; i++) + for (int i = 0; i < relevantSamples.Count; i++) { - if (samples[i].Name == sampleName) - samples.RemoveAt(i--); + if (relevantSamples[i].Name == sampleName) + relevantSamples.RemoveAt(i--); } }); From 3110f87831014a044ed316c78a8125043965f8cd Mon Sep 17 00:00:00 2001 From: OliBomby Date: Thu, 1 Jun 2023 18:48:12 +0200 Subject: [PATCH 0060/2493] Revert "Add PopoverContainer to TimelineTestScene" This reverts commit 8acfe6b58b1de2de3466315fc74b000a428860c1. --- .../Visual/Editing/TimelineTestScene.cs | 36 ++++++++----------- 1 file changed, 14 insertions(+), 22 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TimelineTestScene.cs b/osu.Game.Tests/Visual/Editing/TimelineTestScene.cs index afec75e948..cb45ad5a07 100644 --- a/osu.Game.Tests/Visual/Editing/TimelineTestScene.cs +++ b/osu.Game.Tests/Visual/Editing/TimelineTestScene.cs @@ -9,7 +9,6 @@ using osu.Framework.Audio; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Shapes; using osu.Framework.Testing; using osu.Game.Beatmaps; @@ -52,35 +51,28 @@ namespace osu.Game.Tests.Visual.Editing Composer.Alpha = 0; - Add(new PopoverContainer + Add(new OsuContextMenuContainer { RelativeSizeAxes = Axes.Both, Children = new Drawable[] { - new OsuContextMenuContainer + EditorBeatmap, + Composer, + new FillFlowContainer { - RelativeSizeAxes = Axes.Both, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0, 5), Children = new Drawable[] { - EditorBeatmap, - Composer, - new FillFlowContainer - { - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Vertical, - Spacing = new Vector2(0, 5), - Children = new Drawable[] - { - new StartStopButton(), - new AudioVisualiser(), - } - }, - TimelineArea = new TimelineArea(CreateTestComponent()) - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - } + new StartStopButton(), + new AudioVisualiser(), } + }, + TimelineArea = new TimelineArea(CreateTestComponent()) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, } } }); From 035163a7921ef12225b15973ff9302652a99a204 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Fri, 2 Jun 2023 00:40:00 +0200 Subject: [PATCH 0061/2493] add new behaviour tests to TestSceneHitObjectSampleAdjustments --- .../TestSceneHitObjectSampleAdjustments.cs | 151 ++++++++++++++++++ 1 file changed, 151 insertions(+) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneHitObjectSampleAdjustments.cs b/osu.Game.Tests/Visual/Editing/TestSceneHitObjectSampleAdjustments.cs index b0b51a5dbd..4136deda3e 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneHitObjectSampleAdjustments.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneHitObjectSampleAdjustments.cs @@ -14,9 +14,12 @@ using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Rulesets; using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.UI; +using osu.Game.Screens.Edit.Components.TernaryButtons; using osu.Game.Screens.Edit.Compose.Components.Timeline; using osu.Game.Screens.Edit.Timing; using osu.Game.Tests.Beatmaps; @@ -227,6 +230,84 @@ namespace osu.Game.Tests.Visual.Editing samplePopoverHasSingleBank(HitSampleInfo.BANK_NORMAL); } + [Test] + public void TestPopoverAddSampleAddition() + { + clickSamplePiece(0); + + setBankViaPopover(HitSampleInfo.BANK_SOFT); + hitObjectHasSampleBank(0, HitSampleInfo.BANK_SOFT); + + toggleAdditionViaPopover(0); + + hitObjectHasSampleBank(0, HitSampleInfo.BANK_SOFT); + hitObjectHasSamples(0, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_WHISTLE); + + setAdditionBankViaPopover(HitSampleInfo.BANK_DRUM); + + hitObjectHasSampleNormalBank(0, HitSampleInfo.BANK_SOFT); + hitObjectHasSampleAdditionBank(0, HitSampleInfo.BANK_DRUM); + + toggleAdditionViaPopover(0); + + hitObjectHasSampleBank(0, HitSampleInfo.BANK_SOFT); + hitObjectHasSamples(0, HitSampleInfo.HIT_NORMAL); + } + + [Test] + public void TestNodeSamplePopover() + { + AddStep("add slider", () => + { + EditorBeatmap.Clear(); + EditorBeatmap.Add(new Slider + { + Position = new Vector2(256, 256), + StartTime = 0, + Path = new SliderPath(new[] { new PathControlPoint(Vector2.Zero), new PathControlPoint(new Vector2(250, 0)) }), + Samples = + { + new HitSampleInfo(HitSampleInfo.HIT_NORMAL) + }, + NodeSamples = + { + new List { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) }, + new List { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) }, + } + }); + }); + + clickNodeSamplePiece(0, 1); + + setBankViaPopover(HitSampleInfo.BANK_SOFT); + hitObjectNodeHasSampleBank(0, 0, HitSampleInfo.BANK_NORMAL); + hitObjectNodeHasSampleBank(0, 1, HitSampleInfo.BANK_SOFT); + + toggleAdditionViaPopover(0); + + hitObjectNodeHasSampleBank(0, 0, HitSampleInfo.BANK_NORMAL); + hitObjectNodeHasSampleBank(0, 1, HitSampleInfo.BANK_SOFT); + hitObjectNodeHasSamples(0, 0, HitSampleInfo.HIT_NORMAL); + hitObjectNodeHasSamples(0, 1, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_WHISTLE); + + setAdditionBankViaPopover(HitSampleInfo.BANK_DRUM); + + hitObjectNodeHasSampleBank(0, 0, HitSampleInfo.BANK_NORMAL); + hitObjectNodeHasSampleNormalBank(0, 1, HitSampleInfo.BANK_SOFT); + hitObjectNodeHasSampleAdditionBank(0, 1, HitSampleInfo.BANK_DRUM); + + toggleAdditionViaPopover(0); + + hitObjectNodeHasSampleBank(0, 1, HitSampleInfo.BANK_SOFT); + hitObjectNodeHasSamples(0, 0, HitSampleInfo.HIT_NORMAL); + hitObjectNodeHasSamples(0, 1, HitSampleInfo.HIT_NORMAL); + + setVolumeViaPopover(10); + + hitObjectNodeHasSampleVolume(0, 0, 100); + hitObjectNodeHasSampleVolume(0, 1, 10); + } + [Test] public void TestHotkeysMultipleSelectionWithSameSampleBank() { @@ -330,6 +411,14 @@ namespace osu.Game.Tests.Visual.Editing InputManager.Click(MouseButton.Left); }); + private void clickNodeSamplePiece(int objectIndex, int nodeIndex) => AddStep($"click {objectIndex.ToOrdinalWords()} object {nodeIndex.ToOrdinalWords()} node sample piece", () => + { + var samplePiece = this.ChildrenOfType().Where(piece => piece.HitObject == EditorBeatmap.HitObjects.ElementAt(objectIndex)).ToArray()[nodeIndex]; + + InputManager.MoveMouseTo(samplePiece); + InputManager.Click(MouseButton.Left); + }); + private void samplePopoverHasFocus() => AddUntilStep("sample popover textbox focused", () => { var popover = this.ChildrenOfType().SingleOrDefault(); @@ -391,6 +480,12 @@ namespace osu.Game.Tests.Visual.Editing return h.Samples.All(o => o.Volume == volume); }); + private void hitObjectNodeHasSampleVolume(int objectIndex, int nodeIndex, int volume) => AddAssert($"{objectIndex.ToOrdinalWords()} object {nodeIndex.ToOrdinalWords()} node has volume {volume}", () => + { + var h = EditorBeatmap.HitObjects.ElementAt(objectIndex) as IHasRepeats; + return h is not null && h.NodeSamples[nodeIndex].All(o => o.Volume == volume); + }); + private void setBankViaPopover(string bank) => AddStep($"set bank {bank} via popover", () => { var popover = this.ChildrenOfType().Single(); @@ -402,6 +497,26 @@ namespace osu.Game.Tests.Visual.Editing InputManager.Key(Key.Enter); }); + private void setAdditionBankViaPopover(string bank) => AddStep($"set addition bank {bank} via popover", () => + { + var popover = this.ChildrenOfType().Single(); + var textBox = popover.ChildrenOfType().ToArray()[1]; + textBox.Current.Value = bank; + // force a commit via keyboard. + // this is needed when testing attempting to set empty bank - which should revert to the previous value, but only on commit. + InputManager.ChangeFocus(textBox); + InputManager.Key(Key.Enter); + }); + + private void toggleAdditionViaPopover(int index) => AddStep($"toggle addition {index} via popover", () => + { + var popover = this.ChildrenOfType().First(); + var ternaryButton = popover.ChildrenOfType().ToArray()[index]; + InputManager.MoveMouseTo(ternaryButton); + InputManager.PressButton(MouseButton.Left); + InputManager.ReleaseButton(MouseButton.Left); + }); + private void hitObjectHasSamples(int objectIndex, params string[] samples) => AddAssert($"{objectIndex.ToOrdinalWords()} has samples {string.Join(',', samples)}", () => { var h = EditorBeatmap.HitObjects.ElementAt(objectIndex); @@ -413,5 +528,41 @@ namespace osu.Game.Tests.Visual.Editing var h = EditorBeatmap.HitObjects.ElementAt(objectIndex); return h.Samples.All(o => o.Bank == bank); }); + + private void hitObjectHasSampleNormalBank(int objectIndex, string bank) => AddAssert($"{objectIndex.ToOrdinalWords()} has normal bank {bank}", () => + { + var h = EditorBeatmap.HitObjects.ElementAt(objectIndex); + return h.Samples.Where(o => o.Name == HitSampleInfo.HIT_NORMAL).All(o => o.Bank == bank); + }); + + private void hitObjectHasSampleAdditionBank(int objectIndex, string bank) => AddAssert($"{objectIndex.ToOrdinalWords()} has addition bank {bank}", () => + { + var h = EditorBeatmap.HitObjects.ElementAt(objectIndex); + return h.Samples.Where(o => o.Name != HitSampleInfo.HIT_NORMAL).All(o => o.Bank == bank); + }); + + private void hitObjectNodeHasSamples(int objectIndex, int nodeIndex, params string[] samples) => AddAssert($"{objectIndex.ToOrdinalWords()} object {nodeIndex.ToOrdinalWords()} node has samples {string.Join(',', samples)}", () => + { + var h = EditorBeatmap.HitObjects.ElementAt(objectIndex) as IHasRepeats; + return h is not null && h.NodeSamples[nodeIndex].Select(s => s.Name).SequenceEqual(samples); + }); + + private void hitObjectNodeHasSampleBank(int objectIndex, int nodeIndex, string bank) => AddAssert($"{objectIndex.ToOrdinalWords()} object {nodeIndex.ToOrdinalWords()} node has bank {bank}", () => + { + var h = EditorBeatmap.HitObjects.ElementAt(objectIndex) as IHasRepeats; + return h is not null && h.NodeSamples[nodeIndex].All(o => o.Bank == bank); + }); + + private void hitObjectNodeHasSampleNormalBank(int objectIndex, int nodeIndex, string bank) => AddAssert($"{objectIndex.ToOrdinalWords()} object {nodeIndex.ToOrdinalWords()} node has normal bank {bank}", () => + { + var h = EditorBeatmap.HitObjects.ElementAt(objectIndex) as IHasRepeats; + return h is not null && h.NodeSamples[nodeIndex].Where(o => o.Name == HitSampleInfo.HIT_NORMAL).All(o => o.Bank == bank); + }); + + private void hitObjectNodeHasSampleAdditionBank(int objectIndex, int nodeIndex, string bank) => AddAssert($"{objectIndex.ToOrdinalWords()} object {nodeIndex.ToOrdinalWords()} node has addition bank {bank}", () => + { + var h = EditorBeatmap.HitObjects.ElementAt(objectIndex) as IHasRepeats; + return h is not null && h.NodeSamples[nodeIndex].Where(o => o.Name != HitSampleInfo.HIT_NORMAL).All(o => o.Bank == bank); + }); } } From acd8ff9a242f88a88997ef834469ea2b9a4f43ed Mon Sep 17 00:00:00 2001 From: OliBomby Date: Fri, 2 Jun 2023 00:42:36 +0200 Subject: [PATCH 0062/2493] revert add sample point piece tests --- .../TestSceneTimelineHitObjectBlueprint.cs | 163 ------------------ 1 file changed, 163 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneTimelineHitObjectBlueprint.cs b/osu.Game.Tests/Visual/Editing/TestSceneTimelineHitObjectBlueprint.cs index 4410ae4112..08e036248b 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneTimelineHitObjectBlueprint.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneTimelineHitObjectBlueprint.cs @@ -3,21 +3,15 @@ #nullable disable -using System.Collections.Generic; using System.Linq; using NUnit.Framework; using osu.Framework.Graphics; using osu.Framework.Graphics.UserInterface; using osu.Framework.Testing; -using osu.Game.Audio; using osu.Game.Graphics.UserInterface; -using osu.Game.Graphics.UserInterfaceV2; -using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu.Objects; -using osu.Game.Screens.Edit.Components.TernaryButtons; using osu.Game.Screens.Edit.Compose.Components.Timeline; -using osu.Game.Screens.Edit.Timing; using osuTK; using osuTK.Input; using static osu.Game.Screens.Edit.Compose.Components.Timeline.TimelineHitObjectBlueprint; @@ -117,162 +111,5 @@ namespace osu.Game.Tests.Visual.Editing AddAssert("object has zero repeats", () => EditorBeatmap.HitObjects.OfType().Single().RepeatCount == 0); } - - [Test] - public void TestSamplePointPiece() - { - SamplePointPiece.SampleEditPopover popover = null!; - - AddStep("add circle", () => - { - EditorBeatmap.Clear(); - EditorBeatmap.Add(new HitCircle - { - Position = new Vector2(256, 256), - StartTime = 2700, - Samples = - { - new HitSampleInfo(HitSampleInfo.HIT_NORMAL) - } - }); - }); - - AddStep("open hitsound popover", () => - { - var samplePointPiece = this.ChildrenOfType().Single(); - InputManager.MoveMouseTo(samplePointPiece); - InputManager.PressButton(MouseButton.Left); - InputManager.ReleaseButton(MouseButton.Left); - }); - - AddStep("add whistle addition", () => - { - popover = this.ChildrenOfType().First(); - var whistleTernaryButton = popover.ChildrenOfType().First(); - InputManager.MoveMouseTo(whistleTernaryButton); - InputManager.PressButton(MouseButton.Left); - InputManager.ReleaseButton(MouseButton.Left); - }); - - AddAssert("has whistle sample", () => EditorBeatmap.HitObjects.First().Samples.Any(o => o.Name == HitSampleInfo.HIT_WHISTLE)); - - AddStep("change bank name", () => - { - var bankTextBox = popover.ChildrenOfType().First(); - bankTextBox.Current.Value = "soft"; - }); - - AddAssert("bank name changed", () => - EditorBeatmap.HitObjects.First().Samples.Where(o => o.Name == HitSampleInfo.HIT_NORMAL).All(o => o.Bank == "soft") - && EditorBeatmap.HitObjects.First().Samples.Where(o => o.Name != HitSampleInfo.HIT_NORMAL).All(o => o.Bank == "normal")); - - AddStep("change addition bank name", () => - { - var bankTextBox = popover.ChildrenOfType().ToArray()[1]; - bankTextBox.Current.Value = "drum"; - }); - - AddAssert("addition bank name changed", () => - EditorBeatmap.HitObjects.First().Samples.Where(o => o.Name == HitSampleInfo.HIT_NORMAL).All(o => o.Bank == "soft") - && EditorBeatmap.HitObjects.First().Samples.Where(o => o.Name != HitSampleInfo.HIT_NORMAL).All(o => o.Bank == "drum")); - - AddStep("change volume", () => - { - var bankTextBox = popover.ChildrenOfType>().Single(); - bankTextBox.Current.Value = 30; - }); - - AddAssert("volume changed", () => EditorBeatmap.HitObjects.First().Samples.All(o => o.Volume == 30)); - - AddStep("close popover", () => - { - InputManager.MoveMouseTo(popover, new Vector2(200, 0)); - InputManager.PressButton(MouseButton.Left); - InputManager.ReleaseButton(MouseButton.Left); - popover = null; - }); - } - - [Test] - public void TestNodeSamplePointPiece() - { - Slider slider = null!; - SamplePointPiece.SampleEditPopover popover = null!; - - AddStep("add slider", () => - { - EditorBeatmap.Clear(); - EditorBeatmap.Add(slider = new Slider - { - Position = new Vector2(256, 256), - StartTime = 2700, - Path = new SliderPath(new[] { new PathControlPoint(Vector2.Zero), new PathControlPoint(new Vector2(250, 0)) }), - Samples = - { - new HitSampleInfo(HitSampleInfo.HIT_NORMAL) - }, - NodeSamples = - { - new List { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) }, - new List { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) }, - } - }); - }); - - AddStep("open slider end hitsound popover", () => - { - var samplePointPiece = this.ChildrenOfType().Last(); - InputManager.MoveMouseTo(samplePointPiece); - InputManager.PressButton(MouseButton.Left); - InputManager.ReleaseButton(MouseButton.Left); - }); - - AddStep("add whistle addition", () => - { - popover = this.ChildrenOfType().First(); - var whistleTernaryButton = popover.ChildrenOfType().First(); - InputManager.MoveMouseTo(whistleTernaryButton); - InputManager.PressButton(MouseButton.Left); - InputManager.ReleaseButton(MouseButton.Left); - }); - - AddAssert("has whistle sample", () => slider.NodeSamples[1].Any(o => o.Name == HitSampleInfo.HIT_WHISTLE)); - - AddStep("change bank name", () => - { - var bankTextBox = popover.ChildrenOfType().First(); - bankTextBox.Current.Value = "soft"; - }); - - AddAssert("bank name changed", () => - slider.NodeSamples[1].Where(o => o.Name == HitSampleInfo.HIT_NORMAL).All(o => o.Bank == "soft") - && slider.NodeSamples[1].Where(o => o.Name != HitSampleInfo.HIT_NORMAL).All(o => o.Bank == "normal")); - - AddStep("change addition bank name", () => - { - var bankTextBox = popover.ChildrenOfType().ToArray()[1]; - bankTextBox.Current.Value = "drum"; - }); - - AddAssert("addition bank name changed", () => - slider.NodeSamples[1].Where(o => o.Name == HitSampleInfo.HIT_NORMAL).All(o => o.Bank == "soft") - && slider.NodeSamples[1].Where(o => o.Name != HitSampleInfo.HIT_NORMAL).All(o => o.Bank == "drum")); - - AddStep("change volume", () => - { - var bankTextBox = popover.ChildrenOfType>().Single(); - bankTextBox.Current.Value = 30; - }); - - AddAssert("volume changed", () => slider.NodeSamples[1].All(o => o.Volume == 30)); - - AddStep("close popover", () => - { - InputManager.MoveMouseTo(popover, new Vector2(200, 0)); - InputManager.PressButton(MouseButton.Left); - InputManager.ReleaseButton(MouseButton.Left); - popover = null; - }); - } } } From da516b90390ef06682155e051911ddd21d2e95a5 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Fri, 2 Jun 2023 00:50:21 +0200 Subject: [PATCH 0063/2493] Change purple to darker pink --- .../Edit/Compose/Components/Timeline/SamplePointPiece.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs index 2060a8ddd3..de85435d02 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs @@ -38,7 +38,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline public bool AlternativeColor { get; init; } - protected override Color4 GetRepresentingColour(OsuColour colours) => AlternativeColor ? colours.Purple : colours.Pink; + protected override Color4 GetRepresentingColour(OsuColour colours) => AlternativeColor ? colours.PinkDarker : colours.Pink; [BackgroundDependencyLoader] private void load() From 848f0e305eafd6d5258e8bf86d1f7ea547b67cef Mon Sep 17 00:00:00 2001 From: OliBomby Date: Fri, 2 Jun 2023 00:55:37 +0200 Subject: [PATCH 0064/2493] Change position of sliderbody hitsound piece for less overlap --- .../Compose/Components/Timeline/TimelineHitObjectBlueprint.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs index ddac3bb667..c642b9f29f 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs @@ -111,7 +111,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline { Anchor = Anchor.BottomLeft, Origin = Anchor.TopCentre, - X = Item is IHasRepeats ? -10 : 0, + X = Item is IHasRepeats ? 30 : 0, AlternativeColor = Item is IHasRepeats }, }); From 3f96795bbf9c6f69b673a89a71f82586da25ac4d Mon Sep 17 00:00:00 2001 From: OliBomby Date: Fri, 2 Jun 2023 01:02:35 +0200 Subject: [PATCH 0065/2493] fix merge conflict --- .../Compose/Components/Timeline/TimelineHitObjectBlueprint.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs index 00bd1a7019..f41daf30ce 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs @@ -109,7 +109,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline { RelativeSizeAxes = Axes.Both, }, - new SamplePointPiece(Item) + samplePointPiece = new SamplePointPiece(Item) { Anchor = Anchor.BottomLeft, Origin = Anchor.TopCentre, From 00250972c34caba4c5f1ab8dbdd29fa0ba80c697 Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Thu, 6 Jul 2023 02:31:12 -0400 Subject: [PATCH 0066/2493] skip frames after a negative frame until the negative time is "paid back" --- osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs index c6461840aa..f91c96efb6 100644 --- a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs +++ b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs @@ -269,6 +269,13 @@ namespace osu.Game.Scoring.Legacy float lastTime = beatmapOffset; ReplayFrame currentFrame = null; + // the negative time amount that must be "paid back" by positive frames before we start including frames again. + // When a negative frame occurs in a replay, all future frames are skipped until the sum total of their times + // is equal to or greater than the time of that negative frame. + // This value will be negative if we are in a time deficit, ie we have a negative frame that must be paid back. + // Otherwise it will be 0. + float timeDeficit = 0; + string[] frames = reader.ReadToEnd().Split(','); for (int i = 0; i < frames.Length; i++) @@ -296,9 +303,13 @@ namespace osu.Game.Scoring.Legacy // ignore these frames as they serve no real purpose (and can even mislead ruleset-specific handlers - see mania) continue; + timeDeficit += diff; + timeDeficit = Math.Min(0, timeDeficit); + + // still paying back the deficit from a negative frame. Skip this frame. // Todo: At some point we probably want to rewind and play back the negative-time frames // but for now we'll achieve equal playback to stable by skipping negative frames - if (diff < 0) + if (timeDeficit < 0) continue; currentFrame = convertFrame(new LegacyReplayFrame(lastTime, From cc6646c82b13e021514a0465b118383d8e96ba7f Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Thu, 6 Jul 2023 17:13:33 -0400 Subject: [PATCH 0067/2493] properly handle negative frame before a break this was causing replay data before the skip to be...skipped. --- osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs index f91c96efb6..63465652e8 100644 --- a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs +++ b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs @@ -267,6 +267,7 @@ namespace osu.Game.Scoring.Legacy private void readLegacyReplay(Replay replay, StreamReader reader) { float lastTime = beatmapOffset; + bool skipFramesPresent = false; ReplayFrame currentFrame = null; // the negative time amount that must be "paid back" by positive frames before we start including frames again. @@ -298,18 +299,31 @@ namespace osu.Game.Scoring.Legacy lastTime += diff; if (i < 2 && mouseX == 256 && mouseY == -500) + { // at the start of the replay, stable places two replay frames, at time 0 and SkipBoundary - 1, respectively. // both frames use a position of (256, -500). // ignore these frames as they serve no real purpose (and can even mislead ruleset-specific handlers - see mania) + skipFramesPresent = true; continue; + } - timeDeficit += diff; - timeDeficit = Math.Min(0, timeDeficit); + // if the skip frames inserted by stable are present, the third frame will have a large negative time + // roughly equal to SkipBoundary. We don't want this to count towards the deficit: doing so would cause + // the replay data before the skip to be, well, skipped. + // In other words, this frame, if present, is a different kind of negative frame. It sets the "offset" + // for the beginning of the replay. This is the only negative frame to be handled in such a way. + bool isNegativeBreakFrame = i == 2 && skipFramesPresent && diff < 0; + + if (!isNegativeBreakFrame) + { + timeDeficit += diff; + timeDeficit = Math.Min(0, timeDeficit); + } // still paying back the deficit from a negative frame. Skip this frame. // Todo: At some point we probably want to rewind and play back the negative-time frames // but for now we'll achieve equal playback to stable by skipping negative frames - if (timeDeficit < 0) + if (timeDeficit < 0 || isNegativeBreakFrame) continue; currentFrame = convertFrame(new LegacyReplayFrame(lastTime, From 217b07810fb497f571fadfeb4d015394a43075d0 Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Thu, 27 Jul 2023 02:12:21 -0400 Subject: [PATCH 0068/2493] don't skip the negative break frame investigation reveals this frame is played back by stable --- osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs index 63465652e8..8b1b24ce95 100644 --- a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs +++ b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.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 @@ -323,7 +323,7 @@ namespace osu.Game.Scoring.Legacy // still paying back the deficit from a negative frame. Skip this frame. // Todo: At some point we probably want to rewind and play back the negative-time frames // but for now we'll achieve equal playback to stable by skipping negative frames - if (timeDeficit < 0 || isNegativeBreakFrame) + if (timeDeficit < 0) continue; currentFrame = convertFrame(new LegacyReplayFrame(lastTime, From a93561cab05807be032e963b37092ff349f12fc1 Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Thu, 27 Jul 2023 02:12:43 -0400 Subject: [PATCH 0069/2493] remove resolved comment --- osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs index 8b1b24ce95..79224b7d4f 100644 --- a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs +++ b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs @@ -321,8 +321,6 @@ namespace osu.Game.Scoring.Legacy } // still paying back the deficit from a negative frame. Skip this frame. - // Todo: At some point we probably want to rewind and play back the negative-time frames - // but for now we'll achieve equal playback to stable by skipping negative frames if (timeDeficit < 0) continue; From 61760f614a9900e5cd71a298a8979ca69d9b8eb0 Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Thu, 27 Jul 2023 16:34:18 -0400 Subject: [PATCH 0070/2493] fix legacy score decode tests for negative frame --- .../Beatmaps/Formats/LegacyScoreDecoderTest.cs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs index 93cda34ef7..89b6d76e54 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs @@ -92,17 +92,20 @@ namespace osu.Game.Tests.Beatmaps.Formats [TestCase(LegacyBeatmapDecoder.LATEST_VERSION, false)] public void TestLegacyBeatmapReplayOffsetsDecode(int beatmapVersion, bool offsetApplied) { - const double first_frame_time = 48; - const double second_frame_time = 65; + const double first_frame_time = 31; + const double second_frame_time = 48; + const double third_frame_time = 65; var decoder = new TestLegacyScoreDecoder(beatmapVersion); using (var resourceStream = TestResources.OpenResource("Replays/mania-replay.osr")) { var score = decoder.Parse(resourceStream); + int offset = offsetApplied ? LegacyBeatmapDecoder.EARLY_VERSION_TIMING_OFFSET : 0; - Assert.That(score.Replay.Frames[0].Time, Is.EqualTo(first_frame_time + (offsetApplied ? LegacyBeatmapDecoder.EARLY_VERSION_TIMING_OFFSET : 0))); - Assert.That(score.Replay.Frames[1].Time, Is.EqualTo(second_frame_time + (offsetApplied ? LegacyBeatmapDecoder.EARLY_VERSION_TIMING_OFFSET : 0))); + Assert.That(score.Replay.Frames[0].Time, Is.EqualTo(first_frame_time + offset)); + Assert.That(score.Replay.Frames[1].Time, Is.EqualTo(second_frame_time + offset)); + Assert.That(score.Replay.Frames[2].Time, Is.EqualTo(third_frame_time + offset)); } } From 7d174dd8bb4998ac264463067b798fae14541f0c Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Thu, 27 Jul 2023 17:20:54 -0400 Subject: [PATCH 0071/2493] dont count any of first three frames towards time deficit --- osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs | 21 ++++++++----------- 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs index 79224b7d4f..eceaada399 100644 --- a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs +++ b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs @@ -267,7 +267,6 @@ namespace osu.Game.Scoring.Legacy private void readLegacyReplay(Replay replay, StreamReader reader) { float lastTime = beatmapOffset; - bool skipFramesPresent = false; ReplayFrame currentFrame = null; // the negative time amount that must be "paid back" by positive frames before we start including frames again. @@ -299,22 +298,20 @@ namespace osu.Game.Scoring.Legacy lastTime += diff; if (i < 2 && mouseX == 256 && mouseY == -500) - { // at the start of the replay, stable places two replay frames, at time 0 and SkipBoundary - 1, respectively. // both frames use a position of (256, -500). // ignore these frames as they serve no real purpose (and can even mislead ruleset-specific handlers - see mania) - skipFramesPresent = true; continue; - } - // if the skip frames inserted by stable are present, the third frame will have a large negative time - // roughly equal to SkipBoundary. We don't want this to count towards the deficit: doing so would cause - // the replay data before the skip to be, well, skipped. - // In other words, this frame, if present, is a different kind of negative frame. It sets the "offset" - // for the beginning of the replay. This is the only negative frame to be handled in such a way. - bool isNegativeBreakFrame = i == 2 && skipFramesPresent && diff < 0; - - if (!isNegativeBreakFrame) + // negative frames are only counted towards the deficit after the very beginning of the replay. + // When the two skip frames are present (see directly above), the third frame will have a large + // negative time roughly equal to SkipBoundary. This shouldn't be counted towards the deficit, otherwise + // any replay data before the skip would be, well, skipped. + // + // On testing against stable it appears that stable ignores the negative time of *any* of the first + // three frames, regardless of if the skip frames are present. Hence the condition here. + // But this may be incorrect and need to be revisited later. + if (i > 2) { timeDeficit += diff; timeDeficit = Math.Min(0, timeDeficit); From 04ef04b9026dfe84c323e3d08204f5a722fb5d74 Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Thu, 27 Jul 2023 21:12:08 -0400 Subject: [PATCH 0072/2493] only ignore the first negative frame among the first 3 replay frames --- osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs index eceaada399..fdeda24c75 100644 --- a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs +++ b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs @@ -267,6 +267,7 @@ namespace osu.Game.Scoring.Legacy private void readLegacyReplay(Replay replay, StreamReader reader) { float lastTime = beatmapOffset; + bool negativeFrameEncounted = false; ReplayFrame currentFrame = null; // the negative time amount that must be "paid back" by positive frames before we start including frames again. @@ -308,15 +309,19 @@ namespace osu.Game.Scoring.Legacy // negative time roughly equal to SkipBoundary. This shouldn't be counted towards the deficit, otherwise // any replay data before the skip would be, well, skipped. // - // On testing against stable it appears that stable ignores the negative time of *any* of the first - // three frames, regardless of if the skip frames are present. Hence the condition here. - // But this may be incorrect and need to be revisited later. - if (i > 2) + // On testing against stable, it appears that stable ignores the negative time of only the first + // negative frame of the first three replay frames, regardless of if the skip frames are present. + // Hence the condition here. + // But there is a possibility this is incorrect and may need to be revisited later. + if (i > 2 || negativeFrameEncounted) { timeDeficit += diff; timeDeficit = Math.Min(0, timeDeficit); } + if (diff < 0) + negativeFrameEncounted = true; + // still paying back the deficit from a negative frame. Skip this frame. if (timeDeficit < 0) continue; From 4060373e0399d3d0cd80639050fcbf06e03dc0ae Mon Sep 17 00:00:00 2001 From: nanashi-1 Date: Sat, 5 Aug 2023 21:15:31 +0800 Subject: [PATCH 0073/2493] add rank display element --- osu.Game/Screens/Play/HUD/RankDisplay.cs | 46 ++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 osu.Game/Screens/Play/HUD/RankDisplay.cs diff --git a/osu.Game/Screens/Play/HUD/RankDisplay.cs b/osu.Game/Screens/Play/HUD/RankDisplay.cs new file mode 100644 index 0000000000..be8841b647 --- /dev/null +++ b/osu.Game/Screens/Play/HUD/RankDisplay.cs @@ -0,0 +1,46 @@ +// 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.Game.Rulesets.Scoring; +using osu.Game.Skinning; +using osu.Framework.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Extensions; + +namespace osu.Game.Screens.Play.HUD +{ + public partial class RankDisplay : FontAdjustableSkinComponent + { + [Resolved] + private ScoreProcessor scoreProcessor { get; set; } = null!; + + private readonly OsuSpriteText text; + + public RankDisplay() + { + AutoSizeAxes = Axes.Both; + + InternalChildren = new Drawable[] + { + text = new OsuSpriteText + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + } + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + text.Text = scoreProcessor.Rank.Value.GetDescription(); + + scoreProcessor.Rank.BindValueChanged(v => text.Text = v.NewValue.GetDescription()); + } + + protected override void SetFont(FontUsage font) => text.Font = font.With(size: 40); + } +} From c67f6d949bca80561bf82456c1132f6a5dea8403 Mon Sep 17 00:00:00 2001 From: nanashi-1 Date: Sun, 6 Aug 2023 18:15:14 +0800 Subject: [PATCH 0074/2493] tests --- .../Archives/modified-argon-20230806.osk | Bin 0 -> 1354 bytes osu.Game.Tests/Skins/SkinDeserialisationTest.cs | 4 +++- 2 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 osu.Game.Tests/Resources/Archives/modified-argon-20230806.osk diff --git a/osu.Game.Tests/Resources/Archives/modified-argon-20230806.osk b/osu.Game.Tests/Resources/Archives/modified-argon-20230806.osk new file mode 100644 index 0000000000000000000000000000000000000000..e7c035811d62df429b930fd41548457307cbf90a GIT binary patch literal 1354 zcmWIWW@Zs#U|`^2_)x_b{xqpXate^Q9xTGZP@J8ar42}OmnuF-Rk~{%W2L3)BpF`hq7LuxG?;EX_=A3@+2KLW50%z zCJT1*?Tu&XoUtI*VfhwjufoTAdrWT0Ma{Xkf@$w(9SMn5xn44g8x$8lRNOs3(qd74 z%Jfi^|ABpl0Vfx(=r4_R5}XqAe#WUcQ*y4{IN&>P(dHEU0JzIP0$qM&RRUiT(A6D4 z%nQUImuKdsz#gq{ny90<(fJCWZu^Z35nV4SU;s zezd&&{rb(l6CZWTX0CB?f8})STDef8DX&0-clIHVq~7&z*EpW3q-AL+#q+qH5nYgJ zvuyS;c?VC{_qW5o8TKyp>l6GXlhoQW^O(vvN!~iABF$+Be%qw2$$KZ^)3U@X{KluV zAFX*U)-Jnh)Sa;?@cZP{{EfZ~ue1iJ=uZv0$-0a&G2qsM8Pleh$E~n?aL_4X)&OEvgYy;j{w*|8v}ZGzaIeJgDh3p?^IJ1ESF zGT)sN8N0h{)!T)llX3-E3@0gAZY}Elo??03He!zw^Q84#%@-FfE%o^CtvPq`Y8$pg zSCYP5__pSq&1TE$)K6i#i}(ygYP2dW90ip;D#MOSo%?9AdF{Q|rrEbr7rkBBULB-& zWUaVTq;82zB2RwC37_z_-gepkxyRe&*4fsU&hx&LH~;#yyxk>lwrQuGF$|3tWc|ma zJAcbQfjiE2pAt&Fd|$~NUsC@#l0&fIso|-E!Q4_hJLh#pHne=1BslE-~E|J=o2m$7s89QqI{qdu7~e5_aE^w!dV5Of5&o z%suLV`NPbzhb#qp21+&!oeN@4YRUY)bbQ9XTUV_eL*D%;cl`0`ioSyG>dU2b`hNdm zU6LCeyZy1r?)PtUHsoIsS3e$8@YTvc?Y=pGep(76YbrMkWyk z+=U3xSO{nYQLrKfSr@hp4Ale7G`pa>;Mo~nD|+@qXboq?mD$kEKo1Cn86TO^gC)S5 Rl?^1%0)!udv@;8c2LR#77DE64 literal 0 HcmV?d00001 diff --git a/osu.Game.Tests/Skins/SkinDeserialisationTest.cs b/osu.Game.Tests/Skins/SkinDeserialisationTest.cs index 82d204f134..72581f5513 100644 --- a/osu.Game.Tests/Skins/SkinDeserialisationTest.cs +++ b/osu.Game.Tests/Skins/SkinDeserialisationTest.cs @@ -52,7 +52,9 @@ namespace osu.Game.Tests.Skins // Covers player avatar and flag. "Archives/modified-argon-20230305.osk", // Covers key counters - "Archives/modified-argon-pro-20230618.osk" + "Archives/modified-argon-pro-20230618.osk", + // Covers rank display + "Archives/modified-argon-20230806.osk" }; /// From 92bf363ecf499a8a4b6ca9b56b63dd6024c40877 Mon Sep 17 00:00:00 2001 From: nanashi-1 Date: Sun, 6 Aug 2023 20:43:09 +0800 Subject: [PATCH 0075/2493] use Drawable Rank --- .../Screens/Play/HUD/DefaultRankDisplay.cs | 40 ++++++++++++++++ .../Screens/Play/HUD/GameplayRankDisplay.cs | 14 ++++++ osu.Game/Screens/Play/HUD/RankDisplay.cs | 46 ------------------- 3 files changed, 54 insertions(+), 46 deletions(-) create mode 100644 osu.Game/Screens/Play/HUD/DefaultRankDisplay.cs create mode 100644 osu.Game/Screens/Play/HUD/GameplayRankDisplay.cs delete mode 100644 osu.Game/Screens/Play/HUD/RankDisplay.cs diff --git a/osu.Game/Screens/Play/HUD/DefaultRankDisplay.cs b/osu.Game/Screens/Play/HUD/DefaultRankDisplay.cs new file mode 100644 index 0000000000..a686b6d9fb --- /dev/null +++ b/osu.Game/Screens/Play/HUD/DefaultRankDisplay.cs @@ -0,0 +1,40 @@ +// 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.Game.Online.Leaderboards; +using osu.Game.Rulesets.Scoring; +using osuTK; + + +namespace osu.Game.Screens.Play.HUD +{ + public partial class DefaultRankDisplay : GameplayRankDisplay + { + [Resolved] + private ScoreProcessor scoreProcessor { get; set; } = null!; + + private UpdateableRank rank; + + public DefaultRankDisplay() + { + Size = new Vector2(70, 35); + + InternalChildren = new Drawable[] { + rank = new UpdateableRank(Scoring.ScoreRank.X) { + RelativeSizeAxes = Axes.Both + }, + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + rank.Rank = scoreProcessor.Rank.Value; + + scoreProcessor.Rank.BindValueChanged(v => rank.Rank = v.NewValue); + } + } +} \ No newline at end of file diff --git a/osu.Game/Screens/Play/HUD/GameplayRankDisplay.cs b/osu.Game/Screens/Play/HUD/GameplayRankDisplay.cs new file mode 100644 index 0000000000..402a733abd --- /dev/null +++ b/osu.Game/Screens/Play/HUD/GameplayRankDisplay.cs @@ -0,0 +1,14 @@ +// 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.Skinning; +using osu.Framework.Graphics.Containers; + + +namespace osu.Game.Screens.Play.HUD +{ + public abstract partial class GameplayRankDisplay : Container, ISerialisableDrawable + { + public bool UsesFixedAnchor { get; set; } + } +} diff --git a/osu.Game/Screens/Play/HUD/RankDisplay.cs b/osu.Game/Screens/Play/HUD/RankDisplay.cs deleted file mode 100644 index be8841b647..0000000000 --- a/osu.Game/Screens/Play/HUD/RankDisplay.cs +++ /dev/null @@ -1,46 +0,0 @@ -// 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.Game.Rulesets.Scoring; -using osu.Game.Skinning; -using osu.Framework.Graphics; -using osu.Game.Graphics.Sprites; -using osu.Framework.Graphics.Sprites; -using osu.Framework.Extensions; - -namespace osu.Game.Screens.Play.HUD -{ - public partial class RankDisplay : FontAdjustableSkinComponent - { - [Resolved] - private ScoreProcessor scoreProcessor { get; set; } = null!; - - private readonly OsuSpriteText text; - - public RankDisplay() - { - AutoSizeAxes = Axes.Both; - - InternalChildren = new Drawable[] - { - text = new OsuSpriteText - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - } - }; - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - text.Text = scoreProcessor.Rank.Value.GetDescription(); - - scoreProcessor.Rank.BindValueChanged(v => text.Text = v.NewValue.GetDescription()); - } - - protected override void SetFont(FontUsage font) => text.Font = font.With(size: 40); - } -} From f3f7d1ba7c97ae6ca35092df6987fbed0b17b70a Mon Sep 17 00:00:00 2001 From: nanashi-1 Date: Mon, 7 Aug 2023 09:50:24 +0800 Subject: [PATCH 0076/2493] remove measly abstract --- osu.Game/Screens/Play/HUD/DefaultRankDisplay.cs | 7 +++++-- osu.Game/Screens/Play/HUD/GameplayRankDisplay.cs | 14 -------------- 2 files changed, 5 insertions(+), 16 deletions(-) delete mode 100644 osu.Game/Screens/Play/HUD/GameplayRankDisplay.cs diff --git a/osu.Game/Screens/Play/HUD/DefaultRankDisplay.cs b/osu.Game/Screens/Play/HUD/DefaultRankDisplay.cs index a686b6d9fb..433acf678a 100644 --- a/osu.Game/Screens/Play/HUD/DefaultRankDisplay.cs +++ b/osu.Game/Screens/Play/HUD/DefaultRankDisplay.cs @@ -3,19 +3,22 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Game.Online.Leaderboards; using osu.Game.Rulesets.Scoring; +using osu.Game.Skinning; using osuTK; namespace osu.Game.Screens.Play.HUD { - public partial class DefaultRankDisplay : GameplayRankDisplay + public partial class DefaultRankDisplay : Container, ISerialisableDrawable { [Resolved] private ScoreProcessor scoreProcessor { get; set; } = null!; + public bool UsesFixedAnchor { get; set; } - private UpdateableRank rank; + private readonly UpdateableRank rank; public DefaultRankDisplay() { diff --git a/osu.Game/Screens/Play/HUD/GameplayRankDisplay.cs b/osu.Game/Screens/Play/HUD/GameplayRankDisplay.cs deleted file mode 100644 index 402a733abd..0000000000 --- a/osu.Game/Screens/Play/HUD/GameplayRankDisplay.cs +++ /dev/null @@ -1,14 +0,0 @@ -// 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.Skinning; -using osu.Framework.Graphics.Containers; - - -namespace osu.Game.Screens.Play.HUD -{ - public abstract partial class GameplayRankDisplay : Container, ISerialisableDrawable - { - public bool UsesFixedAnchor { get; set; } - } -} From eb3d3b51e204fef93fdd80e59218e8fb261ae275 Mon Sep 17 00:00:00 2001 From: nanashi-1 Date: Mon, 7 Aug 2023 09:50:44 +0800 Subject: [PATCH 0077/2493] add legacy rank display --- osu.Game/Skinning/LegacyRankDisplay.cs | 47 ++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 osu.Game/Skinning/LegacyRankDisplay.cs diff --git a/osu.Game/Skinning/LegacyRankDisplay.cs b/osu.Game/Skinning/LegacyRankDisplay.cs new file mode 100644 index 0000000000..0ae3b45107 --- /dev/null +++ b/osu.Game/Skinning/LegacyRankDisplay.cs @@ -0,0 +1,47 @@ +// 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; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Textures; +using osu.Game.Graphics.Sprites; +using osu.Game.Rulesets.Scoring; + +namespace osu.Game.Skinning +{ + public partial class LegacyRankDisplay : CompositeDrawable, ISerialisableDrawable + { + public bool UsesFixedAnchor { get; set; } + + [Resolved] + private ScoreProcessor scoreProcessor { get; set; } = null!; + + [Resolved] + private ISkinSource source { get; set; } = null!; + + private readonly Sprite rank; + + public LegacyRankDisplay() + { + AutoSizeAxes = Axes.Both; + + AddInternal(rank = new Sprite()); + } + + protected override void LoadComplete() + { + + var skin = source.FindProvider(s => getTexture(s, "A") != null); + + rank.Texture = getTexture(skin, scoreProcessor.Rank.Value.ToString()); + + scoreProcessor.Rank.BindValueChanged(v => rank.Texture = getTexture(skin, v.NewValue.ToString())); + } + + private static Texture getTexture(ISkin skin, string name) => skin?.GetTexture($"ranking-{name}"); + } +} \ No newline at end of file From 9bdff29dd72c24b7b48292d7f7312363a16a1e2e Mon Sep 17 00:00:00 2001 From: nanashi-1 Date: Mon, 7 Aug 2023 09:50:54 +0800 Subject: [PATCH 0078/2493] add visual test --- .../Gameplay/TestSceneSkinnableRankDisplay.cs | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableRankDisplay.cs diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableRankDisplay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableRankDisplay.cs new file mode 100644 index 0000000000..dc8b3d994b --- /dev/null +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableRankDisplay.cs @@ -0,0 +1,37 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.Scoring; +using osu.Game.Screens.Play.HUD; +using osu.Game.Skinning; + +namespace osu.Game.Tests.Visual.Gameplay +{ + public partial class TestSceneSkinnableRankDisplay : SkinnableHUDComponentTestScene + { + [Cached] + private ScoreProcessor scoreProcessor = new ScoreProcessor(new OsuRuleset()); + + protected override Drawable CreateDefaultImplementation() => new DefaultRankDisplay(); + + protected override Drawable CreateLegacyImplementation() => new LegacyRankDisplay(); + + [Test] + public void TestChangingRank() + { + AddStep("Set rank to SS Hidden", () => scoreProcessor.Rank.Value = Scoring.ScoreRank.XH); + AddStep("Set rank to SS", () => scoreProcessor.Rank.Value = Scoring.ScoreRank.X); + AddStep("Set rank to S Hidden", () => scoreProcessor.Rank.Value = Scoring.ScoreRank.SH); + AddStep("Set rank to S", () => scoreProcessor.Rank.Value = Scoring.ScoreRank.S); + AddStep("Set rank to A", () => scoreProcessor.Rank.Value = Scoring.ScoreRank.A); + AddStep("Set rank to B", () => scoreProcessor.Rank.Value = Scoring.ScoreRank.B); + AddStep("Set rank to C", () => scoreProcessor.Rank.Value = Scoring.ScoreRank.C); + AddStep("Set rank to D", () => scoreProcessor.Rank.Value = Scoring.ScoreRank.D); + AddStep("Set rank to F", () => scoreProcessor.Rank.Value = Scoring.ScoreRank.F); + } + } +} \ No newline at end of file From bd67e933105435ee161d8b639cd7221fec88e003 Mon Sep 17 00:00:00 2001 From: nanashi-1 Date: Mon, 7 Aug 2023 11:16:51 +0800 Subject: [PATCH 0079/2493] fix code format --- osu.Game/Screens/Play/HUD/DefaultRankDisplay.cs | 10 ++++++---- osu.Game/Skinning/LegacyRankDisplay.cs | 2 -- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/DefaultRankDisplay.cs b/osu.Game/Screens/Play/HUD/DefaultRankDisplay.cs index 433acf678a..09ab7d156c 100644 --- a/osu.Game/Screens/Play/HUD/DefaultRankDisplay.cs +++ b/osu.Game/Screens/Play/HUD/DefaultRankDisplay.cs @@ -9,13 +9,13 @@ using osu.Game.Rulesets.Scoring; using osu.Game.Skinning; using osuTK; - namespace osu.Game.Screens.Play.HUD { public partial class DefaultRankDisplay : Container, ISerialisableDrawable { [Resolved] private ScoreProcessor scoreProcessor { get; set; } = null!; + public bool UsesFixedAnchor { get; set; } private readonly UpdateableRank rank; @@ -24,11 +24,13 @@ namespace osu.Game.Screens.Play.HUD { Size = new Vector2(70, 35); - InternalChildren = new Drawable[] { - rank = new UpdateableRank(Scoring.ScoreRank.X) { + InternalChildren = new Drawable[] + { + rank = new UpdateableRank(Scoring.ScoreRank.X) + { RelativeSizeAxes = Axes.Both }, - }; + }; } protected override void LoadComplete() diff --git a/osu.Game/Skinning/LegacyRankDisplay.cs b/osu.Game/Skinning/LegacyRankDisplay.cs index 0ae3b45107..83d4299360 100644 --- a/osu.Game/Skinning/LegacyRankDisplay.cs +++ b/osu.Game/Skinning/LegacyRankDisplay.cs @@ -8,7 +8,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; -using osu.Game.Graphics.Sprites; using osu.Game.Rulesets.Scoring; namespace osu.Game.Skinning @@ -34,7 +33,6 @@ namespace osu.Game.Skinning protected override void LoadComplete() { - var skin = source.FindProvider(s => getTexture(s, "A") != null); rank.Texture = getTexture(skin, scoreProcessor.Rank.Value.ToString()); From 07b4f6115b8d35fbb9a03a6399846e5bbeef9db9 Mon Sep 17 00:00:00 2001 From: nanashi-1 Date: Mon, 7 Aug 2023 20:26:32 +0800 Subject: [PATCH 0080/2493] use small --- osu.Game/Skinning/LegacyRankDisplay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Skinning/LegacyRankDisplay.cs b/osu.Game/Skinning/LegacyRankDisplay.cs index 83d4299360..cd121deb8c 100644 --- a/osu.Game/Skinning/LegacyRankDisplay.cs +++ b/osu.Game/Skinning/LegacyRankDisplay.cs @@ -40,6 +40,6 @@ namespace osu.Game.Skinning scoreProcessor.Rank.BindValueChanged(v => rank.Texture = getTexture(skin, v.NewValue.ToString())); } - private static Texture getTexture(ISkin skin, string name) => skin?.GetTexture($"ranking-{name}"); + private static Texture getTexture(ISkin skin, string name) => skin?.GetTexture($"ranking-{name}-small"); } } \ No newline at end of file From fed338a42b12a248b902f2ea1389f06fcec0ec70 Mon Sep 17 00:00:00 2001 From: nanashi-1 Date: Wed, 9 Aug 2023 07:34:30 +0800 Subject: [PATCH 0081/2493] improve code --- osu.Game/Skinning/LegacyRankDisplay.cs | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/osu.Game/Skinning/LegacyRankDisplay.cs b/osu.Game/Skinning/LegacyRankDisplay.cs index cd121deb8c..b663f52097 100644 --- a/osu.Game/Skinning/LegacyRankDisplay.cs +++ b/osu.Game/Skinning/LegacyRankDisplay.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; @@ -33,13 +31,7 @@ namespace osu.Game.Skinning protected override void LoadComplete() { - var skin = source.FindProvider(s => getTexture(s, "A") != null); - - rank.Texture = getTexture(skin, scoreProcessor.Rank.Value.ToString()); - - scoreProcessor.Rank.BindValueChanged(v => rank.Texture = getTexture(skin, v.NewValue.ToString())); + scoreProcessor.Rank.BindValueChanged(v => rank.Texture = source.GetTexture($"ranking-{v.NewValue}-small"), true); } - - private static Texture getTexture(ISkin skin, string name) => skin?.GetTexture($"ranking-{name}-small"); } } \ No newline at end of file From 94613b42e4622a80d724f0e2d700f68f2986c7d9 Mon Sep 17 00:00:00 2001 From: nanashi-1 Date: Wed, 9 Aug 2023 08:33:50 +0800 Subject: [PATCH 0082/2493] update tests --- .../Archives/modified-classic-20230809.osk | Bin 0 -> 1603 bytes .../Archives/modified-default-20230809.osk | Bin 0 -> 2141 bytes osu.Game.Tests/Skins/SkinDeserialisationTest.cs | 6 ++++-- 3 files changed, 4 insertions(+), 2 deletions(-) create mode 100644 osu.Game.Tests/Resources/Archives/modified-classic-20230809.osk create mode 100644 osu.Game.Tests/Resources/Archives/modified-default-20230809.osk diff --git a/osu.Game.Tests/Resources/Archives/modified-classic-20230809.osk b/osu.Game.Tests/Resources/Archives/modified-classic-20230809.osk new file mode 100644 index 0000000000000000000000000000000000000000..b200ab126188d5d056f8d0b11ba37ef9fbc715cf GIT binary patch literal 1603 zcmWIWW@Zs#U|`^2xZ}VX-n-?LnWvYTmwC3|n(vT-fJ^oKq!&#r zDodP7LP|mc6uVUKHMH*S&T$t^U;Mx$rm=s)Bc>^fE30PhjL%Q){5tXCs_(a~Ef)ly z)RDAR4VZ2kP{vzX&lq`H>^HYbDf11zeFfs@q&{<0dz}wrUHe%_MB=G;mZegGn)gh# zwB0EWJ?hfK{FDBPna&F;;_X^+ZEN3KfukiEVzG8vbCQ+SFUIx!yu9#6-Qyq3J+D`- zTODb7_wSqvh5!^FElG=;+y?Z?G9c!K`Y1CmEnhFII6u#{cO%y!0}yzAT>)Sz5C*M&Em(6e<%On^HgA>a-zz4UF-dFCojJbiDOSv zdzYFltCwtZTkfp(MIkfKl==Fo4qn7Dvsn}9P#qxV2jbxTy!7DIoYdqJu#;Cm3knD* z()7~t_c`VH^^EuF@J&H0*t!^9FBgBBEL7a^=S9Q<2igBCI$uzK?Vjf zpsBuznRy}_J{A+TOZ!HTI`PLNh zWQqH$t6L|&J)m{uzn%J&N!t>IS8NeI7=KlPsPWM!U+}m@oe5)HgK+ z?{v~GHl6M%`dt%fZ(Y1++uXD34n|o8O@F~Xmr2TRhJcUA3u_UR`Cp%!Oshrnpf*&+qITO+5 zI;;3kKdaUuHM8miVi#(gpNB|UJ&&wfYkWqt-K?xxoZX z(K^rN|FqNJZ$F80D%ZarIB)u@b(`*Li~Z8z5@8)i2 z7pF-r2lYy;m&r8CYERc@Sa|V3n5dPdy_@=(njo)L@(I-|+~ikAIepvvsp^TY>6bfZ zyc>UK@y?d~qvy`*<2YlAG2=?f%{59ntdaW@RBy%!%|`z5bNl*8=kyv14`%R_t}mfkw^%Ji_e-JG-o^K+#0zOBCfAtoc3_x|!L zIkhZ@^BM|-e%z~4?=;~0YtVI=w{()g(YiS*_o5c;RIEF?pW*oLyFtIcKD+B1e$dcM zNk-*$`m}l7HTK_P=5LXYja!%d)qIoGv4c@-cDDX`-m_(H+T`%7TQA+7CiBJgtgh?R zrg>i_FIS20dCODbbu{I+aKicGS<#V@p76axIMkWyk+yx-e zSO{nYQLy3=T^D*G1J%R8(0B=|3tr@)YemoQ2(8V)G8irMqiaUb)da3BBx literal 0 HcmV?d00001 diff --git a/osu.Game.Tests/Resources/Archives/modified-default-20230809.osk b/osu.Game.Tests/Resources/Archives/modified-default-20230809.osk new file mode 100644 index 0000000000000000000000000000000000000000..a46c20d2b83309aeadcc6a563d504a7db203258b GIT binary patch literal 2141 zcmZ`)2{ha37XMpft$mrG)U+6}L^8EaJ6ijiR>V3zc#XBOB-TMtl13E?+Nz?KVv3ep zYfx&NDiV8JS`td_<)LberIYscy&m3s-*@i$&UerG-Shpvdw=&LtvEQv0RZ3u)bx2B zX*@;jL#$f?=!*vcf_*VqCq)X7Xj4jkBIfpkn`LHK&5qG64&<6xQ57e;|UG1b_;1UR{^{9)8^NC!S zPJUFf(IFY63y+zjuXx$lp^8?iXVY^jJCF0)r`C^ye5#aWBR)LKsqg-XE30qM4n5$K zbafv;H@?k`Iei|!t`}L^rx#tjDTqA8+b<>FGM&X&j12$;zVXIjy>L*UU>r6lrHZc| zCT_BIwd1rczrXoOx4_U;D0ERlmi+>)aG75LA~@CJJ3w}!L$~OYx!zo@eR%C9poi(n z5fM;=$slXU=lRF$WtGQ9Cfa-BpCR&5h7cRtQx`PKyq22wX20!ELuxdQ8qS<23l4Wo z=ty^;n;+AezE%`_=Kh20_}$a*i&GR^$mS%?Ro`mz1zt>R^@z-20yfie+)Q==AV>j# zB#XcWGzM#KZ)}9~55QqPu_3`@6+!lCIeqqJ^^$Nny}t6!?T1j_Ha zQ1Q@l#Pm-NyBnXQFIr$Mi33ley_}0iGZQW=eFuz&u;z)Hvi)3zI*+b*{3nw3g@&KC z9m|5j6?)1ve(+W8{Gob%M+@BDjD#=|Ua?$NobUKHq%sR{C&?|gW&D8%>+n;4dU8hE zgLdgrU0I&Igv6e3VMnoNC?!Z`GS%pjB3ivPPf&`|(zfS}Ae{J^R<%E@LTpCb$?#jR z&2Hzg`Hh(RldHy5EN4xp!gbRlrOBK5VvJV-`L2BPhm1=4ljg)+@L>F-TF(3QS zfR3s&^$BH<^92f)m6L5PrXm?K@fvxCctFjp?Qd3L|2t27iu=aZdfoE+!4t zWed|OH|#Vn{}GqzE#tV>bRFBbUJ;VkdaOPfJR`ioODU#{rw<0_fn@lVP*MU3WNb-+ z5w#?D?(Lw;Odo;{juB~Kvk)yoB~cuC2gzNKF7t(*sh%Y*S<~yS4%J%ddm|)mY~p-YaGuIvQ-E8WS^r3_wM2P} zbAMn?B8QNkOeCRE5hXkAJ`$=|@oTf1;ypUt_lg+L0ahA`Jd4ldh4Y7rjb}CGjfiD`^ zi<=~`Vi=jz8BrMSi&3=cs?^`FQTB#|cFW zwXhvl%NdvU*&3GAQ@8*?h@}==9M;>`)6dgAxFS-@y38-Ohkwo!reSa`>3zayfT!p+JVZ@F0?qW;^)sKl7jwckvTRQHoe8^lU~8YGmLbvA9RV?7f?_VU`W8EBa2s3#!GwB{-5RuD z@wID%49OEN#34kYMeJC=nJtZC4->geDWIF3z8CBeb*hwy(HTEYhS(fv24)9oT|k?j6kWDo%m}X> zxT)-bYOs&jTP=388h#~MBZtp7h(Y_-k1zj*EYH^vBnL7^QZ@z~$IqU;dcR-)x4==+ z@y)Zngc0nD;*6xvN*gs!yvRl~JaHj$%@;(OE}bYe+xskC1n+{x4}oyAVnmB z^LoF36Mivp-gSG`=qmNO`7KTAkggn9s?Kh6E&j#Yk{RXWqo{KG(HODUg-ngZ`8|He z4X?z?D}-f&EJOOCBfK!4ejZ=bTRMwEB7-27YSsv<#YT@6&9NF)!j;HovYuo`$ewqS zuShSI1dvv2?Bc-xAC0WA|5}7E$G->VQPk1F@(sgk)Z5=s{~0q!p+{T#cW48vbpLmQ tKN{m`p?{At$N67ne-wQ57r%o$S-Ls=r${Rh%Lf1;)`qg4C&m5c_BT%1iFW`1 literal 0 HcmV?d00001 diff --git a/osu.Game.Tests/Skins/SkinDeserialisationTest.cs b/osu.Game.Tests/Skins/SkinDeserialisationTest.cs index 72581f5513..7fa10559dc 100644 --- a/osu.Game.Tests/Skins/SkinDeserialisationTest.cs +++ b/osu.Game.Tests/Skins/SkinDeserialisationTest.cs @@ -53,8 +53,10 @@ namespace osu.Game.Tests.Skins "Archives/modified-argon-20230305.osk", // Covers key counters "Archives/modified-argon-pro-20230618.osk", - // Covers rank display - "Archives/modified-argon-20230806.osk" + // Covers default rank display + "Archives/modified-default-20230809.osk", + // Covers legacy rank display + "Archives/modified-classic-20230809.osk" }; /// From 2c3d5dc21a31b4d65d618fbe8dedf714135f32a2 Mon Sep 17 00:00:00 2001 From: nanashi-1 Date: Wed, 9 Aug 2023 08:44:01 +0800 Subject: [PATCH 0083/2493] remove unnecesary directive --- osu.Game/Skinning/LegacyRankDisplay.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Skinning/LegacyRankDisplay.cs b/osu.Game/Skinning/LegacyRankDisplay.cs index b663f52097..38ece4e5e4 100644 --- a/osu.Game/Skinning/LegacyRankDisplay.cs +++ b/osu.Game/Skinning/LegacyRankDisplay.cs @@ -5,7 +5,6 @@ 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.Game.Rulesets.Scoring; namespace osu.Game.Skinning From 56eaf48892e23dbb5245cbe78e742be3e0faedd1 Mon Sep 17 00:00:00 2001 From: nanashi-1 Date: Wed, 9 Aug 2023 11:29:52 +0800 Subject: [PATCH 0084/2493] remove unnecessary archive --- .../Archives/modified-argon-20230806.osk | Bin 1354 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 osu.Game.Tests/Resources/Archives/modified-argon-20230806.osk diff --git a/osu.Game.Tests/Resources/Archives/modified-argon-20230806.osk b/osu.Game.Tests/Resources/Archives/modified-argon-20230806.osk deleted file mode 100644 index e7c035811d62df429b930fd41548457307cbf90a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1354 zcmWIWW@Zs#U|`^2_)x_b{xqpXate^Q9xTGZP@J8ar42}OmnuF-Rk~{%W2L3)BpF`hq7LuxG?;EX_=A3@+2KLW50%z zCJT1*?Tu&XoUtI*VfhwjufoTAdrWT0Ma{Xkf@$w(9SMn5xn44g8x$8lRNOs3(qd74 z%Jfi^|ABpl0Vfx(=r4_R5}XqAe#WUcQ*y4{IN&>P(dHEU0JzIP0$qM&RRUiT(A6D4 z%nQUImuKdsz#gq{ny90<(fJCWZu^Z35nV4SU;s zezd&&{rb(l6CZWTX0CB?f8})STDef8DX&0-clIHVq~7&z*EpW3q-AL+#q+qH5nYgJ zvuyS;c?VC{_qW5o8TKyp>l6GXlhoQW^O(vvN!~iABF$+Be%qw2$$KZ^)3U@X{KluV zAFX*U)-Jnh)Sa;?@cZP{{EfZ~ue1iJ=uZv0$-0a&G2qsM8Pleh$E~n?aL_4X)&OEvgYy;j{w*|8v}ZGzaIeJgDh3p?^IJ1ESF zGT)sN8N0h{)!T)llX3-E3@0gAZY}Elo??03He!zw^Q84#%@-FfE%o^CtvPq`Y8$pg zSCYP5__pSq&1TE$)K6i#i}(ygYP2dW90ip;D#MOSo%?9AdF{Q|rrEbr7rkBBULB-& zWUaVTq;82zB2RwC37_z_-gepkxyRe&*4fsU&hx&LH~;#yyxk>lwrQuGF$|3tWc|ma zJAcbQfjiE2pAt&Fd|$~NUsC@#l0&fIso|-E!Q4_hJLh#pHne=1BslE-~E|J=o2m$7s89QqI{qdu7~e5_aE^w!dV5Of5&o z%suLV`NPbzhb#qp21+&!oeN@4YRUY)bbQ9XTUV_eL*D%;cl`0`ioSyG>dU2b`hNdm zU6LCeyZy1r?)PtUHsoIsS3e$8@YTvc?Y=pGep(76YbrMkWyk z+=U3xSO{nYQLrKfSr@hp4Ale7G`pa>;Mo~nD|+@qXboq?mD$kEKo1Cn86TO^gC)S5 Rl?^1%0)!udv@;8c2LR#77DE64 From cc4e11a5ac3f6ab2debfa10f08c402621f8acb79 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Wed, 16 Aug 2023 20:48:52 +0200 Subject: [PATCH 0085/2493] Ensure populated node samples so new objects have unique node sample lists --- osu.Game.Rulesets.Catch/Objects/JuiceStream.cs | 2 ++ osu.Game.Rulesets.Osu/Objects/Slider.cs | 2 ++ osu.Game/Rulesets/Objects/Types/IHasRepeats.cs | 15 +++++++++++++++ 3 files changed, 19 insertions(+) diff --git a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs index 169e99c90c..d9bbbedfcf 100644 --- a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs +++ b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs @@ -72,6 +72,8 @@ namespace osu.Game.Rulesets.Catch.Objects { base.CreateNestedHitObjects(cancellationToken); + this.PopulateNodeSamples(); + var dropletSamples = Samples.Select(s => s.With(@"slidertick")).ToList(); int nodeIndex = 0; diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs index 4189f8ba1e..5ae76f1ac7 100644 --- a/osu.Game.Rulesets.Osu/Objects/Slider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs @@ -246,6 +246,8 @@ namespace osu.Game.Rulesets.Osu.Objects protected void UpdateNestedSamples() { + this.PopulateNodeSamples(); + var firstSample = Samples.FirstOrDefault(s => s.Name == HitSampleInfo.HIT_NORMAL) ?? Samples.FirstOrDefault(); // TODO: remove this when guaranteed sort is present for samples (https://github.com/ppy/osu/issues/1933) var sampleList = new List(); diff --git a/osu.Game/Rulesets/Objects/Types/IHasRepeats.cs b/osu.Game/Rulesets/Objects/Types/IHasRepeats.cs index 2a4215b960..9677ac4fbd 100644 --- a/osu.Game/Rulesets/Objects/Types/IHasRepeats.cs +++ b/osu.Game/Rulesets/Objects/Types/IHasRepeats.cs @@ -3,6 +3,7 @@ using osu.Game.Audio; using System.Collections.Generic; +using System.Linq; namespace osu.Game.Rulesets.Objects.Types { @@ -45,5 +46,19 @@ namespace osu.Game.Rulesets.Objects.Types public static IList GetNodeSamples(this T obj, int nodeIndex) where T : HitObject, IHasRepeats => nodeIndex < obj.NodeSamples.Count ? obj.NodeSamples[nodeIndex] : obj.Samples; + + /// + /// Ensures that the list of node samples is at least as long as the number of nodes. + /// + /// The . + public static void PopulateNodeSamples(this T obj) + where T : HitObject, IHasRepeats + { + if (obj.NodeSamples.Count >= obj.RepeatCount + 2) + return; + + while (obj.NodeSamples.Count < obj.RepeatCount + 2) + obj.NodeSamples.Add(obj.Samples.Select(o => o.With()).ToList()); + } } } From 02b7c8f27be47d968728a1fec3eb0eb77a3a7cf5 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Wed, 16 Aug 2023 21:15:47 +0200 Subject: [PATCH 0086/2493] Move sliderbody hs to middle of first span --- .../Timeline/TimelineHitObjectBlueprint.cs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs index f41daf30ce..d3a045db07 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs @@ -105,17 +105,17 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline }, } }, - sampleComponents = new Container - { - RelativeSizeAxes = Axes.Both, - }, samplePointPiece = new SamplePointPiece(Item) { Anchor = Anchor.BottomLeft, Origin = Anchor.TopCentre, - X = Item is IHasRepeats ? 30 : 0, + RelativePositionAxes = Axes.X, AlternativeColor = Item is IHasRepeats }, + sampleComponents = new Container + { + RelativeSizeAxes = Axes.Both, + }, }); if (item is IHasDuration) @@ -262,6 +262,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline Origin = Anchor.TopCentre }); } + + samplePointPiece.X = 1f / (repeats.RepeatCount + 1) / 2; } protected override bool ShouldBeConsideredForInput(Drawable child) => true; From a938b810b467d5d89acfe03ce43cf493a5e55d8d Mon Sep 17 00:00:00 2001 From: OliBomby Date: Wed, 16 Aug 2023 22:07:36 +0200 Subject: [PATCH 0087/2493] Add keybind for bank setting --- .../Components/Timeline/SamplePointPiece.cs | 31 ++++++++++++++++--- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs index de85435d02..664998c267 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs @@ -182,7 +182,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline }); // on commit, ensure that the value is correct by sourcing it from the objects' samples again. // this ensures that committing empty text causes a revert to the previous value. - bank.OnCommit += (_, _) => bank.Current.Value = getCommonBank(); + bank.OnCommit += (_, _) => updateBankText(); updateAdditionBankText(); updateAdditionBankVisual(); @@ -261,6 +261,11 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline }); } + private void updateBankText() + { + bank.Current.Value = getCommonBank(); + } + private void updateBankPlaceholderText() { string? commonBank = getCommonBank(); @@ -305,6 +310,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline private readonly Dictionary> selectionSampleStates = new Dictionary>(); + private readonly List banks = new List(); + private void createStateBindables() { foreach (string sampleName in HitSampleInfo.AllAdditions) @@ -330,6 +337,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline selectionSampleStates[sampleName] = bindable; } + + banks.AddRange(HitSampleInfo.AllBanks); } private void updateTernaryStates() @@ -386,14 +395,26 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline protected override bool OnKeyDown(KeyDownEvent e) { - if (e.ControlPressed || e.AltPressed || e.SuperPressed || e.ShiftPressed || !checkRightToggleFromKey(e.Key, out int rightIndex)) + if (e.ControlPressed || e.AltPressed || e.SuperPressed || !checkRightToggleFromKey(e.Key, out int rightIndex)) return base.OnKeyDown(e); - var item = togglesCollection.ElementAtOrDefault(rightIndex); + if (e.ShiftPressed) + { + string? bank = banks.ElementAtOrDefault(rightIndex); + updateBank(bank); + updateBankText(); + updateAdditionBank(bank); + updateAdditionBankText(); + } + else + { + var item = togglesCollection.ElementAtOrDefault(rightIndex); - if (item is not DrawableTernaryButton button) return base.OnKeyDown(e); + if (item is not DrawableTernaryButton button) return base.OnKeyDown(e); + + button.Button.Toggle(); + } - button.Button.Toggle(); return true; } From fd54c329fa59b0b270bfe1d94791fcf9507eae5d Mon Sep 17 00:00:00 2001 From: OliBomby Date: Wed, 16 Aug 2023 22:08:49 +0200 Subject: [PATCH 0088/2493] dont focus volume, so keybinds immediately available --- .../Edit/Compose/Components/Timeline/SamplePointPiece.cs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs index 664998c267..97397ba2da 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs @@ -200,12 +200,6 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline togglesCollection.AddRange(createTernaryButtons().Select(b => new DrawableTernaryButton(b) { RelativeSizeAxes = Axes.None, Size = new Vector2(40, 40) })); } - protected override void LoadComplete() - { - base.LoadComplete(); - ScheduleAfterChildren(() => GetContainingInputManager().ChangeFocus(volume)); - } - private string? getCommonBank() => allRelevantSamples.Select(GetBankValue).Distinct().Count() == 1 ? GetBankValue(allRelevantSamples.First()) : null; private string? getCommonAdditionBank() => allRelevantSamples.Select(GetAdditionBankValue).Distinct().Count() == 1 ? GetAdditionBankValue(allRelevantSamples.First()) : null; private int? getCommonVolume() => allRelevantSamples.Select(GetVolumeValue).Distinct().Count() == 1 ? GetVolumeValue(allRelevantSamples.First()) : null; From 4ff58c681803b323b3d4f7844d53908afcc7b97f Mon Sep 17 00:00:00 2001 From: OliBomby Date: Wed, 16 Aug 2023 22:10:59 +0200 Subject: [PATCH 0089/2493] fix no nodesample test case --- osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs index 4ad78a3190..3fac7c8c6d 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs @@ -133,6 +133,7 @@ namespace osu.Game.Rulesets.Osu.Tests { slider = (DrawableSlider)createSlider(repeats: 1); Add(slider); + slider.HitObject.NodeSamples.Clear(); }); AddStep("change samples", () => slider.HitObject.Samples = new[] From 88e6fe72dc6e679f0c44719fd3db4c15013779ba Mon Sep 17 00:00:00 2001 From: OliBomby Date: Wed, 16 Aug 2023 23:09:04 +0200 Subject: [PATCH 0090/2493] fix code quality --- .../Edit/Compose/Components/Timeline/SamplePointPiece.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs index 97397ba2da..268fb073d7 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs @@ -394,10 +394,10 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline if (e.ShiftPressed) { - string? bank = banks.ElementAtOrDefault(rightIndex); - updateBank(bank); + string? newBank = banks.ElementAtOrDefault(rightIndex); + updateBank(newBank); updateBankText(); - updateAdditionBank(bank); + updateAdditionBank(newBank); updateAdditionBankText(); } else From 080d2b62f4f459f33a398ed1878d2ce0dca0e9a6 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Wed, 16 Aug 2023 23:16:57 +0200 Subject: [PATCH 0091/2493] fix focus test --- .../Editing/TestSceneHitObjectSampleAdjustments.cs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneHitObjectSampleAdjustments.cs b/osu.Game.Tests/Visual/Editing/TestSceneHitObjectSampleAdjustments.cs index 050593ff94..b6a20dae54 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneHitObjectSampleAdjustments.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneHitObjectSampleAdjustments.cs @@ -81,10 +81,10 @@ namespace osu.Game.Tests.Visual.Editing } [Test] - public void TestPopoverHasFocus() + public void TestPopoverHasNoFocus() { clickSamplePiece(0); - samplePopoverHasFocus(); + samplePopoverHasNoFocus(); } [Test] @@ -417,13 +417,13 @@ namespace osu.Game.Tests.Visual.Editing InputManager.Click(MouseButton.Left); }); - private void samplePopoverHasFocus() => AddUntilStep("sample popover textbox focused", () => + private void samplePopoverHasNoFocus() => AddUntilStep("sample popover textbox not focused", () => { var popover = this.ChildrenOfType().SingleOrDefault(); var slider = popover?.ChildrenOfType>().Single(); var textbox = slider?.ChildrenOfType().Single(); - return textbox?.HasFocus == true; + return textbox?.HasFocus == false; }); private void samplePopoverHasSingleVolume(int volume) => AddUntilStep($"sample popover has volume {volume}", () => @@ -460,7 +460,6 @@ namespace osu.Game.Tests.Visual.Editing private void dismissPopover() { - AddStep("unfocus textbox", () => InputManager.Key(Key.Escape)); AddStep("dismiss popover", () => InputManager.Key(Key.Escape)); AddUntilStep("wait for dismiss", () => !this.ChildrenOfType().Any(popover => popover.IsPresent)); } From 6ed1685223bcf1c2811a04233b7e70631048a85c Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Fri, 22 Sep 2023 11:07:49 -0700 Subject: [PATCH 0092/2493] Fix/update score exporting method --- osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs b/osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs index 093037e6d1..8930ee9914 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs @@ -14,8 +14,6 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; using osu.Framework.Localisation; -using osu.Framework.Platform; -using osu.Game.Database; using osu.Game.Extensions; using osu.Game.Graphics; using osu.Game.Graphics.Containers; @@ -62,7 +60,7 @@ namespace osu.Game.Online.Leaderboards private IDialogOverlay? dialogOverlay { get; set; } [Resolved] - private Storage storage { get; set; } = null!; + private ScoreManager scoreManager { get; set; } = null!; private Container content = null!; private Box background = null!; @@ -92,7 +90,7 @@ namespace osu.Game.Online.Leaderboards } [BackgroundDependencyLoader] - private void load(ScoreManager scoreManager) + private void load() { var user = score.User; @@ -138,7 +136,7 @@ namespace osu.Game.Online.Leaderboards Width = 35 }, createCentreContent(user), - createRightSideContent(scoreManager) + createRightSideContent() } } } @@ -230,7 +228,7 @@ namespace osu.Game.Online.Leaderboards } }; - private FillFlowContainer createRightSideContent(ScoreManager scoreManager) => + private FillFlowContainer createRightSideContent() => new FillFlowContainer { Padding = new MarginPadding { Left = 11, Right = 15 }, @@ -482,7 +480,7 @@ namespace osu.Game.Online.Leaderboards if (score.Files.Count <= 0) return items.ToArray(); - items.Add(new OsuMenuItem("Export", MenuItemType.Standard, () => new LegacyScoreExporter(storage).Export(score))); + items.Add(new OsuMenuItem("Export", MenuItemType.Standard, () => scoreManager.Export(score))); items.Add(new OsuMenuItem(CommonStrings.ButtonsDelete, MenuItemType.Destructive, () => dialogOverlay?.Push(new LocalScoreDeleteDialog(score)))); return items.ToArray(); From 9f9f7eb01bc682b9c74b5b0a6014cd61c5c9a339 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Fri, 22 Sep 2023 11:15:46 -0700 Subject: [PATCH 0093/2493] Nuke hit results display --- .../Online/Leaderboards/LeaderboardScoreV2.cs | 22 ------------------- 1 file changed, 22 deletions(-) diff --git a/osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs b/osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs index 8930ee9914..18945e373f 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs @@ -23,7 +23,6 @@ using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays; using osu.Game.Resources.Localisation.Web; using osu.Game.Rulesets.Mods; -using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI; using osu.Game.Scoring; using osu.Game.Screens.Select; @@ -289,7 +288,6 @@ namespace osu.Game.Online.Leaderboards { (BeatmapsetsStrings.ShowScoreboardHeadersCombo.ToUpper(), model.MaxCombo.ToString().Insert(model.MaxCombo.ToString().Length, "x")), (BeatmapsetsStrings.ShowScoreboardHeadersAccuracy.ToUpper(), model.DisplayAccuracy), - (getResultNames(score).ToUpper(), getResults(score).ToUpper()) }; public override void Show() @@ -486,25 +484,5 @@ namespace osu.Game.Online.Leaderboards return items.ToArray(); } } - - private LocalisableString getResults(ScoreInfo score) - { - string resultString = score.GetStatisticsForDisplay() - .Where(s => s.Result.IsBasic()) - .Aggregate(string.Empty, (current, result) => - current.Insert(current.Length, $"{result.Count}/")); - - return resultString.Remove(resultString.Length - 1); - } - - private LocalisableString getResultNames(ScoreInfo score) - { - string resultName = score.GetStatisticsForDisplay() - .Where(s => s.Result.IsBasic()) - .Aggregate(string.Empty, (current, hitResult) => - current.Insert(current.Length, $"{hitResult.DisplayName.ToString().ToUpperInvariant()}/")); - - return resultName.Remove(resultName.Length - 1); - } } } From 228731493eb0f519a80216ac156be83f887275dd Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Fri, 22 Sep 2023 15:39:40 -0700 Subject: [PATCH 0094/2493] Add relative width slider to test --- .../Visual/SongSelect/TestSceneLeaderboardScoreV2.cs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneLeaderboardScoreV2.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneLeaderboardScoreV2.cs index 0823da2248..1ea461be53 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneLeaderboardScoreV2.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneLeaderboardScoreV2.cs @@ -18,6 +18,8 @@ namespace osu.Game.Tests.Visual.SongSelect { public partial class TestSceneLeaderboardScoreV2 : OsuTestScene { + private FillFlowContainer fillFlow = null!; + [BackgroundDependencyLoader] private void load() { @@ -73,12 +75,12 @@ namespace osu.Game.Tests.Visual.SongSelect }, }; - Child = new FillFlowContainer + Child = fillFlow = new FillFlowContainer { - Width = 900, Anchor = Anchor.Centre, Origin = Anchor.Centre, Spacing = new Vector2(0, 10), + RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Children = new Drawable[] { @@ -87,6 +89,11 @@ namespace osu.Game.Tests.Visual.SongSelect new LeaderboardScoreV2(scores[2], null, true) } }; + + AddSliderStep("change relative width", 0, 1f, 0.6f, v => + { + fillFlow.Width = v; + }); } } } From 2bd28e67186995d28c8ad9d2c7260706986f9c3c Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Fri, 22 Sep 2023 16:21:53 -0700 Subject: [PATCH 0095/2493] Move drawable init properties to constructor --- osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs b/osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs index 18945e373f..362eb9ea82 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs @@ -86,6 +86,10 @@ namespace osu.Game.Online.Leaderboards this.score = score; this.rank = rank; this.isPersonalBest = isPersonalBest; + + Shear = shear; + RelativeSizeAxes = Axes.X; + Height = height; } [BackgroundDependencyLoader] @@ -98,9 +102,6 @@ namespace osu.Game.Online.Leaderboards statisticsLabels = GetStatistics(score).Select(s => new ScoreComponentLabel(s, score)).ToList(); - Shear = shear; - RelativeSizeAxes = Axes.X; - Height = height; Child = content = new Container { Masking = true, From f7f390195a7744e2b2ee024a7d970c80e9cf6421 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Fri, 22 Sep 2023 16:34:06 -0700 Subject: [PATCH 0096/2493] Add user covers to centre content --- .../Visual/SongSelect/TestSceneLeaderboardScoreV2.cs | 8 +++++--- osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs | 8 ++++++++ 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneLeaderboardScoreV2.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneLeaderboardScoreV2.cs index 1ea461be53..2a6203f2d8 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneLeaderboardScoreV2.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneLeaderboardScoreV2.cs @@ -39,6 +39,7 @@ namespace osu.Game.Tests.Visual.SongSelect Id = 6602580, Username = @"waaiiru", CountryCode = CountryCode.ES, + CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c1.jpg", }, }, new ScoreInfo @@ -55,6 +56,7 @@ namespace osu.Game.Tests.Visual.SongSelect Id = 1541390, Username = @"Toukai", CountryCode = CountryCode.CA, + CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c2.jpg", }, }, @@ -68,8 +70,7 @@ namespace osu.Game.Tests.Visual.SongSelect Ruleset = new ManiaRuleset().RulesetInfo, User = new APIUser { - Id = 4608074, - Username = @"Skycries", + Username = @"No cover", CountryCode = CountryCode.BR, }, }, @@ -86,7 +87,8 @@ namespace osu.Game.Tests.Visual.SongSelect { new LeaderboardScoreV2(scores[0], 1), new LeaderboardScoreV2(scores[1], null, true), - new LeaderboardScoreV2(scores[2], null, true) + new LeaderboardScoreV2(scores[2], null, true), + new LeaderboardScoreV2(scores[2], null), } }; diff --git a/osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs b/osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs index 362eb9ea82..22efe3b88c 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs @@ -7,6 +7,7 @@ using System.Linq; using osu.Framework.Allocation; using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Effects; @@ -26,6 +27,7 @@ using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.UI; using osu.Game.Scoring; using osu.Game.Screens.Select; +using osu.Game.Users; using osu.Game.Users.Drawables; using osu.Game.Utils; using osuTK; @@ -164,6 +166,12 @@ namespace osu.Game.Online.Leaderboards RelativeSizeAxes = Axes.Both, Colour = foregroundColour }, + new UserCoverBackground + { + RelativeSizeAxes = Axes.Both, + User = score.User, + Colour = ColourInfo.GradientHorizontal(Colour4.White.Opacity(0.5f), Colour4.White.Opacity(0)), + }, avatar = new MaskedWrapper( innerAvatar = new ClickableAvatar(user) { From 236352a1760fae7ff9e2aed4fa29836edbc32cc2 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Fri, 22 Sep 2023 17:00:51 -0700 Subject: [PATCH 0097/2493] Add shadow to centre content Done this way instead of edge effect because of 1px/dimming issues. --- .../Online/Leaderboards/LeaderboardScoreV2.cs | 141 ++++++++++-------- 1 file changed, 80 insertions(+), 61 deletions(-) diff --git a/osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs b/osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs index 22efe3b88c..89ad39c98c 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs @@ -48,6 +48,7 @@ namespace osu.Game.Online.Leaderboards private Colour4 foregroundColour; private Colour4 backgroundColour; + private Colour4 shadowColour; private static readonly Vector2 shear = new Vector2(0.15f, 0); @@ -101,6 +102,7 @@ namespace osu.Game.Online.Leaderboards foregroundColour = isPersonalBest ? colourProvider.Background1 : colourProvider.Background5; backgroundColour = isPersonalBest ? colourProvider.Background2 : colourProvider.Background4; + shadowColour = isPersonalBest ? colourProvider.Background3 : colourProvider.Background6; statisticsLabels = GetStatistics(score).Select(s => new ScoreComponentLabel(s, score)).ToList(); @@ -154,86 +156,103 @@ namespace osu.Game.Online.Leaderboards private Container createCentreContent(APIUser user) => new Container { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, Masking = true, CornerRadius = corner_radius, RelativeSizeAxes = Axes.Both, - Children = new[] + Children = new Drawable[] { - foreground = new Box + new Box { RelativeSizeAxes = Axes.Both, - Colour = foregroundColour + Colour = shadowColour, }, - new UserCoverBackground + new Container { RelativeSizeAxes = Axes.Both, - User = score.User, - Colour = ColourInfo.GradientHorizontal(Colour4.White.Opacity(0.5f), Colour4.White.Opacity(0)), - }, - avatar = new MaskedWrapper( - innerAvatar = new ClickableAvatar(user) + Padding = new MarginPadding { Right = 5 }, + Child = new Container { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Scale = new Vector2(1.1f), - Shear = -shear, + Masking = true, + CornerRadius = corner_radius, RelativeSizeAxes = Axes.Both, - }) - { - RelativeSizeAxes = Axes.None, - Size = new Vector2(height) - }, - new FillFlowContainer - { - Position = new Vector2(height + 9, 9), - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Vertical, - Children = new Drawable[] - { - flagBadgeAndDateContainer = new FillFlowContainer + Children = new[] { - Shear = -shear, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(5f, 0f), - Size = new Vector2(87, 16), - Masking = true, - Children = new Drawable[] + foreground = new Box { - new UpdateableFlag(user.CountryCode) + RelativeSizeAxes = Axes.Both, + Colour = foregroundColour + }, + new UserCoverBackground + { + RelativeSizeAxes = Axes.Both, + User = score.User, + Colour = ColourInfo.GradientHorizontal(Colour4.White.Opacity(0.5f), Colour4.White.Opacity(0)), + }, + avatar = new MaskedWrapper( + innerAvatar = new ClickableAvatar(user) { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Size = new Vector2(24, 16), - }, - new DateLabel(score.Date) + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Scale = new Vector2(1.1f), + Shear = -shear, + RelativeSizeAxes = Axes.Both, + }) + { + RelativeSizeAxes = Axes.None, + Size = new Vector2(height) + }, + new FillFlowContainer + { + Position = new Vector2(height + 9, 9), + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Children = new Drawable[] { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, + flagBadgeAndDateContainer = new FillFlowContainer + { + Shear = -shear, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(5f, 0f), + Size = new Vector2(87, 16), + Masking = true, + Children = new Drawable[] + { + new UpdateableFlag(user.CountryCode) + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Size = new Vector2(24, 16), + }, + new DateLabel(score.Date) + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + } + } + }, + nameLabel = new OsuSpriteText + { + Shear = -shear, + Text = user.Username, + Font = OsuFont.GetFont(size: 24, weight: FontWeight.SemiBold) + } } + }, + new FillFlowContainer + { + Margin = new MarginPadding { Right = 40 }, + Spacing = new Vector2(25, 0), + Shear = -shear, + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Children = statisticsLabels } - }, - nameLabel = new OsuSpriteText - { - Shear = -shear, - Text = user.Username, - Font = OsuFont.GetFont(size: 24, weight: FontWeight.SemiBold) } - } + }, }, - new FillFlowContainer - { - Margin = new MarginPadding { Right = 40 }, - Spacing = new Vector2(25, 0), - Shear = -shear, - Anchor = Anchor.CentreRight, - Origin = Anchor.CentreRight, - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Children = statisticsLabels - } - } + }, }; private FillFlowContainer createRightSideContent() => From 5bea5415be1cdc1aec3f6a5dd6fc2b6d38a4dc78 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Fri, 22 Sep 2023 19:55:00 -0700 Subject: [PATCH 0098/2493] Remove weird yellow background override on mods To who's reading, don't follow figma designs all the time. In most cases, follow what other places are doing. --- osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs b/osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs index 89ad39c98c..f67f7c05a2 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs @@ -466,13 +466,11 @@ namespace osu.Game.Online.Leaderboards { private readonly IMod mod; - [Resolved] - private OsuColour colours { get; set; } = null!; - public ColouredModSwitchTiny(IMod mod) : base(mod) { this.mod = mod; + Active.Value = true; Masking = true; EdgeEffect = new EdgeEffectParameters { @@ -484,12 +482,6 @@ namespace osu.Game.Online.Leaderboards }; } - protected override void UpdateState() - { - AcronymText.Colour = Colour4.FromHex("#555555"); - Background.Colour = colours.Yellow; - } - public LocalisableString TooltipText => (mod as Mod)?.IconTooltip ?? mod.Name; } From c0b8b3509f9507592e6ecc06e408c54710a0f64b Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Fri, 22 Sep 2023 19:56:33 -0700 Subject: [PATCH 0099/2493] Populate dates and add show animation on test --- .../Visual/SongSelect/TestSceneLeaderboardScoreV2.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneLeaderboardScoreV2.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneLeaderboardScoreV2.cs index 2a6203f2d8..1663116e12 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneLeaderboardScoreV2.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneLeaderboardScoreV2.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.Graphics; using osu.Framework.Graphics.Containers; @@ -41,6 +42,7 @@ namespace osu.Game.Tests.Visual.SongSelect CountryCode = CountryCode.ES, CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c1.jpg", }, + Date = DateTimeOffset.Now.AddYears(-2), }, new ScoreInfo { @@ -58,6 +60,7 @@ namespace osu.Game.Tests.Visual.SongSelect CountryCode = CountryCode.CA, CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c2.jpg", }, + Date = DateTimeOffset.Now.AddMonths(-6), }, new ScoreInfo @@ -73,6 +76,7 @@ namespace osu.Game.Tests.Visual.SongSelect Username = @"No cover", CountryCode = CountryCode.BR, }, + Date = DateTimeOffset.Now, }, }; @@ -92,6 +96,9 @@ namespace osu.Game.Tests.Visual.SongSelect } }; + foreach (var score in fillFlow.Children) + score.Show(); + AddSliderStep("change relative width", 0, 1f, 0.6f, v => { fillFlow.Width = v; From dad03778b7aaa05a5250d7ca014e4d3203f0c092 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Sat, 23 Sep 2023 19:40:57 -0700 Subject: [PATCH 0100/2493] Fix leaderboard score caching colour provider --- .../Visual/SongSelect/TestSceneLeaderboardScoreV2.cs | 4 ++++ osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneLeaderboardScoreV2.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneLeaderboardScoreV2.cs index 1663116e12..200faa33ec 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneLeaderboardScoreV2.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneLeaderboardScoreV2.cs @@ -7,6 +7,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Leaderboards; +using osu.Game.Overlays; using osu.Game.Rulesets.Mania; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu; @@ -19,6 +20,9 @@ namespace osu.Game.Tests.Visual.SongSelect { public partial class TestSceneLeaderboardScoreV2 : OsuTestScene { + [Cached] + private OverlayColourProvider colourProvider { get; set; } = new OverlayColourProvider(OverlayColourScheme.Aquamarine); + private FillFlowContainer fillFlow = null!; [BackgroundDependencyLoader] diff --git a/osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs b/osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs index f67f7c05a2..bcb0796ffd 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs @@ -52,8 +52,8 @@ namespace osu.Game.Online.Leaderboards private static readonly Vector2 shear = new Vector2(0.15f, 0); - [Cached] - private OverlayColourProvider colourProvider { get; set; } = new OverlayColourProvider(OverlayColourScheme.Aquamarine); + [Resolved] + private OverlayColourProvider colourProvider { get; set; } = null!; [Resolved] private SongSelect? songSelect { get; set; } From 668e083ddc6292df0ebf4df775dd7860d42461c4 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Tue, 26 Sep 2023 12:05:54 -0700 Subject: [PATCH 0101/2493] Use `AutoSizeAxes` instead of hardcoded `Size` --- osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs b/osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs index bcb0796ffd..0039cce532 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs @@ -213,7 +213,7 @@ namespace osu.Game.Online.Leaderboards Shear = -shear, Direction = FillDirection.Horizontal, Spacing = new Vector2(5f, 0f), - Size = new Vector2(87, 16), + AutoSizeAxes = Axes.Both, Masking = true, Children = new Drawable[] { From 39b008b070e83900dcc54042f5b76a47f11eb754 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Tue, 10 Oct 2023 14:58:22 -0700 Subject: [PATCH 0102/2493] Move test scores to a method and add `TestResources.CreateTestScoreInfo()` --- .../SongSelect/TestSceneLeaderboardScoreV2.cs | 50 ++++++++++--------- 1 file changed, 26 insertions(+), 24 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneLeaderboardScoreV2.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneLeaderboardScoreV2.cs index 200faa33ec..fed5519887 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneLeaderboardScoreV2.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneLeaderboardScoreV2.cs @@ -13,6 +13,7 @@ using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Scoring; +using osu.Game.Tests.Resources; using osu.Game.Users; using osuTK; @@ -27,6 +28,29 @@ namespace osu.Game.Tests.Visual.SongSelect [BackgroundDependencyLoader] private void load() + { + Child = fillFlow = new FillFlowContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Spacing = new Vector2(0, 10), + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + }; + + foreach (var scoreInfo in getTestScores()) + fillFlow.Add(new LeaderboardScoreV2(scoreInfo, scoreInfo.Position, scoreInfo.User.Id == 2)); + + foreach (var score in fillFlow.Children) + score.Show(); + + AddSliderStep("change relative width", 0, 1f, 0.6f, v => + { + fillFlow.Width = v; + }); + } + + private static ScoreInfo[] getTestScores() { var scores = new[] { @@ -66,7 +90,6 @@ namespace osu.Game.Tests.Visual.SongSelect }, Date = DateTimeOffset.Now.AddMonths(-6), }, - new ScoreInfo { Position = 110000, @@ -82,31 +105,10 @@ namespace osu.Game.Tests.Visual.SongSelect }, Date = DateTimeOffset.Now, }, + TestResources.CreateTestScoreInfo(), }; - Child = fillFlow = new FillFlowContainer - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Spacing = new Vector2(0, 10), - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Children = new Drawable[] - { - new LeaderboardScoreV2(scores[0], 1), - new LeaderboardScoreV2(scores[1], null, true), - new LeaderboardScoreV2(scores[2], null, true), - new LeaderboardScoreV2(scores[2], null), - } - }; - - foreach (var score in fillFlow.Children) - score.Show(); - - AddSliderStep("change relative width", 0, 1f, 0.6f, v => - { - fillFlow.Width = v; - }); + return scores; } } } From 43c8d51d02a4b504d6019cb077dab94107e8dd19 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Tue, 10 Oct 2023 15:05:34 -0700 Subject: [PATCH 0103/2493] Add draw width statistic to test --- .../SongSelect/TestSceneLeaderboardScoreV2.cs | 25 ++++++++++++++----- 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneLeaderboardScoreV2.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneLeaderboardScoreV2.cs index fed5519887..bc254f25de 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneLeaderboardScoreV2.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneLeaderboardScoreV2.cs @@ -5,6 +5,7 @@ using System; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Game.Graphics.Sprites; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Leaderboards; using osu.Game.Overlays; @@ -25,17 +26,22 @@ namespace osu.Game.Tests.Visual.SongSelect private OverlayColourProvider colourProvider { get; set; } = new OverlayColourProvider(OverlayColourScheme.Aquamarine); private FillFlowContainer fillFlow = null!; + private OsuSpriteText drawWidthText = null!; [BackgroundDependencyLoader] private void load() { - Child = fillFlow = new FillFlowContainer + Children = new Drawable[] { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Spacing = new Vector2(0, 10), - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, + fillFlow = new FillFlowContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Spacing = new Vector2(0, 10), + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + }, + drawWidthText = new OsuSpriteText(), }; foreach (var scoreInfo in getTestScores()) @@ -50,6 +56,13 @@ namespace osu.Game.Tests.Visual.SongSelect }); } + protected override void UpdateAfterChildren() + { + base.UpdateAfterChildren(); + + drawWidthText.Text = $"DrawWidth: {fillFlow.DrawWidth}"; + } + private static ScoreInfo[] getTestScores() { var scores = new[] From 6087c12d85214ba3f6e25d3cf38316753a7f2144 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Tue, 10 Oct 2023 17:53:37 -0700 Subject: [PATCH 0104/2493] Use grid container for centre content --- .../Online/Leaderboards/LeaderboardScoreV2.cs | 120 ++++++++++-------- 1 file changed, 68 insertions(+), 52 deletions(-) diff --git a/osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs b/osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs index 0039cce532..c92549e9c5 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs @@ -175,7 +175,7 @@ namespace osu.Game.Online.Leaderboards Masking = true, CornerRadius = corner_radius, RelativeSizeAxes = Axes.Both, - Children = new[] + Children = new Drawable[] { foreground = new Box { @@ -188,66 +188,82 @@ namespace osu.Game.Online.Leaderboards User = score.User, Colour = ColourInfo.GradientHorizontal(Colour4.White.Opacity(0.5f), Colour4.White.Opacity(0)), }, - avatar = new MaskedWrapper( - innerAvatar = new ClickableAvatar(user) - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Scale = new Vector2(1.1f), - Shear = -shear, - RelativeSizeAxes = Axes.Both, - }) + new GridContainer { - RelativeSizeAxes = Axes.None, - Size = new Vector2(height) - }, - new FillFlowContainer - { - Position = new Vector2(height + 9, 9), - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Vertical, - Children = new Drawable[] + RelativeSizeAxes = Axes.Both, + ColumnDimensions = new[] { - flagBadgeAndDateContainer = new FillFlowContainer + new Dimension(GridSizeMode.AutoSize), + new Dimension(), + new Dimension(GridSizeMode.AutoSize), + }, + Content = new[] + { + new[] { - Shear = -shear, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(5f, 0f), - AutoSizeAxes = Axes.Both, - Masking = true, - Children = new Drawable[] + avatar = new MaskedWrapper( + innerAvatar = new ClickableAvatar(user) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Scale = new Vector2(1.1f), + Shear = -shear, + RelativeSizeAxes = Axes.Both, + }) { - new UpdateableFlag(user.CountryCode) + RelativeSizeAxes = Axes.None, + Size = new Vector2(height) + }, + new FillFlowContainer + { + Position = new Vector2(9), + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Children = new Drawable[] { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Size = new Vector2(24, 16), - }, - new DateLabel(score.Date) - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, + flagBadgeAndDateContainer = new FillFlowContainer + { + Shear = -shear, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(5f, 0f), + AutoSizeAxes = Axes.Both, + Masking = true, + Children = new Drawable[] + { + new UpdateableFlag(user.CountryCode) + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Size = new Vector2(24, 16), + }, + new DateLabel(score.Date) + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + } + } + }, + nameLabel = new OsuSpriteText + { + Shear = -shear, + Text = user.Username, + Font = OsuFont.GetFont(size: 24, weight: FontWeight.SemiBold) + } } + }, + new FillFlowContainer + { + Margin = new MarginPadding { Right = 40 }, + Spacing = new Vector2(25, 0), + Shear = -shear, + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Children = statisticsLabels } - }, - nameLabel = new OsuSpriteText - { - Shear = -shear, - Text = user.Username, - Font = OsuFont.GetFont(size: 24, weight: FontWeight.SemiBold) } } - }, - new FillFlowContainer - { - Margin = new MarginPadding { Right = 40 }, - Spacing = new Vector2(25, 0), - Shear = -shear, - Anchor = Anchor.CentreRight, - Origin = Anchor.CentreRight, - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Children = statisticsLabels } } }, From bb3f426b935cd67e1cefa0bcb5f984545b0156ff Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Tue, 10 Oct 2023 18:02:59 -0700 Subject: [PATCH 0105/2493] Truncate name label and clean up positioning code Also adds a test score with a long username. --- .../SongSelect/TestSceneLeaderboardScoreV2.cs | 16 ++++++++++++++++ .../Online/Leaderboards/LeaderboardScoreV2.cs | 14 +++++++++----- 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneLeaderboardScoreV2.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneLeaderboardScoreV2.cs index bc254f25de..1b1ece3eff 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneLeaderboardScoreV2.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneLeaderboardScoreV2.cs @@ -118,6 +118,22 @@ namespace osu.Game.Tests.Visual.SongSelect }, Date = DateTimeOffset.Now, }, + new ScoreInfo + { + Position = 110000, + Rank = ScoreRank.A, + Accuracy = 1, + MaxCombo = 244, + TotalScore = 1707827, + Ruleset = new ManiaRuleset().RulesetInfo, + User = new APIUser + { + Id = 226597, + Username = @"WWWWWWWWWWWWWWWWWWWW", + CountryCode = CountryCode.US, + }, + Date = DateTimeOffset.Now, + }, TestResources.CreateTestScoreInfo(), }; diff --git a/osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs b/osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs index c92549e9c5..a6df3bee76 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs @@ -216,16 +216,19 @@ namespace osu.Game.Online.Leaderboards }, new FillFlowContainer { - Position = new Vector2(9), - AutoSizeAxes = Axes.Both, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, Direction = FillDirection.Vertical, + Padding = new MarginPadding { Horizontal = corner_radius }, Children = new Drawable[] { flagBadgeAndDateContainer = new FillFlowContainer { Shear = -shear, Direction = FillDirection.Horizontal, - Spacing = new Vector2(5f, 0f), + Spacing = new Vector2(5), AutoSizeAxes = Axes.Both, Masking = true, Children = new Drawable[] @@ -243,8 +246,9 @@ namespace osu.Game.Online.Leaderboards } } }, - nameLabel = new OsuSpriteText + nameLabel = new TruncatingSpriteText { + RelativeSizeAxes = Axes.X, Shear = -shear, Text = user.Username, Font = OsuFont.GetFont(size: 24, weight: FontWeight.SemiBold) @@ -254,7 +258,7 @@ namespace osu.Game.Online.Leaderboards new FillFlowContainer { Margin = new MarginPadding { Right = 40 }, - Spacing = new Vector2(25, 0), + Spacing = new Vector2(25), Shear = -shear, Anchor = Anchor.CentreRight, Origin = Anchor.CentreRight, From 837437ac5758807220f963d785b3db816d728f07 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Tue, 10 Oct 2023 18:15:05 -0700 Subject: [PATCH 0106/2493] Rename `createRightSideContent()` to `createRightContent()` --- osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs b/osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs index a6df3bee76..e364d411e3 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs @@ -140,7 +140,7 @@ namespace osu.Game.Online.Leaderboards Width = 35 }, createCentreContent(user), - createRightSideContent() + createRightContent() } } } @@ -275,7 +275,7 @@ namespace osu.Game.Online.Leaderboards }, }; - private FillFlowContainer createRightSideContent() => + private FillFlowContainer createRightContent() => new FillFlowContainer { Padding = new MarginPadding { Left = 11, Right = 15 }, From e0c6c1bc668272e08c7a5ce1b5e4c74acee13761 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Tue, 10 Oct 2023 18:20:19 -0700 Subject: [PATCH 0107/2493] Fix rank label tooltip area --- .../Online/Leaderboards/LeaderboardScoreV2.cs | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs b/osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs index e364d411e3..60bac3e713 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs @@ -131,14 +131,7 @@ namespace osu.Game.Online.Leaderboards { new Drawable[] { - new RankLabel(rank) - { - Shear = -shear, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Y, - Width = 35 - }, + new RankLabel(rank) { Shear = -shear }, createCentreContent(user), createRightContent() } @@ -457,13 +450,15 @@ namespace osu.Game.Online.Leaderboards { public RankLabel(int? rank) { + AutoSizeAxes = Axes.Both; + Anchor = Anchor.Centre; + Origin = Anchor.Centre; + if (rank >= 1000) TooltipText = $"#{rank:N0}"; Child = new OsuSpriteText { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, Font = OsuFont.GetFont(size: 20, weight: FontWeight.SemiBold, italics: true), Text = rank == null ? "-" : rank.Value.FormatRank().Insert(0, "#") }; From 3ad5a7c66190aaf0a80e775bf4761767604d862f Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Tue, 10 Oct 2023 18:29:18 -0700 Subject: [PATCH 0108/2493] Reduce indents of private container methods --- .../Online/Leaderboards/LeaderboardScoreV2.cs | 314 +++++++++--------- 1 file changed, 156 insertions(+), 158 deletions(-) diff --git a/osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs b/osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs index 60bac3e713..c96b860b29 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs @@ -146,184 +146,182 @@ namespace osu.Game.Online.Leaderboards modsContainer.Padding = new MarginPadding { Top = modsContainer.Children.Count > 0 ? 4 : 0 }; } - private Container createCentreContent(APIUser user) => - new Container + private Container createCentreContent(APIUser user) => new Container + { + Masking = true, + CornerRadius = corner_radius, + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] { - Masking = true, - CornerRadius = corner_radius, - RelativeSizeAxes = Axes.Both, - Children = new Drawable[] + new Box { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = shadowColour, - }, - new Container - { - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding { Right = 5 }, - Child = new Container - { - Masking = true, - CornerRadius = corner_radius, - RelativeSizeAxes = Axes.Both, - Children = new Drawable[] - { - foreground = new Box - { - RelativeSizeAxes = Axes.Both, - Colour = foregroundColour - }, - new UserCoverBackground - { - RelativeSizeAxes = Axes.Both, - User = score.User, - Colour = ColourInfo.GradientHorizontal(Colour4.White.Opacity(0.5f), Colour4.White.Opacity(0)), - }, - new GridContainer - { - RelativeSizeAxes = Axes.Both, - ColumnDimensions = new[] - { - new Dimension(GridSizeMode.AutoSize), - new Dimension(), - new Dimension(GridSizeMode.AutoSize), - }, - Content = new[] - { - new[] - { - avatar = new MaskedWrapper( - innerAvatar = new ClickableAvatar(user) - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Scale = new Vector2(1.1f), - Shear = -shear, - RelativeSizeAxes = Axes.Both, - }) - { - RelativeSizeAxes = Axes.None, - Size = new Vector2(height) - }, - new FillFlowContainer - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Direction = FillDirection.Vertical, - Padding = new MarginPadding { Horizontal = corner_radius }, - Children = new Drawable[] - { - flagBadgeAndDateContainer = new FillFlowContainer - { - Shear = -shear, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(5), - AutoSizeAxes = Axes.Both, - Masking = true, - Children = new Drawable[] - { - new UpdateableFlag(user.CountryCode) - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Size = new Vector2(24, 16), - }, - new DateLabel(score.Date) - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - } - } - }, - nameLabel = new TruncatingSpriteText - { - RelativeSizeAxes = Axes.X, - Shear = -shear, - Text = user.Username, - Font = OsuFont.GetFont(size: 24, weight: FontWeight.SemiBold) - } - } - }, - new FillFlowContainer - { - Margin = new MarginPadding { Right = 40 }, - Spacing = new Vector2(25), - Shear = -shear, - Anchor = Anchor.CentreRight, - Origin = Anchor.CentreRight, - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Children = statisticsLabels - } - } - } - } - } - }, - }, + RelativeSizeAxes = Axes.Both, + Colour = shadowColour, }, - }; - - private FillFlowContainer createRightContent() => - new FillFlowContainer - { - Padding = new MarginPadding { Left = 11, Right = 15 }, - Y = -5, - AutoSizeAxes = Axes.Y, - RelativeSizeAxes = Axes.X, - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Direction = FillDirection.Vertical, - Spacing = new Vector2(13, 0f), - Children = new Drawable[] + new Container { - new Container + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Right = 5 }, + Child = new Container { - AutoSizeAxes = Axes.Y, - RelativeSizeAxes = Axes.X, + Masking = true, + CornerRadius = corner_radius, + RelativeSizeAxes = Axes.Both, Children = new Drawable[] { - scoreText = new OsuSpriteText + foreground = new Box { - Shear = -shear, - Current = scoreManager.GetBindableTotalScoreString(score), - - //Does not match figma, adjusted to allow 8 digits to fit comfortably - Font = OsuFont.GetFont(size: 28, weight: FontWeight.SemiBold, fixedWidth: false), + RelativeSizeAxes = Axes.Both, + Colour = foregroundColour }, - RankContainer = new Container + new UserCoverBackground { - BypassAutoSizeAxes = Axes.Both, - Y = 2, - Shear = -shear, - Anchor = Anchor.CentreRight, - Origin = Anchor.CentreRight, - AutoSizeAxes = Axes.Both, - Children = new[] + RelativeSizeAxes = Axes.Both, + User = score.User, + Colour = ColourInfo.GradientHorizontal(Colour4.White.Opacity(0.5f), Colour4.White.Opacity(0)), + }, + new GridContainer + { + RelativeSizeAxes = Axes.Both, + ColumnDimensions = new[] { - scoreRank = new UpdateableRank(score.Rank) + new Dimension(GridSizeMode.AutoSize), + new Dimension(), + new Dimension(GridSizeMode.AutoSize), + }, + Content = new[] + { + new[] { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Size = new Vector2(32) + avatar = new MaskedWrapper( + innerAvatar = new ClickableAvatar(user) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Scale = new Vector2(1.1f), + Shear = -shear, + RelativeSizeAxes = Axes.Both, + }) + { + RelativeSizeAxes = Axes.None, + Size = new Vector2(height) + }, + new FillFlowContainer + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Padding = new MarginPadding { Horizontal = corner_radius }, + Children = new Drawable[] + { + flagBadgeAndDateContainer = new FillFlowContainer + { + Shear = -shear, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(5), + AutoSizeAxes = Axes.Both, + Masking = true, + Children = new Drawable[] + { + new UpdateableFlag(user.CountryCode) + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Size = new Vector2(24, 16), + }, + new DateLabel(score.Date) + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + } + } + }, + nameLabel = new TruncatingSpriteText + { + RelativeSizeAxes = Axes.X, + Shear = -shear, + Text = user.Username, + Font = OsuFont.GetFont(size: 24, weight: FontWeight.SemiBold) + } + } + }, + new FillFlowContainer + { + Margin = new MarginPadding { Right = 40 }, + Spacing = new Vector2(25), + Shear = -shear, + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Children = statisticsLabels + } } } } } }, - modsContainer = new FillFlowContainer + }, + }, + }; + + private FillFlowContainer createRightContent() => new FillFlowContainer + { + Padding = new MarginPadding { Left = 11, Right = 15 }, + Y = -5, + AutoSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.X, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Direction = FillDirection.Vertical, + Spacing = new Vector2(13, 0f), + Children = new Drawable[] + { + new Container + { + AutoSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.X, + Children = new Drawable[] { - Shear = -shear, - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - ChildrenEnumerable = score.Mods.Select(mod => new ColouredModSwitchTiny(mod) { Scale = new Vector2(0.375f) }) + scoreText = new OsuSpriteText + { + Shear = -shear, + Current = scoreManager.GetBindableTotalScoreString(score), + + //Does not match figma, adjusted to allow 8 digits to fit comfortably + Font = OsuFont.GetFont(size: 28, weight: FontWeight.SemiBold, fixedWidth: false), + }, + RankContainer = new Container + { + BypassAutoSizeAxes = Axes.Both, + Y = 2, + Shear = -shear, + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + AutoSizeAxes = Axes.Both, + Children = new[] + { + scoreRank = new UpdateableRank(score.Rank) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(32) + } + } + } } + }, + modsContainer = new FillFlowContainer + { + Shear = -shear, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + ChildrenEnumerable = score.Mods.Select(mod => new ColouredModSwitchTiny(mod) { Scale = new Vector2(0.375f) }) } - }; + } + }; protected (CaseTransformableString, LocalisableString DisplayAccuracy)[] GetStatistics(ScoreInfo model) => new[] { From 3c1d15d9b7e67bf0f24290873330d5d4dd06f7de Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Tue, 10 Oct 2023 18:44:20 -0700 Subject: [PATCH 0109/2493] Fix user cover having shear --- osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs b/osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs index c96b860b29..8e3affbe20 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs @@ -178,6 +178,9 @@ namespace osu.Game.Online.Leaderboards { RelativeSizeAxes = Axes.Both, User = score.User, + Shear = -shear, + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, Colour = ColourInfo.GradientHorizontal(Colour4.White.Opacity(0.5f), Colour4.White.Opacity(0)), }, new GridContainer From e049a072f8b5bf708377e80fafc032903ea74817 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Tue, 10 Oct 2023 19:17:50 -0700 Subject: [PATCH 0110/2493] Update right content to latest design - Add more scenarios to test - Future-proof mods display to not overflow --- .../SongSelect/TestSceneLeaderboardScoreV2.cs | 23 ++- osu.Game/Graphics/OsuColour.cs | 21 ++ osu.Game/Online/Leaderboards/DrawableRank.cs | 4 +- .../Online/Leaderboards/LeaderboardScoreV2.cs | 181 ++++++++++++++---- 4 files changed, 180 insertions(+), 49 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneLeaderboardScoreV2.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneLeaderboardScoreV2.cs index 1b1ece3eff..f0d07c8526 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneLeaderboardScoreV2.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneLeaderboardScoreV2.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.Graphics; using osu.Framework.Graphics.Containers; @@ -70,11 +71,10 @@ namespace osu.Game.Tests.Visual.SongSelect new ScoreInfo { Position = 999, - Rank = ScoreRank.XH, + Rank = ScoreRank.X, Accuracy = 1, MaxCombo = 244, TotalScore = 1707827, - Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), new OsuModAlternate(), new OsuModFlashlight(), new OsuModFreezeFrame() }, Ruleset = new OsuRuleset().RulesetInfo, User = new APIUser { @@ -91,7 +91,6 @@ namespace osu.Game.Tests.Visual.SongSelect Rank = ScoreRank.S, Accuracy = 0.1f, MaxCombo = 32040, - Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), new OsuModAlternate(), new OsuModFlashlight(), new OsuModFreezeFrame(), new OsuModClassic() }, TotalScore = 1707827, Ruleset = new OsuRuleset().RulesetInfo, User = new APIUser @@ -106,7 +105,7 @@ namespace osu.Game.Tests.Visual.SongSelect new ScoreInfo { Position = 110000, - Rank = ScoreRank.X, + Rank = ScoreRank.A, Accuracy = 1, MaxCombo = 244, TotalScore = 17078279, @@ -124,7 +123,7 @@ namespace osu.Game.Tests.Visual.SongSelect Rank = ScoreRank.A, Accuracy = 1, MaxCombo = 244, - TotalScore = 1707827, + TotalScore = 1234567890, Ruleset = new ManiaRuleset().RulesetInfo, User = new APIUser { @@ -137,6 +136,20 @@ namespace osu.Game.Tests.Visual.SongSelect TestResources.CreateTestScoreInfo(), }; + for (int i = 0; i < LeaderboardScoreV2.MAX_MODS_EXPANDED; i++) + scores[0].Mods = scores[0].Mods.Concat(new Mod[] { i % 2 == 0 ? new OsuModHidden() : new OsuModHalfTime() }).ToArray(); + + for (int i = 0; i < LeaderboardScoreV2.MAX_MODS_EXPANDED + 1; i++) + scores[1].Mods = scores[1].Mods.Concat(new Mod[] { i % 2 == 0 ? new OsuModHidden() : new OsuModHalfTime() }).ToArray(); + + for (int i = 0; i < LeaderboardScoreV2.MAX_MODS_CONTRACTED; i++) + scores[2].Mods = scores[2].Mods.Concat(new Mod[] { i % 2 == 0 ? new OsuModHidden() : new OsuModHalfTime() }).ToArray(); + + for (int i = 0; i < LeaderboardScoreV2.MAX_MODS_CONTRACTED + 1; i++) + scores[3].Mods = scores[3].Mods.Concat(new Mod[] { i % 2 == 0 ? new OsuModHidden() : new OsuModHalfTime() }).ToArray(); + + scores[4].Mods = scores[4].BeatmapInfo!.Ruleset.CreateInstance().CreateAllMods().ToArray(); + return scores; } } diff --git a/osu.Game/Graphics/OsuColour.cs b/osu.Game/Graphics/OsuColour.cs index 1b21f79c0a..d1b232b26d 100644 --- a/osu.Game/Graphics/OsuColour.cs +++ b/osu.Game/Graphics/OsuColour.cs @@ -3,6 +3,7 @@ using System; using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; using osu.Game.Beatmaps; using osu.Game.Online.Rooms; @@ -68,6 +69,26 @@ namespace osu.Game.Graphics } } + /// + /// Retrieves the colour for the total score depending on . + /// + public static ColourInfo TotalScoreColourFor(ScoreRank rank) + { + switch (rank) + { + case ScoreRank.XH: + case ScoreRank.X: + return ColourInfo.GradientVertical(Colour4.FromHex(@"A4DEFF"), Colour4.FromHex(@"F0AADD")); + + case ScoreRank.SH: + case ScoreRank.S: + return ColourInfo.GradientVertical(Colour4.FromHex(@"FFFFFF"), Colour4.FromHex(@"F7E65D")); + + default: + return Colour4.White; + } + } + /// /// Retrieves the colour for a . /// diff --git a/osu.Game/Online/Leaderboards/DrawableRank.cs b/osu.Game/Online/Leaderboards/DrawableRank.cs index 5177f35478..e4691efc04 100644 --- a/osu.Game/Online/Leaderboards/DrawableRank.cs +++ b/osu.Game/Online/Leaderboards/DrawableRank.cs @@ -57,7 +57,7 @@ namespace osu.Game.Online.Leaderboards Origin = Anchor.Centre, Spacing = new Vector2(-3, 0), Padding = new MarginPadding { Top = 5 }, - Colour = getRankNameColour(), + Colour = GetRankNameColour(rank), Font = OsuFont.Numeric.With(size: 25), Text = GetRankName(rank), ShadowColour = Color4.Black.Opacity(0.3f), @@ -74,7 +74,7 @@ namespace osu.Game.Online.Leaderboards /// /// Retrieves the grade text colour. /// - private ColourInfo getRankNameColour() + public static ColourInfo GetRankNameColour(ScoreRank rank) { switch (rank) { diff --git a/osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs b/osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs index 8e3affbe20..5f15f9dd62 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; +using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; @@ -17,6 +18,7 @@ using osu.Framework.Input.Events; using osu.Framework.Localisation; using osu.Game.Extensions; using osu.Game.Graphics; +using osu.Game.Graphics.Backgrounds; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; @@ -31,11 +33,25 @@ using osu.Game.Users; using osu.Game.Users.Drawables; using osu.Game.Utils; using osuTK; +using osuTK.Graphics; namespace osu.Game.Online.Leaderboards { public partial class LeaderboardScoreV2 : OsuClickableContainer, IHasContextMenu, IHasCustomTooltip { + /// + /// The maximum number of mods when contracted until the mods display width exceeds the . + /// + public const int MAX_MODS_CONTRACTED = 13; + + /// + /// The maximum number of mods when expanded until the mods display width exceeds the . + /// + public const int MAX_MODS_EXPANDED = 4; + + private const float right_content_min_width = 180; + private const float grade_width = 40; + private readonly ScoreInfo score; private const int height = 60; @@ -49,6 +65,7 @@ namespace osu.Game.Online.Leaderboards private Colour4 foregroundColour; private Colour4 backgroundColour; private Colour4 shadowColour; + private ColourInfo totalScoreBackgroundGradient; private static readonly Vector2 shear = new Vector2(0.15f, 0); @@ -77,9 +94,11 @@ namespace osu.Game.Online.Leaderboards protected Container RankContainer { get; private set; } = null!; private FillFlowContainer flagBadgeAndDateContainer = null!; private FillFlowContainer modsContainer = null!; + private OsuSpriteText modsCounter = null!; private OsuSpriteText scoreText = null!; private Drawable scoreRank = null!; + private Box totalScoreBackground = null!; public ITooltip GetCustomTooltip() => new LeaderboardScoreTooltip(); public virtual ScoreInfo TooltipContent => score; @@ -103,6 +122,7 @@ namespace osu.Game.Online.Leaderboards foregroundColour = isPersonalBest ? colourProvider.Background1 : colourProvider.Background5; backgroundColour = isPersonalBest ? colourProvider.Background2 : colourProvider.Background4; shadowColour = isPersonalBest ? colourProvider.Background3 : colourProvider.Background6; + totalScoreBackgroundGradient = ColourInfo.GradientHorizontal(backgroundColour.Opacity(0), backgroundColour); statisticsLabels = GetStatistics(score).Select(s => new ScoreComponentLabel(s, score)).ToList(); @@ -125,7 +145,7 @@ namespace osu.Game.Online.Leaderboards { new Dimension(GridSizeMode.Absolute, 65), new Dimension(), - new Dimension(GridSizeMode.Absolute, 176) + new Dimension(GridSizeMode.AutoSize, minSize: right_content_min_width), // use min size to account for classic scoring }, Content = new[] { @@ -142,12 +162,13 @@ namespace osu.Game.Online.Leaderboards innerAvatar.OnLoadComplete += d => d.FadeInFromZero(200); - modsContainer.Spacing = new Vector2(modsContainer.Children.Count > 5 ? -20 : 2, 0); + modsContainer.Spacing = new Vector2(modsContainer.Children.Count > MAX_MODS_EXPANDED ? -20 : 2, 0); modsContainer.Padding = new MarginPadding { Top = modsContainer.Children.Count > 0 ? 4 : 0 }; } private Container createCentreContent(APIUser user) => new Container { + Name = @"Centre container", Masking = true, CornerRadius = corner_radius, RelativeSizeAxes = Axes.Both, @@ -270,58 +291,130 @@ namespace osu.Game.Online.Leaderboards }, }; - private FillFlowContainer createRightContent() => new FillFlowContainer + private Container createRightContent() => new Container { - Padding = new MarginPadding { Left = 11, Right = 15 }, - Y = -5, - AutoSizeAxes = Axes.Y, - RelativeSizeAxes = Axes.X, - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Direction = FillDirection.Vertical, - Spacing = new Vector2(13, 0f), + Name = @"Right content", + AutoSizeAxes = Axes.X, + RelativeSizeAxes = Axes.Y, + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, Children = new Drawable[] { new Container { - AutoSizeAxes = Axes.Y, - RelativeSizeAxes = Axes.X, - Children = new Drawable[] + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Right = grade_width }, + Child = new Box { - scoreText = new OsuSpriteText + RelativeSizeAxes = Axes.Both, + Colour = ColourInfo.GradientHorizontal(backgroundColour.Opacity(0), OsuColour.ForRank(score.Rank)), + }, + }, + new Box + { + RelativeSizeAxes = Axes.Y, + Width = grade_width, + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + Colour = OsuColour.ForRank(score.Rank), + }, + new TrianglesV2 + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + Masking = true, + SpawnRatio = 2, + Velocity = 0.7f, + Colour = ColourInfo.GradientHorizontal(backgroundColour.Opacity(0), OsuColour.ForRank(score.Rank).Darken(0.2f)), + }, + RankContainer = new Container + { + Shear = -shear, + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + RelativeSizeAxes = Axes.Y, + Width = grade_width, + Child = scoreRank = new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Spacing = new Vector2(-2), + Colour = DrawableRank.GetRankNameColour(score.Rank), + Font = OsuFont.Numeric.With(size: 16), + Text = DrawableRank.GetRankName(score.Rank), + ShadowColour = Color4.Black.Opacity(0.3f), + ShadowOffset = new Vector2(0, 0.08f), + Shadow = true, + UseFullGlyphHeight = false, + }, + }, + new Container + { + AutoSizeAxes = Axes.X, + // makeshift inner border + Height = height - 4, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Padding = new MarginPadding { Right = grade_width }, + Child = new Container + { + AutoSizeAxes = Axes.X, + RelativeSizeAxes = Axes.Y, + Masking = true, + CornerRadius = corner_radius, + Children = new Drawable[] { - Shear = -shear, - Current = scoreManager.GetBindableTotalScoreString(score), - - //Does not match figma, adjusted to allow 8 digits to fit comfortably - Font = OsuFont.GetFont(size: 28, weight: FontWeight.SemiBold, fixedWidth: false), - }, - RankContainer = new Container - { - BypassAutoSizeAxes = Axes.Both, - Y = 2, - Shear = -shear, - Anchor = Anchor.CentreRight, - Origin = Anchor.CentreRight, - AutoSizeAxes = Axes.Both, - Children = new[] + totalScoreBackground = new Box { - scoreRank = new UpdateableRank(score.Rank) + RelativeSizeAxes = Axes.Both, + Colour = totalScoreBackgroundGradient, + }, + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = ColourInfo.GradientHorizontal(backgroundColour.Opacity(0), OsuColour.ForRank(score.Rank).Opacity(0.5f)), + }, + new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Direction = FillDirection.Vertical, + Padding = new MarginPadding { Horizontal = corner_radius }, + Children = new Drawable[] { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Size = new Vector2(32) + scoreText = new OsuSpriteText + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + UseFullGlyphHeight = false, + Shear = -shear, + Current = scoreManager.GetBindableTotalScoreString(score), + Font = OsuFont.GetFont(size: 30, weight: FontWeight.Light), + Colour = OsuColour.TotalScoreColourFor(score.Rank), + }, + modsContainer = new FillFlowContainer + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + Shear = -shear, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + ChildrenEnumerable = score.Mods.Select(mod => new ColouredModSwitchTiny(mod) { Scale = new Vector2(0.375f) }) + }, + modsCounter = new OsuSpriteText + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + Shear = -shear, + Text = $"{score.Mods.Length} mods", + Alpha = 0, + } } } } } - }, - modsContainer = new FillFlowContainer - { - Shear = -shear, - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - ChildrenEnumerable = score.Mods.Select(mod => new ColouredModSwitchTiny(mod) { Scale = new Vector2(0.375f) }) } } }; @@ -361,7 +454,8 @@ namespace osu.Game.Online.Leaderboards using (BeginDelayedSequence(50)) { - var drawables = new Drawable[] { flagBadgeAndDateContainer, modsContainer }.Concat(statisticsLabels).ToArray(); + Drawable modsDrawable = score.Mods.Length > MAX_MODS_CONTRACTED ? modsCounter : modsContainer; + var drawables = new[] { flagBadgeAndDateContainer, modsDrawable }.Concat(statisticsLabels).ToArray(); for (int i = 0; i < drawables.Length; i++) drawables[i].FadeIn(100 + i * 50); } @@ -383,8 +477,11 @@ namespace osu.Game.Online.Leaderboards private void updateState() { + var lightenedGradient = ColourInfo.GradientHorizontal(backgroundColour.Opacity(0).Lighten(0.2f), backgroundColour.Lighten(0.2f)); + foreground.FadeColour(IsHovered ? foregroundColour.Lighten(0.2f) : foregroundColour, transition_duration, Easing.OutQuint); background.FadeColour(IsHovered ? backgroundColour.Lighten(0.2f) : backgroundColour, transition_duration, Easing.OutQuint); + totalScoreBackground.FadeColour(IsHovered ? lightenedGradient : totalScoreBackgroundGradient, transition_duration, Easing.OutQuint); } #region Subclasses From e4f1eab6adc1cac96c5d09436a395ad3976cae6a Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Tue, 10 Oct 2023 20:24:17 -0700 Subject: [PATCH 0111/2493] Add experimental collapse content logic based on width --- .../Online/Leaderboards/LeaderboardScoreV2.cs | 94 ++++++++++++++----- 1 file changed, 73 insertions(+), 21 deletions(-) diff --git a/osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs b/osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs index 5f15f9dd62..c77424902f 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs @@ -15,6 +15,7 @@ using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; +using osu.Framework.Layout; using osu.Framework.Localisation; using osu.Game.Extensions; using osu.Game.Graphics; @@ -51,6 +52,7 @@ namespace osu.Game.Online.Leaderboards private const float right_content_min_width = 180; private const float grade_width = 40; + private const float username_min_width = 100; private readonly ScoreInfo score; @@ -100,6 +102,10 @@ namespace osu.Game.Online.Leaderboards private Drawable scoreRank = null!; private Box totalScoreBackground = null!; + private Container centreContent = null!; + private FillFlowContainer usernameAndFlagContainer = null!; + private FillFlowContainer statisticsContainer = null!; + public ITooltip GetCustomTooltip() => new LeaderboardScoreTooltip(); public virtual ScoreInfo TooltipContent => score; @@ -152,7 +158,7 @@ namespace osu.Game.Online.Leaderboards new Drawable[] { new RankLabel(rank) { Shear = -shear }, - createCentreContent(user), + centreContent = createCentreContent(user), createRightContent() } } @@ -215,22 +221,26 @@ namespace osu.Game.Online.Leaderboards }, Content = new[] { - new[] + new Drawable[] { - avatar = new MaskedWrapper( - innerAvatar = new ClickableAvatar(user) - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Scale = new Vector2(1.1f), - Shear = -shear, - RelativeSizeAxes = Axes.Both, - }) + new Container { - RelativeSizeAxes = Axes.None, - Size = new Vector2(height) + AutoSizeAxes = Axes.Both, + Child = avatar = new MaskedWrapper( + innerAvatar = new ClickableAvatar(user) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Scale = new Vector2(1.1f), + Shear = -shear, + RelativeSizeAxes = Axes.Both, + }) + { + RelativeSizeAxes = Axes.None, + Size = new Vector2(height) + }, }, - new FillFlowContainer + usernameAndFlagContainer = new FillFlowContainer { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, @@ -271,16 +281,22 @@ namespace osu.Game.Online.Leaderboards } } }, - new FillFlowContainer + new Container { - Margin = new MarginPadding { Right = 40 }, - Spacing = new Vector2(25), - Shear = -shear, + AutoSizeAxes = Axes.Both, Anchor = Anchor.CentreRight, Origin = Anchor.CentreRight, - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Children = statisticsLabels + Child = statisticsContainer = new FillFlowContainer + { + Padding = new MarginPadding { Right = 40 }, + Spacing = new Vector2(25), + Shear = -shear, + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Children = statisticsLabels + } } } } @@ -484,6 +500,42 @@ namespace osu.Game.Online.Leaderboards totalScoreBackground.FadeColour(IsHovered ? lightenedGradient : totalScoreBackgroundGradient, transition_duration, Easing.OutQuint); } + protected override bool OnInvalidate(Invalidation invalidation, InvalidationSource source) + { + Scheduler.AddOnce(() => + { + // TODO: may not always invalidate as expected + + // when width decreases + // - hide statistics, then + // - hide avatar, then + // - hide user and flag and show avatar again + + if (centreContent.DrawWidth >= height + username_min_width || centreContent.DrawWidth < username_min_width) + avatar.FadeIn(transition_duration, Easing.OutQuint).MoveToX(0, transition_duration, Easing.OutQuint); + else + avatar.FadeOut(transition_duration, Easing.OutQuint).MoveToX(-avatar.DrawWidth, transition_duration, Easing.OutQuint); + + if (centreContent.DrawWidth >= username_min_width) + { + usernameAndFlagContainer.FadeIn(transition_duration, Easing.OutQuint).MoveToX(0, transition_duration, Easing.OutQuint); + innerAvatar.ShowUsernameTooltip = false; + } + else + { + usernameAndFlagContainer.FadeOut(transition_duration, Easing.OutQuint).MoveToX(usernameAndFlagContainer.DrawWidth, transition_duration, Easing.OutQuint); + innerAvatar.ShowUsernameTooltip = true; + } + + if (centreContent.DrawWidth >= height + statisticsContainer.DrawWidth + username_min_width) + statisticsContainer.FadeIn(transition_duration, Easing.OutQuint).MoveToX(0, transition_duration, Easing.OutQuint); + else + statisticsContainer.FadeOut(transition_duration, Easing.OutQuint).MoveToX(statisticsContainer.DrawWidth, transition_duration, Easing.OutQuint); + }); + + return base.OnInvalidate(invalidation, source); + } + #region Subclasses private partial class DateLabel : DrawableDate From ba62498478bc0d84f10f3fb021662c4bebcd750f Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Tue, 10 Oct 2023 20:41:19 -0700 Subject: [PATCH 0112/2493] Add set up steps to reinit drawables with a different relative width --- .../SongSelect/TestSceneLeaderboardScoreV2.cs | 26 ++++++++++++------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneLeaderboardScoreV2.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneLeaderboardScoreV2.cs index f0d07c8526..b09ba793b6 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneLeaderboardScoreV2.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneLeaderboardScoreV2.cs @@ -3,6 +3,7 @@ using System; using System.Linq; +using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -26,16 +27,28 @@ namespace osu.Game.Tests.Visual.SongSelect [Cached] private OverlayColourProvider colourProvider { get; set; } = new OverlayColourProvider(OverlayColourScheme.Aquamarine); - private FillFlowContainer fillFlow = null!; - private OsuSpriteText drawWidthText = null!; + private FillFlowContainer? fillFlow; + private OsuSpriteText? drawWidthText; + private float relativeWidth; [BackgroundDependencyLoader] private void load() + { + AddSliderStep("change relative width", 0, 1f, 0.6f, v => + { + relativeWidth = v; + if (fillFlow != null) fillFlow.Width = v; + }); + } + + [SetUp] + public void Setup() => Schedule(() => { Children = new Drawable[] { fillFlow = new FillFlowContainer { + Width = relativeWidth, Anchor = Anchor.Centre, Origin = Anchor.Centre, Spacing = new Vector2(0, 10), @@ -50,18 +63,13 @@ namespace osu.Game.Tests.Visual.SongSelect foreach (var score in fillFlow.Children) score.Show(); - - AddSliderStep("change relative width", 0, 1f, 0.6f, v => - { - fillFlow.Width = v; - }); - } + }); protected override void UpdateAfterChildren() { base.UpdateAfterChildren(); - drawWidthText.Text = $"DrawWidth: {fillFlow.DrawWidth}"; + if (drawWidthText != null) drawWidthText.Text = $"DrawWidth: {fillFlow?.DrawWidth}"; } private static ScoreInfo[] getTestScores() From f2aff628b23a5be4d5c69d44573f6754f6f3a8b9 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Tue, 10 Oct 2023 22:14:04 -0700 Subject: [PATCH 0113/2493] Fix statistics container showing for a brief moment on lower widths --- osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs b/osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs index c77424902f..938dfefa4d 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs @@ -130,7 +130,11 @@ namespace osu.Game.Online.Leaderboards shadowColour = isPersonalBest ? colourProvider.Background3 : colourProvider.Background6; totalScoreBackgroundGradient = ColourInfo.GradientHorizontal(backgroundColour.Opacity(0), backgroundColour); - statisticsLabels = GetStatistics(score).Select(s => new ScoreComponentLabel(s, score)).ToList(); + statisticsLabels = GetStatistics(score).Select(s => new ScoreComponentLabel(s, score) + { + // ensure statistics container is the correct width when invalidating + AlwaysPresent = true, + }).ToList(); Child = content = new Container { @@ -288,6 +292,7 @@ namespace osu.Game.Online.Leaderboards Origin = Anchor.CentreRight, Child = statisticsContainer = new FillFlowContainer { + Name = @"Statistics container", Padding = new MarginPadding { Right = 40 }, Spacing = new Vector2(25), Shear = -shear, @@ -295,7 +300,8 @@ namespace osu.Game.Online.Leaderboards Origin = Anchor.CentreRight, AutoSizeAxes = Axes.Both, Direction = FillDirection.Horizontal, - Children = statisticsLabels + Children = statisticsLabels, + Alpha = 0, } } } From e32be36d929dd5cc614c65a4dd6178bba752cd0d Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Tue, 10 Oct 2023 22:14:23 -0700 Subject: [PATCH 0114/2493] Move invalidation issue todo to tests --- osu.Game.Tests/Visual/SongSelect/TestSceneLeaderboardScoreV2.cs | 2 ++ osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs | 2 -- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneLeaderboardScoreV2.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneLeaderboardScoreV2.cs index b09ba793b6..92370e7e58 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneLeaderboardScoreV2.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneLeaderboardScoreV2.cs @@ -34,6 +34,8 @@ namespace osu.Game.Tests.Visual.SongSelect [BackgroundDependencyLoader] private void load() { + // TODO: invalidation seems to be one-off when clicking slider to a certain value, so drag for now + // doesn't seem to happen in-game (when toggling window mode) AddSliderStep("change relative width", 0, 1f, 0.6f, v => { relativeWidth = v; diff --git a/osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs b/osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs index 938dfefa4d..bc9e6476a0 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs @@ -510,8 +510,6 @@ namespace osu.Game.Online.Leaderboards { Scheduler.AddOnce(() => { - // TODO: may not always invalidate as expected - // when width decreases // - hide statistics, then // - hide avatar, then From 42d41add41f705a877a63e9cbe84171a17eb6709 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Wed, 11 Oct 2023 09:40:59 -0700 Subject: [PATCH 0115/2493] Remove unused field --- osu.Game/Online/Leaderboards/DrawableRank.cs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/osu.Game/Online/Leaderboards/DrawableRank.cs b/osu.Game/Online/Leaderboards/DrawableRank.cs index e4691efc04..d21c38090a 100644 --- a/osu.Game/Online/Leaderboards/DrawableRank.cs +++ b/osu.Game/Online/Leaderboards/DrawableRank.cs @@ -18,12 +18,8 @@ namespace osu.Game.Online.Leaderboards { public partial class DrawableRank : CompositeDrawable { - private readonly ScoreRank rank; - public DrawableRank(ScoreRank rank) { - this.rank = rank; - RelativeSizeAxes = Axes.Both; FillMode = FillMode.Fit; FillAspectRatio = 2; From f6741514aafb337ffbb707ab9ba36736e45c193f Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Wed, 11 Oct 2023 09:39:15 -0700 Subject: [PATCH 0116/2493] Remove alternative total score display (colour gradient) for now For simplicity and a future consideration for when the skinning portion is implemented. --- osu.Game/Graphics/OsuColour.cs | 21 ------------------- .../Online/Leaderboards/LeaderboardScoreV2.cs | 1 - 2 files changed, 22 deletions(-) diff --git a/osu.Game/Graphics/OsuColour.cs b/osu.Game/Graphics/OsuColour.cs index d1b232b26d..1b21f79c0a 100644 --- a/osu.Game/Graphics/OsuColour.cs +++ b/osu.Game/Graphics/OsuColour.cs @@ -3,7 +3,6 @@ using System; using osu.Framework.Extensions.Color4Extensions; -using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; using osu.Game.Beatmaps; using osu.Game.Online.Rooms; @@ -69,26 +68,6 @@ namespace osu.Game.Graphics } } - /// - /// Retrieves the colour for the total score depending on . - /// - public static ColourInfo TotalScoreColourFor(ScoreRank rank) - { - switch (rank) - { - case ScoreRank.XH: - case ScoreRank.X: - return ColourInfo.GradientVertical(Colour4.FromHex(@"A4DEFF"), Colour4.FromHex(@"F0AADD")); - - case ScoreRank.SH: - case ScoreRank.S: - return ColourInfo.GradientVertical(Colour4.FromHex(@"FFFFFF"), Colour4.FromHex(@"F7E65D")); - - default: - return Colour4.White; - } - } - /// /// Retrieves the colour for a . /// diff --git a/osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs b/osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs index bc9e6476a0..76986de623 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs @@ -414,7 +414,6 @@ namespace osu.Game.Online.Leaderboards Shear = -shear, Current = scoreManager.GetBindableTotalScoreString(score), Font = OsuFont.GetFont(size: 30, weight: FontWeight.Light), - Colour = OsuColour.TotalScoreColourFor(score.Rank), }, modsContainer = new FillFlowContainer { From 52be580f28d7d1aa90c6a2d213a79cdebde59f18 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Wed, 11 Oct 2023 09:40:34 -0700 Subject: [PATCH 0117/2493] Fix date not aligning with flag --- osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs b/osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs index 76986de623..4e20b7f8f5 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs @@ -273,6 +273,7 @@ namespace osu.Game.Online.Leaderboards { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, + UseFullGlyphHeight = false, } } }, From f17aa6d644eff49b5e144faa9e12178708957f99 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Wed, 11 Oct 2023 12:57:50 -0700 Subject: [PATCH 0118/2493] Revert changes to `ModSwitchTiny` --- osu.Game/Rulesets/UI/ModSwitchTiny.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/osu.Game/Rulesets/UI/ModSwitchTiny.cs b/osu.Game/Rulesets/UI/ModSwitchTiny.cs index df7722761d..a5cf75bd07 100644 --- a/osu.Game/Rulesets/UI/ModSwitchTiny.cs +++ b/osu.Game/Rulesets/UI/ModSwitchTiny.cs @@ -24,8 +24,8 @@ namespace osu.Game.Rulesets.UI private readonly IMod mod; - protected Box Background; - protected OsuSpriteText AcronymText; + private readonly Box background; + private readonly OsuSpriteText acronymText; private Color4 activeForegroundColour; private Color4 inactiveForegroundColour; @@ -44,11 +44,11 @@ namespace osu.Game.Rulesets.UI Masking = true, Children = new Drawable[] { - Background = new Box + background = new Box { RelativeSizeAxes = Axes.Both }, - AcronymText = new OsuSpriteText + acronymText = new OsuSpriteText { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -78,14 +78,14 @@ namespace osu.Game.Rulesets.UI { base.LoadComplete(); - Active.BindValueChanged(_ => UpdateState(), true); + Active.BindValueChanged(_ => updateState(), true); FinishTransforms(true); } - protected virtual void UpdateState() + private void updateState() { - AcronymText.FadeColour(Active.Value ? activeForegroundColour : inactiveForegroundColour, 200, Easing.OutQuint); - Background.FadeColour(Active.Value ? activeBackgroundColour : inactiveBackgroundColour, 200, Easing.OutQuint); + acronymText.FadeColour(Active.Value ? activeForegroundColour : inactiveForegroundColour, 200, Easing.OutQuint); + background.FadeColour(Active.Value ? activeBackgroundColour : inactiveBackgroundColour, 200, Easing.OutQuint); } } } From 418549b48d3855269e2858d25b7bf95fe8445bc9 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Wed, 11 Oct 2023 12:38:46 -0700 Subject: [PATCH 0119/2493] Modify some half time mods on test For use after support of extended info on `ModSwitchTiny`. --- .../Visual/SongSelect/TestSceneLeaderboardScoreV2.cs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneLeaderboardScoreV2.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneLeaderboardScoreV2.cs index 92370e7e58..c5f96d1568 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneLeaderboardScoreV2.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneLeaderboardScoreV2.cs @@ -146,14 +146,22 @@ namespace osu.Game.Tests.Visual.SongSelect TestResources.CreateTestScoreInfo(), }; + var halfTime = new OsuModHalfTime + { + SpeedChange = + { + Value = 0.99 + } + }; + for (int i = 0; i < LeaderboardScoreV2.MAX_MODS_EXPANDED; i++) - scores[0].Mods = scores[0].Mods.Concat(new Mod[] { i % 2 == 0 ? new OsuModHidden() : new OsuModHalfTime() }).ToArray(); + scores[0].Mods = scores[0].Mods.Concat(new Mod[] { i % 2 == 0 ? new OsuModHidden() : halfTime }).ToArray(); for (int i = 0; i < LeaderboardScoreV2.MAX_MODS_EXPANDED + 1; i++) scores[1].Mods = scores[1].Mods.Concat(new Mod[] { i % 2 == 0 ? new OsuModHidden() : new OsuModHalfTime() }).ToArray(); for (int i = 0; i < LeaderboardScoreV2.MAX_MODS_CONTRACTED; i++) - scores[2].Mods = scores[2].Mods.Concat(new Mod[] { i % 2 == 0 ? new OsuModHidden() : new OsuModHalfTime() }).ToArray(); + scores[2].Mods = scores[2].Mods.Concat(new Mod[] { i % 2 == 0 ? new OsuModHidden() : halfTime }).ToArray(); for (int i = 0; i < LeaderboardScoreV2.MAX_MODS_CONTRACTED + 1; i++) scores[3].Mods = scores[3].Mods.Concat(new Mod[] { i % 2 == 0 ? new OsuModHidden() : new OsuModHalfTime() }).ToArray(); From cda9440a296304bd710c1787436ea1a948f6c999 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 11 Dec 2023 14:39:50 +0900 Subject: [PATCH 0120/2493] Fix JuiceStream velocity calculation --- .../Beatmaps/CatchBeatmapConverter.cs | 2 +- .../Objects/JuiceStream.cs | 21 +++++++++---------- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs index 8c460586b0..f5c5ffb529 100644 --- a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs +++ b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs @@ -45,7 +45,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps LegacyConvertedY = yPositionData?.Y ?? CatchHitObject.DEFAULT_LEGACY_CONVERT_Y, // prior to v8, speed multipliers don't adjust for how many ticks are generated over the same distance. // this results in more (or less) ticks being generated in SliderVelocityMultiplierBindable { get; } = new BindableDouble(1) { - Precision = 0.01, MinValue = 0.1, MaxValue = 10 }; @@ -48,16 +48,10 @@ namespace osu.Game.Rulesets.Catch.Objects public double TickDistanceMultiplier = 1; [JsonIgnore] - private double velocityFactor; + public double Velocity { get; private set; } [JsonIgnore] - private double tickDistanceFactor; - - [JsonIgnore] - public double Velocity => velocityFactor * SliderVelocityMultiplier; - - [JsonIgnore] - public double TickDistance => tickDistanceFactor * TickDistanceMultiplier; + public double TickDistance { get; private set; } /// /// The length of one span of this . @@ -70,8 +64,13 @@ namespace osu.Game.Rulesets.Catch.Objects TimingControlPoint timingPoint = controlPointInfo.TimingPointAt(StartTime); - velocityFactor = base_scoring_distance * difficulty.SliderMultiplier / timingPoint.BeatLength; - tickDistanceFactor = base_scoring_distance * difficulty.SliderMultiplier / difficulty.SliderTickRate; + Velocity = base_scoring_distance * difficulty.SliderMultiplier / LegacyRulesetExtensions.GetPrecisionAdjustedBeatLength(this, timingPoint, CatchRuleset.SHORT_NAME); + + // WARNING: this is intentionally not computed as `BASE_SCORING_DISTANCE * difficulty.SliderMultiplier` + // for backwards compatibility reasons (intentionally introducing floating point errors to match stable). + double scoringDistance = Velocity * timingPoint.BeatLength; + + TickDistance = scoringDistance / difficulty.SliderTickRate * TickDistanceMultiplier; } protected override void CreateNestedHitObjects(CancellationToken cancellationToken) From 22a0bc7d9d437ac1e502cd6e54c64fe66afb60c8 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Wed, 20 Dec 2023 00:05:32 +0100 Subject: [PATCH 0121/2493] Add basic slider distance control --- .../TestSceneSliderControlPointPiece.cs | 2 +- .../TestSceneSliderSelectionBlueprint.cs | 2 +- .../Sliders/Components/SliderTailPiece.cs | 104 ++++++++++++++++++ .../Blueprints/Sliders/SliderCircleOverlay.cs | 8 +- .../Sliders/SliderSelectionBlueprint.cs | 4 +- 5 files changed, 112 insertions(+), 8 deletions(-) create mode 100644 osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderTailPiece.cs diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderControlPointPiece.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderControlPointPiece.cs index 99ced30ffe..085e11460f 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderControlPointPiece.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderControlPointPiece.cs @@ -353,7 +353,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor { public new SliderBodyPiece BodyPiece => base.BodyPiece; public new TestSliderCircleOverlay HeadOverlay => (TestSliderCircleOverlay)base.HeadOverlay; - public new TestSliderCircleOverlay TailOverlay => (TestSliderCircleOverlay)base.TailOverlay; + public new TestSliderCircleOverlay TailOverlay => (TestSliderCircleOverlay)base.TailPiece; public new PathControlPointVisualiser ControlPointVisualiser => base.ControlPointVisualiser; public TestSliderBlueprint(Slider slider) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSelectionBlueprint.cs index d4d99e1019..adc4929227 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSelectionBlueprint.cs @@ -198,7 +198,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor { public new SliderBodyPiece BodyPiece => base.BodyPiece; public new TestSliderCircleOverlay HeadOverlay => (TestSliderCircleOverlay)base.HeadOverlay; - public new TestSliderCircleOverlay TailOverlay => (TestSliderCircleOverlay)base.TailOverlay; + public new TestSliderCircleOverlay TailOverlay => (TestSliderCircleOverlay)base.TailPiece; public new PathControlPointVisualiser ControlPointVisualiser => base.ControlPointVisualiser; public TestSliderBlueprint(Slider slider) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderTailPiece.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderTailPiece.cs new file mode 100644 index 0000000000..96169c5e1f --- /dev/null +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderTailPiece.cs @@ -0,0 +1,104 @@ +// 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.Extensions.Color4Extensions; +using osu.Framework.Input; +using osu.Framework.Input.Events; +using osu.Framework.Utils; +using osu.Game.Graphics; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Screens.Edit; +using osuTK; +using osuTK.Graphics; +using osuTK.Input; + +namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components +{ + public partial class SliderTailPiece : SliderCircleOverlay + { + /// + /// Whether this is currently being dragged. + /// + private bool isDragging; + + private InputManager inputManager = null!; + + [Resolved(CanBeNull = true)] + private EditorBeatmap? editorBeatmap { get; set; } + + [Resolved] + private OsuColour colours { get; set; } = null!; + + public SliderTailPiece(Slider slider, SliderPosition position) + : base(slider, position) + { + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + inputManager = GetContainingInputManager(); + } + + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => CirclePiece.ReceivePositionalInputAt(screenSpacePos); + + protected override bool OnHover(HoverEvent e) + { + updateCirclePieceColour(); + return false; + } + + protected override void OnHoverLost(HoverLostEvent e) + { + updateCirclePieceColour(); + } + + private void updateCirclePieceColour() + { + Color4 colour = colours.Yellow; + + if (IsHovered) + colour = colour.Lighten(1); + + CirclePiece.Colour = colour; + } + + protected override bool OnDragStart(DragStartEvent e) + { + if (e.Button == MouseButton.Right || !inputManager.CurrentState.Keyboard.ShiftPressed) + return false; + + isDragging = true; + editorBeatmap?.BeginChange(); + + return true; + } + + protected override void OnDrag(DragEvent e) + { + double proposedDistance = Slider.Path.Distance + e.Delta.X; + + proposedDistance = MathHelper.Clamp(proposedDistance, 0, Slider.Path.CalculatedDistance); + proposedDistance = MathHelper.Clamp(proposedDistance, + 0.1 * Slider.Path.Distance / Slider.SliderVelocityMultiplier, + 10 * Slider.Path.Distance / Slider.SliderVelocityMultiplier); + + if (Precision.AlmostEquals(proposedDistance, Slider.Path.Distance)) + return; + + Slider.SliderVelocityMultiplier *= proposedDistance / Slider.Path.Distance; + Slider.Path.ExpectedDistance.Value = proposedDistance; + editorBeatmap?.Update(Slider); + } + + protected override void OnDragEnd(DragEndEvent e) + { + if (isDragging) + { + editorBeatmap?.EndChange(); + } + } + } +} diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderCircleOverlay.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderCircleOverlay.cs index d47cf6bf23..b00a42748e 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderCircleOverlay.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderCircleOverlay.cs @@ -11,14 +11,14 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders public partial class SliderCircleOverlay : CompositeDrawable { protected readonly HitCirclePiece CirclePiece; + protected readonly Slider Slider; - private readonly Slider slider; - private readonly SliderPosition position; private readonly HitCircleOverlapMarker marker; + private readonly SliderPosition position; public SliderCircleOverlay(Slider slider, SliderPosition position) { - this.slider = slider; + Slider = slider; this.position = position; InternalChildren = new Drawable[] @@ -32,7 +32,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders { base.Update(); - var circle = position == SliderPosition.Start ? (HitCircle)slider.HeadCircle : slider.TailCircle; + var circle = position == SliderPosition.Start ? (HitCircle)Slider.HeadCircle : Slider.TailCircle; CirclePiece.UpdateFrom(circle); marker.UpdateFrom(circle); diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs index b3efe1c495..37c433cf8b 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs @@ -34,7 +34,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders protected SliderBodyPiece BodyPiece { get; private set; } protected SliderCircleOverlay HeadOverlay { get; private set; } - protected SliderCircleOverlay TailOverlay { get; private set; } + protected SliderTailPiece TailPiece { get; private set; } [CanBeNull] protected PathControlPointVisualiser ControlPointVisualiser { get; private set; } @@ -72,7 +72,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders { BodyPiece = new SliderBodyPiece(), HeadOverlay = CreateCircleOverlay(HitObject, SliderPosition.Start), - TailOverlay = CreateCircleOverlay(HitObject, SliderPosition.End), + TailPiece = new SliderTailPiece(HitObject, SliderPosition.End), }; } From 1258a9d378a53a7f352bb5bd6fb01f0a44f3305e Mon Sep 17 00:00:00 2001 From: OliBomby Date: Wed, 20 Dec 2023 01:48:42 +0100 Subject: [PATCH 0122/2493] Find closest distance value to mouse --- .../Sliders/Components/SliderTailPiece.cs | 60 +++++++++++++++++-- 1 file changed, 55 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderTailPiece.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderTailPiece.cs index 96169c5e1f..5cf9346f2e 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderTailPiece.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderTailPiece.cs @@ -1,12 +1,15 @@ // 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.Framework.Caching; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Input; using osu.Framework.Input.Events; using osu.Framework.Utils; using osu.Game.Graphics; +using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Screens.Edit; using osuTK; @@ -24,6 +27,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components private InputManager inputManager = null!; + private readonly Cached fullPathCache = new Cached(); + [Resolved(CanBeNull = true)] private EditorBeatmap? editorBeatmap { get; set; } @@ -33,6 +38,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components public SliderTailPiece(Slider slider, SliderPosition position) : base(slider, position) { + Slider.Path.ControlPoints.CollectionChanged += (_, _) => fullPathCache.Invalidate(); } protected override void LoadComplete() @@ -78,17 +84,18 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components protected override void OnDrag(DragEvent e) { - double proposedDistance = Slider.Path.Distance + e.Delta.X; + double oldDistance = Slider.Path.Distance; + double proposedDistance = findClosestPathDistance(e); proposedDistance = MathHelper.Clamp(proposedDistance, 0, Slider.Path.CalculatedDistance); proposedDistance = MathHelper.Clamp(proposedDistance, - 0.1 * Slider.Path.Distance / Slider.SliderVelocityMultiplier, - 10 * Slider.Path.Distance / Slider.SliderVelocityMultiplier); + 0.1 * oldDistance / Slider.SliderVelocityMultiplier, + 10 * oldDistance / Slider.SliderVelocityMultiplier); - if (Precision.AlmostEquals(proposedDistance, Slider.Path.Distance)) + if (Precision.AlmostEquals(proposedDistance, oldDistance)) return; - Slider.SliderVelocityMultiplier *= proposedDistance / Slider.Path.Distance; + Slider.SliderVelocityMultiplier *= proposedDistance / oldDistance; Slider.Path.ExpectedDistance.Value = proposedDistance; editorBeatmap?.Update(Slider); } @@ -100,5 +107,48 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components editorBeatmap?.EndChange(); } } + + /// + /// Finds the expected distance value for which the slider end is closest to the mouse position. + /// + private double findClosestPathDistance(DragEvent e) + { + const double step1 = 10; + const double step2 = 0.1; + + var desiredPosition = e.MousePosition - Slider.Position; + + if (!fullPathCache.IsValid) + fullPathCache.Value = new SliderPath(Slider.Path.ControlPoints.ToArray()); + + // Do a linear search to find the closest point on the path to the mouse position. + double bestValue = 0; + double minDistance = double.MaxValue; + + for (double d = 0; d <= fullPathCache.Value.CalculatedDistance; d += step1) + { + double t = d / fullPathCache.Value.CalculatedDistance; + float dist = Vector2.Distance(fullPathCache.Value.PositionAt(t), desiredPosition); + + if (dist >= minDistance) continue; + + minDistance = dist; + bestValue = d; + } + + // Do another linear search to fine-tune the result. + for (double d = bestValue - step1; d <= bestValue + step1; d += step2) + { + double t = d / fullPathCache.Value.CalculatedDistance; + float dist = Vector2.Distance(fullPathCache.Value.PositionAt(t), desiredPosition); + + if (dist >= minDistance) continue; + + minDistance = dist; + bestValue = d; + } + + return bestValue; + } } } From 1365a1b7bec8d444dfe99f098ed2ad2c1863f770 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Wed, 20 Dec 2023 01:57:11 +0100 Subject: [PATCH 0123/2493] fix tests --- .../Editor/TestSceneSliderControlPointPiece.cs | 13 ++++++++++++- .../Editor/TestSceneSliderSelectionBlueprint.cs | 15 +++++++++++++-- .../Sliders/SliderSelectionBlueprint.cs | 3 ++- 3 files changed, 27 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderControlPointPiece.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderControlPointPiece.cs index 085e11460f..1a7430704d 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderControlPointPiece.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderControlPointPiece.cs @@ -353,7 +353,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor { public new SliderBodyPiece BodyPiece => base.BodyPiece; public new TestSliderCircleOverlay HeadOverlay => (TestSliderCircleOverlay)base.HeadOverlay; - public new TestSliderCircleOverlay TailOverlay => (TestSliderCircleOverlay)base.TailPiece; + public new TestSliderTailPiece TailPiece => (TestSliderTailPiece)base.TailPiece; public new PathControlPointVisualiser ControlPointVisualiser => base.ControlPointVisualiser; public TestSliderBlueprint(Slider slider) @@ -362,6 +362,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor } protected override SliderCircleOverlay CreateCircleOverlay(Slider slider, SliderPosition position) => new TestSliderCircleOverlay(slider, position); + protected override SliderTailPiece CreateTailPiece(Slider slider, SliderPosition position) => new TestSliderTailPiece(slider, position); } private partial class TestSliderCircleOverlay : SliderCircleOverlay @@ -373,5 +374,15 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor { } } + + private partial class TestSliderTailPiece : SliderTailPiece + { + public new HitCirclePiece CirclePiece => base.CirclePiece; + + public TestSliderTailPiece(Slider slider, SliderPosition position) + : base(slider, position) + { + } + } } } diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSelectionBlueprint.cs index adc4929227..2c5cff3f70 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSelectionBlueprint.cs @@ -179,7 +179,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor () => Precision.AlmostEquals(blueprint.HeadOverlay.CirclePiece.ScreenSpaceDrawQuad.Centre, drawableObject.HeadCircle.ScreenSpaceDrawQuad.Centre)); AddAssert("tail positioned correctly", - () => Precision.AlmostEquals(blueprint.TailOverlay.CirclePiece.ScreenSpaceDrawQuad.Centre, drawableObject.TailCircle.ScreenSpaceDrawQuad.Centre)); + () => Precision.AlmostEquals(blueprint.TailPiece.CirclePiece.ScreenSpaceDrawQuad.Centre, drawableObject.TailCircle.ScreenSpaceDrawQuad.Centre)); } private void moveMouseToControlPoint(int index) @@ -198,7 +198,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor { public new SliderBodyPiece BodyPiece => base.BodyPiece; public new TestSliderCircleOverlay HeadOverlay => (TestSliderCircleOverlay)base.HeadOverlay; - public new TestSliderCircleOverlay TailOverlay => (TestSliderCircleOverlay)base.TailPiece; + public new TestSliderTailPiece TailPiece => (TestSliderTailPiece)base.TailPiece; public new PathControlPointVisualiser ControlPointVisualiser => base.ControlPointVisualiser; public TestSliderBlueprint(Slider slider) @@ -207,6 +207,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor } protected override SliderCircleOverlay CreateCircleOverlay(Slider slider, SliderPosition position) => new TestSliderCircleOverlay(slider, position); + protected override SliderTailPiece CreateTailPiece(Slider slider, SliderPosition position) => new TestSliderTailPiece(slider, position); } private partial class TestSliderCircleOverlay : SliderCircleOverlay @@ -218,5 +219,15 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor { } } + + private partial class TestSliderTailPiece : SliderTailPiece + { + public new HitCirclePiece CirclePiece => base.CirclePiece; + + public TestSliderTailPiece(Slider slider, SliderPosition position) + : base(slider, position) + { + } + } } } diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs index 37c433cf8b..a13eeb0208 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs @@ -72,7 +72,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders { BodyPiece = new SliderBodyPiece(), HeadOverlay = CreateCircleOverlay(HitObject, SliderPosition.Start), - TailPiece = new SliderTailPiece(HitObject, SliderPosition.End), + TailPiece = CreateTailPiece(HitObject, SliderPosition.End), }; } @@ -415,5 +415,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders BodyPiece.ReceivePositionalInputAt(screenSpacePos) || ControlPointVisualiser?.Pieces.Any(p => p.ReceivePositionalInputAt(screenSpacePos)) == true; protected virtual SliderCircleOverlay CreateCircleOverlay(Slider slider, SliderPosition position) => new SliderCircleOverlay(slider, position); + protected virtual SliderTailPiece CreateTailPiece(Slider slider, SliderPosition position) => new SliderTailPiece(slider, position); } } From 3aaf0b39f56e5d3df4a597a5925258470fbb73c3 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Wed, 20 Dec 2023 02:12:16 +0100 Subject: [PATCH 0124/2493] Add slider tail dragging test --- .../TestSceneSliderSelectionBlueprint.cs | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSelectionBlueprint.cs index 2c5cff3f70..3f9620a8d1 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSelectionBlueprint.cs @@ -163,6 +163,54 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor checkControlPointSelected(1, false); } + [Test] + public void TestDragSliderTail() + { + AddStep($"move mouse to slider tail", () => + { + Vector2 position = slider.EndPosition + new Vector2(10, 0); + InputManager.MoveMouseTo(drawableObject.Parent!.ToScreenSpace(position)); + }); + AddStep("shift + drag", () => + { + InputManager.PressKey(Key.ShiftLeft); + InputManager.PressButton(MouseButton.Left); + }); + moveMouseToControlPoint(1); + AddStep("release", () => + { + InputManager.ReleaseButton(MouseButton.Left); + InputManager.ReleaseKey(Key.ShiftLeft); + }); + + AddAssert("expected distance halved", + () => Precision.AlmostEquals(slider.Path.Distance, 172.2, 0.1)); + + AddStep($"move mouse to slider tail", () => + { + Vector2 position = slider.EndPosition + new Vector2(10, 0); + InputManager.MoveMouseTo(drawableObject.Parent!.ToScreenSpace(position)); + }); + AddStep("shift + drag", () => + { + InputManager.PressKey(Key.ShiftLeft); + InputManager.PressButton(MouseButton.Left); + }); + AddStep($"move mouse beyond last control point", () => + { + Vector2 position = slider.Position + slider.Path.ControlPoints[2].Position + new Vector2(50, 0); + InputManager.MoveMouseTo(drawableObject.Parent!.ToScreenSpace(position)); + }); + AddStep("release", () => + { + InputManager.ReleaseButton(MouseButton.Left); + InputManager.ReleaseKey(Key.ShiftLeft); + }); + + AddAssert("expected distance is calculated distance", + () => Precision.AlmostEquals(slider.Path.Distance, slider.Path.CalculatedDistance, 0.1)); + } + private void moveHitObject() { AddStep("move hitobject", () => From 66f4dcc578b3f760e3d2e6935fca2fecfec526a7 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Wed, 20 Dec 2023 02:32:20 +0100 Subject: [PATCH 0125/2493] fix repeat sliders half --- .../Edit/Blueprints/Sliders/Components/SliderTailPiece.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderTailPiece.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderTailPiece.cs index 5cf9346f2e..e60e41f6d5 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderTailPiece.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderTailPiece.cs @@ -65,7 +65,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components { Color4 colour = colours.Yellow; - if (IsHovered) + if (IsHovered && Slider.RepeatCount % 2 == 0) colour = colour.Lighten(1); CirclePiece.Colour = colour; @@ -73,7 +73,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components protected override bool OnDragStart(DragStartEvent e) { - if (e.Button == MouseButton.Right || !inputManager.CurrentState.Keyboard.ShiftPressed) + // Disable dragging if the slider has an uneven number of repeats because the slider tail will be on the wrong side of the path. + if (e.Button == MouseButton.Right || !inputManager.CurrentState.Keyboard.ShiftPressed || Slider.RepeatCount % 2 == 1) return false; isDragging = true; From f7cb6b9ed09bd041f3c21448abcb83c8de3ed09e Mon Sep 17 00:00:00 2001 From: OliBomby Date: Wed, 20 Dec 2023 12:58:32 +0100 Subject: [PATCH 0126/2493] Fix all repeat sliders being draggable --- .../Edit/Blueprints/Sliders/Components/SliderTailPiece.cs | 5 ++--- .../Edit/Blueprints/Sliders/SliderCircleOverlay.cs | 3 ++- osu.Game.Rulesets.Osu/Objects/Slider.cs | 8 +++++++- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderTailPiece.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderTailPiece.cs index e60e41f6d5..5cf9346f2e 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderTailPiece.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderTailPiece.cs @@ -65,7 +65,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components { Color4 colour = colours.Yellow; - if (IsHovered && Slider.RepeatCount % 2 == 0) + if (IsHovered) colour = colour.Lighten(1); CirclePiece.Colour = colour; @@ -73,8 +73,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components protected override bool OnDragStart(DragStartEvent e) { - // Disable dragging if the slider has an uneven number of repeats because the slider tail will be on the wrong side of the path. - if (e.Button == MouseButton.Right || !inputManager.CurrentState.Keyboard.ShiftPressed || Slider.RepeatCount % 2 == 1) + if (e.Button == MouseButton.Right || !inputManager.CurrentState.Keyboard.ShiftPressed) return false; isDragging = true; diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderCircleOverlay.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderCircleOverlay.cs index b00a42748e..2bf5118039 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderCircleOverlay.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderCircleOverlay.cs @@ -32,7 +32,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders { base.Update(); - var circle = position == SliderPosition.Start ? (HitCircle)Slider.HeadCircle : Slider.TailCircle; + var circle = position == SliderPosition.Start ? (HitCircle)Slider.HeadCircle : + Slider.RepeatCount % 2 == 0 ? Slider.TailCircle : Slider.LastRepeat; CirclePiece.UpdateFrom(circle); marker.UpdateFrom(circle); diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs index 506145568e..7a22bf5c4d 100644 --- a/osu.Game.Rulesets.Osu/Objects/Slider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs @@ -162,6 +162,9 @@ namespace osu.Game.Rulesets.Osu.Objects [JsonIgnore] public SliderTailCircle TailCircle { get; protected set; } + [JsonIgnore] + public SliderRepeat LastRepeat { get; protected set; } + public Slider() { SamplesBindable.CollectionChanged += (_, _) => UpdateNestedSamples(); @@ -225,7 +228,7 @@ namespace osu.Game.Rulesets.Osu.Objects break; case SliderEventType.Repeat: - AddNested(new SliderRepeat(this) + AddNested(LastRepeat = new SliderRepeat(this) { RepeatIndex = e.SpanIndex, StartTime = StartTime + (e.SpanIndex + 1) * SpanDuration, @@ -248,6 +251,9 @@ namespace osu.Game.Rulesets.Osu.Objects if (TailCircle != null) TailCircle.Position = EndPosition; + + if (LastRepeat != null) + LastRepeat.Position = RepeatCount % 2 == 0 ? Position : Position + Path.PositionAt(1); } protected void UpdateNestedSamples() From d000da725df5d5a2e031c96ce1861d581feac497 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Wed, 20 Dec 2023 13:14:05 +0100 Subject: [PATCH 0127/2493] fix code quality --- .../Editor/TestSceneSliderSelectionBlueprint.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSelectionBlueprint.cs index 3f9620a8d1..3faf181465 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSelectionBlueprint.cs @@ -166,7 +166,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor [Test] public void TestDragSliderTail() { - AddStep($"move mouse to slider tail", () => + AddStep("move mouse to slider tail", () => { Vector2 position = slider.EndPosition + new Vector2(10, 0); InputManager.MoveMouseTo(drawableObject.Parent!.ToScreenSpace(position)); @@ -186,7 +186,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor AddAssert("expected distance halved", () => Precision.AlmostEquals(slider.Path.Distance, 172.2, 0.1)); - AddStep($"move mouse to slider tail", () => + AddStep("move mouse to slider tail", () => { Vector2 position = slider.EndPosition + new Vector2(10, 0); InputManager.MoveMouseTo(drawableObject.Parent!.ToScreenSpace(position)); @@ -196,7 +196,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor InputManager.PressKey(Key.ShiftLeft); InputManager.PressButton(MouseButton.Left); }); - AddStep($"move mouse beyond last control point", () => + AddStep("move mouse beyond last control point", () => { Vector2 position = slider.Position + slider.Path.ControlPoints[2].Position + new Vector2(50, 0); InputManager.MoveMouseTo(drawableObject.Parent!.ToScreenSpace(position)); From eedb436389afe01b1666d6e3ed73d10cc8b2d070 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 30 Dec 2023 03:47:42 +0300 Subject: [PATCH 0128/2493] Move combo counter to ruleset-specific HUD components target --- .../Argon/CatchArgonSkinTransformer.cs | 2 +- .../Skinning/Argon/OsuArgonSkinTransformer.cs | 2 +- .../Argon/TaikoArgonSkinTransformer.cs | 8 +-- osu.Game/Rulesets/Ruleset.cs | 14 ++++- osu.Game/Skinning/ArgonSkin.cs | 16 +----- osu.Game/Skinning/ArgonSkinTransformer.cs | 53 +++++++++++++++++++ osu.Game/Skinning/LegacySkin.cs | 1 - osu.Game/Skinning/LegacySkinTransformer.cs | 44 +++++++++++++-- 8 files changed, 113 insertions(+), 27 deletions(-) create mode 100644 osu.Game/Skinning/ArgonSkinTransformer.cs diff --git a/osu.Game.Rulesets.Catch/Skinning/Argon/CatchArgonSkinTransformer.cs b/osu.Game.Rulesets.Catch/Skinning/Argon/CatchArgonSkinTransformer.cs index 520c2de248..a67945df98 100644 --- a/osu.Game.Rulesets.Catch/Skinning/Argon/CatchArgonSkinTransformer.cs +++ b/osu.Game.Rulesets.Catch/Skinning/Argon/CatchArgonSkinTransformer.cs @@ -6,7 +6,7 @@ using osu.Game.Skinning; namespace osu.Game.Rulesets.Catch.Skinning.Argon { - public class CatchArgonSkinTransformer : SkinTransformer + public class CatchArgonSkinTransformer : ArgonSkinTransformer { public CatchArgonSkinTransformer(ISkin skin) : base(skin) diff --git a/osu.Game.Rulesets.Osu/Skinning/Argon/OsuArgonSkinTransformer.cs b/osu.Game.Rulesets.Osu/Skinning/Argon/OsuArgonSkinTransformer.cs index 0f9c97059c..9526ea05c9 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Argon/OsuArgonSkinTransformer.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Argon/OsuArgonSkinTransformer.cs @@ -7,7 +7,7 @@ using osu.Game.Skinning; namespace osu.Game.Rulesets.Osu.Skinning.Argon { - public class OsuArgonSkinTransformer : SkinTransformer + public class OsuArgonSkinTransformer : ArgonSkinTransformer { public OsuArgonSkinTransformer(ISkin skin) : base(skin) diff --git a/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs b/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs index 9fcecd2b1a..7d38d6c9e5 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs @@ -7,16 +7,16 @@ using osu.Game.Skinning; namespace osu.Game.Rulesets.Taiko.Skinning.Argon { - public class TaikoArgonSkinTransformer : SkinTransformer + public class TaikoArgonSkinTransformer : ArgonSkinTransformer { public TaikoArgonSkinTransformer(ISkin skin) : base(skin) { } - public override Drawable? GetDrawableComponent(ISkinComponentLookup component) + public override Drawable? GetDrawableComponent(ISkinComponentLookup lookup) { - switch (component) + switch (lookup) { case GameplaySkinComponentLookup resultComponent: // This should eventually be moved to a skin setting, when supported. @@ -75,7 +75,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon break; } - return base.GetDrawableComponent(component); + return base.GetDrawableComponent(lookup); } } } diff --git a/osu.Game/Rulesets/Ruleset.cs b/osu.Game/Rulesets/Ruleset.cs index 37a35fd3ae..c7d4779064 100644 --- a/osu.Game/Rulesets/Ruleset.cs +++ b/osu.Game/Rulesets/Ruleset.cs @@ -212,7 +212,19 @@ namespace osu.Game.Rulesets /// The source skin. /// The current beatmap. /// A skin with a transformer applied, or null if no transformation is provided by this ruleset. - public virtual ISkin? CreateSkinTransformer(ISkin skin, IBeatmap beatmap) => null; + public virtual ISkin? CreateSkinTransformer(ISkin skin, IBeatmap beatmap) + { + switch (skin) + { + case LegacySkin: + return new LegacySkinTransformer(skin); + + case ArgonSkin: + return new ArgonSkinTransformer(skin); + } + + return null; + } protected Ruleset() { diff --git a/osu.Game/Skinning/ArgonSkin.cs b/osu.Game/Skinning/ArgonSkin.cs index 6fcab6a977..bdb65713a0 100644 --- a/osu.Game/Skinning/ArgonSkin.cs +++ b/osu.Game/Skinning/ArgonSkin.cs @@ -111,14 +111,13 @@ namespace osu.Game.Skinning return songSelectComponents; case SkinComponentsContainerLookup.TargetArea.MainHUDComponents: - var skinnableTargetWrapper = new DefaultSkinComponentsContainer(container => + var mainHUDComponents = new DefaultSkinComponentsContainer(container => { var health = container.OfType().FirstOrDefault(); var healthLine = container.OfType().FirstOrDefault(); var wedgePieces = container.OfType().ToArray(); var score = container.OfType().FirstOrDefault(); var accuracy = container.OfType().FirstOrDefault(); - var combo = container.OfType().FirstOrDefault(); var songProgress = container.OfType().FirstOrDefault(); var keyCounter = container.OfType().FirstOrDefault(); @@ -192,13 +191,6 @@ namespace osu.Game.Skinning keyCounter.Origin = Anchor.BottomRight; keyCounter.Position = new Vector2(-(hitError.Width + padding), -(padding * 2 + song_progress_offset_height)); } - - if (combo != null && hitError != null) - { - combo.Anchor = Anchor.BottomLeft; - combo.Origin = Anchor.BottomLeft; - combo.Position = new Vector2((hitError.Width + padding), -(padding * 2 + song_progress_offset_height)); - } } } }) @@ -224,10 +216,6 @@ namespace osu.Game.Skinning CornerRadius = { Value = 0.5f } }, new ArgonAccuracyCounter(), - new ArgonComboCounter - { - Scale = new Vector2(1.3f) - }, new BarHitErrorMeter(), new BarHitErrorMeter(), new ArgonSongProgress(), @@ -235,7 +223,7 @@ namespace osu.Game.Skinning } }; - return skinnableTargetWrapper; + return mainHUDComponents; } return null; diff --git a/osu.Game/Skinning/ArgonSkinTransformer.cs b/osu.Game/Skinning/ArgonSkinTransformer.cs new file mode 100644 index 0000000000..387a7a9c0b --- /dev/null +++ b/osu.Game/Skinning/ArgonSkinTransformer.cs @@ -0,0 +1,53 @@ +// 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.Graphics; +using osu.Game.Screens.Play.HUD; +using osuTK; + +namespace osu.Game.Skinning +{ + public class ArgonSkinTransformer : SkinTransformer + { + public ArgonSkinTransformer(ISkin skin) + : base(skin) + { + } + + public override Drawable? GetDrawableComponent(ISkinComponentLookup lookup) + { + switch (lookup) + { + case SkinComponentsContainerLookup containerLookup: + switch (containerLookup.Target) + { + case SkinComponentsContainerLookup.TargetArea.MainHUDComponents when containerLookup.Ruleset != null: + var rulesetHUDComponents = Skin.GetDrawableComponent(lookup); + + rulesetHUDComponents ??= new DefaultSkinComponentsContainer(container => + { + var combo = container.OfType().FirstOrDefault(); + + if (combo != null) + { + combo.Anchor = Anchor.BottomLeft; + combo.Origin = Anchor.BottomLeft; + combo.Position = new Vector2(36, -66); + combo.Scale = new Vector2(1.3f); + } + }) + { + new ArgonComboCounter(), + }; + + return rulesetHUDComponents; + } + + break; + } + + return base.GetDrawableComponent(lookup); + } + } +} diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index 8f0cd59b68..b8e721165e 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -399,7 +399,6 @@ namespace osu.Game.Skinning { Children = new Drawable[] { - new LegacyComboCounter(), new LegacyScoreCounter(), new LegacyAccuracyCounter(), new LegacySongProgress(), diff --git a/osu.Game/Skinning/LegacySkinTransformer.cs b/osu.Game/Skinning/LegacySkinTransformer.cs index 367e5bae01..3ea316c0c7 100644 --- a/osu.Game/Skinning/LegacySkinTransformer.cs +++ b/osu.Game/Skinning/LegacySkinTransformer.cs @@ -1,28 +1,62 @@ // 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.Audio.Sample; +using osu.Framework.Graphics; using osu.Game.Audio; using osu.Game.Rulesets.Objects.Legacy; +using osuTK; using static osu.Game.Skinning.SkinConfiguration; namespace osu.Game.Skinning { - /// - /// Transformer used to handle support of legacy features for individual rulesets. - /// - public abstract class LegacySkinTransformer : SkinTransformer + public class LegacySkinTransformer : SkinTransformer { /// /// Whether the skin being transformed is able to provide legacy resources for the ruleset. /// public virtual bool IsProvidingLegacyResources => this.HasFont(LegacyFont.Combo); - protected LegacySkinTransformer(ISkin skin) + public LegacySkinTransformer(ISkin skin) : base(skin) { } + public override Drawable? GetDrawableComponent(ISkinComponentLookup lookup) + { + switch (lookup) + { + case SkinComponentsContainerLookup containerLookup: + switch (containerLookup.Target) + { + case SkinComponentsContainerLookup.TargetArea.MainHUDComponents when containerLookup.Ruleset != null: + var rulesetHUDComponents = base.GetDrawableComponent(lookup); + + rulesetHUDComponents ??= new DefaultSkinComponentsContainer(container => + { + var combo = container.OfType().FirstOrDefault(); + + if (combo != null) + { + combo.Anchor = Anchor.BottomLeft; + combo.Origin = Anchor.BottomLeft; + combo.Scale = new Vector2(1.28f); + } + }) + { + new LegacyComboCounter() + }; + + return rulesetHUDComponents; + } + + break; + } + + return base.GetDrawableComponent(lookup); + } + public override ISample? GetSample(ISampleInfo sampleInfo) { if (!(sampleInfo is ConvertHitObjectParser.LegacyHitSampleInfo legacySample)) From e469e06271e4e4b23a93b5d0c30bf7693fb947e8 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 30 Dec 2023 03:54:53 +0300 Subject: [PATCH 0129/2493] Refactor `CatchLegacySkinTransformer` logic and remove `HiddenByRulesetImplementation` entirely --- .../TestSceneCatchPlayerLegacySkin.cs | 8 +- .../Legacy/CatchLegacySkinTransformer.cs | 102 ++++++++---------- .../TestSceneSkinnableComboCounter.cs | 13 --- osu.Game/Skinning/LegacyComboCounter.cs | 12 --- 4 files changed, 49 insertions(+), 86 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatchPlayerLegacySkin.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchPlayerLegacySkin.cs index 5406230359..99325e14c8 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatchPlayerLegacySkin.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchPlayerLegacySkin.cs @@ -4,7 +4,6 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Extensions.IEnumerableExtensions; -using osu.Framework.Graphics.Containers; using osu.Framework.Screens; using osu.Framework.Testing; using osu.Game.Skinning; @@ -19,7 +18,7 @@ namespace osu.Game.Rulesets.Catch.Tests protected override Ruleset CreatePlayerRuleset() => new CatchRuleset(); [Test] - public void TestLegacyHUDComboCounterHidden([Values] bool withModifiedSkin) + public void TestLegacyHUDComboCounterNotExistent([Values] bool withModifiedSkin) { if (withModifiedSkin) { @@ -29,10 +28,7 @@ namespace osu.Game.Rulesets.Catch.Tests CreateTest(); } - AddAssert("legacy HUD combo counter hidden", () => - { - return Player.ChildrenOfType().All(c => c.ChildrenOfType().Single().Alpha == 0f); - }); + AddAssert("legacy HUD combo counter not added", () => !Player.ChildrenOfType().Any()); } } } diff --git a/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs b/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs index fb8af9bdb6..675c61a2c5 100644 --- a/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs @@ -1,10 +1,9 @@ // 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 System.Diagnostics; using osu.Framework.Bindables; using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; using osu.Game.Skinning; using osuTK.Graphics; @@ -28,76 +27,69 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy public override Drawable? GetDrawableComponent(ISkinComponentLookup lookup) { - if (lookup is SkinComponentsContainerLookup containerLookup) + switch (lookup) { - switch (containerLookup.Target) - { - case SkinComponentsContainerLookup.TargetArea.MainHUDComponents: - var components = base.GetDrawableComponent(lookup) as Container; + case SkinComponentsContainerLookup containerLookup: + switch (containerLookup.Target) + { + case SkinComponentsContainerLookup.TargetArea.MainHUDComponents when containerLookup.Ruleset != null: + Debug.Assert(containerLookup.Ruleset.ShortName == CatchRuleset.SHORT_NAME); + // todo: remove CatchSkinComponents.CatchComboCounter and refactor LegacyCatchComboCounter to be added here instead. + return Skin.GetDrawableComponent(lookup); + } - if (providesComboCounter && components != null) - { - // catch may provide its own combo counter; hide the default. - // todo: this should be done in an elegant way per ruleset, defining which HUD skin components should be displayed. - foreach (var legacyComboCounter in components.OfType()) - legacyComboCounter.HiddenByRulesetImplementation = false; - } + break; - return components; - } - } + case CatchSkinComponentLookup catchSkinComponent: + switch (catchSkinComponent.Component) + { + case CatchSkinComponents.Fruit: + if (hasPear) + return new LegacyFruitPiece(); - if (lookup is CatchSkinComponentLookup catchSkinComponent) - { - switch (catchSkinComponent.Component) - { - case CatchSkinComponents.Fruit: - if (hasPear) - return new LegacyFruitPiece(); + return null; - return null; + case CatchSkinComponents.Banana: + if (GetTexture("fruit-bananas") != null) + return new LegacyBananaPiece(); - case CatchSkinComponents.Banana: - if (GetTexture("fruit-bananas") != null) - return new LegacyBananaPiece(); + return null; - return null; + case CatchSkinComponents.Droplet: + if (GetTexture("fruit-drop") != null) + return new LegacyDropletPiece(); - case CatchSkinComponents.Droplet: - if (GetTexture("fruit-drop") != null) - return new LegacyDropletPiece(); + return null; - return null; + case CatchSkinComponents.Catcher: + decimal version = GetConfig(SkinConfiguration.LegacySetting.Version)?.Value ?? 1; - case CatchSkinComponents.Catcher: - decimal version = GetConfig(SkinConfiguration.LegacySetting.Version)?.Value ?? 1; + if (version < 2.3m) + { + if (hasOldStyleCatcherSprite()) + return new LegacyCatcherOld(); + } - if (version < 2.3m) - { - if (hasOldStyleCatcherSprite()) - return new LegacyCatcherOld(); - } + if (hasNewStyleCatcherSprite()) + return new LegacyCatcherNew(); - if (hasNewStyleCatcherSprite()) - return new LegacyCatcherNew(); + return null; - return null; + case CatchSkinComponents.CatchComboCounter: + if (providesComboCounter) + return new LegacyCatchComboCounter(); - case CatchSkinComponents.CatchComboCounter: - if (providesComboCounter) - return new LegacyCatchComboCounter(); + return null; - return null; + case CatchSkinComponents.HitExplosion: + if (hasOldStyleCatcherSprite() || hasNewStyleCatcherSprite()) + return new LegacyHitExplosion(); - case CatchSkinComponents.HitExplosion: - if (hasOldStyleCatcherSprite() || hasNewStyleCatcherSprite()) - return new LegacyHitExplosion(); + return null; - return null; - - default: - throw new UnsupportedSkinComponentException(lookup); - } + default: + throw new UnsupportedSkinComponentException(lookup); + } } return base.GetDrawableComponent(lookup); diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableComboCounter.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableComboCounter.cs index 72f40d9c6f..a15a3197c5 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableComboCounter.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableComboCounter.cs @@ -4,7 +4,6 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; -using osu.Framework.Testing; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Scoring; using osu.Game.Screens.Play.HUD; @@ -28,17 +27,5 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("reset combo", () => scoreProcessor.Combo.Value = 0); } - - [Test] - public void TestLegacyComboCounterHiddenByRulesetImplementation() - { - AddToggleStep("toggle legacy hidden by ruleset", visible => - { - foreach (var legacyCounter in this.ChildrenOfType()) - legacyCounter.HiddenByRulesetImplementation = visible; - }); - - AddRepeatStep("increase combo", () => scoreProcessor.Combo.Value++, 10); - } } } diff --git a/osu.Game/Skinning/LegacyComboCounter.cs b/osu.Game/Skinning/LegacyComboCounter.cs index cd72055fce..d77a39f607 100644 --- a/osu.Game/Skinning/LegacyComboCounter.cs +++ b/osu.Game/Skinning/LegacyComboCounter.cs @@ -43,18 +43,6 @@ namespace osu.Game.Skinning private readonly Container counterContainer; - /// - /// Hides the combo counter internally without affecting its . - /// - /// - /// This is used for rulesets that provide their own combo counter and don't want this HUD one to be visible, - /// without potentially affecting the user's selected skin. - /// - public bool HiddenByRulesetImplementation - { - set => counterContainer.Alpha = value ? 1 : 0; - } - public bool UsesFixedAnchor { get; set; } public LegacyComboCounter() From 78cb6b68518651a568c00c3434b9695ec76f6c53 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 30 Dec 2023 05:29:18 +0300 Subject: [PATCH 0130/2493] Abstractify `LegacyComboCounter` to re-use for mania --- .../TestSceneCatchPlayerLegacySkin.cs | 2 +- .../TestSceneSkinnableComboCounter.cs | 2 +- osu.Game/Skinning/LegacyComboCounter.cs | 163 +++++------------- .../Skinning/LegacyDefaultComboCounter.cs | 85 +++++++++ osu.Game/Skinning/LegacySkinTransformer.cs | 4 +- osu.Game/Skinning/Skin.cs | 1 + 6 files changed, 134 insertions(+), 123 deletions(-) create mode 100644 osu.Game/Skinning/LegacyDefaultComboCounter.cs diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatchPlayerLegacySkin.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchPlayerLegacySkin.cs index 99325e14c8..7812e02a63 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatchPlayerLegacySkin.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchPlayerLegacySkin.cs @@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.Catch.Tests CreateTest(); } - AddAssert("legacy HUD combo counter not added", () => !Player.ChildrenOfType().Any()); + AddAssert("legacy HUD combo counter not added", () => !Player.ChildrenOfType().Any()); } } } diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableComboCounter.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableComboCounter.cs index a15a3197c5..a6196a8ca0 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableComboCounter.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableComboCounter.cs @@ -18,7 +18,7 @@ namespace osu.Game.Tests.Visual.Gameplay protected override Drawable CreateArgonImplementation() => new ArgonComboCounter(); protected override Drawable CreateDefaultImplementation() => new DefaultComboCounter(); - protected override Drawable CreateLegacyImplementation() => new LegacyComboCounter(); + protected override Drawable CreateLegacyImplementation() => new LegacyDefaultComboCounter(); [Test] public void TestComboCounterIncrementing() diff --git a/osu.Game/Skinning/LegacyComboCounter.cs b/osu.Game/Skinning/LegacyComboCounter.cs index d77a39f607..7003e0d3c8 100644 --- a/osu.Game/Skinning/LegacyComboCounter.cs +++ b/osu.Game/Skinning/LegacyComboCounter.cs @@ -5,25 +5,17 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Sprites; using osu.Game.Rulesets.Scoring; -using osuTK; namespace osu.Game.Skinning { /// /// Uses the 'x' symbol and has a pop-out effect while rolling over. /// - public partial class LegacyComboCounter : CompositeDrawable, ISerialisableDrawable + public abstract partial class LegacyComboCounter : CompositeDrawable, ISerialisableDrawable { public Bindable Current { get; } = new BindableInt { MinValue = 0 }; - private uint scheduledPopOutCurrentId; - - private const double big_pop_out_duration = 300; - - private const double small_pop_out_duration = 100; - private const double fade_out_duration = 100; /// @@ -31,9 +23,8 @@ namespace osu.Game.Skinning /// private const double rolling_duration = 20; - private readonly Drawable popOutCount; - - private readonly Drawable displayedCountSpriteText; + protected readonly LegacySpriteText PopOutCountText; + protected readonly LegacySpriteText DisplayedCountText; private int previousValue; @@ -45,17 +36,10 @@ namespace osu.Game.Skinning public bool UsesFixedAnchor { get; set; } - public LegacyComboCounter() + protected LegacyComboCounter() { AutoSizeAxes = Axes.Both; - Anchor = Anchor.BottomLeft; - Origin = Anchor.BottomLeft; - - Margin = new MarginPadding(10); - - Scale = new Vector2(1.28f); - InternalChildren = new[] { counterContainer = new Container @@ -63,18 +47,16 @@ namespace osu.Game.Skinning AlwaysPresent = true, Children = new[] { - popOutCount = new LegacySpriteText(LegacyFont.Combo) + PopOutCountText = new LegacySpriteText(LegacyFont.Combo) { Alpha = 0, Blending = BlendingParameters.Additive, - Anchor = Anchor.BottomLeft, BypassAutoSizeAxes = Axes.Both, }, - displayedCountSpriteText = new LegacySpriteText(LegacyFont.Combo) + DisplayedCountText = new LegacySpriteText(LegacyFont.Combo) { Alpha = 0, AlwaysPresent = true, - Anchor = Anchor.BottomLeft, BypassAutoSizeAxes = Axes.Both, }, } @@ -114,26 +96,12 @@ namespace osu.Game.Skinning { base.LoadComplete(); - ((IHasText)displayedCountSpriteText).Text = formatCount(Current.Value); - ((IHasText)popOutCount).Text = formatCount(Current.Value); + DisplayedCountText.Text = FormatCount(Current.Value); + PopOutCountText.Text = FormatCount(Current.Value); Current.BindValueChanged(combo => updateCount(combo.NewValue == 0), true); - updateLayout(); - } - - private void updateLayout() - { - const float font_height_ratio = 0.625f; - const float vertical_offset = 9; - - displayedCountSpriteText.OriginPosition = new Vector2(0, font_height_ratio * displayedCountSpriteText.Height + vertical_offset); - displayedCountSpriteText.Position = new Vector2(0, -(1 - font_height_ratio) * displayedCountSpriteText.Height + vertical_offset); - - popOutCount.OriginPosition = new Vector2(3, font_height_ratio * popOutCount.Height + vertical_offset); // In stable, the bigger pop out scales a bit to the left - popOutCount.Position = new Vector2(0, -(1 - font_height_ratio) * popOutCount.Height + vertical_offset); - - counterContainer.Size = displayedCountSpriteText.Size; + counterContainer.Size = DisplayedCountText.Size; } private void updateCount(bool rolling) @@ -147,127 +115,84 @@ namespace osu.Game.Skinning if (!rolling) { FinishTransforms(false, nameof(DisplayedCount)); + isRolling = false; DisplayedCount = prev; if (prev + 1 == Current.Value) - onCountIncrement(prev, Current.Value); + OnCountIncrement(); else - onCountChange(Current.Value); + OnCountChange(); } else { - onCountRolling(displayedCount, Current.Value); + OnCountRolling(); isRolling = true; } } - private void transformPopOut(int newValue) + /// + /// Raised when the counter should display the new value with transitions. + /// + protected virtual void OnCountIncrement() { - ((IHasText)popOutCount).Text = formatCount(newValue); - - popOutCount.ScaleTo(1.56f) - .ScaleTo(1, big_pop_out_duration); - - popOutCount.FadeTo(0.6f) - .FadeOut(big_pop_out_duration); - } - - private void transformNoPopOut(int newValue) - { - ((IHasText)displayedCountSpriteText).Text = formatCount(newValue); - - counterContainer.Size = displayedCountSpriteText.Size; - - displayedCountSpriteText.ScaleTo(1); - } - - private void transformPopOutSmall(int newValue) - { - ((IHasText)displayedCountSpriteText).Text = formatCount(newValue); - - counterContainer.Size = displayedCountSpriteText.Size; - - displayedCountSpriteText.ScaleTo(1).Then() - .ScaleTo(1.1f, small_pop_out_duration / 2, Easing.In).Then() - .ScaleTo(1, small_pop_out_duration / 2, Easing.Out); - } - - private void scheduledPopOutSmall(uint id) - { - // Too late; scheduled task invalidated - if (id != scheduledPopOutCurrentId) - return; + if (DisplayedCount < Current.Value - 1) + DisplayedCount++; DisplayedCount++; } - private void onCountIncrement(int currentValue, int newValue) + /// + /// Raised when the counter should roll to the new combo value (usually roll back to zero). + /// + protected virtual void OnCountRolling() { - scheduledPopOutCurrentId++; - - if (DisplayedCount < currentValue) - DisplayedCount++; - - displayedCountSpriteText.Show(); - - transformPopOut(newValue); - - uint newTaskId = scheduledPopOutCurrentId; - - Scheduler.AddDelayed(delegate - { - scheduledPopOutSmall(newTaskId); - }, big_pop_out_duration - 140); - } - - private void onCountRolling(int currentValue, int newValue) - { - scheduledPopOutCurrentId++; - // Hides displayed count if was increasing from 0 to 1 but didn't finish - if (currentValue == 0 && newValue == 0) - displayedCountSpriteText.FadeOut(fade_out_duration); + if (DisplayedCount == 0 && Current.Value == 0) + DisplayedCountText.FadeOut(fade_out_duration); - transformRoll(currentValue, newValue); + transformRoll(DisplayedCount, Current.Value); } - private void onCountChange(int newValue) + /// + /// Raised when the counter should display the new combo value without any transitions. + /// + protected virtual void OnCountChange() { - scheduledPopOutCurrentId++; + if (Current.Value == 0) + DisplayedCountText.FadeOut(); - if (newValue == 0) - displayedCountSpriteText.FadeOut(); - - DisplayedCount = newValue; + DisplayedCount = Current.Value; } private void onDisplayedCountRolling(int newValue) { if (newValue == 0) - displayedCountSpriteText.FadeOut(fade_out_duration); - else - displayedCountSpriteText.Show(); + DisplayedCountText.FadeOut(fade_out_duration); - transformNoPopOut(newValue); + DisplayedCountText.Text = FormatCount(newValue); + counterContainer.Size = DisplayedCountText.Size; } private void onDisplayedCountChange(int newValue) { - displayedCountSpriteText.FadeTo(newValue == 0 ? 0 : 1); - transformNoPopOut(newValue); + DisplayedCountText.FadeTo(newValue == 0 ? 0 : 1); + DisplayedCountText.Text = FormatCount(newValue); + + counterContainer.Size = DisplayedCountText.Size; } private void onDisplayedCountIncrement(int newValue) { - displayedCountSpriteText.Show(); - transformPopOutSmall(newValue); + DisplayedCountText.Text = FormatCount(newValue); + + counterContainer.Size = DisplayedCountText.Size; } private void transformRoll(int currentValue, int newValue) => this.TransformTo(nameof(DisplayedCount), newValue, getProportionalDuration(currentValue, newValue)); - private string formatCount(int count) => $@"{count}x"; + protected virtual string FormatCount(int count) => $@"{count}"; private double getProportionalDuration(int currentValue, int newValue) { diff --git a/osu.Game/Skinning/LegacyDefaultComboCounter.cs b/osu.Game/Skinning/LegacyDefaultComboCounter.cs new file mode 100644 index 0000000000..f633358993 --- /dev/null +++ b/osu.Game/Skinning/LegacyDefaultComboCounter.cs @@ -0,0 +1,85 @@ +// 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.Graphics; +using osu.Framework.Threading; +using osuTK; + +namespace osu.Game.Skinning +{ + /// + /// Uses the 'x' symbol and has a pop-out effect while rolling over. + /// + public partial class LegacyDefaultComboCounter : LegacyComboCounter + { + private const double big_pop_out_duration = 300; + private const double small_pop_out_duration = 100; + + private ScheduledDelegate? scheduledPopOut; + + public LegacyDefaultComboCounter() + { + Margin = new MarginPadding(10); + + PopOutCountText.Anchor = Anchor.BottomLeft; + DisplayedCountText.Anchor = Anchor.BottomLeft; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + const float font_height_ratio = 0.625f; + const float vertical_offset = 9; + + DisplayedCountText.OriginPosition = new Vector2(0, font_height_ratio * DisplayedCountText.Height + vertical_offset); + DisplayedCountText.Position = new Vector2(0, -(1 - font_height_ratio) * DisplayedCountText.Height + vertical_offset); + + PopOutCountText.OriginPosition = new Vector2(3, font_height_ratio * PopOutCountText.Height + vertical_offset); // In stable, the bigger pop out scales a bit to the left + PopOutCountText.Position = new Vector2(0, -(1 - font_height_ratio) * PopOutCountText.Height + vertical_offset); + } + + protected override void OnCountIncrement() + { + scheduledPopOut?.Cancel(); + scheduledPopOut = null; + + DisplayedCountText.Show(); + + PopOutCountText.Text = FormatCount(Current.Value); + + PopOutCountText.ScaleTo(1.56f) + .ScaleTo(1, big_pop_out_duration); + + PopOutCountText.FadeTo(0.6f) + .FadeOut(big_pop_out_duration); + + this.Delay(big_pop_out_duration - 140).Schedule(() => + { + base.OnCountIncrement(); + + DisplayedCountText.ScaleTo(1).Then() + .ScaleTo(1.1f, small_pop_out_duration / 2, Easing.In).Then() + .ScaleTo(1, small_pop_out_duration / 2, Easing.Out); + }, out scheduledPopOut); + } + + protected override void OnCountRolling() + { + scheduledPopOut?.Cancel(); + scheduledPopOut = null; + + base.OnCountRolling(); + } + + protected override void OnCountChange() + { + scheduledPopOut?.Cancel(); + scheduledPopOut = null; + + base.OnCountChange(); + } + + protected override string FormatCount(int count) => $@"{count}x"; + } +} diff --git a/osu.Game/Skinning/LegacySkinTransformer.cs b/osu.Game/Skinning/LegacySkinTransformer.cs index 3ea316c0c7..66978fc6b0 100644 --- a/osu.Game/Skinning/LegacySkinTransformer.cs +++ b/osu.Game/Skinning/LegacySkinTransformer.cs @@ -35,7 +35,7 @@ namespace osu.Game.Skinning rulesetHUDComponents ??= new DefaultSkinComponentsContainer(container => { - var combo = container.OfType().FirstOrDefault(); + var combo = container.OfType().FirstOrDefault(); if (combo != null) { @@ -45,7 +45,7 @@ namespace osu.Game.Skinning } }) { - new LegacyComboCounter() + new LegacyDefaultComboCounter() }; return rulesetHUDComponents; diff --git a/osu.Game/Skinning/Skin.cs b/osu.Game/Skinning/Skin.cs index 9ee69d033d..80bb340109 100644 --- a/osu.Game/Skinning/Skin.cs +++ b/osu.Game/Skinning/Skin.cs @@ -153,6 +153,7 @@ namespace osu.Game.Skinning // handle namespace changes... jsonContent = jsonContent.Replace(@"osu.Game.Screens.Play.SongProgress", @"osu.Game.Screens.Play.HUD.DefaultSongProgress"); jsonContent = jsonContent.Replace(@"osu.Game.Screens.Play.HUD.LegacyComboCounter", @"osu.Game.Skinning.LegacyComboCounter"); + jsonContent = jsonContent.Replace(@"osu.Game.Skinning.LegacyComboCounter", @"osu.Game.Skinning.LegacyDefaultComboCounter"); var deserializedContent = JsonConvert.DeserializeObject>(jsonContent); From ece532b837a3ebe76791e8fc5c528cba854a5ad0 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 30 Dec 2023 05:18:56 +0300 Subject: [PATCH 0131/2493] Add legacy mania combo counter lookups --- osu.Game/Skinning/LegacyManiaSkinConfiguration.cs | 1 + osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs | 2 ++ osu.Game/Skinning/LegacyManiaSkinDecoder.cs | 4 ++++ osu.Game/Skinning/LegacySkin.cs | 6 ++++++ 4 files changed, 13 insertions(+) diff --git a/osu.Game/Skinning/LegacyManiaSkinConfiguration.cs b/osu.Game/Skinning/LegacyManiaSkinConfiguration.cs index 042836984a..db1f216b6e 100644 --- a/osu.Game/Skinning/LegacyManiaSkinConfiguration.cs +++ b/osu.Game/Skinning/LegacyManiaSkinConfiguration.cs @@ -39,6 +39,7 @@ namespace osu.Game.Skinning public float HitPosition = DEFAULT_HIT_POSITION; public float LightPosition = (480 - 413) * POSITION_SCALE_FACTOR; + public float ComboPosition = 111 * POSITION_SCALE_FACTOR; public float ScorePosition = 300 * POSITION_SCALE_FACTOR; public bool ShowJudgementLine = true; public bool KeysUnderNotes; diff --git a/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs b/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs index cacca0de23..fc90fc89eb 100644 --- a/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs +++ b/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs @@ -42,6 +42,7 @@ namespace osu.Game.Skinning LeftLineWidth, RightLineWidth, HitPosition, + ComboPosition, ScorePosition, LightPosition, StagePaddingTop, @@ -63,6 +64,7 @@ namespace osu.Game.Skinning JudgementLineColour, ColumnBackgroundColour, ColumnLightColour, + ComboBreakColour, MinimumColumnWidth, LeftStageImage, RightStageImage, diff --git a/osu.Game/Skinning/LegacyManiaSkinDecoder.cs b/osu.Game/Skinning/LegacyManiaSkinDecoder.cs index b472afb74f..5dd8f9c52d 100644 --- a/osu.Game/Skinning/LegacyManiaSkinDecoder.cs +++ b/osu.Game/Skinning/LegacyManiaSkinDecoder.cs @@ -94,6 +94,10 @@ namespace osu.Game.Skinning currentConfig.LightPosition = (480 - float.Parse(pair.Value, CultureInfo.InvariantCulture)) * LegacyManiaSkinConfiguration.POSITION_SCALE_FACTOR; break; + case "ComboPosition": + currentConfig.ComboPosition = (float.Parse(pair.Value, CultureInfo.InvariantCulture)) * LegacyManiaSkinConfiguration.POSITION_SCALE_FACTOR; + break; + case "ScorePosition": currentConfig.ScorePosition = (float.Parse(pair.Value, CultureInfo.InvariantCulture)) * LegacyManiaSkinConfiguration.POSITION_SCALE_FACTOR; break; diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index b8e721165e..848a6366ed 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -152,6 +152,9 @@ namespace osu.Game.Skinning case LegacyManiaSkinConfigurationLookups.HitPosition: return SkinUtils.As(new Bindable(existing.HitPosition)); + case LegacyManiaSkinConfigurationLookups.ComboPosition: + return SkinUtils.As(new Bindable(existing.ComboPosition)); + case LegacyManiaSkinConfigurationLookups.ScorePosition: return SkinUtils.As(new Bindable(existing.ScorePosition)); @@ -189,6 +192,9 @@ namespace osu.Game.Skinning Debug.Assert(maniaLookup.ColumnIndex != null); return SkinUtils.As(getCustomColour(existing, $"ColourLight{maniaLookup.ColumnIndex + 1}")); + case LegacyManiaSkinConfigurationLookups.ComboBreakColour: + return SkinUtils.As(getCustomColour(existing, "ColourBreak")); + case LegacyManiaSkinConfigurationLookups.MinimumColumnWidth: return SkinUtils.As(new Bindable(existing.MinimumColumnWidth)); From 8be3f4f632f62d70c365c45c1f48a1747b4579f3 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 30 Dec 2023 05:20:44 +0300 Subject: [PATCH 0132/2493] Add legacy mania combo counter implementation --- .../Legacy/LegacyManiaComboCounter.cs | 94 +++++++++++++++++++ .../Legacy/ManiaLegacySkinTransformer.cs | 31 ++++++ 2 files changed, 125 insertions(+) create mode 100644 osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyManiaComboCounter.cs diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyManiaComboCounter.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyManiaComboCounter.cs new file mode 100644 index 0000000000..fd309f6250 --- /dev/null +++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyManiaComboCounter.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 osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Game.Rulesets.Mania.UI; +using osu.Game.Rulesets.UI; +using osu.Game.Rulesets.UI.Scrolling; +using osu.Game.Skinning; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Rulesets.Mania.Skinning.Legacy +{ + public partial class LegacyManiaComboCounter : LegacyComboCounter, ISerialisableDrawable + { + private DrawableManiaRuleset maniaRuleset = null!; + + bool ISerialisableDrawable.SupportsClosestAnchor => false; + + [BackgroundDependencyLoader] + private void load(ISkinSource skin, DrawableRuleset ruleset) + { + maniaRuleset = (DrawableManiaRuleset)ruleset; + + DisplayedCountText.Anchor = Anchor.Centre; + DisplayedCountText.Origin = Anchor.Centre; + + PopOutCountText.Anchor = Anchor.Centre; + PopOutCountText.Origin = Anchor.Centre; + PopOutCountText.Colour = skin.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.ComboBreakColour)?.Value ?? Color4.Red; + + UsesFixedAnchor = true; + } + + private IBindable direction = null!; + + protected override void LoadComplete() + { + base.LoadComplete(); + + direction = maniaRuleset.ScrollingInfo.Direction.GetBoundCopy(); + direction.BindValueChanged(_ => updateAnchor()); + + // two schedules are required so that updateAnchor is executed in the next frame, + // which is when the combo counter receives its Y position by the default layout in LegacyManiaSkinTransformer. + Schedule(() => Schedule(updateAnchor)); + } + + private void updateAnchor() + { + Anchor &= ~(Anchor.y0 | Anchor.y2); + Anchor |= direction.Value == ScrollingDirection.Up ? Anchor.y2 : Anchor.y0; + + // since we flip the vertical anchor when changing scroll direction, + // we can use the sign of the Y value as an indicator to make the combo counter displayed correctly. + if ((Y < 0 && direction.Value == ScrollingDirection.Down) || (Y > 0 && direction.Value == ScrollingDirection.Up)) + Y = -Y; + } + + protected override void OnCountIncrement() + { + base.OnCountIncrement(); + + PopOutCountText.Hide(); + DisplayedCountText.ScaleTo(new Vector2(1f, 1.4f)) + .ScaleTo(new Vector2(1f), 300, Easing.Out) + .FadeIn(120); + } + + protected override void OnCountChange() + { + base.OnCountChange(); + + PopOutCountText.Hide(); + DisplayedCountText.ScaleTo(1f); + } + + protected override void OnCountRolling() + { + if (DisplayedCount > 0) + { + PopOutCountText.Text = FormatCount(DisplayedCount); + PopOutCountText.FadeTo(0.8f).FadeOut(200) + .ScaleTo(1f).ScaleTo(4f, 200); + + DisplayedCountText.FadeTo(0.5f, 300); + } + + base.OnCountRolling(); + } + } +} diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs index 73c521b2ed..c539c239bd 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs @@ -5,9 +5,12 @@ using System; using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; using osu.Framework.Audio.Sample; using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Framework.Testing; using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Rulesets.Mania.Beatmaps; @@ -78,6 +81,34 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy { switch (lookup) { + case SkinComponentsContainerLookup containerLookup: + switch (containerLookup.Target) + { + case SkinComponentsContainerLookup.TargetArea.MainHUDComponents when containerLookup.Ruleset != null: + Debug.Assert(containerLookup.Ruleset.ShortName == ManiaRuleset.SHORT_NAME); + + var rulesetHUDComponents = Skin.GetDrawableComponent(lookup); + + rulesetHUDComponents ??= new DefaultSkinComponentsContainer(container => + { + var combo = container.ChildrenOfType().FirstOrDefault(); + + if (combo != null) + { + combo.Anchor = Anchor.TopCentre; + combo.Origin = Anchor.Centre; + combo.Y = this.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.ComboPosition)?.Value ?? 0; + } + }) + { + new LegacyManiaComboCounter(), + }; + + return rulesetHUDComponents; + } + + break; + case GameplaySkinComponentLookup resultComponent: return getResult(resultComponent.Component); From 408287e086b18187c72e0bde57571be39fd621b0 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 30 Dec 2023 05:20:51 +0300 Subject: [PATCH 0133/2493] Add very basic argon mania combo counter implementation --- .../Skinning/Argon/ArgonManiaComboCounter.cs | 84 +++++++++++++++++++ .../Argon/ManiaArgonSkinTransformer.cs | 33 +++++++- 2 files changed, 116 insertions(+), 1 deletion(-) create mode 100644 osu.Game.Rulesets.Mania/Skinning/Argon/ArgonManiaComboCounter.cs diff --git a/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonManiaComboCounter.cs b/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonManiaComboCounter.cs new file mode 100644 index 0000000000..1c8e43345a --- /dev/null +++ b/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonManiaComboCounter.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 osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Sprites; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Rulesets.Mania.UI; +using osu.Game.Rulesets.Scoring; +using osu.Game.Rulesets.UI; +using osu.Game.Rulesets.UI.Scrolling; +using osu.Game.Screens.Play.HUD; +using osu.Game.Skinning; +using osuTK.Graphics; + +namespace osu.Game.Rulesets.Mania.Skinning.Argon +{ + public partial class ArgonManiaComboCounter : ComboCounter, ISerialisableDrawable + { + private OsuSpriteText text = null!; + + protected override double RollingDuration => 500; + protected override Easing RollingEasing => Easing.OutQuint; + + private DrawableManiaRuleset maniaRuleset = null!; + + bool ISerialisableDrawable.SupportsClosestAnchor => false; + + [BackgroundDependencyLoader] + private void load(DrawableRuleset ruleset, ScoreProcessor scoreProcessor) + { + maniaRuleset = (DrawableManiaRuleset)ruleset; + + Current.BindTo(scoreProcessor.Combo); + Current.BindValueChanged(combo => + { + if (combo.OldValue == 0 && combo.NewValue > 0) + text.FadeIn(200, Easing.OutQuint); + else if (combo.OldValue > 0 && combo.NewValue == 0) + { + if (combo.OldValue > 1) + text.FlashColour(Color4.Red, 2000, Easing.OutQuint); + + text.FadeOut(200, Easing.InQuint); + } + }); + + UsesFixedAnchor = true; + } + + private IBindable direction = null!; + + protected override void LoadComplete() + { + base.LoadComplete(); + text.Alpha = Current.Value > 0 ? 1 : 0; + + direction = maniaRuleset.ScrollingInfo.Direction.GetBoundCopy(); + direction.BindValueChanged(_ => updateAnchor()); + + // two schedules are required so that updateAnchor is executed in the next frame, + // which is when the combo counter receives its Y position by the default layout in ArgonManiaSkinTransformer. + Schedule(() => Schedule(updateAnchor)); + } + + private void updateAnchor() + { + Anchor &= ~(Anchor.y0 | Anchor.y2); + Anchor |= direction.Value == ScrollingDirection.Up ? Anchor.y2 : Anchor.y0; + + // since we flip the vertical anchor when changing scroll direction, + // we can use the sign of the Y value as an indicator to make the combo counter displayed correctly. + if ((Y < 0 && direction.Value == ScrollingDirection.Down) || (Y > 0 && direction.Value == ScrollingDirection.Up)) + Y = -Y; + } + + protected override IHasText CreateText() => text = new OsuSpriteText + { + Font = OsuFont.Torus.With(size: 32, fixedWidth: true), + }; + } +} diff --git a/osu.Game.Rulesets.Mania/Skinning/Argon/ManiaArgonSkinTransformer.cs b/osu.Game.Rulesets.Mania/Skinning/Argon/ManiaArgonSkinTransformer.cs index 7f6540e7b5..b0a6086f2a 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Argon/ManiaArgonSkinTransformer.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Argon/ManiaArgonSkinTransformer.cs @@ -2,8 +2,11 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Diagnostics; +using System.Linq; using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Scoring; @@ -12,7 +15,7 @@ using osuTK.Graphics; namespace osu.Game.Rulesets.Mania.Skinning.Argon { - public class ManiaArgonSkinTransformer : SkinTransformer + public class ManiaArgonSkinTransformer : ArgonSkinTransformer { private readonly ManiaBeatmap beatmap; @@ -26,6 +29,34 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon { switch (lookup) { + case SkinComponentsContainerLookup containerLookup: + switch (containerLookup.Target) + { + case SkinComponentsContainerLookup.TargetArea.MainHUDComponents when containerLookup.Ruleset != null: + Debug.Assert(containerLookup.Ruleset.ShortName == ManiaRuleset.SHORT_NAME); + + var rulesetHUDComponents = Skin.GetDrawableComponent(lookup); + + rulesetHUDComponents ??= new DefaultSkinComponentsContainer(container => + { + var combo = container.ChildrenOfType().FirstOrDefault(); + + if (combo != null) + { + combo.Anchor = Anchor.TopCentre; + combo.Origin = Anchor.Centre; + combo.Y = 200; + } + }) + { + new ArgonManiaComboCounter(), + }; + + return rulesetHUDComponents; + } + + break; + case GameplaySkinComponentLookup resultComponent: // This should eventually be moved to a skin setting, when supported. if (Skin is ArgonProSkin && resultComponent.Component >= HitResult.Great) From 95961dc98955aed19f1e5fd482ba119be868ec0b Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 30 Dec 2023 05:21:10 +0300 Subject: [PATCH 0134/2493] Add various visual test coverage --- .../Skinning/TestSceneComboCounter.cs | 38 +++++++++++++++++++ .../Skinning/TestScenePlayfield.cs | 13 +++++++ .../TestSceneManiaPlayerLegacySkin.cs | 36 ++++++++++++++++++ 3 files changed, 87 insertions(+) create mode 100644 osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneComboCounter.cs create mode 100644 osu.Game.Rulesets.Mania.Tests/TestSceneManiaPlayerLegacySkin.cs diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneComboCounter.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneComboCounter.cs new file mode 100644 index 0000000000..c1e1cfd7af --- /dev/null +++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneComboCounter.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 osu.Framework.Allocation; +using osu.Framework.Testing; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Mania.Skinning.Argon; +using osu.Game.Rulesets.Mania.Skinning.Legacy; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Scoring; +using osu.Game.Skinning; + +namespace osu.Game.Rulesets.Mania.Tests.Skinning +{ + public partial class TestSceneComboCounter : ManiaSkinnableTestScene + { + [Cached] + private ScoreProcessor scoreProcessor = new ScoreProcessor(new ManiaRuleset()); + + [SetUpSteps] + public void SetUpSteps() + { + AddStep("setup", () => SetContents(s => + { + if (s is ArgonSkin) + return new ArgonManiaComboCounter(); + + if (s is LegacySkin) + return new LegacyManiaComboCounter(); + + return new LegacyManiaComboCounter(); + })); + + AddRepeatStep("perform hit", () => scoreProcessor.ApplyResult(new JudgementResult(new HitObject(), new Judgement()) { Type = HitResult.Great }), 20); + AddStep("perform miss", () => scoreProcessor.ApplyResult(new JudgementResult(new HitObject(), new Judgement()) { Type = HitResult.Miss })); + } + } +} diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestScenePlayfield.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestScenePlayfield.cs index 29c47ca93a..110336d823 100644 --- a/osu.Game.Rulesets.Mania.Tests/Skinning/TestScenePlayfield.cs +++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestScenePlayfield.cs @@ -3,15 +3,22 @@ using System.Collections.Generic; using NUnit.Framework; +using osu.Framework.Allocation; using osu.Game.Beatmaps; +using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.UI; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Scoring; using osuTK; namespace osu.Game.Rulesets.Mania.Tests.Skinning { public partial class TestScenePlayfield : ManiaSkinnableTestScene { + [Cached] + private ScoreProcessor scoreProcessor = new ScoreProcessor(new ManiaRuleset()); + private List stageDefinitions = new List(); [Test] @@ -29,6 +36,9 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning Child = new ManiaPlayfield(stageDefinitions) }); }); + + AddRepeatStep("perform hit", () => scoreProcessor.ApplyResult(new JudgementResult(new HitObject(), new Judgement()) { Type = HitResult.Perfect }), 20); + AddStep("perform miss", () => scoreProcessor.ApplyResult(new JudgementResult(new HitObject(), new Judgement()) { Type = HitResult.Miss })); } [TestCase(2)] @@ -54,6 +64,9 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning } }); }); + + AddRepeatStep("perform hit", () => scoreProcessor.ApplyResult(new JudgementResult(new HitObject(), new Judgement()) { Type = HitResult.Perfect }), 20); + AddStep("perform miss", () => scoreProcessor.ApplyResult(new JudgementResult(new HitObject(), new Judgement()) { Type = HitResult.Miss })); } protected override IBeatmap CreateBeatmapForSkinProvider() diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneManiaPlayerLegacySkin.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneManiaPlayerLegacySkin.cs new file mode 100644 index 0000000000..0f10f96dbf --- /dev/null +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneManiaPlayerLegacySkin.cs @@ -0,0 +1,36 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Mania.Mods; +using osu.Game.Rulesets.Mods; +using osu.Game.Tests.Beatmaps; +using osu.Game.Tests.Visual; + +namespace osu.Game.Rulesets.Mania.Tests +{ + public partial class TestSceneManiaPlayerLegacySkin : LegacySkinPlayerTestScene + { + protected override Ruleset CreatePlayerRuleset() => new ManiaRuleset(); + + // play with a converted beatmap to allow dual stages mod to work. + protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(new RulesetInfo()); + + protected override bool HasCustomSteps => true; + + [Test] + public void TestSingleStage() + { + AddStep("Load single stage", LoadPlayer); + AddUntilStep("player loaded", () => Player.IsLoaded && Player.Alpha == 1); + } + + [Test] + public void TestDualStage() + { + AddStep("Load dual stage", () => LoadPlayer(new Mod[] { new ManiaModDualStages() })); + AddUntilStep("player loaded", () => Player.IsLoaded && Player.Alpha == 1); + } + } +} From 01219fa3712f46ea55c0df150c2c3be2c0a31a48 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 30 Dec 2023 05:16:41 +0300 Subject: [PATCH 0135/2493] Disable "closest" anchor in mania combo counter for convenience --- osu.Game/Overlays/SkinEditor/SkinEditor.cs | 3 +++ osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs | 3 ++- osu.Game/Skinning/ISerialisableDrawable.cs | 8 ++++++++ 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/SkinEditor/SkinEditor.cs b/osu.Game/Overlays/SkinEditor/SkinEditor.cs index f972186333..cc1e5b26ec 100644 --- a/osu.Game/Overlays/SkinEditor/SkinEditor.cs +++ b/osu.Game/Overlays/SkinEditor/SkinEditor.cs @@ -441,6 +441,9 @@ namespace osu.Game.Overlays.SkinEditor drawableComponent.Origin = Anchor.TopCentre; drawableComponent.Anchor = Anchor.TopCentre; drawableComponent.Y = targetContainer.DrawSize.Y / 2; + + if (!component.SupportsClosestAnchor) + component.UsesFixedAnchor = true; } try diff --git a/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs b/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs index cf6fb60636..208bd71005 100644 --- a/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs +++ b/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs @@ -233,7 +233,8 @@ namespace osu.Game.Overlays.SkinEditor { var closestItem = new TernaryStateRadioMenuItem("Closest", MenuItemType.Standard, _ => applyClosestAnchors()) { - State = { Value = GetStateFromSelection(selection, c => !c.Item.UsesFixedAnchor) } + State = { Value = GetStateFromSelection(selection, c => !c.Item.UsesFixedAnchor), }, + Action = { Disabled = selection.Any(c => !c.Item.SupportsClosestAnchor) }, }; yield return new OsuMenuItem("Anchor") diff --git a/osu.Game/Skinning/ISerialisableDrawable.cs b/osu.Game/Skinning/ISerialisableDrawable.cs index c9dcaca6d1..898186bcc1 100644 --- a/osu.Game/Skinning/ISerialisableDrawable.cs +++ b/osu.Game/Skinning/ISerialisableDrawable.cs @@ -27,6 +27,14 @@ namespace osu.Game.Skinning /// bool IsEditable => true; + /// + /// Whether this component supports the "closest" anchor. + /// + /// + /// This is disabled by some components that shift position automatically. + /// + bool SupportsClosestAnchor => true; + /// /// In the context of the skin layout editor, whether this has a permanent anchor defined. /// If , this 's is automatically determined by proximity, From 02f5ea200ea7b464d6d78170dc7b7beeb3e61559 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 30 Dec 2023 07:41:55 +0300 Subject: [PATCH 0136/2493] Fix failing tests --- .../Skinning/Argon/ArgonManiaComboCounter.cs | 13 +++++------ .../Legacy/LegacyManiaComboCounter.cs | 22 ++++++++++--------- osu.Game/Screens/Play/Player.cs | 4 ++++ 3 files changed, 21 insertions(+), 18 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonManiaComboCounter.cs b/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonManiaComboCounter.cs index 1c8e43345a..ad515528fb 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonManiaComboCounter.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonManiaComboCounter.cs @@ -7,9 +7,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; -using osu.Game.Rulesets.Mania.UI; using osu.Game.Rulesets.Scoring; -using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Screens.Play.HUD; using osu.Game.Skinning; @@ -24,15 +22,11 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon protected override double RollingDuration => 500; protected override Easing RollingEasing => Easing.OutQuint; - private DrawableManiaRuleset maniaRuleset = null!; - bool ISerialisableDrawable.SupportsClosestAnchor => false; [BackgroundDependencyLoader] - private void load(DrawableRuleset ruleset, ScoreProcessor scoreProcessor) + private void load(ScoreProcessor scoreProcessor) { - maniaRuleset = (DrawableManiaRuleset)ruleset; - Current.BindTo(scoreProcessor.Combo); Current.BindValueChanged(combo => { @@ -50,6 +44,9 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon UsesFixedAnchor = true; } + [Resolved] + private IScrollingInfo scrollingInfo { get; set; } = null!; + private IBindable direction = null!; protected override void LoadComplete() @@ -57,7 +54,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon base.LoadComplete(); text.Alpha = Current.Value > 0 ? 1 : 0; - direction = maniaRuleset.ScrollingInfo.Direction.GetBoundCopy(); + direction = scrollingInfo.Direction.GetBoundCopy(); direction.BindValueChanged(_ => updateAnchor()); // two schedules are required so that updateAnchor is executed in the next frame, diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyManiaComboCounter.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyManiaComboCounter.cs index fd309f6250..00619834c8 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyManiaComboCounter.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyManiaComboCounter.cs @@ -3,9 +3,8 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Graphics; -using osu.Game.Rulesets.Mania.UI; -using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Skinning; using osuTK; @@ -15,15 +14,11 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy { public partial class LegacyManiaComboCounter : LegacyComboCounter, ISerialisableDrawable { - private DrawableManiaRuleset maniaRuleset = null!; - bool ISerialisableDrawable.SupportsClosestAnchor => false; [BackgroundDependencyLoader] - private void load(ISkinSource skin, DrawableRuleset ruleset) + private void load(ISkinSource skin) { - maniaRuleset = (DrawableManiaRuleset)ruleset; - DisplayedCountText.Anchor = Anchor.Centre; DisplayedCountText.Origin = Anchor.Centre; @@ -34,13 +29,16 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy UsesFixedAnchor = true; } + [Resolved] + private IScrollingInfo scrollingInfo { get; set; } = null!; + private IBindable direction = null!; protected override void LoadComplete() { base.LoadComplete(); - direction = maniaRuleset.ScrollingInfo.Direction.GetBoundCopy(); + direction = scrollingInfo.Direction.GetBoundCopy(); direction.BindValueChanged(_ => updateAnchor()); // two schedules are required so that updateAnchor is executed in the next frame, @@ -50,8 +48,12 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy private void updateAnchor() { - Anchor &= ~(Anchor.y0 | Anchor.y2); - Anchor |= direction.Value == ScrollingDirection.Up ? Anchor.y2 : Anchor.y0; + // if the anchor isn't a vertical center, set top or bottom anchor based on scroll direction + if (!Anchor.HasFlagFast(Anchor.y1)) + { + Anchor &= ~(Anchor.y0 | Anchor.y2); + Anchor |= direction.Value == ScrollingDirection.Up ? Anchor.y2 : Anchor.y0; + } // since we flip the vertical anchor when changing scroll direction, // we can use the sign of the Y value as an indicator to make the combo counter displayed correctly. diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index c960ac357f..c10ef9731f 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -32,6 +32,7 @@ using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI; +using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Scoring; using osu.Game.Scoring.Legacy; using osu.Game.Screens.Play.HUD; @@ -224,6 +225,9 @@ namespace osu.Game.Screens.Play DrawableRuleset = ruleset.CreateDrawableRulesetWith(playableBeatmap, gameplayMods); dependencies.CacheAs(DrawableRuleset); + if (DrawableRuleset is IDrawableScrollingRuleset scrollingRuleset) + dependencies.CacheAs(scrollingRuleset.ScrollingInfo); + ScoreProcessor = ruleset.CreateScoreProcessor(); ScoreProcessor.Mods.Value = gameplayMods; ScoreProcessor.ApplyBeatmap(playableBeatmap); From f3b88c318b57ee222973779359611fdc18fe6eb6 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Thu, 28 Dec 2023 17:49:14 +0100 Subject: [PATCH 0137/2493] Add rotation to snap grid visual --- .../TestSceneRectangularPositionSnapGrid.cs | 16 +- .../Components/RectangularPositionSnapGrid.cs | 140 +++++++++++++++--- 2 files changed, 125 insertions(+), 31 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneRectangularPositionSnapGrid.cs b/osu.Game.Tests/Visual/Editing/TestSceneRectangularPositionSnapGrid.cs index e73a45e154..210af09055 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneRectangularPositionSnapGrid.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneRectangularPositionSnapGrid.cs @@ -33,28 +33,30 @@ namespace osu.Game.Tests.Visual.Editing }, content = new Container { - RelativeSizeAxes = Axes.Both + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding(10), } }); } private static readonly object[][] test_cases = { - new object[] { new Vector2(0, 0), new Vector2(10, 10) }, - new object[] { new Vector2(240, 180), new Vector2(10, 15) }, - new object[] { new Vector2(160, 120), new Vector2(30, 20) }, - new object[] { new Vector2(480, 360), new Vector2(100, 100) }, + new object[] { new Vector2(0, 0), new Vector2(10, 10), 0f }, + new object[] { new Vector2(240, 180), new Vector2(10, 15), 30f }, + new object[] { new Vector2(160, 120), new Vector2(30, 20), -30f }, + new object[] { new Vector2(480, 360), new Vector2(100, 100), 0f }, }; [TestCaseSource(nameof(test_cases))] - public void TestRectangularGrid(Vector2 position, Vector2 spacing) + public void TestRectangularGrid(Vector2 position, Vector2 spacing, float rotation) { RectangularPositionSnapGrid grid = null; AddStep("create grid", () => Child = grid = new RectangularPositionSnapGrid(position) { RelativeSizeAxes = Axes.Both, - Spacing = spacing + Spacing = spacing, + GridLineRotation = rotation }); AddStep("add snapping cursor", () => Add(new SnappingCursorContainer diff --git a/osu.Game/Screens/Edit/Compose/Components/RectangularPositionSnapGrid.cs b/osu.Game/Screens/Edit/Compose/Components/RectangularPositionSnapGrid.cs index cfc01fe17b..160e7e026b 100644 --- a/osu.Game/Screens/Edit/Compose/Components/RectangularPositionSnapGrid.cs +++ b/osu.Game/Screens/Edit/Compose/Components/RectangularPositionSnapGrid.cs @@ -15,10 +15,20 @@ namespace osu.Game.Screens.Edit.Compose.Components { public partial class RectangularPositionSnapGrid : CompositeDrawable { + private Vector2 startPosition; + /// /// The position of the origin of this in local coordinates. /// - public Vector2 StartPosition { get; } + public Vector2 StartPosition + { + get => startPosition; + set + { + startPosition = value; + gridCache.Invalidate(); + } + } private Vector2 spacing = Vector2.One; @@ -38,11 +48,27 @@ namespace osu.Game.Screens.Edit.Compose.Components } } + private float gridLineRotation; + + /// + /// The rotation in degrees of the grid lines of this . + /// + public float GridLineRotation + { + get => gridLineRotation; + set + { + gridLineRotation = value; + gridCache.Invalidate(); + } + } + private readonly LayoutValue gridCache = new LayoutValue(Invalidation.RequiredParentSizeToFit); public RectangularPositionSnapGrid(Vector2 startPosition) { StartPosition = startPosition; + Masking = true; AddLayout(gridCache); } @@ -65,47 +91,43 @@ namespace osu.Game.Screens.Edit.Compose.Components private void createContent() { var drawSize = DrawSize; + var rot = Quaternion.FromAxisAngle(Vector3.UnitZ, MathHelper.DegreesToRadians(GridLineRotation)); - generateGridLines(Direction.Horizontal, StartPosition.Y, 0, -Spacing.Y); - generateGridLines(Direction.Horizontal, StartPosition.Y, drawSize.Y, Spacing.Y); + generateGridLines(Vector2.Transform(new Vector2(0, -Spacing.Y), rot), GridLineRotation + 90, drawSize); + generateGridLines(Vector2.Transform(new Vector2(0, Spacing.Y), rot), GridLineRotation + 90, drawSize); - generateGridLines(Direction.Vertical, StartPosition.X, 0, -Spacing.X); - generateGridLines(Direction.Vertical, StartPosition.X, drawSize.X, Spacing.X); + generateGridLines(Vector2.Transform(new Vector2(-Spacing.X, 0), rot), GridLineRotation, drawSize); + generateGridLines(Vector2.Transform(new Vector2(Spacing.X, 0), rot), GridLineRotation, drawSize); + + generateOutline(drawSize); } - private void generateGridLines(Direction direction, float startPosition, float endPosition, float step) + private void generateGridLines(Vector2 step, float rotation, Vector2 drawSize) { int index = 0; - float currentPosition = startPosition; + var currentPosition = startPosition; // Make lines the same width independent of display resolution. float lineWidth = DrawWidth / ScreenSpaceDrawQuad.Width; + float lineLength = drawSize.Length * 2; List generatedLines = new List(); - while (Precision.AlmostBigger((endPosition - currentPosition) * Math.Sign(step), 0)) + while (lineDefinitelyIntersectsBox(currentPosition, step.PerpendicularLeft, drawSize) || + isMovingTowardsBox(currentPosition, step, drawSize)) { var gridLine = new Box { Colour = Colour4.White, Alpha = 0.1f, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.None, + Width = lineWidth, + Height = lineLength, + Position = currentPosition, + Rotation = rotation, }; - if (direction == Direction.Horizontal) - { - gridLine.Origin = Anchor.CentreLeft; - gridLine.RelativeSizeAxes = Axes.X; - gridLine.Height = lineWidth; - gridLine.Y = currentPosition; - } - else - { - gridLine.Origin = Anchor.TopCentre; - gridLine.RelativeSizeAxes = Axes.Y; - gridLine.Width = lineWidth; - gridLine.X = currentPosition; - } - generatedLines.Add(gridLine); index += 1; @@ -116,11 +138,81 @@ namespace osu.Game.Screens.Edit.Compose.Components return; generatedLines.First().Alpha = 0.3f; - generatedLines.Last().Alpha = 0.3f; AddRangeInternal(generatedLines); } + private bool isMovingTowardsBox(Vector2 currentPosition, Vector2 step, Vector2 box) + { + return (currentPosition + step).LengthSquared < currentPosition.LengthSquared || + (currentPosition + step - box).LengthSquared < (currentPosition - box).LengthSquared; + } + + private bool lineDefinitelyIntersectsBox(Vector2 lineStart, Vector2 lineDir, Vector2 box) + { + var p2 = lineStart + lineDir; + + double d1 = det(Vector2.Zero); + double d2 = det(new Vector2(box.X, 0)); + double d3 = det(new Vector2(0, box.Y)); + double d4 = det(box); + + return definitelyDifferentSign(d1, d2) || definitelyDifferentSign(d3, d4) || + definitelyDifferentSign(d1, d3) || definitelyDifferentSign(d2, d4); + + double det(Vector2 p) => (p.X - lineStart.X) * (p2.Y - lineStart.Y) - (p.Y - lineStart.Y) * (p2.X - lineStart.X); + + bool definitelyDifferentSign(double a, double b) => !Precision.AlmostEquals(a, 0) && + !Precision.AlmostEquals(b, 0) && + Math.Sign(a) != Math.Sign(b); + } + + private void generateOutline(Vector2 drawSize) + { + // Make lines the same width independent of display resolution. + float lineWidth = DrawWidth / ScreenSpaceDrawQuad.Width; + + AddRangeInternal(new[] + { + new Box + { + Colour = Colour4.White, + Alpha = 0.3f, + Origin = Anchor.CentreLeft, + RelativeSizeAxes = Axes.X, + Height = lineWidth, + Y = 0, + }, + new Box + { + Colour = Colour4.White, + Alpha = 0.3f, + Origin = Anchor.CentreLeft, + RelativeSizeAxes = Axes.X, + Height = lineWidth, + Y = drawSize.Y, + }, + new Box + { + Colour = Colour4.White, + Alpha = 0.3f, + Origin = Anchor.TopCentre, + RelativeSizeAxes = Axes.Y, + Width = lineWidth, + X = 0, + }, + new Box + { + Colour = Colour4.White, + Alpha = 0.3f, + Origin = Anchor.TopCentre, + RelativeSizeAxes = Axes.Y, + Width = lineWidth, + X = drawSize.X, + }, + }); + } + public Vector2 GetSnappedPosition(Vector2 original) { Vector2 relativeToStart = original - StartPosition; From f2edd705ea537774a662ca6121092b137bb2bb8e Mon Sep 17 00:00:00 2001 From: OliBomby Date: Thu, 28 Dec 2023 18:56:49 +0100 Subject: [PATCH 0138/2493] add rotation to snapped position --- .../Components/RectangularPositionSnapGrid.cs | 5 +++-- osu.Game/Utils/GeometryUtils.cs | 18 +++++++++++++++--- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/RectangularPositionSnapGrid.cs b/osu.Game/Screens/Edit/Compose/Components/RectangularPositionSnapGrid.cs index 160e7e026b..ea9eaf41bb 100644 --- a/osu.Game/Screens/Edit/Compose/Components/RectangularPositionSnapGrid.cs +++ b/osu.Game/Screens/Edit/Compose/Components/RectangularPositionSnapGrid.cs @@ -9,6 +9,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Layout; using osu.Framework.Utils; +using osu.Game.Utils; using osuTK; namespace osu.Game.Screens.Edit.Compose.Components @@ -215,11 +216,11 @@ namespace osu.Game.Screens.Edit.Compose.Components public Vector2 GetSnappedPosition(Vector2 original) { - Vector2 relativeToStart = original - StartPosition; + Vector2 relativeToStart = GeometryUtils.RotateVector(original - StartPosition, GridLineRotation); Vector2 offset = Vector2.Divide(relativeToStart, Spacing); Vector2 roundedOffset = new Vector2(MathF.Round(offset.X), MathF.Round(offset.Y)); - return StartPosition + Vector2.Multiply(roundedOffset, Spacing); + return StartPosition + GeometryUtils.RotateVector(Vector2.Multiply(roundedOffset, Spacing), -GridLineRotation); } } } diff --git a/osu.Game/Utils/GeometryUtils.cs b/osu.Game/Utils/GeometryUtils.cs index 725e93d098..fcc6b8ae2a 100644 --- a/osu.Game/Utils/GeometryUtils.cs +++ b/osu.Game/Utils/GeometryUtils.cs @@ -27,9 +27,8 @@ namespace osu.Game.Utils point.X -= origin.X; point.Y -= origin.Y; - Vector2 ret; - ret.X = point.X * MathF.Cos(MathUtils.DegreesToRadians(angle)) + point.Y * MathF.Sin(MathUtils.DegreesToRadians(angle)); - ret.Y = point.X * -MathF.Sin(MathUtils.DegreesToRadians(angle)) + point.Y * MathF.Cos(MathUtils.DegreesToRadians(angle)); + Vector2 ret = RotateVector(point, angle); + Matrix2 ret.X += origin.X; ret.Y += origin.Y; @@ -37,6 +36,19 @@ namespace osu.Game.Utils return ret; } + /// + /// Rotate a vector around the origin. + /// + /// The vector. + /// The angle to rotate (in degrees). + public static Vector2 RotateVector(Vector2 vector, float angle) + { + return new Vector2( + vector.X * MathF.Cos(MathUtils.DegreesToRadians(angle)) + vector.Y * MathF.Sin(MathUtils.DegreesToRadians(angle)), + vector.X * -MathF.Sin(MathUtils.DegreesToRadians(angle)) + vector.Y * MathF.Cos(MathUtils.DegreesToRadians(angle)) + ); + } + /// /// Given a flip direction, a surrounding quad for all selected objects, and a position, /// will return the flipped position in screen space coordinates. From 2193601f3a70150d2e777939b3e75c9a6dd248db Mon Sep 17 00:00:00 2001 From: OliBomby Date: Thu, 28 Dec 2023 20:00:24 +0100 Subject: [PATCH 0139/2493] fix typo --- osu.Game/Utils/GeometryUtils.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Utils/GeometryUtils.cs b/osu.Game/Utils/GeometryUtils.cs index fcc6b8ae2a..e0d217dd48 100644 --- a/osu.Game/Utils/GeometryUtils.cs +++ b/osu.Game/Utils/GeometryUtils.cs @@ -28,7 +28,6 @@ namespace osu.Game.Utils point.Y -= origin.Y; Vector2 ret = RotateVector(point, angle); - Matrix2 ret.X += origin.X; ret.Y += origin.Y; From 92c3b142a4941b14c83c479079adec3d6d6a9be5 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Thu, 28 Dec 2023 20:52:11 +0100 Subject: [PATCH 0140/2493] Added Triangular snap grid --- .../TestSceneTriangularPositionSnapGrid.cs | 108 ++++++++ .../Components/TriangularPositionSnapGrid.cs | 258 ++++++++++++++++++ 2 files changed, 366 insertions(+) create mode 100644 osu.Game.Tests/Visual/Editing/TestSceneTriangularPositionSnapGrid.cs create mode 100644 osu.Game/Screens/Edit/Compose/Components/TriangularPositionSnapGrid.cs diff --git a/osu.Game.Tests/Visual/Editing/TestSceneTriangularPositionSnapGrid.cs b/osu.Game.Tests/Visual/Editing/TestSceneTriangularPositionSnapGrid.cs new file mode 100644 index 0000000000..2f5ffd8423 --- /dev/null +++ b/osu.Game.Tests/Visual/Editing/TestSceneTriangularPositionSnapGrid.cs @@ -0,0 +1,108 @@ +// 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 NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Input.Events; +using osu.Game.Screens.Edit.Compose.Components; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Tests.Visual.Editing +{ + public partial class TestSceneTriangularPositionSnapGrid : OsuManualInputManagerTestScene + { + private Container content; + protected override Container Content => content; + + [BackgroundDependencyLoader] + private void load() + { + base.Content.AddRange(new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Colour4.Gray + }, + content = new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding(10), + } + }); + } + + private static readonly object[][] test_cases = + { + new object[] { new Vector2(0, 0), 10, 0f }, + new object[] { new Vector2(240, 180), 10, 10f }, + new object[] { new Vector2(160, 120), 30, -10f }, + new object[] { new Vector2(480, 360), 100, 0f }, + }; + + [TestCaseSource(nameof(test_cases))] + public void TestTriangularGrid(Vector2 position, float spacing, float rotation) + { + TriangularPositionSnapGrid grid = null; + + AddStep("create grid", () => Child = grid = new TriangularPositionSnapGrid(position) + { + RelativeSizeAxes = Axes.Both, + Spacing = spacing, + GridLineRotation = rotation + }); + + AddStep("add snapping cursor", () => Add(new SnappingCursorContainer + { + RelativeSizeAxes = Axes.Both, + GetSnapPosition = pos => grid.GetSnappedPosition(grid.ToLocalSpace(pos)) + })); + } + + private partial class SnappingCursorContainer : CompositeDrawable + { + public Func GetSnapPosition; + + private readonly Drawable cursor; + + public SnappingCursorContainer() + { + RelativeSizeAxes = Axes.Both; + + InternalChild = cursor = new Circle + { + Origin = Anchor.Centre, + Size = new Vector2(50), + Colour = Color4.Red + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + updatePosition(GetContainingInputManager().CurrentState.Mouse.Position); + } + + protected override bool OnMouseMove(MouseMoveEvent e) + { + base.OnMouseMove(e); + + updatePosition(e.ScreenSpaceMousePosition); + return true; + } + + private void updatePosition(Vector2 screenSpacePosition) + { + cursor.Position = GetSnapPosition.Invoke(screenSpacePosition); + } + } + } +} diff --git a/osu.Game/Screens/Edit/Compose/Components/TriangularPositionSnapGrid.cs b/osu.Game/Screens/Edit/Compose/Components/TriangularPositionSnapGrid.cs new file mode 100644 index 0000000000..58889bd085 --- /dev/null +++ b/osu.Game/Screens/Edit/Compose/Components/TriangularPositionSnapGrid.cs @@ -0,0 +1,258 @@ +// 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.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Layout; +using osu.Framework.Utils; +using osu.Game.Utils; +using osuTK; + +namespace osu.Game.Screens.Edit.Compose.Components +{ + public partial class TriangularPositionSnapGrid : CompositeDrawable + { + private Vector2 startPosition; + + /// + /// The position of the origin of this in local coordinates. + /// + public Vector2 StartPosition + { + get => startPosition; + set + { + startPosition = value; + gridCache.Invalidate(); + } + } + + private float spacing = 1; + + /// + /// The spacing between grid lines of this . + /// + public float Spacing + { + get => spacing; + set + { + if (spacing <= 0) + throw new ArgumentException("Grid spacing must be positive."); + + spacing = value; + gridCache.Invalidate(); + } + } + + private float gridLineRotation; + + /// + /// The rotation in degrees of the grid lines of this . + /// + public float GridLineRotation + { + get => gridLineRotation; + set + { + gridLineRotation = value; + gridCache.Invalidate(); + } + } + + private readonly LayoutValue gridCache = new LayoutValue(Invalidation.RequiredParentSizeToFit); + + public TriangularPositionSnapGrid(Vector2 startPosition) + { + StartPosition = startPosition; + Masking = true; + + AddLayout(gridCache); + } + + protected override void Update() + { + base.Update(); + + if (!gridCache.IsValid) + { + ClearInternal(); + + if (DrawWidth > 0 && DrawHeight > 0) + createContent(); + + gridCache.Validate(); + } + } + + private const float sqrt3 = 1.73205080757f; + private const float sqrt3_over2 = 0.86602540378f; + private const float one_over_sqrt3 = 0.57735026919f; + + private void createContent() + { + var drawSize = DrawSize; + float stepSpacing = Spacing * sqrt3_over2; + var step1 = GeometryUtils.RotateVector(new Vector2(stepSpacing, 0), -GridLineRotation - 30); + var step2 = GeometryUtils.RotateVector(new Vector2(stepSpacing, 0), -GridLineRotation - 90); + var step3 = GeometryUtils.RotateVector(new Vector2(stepSpacing, 0), -GridLineRotation - 150); + + generateGridLines(step1, drawSize); + generateGridLines(-step1, drawSize); + + generateGridLines(step2, drawSize); + generateGridLines(-step2, drawSize); + + generateGridLines(step3, drawSize); + generateGridLines(-step3, drawSize); + + generateOutline(drawSize); + } + + private void generateGridLines(Vector2 step, Vector2 drawSize) + { + int index = 0; + var currentPosition = startPosition; + + // Make lines the same width independent of display resolution. + float lineWidth = DrawWidth / ScreenSpaceDrawQuad.Width; + float lineLength = drawSize.Length * 2; + + List generatedLines = new List(); + + while (lineDefinitelyIntersectsBox(currentPosition, step.PerpendicularLeft, drawSize) || + isMovingTowardsBox(currentPosition, step, drawSize)) + { + var gridLine = new Box + { + Colour = Colour4.White, + Alpha = 0.1f, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.None, + Width = lineWidth, + Height = lineLength, + Position = currentPosition, + Rotation = MathHelper.RadiansToDegrees(MathF.Atan2(step.Y, step.X)), + }; + + generatedLines.Add(gridLine); + + index += 1; + currentPosition = startPosition + index * step; + } + + if (generatedLines.Count == 0) + return; + + generatedLines.First().Alpha = 0.3f; + + AddRangeInternal(generatedLines); + } + + private bool isMovingTowardsBox(Vector2 currentPosition, Vector2 step, Vector2 box) + { + return (currentPosition + step).LengthSquared < currentPosition.LengthSquared || + (currentPosition + step - box).LengthSquared < (currentPosition - box).LengthSquared; + } + + private bool lineDefinitelyIntersectsBox(Vector2 lineStart, Vector2 lineDir, Vector2 box) + { + var p2 = lineStart + lineDir; + + double d1 = det(Vector2.Zero); + double d2 = det(new Vector2(box.X, 0)); + double d3 = det(new Vector2(0, box.Y)); + double d4 = det(box); + + return definitelyDifferentSign(d1, d2) || definitelyDifferentSign(d3, d4) || + definitelyDifferentSign(d1, d3) || definitelyDifferentSign(d2, d4); + + double det(Vector2 p) => (p.X - lineStart.X) * (p2.Y - lineStart.Y) - (p.Y - lineStart.Y) * (p2.X - lineStart.X); + + bool definitelyDifferentSign(double a, double b) => !Precision.AlmostEquals(a, 0) && + !Precision.AlmostEquals(b, 0) && + Math.Sign(a) != Math.Sign(b); + } + + private void generateOutline(Vector2 drawSize) + { + // Make lines the same width independent of display resolution. + float lineWidth = DrawWidth / ScreenSpaceDrawQuad.Width; + + AddRangeInternal(new[] + { + new Box + { + Colour = Colour4.White, + Alpha = 0.3f, + Origin = Anchor.CentreLeft, + RelativeSizeAxes = Axes.X, + Height = lineWidth, + Y = 0, + }, + new Box + { + Colour = Colour4.White, + Alpha = 0.3f, + Origin = Anchor.CentreLeft, + RelativeSizeAxes = Axes.X, + Height = lineWidth, + Y = drawSize.Y, + }, + new Box + { + Colour = Colour4.White, + Alpha = 0.3f, + Origin = Anchor.TopCentre, + RelativeSizeAxes = Axes.Y, + Width = lineWidth, + X = 0, + }, + new Box + { + Colour = Colour4.White, + Alpha = 0.3f, + Origin = Anchor.TopCentre, + RelativeSizeAxes = Axes.Y, + Width = lineWidth, + X = drawSize.X, + }, + }); + } + + public Vector2 GetSnappedPosition(Vector2 original) + { + Vector2 relativeToStart = GeometryUtils.RotateVector(original - StartPosition, GridLineRotation); + Vector2 hex = pixelToHex(relativeToStart); + + return StartPosition + GeometryUtils.RotateVector(hexToPixel(hex), -GridLineRotation); + } + + private Vector2 pixelToHex(Vector2 pixel) + { + float x = pixel.X / Spacing; + float y = pixel.Y / Spacing; + // Algorithm from Charles Chambers + // with modifications and comments by Chris Cox 2023 + // + float t = sqrt3 * y + 1; // scaled y, plus phase + float temp1 = MathF.Floor(t + x); // (y+x) diagonal, this calc needs floor + float temp2 = t - x; // (y-x) diagonal, no floor needed + float temp3 = 2 * x + 1; // scaled horizontal, no floor needed, needs +1 to get correct phase + float qf = (temp1 + temp3) / 3.0f; // pseudo x with fraction + float rf = (temp1 + temp2) / 3.0f; // pseudo y with fraction + float q = MathF.Floor(qf); // pseudo x, quantized and thus requires floor + float r = MathF.Floor(rf); // pseudo y, quantized and thus requires floor + return new Vector2(q, r); + } + + private Vector2 hexToPixel(Vector2 hex) + { + return new Vector2(Spacing * (hex.X - hex.Y / 2), Spacing * one_over_sqrt3 * 1.5f * hex.Y); + } + } +} From d0c8b285cefc29025407597467357cd2940cb862 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Thu, 28 Dec 2023 21:00:47 +0100 Subject: [PATCH 0141/2493] clean up code duplication --- .../Components/LinedPositionSnapGrid.cs | 173 +++++++++++++++++ .../Components/RectangularPositionSnapGrid.cs | 167 +--------------- .../Components/TriangularPositionSnapGrid.cs | 179 ++---------------- 3 files changed, 195 insertions(+), 324 deletions(-) create mode 100644 osu.Game/Screens/Edit/Compose/Components/LinedPositionSnapGrid.cs diff --git a/osu.Game/Screens/Edit/Compose/Components/LinedPositionSnapGrid.cs b/osu.Game/Screens/Edit/Compose/Components/LinedPositionSnapGrid.cs new file mode 100644 index 0000000000..642a125265 --- /dev/null +++ b/osu.Game/Screens/Edit/Compose/Components/LinedPositionSnapGrid.cs @@ -0,0 +1,173 @@ +// 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.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Layout; +using osu.Framework.Utils; +using osuTK; + +namespace osu.Game.Screens.Edit.Compose.Components +{ + public abstract partial class LinedPositionSnapGrid : CompositeDrawable + { + private Vector2 startPosition; + + /// + /// The position of the origin of this in local coordinates. + /// + public Vector2 StartPosition + { + get => startPosition; + set + { + startPosition = value; + GridCache.Invalidate(); + } + } + + protected readonly LayoutValue GridCache = new LayoutValue(Invalidation.RequiredParentSizeToFit); + + protected LinedPositionSnapGrid(Vector2 startPosition) + { + StartPosition = startPosition; + Masking = true; + + AddLayout(GridCache); + } + + protected override void Update() + { + base.Update(); + + if (!GridCache.IsValid) + { + ClearInternal(); + + if (DrawWidth > 0 && DrawHeight > 0) + CreateContent(); + + GridCache.Validate(); + } + } + + protected abstract void CreateContent(); + + protected void GenerateGridLines(Vector2 step, Vector2 drawSize) + { + int index = 0; + var currentPosition = startPosition; + + // Make lines the same width independent of display resolution. + float lineWidth = DrawWidth / ScreenSpaceDrawQuad.Width; + float lineLength = drawSize.Length * 2; + + List generatedLines = new List(); + + while (lineDefinitelyIntersectsBox(currentPosition, step.PerpendicularLeft, drawSize) || + isMovingTowardsBox(currentPosition, step, drawSize)) + { + var gridLine = new Box + { + Colour = Colour4.White, + Alpha = 0.1f, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.None, + Width = lineWidth, + Height = lineLength, + Position = currentPosition, + Rotation = MathHelper.RadiansToDegrees(MathF.Atan2(step.Y, step.X)), + }; + + generatedLines.Add(gridLine); + + index += 1; + currentPosition = startPosition + index * step; + } + + if (generatedLines.Count == 0) + return; + + generatedLines.First().Alpha = 0.3f; + + AddRangeInternal(generatedLines); + } + + private bool isMovingTowardsBox(Vector2 currentPosition, Vector2 step, Vector2 box) + { + return (currentPosition + step).LengthSquared < currentPosition.LengthSquared || + (currentPosition + step - box).LengthSquared < (currentPosition - box).LengthSquared; + } + + private bool lineDefinitelyIntersectsBox(Vector2 lineStart, Vector2 lineDir, Vector2 box) + { + var p2 = lineStart + lineDir; + + double d1 = det(Vector2.Zero); + double d2 = det(new Vector2(box.X, 0)); + double d3 = det(new Vector2(0, box.Y)); + double d4 = det(box); + + return definitelyDifferentSign(d1, d2) || definitelyDifferentSign(d3, d4) || + definitelyDifferentSign(d1, d3) || definitelyDifferentSign(d2, d4); + + double det(Vector2 p) => (p.X - lineStart.X) * (p2.Y - lineStart.Y) - (p.Y - lineStart.Y) * (p2.X - lineStart.X); + + bool definitelyDifferentSign(double a, double b) => !Precision.AlmostEquals(a, 0) && + !Precision.AlmostEquals(b, 0) && + Math.Sign(a) != Math.Sign(b); + } + + protected void GenerateOutline(Vector2 drawSize) + { + // Make lines the same width independent of display resolution. + float lineWidth = DrawWidth / ScreenSpaceDrawQuad.Width; + + AddRangeInternal(new[] + { + new Box + { + Colour = Colour4.White, + Alpha = 0.3f, + Origin = Anchor.CentreLeft, + RelativeSizeAxes = Axes.X, + Height = lineWidth, + Y = 0, + }, + new Box + { + Colour = Colour4.White, + Alpha = 0.3f, + Origin = Anchor.CentreLeft, + RelativeSizeAxes = Axes.X, + Height = lineWidth, + Y = drawSize.Y, + }, + new Box + { + Colour = Colour4.White, + Alpha = 0.3f, + Origin = Anchor.TopCentre, + RelativeSizeAxes = Axes.Y, + Width = lineWidth, + X = 0, + }, + new Box + { + Colour = Colour4.White, + Alpha = 0.3f, + Origin = Anchor.TopCentre, + RelativeSizeAxes = Axes.Y, + Width = lineWidth, + X = drawSize.X, + }, + }); + } + + public abstract Vector2 GetSnappedPosition(Vector2 original); + } +} diff --git a/osu.Game/Screens/Edit/Compose/Components/RectangularPositionSnapGrid.cs b/osu.Game/Screens/Edit/Compose/Components/RectangularPositionSnapGrid.cs index ea9eaf41bb..14a0e3625a 100644 --- a/osu.Game/Screens/Edit/Compose/Components/RectangularPositionSnapGrid.cs +++ b/osu.Game/Screens/Edit/Compose/Components/RectangularPositionSnapGrid.cs @@ -2,35 +2,15 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Collections.Generic; -using System.Linq; using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; using osu.Framework.Layout; -using osu.Framework.Utils; using osu.Game.Utils; using osuTK; namespace osu.Game.Screens.Edit.Compose.Components { - public partial class RectangularPositionSnapGrid : CompositeDrawable + public partial class RectangularPositionSnapGrid : LinedPositionSnapGrid { - private Vector2 startPosition; - - /// - /// The position of the origin of this in local coordinates. - /// - public Vector2 StartPosition - { - get => startPosition; - set - { - startPosition = value; - gridCache.Invalidate(); - } - } - private Vector2 spacing = Vector2.One; /// @@ -67,154 +47,25 @@ namespace osu.Game.Screens.Edit.Compose.Components private readonly LayoutValue gridCache = new LayoutValue(Invalidation.RequiredParentSizeToFit); public RectangularPositionSnapGrid(Vector2 startPosition) + : base(startPosition) { - StartPosition = startPosition; - Masking = true; - - AddLayout(gridCache); } - protected override void Update() - { - base.Update(); - - if (!gridCache.IsValid) - { - ClearInternal(); - - if (DrawWidth > 0 && DrawHeight > 0) - createContent(); - - gridCache.Validate(); - } - } - - private void createContent() + protected override void CreateContent() { var drawSize = DrawSize; var rot = Quaternion.FromAxisAngle(Vector3.UnitZ, MathHelper.DegreesToRadians(GridLineRotation)); - generateGridLines(Vector2.Transform(new Vector2(0, -Spacing.Y), rot), GridLineRotation + 90, drawSize); - generateGridLines(Vector2.Transform(new Vector2(0, Spacing.Y), rot), GridLineRotation + 90, drawSize); + GenerateGridLines(Vector2.Transform(new Vector2(0, -Spacing.Y), rot), drawSize); + GenerateGridLines(Vector2.Transform(new Vector2(0, Spacing.Y), rot), drawSize); - generateGridLines(Vector2.Transform(new Vector2(-Spacing.X, 0), rot), GridLineRotation, drawSize); - generateGridLines(Vector2.Transform(new Vector2(Spacing.X, 0), rot), GridLineRotation, drawSize); + GenerateGridLines(Vector2.Transform(new Vector2(-Spacing.X, 0), rot), drawSize); + GenerateGridLines(Vector2.Transform(new Vector2(Spacing.X, 0), rot), drawSize); - generateOutline(drawSize); + GenerateOutline(drawSize); } - private void generateGridLines(Vector2 step, float rotation, Vector2 drawSize) - { - int index = 0; - var currentPosition = startPosition; - - // Make lines the same width independent of display resolution. - float lineWidth = DrawWidth / ScreenSpaceDrawQuad.Width; - float lineLength = drawSize.Length * 2; - - List generatedLines = new List(); - - while (lineDefinitelyIntersectsBox(currentPosition, step.PerpendicularLeft, drawSize) || - isMovingTowardsBox(currentPosition, step, drawSize)) - { - var gridLine = new Box - { - Colour = Colour4.White, - Alpha = 0.1f, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.None, - Width = lineWidth, - Height = lineLength, - Position = currentPosition, - Rotation = rotation, - }; - - generatedLines.Add(gridLine); - - index += 1; - currentPosition = startPosition + index * step; - } - - if (generatedLines.Count == 0) - return; - - generatedLines.First().Alpha = 0.3f; - - AddRangeInternal(generatedLines); - } - - private bool isMovingTowardsBox(Vector2 currentPosition, Vector2 step, Vector2 box) - { - return (currentPosition + step).LengthSquared < currentPosition.LengthSquared || - (currentPosition + step - box).LengthSquared < (currentPosition - box).LengthSquared; - } - - private bool lineDefinitelyIntersectsBox(Vector2 lineStart, Vector2 lineDir, Vector2 box) - { - var p2 = lineStart + lineDir; - - double d1 = det(Vector2.Zero); - double d2 = det(new Vector2(box.X, 0)); - double d3 = det(new Vector2(0, box.Y)); - double d4 = det(box); - - return definitelyDifferentSign(d1, d2) || definitelyDifferentSign(d3, d4) || - definitelyDifferentSign(d1, d3) || definitelyDifferentSign(d2, d4); - - double det(Vector2 p) => (p.X - lineStart.X) * (p2.Y - lineStart.Y) - (p.Y - lineStart.Y) * (p2.X - lineStart.X); - - bool definitelyDifferentSign(double a, double b) => !Precision.AlmostEquals(a, 0) && - !Precision.AlmostEquals(b, 0) && - Math.Sign(a) != Math.Sign(b); - } - - private void generateOutline(Vector2 drawSize) - { - // Make lines the same width independent of display resolution. - float lineWidth = DrawWidth / ScreenSpaceDrawQuad.Width; - - AddRangeInternal(new[] - { - new Box - { - Colour = Colour4.White, - Alpha = 0.3f, - Origin = Anchor.CentreLeft, - RelativeSizeAxes = Axes.X, - Height = lineWidth, - Y = 0, - }, - new Box - { - Colour = Colour4.White, - Alpha = 0.3f, - Origin = Anchor.CentreLeft, - RelativeSizeAxes = Axes.X, - Height = lineWidth, - Y = drawSize.Y, - }, - new Box - { - Colour = Colour4.White, - Alpha = 0.3f, - Origin = Anchor.TopCentre, - RelativeSizeAxes = Axes.Y, - Width = lineWidth, - X = 0, - }, - new Box - { - Colour = Colour4.White, - Alpha = 0.3f, - Origin = Anchor.TopCentre, - RelativeSizeAxes = Axes.Y, - Width = lineWidth, - X = drawSize.X, - }, - }); - } - - public Vector2 GetSnappedPosition(Vector2 original) + public override Vector2 GetSnappedPosition(Vector2 original) { Vector2 relativeToStart = GeometryUtils.RotateVector(original - StartPosition, GridLineRotation); Vector2 offset = Vector2.Divide(relativeToStart, Spacing); diff --git a/osu.Game/Screens/Edit/Compose/Components/TriangularPositionSnapGrid.cs b/osu.Game/Screens/Edit/Compose/Components/TriangularPositionSnapGrid.cs index 58889bd085..4b6c5dcfe4 100644 --- a/osu.Game/Screens/Edit/Compose/Components/TriangularPositionSnapGrid.cs +++ b/osu.Game/Screens/Edit/Compose/Components/TriangularPositionSnapGrid.cs @@ -2,35 +2,13 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Collections.Generic; -using System.Linq; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Layout; -using osu.Framework.Utils; using osu.Game.Utils; using osuTK; namespace osu.Game.Screens.Edit.Compose.Components { - public partial class TriangularPositionSnapGrid : CompositeDrawable + public partial class TriangularPositionSnapGrid : LinedPositionSnapGrid { - private Vector2 startPosition; - - /// - /// The position of the origin of this in local coordinates. - /// - public Vector2 StartPosition - { - get => startPosition; - set - { - startPosition = value; - gridCache.Invalidate(); - } - } - private float spacing = 1; /// @@ -45,7 +23,7 @@ namespace osu.Game.Screens.Edit.Compose.Components throw new ArgumentException("Grid spacing must be positive."); spacing = value; - gridCache.Invalidate(); + GridCache.Invalidate(); } } @@ -60,40 +38,20 @@ namespace osu.Game.Screens.Edit.Compose.Components set { gridLineRotation = value; - gridCache.Invalidate(); + GridCache.Invalidate(); } } - private readonly LayoutValue gridCache = new LayoutValue(Invalidation.RequiredParentSizeToFit); - public TriangularPositionSnapGrid(Vector2 startPosition) + : base(startPosition) { - StartPosition = startPosition; - Masking = true; - - AddLayout(gridCache); - } - - protected override void Update() - { - base.Update(); - - if (!gridCache.IsValid) - { - ClearInternal(); - - if (DrawWidth > 0 && DrawHeight > 0) - createContent(); - - gridCache.Validate(); - } } private const float sqrt3 = 1.73205080757f; private const float sqrt3_over2 = 0.86602540378f; private const float one_over_sqrt3 = 0.57735026919f; - private void createContent() + protected override void CreateContent() { var drawSize = DrawSize; float stepSpacing = Spacing * sqrt3_over2; @@ -101,130 +59,19 @@ namespace osu.Game.Screens.Edit.Compose.Components var step2 = GeometryUtils.RotateVector(new Vector2(stepSpacing, 0), -GridLineRotation - 90); var step3 = GeometryUtils.RotateVector(new Vector2(stepSpacing, 0), -GridLineRotation - 150); - generateGridLines(step1, drawSize); - generateGridLines(-step1, drawSize); + GenerateGridLines(step1, drawSize); + GenerateGridLines(-step1, drawSize); - generateGridLines(step2, drawSize); - generateGridLines(-step2, drawSize); + GenerateGridLines(step2, drawSize); + GenerateGridLines(-step2, drawSize); - generateGridLines(step3, drawSize); - generateGridLines(-step3, drawSize); + GenerateGridLines(step3, drawSize); + GenerateGridLines(-step3, drawSize); - generateOutline(drawSize); + GenerateOutline(drawSize); } - private void generateGridLines(Vector2 step, Vector2 drawSize) - { - int index = 0; - var currentPosition = startPosition; - - // Make lines the same width independent of display resolution. - float lineWidth = DrawWidth / ScreenSpaceDrawQuad.Width; - float lineLength = drawSize.Length * 2; - - List generatedLines = new List(); - - while (lineDefinitelyIntersectsBox(currentPosition, step.PerpendicularLeft, drawSize) || - isMovingTowardsBox(currentPosition, step, drawSize)) - { - var gridLine = new Box - { - Colour = Colour4.White, - Alpha = 0.1f, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.None, - Width = lineWidth, - Height = lineLength, - Position = currentPosition, - Rotation = MathHelper.RadiansToDegrees(MathF.Atan2(step.Y, step.X)), - }; - - generatedLines.Add(gridLine); - - index += 1; - currentPosition = startPosition + index * step; - } - - if (generatedLines.Count == 0) - return; - - generatedLines.First().Alpha = 0.3f; - - AddRangeInternal(generatedLines); - } - - private bool isMovingTowardsBox(Vector2 currentPosition, Vector2 step, Vector2 box) - { - return (currentPosition + step).LengthSquared < currentPosition.LengthSquared || - (currentPosition + step - box).LengthSquared < (currentPosition - box).LengthSquared; - } - - private bool lineDefinitelyIntersectsBox(Vector2 lineStart, Vector2 lineDir, Vector2 box) - { - var p2 = lineStart + lineDir; - - double d1 = det(Vector2.Zero); - double d2 = det(new Vector2(box.X, 0)); - double d3 = det(new Vector2(0, box.Y)); - double d4 = det(box); - - return definitelyDifferentSign(d1, d2) || definitelyDifferentSign(d3, d4) || - definitelyDifferentSign(d1, d3) || definitelyDifferentSign(d2, d4); - - double det(Vector2 p) => (p.X - lineStart.X) * (p2.Y - lineStart.Y) - (p.Y - lineStart.Y) * (p2.X - lineStart.X); - - bool definitelyDifferentSign(double a, double b) => !Precision.AlmostEquals(a, 0) && - !Precision.AlmostEquals(b, 0) && - Math.Sign(a) != Math.Sign(b); - } - - private void generateOutline(Vector2 drawSize) - { - // Make lines the same width independent of display resolution. - float lineWidth = DrawWidth / ScreenSpaceDrawQuad.Width; - - AddRangeInternal(new[] - { - new Box - { - Colour = Colour4.White, - Alpha = 0.3f, - Origin = Anchor.CentreLeft, - RelativeSizeAxes = Axes.X, - Height = lineWidth, - Y = 0, - }, - new Box - { - Colour = Colour4.White, - Alpha = 0.3f, - Origin = Anchor.CentreLeft, - RelativeSizeAxes = Axes.X, - Height = lineWidth, - Y = drawSize.Y, - }, - new Box - { - Colour = Colour4.White, - Alpha = 0.3f, - Origin = Anchor.TopCentre, - RelativeSizeAxes = Axes.Y, - Width = lineWidth, - X = 0, - }, - new Box - { - Colour = Colour4.White, - Alpha = 0.3f, - Origin = Anchor.TopCentre, - RelativeSizeAxes = Axes.Y, - Width = lineWidth, - X = drawSize.X, - }, - }); - } - - public Vector2 GetSnappedPosition(Vector2 original) + public override Vector2 GetSnappedPosition(Vector2 original) { Vector2 relativeToStart = GeometryUtils.RotateVector(original - StartPosition, GridLineRotation); Vector2 hex = pixelToHex(relativeToStart); From a20c430d6f1c4e8deeb2ca974d719ae032bd26fd Mon Sep 17 00:00:00 2001 From: OliBomby Date: Thu, 28 Dec 2023 22:35:00 +0100 Subject: [PATCH 0142/2493] fix wrong grid cache being used --- .../Compose/Components/RectangularPositionSnapGrid.cs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/RectangularPositionSnapGrid.cs b/osu.Game/Screens/Edit/Compose/Components/RectangularPositionSnapGrid.cs index 14a0e3625a..930a592850 100644 --- a/osu.Game/Screens/Edit/Compose/Components/RectangularPositionSnapGrid.cs +++ b/osu.Game/Screens/Edit/Compose/Components/RectangularPositionSnapGrid.cs @@ -2,8 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using osu.Framework.Graphics; -using osu.Framework.Layout; using osu.Game.Utils; using osuTK; @@ -25,7 +23,7 @@ namespace osu.Game.Screens.Edit.Compose.Components throw new ArgumentException("Grid spacing must be positive."); spacing = value; - gridCache.Invalidate(); + GridCache.Invalidate(); } } @@ -40,12 +38,10 @@ namespace osu.Game.Screens.Edit.Compose.Components set { gridLineRotation = value; - gridCache.Invalidate(); + GridCache.Invalidate(); } } - private readonly LayoutValue gridCache = new LayoutValue(Invalidation.RequiredParentSizeToFit); - public RectangularPositionSnapGrid(Vector2 startPosition) : base(startPosition) { From b16c232490a5353dbf3fd4e59390f8147b4e816b Mon Sep 17 00:00:00 2001 From: OliBomby Date: Thu, 28 Dec 2023 22:36:30 +0100 Subject: [PATCH 0143/2493] add basic control by grid tool box --- .../Edit/OsuHitObjectComposer.cs | 11 ++ osu.Game/Rulesets/Edit/GridToolboxGroup.cs | 108 ++++++++++++++++++ 2 files changed, 119 insertions(+) create mode 100644 osu.Game/Rulesets/Edit/GridToolboxGroup.cs diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs index 448cfaf84c..e487a5d490 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs @@ -65,6 +65,9 @@ namespace osu.Game.Rulesets.Osu.Edit [Cached(typeof(IDistanceSnapProvider))] protected readonly OsuDistanceSnapProvider DistanceSnapProvider = new OsuDistanceSnapProvider(); + [Cached] + protected readonly GridToolboxGroup GridToolboxGroup = new GridToolboxGroup(); + [Cached] protected readonly FreehandSliderToolboxGroup FreehandlSliderToolboxGroup = new FreehandSliderToolboxGroup(); @@ -99,8 +102,16 @@ namespace osu.Game.Rulesets.Osu.Edit // we may be entering the screen with a selection already active updateDistanceSnapGrid(); + GridToolboxGroup.StartPositionX.ValueChanged += x => + rectangularPositionSnapGrid.StartPosition = new Vector2(x.NewValue, rectangularPositionSnapGrid.StartPosition.Y); + GridToolboxGroup.StartPositionY.ValueChanged += y => + rectangularPositionSnapGrid.StartPosition = new Vector2(rectangularPositionSnapGrid.StartPosition.X, y.NewValue); + GridToolboxGroup.Spacing.ValueChanged += s => rectangularPositionSnapGrid.Spacing = new Vector2(s.NewValue); + GridToolboxGroup.GridLinesRotation.ValueChanged += r => rectangularPositionSnapGrid.GridLineRotation = r.NewValue; + RightToolbox.AddRange(new EditorToolboxGroup[] { + GridToolboxGroup, new TransformToolboxGroup { RotationHandler = BlueprintContainer.SelectionHandler.RotationHandler, }, FreehandlSliderToolboxGroup } diff --git a/osu.Game/Rulesets/Edit/GridToolboxGroup.cs b/osu.Game/Rulesets/Edit/GridToolboxGroup.cs new file mode 100644 index 0000000000..b6903c1369 --- /dev/null +++ b/osu.Game/Rulesets/Edit/GridToolboxGroup.cs @@ -0,0 +1,108 @@ +// 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.Bindables; +using osu.Framework.Graphics; +using osu.Game.Graphics.UserInterface; +using osu.Game.Screens.Edit; + +namespace osu.Game.Rulesets.Edit +{ + public partial class GridToolboxGroup : EditorToolboxGroup + { + [Resolved] + private EditorBeatmap editorBeatmap { get; set; } = null!; + + public GridToolboxGroup() + : base("grid") + { + } + + public BindableFloat StartPositionX { get; } = new BindableFloat(256f) + { + MinValue = 0f, + MaxValue = 512f, + Precision = 1f + }; + + public BindableFloat StartPositionY { get; } = new BindableFloat(192) + { + MinValue = 0f, + MaxValue = 384f, + Precision = 1f + }; + + public BindableFloat Spacing { get; } = new BindableFloat(4f) + { + MinValue = 4f, + MaxValue = 128f, + Precision = 1f + }; + + public BindableFloat GridLinesRotation { get; } = new BindableFloat(0f) + { + MinValue = -180f, + MaxValue = 180f, + Precision = 1f + }; + + private ExpandableSlider startPositionXSlider = null!; + private ExpandableSlider startPositionYSlider = null!; + private ExpandableSlider spacingSlider = null!; + private ExpandableSlider gridLinesRotationSlider = null!; + + [BackgroundDependencyLoader] + private void load() + { + Children = new Drawable[] + { + startPositionXSlider = new ExpandableSlider + { + Current = StartPositionX + }, + startPositionYSlider = new ExpandableSlider + { + Current = StartPositionY + }, + spacingSlider = new ExpandableSlider + { + Current = Spacing + }, + gridLinesRotationSlider = new ExpandableSlider + { + Current = GridLinesRotation + } + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + StartPositionX.BindValueChanged(x => + { + startPositionXSlider.ContractedLabelText = $"X: {x.NewValue:N0}"; + startPositionXSlider.ExpandedLabelText = $"X Offset: {x.NewValue:N0}"; + }, true); + + StartPositionY.BindValueChanged(y => + { + startPositionYSlider.ContractedLabelText = $"Y: {y.NewValue:N0}"; + startPositionYSlider.ExpandedLabelText = $"Y Offset: {y.NewValue:N0}"; + }, true); + + Spacing.BindValueChanged(spacing => + { + spacingSlider.ContractedLabelText = $"S: {spacing.NewValue:N0}"; + spacingSlider.ExpandedLabelText = $"Spacing: {spacing.NewValue:N0}"; + }, true); + + GridLinesRotation.BindValueChanged(rotation => + { + gridLinesRotationSlider.ContractedLabelText = $"R: {rotation.NewValue:N0}"; + gridLinesRotationSlider.ExpandedLabelText = $"Rotation: {rotation.NewValue:N0}"; + }, true); + } + } +} From 0ce1a48e68ea26d9295a611e6648ffafc8a8651f Mon Sep 17 00:00:00 2001 From: OliBomby Date: Thu, 28 Dec 2023 22:54:30 +0100 Subject: [PATCH 0144/2493] Add comment --- .../Edit/Compose/Components/TriangularPositionSnapGrid.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Screens/Edit/Compose/Components/TriangularPositionSnapGrid.cs b/osu.Game/Screens/Edit/Compose/Components/TriangularPositionSnapGrid.cs index 4b6c5dcfe4..af44641f5e 100644 --- a/osu.Game/Screens/Edit/Compose/Components/TriangularPositionSnapGrid.cs +++ b/osu.Game/Screens/Edit/Compose/Components/TriangularPositionSnapGrid.cs @@ -99,6 +99,8 @@ namespace osu.Game.Screens.Edit.Compose.Components private Vector2 hexToPixel(Vector2 hex) { + // Taken from + // with modifications for the different definition of size. return new Vector2(Spacing * (hex.X - hex.Y / 2), Spacing * one_over_sqrt3 * 1.5f * hex.Y); } } From f223487e1cd9cd2a391f79575a13222cfa19987e Mon Sep 17 00:00:00 2001 From: OliBomby Date: Thu, 28 Dec 2023 23:10:06 +0100 Subject: [PATCH 0145/2493] improve code --- .../Editor/TestSceneOsuEditorGrids.cs | 7 +- .../Edit/OsuGridToolboxGroup.cs | 71 +++++++++++++++---- .../Edit/OsuHitObjectComposer.cs | 18 ++--- .../Edit/OsuRectangularPositionSnapGrid.cs | 69 ------------------ .../TestSceneRectangularPositionSnapGrid.cs | 3 +- .../Screens/Editors/LadderEditorScreen.cs | 2 +- .../Components/LinedPositionSnapGrid.cs | 3 +- .../Components/RectangularPositionSnapGrid.cs | 5 -- .../Components/TriangularPositionSnapGrid.cs | 5 -- 9 files changed, 75 insertions(+), 108 deletions(-) rename osu.Game/Rulesets/Edit/GridToolboxGroup.cs => osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs (65%) delete mode 100644 osu.Game.Rulesets.Osu/Edit/OsuRectangularPositionSnapGrid.cs diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorGrids.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorGrids.cs index d14e593587..299db23ccc 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorGrids.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorGrids.cs @@ -7,6 +7,7 @@ using osu.Framework.Testing; using osu.Framework.Utils; using osu.Game.Rulesets.Osu.Edit; using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles; +using osu.Game.Screens.Edit.Compose.Components; using osu.Game.Tests.Visual; using osuTK; using osuTK.Input; @@ -69,7 +70,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor AddStep("choose placement tool", () => InputManager.Key(Key.Number2)); AddStep("move cursor to (1, 1)", () => { - var composer = Editor.ChildrenOfType().Single(); + var composer = Editor.ChildrenOfType().Single(); InputManager.MoveMouseTo(composer.ToScreenSpace(new Vector2(1, 1))); }); @@ -83,7 +84,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor public void TestGridSizeToggling() { AddStep("enable rectangular grid", () => InputManager.Key(Key.Y)); - AddUntilStep("rectangular grid visible", () => this.ChildrenOfType().Any()); + AddUntilStep("rectangular grid visible", () => this.ChildrenOfType().Any()); gridSizeIs(4); nextGridSizeIs(8); @@ -99,7 +100,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor } private void gridSizeIs(int size) - => AddAssert($"grid size is {size}", () => this.ChildrenOfType().Single().Spacing == new Vector2(size) + => AddAssert($"grid size is {size}", () => this.ChildrenOfType().Single().Spacing == new Vector2(size) && EditorBeatmap.BeatmapInfo.GridSize == size); } } diff --git a/osu.Game/Rulesets/Edit/GridToolboxGroup.cs b/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs similarity index 65% rename from osu.Game/Rulesets/Edit/GridToolboxGroup.cs rename to osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs index b6903c1369..d93b6c27c0 100644 --- a/osu.Game/Rulesets/Edit/GridToolboxGroup.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs @@ -1,35 +1,40 @@ // 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; +using osu.Framework.Input.Bindings; +using osu.Framework.Input.Events; using osu.Game.Graphics.UserInterface; +using osu.Game.Input.Bindings; +using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Osu.UI; using osu.Game.Screens.Edit; -namespace osu.Game.Rulesets.Edit +namespace osu.Game.Rulesets.Osu.Edit { - public partial class GridToolboxGroup : EditorToolboxGroup + public partial class OsuGridToolboxGroup : EditorToolboxGroup, IKeyBindingHandler { + private static readonly int[] grid_sizes = { 4, 8, 16, 32 }; + + private int currentGridSizeIndex = grid_sizes.Length - 1; + [Resolved] private EditorBeatmap editorBeatmap { get; set; } = null!; - public GridToolboxGroup() - : base("grid") - { - } - - public BindableFloat StartPositionX { get; } = new BindableFloat(256f) + public BindableFloat StartPositionX { get; } = new BindableFloat(OsuPlayfield.BASE_SIZE.X / 2) { MinValue = 0f, - MaxValue = 512f, + MaxValue = OsuPlayfield.BASE_SIZE.X, Precision = 1f }; - public BindableFloat StartPositionY { get; } = new BindableFloat(192) + public BindableFloat StartPositionY { get; } = new BindableFloat(OsuPlayfield.BASE_SIZE.Y / 2) { MinValue = 0f, - MaxValue = 384f, + MaxValue = OsuPlayfield.BASE_SIZE.Y, Precision = 1f }; @@ -42,8 +47,8 @@ namespace osu.Game.Rulesets.Edit public BindableFloat GridLinesRotation { get; } = new BindableFloat(0f) { - MinValue = -180f, - MaxValue = 180f, + MinValue = -45f, + MaxValue = 45f, Precision = 1f }; @@ -52,6 +57,11 @@ namespace osu.Game.Rulesets.Edit private ExpandableSlider spacingSlider = null!; private ExpandableSlider gridLinesRotationSlider = null!; + public OsuGridToolboxGroup() + : base("grid") + { + } + [BackgroundDependencyLoader] private void load() { @@ -74,6 +84,11 @@ namespace osu.Game.Rulesets.Edit Current = GridLinesRotation } }; + + int gridSizeIndex = Array.IndexOf(grid_sizes, editorBeatmap.BeatmapInfo.GridSize); + if (gridSizeIndex >= 0) + currentGridSizeIndex = gridSizeIndex; + updateSpacing(); } protected override void LoadComplete() @@ -104,5 +119,35 @@ namespace osu.Game.Rulesets.Edit gridLinesRotationSlider.ExpandedLabelText = $"Rotation: {rotation.NewValue:N0}"; }, true); } + + private void nextGridSize() + { + currentGridSizeIndex = (currentGridSizeIndex + 1) % grid_sizes.Length; + updateSpacing(); + } + + private void updateSpacing() + { + int gridSize = grid_sizes[currentGridSizeIndex]; + + editorBeatmap.BeatmapInfo.GridSize = gridSize; + Spacing.Value = gridSize; + } + + public bool OnPressed(KeyBindingPressEvent e) + { + switch (e.Action) + { + case GlobalAction.EditorCycleGridDisplayMode: + nextGridSize(); + return true; + } + + return false; + } + + public void OnReleased(KeyBindingReleaseEvent e) + { + } } } diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs index e487a5d490..12457fc88d 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs @@ -66,7 +66,7 @@ namespace osu.Game.Rulesets.Osu.Edit protected readonly OsuDistanceSnapProvider DistanceSnapProvider = new OsuDistanceSnapProvider(); [Cached] - protected readonly GridToolboxGroup GridToolboxGroup = new GridToolboxGroup(); + protected readonly OsuGridToolboxGroup OsuGridToolboxGroup = new OsuGridToolboxGroup(); [Cached] protected readonly FreehandSliderToolboxGroup FreehandlSliderToolboxGroup = new FreehandSliderToolboxGroup(); @@ -86,7 +86,7 @@ namespace osu.Game.Rulesets.Osu.Edit { RelativeSizeAxes = Axes.Both }, - rectangularPositionSnapGrid = new OsuRectangularPositionSnapGrid + rectangularPositionSnapGrid = new RectangularPositionSnapGrid { RelativeSizeAxes = Axes.Both } @@ -102,16 +102,16 @@ namespace osu.Game.Rulesets.Osu.Edit // we may be entering the screen with a selection already active updateDistanceSnapGrid(); - GridToolboxGroup.StartPositionX.ValueChanged += x => - rectangularPositionSnapGrid.StartPosition = new Vector2(x.NewValue, rectangularPositionSnapGrid.StartPosition.Y); - GridToolboxGroup.StartPositionY.ValueChanged += y => - rectangularPositionSnapGrid.StartPosition = new Vector2(rectangularPositionSnapGrid.StartPosition.X, y.NewValue); - GridToolboxGroup.Spacing.ValueChanged += s => rectangularPositionSnapGrid.Spacing = new Vector2(s.NewValue); - GridToolboxGroup.GridLinesRotation.ValueChanged += r => rectangularPositionSnapGrid.GridLineRotation = r.NewValue; + OsuGridToolboxGroup.StartPositionX.BindValueChanged(x => + rectangularPositionSnapGrid.StartPosition = new Vector2(x.NewValue, rectangularPositionSnapGrid.StartPosition.Y), true); + OsuGridToolboxGroup.StartPositionY.BindValueChanged(y => + rectangularPositionSnapGrid.StartPosition = new Vector2(rectangularPositionSnapGrid.StartPosition.X, y.NewValue), true); + OsuGridToolboxGroup.Spacing.BindValueChanged(s => rectangularPositionSnapGrid.Spacing = new Vector2(s.NewValue), true); + OsuGridToolboxGroup.GridLinesRotation.BindValueChanged(r => rectangularPositionSnapGrid.GridLineRotation = r.NewValue, true); RightToolbox.AddRange(new EditorToolboxGroup[] { - GridToolboxGroup, + OsuGridToolboxGroup, new TransformToolboxGroup { RotationHandler = BlueprintContainer.SelectionHandler.RotationHandler, }, FreehandlSliderToolboxGroup } diff --git a/osu.Game.Rulesets.Osu/Edit/OsuRectangularPositionSnapGrid.cs b/osu.Game.Rulesets.Osu/Edit/OsuRectangularPositionSnapGrid.cs deleted file mode 100644 index efc6668ebf..0000000000 --- a/osu.Game.Rulesets.Osu/Edit/OsuRectangularPositionSnapGrid.cs +++ /dev/null @@ -1,69 +0,0 @@ -// 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.Input.Bindings; -using osu.Framework.Input.Events; -using osu.Game.Input.Bindings; -using osu.Game.Rulesets.Osu.UI; -using osu.Game.Screens.Edit; -using osu.Game.Screens.Edit.Compose.Components; -using osuTK; - -namespace osu.Game.Rulesets.Osu.Edit -{ - public partial class OsuRectangularPositionSnapGrid : RectangularPositionSnapGrid, IKeyBindingHandler - { - private static readonly int[] grid_sizes = { 4, 8, 16, 32 }; - - private int currentGridSizeIndex = grid_sizes.Length - 1; - - [Resolved] - private EditorBeatmap editorBeatmap { get; set; } = null!; - - public OsuRectangularPositionSnapGrid() - : base(OsuPlayfield.BASE_SIZE / 2) - { - } - - [BackgroundDependencyLoader] - private void load() - { - int gridSizeIndex = Array.IndexOf(grid_sizes, editorBeatmap.BeatmapInfo.GridSize); - if (gridSizeIndex >= 0) - currentGridSizeIndex = gridSizeIndex; - updateSpacing(); - } - - private void nextGridSize() - { - currentGridSizeIndex = (currentGridSizeIndex + 1) % grid_sizes.Length; - updateSpacing(); - } - - private void updateSpacing() - { - int gridSize = grid_sizes[currentGridSizeIndex]; - - editorBeatmap.BeatmapInfo.GridSize = gridSize; - Spacing = new Vector2(gridSize); - } - - public bool OnPressed(KeyBindingPressEvent e) - { - switch (e.Action) - { - case GlobalAction.EditorCycleGridDisplayMode: - nextGridSize(); - return true; - } - - return false; - } - - public void OnReleased(KeyBindingReleaseEvent e) - { - } - } -} diff --git a/osu.Game.Tests/Visual/Editing/TestSceneRectangularPositionSnapGrid.cs b/osu.Game.Tests/Visual/Editing/TestSceneRectangularPositionSnapGrid.cs index 210af09055..a0042cf605 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneRectangularPositionSnapGrid.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneRectangularPositionSnapGrid.cs @@ -52,9 +52,10 @@ namespace osu.Game.Tests.Visual.Editing { RectangularPositionSnapGrid grid = null; - AddStep("create grid", () => Child = grid = new RectangularPositionSnapGrid(position) + AddStep("create grid", () => Child = grid = new RectangularPositionSnapGrid() { RelativeSizeAxes = Axes.Both, + StartPosition = position, Spacing = spacing, GridLineRotation = rotation }); diff --git a/osu.Game.Tournament/Screens/Editors/LadderEditorScreen.cs b/osu.Game.Tournament/Screens/Editors/LadderEditorScreen.cs index 4074e681f9..ad00e8d47d 100644 --- a/osu.Game.Tournament/Screens/Editors/LadderEditorScreen.cs +++ b/osu.Game.Tournament/Screens/Editors/LadderEditorScreen.cs @@ -51,7 +51,7 @@ namespace osu.Game.Tournament.Screens.Editors AddInternal(rightClickMessage = new WarningBox("Right click to place and link matches")); - ScrollContent.Add(grid = new RectangularPositionSnapGrid(Vector2.Zero) + ScrollContent.Add(grid = new RectangularPositionSnapGrid { Spacing = new Vector2(GRID_SPACING), Anchor = Anchor.Centre, diff --git a/osu.Game/Screens/Edit/Compose/Components/LinedPositionSnapGrid.cs b/osu.Game/Screens/Edit/Compose/Components/LinedPositionSnapGrid.cs index 642a125265..3616bc1ca1 100644 --- a/osu.Game/Screens/Edit/Compose/Components/LinedPositionSnapGrid.cs +++ b/osu.Game/Screens/Edit/Compose/Components/LinedPositionSnapGrid.cs @@ -32,9 +32,8 @@ namespace osu.Game.Screens.Edit.Compose.Components protected readonly LayoutValue GridCache = new LayoutValue(Invalidation.RequiredParentSizeToFit); - protected LinedPositionSnapGrid(Vector2 startPosition) + protected LinedPositionSnapGrid() { - StartPosition = startPosition; Masking = true; AddLayout(GridCache); diff --git a/osu.Game/Screens/Edit/Compose/Components/RectangularPositionSnapGrid.cs b/osu.Game/Screens/Edit/Compose/Components/RectangularPositionSnapGrid.cs index 930a592850..2392921203 100644 --- a/osu.Game/Screens/Edit/Compose/Components/RectangularPositionSnapGrid.cs +++ b/osu.Game/Screens/Edit/Compose/Components/RectangularPositionSnapGrid.cs @@ -42,11 +42,6 @@ namespace osu.Game.Screens.Edit.Compose.Components } } - public RectangularPositionSnapGrid(Vector2 startPosition) - : base(startPosition) - { - } - protected override void CreateContent() { var drawSize = DrawSize; diff --git a/osu.Game/Screens/Edit/Compose/Components/TriangularPositionSnapGrid.cs b/osu.Game/Screens/Edit/Compose/Components/TriangularPositionSnapGrid.cs index af44641f5e..c98890e294 100644 --- a/osu.Game/Screens/Edit/Compose/Components/TriangularPositionSnapGrid.cs +++ b/osu.Game/Screens/Edit/Compose/Components/TriangularPositionSnapGrid.cs @@ -42,11 +42,6 @@ namespace osu.Game.Screens.Edit.Compose.Components } } - public TriangularPositionSnapGrid(Vector2 startPosition) - : base(startPosition) - { - } - private const float sqrt3 = 1.73205080757f; private const float sqrt3_over2 = 0.86602540378f; private const float one_over_sqrt3 = 0.57735026919f; From 351cfbff3e02f96b79eff3a020f0a519e1348052 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Thu, 28 Dec 2023 23:38:10 +0100 Subject: [PATCH 0146/2493] Fix snapping going out of bounds --- osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs index 12457fc88d..3e5cede1d3 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs @@ -24,6 +24,7 @@ using osu.Game.Rulesets.Edit.Tools; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Osu.UI; using osu.Game.Rulesets.UI; using osu.Game.Screens.Edit.Components.TernaryButtons; using osu.Game.Screens.Edit.Compose.Components; @@ -218,6 +219,10 @@ namespace osu.Game.Rulesets.Osu.Edit { Vector2 pos = rectangularPositionSnapGrid.GetSnappedPosition(rectangularPositionSnapGrid.ToLocalSpace(result.ScreenSpacePosition)); + // A rotated grid can produce a position that is outside of the playfield. + // We need to clamp the position to the playfield bounds to ensure that the snapped position is always in bounds. + pos = Vector2.Clamp(pos, Vector2.Zero, OsuPlayfield.BASE_SIZE); + result.ScreenSpacePosition = rectangularPositionSnapGrid.ToScreenSpace(pos); } } From 8ef9bdf861d40eafb12eedbfeb8c093e00c987a2 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Thu, 28 Dec 2023 23:39:24 +0100 Subject: [PATCH 0147/2493] clarify comment --- osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs index 3e5cede1d3..7b872eef38 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs @@ -219,7 +219,7 @@ namespace osu.Game.Rulesets.Osu.Edit { Vector2 pos = rectangularPositionSnapGrid.GetSnappedPosition(rectangularPositionSnapGrid.ToLocalSpace(result.ScreenSpacePosition)); - // A rotated grid can produce a position that is outside of the playfield. + // A grid which doesn't perfectly fit the playfield can produce a position that is outside of the playfield. // We need to clamp the position to the playfield bounds to ensure that the snapped position is always in bounds. pos = Vector2.Clamp(pos, Vector2.Zero, OsuPlayfield.BASE_SIZE); From 040fd5ef9c65ca22aeb5dd9d903d0ed1cf6f0951 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Fri, 29 Dec 2023 16:59:12 +0100 Subject: [PATCH 0148/2493] Add option to change grid type --- .../Edit/OsuGridToolboxGroup.cs | 30 ++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs b/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs index d93b6c27c0..892c89ebc2 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs @@ -2,9 +2,12 @@ // 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; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Game.Graphics.UserInterface; @@ -12,6 +15,7 @@ using osu.Game.Input.Bindings; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Osu.UI; using osu.Game.Screens.Edit; +using osu.Game.Screens.Edit.Components.RadioButtons; namespace osu.Game.Rulesets.Osu.Edit { @@ -52,10 +56,13 @@ namespace osu.Game.Rulesets.Osu.Edit Precision = 1f }; + public Bindable GridType { get; } = new Bindable(); + private ExpandableSlider startPositionXSlider = null!; private ExpandableSlider startPositionYSlider = null!; private ExpandableSlider spacingSlider = null!; private ExpandableSlider gridLinesRotationSlider = null!; + private EditorRadioButtonCollection gridTypeButtons = null!; public OsuGridToolboxGroup() : base("grid") @@ -82,7 +89,20 @@ namespace osu.Game.Rulesets.Osu.Edit gridLinesRotationSlider = new ExpandableSlider { Current = GridLinesRotation - } + }, + gridTypeButtons = new EditorRadioButtonCollection + { + RelativeSizeAxes = Axes.X, + Items = new[] + { + new RadioButton("Square", + () => GridType.Value = PositionSnapGridType.Square, + () => new SpriteIcon { Icon = FontAwesome.Regular.Square }), + new RadioButton("Triangle", + () => GridType.Value = PositionSnapGridType.Triangle, + () => new Triangle()) + } + }, }; int gridSizeIndex = Array.IndexOf(grid_sizes, editorBeatmap.BeatmapInfo.GridSize); @@ -95,6 +115,8 @@ namespace osu.Game.Rulesets.Osu.Edit { base.LoadComplete(); + gridTypeButtons.Items.First().Select(); + StartPositionX.BindValueChanged(x => { startPositionXSlider.ContractedLabelText = $"X: {x.NewValue:N0}"; @@ -150,4 +172,10 @@ namespace osu.Game.Rulesets.Osu.Edit { } } + + public enum PositionSnapGridType + { + Square, + Triangle, + } } From 8a331057b0bb76578608c7dd4afc79e98fab82b2 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Fri, 29 Dec 2023 17:25:17 +0100 Subject: [PATCH 0149/2493] Make it actually possible to change grid type --- .../Edit/OsuHitObjectComposer.cs | 60 ++++++++--- .../Components/LinedPositionSnapGrid.cs | 97 +---------------- .../Compose/Components/PositionSnapGrid.cs | 102 ++++++++++++++++++ 3 files changed, 152 insertions(+), 107 deletions(-) create mode 100644 osu.Game/Screens/Edit/Compose/Components/PositionSnapGrid.cs diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs index 7b872eef38..b0ac8467c1 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs @@ -84,10 +84,6 @@ namespace osu.Game.Rulesets.Osu.Edit LayerBelowRuleset.AddRange(new Drawable[] { distanceSnapGridContainer = new Container - { - RelativeSizeAxes = Axes.Both - }, - rectangularPositionSnapGrid = new RectangularPositionSnapGrid { RelativeSizeAxes = Axes.Both } @@ -103,12 +99,7 @@ namespace osu.Game.Rulesets.Osu.Edit // we may be entering the screen with a selection already active updateDistanceSnapGrid(); - OsuGridToolboxGroup.StartPositionX.BindValueChanged(x => - rectangularPositionSnapGrid.StartPosition = new Vector2(x.NewValue, rectangularPositionSnapGrid.StartPosition.Y), true); - OsuGridToolboxGroup.StartPositionY.BindValueChanged(y => - rectangularPositionSnapGrid.StartPosition = new Vector2(rectangularPositionSnapGrid.StartPosition.X, y.NewValue), true); - OsuGridToolboxGroup.Spacing.BindValueChanged(s => rectangularPositionSnapGrid.Spacing = new Vector2(s.NewValue), true); - OsuGridToolboxGroup.GridLinesRotation.BindValueChanged(r => rectangularPositionSnapGrid.GridLineRotation = r.NewValue, true); + OsuGridToolboxGroup.GridType.BindValueChanged(updatePositionSnapGrid, true); RightToolbox.AddRange(new EditorToolboxGroup[] { @@ -119,6 +110,49 @@ namespace osu.Game.Rulesets.Osu.Edit ); } + private void updatePositionSnapGrid(ValueChangedEvent obj) + { + if (positionSnapGrid != null) + LayerBelowRuleset.Remove(positionSnapGrid, true); + + switch (obj.NewValue) + { + case PositionSnapGridType.Square: + var rectangularPositionSnapGrid = new RectangularPositionSnapGrid(); + + OsuGridToolboxGroup.Spacing.BindValueChanged(s => rectangularPositionSnapGrid.Spacing = new Vector2(s.NewValue), true); + OsuGridToolboxGroup.GridLinesRotation.BindValueChanged(r => rectangularPositionSnapGrid.GridLineRotation = r.NewValue, true); + + positionSnapGrid = rectangularPositionSnapGrid; + break; + + case PositionSnapGridType.Triangle: + var triangularPositionSnapGrid = new TriangularPositionSnapGrid(); + + OsuGridToolboxGroup.Spacing.BindValueChanged(s => triangularPositionSnapGrid.Spacing = s.NewValue, true); + OsuGridToolboxGroup.GridLinesRotation.BindValueChanged(r => triangularPositionSnapGrid.GridLineRotation = r.NewValue, true); + + positionSnapGrid = triangularPositionSnapGrid; + break; + + default: + throw new NotImplementedException($"{OsuGridToolboxGroup.GridType} has an incorrect value."); + } + + bindPositionSnapGridStartPosition(positionSnapGrid); + positionSnapGrid.RelativeSizeAxes = Axes.Both; + LayerBelowRuleset.Add(positionSnapGrid); + return; + + void bindPositionSnapGridStartPosition(PositionSnapGrid snapGrid) + { + OsuGridToolboxGroup.StartPositionX.BindValueChanged(x => + snapGrid.StartPosition = new Vector2(x.NewValue, snapGrid.StartPosition.Y), true); + OsuGridToolboxGroup.StartPositionY.BindValueChanged(y => + snapGrid.StartPosition = new Vector2(snapGrid.StartPosition.X, y.NewValue), true); + } + } + protected override ComposeBlueprintContainer CreateBlueprintContainer() => new OsuBlueprintContainer(this); @@ -159,7 +193,7 @@ namespace osu.Game.Rulesets.Osu.Edit private readonly Cached distanceSnapGridCache = new Cached(); private double? lastDistanceSnapGridTime; - private RectangularPositionSnapGrid rectangularPositionSnapGrid; + private PositionSnapGrid positionSnapGrid; protected override void Update() { @@ -217,13 +251,13 @@ namespace osu.Game.Rulesets.Osu.Edit { if (rectangularGridSnapToggle.Value == TernaryState.True) { - Vector2 pos = rectangularPositionSnapGrid.GetSnappedPosition(rectangularPositionSnapGrid.ToLocalSpace(result.ScreenSpacePosition)); + Vector2 pos = positionSnapGrid.GetSnappedPosition(positionSnapGrid.ToLocalSpace(result.ScreenSpacePosition)); // A grid which doesn't perfectly fit the playfield can produce a position that is outside of the playfield. // We need to clamp the position to the playfield bounds to ensure that the snapped position is always in bounds. pos = Vector2.Clamp(pos, Vector2.Zero, OsuPlayfield.BASE_SIZE); - result.ScreenSpacePosition = rectangularPositionSnapGrid.ToScreenSpace(pos); + result.ScreenSpacePosition = positionSnapGrid.ToScreenSpace(pos); } } diff --git a/osu.Game/Screens/Edit/Compose/Components/LinedPositionSnapGrid.cs b/osu.Game/Screens/Edit/Compose/Components/LinedPositionSnapGrid.cs index 3616bc1ca1..9ab12e4b71 100644 --- a/osu.Game/Screens/Edit/Compose/Components/LinedPositionSnapGrid.cs +++ b/osu.Game/Screens/Edit/Compose/Components/LinedPositionSnapGrid.cs @@ -5,61 +5,18 @@ using System; using System.Collections.Generic; using System.Linq; using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; -using osu.Framework.Layout; using osu.Framework.Utils; using osuTK; namespace osu.Game.Screens.Edit.Compose.Components { - public abstract partial class LinedPositionSnapGrid : CompositeDrawable + public abstract partial class LinedPositionSnapGrid : PositionSnapGrid { - private Vector2 startPosition; - - /// - /// The position of the origin of this in local coordinates. - /// - public Vector2 StartPosition - { - get => startPosition; - set - { - startPosition = value; - GridCache.Invalidate(); - } - } - - protected readonly LayoutValue GridCache = new LayoutValue(Invalidation.RequiredParentSizeToFit); - - protected LinedPositionSnapGrid() - { - Masking = true; - - AddLayout(GridCache); - } - - protected override void Update() - { - base.Update(); - - if (!GridCache.IsValid) - { - ClearInternal(); - - if (DrawWidth > 0 && DrawHeight > 0) - CreateContent(); - - GridCache.Validate(); - } - } - - protected abstract void CreateContent(); - protected void GenerateGridLines(Vector2 step, Vector2 drawSize) { int index = 0; - var currentPosition = startPosition; + var currentPosition = StartPosition; // Make lines the same width independent of display resolution. float lineWidth = DrawWidth / ScreenSpaceDrawQuad.Width; @@ -85,7 +42,7 @@ namespace osu.Game.Screens.Edit.Compose.Components generatedLines.Add(gridLine); index += 1; - currentPosition = startPosition + index * step; + currentPosition = StartPosition + index * step; } if (generatedLines.Count == 0) @@ -120,53 +77,5 @@ namespace osu.Game.Screens.Edit.Compose.Components !Precision.AlmostEquals(b, 0) && Math.Sign(a) != Math.Sign(b); } - - protected void GenerateOutline(Vector2 drawSize) - { - // Make lines the same width independent of display resolution. - float lineWidth = DrawWidth / ScreenSpaceDrawQuad.Width; - - AddRangeInternal(new[] - { - new Box - { - Colour = Colour4.White, - Alpha = 0.3f, - Origin = Anchor.CentreLeft, - RelativeSizeAxes = Axes.X, - Height = lineWidth, - Y = 0, - }, - new Box - { - Colour = Colour4.White, - Alpha = 0.3f, - Origin = Anchor.CentreLeft, - RelativeSizeAxes = Axes.X, - Height = lineWidth, - Y = drawSize.Y, - }, - new Box - { - Colour = Colour4.White, - Alpha = 0.3f, - Origin = Anchor.TopCentre, - RelativeSizeAxes = Axes.Y, - Width = lineWidth, - X = 0, - }, - new Box - { - Colour = Colour4.White, - Alpha = 0.3f, - Origin = Anchor.TopCentre, - RelativeSizeAxes = Axes.Y, - Width = lineWidth, - X = drawSize.X, - }, - }); - } - - public abstract Vector2 GetSnappedPosition(Vector2 original); } } diff --git a/osu.Game/Screens/Edit/Compose/Components/PositionSnapGrid.cs b/osu.Game/Screens/Edit/Compose/Components/PositionSnapGrid.cs new file mode 100644 index 0000000000..dd412e3cd3 --- /dev/null +++ b/osu.Game/Screens/Edit/Compose/Components/PositionSnapGrid.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.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Layout; +using osuTK; + +namespace osu.Game.Screens.Edit.Compose.Components +{ + public abstract partial class PositionSnapGrid : CompositeDrawable + { + private Vector2 startPosition; + + /// + /// The position of the origin of this in local coordinates. + /// + public Vector2 StartPosition + { + get => startPosition; + set + { + startPosition = value; + GridCache.Invalidate(); + } + } + + protected readonly LayoutValue GridCache = new LayoutValue(Invalidation.RequiredParentSizeToFit); + + protected PositionSnapGrid() + { + Masking = true; + + AddLayout(GridCache); + } + + protected override void Update() + { + base.Update(); + + if (GridCache.IsValid) return; + + ClearInternal(); + + if (DrawWidth > 0 && DrawHeight > 0) + CreateContent(); + + GridCache.Validate(); + } + + protected abstract void CreateContent(); + + protected void GenerateOutline(Vector2 drawSize) + { + // Make lines the same width independent of display resolution. + float lineWidth = DrawWidth / ScreenSpaceDrawQuad.Width; + + AddRangeInternal(new[] + { + new Box + { + Colour = Colour4.White, + Alpha = 0.3f, + Origin = Anchor.CentreLeft, + RelativeSizeAxes = Axes.X, + Height = lineWidth, + Y = 0, + }, + new Box + { + Colour = Colour4.White, + Alpha = 0.3f, + Origin = Anchor.CentreLeft, + RelativeSizeAxes = Axes.X, + Height = lineWidth, + Y = drawSize.Y, + }, + new Box + { + Colour = Colour4.White, + Alpha = 0.3f, + Origin = Anchor.TopCentre, + RelativeSizeAxes = Axes.Y, + Width = lineWidth, + X = 0, + }, + new Box + { + Colour = Colour4.White, + Alpha = 0.3f, + Origin = Anchor.TopCentre, + RelativeSizeAxes = Axes.Y, + Width = lineWidth, + X = drawSize.X, + }, + }); + } + + public abstract Vector2 GetSnappedPosition(Vector2 original); + } +} From 847f04e63a0fb9b58bcea0ca35ea9d61880a5528 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Fri, 29 Dec 2023 17:32:09 +0100 Subject: [PATCH 0150/2493] reduce opacity of middle cardinal lines --- .../Screens/Edit/Compose/Components/LinedPositionSnapGrid.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/LinedPositionSnapGrid.cs b/osu.Game/Screens/Edit/Compose/Components/LinedPositionSnapGrid.cs index 9ab12e4b71..94da9c5b84 100644 --- a/osu.Game/Screens/Edit/Compose/Components/LinedPositionSnapGrid.cs +++ b/osu.Game/Screens/Edit/Compose/Components/LinedPositionSnapGrid.cs @@ -48,7 +48,7 @@ namespace osu.Game.Screens.Edit.Compose.Components if (generatedLines.Count == 0) return; - generatedLines.First().Alpha = 0.3f; + generatedLines.First().Alpha = 0.2f; AddRangeInternal(generatedLines); } From d0ca3f2b2b086ddbfae4ed5f9d6e44b471804722 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Fri, 29 Dec 2023 18:24:05 +0100 Subject: [PATCH 0151/2493] Add circular grid --- .../Edit/OsuGridToolboxGroup.cs | 6 +- .../Edit/OsuHitObjectComposer.cs | 8 ++ .../Components/CircularPositionSnapGrid.cs | 106 ++++++++++++++++++ 3 files changed, 119 insertions(+), 1 deletion(-) create mode 100644 osu.Game/Screens/Edit/Compose/Components/CircularPositionSnapGrid.cs diff --git a/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs b/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs index 892c89ebc2..0013f23057 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs @@ -100,7 +100,10 @@ namespace osu.Game.Rulesets.Osu.Edit () => new SpriteIcon { Icon = FontAwesome.Regular.Square }), new RadioButton("Triangle", () => GridType.Value = PositionSnapGridType.Triangle, - () => new Triangle()) + () => new Triangle()), + new RadioButton("Circle", + () => GridType.Value = PositionSnapGridType.Circle, + () => new SpriteIcon { Icon = FontAwesome.Regular.Circle }), } }, }; @@ -177,5 +180,6 @@ namespace osu.Game.Rulesets.Osu.Edit { Square, Triangle, + Circle, } } diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs index b0ac8467c1..1e7d07dbdd 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs @@ -135,6 +135,14 @@ namespace osu.Game.Rulesets.Osu.Edit positionSnapGrid = triangularPositionSnapGrid; break; + case PositionSnapGridType.Circle: + var circularPositionSnapGrid = new CircularPositionSnapGrid(); + + OsuGridToolboxGroup.Spacing.BindValueChanged(s => circularPositionSnapGrid.Spacing = s.NewValue, true); + + positionSnapGrid = circularPositionSnapGrid; + break; + default: throw new NotImplementedException($"{OsuGridToolboxGroup.GridType} has an incorrect value."); } diff --git a/osu.Game/Screens/Edit/Compose/Components/CircularPositionSnapGrid.cs b/osu.Game/Screens/Edit/Compose/Components/CircularPositionSnapGrid.cs new file mode 100644 index 0000000000..b9b0cbe389 --- /dev/null +++ b/osu.Game/Screens/Edit/Compose/Components/CircularPositionSnapGrid.cs @@ -0,0 +1,106 @@ +// 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.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Utils; +using osu.Game.Utils; +using osuTK; + +namespace osu.Game.Screens.Edit.Compose.Components +{ + public partial class CircularPositionSnapGrid : PositionSnapGrid + { + private float spacing = 1; + + /// + /// The spacing between grid lines of this . + /// + public float Spacing + { + get => spacing; + set + { + if (spacing <= 0) + throw new ArgumentException("Grid spacing must be positive."); + + spacing = value; + GridCache.Invalidate(); + } + } + + protected override void CreateContent() + { + var drawSize = DrawSize; + + // Calculate the maximum distance from the origin to the edge of the grid. + float maxDist = MathF.Max( + MathF.Max(StartPosition.Length, (StartPosition - drawSize).Length), + MathF.Max((StartPosition - new Vector2(drawSize.X, 0)).Length, (StartPosition - new Vector2(0, drawSize.Y)).Length) + ); + + generateCircles((int)(maxDist / Spacing) + 1); + + GenerateOutline(drawSize); + } + + private void generateCircles(int count) + { + // Make lines the same width independent of display resolution. + float lineWidth = 2 * DrawWidth / ScreenSpaceDrawQuad.Width; + + List generatedCircles = new List(); + + for (int i = 0; i < count; i++) + { + // Add a minimum diameter so the center circle is clearly visible. + float diameter = MathF.Max(lineWidth * 1.5f, i * Spacing * 2); + + var gridCircle = new CircularContainer + { + BorderColour = Colour4.White, + BorderThickness = lineWidth, + Alpha = 0.2f, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.None, + Width = diameter, + Height = diameter, + Position = StartPosition, + Masking = true, + Child = new Box + { + RelativeSizeAxes = Axes.Both, + AlwaysPresent = true, + Alpha = 0f, + } + }; + + generatedCircles.Add(gridCircle); + } + + if (generatedCircles.Count == 0) + return; + + generatedCircles.First().Alpha = 0.8f; + + AddRangeInternal(generatedCircles); + } + + public override Vector2 GetSnappedPosition(Vector2 original) + { + Vector2 relativeToStart = original - StartPosition; + + if (relativeToStart.LengthSquared < Precision.FLOAT_EPSILON) + return StartPosition; + + float length = relativeToStart.Length; + float wantedLength = MathF.Round(length / Spacing) * Spacing; + + return StartPosition + Vector2.Multiply(relativeToStart, wantedLength / length); + } + } +} From f649fa106f5de94592ff6d2ba55684625451bde3 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Sat, 30 Dec 2023 00:43:41 +0100 Subject: [PATCH 0152/2493] Added bindables and binding with BindTo --- .../Editor/TestSceneOsuEditorGrids.cs | 2 +- .../Edit/OsuGridToolboxGroup.cs | 28 +++++++++++ .../Edit/OsuHitObjectComposer.cs | 23 +++------ .../Components/CircularPositionSnapGrid.cs | 37 ++++++-------- .../Components/LinedPositionSnapGrid.cs | 4 +- .../Compose/Components/PositionSnapGrid.cs | 15 ++---- .../Components/RectangularPositionSnapGrid.cs | 46 ++++++----------- .../Components/TriangularPositionSnapGrid.cs | 49 +++++++------------ 8 files changed, 92 insertions(+), 112 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorGrids.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorGrids.cs index 299db23ccc..ff406b1b88 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorGrids.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorGrids.cs @@ -100,7 +100,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor } private void gridSizeIs(int size) - => AddAssert($"grid size is {size}", () => this.ChildrenOfType().Single().Spacing == new Vector2(size) + => AddAssert($"grid size is {size}", () => this.ChildrenOfType().Single().Spacing.Value == new Vector2(size) && EditorBeatmap.BeatmapInfo.GridSize == size); } } diff --git a/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs b/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs index 0013f23057..46e43deaae 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs @@ -16,6 +16,7 @@ using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Osu.UI; using osu.Game.Screens.Edit; using osu.Game.Screens.Edit.Components.RadioButtons; +using osuTK; namespace osu.Game.Rulesets.Osu.Edit { @@ -28,6 +29,9 @@ namespace osu.Game.Rulesets.Osu.Edit [Resolved] private EditorBeatmap editorBeatmap { get; set; } = null!; + /// + /// X position of the grid's origin. + /// public BindableFloat StartPositionX { get; } = new BindableFloat(OsuPlayfield.BASE_SIZE.X / 2) { MinValue = 0f, @@ -35,6 +39,9 @@ namespace osu.Game.Rulesets.Osu.Edit Precision = 1f }; + /// + /// Y position of the grid's origin. + /// public BindableFloat StartPositionY { get; } = new BindableFloat(OsuPlayfield.BASE_SIZE.Y / 2) { MinValue = 0f, @@ -42,6 +49,9 @@ namespace osu.Game.Rulesets.Osu.Edit Precision = 1f }; + /// + /// The spacing between grid lines. + /// public BindableFloat Spacing { get; } = new BindableFloat(4f) { MinValue = 4f, @@ -49,6 +59,9 @@ namespace osu.Game.Rulesets.Osu.Edit Precision = 1f }; + /// + /// Rotation of the grid lines in degrees. + /// public BindableFloat GridLinesRotation { get; } = new BindableFloat(0f) { MinValue = -45f, @@ -56,6 +69,18 @@ namespace osu.Game.Rulesets.Osu.Edit Precision = 1f }; + /// + /// Read-only bindable representing the grid's origin. + /// Equivalent to new Vector2(StartPositionX, StartPositionY) + /// + public Bindable StartPosition { get; } = new Bindable(); + + /// + /// Read-only bindable representing the grid's spacing in both the X and Y dimension. + /// Equivalent to new Vector2(Spacing) + /// + public Bindable SpacingVector { get; } = new Bindable(); + public Bindable GridType { get; } = new Bindable(); private ExpandableSlider startPositionXSlider = null!; @@ -124,18 +149,21 @@ namespace osu.Game.Rulesets.Osu.Edit { startPositionXSlider.ContractedLabelText = $"X: {x.NewValue:N0}"; startPositionXSlider.ExpandedLabelText = $"X Offset: {x.NewValue:N0}"; + StartPosition.Value = new Vector2(x.NewValue, StartPosition.Value.Y); }, true); StartPositionY.BindValueChanged(y => { startPositionYSlider.ContractedLabelText = $"Y: {y.NewValue:N0}"; startPositionYSlider.ExpandedLabelText = $"Y Offset: {y.NewValue:N0}"; + StartPosition.Value = new Vector2(StartPosition.Value.X, y.NewValue); }, true); Spacing.BindValueChanged(spacing => { spacingSlider.ContractedLabelText = $"S: {spacing.NewValue:N0}"; spacingSlider.ExpandedLabelText = $"Spacing: {spacing.NewValue:N0}"; + SpacingVector.Value = new Vector2(spacing.NewValue); }, true); GridLinesRotation.BindValueChanged(rotation => diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs index 1e7d07dbdd..84d5adbc52 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs @@ -120,8 +120,8 @@ namespace osu.Game.Rulesets.Osu.Edit case PositionSnapGridType.Square: var rectangularPositionSnapGrid = new RectangularPositionSnapGrid(); - OsuGridToolboxGroup.Spacing.BindValueChanged(s => rectangularPositionSnapGrid.Spacing = new Vector2(s.NewValue), true); - OsuGridToolboxGroup.GridLinesRotation.BindValueChanged(r => rectangularPositionSnapGrid.GridLineRotation = r.NewValue, true); + rectangularPositionSnapGrid.Spacing.BindTo(OsuGridToolboxGroup.SpacingVector); + rectangularPositionSnapGrid.GridLineRotation.BindTo(OsuGridToolboxGroup.GridLinesRotation); positionSnapGrid = rectangularPositionSnapGrid; break; @@ -129,8 +129,8 @@ namespace osu.Game.Rulesets.Osu.Edit case PositionSnapGridType.Triangle: var triangularPositionSnapGrid = new TriangularPositionSnapGrid(); - OsuGridToolboxGroup.Spacing.BindValueChanged(s => triangularPositionSnapGrid.Spacing = s.NewValue, true); - OsuGridToolboxGroup.GridLinesRotation.BindValueChanged(r => triangularPositionSnapGrid.GridLineRotation = r.NewValue, true); + triangularPositionSnapGrid.Spacing.BindTo(OsuGridToolboxGroup.Spacing); + triangularPositionSnapGrid.GridLineRotation.BindTo(OsuGridToolboxGroup.GridLinesRotation); positionSnapGrid = triangularPositionSnapGrid; break; @@ -138,7 +138,7 @@ namespace osu.Game.Rulesets.Osu.Edit case PositionSnapGridType.Circle: var circularPositionSnapGrid = new CircularPositionSnapGrid(); - OsuGridToolboxGroup.Spacing.BindValueChanged(s => circularPositionSnapGrid.Spacing = s.NewValue, true); + circularPositionSnapGrid.Spacing.BindTo(OsuGridToolboxGroup.Spacing); positionSnapGrid = circularPositionSnapGrid; break; @@ -147,18 +147,11 @@ namespace osu.Game.Rulesets.Osu.Edit throw new NotImplementedException($"{OsuGridToolboxGroup.GridType} has an incorrect value."); } - bindPositionSnapGridStartPosition(positionSnapGrid); + // Bind the start position to the toolbox sliders. + positionSnapGrid.StartPosition.BindTo(OsuGridToolboxGroup.StartPosition); + positionSnapGrid.RelativeSizeAxes = Axes.Both; LayerBelowRuleset.Add(positionSnapGrid); - return; - - void bindPositionSnapGridStartPosition(PositionSnapGrid snapGrid) - { - OsuGridToolboxGroup.StartPositionX.BindValueChanged(x => - snapGrid.StartPosition = new Vector2(x.NewValue, snapGrid.StartPosition.Y), true); - OsuGridToolboxGroup.StartPositionY.BindValueChanged(y => - snapGrid.StartPosition = new Vector2(snapGrid.StartPosition.X, y.NewValue), true); - } } protected override ComposeBlueprintContainer CreateBlueprintContainer() diff --git a/osu.Game/Screens/Edit/Compose/Components/CircularPositionSnapGrid.cs b/osu.Game/Screens/Edit/Compose/Components/CircularPositionSnapGrid.cs index b9b0cbe389..403a270359 100644 --- a/osu.Game/Screens/Edit/Compose/Components/CircularPositionSnapGrid.cs +++ b/osu.Game/Screens/Edit/Compose/Components/CircularPositionSnapGrid.cs @@ -4,33 +4,28 @@ using System; using System.Collections.Generic; using System.Linq; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Utils; -using osu.Game.Utils; using osuTK; namespace osu.Game.Screens.Edit.Compose.Components { public partial class CircularPositionSnapGrid : PositionSnapGrid { - private float spacing = 1; - /// /// The spacing between grid lines of this . /// - public float Spacing + public BindableFloat Spacing { get; } = new BindableFloat(1f) { - get => spacing; - set - { - if (spacing <= 0) - throw new ArgumentException("Grid spacing must be positive."); + MinValue = 0f, + }; - spacing = value; - GridCache.Invalidate(); - } + public CircularPositionSnapGrid() + { + Spacing.BindValueChanged(_ => GridCache.Invalidate()); } protected override void CreateContent() @@ -39,11 +34,11 @@ namespace osu.Game.Screens.Edit.Compose.Components // Calculate the maximum distance from the origin to the edge of the grid. float maxDist = MathF.Max( - MathF.Max(StartPosition.Length, (StartPosition - drawSize).Length), - MathF.Max((StartPosition - new Vector2(drawSize.X, 0)).Length, (StartPosition - new Vector2(0, drawSize.Y)).Length) + MathF.Max(StartPosition.Value.Length, (StartPosition.Value - drawSize).Length), + MathF.Max((StartPosition.Value - new Vector2(drawSize.X, 0)).Length, (StartPosition.Value - new Vector2(0, drawSize.Y)).Length) ); - generateCircles((int)(maxDist / Spacing) + 1); + generateCircles((int)(maxDist / Spacing.Value) + 1); GenerateOutline(drawSize); } @@ -58,7 +53,7 @@ namespace osu.Game.Screens.Edit.Compose.Components for (int i = 0; i < count; i++) { // Add a minimum diameter so the center circle is clearly visible. - float diameter = MathF.Max(lineWidth * 1.5f, i * Spacing * 2); + float diameter = MathF.Max(lineWidth * 1.5f, i * Spacing.Value * 2); var gridCircle = new CircularContainer { @@ -69,7 +64,7 @@ namespace osu.Game.Screens.Edit.Compose.Components RelativeSizeAxes = Axes.None, Width = diameter, Height = diameter, - Position = StartPosition, + Position = StartPosition.Value, Masking = true, Child = new Box { @@ -92,15 +87,15 @@ namespace osu.Game.Screens.Edit.Compose.Components public override Vector2 GetSnappedPosition(Vector2 original) { - Vector2 relativeToStart = original - StartPosition; + Vector2 relativeToStart = original - StartPosition.Value; if (relativeToStart.LengthSquared < Precision.FLOAT_EPSILON) - return StartPosition; + return StartPosition.Value; float length = relativeToStart.Length; - float wantedLength = MathF.Round(length / Spacing) * Spacing; + float wantedLength = MathF.Round(length / Spacing.Value) * Spacing.Value; - return StartPosition + Vector2.Multiply(relativeToStart, wantedLength / length); + return StartPosition.Value + Vector2.Multiply(relativeToStart, wantedLength / length); } } } diff --git a/osu.Game/Screens/Edit/Compose/Components/LinedPositionSnapGrid.cs b/osu.Game/Screens/Edit/Compose/Components/LinedPositionSnapGrid.cs index 94da9c5b84..ebdd76a4e2 100644 --- a/osu.Game/Screens/Edit/Compose/Components/LinedPositionSnapGrid.cs +++ b/osu.Game/Screens/Edit/Compose/Components/LinedPositionSnapGrid.cs @@ -16,7 +16,7 @@ namespace osu.Game.Screens.Edit.Compose.Components protected void GenerateGridLines(Vector2 step, Vector2 drawSize) { int index = 0; - var currentPosition = StartPosition; + var currentPosition = StartPosition.Value; // Make lines the same width independent of display resolution. float lineWidth = DrawWidth / ScreenSpaceDrawQuad.Width; @@ -42,7 +42,7 @@ namespace osu.Game.Screens.Edit.Compose.Components generatedLines.Add(gridLine); index += 1; - currentPosition = StartPosition + index * step; + currentPosition = StartPosition.Value + index * step; } if (generatedLines.Count == 0) diff --git a/osu.Game/Screens/Edit/Compose/Components/PositionSnapGrid.cs b/osu.Game/Screens/Edit/Compose/Components/PositionSnapGrid.cs index dd412e3cd3..36687ef73a 100644 --- a/osu.Game/Screens/Edit/Compose/Components/PositionSnapGrid.cs +++ b/osu.Game/Screens/Edit/Compose/Components/PositionSnapGrid.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.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; @@ -11,20 +12,10 @@ namespace osu.Game.Screens.Edit.Compose.Components { public abstract partial class PositionSnapGrid : CompositeDrawable { - private Vector2 startPosition; - /// /// The position of the origin of this in local coordinates. /// - public Vector2 StartPosition - { - get => startPosition; - set - { - startPosition = value; - GridCache.Invalidate(); - } - } + public Bindable StartPosition { get; } = new Bindable(Vector2.Zero); protected readonly LayoutValue GridCache = new LayoutValue(Invalidation.RequiredParentSizeToFit); @@ -32,6 +23,8 @@ namespace osu.Game.Screens.Edit.Compose.Components { Masking = true; + StartPosition.BindValueChanged(_ => GridCache.Invalidate()); + AddLayout(GridCache); } diff --git a/osu.Game/Screens/Edit/Compose/Components/RectangularPositionSnapGrid.cs b/osu.Game/Screens/Edit/Compose/Components/RectangularPositionSnapGrid.cs index 2392921203..3bf0ef8ac3 100644 --- a/osu.Game/Screens/Edit/Compose/Components/RectangularPositionSnapGrid.cs +++ b/osu.Game/Screens/Edit/Compose/Components/RectangularPositionSnapGrid.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using osu.Framework.Bindables; using osu.Game.Utils; using osuTK; @@ -9,60 +10,43 @@ namespace osu.Game.Screens.Edit.Compose.Components { public partial class RectangularPositionSnapGrid : LinedPositionSnapGrid { - private Vector2 spacing = Vector2.One; - /// /// The spacing between grid lines of this . /// - public Vector2 Spacing - { - get => spacing; - set - { - if (spacing.X <= 0 || spacing.Y <= 0) - throw new ArgumentException("Grid spacing must be positive."); - - spacing = value; - GridCache.Invalidate(); - } - } - - private float gridLineRotation; + public Bindable Spacing { get; } = new Bindable(Vector2.One); /// /// The rotation in degrees of the grid lines of this . /// - public float GridLineRotation + public BindableFloat GridLineRotation { get; } = new BindableFloat(); + + public RectangularPositionSnapGrid() { - get => gridLineRotation; - set - { - gridLineRotation = value; - GridCache.Invalidate(); - } + Spacing.BindValueChanged(_ => GridCache.Invalidate()); + GridLineRotation.BindValueChanged(_ => GridCache.Invalidate()); } protected override void CreateContent() { var drawSize = DrawSize; - var rot = Quaternion.FromAxisAngle(Vector3.UnitZ, MathHelper.DegreesToRadians(GridLineRotation)); + var rot = Quaternion.FromAxisAngle(Vector3.UnitZ, MathHelper.DegreesToRadians(GridLineRotation.Value)); - GenerateGridLines(Vector2.Transform(new Vector2(0, -Spacing.Y), rot), drawSize); - GenerateGridLines(Vector2.Transform(new Vector2(0, Spacing.Y), rot), drawSize); + GenerateGridLines(Vector2.Transform(new Vector2(0, -Spacing.Value.Y), rot), drawSize); + GenerateGridLines(Vector2.Transform(new Vector2(0, Spacing.Value.Y), rot), drawSize); - GenerateGridLines(Vector2.Transform(new Vector2(-Spacing.X, 0), rot), drawSize); - GenerateGridLines(Vector2.Transform(new Vector2(Spacing.X, 0), rot), drawSize); + GenerateGridLines(Vector2.Transform(new Vector2(-Spacing.Value.X, 0), rot), drawSize); + GenerateGridLines(Vector2.Transform(new Vector2(Spacing.Value.X, 0), rot), drawSize); GenerateOutline(drawSize); } public override Vector2 GetSnappedPosition(Vector2 original) { - Vector2 relativeToStart = GeometryUtils.RotateVector(original - StartPosition, GridLineRotation); - Vector2 offset = Vector2.Divide(relativeToStart, Spacing); + Vector2 relativeToStart = GeometryUtils.RotateVector(original - StartPosition.Value, GridLineRotation.Value); + Vector2 offset = Vector2.Divide(relativeToStart, Spacing.Value); Vector2 roundedOffset = new Vector2(MathF.Round(offset.X), MathF.Round(offset.Y)); - return StartPosition + GeometryUtils.RotateVector(Vector2.Multiply(roundedOffset, Spacing), -GridLineRotation); + return StartPosition.Value + GeometryUtils.RotateVector(Vector2.Multiply(roundedOffset, Spacing.Value), -GridLineRotation.Value); } } } diff --git a/osu.Game/Screens/Edit/Compose/Components/TriangularPositionSnapGrid.cs b/osu.Game/Screens/Edit/Compose/Components/TriangularPositionSnapGrid.cs index c98890e294..93d2c6a74a 100644 --- a/osu.Game/Screens/Edit/Compose/Components/TriangularPositionSnapGrid.cs +++ b/osu.Game/Screens/Edit/Compose/Components/TriangularPositionSnapGrid.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using osu.Framework.Bindables; using osu.Game.Utils; using osuTK; @@ -9,37 +10,23 @@ namespace osu.Game.Screens.Edit.Compose.Components { public partial class TriangularPositionSnapGrid : LinedPositionSnapGrid { - private float spacing = 1; - /// /// The spacing between grid lines of this . /// - public float Spacing + public BindableFloat Spacing { get; } = new BindableFloat(1f) { - get => spacing; - set - { - if (spacing <= 0) - throw new ArgumentException("Grid spacing must be positive."); - - spacing = value; - GridCache.Invalidate(); - } - } - - private float gridLineRotation; + MinValue = 0f, + }; /// /// The rotation in degrees of the grid lines of this . /// - public float GridLineRotation + public BindableFloat GridLineRotation { get; } = new BindableFloat(); + + public TriangularPositionSnapGrid() { - get => gridLineRotation; - set - { - gridLineRotation = value; - GridCache.Invalidate(); - } + Spacing.BindValueChanged(_ => GridCache.Invalidate()); + GridLineRotation.BindValueChanged(_ => GridCache.Invalidate()); } private const float sqrt3 = 1.73205080757f; @@ -49,10 +36,10 @@ namespace osu.Game.Screens.Edit.Compose.Components protected override void CreateContent() { var drawSize = DrawSize; - float stepSpacing = Spacing * sqrt3_over2; - var step1 = GeometryUtils.RotateVector(new Vector2(stepSpacing, 0), -GridLineRotation - 30); - var step2 = GeometryUtils.RotateVector(new Vector2(stepSpacing, 0), -GridLineRotation - 90); - var step3 = GeometryUtils.RotateVector(new Vector2(stepSpacing, 0), -GridLineRotation - 150); + float stepSpacing = Spacing.Value * sqrt3_over2; + var step1 = GeometryUtils.RotateVector(new Vector2(stepSpacing, 0), -GridLineRotation.Value - 30); + var step2 = GeometryUtils.RotateVector(new Vector2(stepSpacing, 0), -GridLineRotation.Value - 90); + var step3 = GeometryUtils.RotateVector(new Vector2(stepSpacing, 0), -GridLineRotation.Value - 150); GenerateGridLines(step1, drawSize); GenerateGridLines(-step1, drawSize); @@ -68,16 +55,16 @@ namespace osu.Game.Screens.Edit.Compose.Components public override Vector2 GetSnappedPosition(Vector2 original) { - Vector2 relativeToStart = GeometryUtils.RotateVector(original - StartPosition, GridLineRotation); + Vector2 relativeToStart = GeometryUtils.RotateVector(original - StartPosition.Value, GridLineRotation.Value); Vector2 hex = pixelToHex(relativeToStart); - return StartPosition + GeometryUtils.RotateVector(hexToPixel(hex), -GridLineRotation); + return StartPosition.Value + GeometryUtils.RotateVector(hexToPixel(hex), -GridLineRotation.Value); } private Vector2 pixelToHex(Vector2 pixel) { - float x = pixel.X / Spacing; - float y = pixel.Y / Spacing; + float x = pixel.X / Spacing.Value; + float y = pixel.Y / Spacing.Value; // Algorithm from Charles Chambers // with modifications and comments by Chris Cox 2023 // @@ -96,7 +83,7 @@ namespace osu.Game.Screens.Edit.Compose.Components { // Taken from // with modifications for the different definition of size. - return new Vector2(Spacing * (hex.X - hex.Y / 2), Spacing * one_over_sqrt3 * 1.5f * hex.Y); + return new Vector2(Spacing.Value * (hex.X - hex.Y / 2), Spacing.Value * one_over_sqrt3 * 1.5f * hex.Y); } } } From 1c75357d77fba3723476d2bd1657e1d1fdd45efd Mon Sep 17 00:00:00 2001 From: OliBomby Date: Sat, 30 Dec 2023 01:12:23 +0100 Subject: [PATCH 0153/2493] fix compile --- osu.Game.Tournament/Screens/Editors/LadderEditorScreen.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tournament/Screens/Editors/LadderEditorScreen.cs b/osu.Game.Tournament/Screens/Editors/LadderEditorScreen.cs index ad00e8d47d..a7f0a52003 100644 --- a/osu.Game.Tournament/Screens/Editors/LadderEditorScreen.cs +++ b/osu.Game.Tournament/Screens/Editors/LadderEditorScreen.cs @@ -53,13 +53,14 @@ namespace osu.Game.Tournament.Screens.Editors ScrollContent.Add(grid = new RectangularPositionSnapGrid { - Spacing = new Vector2(GRID_SPACING), Anchor = Anchor.Centre, Origin = Anchor.Centre, BypassAutoSizeAxes = Axes.Both, Depth = float.MaxValue }); + grid.Spacing.Value = new Vector2(GRID_SPACING); + LadderInfo.Matches.CollectionChanged += (_, _) => updateMessage(); updateMessage(); } From 9a8c41f6ca8a4e1df4d3f98406126d40858ed419 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Sat, 30 Dec 2023 14:32:20 +0100 Subject: [PATCH 0154/2493] Saving exact grid spacing --- osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs b/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs index 46e43deaae..442575711a 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs @@ -133,10 +133,10 @@ namespace osu.Game.Rulesets.Osu.Edit }, }; + Spacing.Value = editorBeatmap.BeatmapInfo.GridSize; int gridSizeIndex = Array.IndexOf(grid_sizes, editorBeatmap.BeatmapInfo.GridSize); if (gridSizeIndex >= 0) currentGridSizeIndex = gridSizeIndex; - updateSpacing(); } protected override void LoadComplete() @@ -164,6 +164,7 @@ namespace osu.Game.Rulesets.Osu.Edit spacingSlider.ContractedLabelText = $"S: {spacing.NewValue:N0}"; spacingSlider.ExpandedLabelText = $"Spacing: {spacing.NewValue:N0}"; SpacingVector.Value = new Vector2(spacing.NewValue); + editorBeatmap.BeatmapInfo.GridSize = (int)spacing.NewValue; }, true); GridLinesRotation.BindValueChanged(rotation => @@ -176,15 +177,7 @@ namespace osu.Game.Rulesets.Osu.Edit private void nextGridSize() { currentGridSizeIndex = (currentGridSizeIndex + 1) % grid_sizes.Length; - updateSpacing(); - } - - private void updateSpacing() - { - int gridSize = grid_sizes[currentGridSizeIndex]; - - editorBeatmap.BeatmapInfo.GridSize = gridSize; - Spacing.Value = gridSize; + Spacing.Value = grid_sizes[currentGridSizeIndex]; } public bool OnPressed(KeyBindingPressEvent e) From 493e3a5f7a2cf9b355cf8319abe16b12c069f92d Mon Sep 17 00:00:00 2001 From: OliBomby Date: Sun, 31 Dec 2023 02:55:47 +0100 Subject: [PATCH 0155/2493] use G to change grid type --- .../Edit/OsuGridToolboxGroup.cs | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs b/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs index 442575711a..c07e8028b6 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs @@ -22,9 +22,9 @@ namespace osu.Game.Rulesets.Osu.Edit { public partial class OsuGridToolboxGroup : EditorToolboxGroup, IKeyBindingHandler { - private static readonly int[] grid_sizes = { 4, 8, 16, 32 }; + private static readonly PositionSnapGridType[] grid_types = Enum.GetValues(typeof(PositionSnapGridType)).Cast().ToArray(); - private int currentGridSizeIndex = grid_sizes.Length - 1; + private int currentGridTypeIndex; [Resolved] private EditorBeatmap editorBeatmap { get; set; } = null!; @@ -134,9 +134,6 @@ namespace osu.Game.Rulesets.Osu.Edit }; Spacing.Value = editorBeatmap.BeatmapInfo.GridSize; - int gridSizeIndex = Array.IndexOf(grid_sizes, editorBeatmap.BeatmapInfo.GridSize); - if (gridSizeIndex >= 0) - currentGridSizeIndex = gridSizeIndex; } protected override void LoadComplete() @@ -174,10 +171,11 @@ namespace osu.Game.Rulesets.Osu.Edit }, true); } - private void nextGridSize() + private void nextGridType() { - currentGridSizeIndex = (currentGridSizeIndex + 1) % grid_sizes.Length; - Spacing.Value = grid_sizes[currentGridSizeIndex]; + currentGridTypeIndex = (currentGridTypeIndex + 1) % grid_types.Length; + GridType.Value = grid_types[currentGridTypeIndex]; + gridTypeButtons.Items[currentGridTypeIndex].Select(); } public bool OnPressed(KeyBindingPressEvent e) @@ -185,7 +183,7 @@ namespace osu.Game.Rulesets.Osu.Edit switch (e.Action) { case GlobalAction.EditorCycleGridDisplayMode: - nextGridSize(); + nextGridType(); return true; } From e47d570e68d155375833f9a06f71820fabd7af47 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Sun, 31 Dec 2023 03:53:42 +0100 Subject: [PATCH 0156/2493] improve UI --- .../Edit/OsuGridToolboxGroup.cs | 38 ++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs b/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs index c07e8028b6..da5849a77a 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs @@ -6,10 +6,12 @@ using System.Linq; 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.Input.Bindings; using osu.Framework.Input.Events; +using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osu.Game.Input.Bindings; using osu.Game.Rulesets.Edit; @@ -17,6 +19,7 @@ using osu.Game.Rulesets.Osu.UI; using osu.Game.Screens.Edit; using osu.Game.Screens.Edit.Components.RadioButtons; using osuTK; +using osuTK.Graphics; namespace osu.Game.Rulesets.Osu.Edit { @@ -29,6 +32,9 @@ namespace osu.Game.Rulesets.Osu.Edit [Resolved] private EditorBeatmap editorBeatmap { get; set; } = null!; + [Resolved] + private IExpandingContainer? expandingContainer { get; set; } + /// /// X position of the grid's origin. /// @@ -125,7 +131,7 @@ namespace osu.Game.Rulesets.Osu.Edit () => new SpriteIcon { Icon = FontAwesome.Regular.Square }), new RadioButton("Triangle", () => GridType.Value = PositionSnapGridType.Triangle, - () => new Triangle()), + () => new OutlineTriangle(true, 20)), new RadioButton("Circle", () => GridType.Value = PositionSnapGridType.Circle, () => new SpriteIcon { Icon = FontAwesome.Regular.Circle }), @@ -136,6 +142,36 @@ namespace osu.Game.Rulesets.Osu.Edit Spacing.Value = editorBeatmap.BeatmapInfo.GridSize; } + public partial class OutlineTriangle : BufferedContainer + { + public OutlineTriangle(bool outlineOnly, float size) + : base(cachedFrameBuffer: true) + { + Size = new Vector2(size); + + InternalChildren = new Drawable[] + { + new EquilateralTriangle { RelativeSizeAxes = Axes.Both }, + }; + + if (outlineOnly) + { + AddInternal(new EquilateralTriangle + { + Anchor = Anchor.TopCentre, + Origin = Anchor.Centre, + RelativePositionAxes = Axes.Y, + Y = 0.48f, + Colour = Color4.Black, + Size = new Vector2(size - 7), + Blending = BlendingParameters.None, + }); + } + + Blending = BlendingParameters.Additive; + } + } + protected override void LoadComplete() { base.LoadComplete(); From 904ea2e436bf694f5e95f0bf02358bd6668304b1 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Sun, 31 Dec 2023 15:48:09 +0100 Subject: [PATCH 0157/2493] move OutlineTriangle code down --- .../Edit/OsuGridToolboxGroup.cs | 60 +++++++++---------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs b/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs index da5849a77a..72e60a5515 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs @@ -142,36 +142,6 @@ namespace osu.Game.Rulesets.Osu.Edit Spacing.Value = editorBeatmap.BeatmapInfo.GridSize; } - public partial class OutlineTriangle : BufferedContainer - { - public OutlineTriangle(bool outlineOnly, float size) - : base(cachedFrameBuffer: true) - { - Size = new Vector2(size); - - InternalChildren = new Drawable[] - { - new EquilateralTriangle { RelativeSizeAxes = Axes.Both }, - }; - - if (outlineOnly) - { - AddInternal(new EquilateralTriangle - { - Anchor = Anchor.TopCentre, - Origin = Anchor.Centre, - RelativePositionAxes = Axes.Y, - Y = 0.48f, - Colour = Color4.Black, - Size = new Vector2(size - 7), - Blending = BlendingParameters.None, - }); - } - - Blending = BlendingParameters.Additive; - } - } - protected override void LoadComplete() { base.LoadComplete(); @@ -229,6 +199,36 @@ namespace osu.Game.Rulesets.Osu.Edit public void OnReleased(KeyBindingReleaseEvent e) { } + + public partial class OutlineTriangle : BufferedContainer + { + public OutlineTriangle(bool outlineOnly, float size) + : base(cachedFrameBuffer: true) + { + Size = new Vector2(size); + + InternalChildren = new Drawable[] + { + new EquilateralTriangle { RelativeSizeAxes = Axes.Both }, + }; + + if (outlineOnly) + { + AddInternal(new EquilateralTriangle + { + Anchor = Anchor.TopCentre, + Origin = Anchor.Centre, + RelativePositionAxes = Axes.Y, + Y = 0.48f, + Colour = Color4.Black, + Size = new Vector2(size - 7), + Blending = BlendingParameters.None, + }); + } + + Blending = BlendingParameters.Additive; + } + } } public enum PositionSnapGridType From 33e559f83502f84f93d328fea61d9e9fc7d83679 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Sun, 31 Dec 2023 16:41:22 +0100 Subject: [PATCH 0158/2493] add integer keyboard step to sliders --- osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs b/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs index 72e60a5515..6ed7054159 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs @@ -107,19 +107,23 @@ namespace osu.Game.Rulesets.Osu.Edit { startPositionXSlider = new ExpandableSlider { - Current = StartPositionX + Current = StartPositionX, + KeyboardStep = 1, }, startPositionYSlider = new ExpandableSlider { - Current = StartPositionY + Current = StartPositionY, + KeyboardStep = 1, }, spacingSlider = new ExpandableSlider { - Current = Spacing + Current = Spacing, + KeyboardStep = 1, }, gridLinesRotationSlider = new ExpandableSlider { - Current = GridLinesRotation + Current = GridLinesRotation, + KeyboardStep = 1, }, gridTypeButtons = new EditorRadioButtonCollection { From 20e338b8920d33180434ce6c4a7f362118a6fdfe Mon Sep 17 00:00:00 2001 From: OliBomby Date: Sun, 31 Dec 2023 16:45:21 +0100 Subject: [PATCH 0159/2493] also hide grid from points button when not hovered --- .../Edit/OsuGridToolboxGroup.cs | 41 +++++++++++++------ 1 file changed, 28 insertions(+), 13 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs b/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs index 6ed7054159..981148858d 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs @@ -125,20 +125,29 @@ namespace osu.Game.Rulesets.Osu.Edit Current = GridLinesRotation, KeyboardStep = 1, }, - gridTypeButtons = new EditorRadioButtonCollection + new FillFlowContainer { RelativeSizeAxes = Axes.X, - Items = new[] + AutoSizeAxes = Axes.Y, + Spacing = new Vector2(0f, 10f), + Children = new Drawable[] { - new RadioButton("Square", - () => GridType.Value = PositionSnapGridType.Square, - () => new SpriteIcon { Icon = FontAwesome.Regular.Square }), - new RadioButton("Triangle", - () => GridType.Value = PositionSnapGridType.Triangle, - () => new OutlineTriangle(true, 20)), - new RadioButton("Circle", - () => GridType.Value = PositionSnapGridType.Circle, - () => new SpriteIcon { Icon = FontAwesome.Regular.Circle }), + gridTypeButtons = new EditorRadioButtonCollection + { + RelativeSizeAxes = Axes.X, + Items = new[] + { + new RadioButton("Square", + () => GridType.Value = PositionSnapGridType.Square, + () => new SpriteIcon { Icon = FontAwesome.Regular.Square }), + new RadioButton("Triangle", + () => GridType.Value = PositionSnapGridType.Triangle, + () => new OutlineTriangle(true, 20)), + new RadioButton("Circle", + () => GridType.Value = PositionSnapGridType.Circle, + () => new SpriteIcon { Icon = FontAwesome.Regular.Circle }), + } + }, } }, }; @@ -176,8 +185,14 @@ namespace osu.Game.Rulesets.Osu.Edit GridLinesRotation.BindValueChanged(rotation => { - gridLinesRotationSlider.ContractedLabelText = $"R: {rotation.NewValue:N0}"; - gridLinesRotationSlider.ExpandedLabelText = $"Rotation: {rotation.NewValue:N0}"; + gridLinesRotationSlider.ContractedLabelText = $"R: {rotation.NewValue:#,0.##}"; + gridLinesRotationSlider.ExpandedLabelText = $"Rotation: {rotation.NewValue:#,0.##}"; + }, true); + + expandingContainer?.Expanded.BindValueChanged(v => + { + gridTypeButtons.FadeTo(v.NewValue ? 1f : 0f, 500, Easing.OutQuint); + gridTypeButtons.BypassAutoSizeAxes = !v.NewValue ? Axes.Y : Axes.None; }, true); } From 8425c7226c20fcafe961ca4e6c72b3d718dc7105 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Sun, 31 Dec 2023 16:54:04 +0100 Subject: [PATCH 0160/2493] fix rectangular and triangular grid tests --- .../Editing/TestSceneRectangularPositionSnapGrid.cs | 13 ++++++++----- .../Editing/TestSceneTriangularPositionSnapGrid.cs | 12 ++++++++---- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneRectangularPositionSnapGrid.cs b/osu.Game.Tests/Visual/Editing/TestSceneRectangularPositionSnapGrid.cs index a0042cf605..19903737f6 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneRectangularPositionSnapGrid.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneRectangularPositionSnapGrid.cs @@ -52,12 +52,15 @@ namespace osu.Game.Tests.Visual.Editing { RectangularPositionSnapGrid grid = null; - AddStep("create grid", () => Child = grid = new RectangularPositionSnapGrid() + AddStep("create grid", () => { - RelativeSizeAxes = Axes.Both, - StartPosition = position, - Spacing = spacing, - GridLineRotation = rotation + Child = grid = new RectangularPositionSnapGrid + { + RelativeSizeAxes = Axes.Both, + }; + grid.StartPosition.Value = position; + grid.Spacing.Value = spacing; + grid.GridLineRotation.Value = rotation; }); AddStep("add snapping cursor", () => Add(new SnappingCursorContainer diff --git a/osu.Game.Tests/Visual/Editing/TestSceneTriangularPositionSnapGrid.cs b/osu.Game.Tests/Visual/Editing/TestSceneTriangularPositionSnapGrid.cs index 2f5ffd8423..b1f82fa114 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneTriangularPositionSnapGrid.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneTriangularPositionSnapGrid.cs @@ -52,11 +52,15 @@ namespace osu.Game.Tests.Visual.Editing { TriangularPositionSnapGrid grid = null; - AddStep("create grid", () => Child = grid = new TriangularPositionSnapGrid(position) + AddStep("create grid", () => { - RelativeSizeAxes = Axes.Both, - Spacing = spacing, - GridLineRotation = rotation + Child = grid = new TriangularPositionSnapGrid + { + RelativeSizeAxes = Axes.Both, + }; + grid.StartPosition.Value = position; + grid.Spacing.Value = spacing; + grid.GridLineRotation.Value = rotation; }); AddStep("add snapping cursor", () => Add(new SnappingCursorContainer From 31d17994807dd4fe36b2cc73b699753884f21d19 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Sun, 31 Dec 2023 16:56:10 +0100 Subject: [PATCH 0161/2493] Create TestSceneCircularPositionSnapGrid.cs --- .../TestSceneCircularPositionSnapGrid.cs | 111 ++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 osu.Game.Tests/Visual/Editing/TestSceneCircularPositionSnapGrid.cs diff --git a/osu.Game.Tests/Visual/Editing/TestSceneCircularPositionSnapGrid.cs b/osu.Game.Tests/Visual/Editing/TestSceneCircularPositionSnapGrid.cs new file mode 100644 index 0000000000..4481199c94 --- /dev/null +++ b/osu.Game.Tests/Visual/Editing/TestSceneCircularPositionSnapGrid.cs @@ -0,0 +1,111 @@ +// 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 NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Input.Events; +using osu.Game.Screens.Edit.Compose.Components; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Tests.Visual.Editing +{ + public partial class TestSceneCircularPositionSnapGrid : OsuManualInputManagerTestScene + { + private Container content; + protected override Container Content => content; + + [BackgroundDependencyLoader] + private void load() + { + base.Content.AddRange(new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Colour4.Gray + }, + content = new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding(10), + } + }); + } + + private static readonly object[][] test_cases = + { + new object[] { new Vector2(0, 0), 10, 0f }, + new object[] { new Vector2(240, 180), 10, 10f }, + new object[] { new Vector2(160, 120), 30, -10f }, + new object[] { new Vector2(480, 360), 100, 0f }, + }; + + [TestCaseSource(nameof(test_cases))] + public void TestCircularGrid(Vector2 position, float spacing, float rotation) + { + CircularPositionSnapGrid grid = null; + + AddStep("create grid", () => + { + Child = grid = new CircularPositionSnapGrid + { + RelativeSizeAxes = Axes.Both, + }; + grid.StartPosition.Value = position; + grid.Spacing.Value = spacing; + }); + + AddStep("add snapping cursor", () => Add(new SnappingCursorContainer + { + RelativeSizeAxes = Axes.Both, + GetSnapPosition = pos => grid.GetSnappedPosition(grid.ToLocalSpace(pos)) + })); + } + + private partial class SnappingCursorContainer : CompositeDrawable + { + public Func GetSnapPosition; + + private readonly Drawable cursor; + + public SnappingCursorContainer() + { + RelativeSizeAxes = Axes.Both; + + InternalChild = cursor = new Circle + { + Origin = Anchor.Centre, + Size = new Vector2(50), + Colour = Color4.Red + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + updatePosition(GetContainingInputManager().CurrentState.Mouse.Position); + } + + protected override bool OnMouseMove(MouseMoveEvent e) + { + base.OnMouseMove(e); + + updatePosition(e.ScreenSpaceMousePosition); + return true; + } + + private void updatePosition(Vector2 screenSpacePosition) + { + cursor.Position = GetSnapPosition.Invoke(screenSpacePosition); + } + } + } +} From 9796fcff520b52a150cb469c046d13a42a0cc543 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Sun, 31 Dec 2023 16:59:46 +0100 Subject: [PATCH 0162/2493] Merge position snap grid tests into single file --- .../TestSceneCircularPositionSnapGrid.cs | 111 ----------------- ...apGrid.cs => TestScenePositionSnapGrid.cs} | 51 +++++++- .../TestSceneTriangularPositionSnapGrid.cs | 112 ------------------ 3 files changed, 48 insertions(+), 226 deletions(-) delete mode 100644 osu.Game.Tests/Visual/Editing/TestSceneCircularPositionSnapGrid.cs rename osu.Game.Tests/Visual/Editing/{TestSceneRectangularPositionSnapGrid.cs => TestScenePositionSnapGrid.cs} (66%) delete mode 100644 osu.Game.Tests/Visual/Editing/TestSceneTriangularPositionSnapGrid.cs diff --git a/osu.Game.Tests/Visual/Editing/TestSceneCircularPositionSnapGrid.cs b/osu.Game.Tests/Visual/Editing/TestSceneCircularPositionSnapGrid.cs deleted file mode 100644 index 4481199c94..0000000000 --- a/osu.Game.Tests/Visual/Editing/TestSceneCircularPositionSnapGrid.cs +++ /dev/null @@ -1,111 +0,0 @@ -// 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 NUnit.Framework; -using osu.Framework.Allocation; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Input.Events; -using osu.Game.Screens.Edit.Compose.Components; -using osuTK; -using osuTK.Graphics; - -namespace osu.Game.Tests.Visual.Editing -{ - public partial class TestSceneCircularPositionSnapGrid : OsuManualInputManagerTestScene - { - private Container content; - protected override Container Content => content; - - [BackgroundDependencyLoader] - private void load() - { - base.Content.AddRange(new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = Colour4.Gray - }, - content = new Container - { - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding(10), - } - }); - } - - private static readonly object[][] test_cases = - { - new object[] { new Vector2(0, 0), 10, 0f }, - new object[] { new Vector2(240, 180), 10, 10f }, - new object[] { new Vector2(160, 120), 30, -10f }, - new object[] { new Vector2(480, 360), 100, 0f }, - }; - - [TestCaseSource(nameof(test_cases))] - public void TestCircularGrid(Vector2 position, float spacing, float rotation) - { - CircularPositionSnapGrid grid = null; - - AddStep("create grid", () => - { - Child = grid = new CircularPositionSnapGrid - { - RelativeSizeAxes = Axes.Both, - }; - grid.StartPosition.Value = position; - grid.Spacing.Value = spacing; - }); - - AddStep("add snapping cursor", () => Add(new SnappingCursorContainer - { - RelativeSizeAxes = Axes.Both, - GetSnapPosition = pos => grid.GetSnappedPosition(grid.ToLocalSpace(pos)) - })); - } - - private partial class SnappingCursorContainer : CompositeDrawable - { - public Func GetSnapPosition; - - private readonly Drawable cursor; - - public SnappingCursorContainer() - { - RelativeSizeAxes = Axes.Both; - - InternalChild = cursor = new Circle - { - Origin = Anchor.Centre, - Size = new Vector2(50), - Colour = Color4.Red - }; - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - updatePosition(GetContainingInputManager().CurrentState.Mouse.Position); - } - - protected override bool OnMouseMove(MouseMoveEvent e) - { - base.OnMouseMove(e); - - updatePosition(e.ScreenSpaceMousePosition); - return true; - } - - private void updatePosition(Vector2 screenSpacePosition) - { - cursor.Position = GetSnapPosition.Invoke(screenSpacePosition); - } - } - } -} diff --git a/osu.Game.Tests/Visual/Editing/TestSceneRectangularPositionSnapGrid.cs b/osu.Game.Tests/Visual/Editing/TestScenePositionSnapGrid.cs similarity index 66% rename from osu.Game.Tests/Visual/Editing/TestSceneRectangularPositionSnapGrid.cs rename to osu.Game.Tests/Visual/Editing/TestScenePositionSnapGrid.cs index 19903737f6..7e66edc2dd 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneRectangularPositionSnapGrid.cs +++ b/osu.Game.Tests/Visual/Editing/TestScenePositionSnapGrid.cs @@ -16,7 +16,7 @@ using osuTK.Graphics; namespace osu.Game.Tests.Visual.Editing { - public partial class TestSceneRectangularPositionSnapGrid : OsuManualInputManagerTestScene + public partial class TestScenePositionSnapGrid : OsuManualInputManagerTestScene { private Container content; protected override Container Content => content; @@ -42,8 +42,8 @@ namespace osu.Game.Tests.Visual.Editing private static readonly object[][] test_cases = { new object[] { new Vector2(0, 0), new Vector2(10, 10), 0f }, - new object[] { new Vector2(240, 180), new Vector2(10, 15), 30f }, - new object[] { new Vector2(160, 120), new Vector2(30, 20), -30f }, + new object[] { new Vector2(240, 180), new Vector2(10, 15), 10f }, + new object[] { new Vector2(160, 120), new Vector2(30, 20), -10f }, new object[] { new Vector2(480, 360), new Vector2(100, 100), 0f }, }; @@ -70,6 +70,51 @@ namespace osu.Game.Tests.Visual.Editing })); } + [TestCaseSource(nameof(test_cases))] + public void TestTriangularGrid(Vector2 position, Vector2 spacing, float rotation) + { + TriangularPositionSnapGrid grid = null; + + AddStep("create grid", () => + { + Child = grid = new TriangularPositionSnapGrid + { + RelativeSizeAxes = Axes.Both, + }; + grid.StartPosition.Value = position; + grid.Spacing.Value = spacing.X; + grid.GridLineRotation.Value = rotation; + }); + + AddStep("add snapping cursor", () => Add(new SnappingCursorContainer + { + RelativeSizeAxes = Axes.Both, + GetSnapPosition = pos => grid.GetSnappedPosition(grid.ToLocalSpace(pos)) + })); + } + + [TestCaseSource(nameof(test_cases))] + public void TestCircularGrid(Vector2 position, Vector2 spacing, float rotation) + { + CircularPositionSnapGrid grid = null; + + AddStep("create grid", () => + { + Child = grid = new CircularPositionSnapGrid + { + RelativeSizeAxes = Axes.Both, + }; + grid.StartPosition.Value = position; + grid.Spacing.Value = spacing.X; + }); + + AddStep("add snapping cursor", () => Add(new SnappingCursorContainer + { + RelativeSizeAxes = Axes.Both, + GetSnapPosition = pos => grid.GetSnappedPosition(grid.ToLocalSpace(pos)) + })); + } + private partial class SnappingCursorContainer : CompositeDrawable { public Func GetSnapPosition; diff --git a/osu.Game.Tests/Visual/Editing/TestSceneTriangularPositionSnapGrid.cs b/osu.Game.Tests/Visual/Editing/TestSceneTriangularPositionSnapGrid.cs deleted file mode 100644 index b1f82fa114..0000000000 --- a/osu.Game.Tests/Visual/Editing/TestSceneTriangularPositionSnapGrid.cs +++ /dev/null @@ -1,112 +0,0 @@ -// 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 NUnit.Framework; -using osu.Framework.Allocation; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Input.Events; -using osu.Game.Screens.Edit.Compose.Components; -using osuTK; -using osuTK.Graphics; - -namespace osu.Game.Tests.Visual.Editing -{ - public partial class TestSceneTriangularPositionSnapGrid : OsuManualInputManagerTestScene - { - private Container content; - protected override Container Content => content; - - [BackgroundDependencyLoader] - private void load() - { - base.Content.AddRange(new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = Colour4.Gray - }, - content = new Container - { - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding(10), - } - }); - } - - private static readonly object[][] test_cases = - { - new object[] { new Vector2(0, 0), 10, 0f }, - new object[] { new Vector2(240, 180), 10, 10f }, - new object[] { new Vector2(160, 120), 30, -10f }, - new object[] { new Vector2(480, 360), 100, 0f }, - }; - - [TestCaseSource(nameof(test_cases))] - public void TestTriangularGrid(Vector2 position, float spacing, float rotation) - { - TriangularPositionSnapGrid grid = null; - - AddStep("create grid", () => - { - Child = grid = new TriangularPositionSnapGrid - { - RelativeSizeAxes = Axes.Both, - }; - grid.StartPosition.Value = position; - grid.Spacing.Value = spacing; - grid.GridLineRotation.Value = rotation; - }); - - AddStep("add snapping cursor", () => Add(new SnappingCursorContainer - { - RelativeSizeAxes = Axes.Both, - GetSnapPosition = pos => grid.GetSnappedPosition(grid.ToLocalSpace(pos)) - })); - } - - private partial class SnappingCursorContainer : CompositeDrawable - { - public Func GetSnapPosition; - - private readonly Drawable cursor; - - public SnappingCursorContainer() - { - RelativeSizeAxes = Axes.Both; - - InternalChild = cursor = new Circle - { - Origin = Anchor.Centre, - Size = new Vector2(50), - Colour = Color4.Red - }; - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - updatePosition(GetContainingInputManager().CurrentState.Mouse.Position); - } - - protected override bool OnMouseMove(MouseMoveEvent e) - { - base.OnMouseMove(e); - - updatePosition(e.ScreenSpaceMousePosition); - return true; - } - - private void updatePosition(Vector2 screenSpacePosition) - { - cursor.Position = GetSnapPosition.Invoke(screenSpacePosition); - } - } - } -} From c5edf4328338ac1ab2ef42f0b8ed004c674aed3a Mon Sep 17 00:00:00 2001 From: OliBomby Date: Sun, 31 Dec 2023 19:53:32 +0100 Subject: [PATCH 0163/2493] fix grid test --- .../Editor/TestSceneOsuEditorGrids.cs | 74 ++++++++++++------- 1 file changed, 48 insertions(+), 26 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorGrids.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorGrids.cs index ff406b1b88..baeb0639d5 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorGrids.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorGrids.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.Linq; using NUnit.Framework; using osu.Framework.Testing; @@ -9,6 +10,7 @@ using osu.Game.Rulesets.Osu.Edit; using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles; using osu.Game.Screens.Edit.Compose.Components; using osu.Game.Tests.Visual; +using osu.Game.Utils; using osuTK; using osuTK.Input; @@ -25,22 +27,22 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor AddStep("select second object", () => EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects.ElementAt(1))); AddUntilStep("distance snap grid visible", () => this.ChildrenOfType().Any()); - rectangularGridActive(false); + gridActive(false); AddStep("enable rectangular grid", () => InputManager.Key(Key.Y)); AddStep("select second object", () => EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects.ElementAt(1))); AddUntilStep("distance snap grid still visible", () => this.ChildrenOfType().Any()); - rectangularGridActive(true); + gridActive(true); AddStep("disable distance snap grid", () => InputManager.Key(Key.T)); AddUntilStep("distance snap grid hidden", () => !this.ChildrenOfType().Any()); AddStep("select second object", () => EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects.ElementAt(1))); - rectangularGridActive(true); + gridActive(true); AddStep("disable rectangular grid", () => InputManager.Key(Key.Y)); AddUntilStep("distance snap grid still hidden", () => !this.ChildrenOfType().Any()); - rectangularGridActive(false); + gridActive(false); } [Test] @@ -58,49 +60,69 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor [Test] public void TestGridSnapMomentaryToggle() { - rectangularGridActive(false); + gridActive(false); AddStep("hold shift", () => InputManager.PressKey(Key.ShiftLeft)); - rectangularGridActive(true); + gridActive(true); AddStep("release shift", () => InputManager.ReleaseKey(Key.ShiftLeft)); - rectangularGridActive(false); + gridActive(false); } - private void rectangularGridActive(bool active) + private void gridActive(bool active) where T : PositionSnapGrid { AddStep("choose placement tool", () => InputManager.Key(Key.Number2)); - AddStep("move cursor to (1, 1)", () => + AddStep("move cursor to spacing + (1, 1)", () => { - var composer = Editor.ChildrenOfType().Single(); - InputManager.MoveMouseTo(composer.ToScreenSpace(new Vector2(1, 1))); + var composer = Editor.ChildrenOfType().Single(); + InputManager.MoveMouseTo(composer.ToScreenSpace(uniqueSnappingPosition(composer) + new Vector2(1, 1))); }); if (active) - AddAssert("placement blueprint at (0, 0)", () => Precision.AlmostEquals(Editor.ChildrenOfType().Single().HitObject.Position, new Vector2(0, 0))); + { + AddAssert("placement blueprint at spacing + (0, 0)", () => + { + var composer = Editor.ChildrenOfType().Single(); + return Precision.AlmostEquals(Editor.ChildrenOfType().Single().HitObject.Position, + uniqueSnappingPosition(composer)); + }); + } else - AddAssert("placement blueprint at (1, 1)", () => Precision.AlmostEquals(Editor.ChildrenOfType().Single().HitObject.Position, new Vector2(1, 1))); + { + AddAssert("placement blueprint at spacing + (1, 1)", () => + { + var composer = Editor.ChildrenOfType().Single(); + return Precision.AlmostEquals(Editor.ChildrenOfType().Single().HitObject.Position, + uniqueSnappingPosition(composer) + new Vector2(1, 1)); + }); + } + } + + private Vector2 uniqueSnappingPosition(PositionSnapGrid grid) + { + return grid switch + { + RectangularPositionSnapGrid rectangular => rectangular.StartPosition.Value + GeometryUtils.RotateVector(rectangular.Spacing.Value, -rectangular.GridLineRotation.Value), + TriangularPositionSnapGrid triangular => triangular.StartPosition.Value + GeometryUtils.RotateVector(new Vector2(triangular.Spacing.Value / 2, triangular.Spacing.Value / 2 * MathF.Sqrt(3)), -triangular.GridLineRotation.Value), + CircularPositionSnapGrid circular => circular.StartPosition.Value + GeometryUtils.RotateVector(new Vector2(circular.Spacing.Value, 0), -45), + _ => Vector2.Zero + }; } [Test] - public void TestGridSizeToggling() + public void TestGridTypeToggling() { AddStep("enable rectangular grid", () => InputManager.Key(Key.Y)); AddUntilStep("rectangular grid visible", () => this.ChildrenOfType().Any()); - gridSizeIs(4); + gridActive(true); - nextGridSizeIs(8); - nextGridSizeIs(16); - nextGridSizeIs(32); - nextGridSizeIs(4); + nextGridTypeIs(); + nextGridTypeIs(); + nextGridTypeIs(); } - private void nextGridSizeIs(int size) + private void nextGridTypeIs() where T : PositionSnapGrid { - AddStep("toggle to next grid size", () => InputManager.Key(Key.G)); - gridSizeIs(size); + AddStep("toggle to next grid type", () => InputManager.Key(Key.G)); + gridActive(true); } - - private void gridSizeIs(int size) - => AddAssert($"grid size is {size}", () => this.ChildrenOfType().Single().Spacing.Value == new Vector2(size) - && EditorBeatmap.BeatmapInfo.GridSize == size); } } From 594b6fe1672ddd4983e06986496519a6661c2352 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Sun, 31 Dec 2023 21:57:11 +0100 Subject: [PATCH 0164/2493] Add back the old keybind for cycling grid spacing --- .../Editor/TestSceneOsuEditorGrids.cs | 25 ++++++++++++++++++- .../Edit/OsuGridToolboxGroup.cs | 10 ++++---- 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorGrids.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorGrids.cs index baeb0639d5..5636bb51b9 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorGrids.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorGrids.cs @@ -107,6 +107,29 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor }; } + [Test] + public void TestGridSizeToggling() + { + AddStep("enable rectangular grid", () => InputManager.Key(Key.Y)); + AddUntilStep("rectangular grid visible", () => this.ChildrenOfType().Any()); + gridSizeIs(4); + + nextGridSizeIs(8); + nextGridSizeIs(16); + nextGridSizeIs(32); + nextGridSizeIs(4); + } + + private void nextGridSizeIs(int size) + { + AddStep("toggle to next grid size", () => InputManager.Key(Key.G)); + gridSizeIs(size); + } + + private void gridSizeIs(int size) + => AddAssert($"grid size is {size}", () => this.ChildrenOfType().Single().Spacing.Value == new Vector2(size) + && EditorBeatmap.BeatmapInfo.GridSize == size); + [Test] public void TestGridTypeToggling() { @@ -121,7 +144,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor private void nextGridTypeIs() where T : PositionSnapGrid { - AddStep("toggle to next grid type", () => InputManager.Key(Key.G)); + AddStep("toggle to next grid type", () => InputManager.Key(Key.H)); gridActive(true); } } diff --git a/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs b/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs index 981148858d..237ccf3e58 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs @@ -100,6 +100,8 @@ namespace osu.Game.Rulesets.Osu.Edit { } + private const float max_automatic_spacing = 64; + [BackgroundDependencyLoader] private void load() { @@ -196,11 +198,9 @@ namespace osu.Game.Rulesets.Osu.Edit }, true); } - private void nextGridType() + private void nextGridSize() { - currentGridTypeIndex = (currentGridTypeIndex + 1) % grid_types.Length; - GridType.Value = grid_types[currentGridTypeIndex]; - gridTypeButtons.Items[currentGridTypeIndex].Select(); + Spacing.Value = Spacing.Value * 2 >= max_automatic_spacing ? Spacing.Value / 8 : Spacing.Value * 2; } public bool OnPressed(KeyBindingPressEvent e) @@ -208,7 +208,7 @@ namespace osu.Game.Rulesets.Osu.Edit switch (e.Action) { case GlobalAction.EditorCycleGridDisplayMode: - nextGridType(); + nextGridSize(); return true; } From 39f4a1aa8e19a6570c24bef2a71e0c078e7a1049 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Mon, 1 Jan 2024 15:34:05 +0100 Subject: [PATCH 0165/2493] conflict fixes --- .../Editor/TestSceneOsuEditorGrids.cs | 18 ------------------ .../Edit/OsuGridToolboxGroup.cs | 5 ----- 2 files changed, 23 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorGrids.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorGrids.cs index 5636bb51b9..21427ba281 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorGrids.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorGrids.cs @@ -129,23 +129,5 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor private void gridSizeIs(int size) => AddAssert($"grid size is {size}", () => this.ChildrenOfType().Single().Spacing.Value == new Vector2(size) && EditorBeatmap.BeatmapInfo.GridSize == size); - - [Test] - public void TestGridTypeToggling() - { - AddStep("enable rectangular grid", () => InputManager.Key(Key.Y)); - AddUntilStep("rectangular grid visible", () => this.ChildrenOfType().Any()); - gridActive(true); - - nextGridTypeIs(); - nextGridTypeIs(); - nextGridTypeIs(); - } - - private void nextGridTypeIs() where T : PositionSnapGrid - { - AddStep("toggle to next grid type", () => InputManager.Key(Key.H)); - gridActive(true); - } } } diff --git a/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs b/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs index 237ccf3e58..76e735449a 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.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.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -25,10 +24,6 @@ namespace osu.Game.Rulesets.Osu.Edit { public partial class OsuGridToolboxGroup : EditorToolboxGroup, IKeyBindingHandler { - private static readonly PositionSnapGridType[] grid_types = Enum.GetValues(typeof(PositionSnapGridType)).Cast().ToArray(); - - private int currentGridTypeIndex; - [Resolved] private EditorBeatmap editorBeatmap { get; set; } = null!; From de14da95fa6d0230af1aeef7e9b0afd5caaa059e Mon Sep 17 00:00:00 2001 From: OliBomby Date: Mon, 1 Jan 2024 15:44:20 +0100 Subject: [PATCH 0166/2493] Remove other grid types --- .../Editor/TestSceneOsuEditorGrids.cs | 3 - .../Edit/OsuGridToolboxGroup.cs | 79 -------------- .../Edit/OsuHitObjectComposer.cs | 41 ++----- .../Editing/TestScenePositionSnapGrid.cs | 45 -------- .../Components/CircularPositionSnapGrid.cs | 101 ------------------ .../Components/TriangularPositionSnapGrid.cs | 89 --------------- 6 files changed, 7 insertions(+), 351 deletions(-) delete mode 100644 osu.Game/Screens/Edit/Compose/Components/CircularPositionSnapGrid.cs delete mode 100644 osu.Game/Screens/Edit/Compose/Components/TriangularPositionSnapGrid.cs diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorGrids.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorGrids.cs index 21427ba281..7cafd10454 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorGrids.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorGrids.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.Linq; using NUnit.Framework; using osu.Framework.Testing; @@ -101,8 +100,6 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor return grid switch { RectangularPositionSnapGrid rectangular => rectangular.StartPosition.Value + GeometryUtils.RotateVector(rectangular.Spacing.Value, -rectangular.GridLineRotation.Value), - TriangularPositionSnapGrid triangular => triangular.StartPosition.Value + GeometryUtils.RotateVector(new Vector2(triangular.Spacing.Value / 2, triangular.Spacing.Value / 2 * MathF.Sqrt(3)), -triangular.GridLineRotation.Value), - CircularPositionSnapGrid circular => circular.StartPosition.Value + GeometryUtils.RotateVector(new Vector2(circular.Spacing.Value, 0), -45), _ => Vector2.Zero }; } diff --git a/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs b/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs index 76e735449a..e82ca780ad 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs @@ -1,13 +1,9 @@ // 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.Framework.Bindables; using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Game.Graphics.Containers; @@ -16,9 +12,7 @@ using osu.Game.Input.Bindings; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Osu.UI; using osu.Game.Screens.Edit; -using osu.Game.Screens.Edit.Components.RadioButtons; using osuTK; -using osuTK.Graphics; namespace osu.Game.Rulesets.Osu.Edit { @@ -82,13 +76,10 @@ namespace osu.Game.Rulesets.Osu.Edit /// public Bindable SpacingVector { get; } = new Bindable(); - public Bindable GridType { get; } = new Bindable(); - private ExpandableSlider startPositionXSlider = null!; private ExpandableSlider startPositionYSlider = null!; private ExpandableSlider spacingSlider = null!; private ExpandableSlider gridLinesRotationSlider = null!; - private EditorRadioButtonCollection gridTypeButtons = null!; public OsuGridToolboxGroup() : base("grid") @@ -122,31 +113,6 @@ namespace osu.Game.Rulesets.Osu.Edit Current = GridLinesRotation, KeyboardStep = 1, }, - new FillFlowContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Spacing = new Vector2(0f, 10f), - Children = new Drawable[] - { - gridTypeButtons = new EditorRadioButtonCollection - { - RelativeSizeAxes = Axes.X, - Items = new[] - { - new RadioButton("Square", - () => GridType.Value = PositionSnapGridType.Square, - () => new SpriteIcon { Icon = FontAwesome.Regular.Square }), - new RadioButton("Triangle", - () => GridType.Value = PositionSnapGridType.Triangle, - () => new OutlineTriangle(true, 20)), - new RadioButton("Circle", - () => GridType.Value = PositionSnapGridType.Circle, - () => new SpriteIcon { Icon = FontAwesome.Regular.Circle }), - } - }, - } - }, }; Spacing.Value = editorBeatmap.BeatmapInfo.GridSize; @@ -156,8 +122,6 @@ namespace osu.Game.Rulesets.Osu.Edit { base.LoadComplete(); - gridTypeButtons.Items.First().Select(); - StartPositionX.BindValueChanged(x => { startPositionXSlider.ContractedLabelText = $"X: {x.NewValue:N0}"; @@ -185,12 +149,6 @@ namespace osu.Game.Rulesets.Osu.Edit gridLinesRotationSlider.ContractedLabelText = $"R: {rotation.NewValue:#,0.##}"; gridLinesRotationSlider.ExpandedLabelText = $"Rotation: {rotation.NewValue:#,0.##}"; }, true); - - expandingContainer?.Expanded.BindValueChanged(v => - { - gridTypeButtons.FadeTo(v.NewValue ? 1f : 0f, 500, Easing.OutQuint); - gridTypeButtons.BypassAutoSizeAxes = !v.NewValue ? Axes.Y : Axes.None; - }, true); } private void nextGridSize() @@ -213,42 +171,5 @@ namespace osu.Game.Rulesets.Osu.Edit public void OnReleased(KeyBindingReleaseEvent e) { } - - public partial class OutlineTriangle : BufferedContainer - { - public OutlineTriangle(bool outlineOnly, float size) - : base(cachedFrameBuffer: true) - { - Size = new Vector2(size); - - InternalChildren = new Drawable[] - { - new EquilateralTriangle { RelativeSizeAxes = Axes.Both }, - }; - - if (outlineOnly) - { - AddInternal(new EquilateralTriangle - { - Anchor = Anchor.TopCentre, - Origin = Anchor.Centre, - RelativePositionAxes = Axes.Y, - Y = 0.48f, - Colour = Color4.Black, - Size = new Vector2(size - 7), - Blending = BlendingParameters.None, - }); - } - - Blending = BlendingParameters.Additive; - } - } - } - - public enum PositionSnapGridType - { - Square, - Triangle, - Circle, } } diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs index 84d5adbc52..51bb74926f 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs @@ -99,7 +99,7 @@ namespace osu.Game.Rulesets.Osu.Edit // we may be entering the screen with a selection already active updateDistanceSnapGrid(); - OsuGridToolboxGroup.GridType.BindValueChanged(updatePositionSnapGrid, true); + updatePositionSnapGrid(); RightToolbox.AddRange(new EditorToolboxGroup[] { @@ -110,45 +110,18 @@ namespace osu.Game.Rulesets.Osu.Edit ); } - private void updatePositionSnapGrid(ValueChangedEvent obj) + private void updatePositionSnapGrid() { if (positionSnapGrid != null) LayerBelowRuleset.Remove(positionSnapGrid, true); - switch (obj.NewValue) - { - case PositionSnapGridType.Square: - var rectangularPositionSnapGrid = new RectangularPositionSnapGrid(); + var rectangularPositionSnapGrid = new RectangularPositionSnapGrid(); - rectangularPositionSnapGrid.Spacing.BindTo(OsuGridToolboxGroup.SpacingVector); - rectangularPositionSnapGrid.GridLineRotation.BindTo(OsuGridToolboxGroup.GridLinesRotation); + rectangularPositionSnapGrid.StartPosition.BindTo(OsuGridToolboxGroup.StartPosition); + rectangularPositionSnapGrid.Spacing.BindTo(OsuGridToolboxGroup.SpacingVector); + rectangularPositionSnapGrid.GridLineRotation.BindTo(OsuGridToolboxGroup.GridLinesRotation); - positionSnapGrid = rectangularPositionSnapGrid; - break; - - case PositionSnapGridType.Triangle: - var triangularPositionSnapGrid = new TriangularPositionSnapGrid(); - - triangularPositionSnapGrid.Spacing.BindTo(OsuGridToolboxGroup.Spacing); - triangularPositionSnapGrid.GridLineRotation.BindTo(OsuGridToolboxGroup.GridLinesRotation); - - positionSnapGrid = triangularPositionSnapGrid; - break; - - case PositionSnapGridType.Circle: - var circularPositionSnapGrid = new CircularPositionSnapGrid(); - - circularPositionSnapGrid.Spacing.BindTo(OsuGridToolboxGroup.Spacing); - - positionSnapGrid = circularPositionSnapGrid; - break; - - default: - throw new NotImplementedException($"{OsuGridToolboxGroup.GridType} has an incorrect value."); - } - - // Bind the start position to the toolbox sliders. - positionSnapGrid.StartPosition.BindTo(OsuGridToolboxGroup.StartPosition); + positionSnapGrid = rectangularPositionSnapGrid; positionSnapGrid.RelativeSizeAxes = Axes.Both; LayerBelowRuleset.Add(positionSnapGrid); diff --git a/osu.Game.Tests/Visual/Editing/TestScenePositionSnapGrid.cs b/osu.Game.Tests/Visual/Editing/TestScenePositionSnapGrid.cs index 7e66edc2dd..2721bc3602 100644 --- a/osu.Game.Tests/Visual/Editing/TestScenePositionSnapGrid.cs +++ b/osu.Game.Tests/Visual/Editing/TestScenePositionSnapGrid.cs @@ -70,51 +70,6 @@ namespace osu.Game.Tests.Visual.Editing })); } - [TestCaseSource(nameof(test_cases))] - public void TestTriangularGrid(Vector2 position, Vector2 spacing, float rotation) - { - TriangularPositionSnapGrid grid = null; - - AddStep("create grid", () => - { - Child = grid = new TriangularPositionSnapGrid - { - RelativeSizeAxes = Axes.Both, - }; - grid.StartPosition.Value = position; - grid.Spacing.Value = spacing.X; - grid.GridLineRotation.Value = rotation; - }); - - AddStep("add snapping cursor", () => Add(new SnappingCursorContainer - { - RelativeSizeAxes = Axes.Both, - GetSnapPosition = pos => grid.GetSnappedPosition(grid.ToLocalSpace(pos)) - })); - } - - [TestCaseSource(nameof(test_cases))] - public void TestCircularGrid(Vector2 position, Vector2 spacing, float rotation) - { - CircularPositionSnapGrid grid = null; - - AddStep("create grid", () => - { - Child = grid = new CircularPositionSnapGrid - { - RelativeSizeAxes = Axes.Both, - }; - grid.StartPosition.Value = position; - grid.Spacing.Value = spacing.X; - }); - - AddStep("add snapping cursor", () => Add(new SnappingCursorContainer - { - RelativeSizeAxes = Axes.Both, - GetSnapPosition = pos => grid.GetSnappedPosition(grid.ToLocalSpace(pos)) - })); - } - private partial class SnappingCursorContainer : CompositeDrawable { public Func GetSnapPosition; diff --git a/osu.Game/Screens/Edit/Compose/Components/CircularPositionSnapGrid.cs b/osu.Game/Screens/Edit/Compose/Components/CircularPositionSnapGrid.cs deleted file mode 100644 index 403a270359..0000000000 --- a/osu.Game/Screens/Edit/Compose/Components/CircularPositionSnapGrid.cs +++ /dev/null @@ -1,101 +0,0 @@ -// 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.Bindables; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Utils; -using osuTK; - -namespace osu.Game.Screens.Edit.Compose.Components -{ - public partial class CircularPositionSnapGrid : PositionSnapGrid - { - /// - /// The spacing between grid lines of this . - /// - public BindableFloat Spacing { get; } = new BindableFloat(1f) - { - MinValue = 0f, - }; - - public CircularPositionSnapGrid() - { - Spacing.BindValueChanged(_ => GridCache.Invalidate()); - } - - protected override void CreateContent() - { - var drawSize = DrawSize; - - // Calculate the maximum distance from the origin to the edge of the grid. - float maxDist = MathF.Max( - MathF.Max(StartPosition.Value.Length, (StartPosition.Value - drawSize).Length), - MathF.Max((StartPosition.Value - new Vector2(drawSize.X, 0)).Length, (StartPosition.Value - new Vector2(0, drawSize.Y)).Length) - ); - - generateCircles((int)(maxDist / Spacing.Value) + 1); - - GenerateOutline(drawSize); - } - - private void generateCircles(int count) - { - // Make lines the same width independent of display resolution. - float lineWidth = 2 * DrawWidth / ScreenSpaceDrawQuad.Width; - - List generatedCircles = new List(); - - for (int i = 0; i < count; i++) - { - // Add a minimum diameter so the center circle is clearly visible. - float diameter = MathF.Max(lineWidth * 1.5f, i * Spacing.Value * 2); - - var gridCircle = new CircularContainer - { - BorderColour = Colour4.White, - BorderThickness = lineWidth, - Alpha = 0.2f, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.None, - Width = diameter, - Height = diameter, - Position = StartPosition.Value, - Masking = true, - Child = new Box - { - RelativeSizeAxes = Axes.Both, - AlwaysPresent = true, - Alpha = 0f, - } - }; - - generatedCircles.Add(gridCircle); - } - - if (generatedCircles.Count == 0) - return; - - generatedCircles.First().Alpha = 0.8f; - - AddRangeInternal(generatedCircles); - } - - public override Vector2 GetSnappedPosition(Vector2 original) - { - Vector2 relativeToStart = original - StartPosition.Value; - - if (relativeToStart.LengthSquared < Precision.FLOAT_EPSILON) - return StartPosition.Value; - - float length = relativeToStart.Length; - float wantedLength = MathF.Round(length / Spacing.Value) * Spacing.Value; - - return StartPosition.Value + Vector2.Multiply(relativeToStart, wantedLength / length); - } - } -} diff --git a/osu.Game/Screens/Edit/Compose/Components/TriangularPositionSnapGrid.cs b/osu.Game/Screens/Edit/Compose/Components/TriangularPositionSnapGrid.cs deleted file mode 100644 index 93d2c6a74a..0000000000 --- a/osu.Game/Screens/Edit/Compose/Components/TriangularPositionSnapGrid.cs +++ /dev/null @@ -1,89 +0,0 @@ -// 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.Bindables; -using osu.Game.Utils; -using osuTK; - -namespace osu.Game.Screens.Edit.Compose.Components -{ - public partial class TriangularPositionSnapGrid : LinedPositionSnapGrid - { - /// - /// The spacing between grid lines of this . - /// - public BindableFloat Spacing { get; } = new BindableFloat(1f) - { - MinValue = 0f, - }; - - /// - /// The rotation in degrees of the grid lines of this . - /// - public BindableFloat GridLineRotation { get; } = new BindableFloat(); - - public TriangularPositionSnapGrid() - { - Spacing.BindValueChanged(_ => GridCache.Invalidate()); - GridLineRotation.BindValueChanged(_ => GridCache.Invalidate()); - } - - private const float sqrt3 = 1.73205080757f; - private const float sqrt3_over2 = 0.86602540378f; - private const float one_over_sqrt3 = 0.57735026919f; - - protected override void CreateContent() - { - var drawSize = DrawSize; - float stepSpacing = Spacing.Value * sqrt3_over2; - var step1 = GeometryUtils.RotateVector(new Vector2(stepSpacing, 0), -GridLineRotation.Value - 30); - var step2 = GeometryUtils.RotateVector(new Vector2(stepSpacing, 0), -GridLineRotation.Value - 90); - var step3 = GeometryUtils.RotateVector(new Vector2(stepSpacing, 0), -GridLineRotation.Value - 150); - - GenerateGridLines(step1, drawSize); - GenerateGridLines(-step1, drawSize); - - GenerateGridLines(step2, drawSize); - GenerateGridLines(-step2, drawSize); - - GenerateGridLines(step3, drawSize); - GenerateGridLines(-step3, drawSize); - - GenerateOutline(drawSize); - } - - public override Vector2 GetSnappedPosition(Vector2 original) - { - Vector2 relativeToStart = GeometryUtils.RotateVector(original - StartPosition.Value, GridLineRotation.Value); - Vector2 hex = pixelToHex(relativeToStart); - - return StartPosition.Value + GeometryUtils.RotateVector(hexToPixel(hex), -GridLineRotation.Value); - } - - private Vector2 pixelToHex(Vector2 pixel) - { - float x = pixel.X / Spacing.Value; - float y = pixel.Y / Spacing.Value; - // Algorithm from Charles Chambers - // with modifications and comments by Chris Cox 2023 - // - float t = sqrt3 * y + 1; // scaled y, plus phase - float temp1 = MathF.Floor(t + x); // (y+x) diagonal, this calc needs floor - float temp2 = t - x; // (y-x) diagonal, no floor needed - float temp3 = 2 * x + 1; // scaled horizontal, no floor needed, needs +1 to get correct phase - float qf = (temp1 + temp3) / 3.0f; // pseudo x with fraction - float rf = (temp1 + temp2) / 3.0f; // pseudo y with fraction - float q = MathF.Floor(qf); // pseudo x, quantized and thus requires floor - float r = MathF.Floor(rf); // pseudo y, quantized and thus requires floor - return new Vector2(q, r); - } - - private Vector2 hexToPixel(Vector2 hex) - { - // Taken from - // with modifications for the different definition of size. - return new Vector2(Spacing.Value * (hex.X - hex.Y / 2), Spacing.Value * one_over_sqrt3 * 1.5f * hex.Y); - } - } -} From 6bb72a9fccb92760377e3b081b99088a480c33c9 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Mon, 1 Jan 2024 15:46:07 +0100 Subject: [PATCH 0167/2493] Revert "Remove other grid types" This reverts commit de14da95fa6d0230af1aeef7e9b0afd5caaa059e. --- .../Editor/TestSceneOsuEditorGrids.cs | 3 + .../Edit/OsuGridToolboxGroup.cs | 79 ++++++++++++++ .../Edit/OsuHitObjectComposer.cs | 41 +++++-- .../Editing/TestScenePositionSnapGrid.cs | 45 ++++++++ .../Components/CircularPositionSnapGrid.cs | 101 ++++++++++++++++++ .../Components/TriangularPositionSnapGrid.cs | 89 +++++++++++++++ 6 files changed, 351 insertions(+), 7 deletions(-) create mode 100644 osu.Game/Screens/Edit/Compose/Components/CircularPositionSnapGrid.cs create mode 100644 osu.Game/Screens/Edit/Compose/Components/TriangularPositionSnapGrid.cs diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorGrids.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorGrids.cs index 7cafd10454..21427ba281 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorGrids.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorGrids.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.Linq; using NUnit.Framework; using osu.Framework.Testing; @@ -100,6 +101,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor return grid switch { RectangularPositionSnapGrid rectangular => rectangular.StartPosition.Value + GeometryUtils.RotateVector(rectangular.Spacing.Value, -rectangular.GridLineRotation.Value), + TriangularPositionSnapGrid triangular => triangular.StartPosition.Value + GeometryUtils.RotateVector(new Vector2(triangular.Spacing.Value / 2, triangular.Spacing.Value / 2 * MathF.Sqrt(3)), -triangular.GridLineRotation.Value), + CircularPositionSnapGrid circular => circular.StartPosition.Value + GeometryUtils.RotateVector(new Vector2(circular.Spacing.Value, 0), -45), _ => Vector2.Zero }; } diff --git a/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs b/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs index e82ca780ad..76e735449a 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs @@ -1,9 +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.Linq; 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.Input.Bindings; using osu.Framework.Input.Events; using osu.Game.Graphics.Containers; @@ -12,7 +16,9 @@ using osu.Game.Input.Bindings; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Osu.UI; using osu.Game.Screens.Edit; +using osu.Game.Screens.Edit.Components.RadioButtons; using osuTK; +using osuTK.Graphics; namespace osu.Game.Rulesets.Osu.Edit { @@ -76,10 +82,13 @@ namespace osu.Game.Rulesets.Osu.Edit /// public Bindable SpacingVector { get; } = new Bindable(); + public Bindable GridType { get; } = new Bindable(); + private ExpandableSlider startPositionXSlider = null!; private ExpandableSlider startPositionYSlider = null!; private ExpandableSlider spacingSlider = null!; private ExpandableSlider gridLinesRotationSlider = null!; + private EditorRadioButtonCollection gridTypeButtons = null!; public OsuGridToolboxGroup() : base("grid") @@ -113,6 +122,31 @@ namespace osu.Game.Rulesets.Osu.Edit Current = GridLinesRotation, KeyboardStep = 1, }, + new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Spacing = new Vector2(0f, 10f), + Children = new Drawable[] + { + gridTypeButtons = new EditorRadioButtonCollection + { + RelativeSizeAxes = Axes.X, + Items = new[] + { + new RadioButton("Square", + () => GridType.Value = PositionSnapGridType.Square, + () => new SpriteIcon { Icon = FontAwesome.Regular.Square }), + new RadioButton("Triangle", + () => GridType.Value = PositionSnapGridType.Triangle, + () => new OutlineTriangle(true, 20)), + new RadioButton("Circle", + () => GridType.Value = PositionSnapGridType.Circle, + () => new SpriteIcon { Icon = FontAwesome.Regular.Circle }), + } + }, + } + }, }; Spacing.Value = editorBeatmap.BeatmapInfo.GridSize; @@ -122,6 +156,8 @@ namespace osu.Game.Rulesets.Osu.Edit { base.LoadComplete(); + gridTypeButtons.Items.First().Select(); + StartPositionX.BindValueChanged(x => { startPositionXSlider.ContractedLabelText = $"X: {x.NewValue:N0}"; @@ -149,6 +185,12 @@ namespace osu.Game.Rulesets.Osu.Edit gridLinesRotationSlider.ContractedLabelText = $"R: {rotation.NewValue:#,0.##}"; gridLinesRotationSlider.ExpandedLabelText = $"Rotation: {rotation.NewValue:#,0.##}"; }, true); + + expandingContainer?.Expanded.BindValueChanged(v => + { + gridTypeButtons.FadeTo(v.NewValue ? 1f : 0f, 500, Easing.OutQuint); + gridTypeButtons.BypassAutoSizeAxes = !v.NewValue ? Axes.Y : Axes.None; + }, true); } private void nextGridSize() @@ -171,5 +213,42 @@ namespace osu.Game.Rulesets.Osu.Edit public void OnReleased(KeyBindingReleaseEvent e) { } + + public partial class OutlineTriangle : BufferedContainer + { + public OutlineTriangle(bool outlineOnly, float size) + : base(cachedFrameBuffer: true) + { + Size = new Vector2(size); + + InternalChildren = new Drawable[] + { + new EquilateralTriangle { RelativeSizeAxes = Axes.Both }, + }; + + if (outlineOnly) + { + AddInternal(new EquilateralTriangle + { + Anchor = Anchor.TopCentre, + Origin = Anchor.Centre, + RelativePositionAxes = Axes.Y, + Y = 0.48f, + Colour = Color4.Black, + Size = new Vector2(size - 7), + Blending = BlendingParameters.None, + }); + } + + Blending = BlendingParameters.Additive; + } + } + } + + public enum PositionSnapGridType + { + Square, + Triangle, + Circle, } } diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs index 51bb74926f..84d5adbc52 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs @@ -99,7 +99,7 @@ namespace osu.Game.Rulesets.Osu.Edit // we may be entering the screen with a selection already active updateDistanceSnapGrid(); - updatePositionSnapGrid(); + OsuGridToolboxGroup.GridType.BindValueChanged(updatePositionSnapGrid, true); RightToolbox.AddRange(new EditorToolboxGroup[] { @@ -110,18 +110,45 @@ namespace osu.Game.Rulesets.Osu.Edit ); } - private void updatePositionSnapGrid() + private void updatePositionSnapGrid(ValueChangedEvent obj) { if (positionSnapGrid != null) LayerBelowRuleset.Remove(positionSnapGrid, true); - var rectangularPositionSnapGrid = new RectangularPositionSnapGrid(); + switch (obj.NewValue) + { + case PositionSnapGridType.Square: + var rectangularPositionSnapGrid = new RectangularPositionSnapGrid(); - rectangularPositionSnapGrid.StartPosition.BindTo(OsuGridToolboxGroup.StartPosition); - rectangularPositionSnapGrid.Spacing.BindTo(OsuGridToolboxGroup.SpacingVector); - rectangularPositionSnapGrid.GridLineRotation.BindTo(OsuGridToolboxGroup.GridLinesRotation); + rectangularPositionSnapGrid.Spacing.BindTo(OsuGridToolboxGroup.SpacingVector); + rectangularPositionSnapGrid.GridLineRotation.BindTo(OsuGridToolboxGroup.GridLinesRotation); - positionSnapGrid = rectangularPositionSnapGrid; + positionSnapGrid = rectangularPositionSnapGrid; + break; + + case PositionSnapGridType.Triangle: + var triangularPositionSnapGrid = new TriangularPositionSnapGrid(); + + triangularPositionSnapGrid.Spacing.BindTo(OsuGridToolboxGroup.Spacing); + triangularPositionSnapGrid.GridLineRotation.BindTo(OsuGridToolboxGroup.GridLinesRotation); + + positionSnapGrid = triangularPositionSnapGrid; + break; + + case PositionSnapGridType.Circle: + var circularPositionSnapGrid = new CircularPositionSnapGrid(); + + circularPositionSnapGrid.Spacing.BindTo(OsuGridToolboxGroup.Spacing); + + positionSnapGrid = circularPositionSnapGrid; + break; + + default: + throw new NotImplementedException($"{OsuGridToolboxGroup.GridType} has an incorrect value."); + } + + // Bind the start position to the toolbox sliders. + positionSnapGrid.StartPosition.BindTo(OsuGridToolboxGroup.StartPosition); positionSnapGrid.RelativeSizeAxes = Axes.Both; LayerBelowRuleset.Add(positionSnapGrid); diff --git a/osu.Game.Tests/Visual/Editing/TestScenePositionSnapGrid.cs b/osu.Game.Tests/Visual/Editing/TestScenePositionSnapGrid.cs index 2721bc3602..7e66edc2dd 100644 --- a/osu.Game.Tests/Visual/Editing/TestScenePositionSnapGrid.cs +++ b/osu.Game.Tests/Visual/Editing/TestScenePositionSnapGrid.cs @@ -70,6 +70,51 @@ namespace osu.Game.Tests.Visual.Editing })); } + [TestCaseSource(nameof(test_cases))] + public void TestTriangularGrid(Vector2 position, Vector2 spacing, float rotation) + { + TriangularPositionSnapGrid grid = null; + + AddStep("create grid", () => + { + Child = grid = new TriangularPositionSnapGrid + { + RelativeSizeAxes = Axes.Both, + }; + grid.StartPosition.Value = position; + grid.Spacing.Value = spacing.X; + grid.GridLineRotation.Value = rotation; + }); + + AddStep("add snapping cursor", () => Add(new SnappingCursorContainer + { + RelativeSizeAxes = Axes.Both, + GetSnapPosition = pos => grid.GetSnappedPosition(grid.ToLocalSpace(pos)) + })); + } + + [TestCaseSource(nameof(test_cases))] + public void TestCircularGrid(Vector2 position, Vector2 spacing, float rotation) + { + CircularPositionSnapGrid grid = null; + + AddStep("create grid", () => + { + Child = grid = new CircularPositionSnapGrid + { + RelativeSizeAxes = Axes.Both, + }; + grid.StartPosition.Value = position; + grid.Spacing.Value = spacing.X; + }); + + AddStep("add snapping cursor", () => Add(new SnappingCursorContainer + { + RelativeSizeAxes = Axes.Both, + GetSnapPosition = pos => grid.GetSnappedPosition(grid.ToLocalSpace(pos)) + })); + } + private partial class SnappingCursorContainer : CompositeDrawable { public Func GetSnapPosition; diff --git a/osu.Game/Screens/Edit/Compose/Components/CircularPositionSnapGrid.cs b/osu.Game/Screens/Edit/Compose/Components/CircularPositionSnapGrid.cs new file mode 100644 index 0000000000..403a270359 --- /dev/null +++ b/osu.Game/Screens/Edit/Compose/Components/CircularPositionSnapGrid.cs @@ -0,0 +1,101 @@ +// 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.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Utils; +using osuTK; + +namespace osu.Game.Screens.Edit.Compose.Components +{ + public partial class CircularPositionSnapGrid : PositionSnapGrid + { + /// + /// The spacing between grid lines of this . + /// + public BindableFloat Spacing { get; } = new BindableFloat(1f) + { + MinValue = 0f, + }; + + public CircularPositionSnapGrid() + { + Spacing.BindValueChanged(_ => GridCache.Invalidate()); + } + + protected override void CreateContent() + { + var drawSize = DrawSize; + + // Calculate the maximum distance from the origin to the edge of the grid. + float maxDist = MathF.Max( + MathF.Max(StartPosition.Value.Length, (StartPosition.Value - drawSize).Length), + MathF.Max((StartPosition.Value - new Vector2(drawSize.X, 0)).Length, (StartPosition.Value - new Vector2(0, drawSize.Y)).Length) + ); + + generateCircles((int)(maxDist / Spacing.Value) + 1); + + GenerateOutline(drawSize); + } + + private void generateCircles(int count) + { + // Make lines the same width independent of display resolution. + float lineWidth = 2 * DrawWidth / ScreenSpaceDrawQuad.Width; + + List generatedCircles = new List(); + + for (int i = 0; i < count; i++) + { + // Add a minimum diameter so the center circle is clearly visible. + float diameter = MathF.Max(lineWidth * 1.5f, i * Spacing.Value * 2); + + var gridCircle = new CircularContainer + { + BorderColour = Colour4.White, + BorderThickness = lineWidth, + Alpha = 0.2f, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.None, + Width = diameter, + Height = diameter, + Position = StartPosition.Value, + Masking = true, + Child = new Box + { + RelativeSizeAxes = Axes.Both, + AlwaysPresent = true, + Alpha = 0f, + } + }; + + generatedCircles.Add(gridCircle); + } + + if (generatedCircles.Count == 0) + return; + + generatedCircles.First().Alpha = 0.8f; + + AddRangeInternal(generatedCircles); + } + + public override Vector2 GetSnappedPosition(Vector2 original) + { + Vector2 relativeToStart = original - StartPosition.Value; + + if (relativeToStart.LengthSquared < Precision.FLOAT_EPSILON) + return StartPosition.Value; + + float length = relativeToStart.Length; + float wantedLength = MathF.Round(length / Spacing.Value) * Spacing.Value; + + return StartPosition.Value + Vector2.Multiply(relativeToStart, wantedLength / length); + } + } +} diff --git a/osu.Game/Screens/Edit/Compose/Components/TriangularPositionSnapGrid.cs b/osu.Game/Screens/Edit/Compose/Components/TriangularPositionSnapGrid.cs new file mode 100644 index 0000000000..93d2c6a74a --- /dev/null +++ b/osu.Game/Screens/Edit/Compose/Components/TriangularPositionSnapGrid.cs @@ -0,0 +1,89 @@ +// 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.Bindables; +using osu.Game.Utils; +using osuTK; + +namespace osu.Game.Screens.Edit.Compose.Components +{ + public partial class TriangularPositionSnapGrid : LinedPositionSnapGrid + { + /// + /// The spacing between grid lines of this . + /// + public BindableFloat Spacing { get; } = new BindableFloat(1f) + { + MinValue = 0f, + }; + + /// + /// The rotation in degrees of the grid lines of this . + /// + public BindableFloat GridLineRotation { get; } = new BindableFloat(); + + public TriangularPositionSnapGrid() + { + Spacing.BindValueChanged(_ => GridCache.Invalidate()); + GridLineRotation.BindValueChanged(_ => GridCache.Invalidate()); + } + + private const float sqrt3 = 1.73205080757f; + private const float sqrt3_over2 = 0.86602540378f; + private const float one_over_sqrt3 = 0.57735026919f; + + protected override void CreateContent() + { + var drawSize = DrawSize; + float stepSpacing = Spacing.Value * sqrt3_over2; + var step1 = GeometryUtils.RotateVector(new Vector2(stepSpacing, 0), -GridLineRotation.Value - 30); + var step2 = GeometryUtils.RotateVector(new Vector2(stepSpacing, 0), -GridLineRotation.Value - 90); + var step3 = GeometryUtils.RotateVector(new Vector2(stepSpacing, 0), -GridLineRotation.Value - 150); + + GenerateGridLines(step1, drawSize); + GenerateGridLines(-step1, drawSize); + + GenerateGridLines(step2, drawSize); + GenerateGridLines(-step2, drawSize); + + GenerateGridLines(step3, drawSize); + GenerateGridLines(-step3, drawSize); + + GenerateOutline(drawSize); + } + + public override Vector2 GetSnappedPosition(Vector2 original) + { + Vector2 relativeToStart = GeometryUtils.RotateVector(original - StartPosition.Value, GridLineRotation.Value); + Vector2 hex = pixelToHex(relativeToStart); + + return StartPosition.Value + GeometryUtils.RotateVector(hexToPixel(hex), -GridLineRotation.Value); + } + + private Vector2 pixelToHex(Vector2 pixel) + { + float x = pixel.X / Spacing.Value; + float y = pixel.Y / Spacing.Value; + // Algorithm from Charles Chambers + // with modifications and comments by Chris Cox 2023 + // + float t = sqrt3 * y + 1; // scaled y, plus phase + float temp1 = MathF.Floor(t + x); // (y+x) diagonal, this calc needs floor + float temp2 = t - x; // (y-x) diagonal, no floor needed + float temp3 = 2 * x + 1; // scaled horizontal, no floor needed, needs +1 to get correct phase + float qf = (temp1 + temp3) / 3.0f; // pseudo x with fraction + float rf = (temp1 + temp2) / 3.0f; // pseudo y with fraction + float q = MathF.Floor(qf); // pseudo x, quantized and thus requires floor + float r = MathF.Floor(rf); // pseudo y, quantized and thus requires floor + return new Vector2(q, r); + } + + private Vector2 hexToPixel(Vector2 hex) + { + // Taken from + // with modifications for the different definition of size. + return new Vector2(Spacing.Value * (hex.X - hex.Y / 2), Spacing.Value * one_over_sqrt3 * 1.5f * hex.Y); + } + } +} From e803b0215f146e449f12a77fae1f09db4fdae30f Mon Sep 17 00:00:00 2001 From: OliBomby Date: Sat, 30 Dec 2023 01:38:08 +0100 Subject: [PATCH 0168/2493] flip along grid axis --- .../Edit/OsuSelectionHandler.cs | 19 ++++++++++--------- osu.Game/Utils/GeometryUtils.cs | 11 +++++++++++ 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs index cea2adc6e2..021c735ebd 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.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 osu.Framework.Allocation; @@ -15,7 +16,6 @@ using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu.Objects; -using osu.Game.Rulesets.Osu.UI; using osu.Game.Screens.Edit.Compose.Components; using osu.Game.Utils; using osuTK; @@ -28,6 +28,9 @@ namespace osu.Game.Rulesets.Osu.Edit [Resolved(CanBeNull = true)] private IDistanceSnapProvider? snapProvider { get; set; } + [Resolved] + private OsuGridToolboxGroup gridToolbox { get; set; } = null!; + /// /// During a transform, the initial path types of a single selected slider are stored so they /// can be maintained throughout the operation. @@ -104,13 +107,16 @@ namespace osu.Game.Rulesets.Osu.Edit { var hitObjects = selectedMovableObjects; - var flipQuad = flipOverOrigin ? new Quad(0, 0, OsuPlayfield.BASE_SIZE.X, OsuPlayfield.BASE_SIZE.Y) : GeometryUtils.GetSurroundingQuad(hitObjects); + var flipQuad = flipOverOrigin ? new Quad(gridToolbox.StartPositionX.Value, gridToolbox.StartPositionY.Value, 0, 0) : GeometryUtils.GetSurroundingQuad(hitObjects); + var flipAxis = flipOverOrigin ? new Vector2(MathF.Cos(MathHelper.DegreesToRadians(gridToolbox.GridLinesRotation.Value)), MathF.Sin(MathHelper.DegreesToRadians(gridToolbox.GridLinesRotation.Value))) : Vector2.UnitX; + flipAxis = direction == Direction.Vertical ? flipAxis.PerpendicularLeft : flipAxis; + var controlPointFlipQuad = new Quad(); bool didFlip = false; foreach (var h in hitObjects) { - var flippedPosition = GeometryUtils.GetFlippedPosition(direction, flipQuad, h.Position); + var flippedPosition = GeometryUtils.GetFlippedPosition(flipAxis, flipQuad, h.Position); if (!Precision.AlmostEquals(flippedPosition, h.Position)) { @@ -123,12 +129,7 @@ namespace osu.Game.Rulesets.Osu.Edit didFlip = true; foreach (var cp in slider.Path.ControlPoints) - { - cp.Position = new Vector2( - (direction == Direction.Horizontal ? -1 : 1) * cp.Position.X, - (direction == Direction.Vertical ? -1 : 1) * cp.Position.Y - ); - } + cp.Position = GeometryUtils.GetFlippedPosition(flipAxis, controlPointFlipQuad, cp.Position); } } diff --git a/osu.Game/Utils/GeometryUtils.cs b/osu.Game/Utils/GeometryUtils.cs index e0d217dd48..aacf9b91f9 100644 --- a/osu.Game/Utils/GeometryUtils.cs +++ b/osu.Game/Utils/GeometryUtils.cs @@ -70,6 +70,17 @@ namespace osu.Game.Utils return position; } + /// + /// Given a flip axis vector, a surrounding quad for all selected objects, and a position, + /// will return the flipped position in screen space coordinates. + /// + public static Vector2 GetFlippedPosition(Vector2 axis, Quad quad, Vector2 position) + { + var centre = quad.Centre; + + return position - 2 * Vector2.Dot(position - centre, axis) * axis; + } + /// /// Given a scale vector, a surrounding quad for all selected objects, and a position, /// will return the scaled position in screen space coordinates. From 078fe5a78ca5b0744d12c6f4fa8f242a0730986c Mon Sep 17 00:00:00 2001 From: OliBomby Date: Sat, 30 Dec 2023 01:53:19 +0100 Subject: [PATCH 0169/2493] Rotate popover rotates around grid center --- osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs | 2 +- osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs | 8 +++++--- osu.Game.Rulesets.Osu/Edit/TransformToolboxGroup.cs | 4 +++- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs index 84d5adbc52..02e98d75a7 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs @@ -104,7 +104,7 @@ namespace osu.Game.Rulesets.Osu.Edit RightToolbox.AddRange(new EditorToolboxGroup[] { OsuGridToolboxGroup, - new TransformToolboxGroup { RotationHandler = BlueprintContainer.SelectionHandler.RotationHandler, }, + new TransformToolboxGroup { RotationHandler = BlueprintContainer.SelectionHandler.RotationHandler, GridToolbox = OsuGridToolboxGroup }, FreehandlSliderToolboxGroup } ); diff --git a/osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs b/osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs index f09d6b78e6..02a8ff5872 100644 --- a/osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs +++ b/osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs @@ -8,7 +8,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; using osu.Game.Graphics.UserInterfaceV2; -using osu.Game.Rulesets.Osu.UI; using osu.Game.Screens.Edit.Components.RadioButtons; using osu.Game.Screens.Edit.Compose.Components; using osuTK; @@ -19,14 +18,17 @@ namespace osu.Game.Rulesets.Osu.Edit { private readonly SelectionRotationHandler rotationHandler; + private readonly OsuGridToolboxGroup gridToolbox; + private readonly Bindable rotationInfo = new Bindable(new PreciseRotationInfo(0, RotationOrigin.PlayfieldCentre)); private SliderWithTextBoxInput angleInput = null!; private EditorRadioButtonCollection rotationOrigin = null!; - public PreciseRotationPopover(SelectionRotationHandler rotationHandler) + public PreciseRotationPopover(SelectionRotationHandler rotationHandler, OsuGridToolboxGroup gridToolbox) { this.rotationHandler = rotationHandler; + this.gridToolbox = gridToolbox; AllowableAnchors = new[] { Anchor.CentreLeft, Anchor.CentreRight }; } @@ -78,7 +80,7 @@ namespace osu.Game.Rulesets.Osu.Edit rotationInfo.BindValueChanged(rotation => { - rotationHandler.Update(rotation.NewValue.Degrees, rotation.NewValue.Origin == RotationOrigin.PlayfieldCentre ? OsuPlayfield.BASE_SIZE / 2 : null); + rotationHandler.Update(rotation.NewValue.Degrees, rotation.NewValue.Origin == RotationOrigin.PlayfieldCentre ? gridToolbox.StartPosition.Value : null); }); } diff --git a/osu.Game.Rulesets.Osu/Edit/TransformToolboxGroup.cs b/osu.Game.Rulesets.Osu/Edit/TransformToolboxGroup.cs index 3da9f5b69b..f8df45f545 100644 --- a/osu.Game.Rulesets.Osu/Edit/TransformToolboxGroup.cs +++ b/osu.Game.Rulesets.Osu/Edit/TransformToolboxGroup.cs @@ -24,6 +24,8 @@ namespace osu.Game.Rulesets.Osu.Edit public SelectionRotationHandler RotationHandler { get; init; } = null!; + public OsuGridToolboxGroup GridToolbox { get; init; } = null!; + public TransformToolboxGroup() : base("transform") { @@ -41,7 +43,7 @@ namespace osu.Game.Rulesets.Osu.Edit { rotateButton = new EditorToolButton("Rotate", () => new SpriteIcon { Icon = FontAwesome.Solid.Undo }, - () => new PreciseRotationPopover(RotationHandler)), + () => new PreciseRotationPopover(RotationHandler, GridToolbox)), // TODO: scale } }; From 09852bc46b6b0b7a78914e19ba3414efab60afdd Mon Sep 17 00:00:00 2001 From: OliBomby Date: Sun, 31 Dec 2023 21:23:13 +0100 Subject: [PATCH 0170/2493] fix horizontal vs vertical flips being mixed up when rotation angle is too big --- osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs index 021c735ebd..1cb206c2f8 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.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 osu.Framework.Allocation; @@ -107,9 +106,13 @@ namespace osu.Game.Rulesets.Osu.Edit { var hitObjects = selectedMovableObjects; + // If we're flipping over the origin, we take the grid origin position from the grid toolbox. var flipQuad = flipOverOrigin ? new Quad(gridToolbox.StartPositionX.Value, gridToolbox.StartPositionY.Value, 0, 0) : GeometryUtils.GetSurroundingQuad(hitObjects); - var flipAxis = flipOverOrigin ? new Vector2(MathF.Cos(MathHelper.DegreesToRadians(gridToolbox.GridLinesRotation.Value)), MathF.Sin(MathHelper.DegreesToRadians(gridToolbox.GridLinesRotation.Value))) : Vector2.UnitX; + // If we're flipping over the origin, we take the grid rotation from the grid toolbox. + // We want to normalize the rotation angle to -45 to 45 degrees, so horizontal vs vertical flip is not mixed up by the rotation and it stays intuitive to use. + var flipAxis = flipOverOrigin ? GeometryUtils.RotateVector(Vector2.UnitX, -((gridToolbox.GridLinesRotation.Value + 405) % 90 - 45)) : Vector2.UnitX; flipAxis = direction == Direction.Vertical ? flipAxis.PerpendicularLeft : flipAxis; + var controlPointFlipQuad = new Quad(); bool didFlip = false; From 460c584dca79ec4dc40df0f49e6721edcb6e6fa9 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Mon, 1 Jan 2024 16:21:33 +0100 Subject: [PATCH 0171/2493] fix code quality --- osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs b/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs index e82ca780ad..21cce553b1 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs @@ -6,7 +6,6 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; -using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osu.Game.Input.Bindings; using osu.Game.Rulesets.Edit; @@ -21,9 +20,6 @@ namespace osu.Game.Rulesets.Osu.Edit [Resolved] private EditorBeatmap editorBeatmap { get; set; } = null!; - [Resolved] - private IExpandingContainer? expandingContainer { get; set; } - /// /// X position of the grid's origin. /// From eea87090fb613538e670611bb9e2ad830f23fd83 Mon Sep 17 00:00:00 2001 From: B3nn1 Date: Sat, 6 Jan 2024 19:25:49 +0100 Subject: [PATCH 0172/2493] Make `changeHandler` save changes to `PathTypes` --- .../Blueprints/Sliders/Components/PathControlPointVisualiser.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs index 24e2210b45..0cef93fbb5 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs @@ -410,8 +410,10 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components var item = new TernaryStateRadioMenuItem(type?.Description ?? "Inherit", MenuItemType.Standard, _ => { + changeHandler?.BeginChange(); foreach (var p in Pieces.Where(p => p.IsSelected.Value)) updatePathType(p, type); + changeHandler?.EndChange(); }); if (countOfState == totalCount) From b74c3b1c5cbdd1d4bfc2b108bc9bd0a9b7b68e7f Mon Sep 17 00:00:00 2001 From: Nitrous Date: Wed, 10 Jan 2024 15:19:38 +0800 Subject: [PATCH 0173/2493] Make all hit objects before the start time marked as hit. --- osu.Game/Screens/Edit/Editor.cs | 2 +- .../Screens/Edit/GameplayTest/EditorPlayer.cs | 37 ++++++++++++++++++- .../Edit/GameplayTest/EditorPlayerLoader.cs | 5 ++- 3 files changed, 40 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index c1f6c02301..224823de70 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -452,7 +452,7 @@ namespace osu.Game.Screens.Edit pushEditorPlayer(); } - void pushEditorPlayer() => this.Push(new EditorPlayerLoader(this)); + void pushEditorPlayer() => this.Push(new EditorPlayerLoader(this, playableBeatmap)); } /// diff --git a/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs b/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs index 7dff05667d..47abcff476 100644 --- a/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs +++ b/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs @@ -1,11 +1,17 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Screens; using osu.Game.Beatmaps; +using osu.Game.Online.Spectator; using osu.Game.Overlays; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Replays; +using osu.Game.Rulesets.Scoring; +using osu.Game.Scoring; using osu.Game.Screens.Play; using osu.Game.Users; @@ -15,17 +21,19 @@ namespace osu.Game.Screens.Edit.GameplayTest { private readonly Editor editor; private readonly EditorState editorState; + private readonly IBeatmap playableBeatmap; protected override UserActivity InitialActivity => new UserActivity.TestingBeatmap(Beatmap.Value.BeatmapInfo, Ruleset.Value); [Resolved] private MusicController musicController { get; set; } = null!; - public EditorPlayer(Editor editor) + public EditorPlayer(Editor editor, IBeatmap playableBeatmap) : base(new PlayerConfiguration { ShowResults = false }) { this.editor = editor; editorState = editor.GetState(); + this.playableBeatmap = playableBeatmap; } protected override GameplayClockContainer CreateGameplayClockContainer(WorkingBeatmap beatmap, double gameplayStart) @@ -43,6 +51,22 @@ namespace osu.Game.Screens.Edit.GameplayTest protected override void LoadComplete() { base.LoadComplete(); + + var frame = new ReplayFrame { Header = new FrameHeader(new ScoreInfo(), new ScoreProcessorStatistics()) }; + + foreach (var hitObject in enumerateHitObjects(playableBeatmap.HitObjects.Where(h => h.StartTime < editorState.Time))) + { + var judgement = hitObject.CreateJudgement(); + + if (!frame.Header.Statistics.ContainsKey(judgement.MaxResult)) + frame.Header.Statistics.Add(judgement.MaxResult, 0); + + frame.Header.Statistics[judgement.MaxResult]++; + } + + HealthProcessor.ResetFromReplayFrame(frame); + ScoreProcessor.ResetFromReplayFrame(frame); + ScoreProcessor.HasCompleted.BindValueChanged(completed => { if (completed.NewValue) @@ -54,6 +78,17 @@ namespace osu.Game.Screens.Edit.GameplayTest }, RESULTS_DISPLAY_DELAY); } }); + + static IEnumerable enumerateHitObjects(IEnumerable hitObjects) + { + foreach (var hitObject in hitObjects) + { + foreach (var nested in hitObject.NestedHitObjects) + yield return nested; + + yield return hitObject; + } + } } protected override void PrepareReplay() diff --git a/osu.Game/Screens/Edit/GameplayTest/EditorPlayerLoader.cs b/osu.Game/Screens/Edit/GameplayTest/EditorPlayerLoader.cs index bb151e4a45..c62b8cafb8 100644 --- a/osu.Game/Screens/Edit/GameplayTest/EditorPlayerLoader.cs +++ b/osu.Game/Screens/Edit/GameplayTest/EditorPlayerLoader.cs @@ -4,6 +4,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Screens; +using osu.Game.Beatmaps; using osu.Game.Screens.Menu; using osu.Game.Screens.Play; @@ -14,8 +15,8 @@ namespace osu.Game.Screens.Edit.GameplayTest [Resolved] private OsuLogo osuLogo { get; set; } = null!; - public EditorPlayerLoader(Editor editor) - : base(() => new EditorPlayer(editor)) + public EditorPlayerLoader(Editor editor, IBeatmap playableBeatmap) + : base(() => new EditorPlayer(editor, playableBeatmap)) { } From 72e302dfac77c3b8a980e7f8026ace08eb95d191 Mon Sep 17 00:00:00 2001 From: Nitrous Date: Wed, 10 Jan 2024 15:27:41 +0800 Subject: [PATCH 0174/2493] Enumerate nested hit objects --- osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs b/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs index 47abcff476..e2b2b067e0 100644 --- a/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs +++ b/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs @@ -83,7 +83,7 @@ namespace osu.Game.Screens.Edit.GameplayTest { foreach (var hitObject in hitObjects) { - foreach (var nested in hitObject.NestedHitObjects) + foreach (var nested in enumerateHitObjects(hitObject.NestedHitObjects)) yield return nested; yield return hitObject; From aa83ac1896f0c54e15d967157f8ddd113a520342 Mon Sep 17 00:00:00 2001 From: Nitrous Date: Wed, 10 Jan 2024 15:53:54 +0800 Subject: [PATCH 0175/2493] add test case --- .../Editing/TestSceneEditorTestGameplay.cs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorTestGameplay.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorTestGameplay.cs index bbd7123f20..ccc17dc3f0 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorTestGameplay.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorTestGameplay.cs @@ -126,6 +126,24 @@ namespace osu.Game.Tests.Visual.Editing AddAssert("sample playback re-enabled", () => !Editor.SamplePlaybackDisabled.Value); } + [Test] + public void TestGameplayTestAtEndOfBeatmap() + { + AddStep("seek to last 2 seconds", () => EditorClock.Seek(importedBeatmapSet.MaxLength - 2000)); + AddStep("click test gameplay button", () => + { + var button = Editor.ChildrenOfType().Single(); + + InputManager.MoveMouseTo(button); + InputManager.Click(MouseButton.Left); + }); + + AddUntilStep("player pushed", () => Stack.CurrentScreen is EditorPlayer); + + AddWaitStep("wait some", 5); + AddAssert("current screen is editor", () => Stack.CurrentScreen is Editor); + } + [Test] public void TestCancelGameplayTestWithUnsavedChanges() { From 6cd255f549f6a7499bc2b87f90ce9fef2180e5ac Mon Sep 17 00:00:00 2001 From: Nitrous Date: Thu, 11 Jan 2024 11:36:58 +0800 Subject: [PATCH 0176/2493] `Contains` + `Add` to `TryAdd` --- osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs b/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs index e2b2b067e0..e1519f9a09 100644 --- a/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs +++ b/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs @@ -57,10 +57,7 @@ namespace osu.Game.Screens.Edit.GameplayTest foreach (var hitObject in enumerateHitObjects(playableBeatmap.HitObjects.Where(h => h.StartTime < editorState.Time))) { var judgement = hitObject.CreateJudgement(); - - if (!frame.Header.Statistics.ContainsKey(judgement.MaxResult)) - frame.Header.Statistics.Add(judgement.MaxResult, 0); - + frame.Header.Statistics.TryAdd(judgement.MaxResult, 0); frame.Header.Statistics[judgement.MaxResult]++; } From 26c0d1077a7a4d00fb9ae22bcb16dde08365b987 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Sat, 20 Jan 2024 00:22:53 +0100 Subject: [PATCH 0177/2493] Refactor scale handling in editor to facilitate reuse --- .../Edit/OsuSelectionHandler.cs | 142 +----------- .../Edit/OsuSelectionScaleHandler.cs | 205 ++++++++++++++++++ .../Edit/Compose/Components/SelectionBox.cs | 2 - .../Components/SelectionBoxScaleHandle.cs | 94 +++++++- .../Compose/Components/SelectionHandler.cs | 10 +- .../Components/SelectionScaleHandler.cs | 88 ++++++++ osu.Game/Utils/GeometryUtils.cs | 9 + 7 files changed, 402 insertions(+), 148 deletions(-) create mode 100644 osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs create mode 100644 osu.Game/Screens/Edit/Compose/Components/SelectionScaleHandler.cs diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs index cea2adc6e2..c36b535bfa 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Linq; -using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.UserInterface; @@ -25,15 +24,6 @@ namespace osu.Game.Rulesets.Osu.Edit { public partial class OsuSelectionHandler : EditorSelectionHandler { - [Resolved(CanBeNull = true)] - private IDistanceSnapProvider? snapProvider { get; set; } - - /// - /// During a transform, the initial path types of a single selected slider are stored so they - /// can be maintained throughout the operation. - /// - private List? referencePathTypes; - protected override void OnSelectionChanged() { base.OnSelectionChanged(); @@ -46,12 +36,6 @@ namespace osu.Game.Rulesets.Osu.Edit SelectionBox.CanReverse = EditorBeatmap.SelectedHitObjects.Count > 1 || EditorBeatmap.SelectedHitObjects.Any(s => s is Slider); } - protected override void OnOperationEnded() - { - base.OnOperationEnded(); - referencePathTypes = null; - } - protected override bool OnKeyDown(KeyDownEvent e) { if (e.Key == Key.M && e.ControlPressed && e.ShiftPressed) @@ -135,96 +119,9 @@ namespace osu.Game.Rulesets.Osu.Edit return didFlip; } - public override bool HandleScale(Vector2 scale, Anchor reference) - { - adjustScaleFromAnchor(ref scale, reference); - - var hitObjects = selectedMovableObjects; - - // for the time being, allow resizing of slider paths only if the slider is - // the only hit object selected. with a group selection, it's likely the user - // is not looking to change the duration of the slider but expand the whole pattern. - if (hitObjects.Length == 1 && hitObjects.First() is Slider slider) - scaleSlider(slider, scale); - else - scaleHitObjects(hitObjects, reference, scale); - - moveSelectionInBounds(); - return true; - } - - private static void adjustScaleFromAnchor(ref Vector2 scale, Anchor reference) - { - // cancel out scale in axes we don't care about (based on which drag handle was used). - if ((reference & Anchor.x1) > 0) scale.X = 0; - if ((reference & Anchor.y1) > 0) scale.Y = 0; - - // reverse the scale direction if dragging from top or left. - if ((reference & Anchor.x0) > 0) scale.X = -scale.X; - if ((reference & Anchor.y0) > 0) scale.Y = -scale.Y; - } - public override SelectionRotationHandler CreateRotationHandler() => new OsuSelectionRotationHandler(); - private void scaleSlider(Slider slider, Vector2 scale) - { - referencePathTypes ??= slider.Path.ControlPoints.Select(p => p.Type).ToList(); - - Quad sliderQuad = GeometryUtils.GetSurroundingQuad(slider.Path.ControlPoints.Select(p => p.Position)); - - // Limit minimum distance between control points after scaling to almost 0. Less than 0 causes the slider to flip, exactly 0 causes a crash through division by 0. - scale = Vector2.ComponentMax(new Vector2(Precision.FLOAT_EPSILON), sliderQuad.Size + scale) - sliderQuad.Size; - - Vector2 pathRelativeDeltaScale = new Vector2( - sliderQuad.Width == 0 ? 0 : 1 + scale.X / sliderQuad.Width, - sliderQuad.Height == 0 ? 0 : 1 + scale.Y / sliderQuad.Height); - - Queue oldControlPoints = new Queue(); - - foreach (var point in slider.Path.ControlPoints) - { - oldControlPoints.Enqueue(point.Position); - point.Position *= pathRelativeDeltaScale; - } - - // Maintain the path types in case they were defaulted to bezier at some point during scaling - for (int i = 0; i < slider.Path.ControlPoints.Count; ++i) - slider.Path.ControlPoints[i].Type = referencePathTypes[i]; - - // Snap the slider's length to the current beat divisor - // to calculate the final resulting duration / bounding box before the final checks. - slider.SnapTo(snapProvider); - - //if sliderhead or sliderend end up outside playfield, revert scaling. - Quad scaledQuad = GeometryUtils.GetSurroundingQuad(new OsuHitObject[] { slider }); - (bool xInBounds, bool yInBounds) = isQuadInBounds(scaledQuad); - - if (xInBounds && yInBounds && slider.Path.HasValidLength) - return; - - foreach (var point in slider.Path.ControlPoints) - point.Position = oldControlPoints.Dequeue(); - - // Snap the slider's length again to undo the potentially-invalid length applied by the previous snap. - slider.SnapTo(snapProvider); - } - - private void scaleHitObjects(OsuHitObject[] hitObjects, Anchor reference, Vector2 scale) - { - scale = getClampedScale(hitObjects, reference, scale); - Quad selectionQuad = GeometryUtils.GetSurroundingQuad(hitObjects); - - foreach (var h in hitObjects) - h.Position = GeometryUtils.GetScaledPosition(reference, scale, selectionQuad, h.Position); - } - - private (bool X, bool Y) isQuadInBounds(Quad quad) - { - bool xInBounds = (quad.TopLeft.X >= 0) && (quad.BottomRight.X <= DrawWidth); - bool yInBounds = (quad.TopLeft.Y >= 0) && (quad.BottomRight.Y <= DrawHeight); - - return (xInBounds, yInBounds); - } + public override SelectionScaleHandler CreateScaleHandler() => new OsuSelectionScaleHandler(); private void moveSelectionInBounds() { @@ -248,43 +145,6 @@ namespace osu.Game.Rulesets.Osu.Edit h.Position += delta; } - /// - /// Clamp scale for multi-object-scaling where selection does not exceed playfield bounds or flip. - /// - /// The hitobjects to be scaled - /// The anchor from which the scale operation is performed - /// The scale to be clamped - /// The clamped scale vector - private Vector2 getClampedScale(OsuHitObject[] hitObjects, Anchor reference, Vector2 scale) - { - float xOffset = ((reference & Anchor.x0) > 0) ? -scale.X : 0; - float yOffset = ((reference & Anchor.y0) > 0) ? -scale.Y : 0; - - Quad selectionQuad = GeometryUtils.GetSurroundingQuad(hitObjects); - - //todo: this is not always correct for selections involving sliders. This approximation assumes each point is scaled independently, but sliderends move with the sliderhead. - Quad scaledQuad = new Quad(selectionQuad.TopLeft.X + xOffset, selectionQuad.TopLeft.Y + yOffset, selectionQuad.Width + scale.X, selectionQuad.Height + scale.Y); - - //max Size -> playfield bounds - if (scaledQuad.TopLeft.X < 0) - scale.X += scaledQuad.TopLeft.X; - if (scaledQuad.TopLeft.Y < 0) - scale.Y += scaledQuad.TopLeft.Y; - - if (scaledQuad.BottomRight.X > DrawWidth) - scale.X -= scaledQuad.BottomRight.X - DrawWidth; - if (scaledQuad.BottomRight.Y > DrawHeight) - scale.Y -= scaledQuad.BottomRight.Y - DrawHeight; - - //min Size -> almost 0. Less than 0 causes the quad to flip, exactly 0 causes scaling to get stuck at minimum scale. - Vector2 scaledSize = selectionQuad.Size + scale; - Vector2 minSize = new Vector2(Precision.FLOAT_EPSILON); - - scale = Vector2.ComponentMax(minSize, scaledSize) - selectionQuad.Size; - - return scale; - } - /// /// All osu! hitobjects which can be moved/rotated/scaled. /// diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs new file mode 100644 index 0000000000..8068c73131 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs @@ -0,0 +1,205 @@ +// 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.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics.Primitives; +using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Osu.UI; +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 partial class OsuSelectionScaleHandler : SelectionScaleHandler + { + [Resolved] + private IEditorChangeHandler? changeHandler { get; set; } + + [Resolved(CanBeNull = true)] + private IDistanceSnapProvider? snapProvider { get; set; } + + private BindableList selectedItems { get; } = new BindableList(); + + [BackgroundDependencyLoader] + private void load(EditorBeatmap editorBeatmap) + { + selectedItems.BindTo(editorBeatmap.SelectedHitObjects); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + selectedItems.CollectionChanged += (_, __) => updateState(); + updateState(); + } + + private void updateState() + { + var quad = GeometryUtils.GetSurroundingQuad(selectedMovableObjects); + CanScale.Value = quad.Width > 0 || quad.Height > 0; + } + + private OsuHitObject[]? objectsInScale; + + private Vector2? defaultOrigin; + private Dictionary? originalPositions; + private Dictionary? originalPathControlPointPositions; + private Dictionary? originalPathControlPointTypes; + + public override void Begin() + { + if (objectsInScale != null) + throw new InvalidOperationException($"Cannot {nameof(Begin)} a scale operation while another is in progress!"); + + changeHandler?.BeginChange(); + + objectsInScale = selectedMovableObjects.ToArray(); + OriginalSurroundingQuad = objectsInScale.Length == 1 && objectsInScale.First() is Slider slider + ? GeometryUtils.GetSurroundingQuad(slider.Path.ControlPoints.Select(p => p.Position)) + : GeometryUtils.GetSurroundingQuad(objectsInScale); + defaultOrigin = OriginalSurroundingQuad.Value.Centre; + originalPositions = objectsInScale.ToDictionary(obj => obj, obj => obj.Position); + originalPathControlPointPositions = objectsInScale.OfType().ToDictionary( + obj => obj, + obj => obj.Path.ControlPoints.Select(point => point.Position).ToArray()); + originalPathControlPointTypes = objectsInScale.OfType().ToDictionary( + obj => obj, + obj => obj.Path.ControlPoints.Select(p => p.Type).ToArray()); + } + + public override void Update(Vector2 scale, Vector2? origin = null) + { + if (objectsInScale == null) + throw new InvalidOperationException($"Cannot {nameof(Update)} a scale operation without calling {nameof(Begin)} first!"); + + Debug.Assert(originalPositions != null && originalPathControlPointPositions != null && defaultOrigin != null && originalPathControlPointTypes != null && OriginalSurroundingQuad != null); + + Vector2 actualOrigin = origin ?? defaultOrigin.Value; + + // for the time being, allow resizing of slider paths only if the slider is + // the only hit object selected. with a group selection, it's likely the user + // is not looking to change the duration of the slider but expand the whole pattern. + if (objectsInScale.Length == 1 && objectsInScale.First() is Slider slider) + scaleSlider(slider, scale, originalPathControlPointPositions[slider], originalPathControlPointTypes[slider]); + else + { + scale = getClampedScale(OriginalSurroundingQuad.Value, actualOrigin, scale); + + foreach (var ho in objectsInScale) + { + ho.Position = GeometryUtils.GetScaledPositionMultiply(scale, actualOrigin, originalPositions[ho]); + } + } + + moveSelectionInBounds(); + } + + public override void Commit() + { + if (objectsInScale == null) + throw new InvalidOperationException($"Cannot {nameof(Commit)} a rotate operation without calling {nameof(Begin)} first!"); + + changeHandler?.EndChange(); + + objectsInScale = null; + OriginalSurroundingQuad = null; + originalPositions = null; + originalPathControlPointPositions = null; + originalPathControlPointTypes = null; + defaultOrigin = null; + } + + private IEnumerable selectedMovableObjects => selectedItems.Cast() + .Where(h => h is not Spinner); + + private void scaleSlider(Slider slider, Vector2 scale, Vector2[] originalPathPositions, PathType?[] originalPathTypes) + { + // Maintain the path types in case they were defaulted to bezier at some point during scaling + for (int i = 0; i < slider.Path.ControlPoints.Count; i++) + { + slider.Path.ControlPoints[i].Position = originalPathPositions[i] * scale; + slider.Path.ControlPoints[i].Type = originalPathTypes[i]; + } + + // Snap the slider's length to the current beat divisor + // to calculate the final resulting duration / bounding box before the final checks. + slider.SnapTo(snapProvider); + + //if sliderhead or sliderend end up outside playfield, revert scaling. + Quad scaledQuad = GeometryUtils.GetSurroundingQuad(new OsuHitObject[] { slider }); + (bool xInBounds, bool yInBounds) = isQuadInBounds(scaledQuad); + + if (xInBounds && yInBounds && slider.Path.HasValidLength) + return; + + for (int i = 0; i < slider.Path.ControlPoints.Count; i++) + slider.Path.ControlPoints[i].Position = originalPathPositions[i]; + + // Snap the slider's length again to undo the potentially-invalid length applied by the previous snap. + slider.SnapTo(snapProvider); + } + + private (bool X, bool Y) isQuadInBounds(Quad quad) + { + bool xInBounds = (quad.TopLeft.X >= 0) && (quad.BottomRight.X <= OsuPlayfield.BASE_SIZE.X); + bool yInBounds = (quad.TopLeft.Y >= 0) && (quad.BottomRight.Y <= OsuPlayfield.BASE_SIZE.Y); + + return (xInBounds, yInBounds); + } + + /// + /// Clamp scale for multi-object-scaling where selection does not exceed playfield bounds or flip. + /// + /// The quad surrounding the hitobjects + /// The origin from which the scale operation is performed + /// The scale to be clamped + /// The clamped scale vector + private Vector2 getClampedScale(Quad selectionQuad, Vector2 origin, Vector2 scale) + { + //todo: this is not always correct for selections involving sliders. This approximation assumes each point is scaled independently, but sliderends move with the sliderhead. + + var tl1 = Vector2.Divide(-origin, selectionQuad.TopLeft - origin); + var tl2 = Vector2.Divide(OsuPlayfield.BASE_SIZE - origin, selectionQuad.TopLeft - origin); + var br1 = Vector2.Divide(-origin, selectionQuad.BottomRight - origin); + var br2 = Vector2.Divide(OsuPlayfield.BASE_SIZE - origin, selectionQuad.BottomRight - origin); + + scale.X = selectionQuad.TopLeft.X - origin.X < 0 ? MathHelper.Clamp(scale.X, tl2.X, tl1.X) : MathHelper.Clamp(scale.X, tl1.X, tl2.X); + scale.Y = selectionQuad.TopLeft.Y - origin.Y < 0 ? MathHelper.Clamp(scale.Y, tl2.Y, tl1.Y) : MathHelper.Clamp(scale.Y, tl1.Y, tl2.Y); + scale.X = selectionQuad.BottomRight.X - origin.X < 0 ? MathHelper.Clamp(scale.X, br2.X, br1.X) : MathHelper.Clamp(scale.X, br1.X, br2.X); + scale.Y = selectionQuad.BottomRight.Y - origin.Y < 0 ? MathHelper.Clamp(scale.Y, br2.Y, br1.Y) : MathHelper.Clamp(scale.Y, br1.Y, br2.Y); + + return scale; + } + + private void moveSelectionInBounds() + { + Quad quad = GeometryUtils.GetSurroundingQuad(objectsInScale!); + + Vector2 delta = Vector2.Zero; + + if (quad.TopLeft.X < 0) + delta.X -= quad.TopLeft.X; + if (quad.TopLeft.Y < 0) + delta.Y -= quad.TopLeft.Y; + + if (quad.BottomRight.X > OsuPlayfield.BASE_SIZE.X) + delta.X -= quad.BottomRight.X - OsuPlayfield.BASE_SIZE.X; + if (quad.BottomRight.Y > OsuPlayfield.BASE_SIZE.Y) + delta.Y -= quad.BottomRight.Y - OsuPlayfield.BASE_SIZE.Y; + + foreach (var h in objectsInScale!) + h.Position += delta; + } + } +} diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs index 0b16941bc4..e8b3e430eb 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs @@ -27,7 +27,6 @@ namespace osu.Game.Screens.Edit.Compose.Components [Resolved] private SelectionRotationHandler? rotationHandler { get; set; } - public Func? OnScale; public Func? OnFlip; public Func? OnReverse; @@ -353,7 +352,6 @@ namespace osu.Game.Screens.Edit.Compose.Components var handle = new SelectionBoxScaleHandle { Anchor = anchor, - HandleScale = (delta, a) => OnScale?.Invoke(delta, a) }; handle.OperationStarted += operationStarted; diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionBoxScaleHandle.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionBoxScaleHandle.cs index 7943065c82..56c5585ae7 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionBoxScaleHandle.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionBoxScaleHandle.cs @@ -1,19 +1,22 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Input.Events; using osuTK; +using osuTK.Input; namespace osu.Game.Screens.Edit.Compose.Components { public partial class SelectionBoxScaleHandle : SelectionBoxDragHandle { - public Action HandleScale { get; set; } + [Resolved] + private SelectionBox selectionBox { get; set; } = null!; + + [Resolved] + private SelectionScaleHandler? scaleHandler { get; set; } [BackgroundDependencyLoader] private void load() @@ -21,10 +24,93 @@ namespace osu.Game.Screens.Edit.Compose.Components Size = new Vector2(10); } + protected override bool OnDragStart(DragStartEvent e) + { + if (e.Button != MouseButton.Left) + return false; + + if (scaleHandler == null) return false; + + scaleHandler.Begin(); + return true; + } + + private Vector2 getOriginPosition() + { + var quad = scaleHandler!.OriginalSurroundingQuad!.Value; + Vector2 origin = quad.TopLeft; + + if ((Anchor & Anchor.x0) > 0) + origin.X += quad.Width; + + if ((Anchor & Anchor.y0) > 0) + origin.Y += quad.Height; + + return origin; + } + + private Vector2 rawScale; + protected override void OnDrag(DragEvent e) { - HandleScale?.Invoke(e.Delta, Anchor); base.OnDrag(e); + + if (scaleHandler == null) return; + + rawScale = convertDragEventToScaleMultiplier(e); + + applyScale(shouldKeepAspectRatio: e.ShiftPressed); + } + + protected override bool OnKeyDown(KeyDownEvent e) + { + if (IsDragged && (e.Key == Key.ShiftLeft || e.Key == Key.ShiftRight)) + { + applyScale(shouldKeepAspectRatio: true); + return true; + } + + return base.OnKeyDown(e); + } + + protected override void OnKeyUp(KeyUpEvent e) + { + base.OnKeyUp(e); + + if (IsDragged && (e.Key == Key.ShiftLeft || e.Key == Key.ShiftRight)) + applyScale(shouldKeepAspectRatio: false); + } + + protected override void OnDragEnd(DragEndEvent e) + { + scaleHandler?.Commit(); + } + + private Vector2 convertDragEventToScaleMultiplier(DragEvent e) + { + Vector2 scale = e.MousePosition - e.MouseDownPosition; + adjustScaleFromAnchor(ref scale); + return Vector2.Divide(scale, scaleHandler!.OriginalSurroundingQuad!.Value.Size) + Vector2.One; + } + + private void adjustScaleFromAnchor(ref Vector2 scale) + { + // cancel out scale in axes we don't care about (based on which drag handle was used). + if ((Anchor & Anchor.x1) > 0) scale.X = 1; + if ((Anchor & Anchor.y1) > 0) scale.Y = 1; + + // reverse the scale direction if dragging from top or left. + if ((Anchor & Anchor.x0) > 0) scale.X = -scale.X; + if ((Anchor & Anchor.y0) > 0) scale.Y = -scale.Y; + } + + private void applyScale(bool shouldKeepAspectRatio) + { + var newScale = shouldKeepAspectRatio + ? new Vector2(MathF.Max(rawScale.X, rawScale.Y)) + : rawScale; + + scaleHandler!.Update(newScale, getOriginPosition()); } } } diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs index 3c859c65ff..dd6bd43f4d 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs @@ -57,6 +57,8 @@ namespace osu.Game.Screens.Edit.Compose.Components public SelectionRotationHandler RotationHandler { get; private set; } + public SelectionScaleHandler ScaleHandler { get; private set; } + protected SelectionHandler() { selectedBlueprints = new List>(); @@ -69,6 +71,7 @@ namespace osu.Game.Screens.Edit.Compose.Components { var dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); dependencies.CacheAs(RotationHandler = CreateRotationHandler()); + dependencies.CacheAs(ScaleHandler = CreateScaleHandler()); return dependencies; } @@ -78,6 +81,7 @@ namespace osu.Game.Screens.Edit.Compose.Components AddRangeInternal(new Drawable[] { RotationHandler, + ScaleHandler, SelectionBox = CreateSelectionBox(), }); @@ -93,7 +97,6 @@ namespace osu.Game.Screens.Edit.Compose.Components OperationStarted = OnOperationBegan, OperationEnded = OnOperationEnded, - OnScale = HandleScale, OnFlip = HandleFlip, OnReverse = HandleReverse, }; @@ -157,6 +160,11 @@ namespace osu.Game.Screens.Edit.Compose.Components /// Whether any items could be scaled. public virtual bool HandleScale(Vector2 scale, Anchor anchor) => false; + /// + /// Creates the handler to use for scale operations. + /// + public virtual SelectionScaleHandler CreateScaleHandler() => new SelectionScaleHandler(); + /// /// Handles the selected items being flipped. /// diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionScaleHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionScaleHandler.cs new file mode 100644 index 0000000000..b7c8f16a02 --- /dev/null +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionScaleHandler.cs @@ -0,0 +1,88 @@ +// 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 osu.Framework.Graphics; +using osu.Framework.Graphics.Primitives; +using osuTK; + +namespace osu.Game.Screens.Edit.Compose.Components +{ + /// + /// Base handler for editor scale operations. + /// + public partial class SelectionScaleHandler : Component + { + /// + /// Whether the scale can currently be performed. + /// + public Bindable CanScale { get; private set; } = new BindableBool(); + + public Quad? OriginalSurroundingQuad { get; protected set; } + + /// + /// Performs a single, instant, atomic scale operation. + /// + /// + /// This method is intended to be used in atomic contexts (such as when pressing a single button). + /// For continuous operations, see the -- flow. + /// + /// The scale to apply, as multiplier. + /// + /// The origin point to scale from. + /// If the default value is supplied, a sane implementation-defined default will be used. + /// + public void ScaleSelection(Vector2 scale, Vector2? origin = null) + { + Begin(); + Update(scale, origin); + Commit(); + } + + /// + /// Begins a continuous scale operation. + /// + /// + /// This flow is intended to be used when a scale operation is made incrementally (such as when dragging a scale handle or slider). + /// For instantaneous, atomic operations, use the convenience method. + /// + public virtual void Begin() + { + } + + /// + /// Updates a continuous scale operation. + /// Must be preceded by a call. + /// + /// + /// + /// This flow is intended to be used when a scale operation is made incrementally (such as when dragging a scale handle or slider). + /// As such, the values of and supplied should be relative to the state of the objects being scaled + /// when was called, rather than instantaneous deltas. + /// + /// + /// For instantaneous, atomic operations, use the convenience method. + /// + /// + /// The Scale to apply, as multiplier. + /// + /// The origin point to scale from. + /// If the default value is supplied, a sane implementation-defined default will be used. + /// + public virtual void Update(Vector2 scale, Vector2? origin = null) + { + } + + /// + /// Ends a continuous scale operation. + /// Must be preceded by a call. + /// + /// + /// This flow is intended to be used when a scale operation is made incrementally (such as when dragging a scale handle or slider). + /// For instantaneous, atomic operations, use the convenience method. + /// + public virtual void Commit() + { + } + } +} diff --git a/osu.Game/Utils/GeometryUtils.cs b/osu.Game/Utils/GeometryUtils.cs index 725e93d098..ef362d8223 100644 --- a/osu.Game/Utils/GeometryUtils.cs +++ b/osu.Game/Utils/GeometryUtils.cs @@ -79,6 +79,15 @@ namespace osu.Game.Utils return position; } + /// + /// Given a scale multiplier, an origin, and a position, + /// will return the scaled position in screen space coordinates. + /// + public static Vector2 GetScaledPositionMultiply(Vector2 scale, Vector2 origin, Vector2 position) + { + return origin + (position - origin) * scale; + } + /// /// Returns a quad surrounding the provided points. /// From a4f771ec089baff91ddea3d4714355e64a8237dd Mon Sep 17 00:00:00 2001 From: OliBomby Date: Sat, 20 Jan 2024 01:13:01 +0100 Subject: [PATCH 0178/2493] refactor CanScale properties --- .../Edit/OsuSelectionHandler.cs | 5 +- .../Edit/OsuSelectionScaleHandler.cs | 7 +- .../SkinEditor/SkinSelectionHandler.cs | 3 - .../Edit/Compose/Components/SelectionBox.cs | 76 +++++-------------- .../Components/SelectionScaleHandler.cs | 18 ++++- osu.Game/Utils/GeometryUtils.cs | 2 +- 6 files changed, 44 insertions(+), 67 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs index c36b535bfa..00c90cdbd6 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs @@ -30,9 +30,8 @@ namespace osu.Game.Rulesets.Osu.Edit Quad quad = selectedMovableObjects.Length > 0 ? GeometryUtils.GetSurroundingQuad(selectedMovableObjects) : new Quad(); - SelectionBox.CanFlipX = SelectionBox.CanScaleX = quad.Width > 0; - SelectionBox.CanFlipY = SelectionBox.CanScaleY = quad.Height > 0; - SelectionBox.CanScaleDiagonally = SelectionBox.CanScaleX && SelectionBox.CanScaleY; + SelectionBox.CanFlipX = quad.Width > 0; + SelectionBox.CanFlipY = quad.Height > 0; SelectionBox.CanReverse = EditorBeatmap.SelectedHitObjects.Count > 1 || EditorBeatmap.SelectedHitObjects.Any(s => s is Slider); } diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs index 8068c73131..7b0ae947e7 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs @@ -47,7 +47,10 @@ namespace osu.Game.Rulesets.Osu.Edit private void updateState() { var quad = GeometryUtils.GetSurroundingQuad(selectedMovableObjects); - CanScale.Value = quad.Width > 0 || quad.Height > 0; + + CanScaleX.Value = quad.Width > 0; + CanScaleY.Value = quad.Height > 0; + CanScaleDiagonally.Value = CanScaleX.Value && CanScaleY.Value; } private OsuHitObject[]? objectsInScale; @@ -98,7 +101,7 @@ namespace osu.Game.Rulesets.Osu.Edit foreach (var ho in objectsInScale) { - ho.Position = GeometryUtils.GetScaledPositionMultiply(scale, actualOrigin, originalPositions[ho]); + ho.Position = GeometryUtils.GetScaledPosition(scale, actualOrigin, originalPositions[ho]); } } diff --git a/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs b/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs index cf6fb60636..efca6f0080 100644 --- a/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs +++ b/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs @@ -218,9 +218,6 @@ namespace osu.Game.Overlays.SkinEditor { base.OnSelectionChanged(); - SelectionBox.CanScaleX = allSelectedSupportManualSizing(Axes.X); - SelectionBox.CanScaleY = allSelectedSupportManualSizing(Axes.Y); - SelectionBox.CanScaleDiagonally = true; SelectionBox.CanFlipX = true; SelectionBox.CanFlipY = true; SelectionBox.CanReverse = false; diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs index e8b3e430eb..2329a466fe 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs @@ -27,6 +27,9 @@ namespace osu.Game.Screens.Edit.Compose.Components [Resolved] private SelectionRotationHandler? rotationHandler { get; set; } + [Resolved] + private SelectionScaleHandler? scaleHandler { get; set; } + public Func? OnFlip; public Func? OnReverse; @@ -56,60 +59,11 @@ namespace osu.Game.Screens.Edit.Compose.Components private readonly IBindable canRotate = new BindableBool(); - private bool canScaleX; + private readonly IBindable canScaleX = new BindableBool(); - /// - /// Whether horizontal scaling (from the left or right edge) support should be enabled. - /// - public bool CanScaleX - { - get => canScaleX; - set - { - if (canScaleX == value) return; + private readonly IBindable canScaleY = new BindableBool(); - canScaleX = value; - recreate(); - } - } - - private bool canScaleY; - - /// - /// Whether vertical scaling (from the top or bottom edge) support should be enabled. - /// - public bool CanScaleY - { - get => canScaleY; - set - { - if (canScaleY == value) return; - - canScaleY = value; - recreate(); - } - } - - private bool canScaleDiagonally; - - /// - /// Whether diagonal scaling (from a corner) support should be enabled. - /// - /// - /// There are some cases where we only want to allow proportional resizing, and not allow - /// one or both explicit directions of scale. - /// - public bool CanScaleDiagonally - { - get => canScaleDiagonally; - set - { - if (canScaleDiagonally == value) return; - - canScaleDiagonally = value; - recreate(); - } - } + private readonly IBindable canScaleDiagonally = new BindableBool(); private bool canFlipX; @@ -175,7 +129,17 @@ namespace osu.Game.Screens.Edit.Compose.Components if (rotationHandler != null) canRotate.BindTo(rotationHandler.CanRotate); - canRotate.BindValueChanged(_ => recreate(), true); + if (scaleHandler != null) + { + canScaleX.BindTo(scaleHandler.CanScaleX); + canScaleY.BindTo(scaleHandler.CanScaleY); + canScaleDiagonally.BindTo(scaleHandler.CanScaleDiagonally); + } + + canRotate.BindValueChanged(_ => recreate()); + canScaleX.BindValueChanged(_ => recreate()); + canScaleY.BindValueChanged(_ => recreate()); + canScaleDiagonally.BindValueChanged(_ => recreate(), true); } protected override bool OnKeyDown(KeyDownEvent e) @@ -264,9 +228,9 @@ namespace osu.Game.Screens.Edit.Compose.Components } }; - if (CanScaleX) addXScaleComponents(); - if (CanScaleDiagonally) addFullScaleComponents(); - if (CanScaleY) addYScaleComponents(); + if (canScaleX.Value) addXScaleComponents(); + if (canScaleDiagonally.Value) addFullScaleComponents(); + if (canScaleY.Value) addYScaleComponents(); if (CanFlipX) addXFlipComponents(); if (CanFlipY) addYFlipComponents(); if (canRotate.Value) addRotationComponents(); diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionScaleHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionScaleHandler.cs index b7c8f16a02..59406b3184 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionScaleHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionScaleHandler.cs @@ -14,9 +14,23 @@ namespace osu.Game.Screens.Edit.Compose.Components public partial class SelectionScaleHandler : Component { /// - /// Whether the scale can currently be performed. + /// Whether horizontal scaling (from the left or right edge) support should be enabled. /// - public Bindable CanScale { get; private set; } = new BindableBool(); + public Bindable CanScaleX { get; private set; } = new BindableBool(); + + /// + /// Whether vertical scaling (from the top or bottom edge) support should be enabled. + /// + public Bindable CanScaleY { get; private set; } = new BindableBool(); + + /// + /// Whether diagonal scaling (from a corner) support should be enabled. + /// + /// + /// There are some cases where we only want to allow proportional resizing, and not allow + /// one or both explicit directions of scale. + /// + public Bindable CanScaleDiagonally { get; private set; } = new BindableBool(); public Quad? OriginalSurroundingQuad { get; protected set; } diff --git a/osu.Game/Utils/GeometryUtils.cs b/osu.Game/Utils/GeometryUtils.cs index ef362d8223..6d8237ea34 100644 --- a/osu.Game/Utils/GeometryUtils.cs +++ b/osu.Game/Utils/GeometryUtils.cs @@ -83,7 +83,7 @@ namespace osu.Game.Utils /// Given a scale multiplier, an origin, and a position, /// will return the scaled position in screen space coordinates. /// - public static Vector2 GetScaledPositionMultiply(Vector2 scale, Vector2 origin, Vector2 position) + public static Vector2 GetScaledPosition(Vector2 scale, Vector2 origin, Vector2 position) { return origin + (position - origin) * scale; } From bc0e6baba70cd9c69dbf9f87180c24c8a47dcff9 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Sat, 20 Jan 2024 01:13:05 +0100 Subject: [PATCH 0179/2493] fix test --- .../Editing/TestSceneComposeSelectBox.cs | 77 ++++++++++++------- 1 file changed, 51 insertions(+), 26 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneComposeSelectBox.cs b/osu.Game.Tests/Visual/Editing/TestSceneComposeSelectBox.cs index f6637d0e80..680a76f9b8 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneComposeSelectBox.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneComposeSelectBox.cs @@ -10,9 +10,11 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Primitives; using osu.Framework.Testing; using osu.Framework.Threading; using osu.Game.Screens.Edit.Compose.Components; +using osu.Game.Utils; using osuTK; using osuTK.Input; @@ -26,9 +28,13 @@ namespace osu.Game.Tests.Visual.Editing [Cached(typeof(SelectionRotationHandler))] private TestSelectionRotationHandler rotationHandler; + [Cached(typeof(SelectionScaleHandler))] + private TestSelectionScaleHandler scaleHandler; + public TestSceneComposeSelectBox() { rotationHandler = new TestSelectionRotationHandler(() => selectionArea); + scaleHandler = new TestSelectionScaleHandler(() => selectionArea); } [SetUp] @@ -45,13 +51,8 @@ namespace osu.Game.Tests.Visual.Editing { RelativeSizeAxes = Axes.Both, - CanScaleX = true, - CanScaleY = true, - CanScaleDiagonally = true, CanFlipX = true, CanFlipY = true, - - OnScale = handleScale } } }; @@ -60,27 +61,6 @@ namespace osu.Game.Tests.Visual.Editing InputManager.ReleaseButton(MouseButton.Left); }); - private bool handleScale(Vector2 amount, Anchor reference) - { - if ((reference & Anchor.y1) == 0) - { - int directionY = (reference & Anchor.y0) > 0 ? -1 : 1; - if (directionY < 0) - selectionArea.Y += amount.Y; - selectionArea.Height += directionY * amount.Y; - } - - if ((reference & Anchor.x1) == 0) - { - int directionX = (reference & Anchor.x0) > 0 ? -1 : 1; - if (directionX < 0) - selectionArea.X += amount.X; - selectionArea.Width += directionX * amount.X; - } - - return true; - } - private partial class TestSelectionRotationHandler : SelectionRotationHandler { private readonly Func getTargetContainer; @@ -125,6 +105,51 @@ namespace osu.Game.Tests.Visual.Editing } } + private partial class TestSelectionScaleHandler : SelectionScaleHandler + { + private readonly Func getTargetContainer; + + public TestSelectionScaleHandler(Func getTargetContainer) + { + this.getTargetContainer = getTargetContainer; + + CanScaleX.Value = true; + CanScaleY.Value = true; + CanScaleDiagonally.Value = true; + } + + [CanBeNull] + private Container targetContainer; + + public override void Begin() + { + if (targetContainer != null) + throw new InvalidOperationException($"Cannot {nameof(Begin)} a scale operation while another is in progress!"); + + targetContainer = getTargetContainer(); + OriginalSurroundingQuad = new Quad(targetContainer!.X, targetContainer.Y, targetContainer.Width, targetContainer.Height); + } + + public override void Update(Vector2 scale, Vector2? origin = null) + { + if (targetContainer == null) + throw new InvalidOperationException($"Cannot {nameof(Update)} a scale operation without calling {nameof(Begin)} first!"); + + Vector2 actualOrigin = origin ?? Vector2.Zero; + + targetContainer.Position = GeometryUtils.GetScaledPosition(scale, actualOrigin, OriginalSurroundingQuad!.Value.TopLeft); + targetContainer.Size = OriginalSurroundingQuad!.Value.Size * scale; + } + + public override void Commit() + { + if (targetContainer == null) + throw new InvalidOperationException($"Cannot {nameof(Commit)} a scale operation without calling {nameof(Begin)} first!"); + + targetContainer = null; + } + } + [Test] public void TestRotationHandleShownOnHover() { From ed430a3df4bbcacf5860db8f21ac625d2a176bbc Mon Sep 17 00:00:00 2001 From: OliBomby Date: Sat, 20 Jan 2024 02:49:56 +0100 Subject: [PATCH 0180/2493] refactor skin editor scale --- .../SkinEditor/SkinSelectionHandler.cs | 157 +------------- .../SkinEditor/SkinSelectionScaleHandler.cs | 198 ++++++++++++++++++ 2 files changed, 204 insertions(+), 151 deletions(-) create mode 100644 osu.Game/Overlays/SkinEditor/SkinSelectionScaleHandler.cs diff --git a/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs b/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs index efca6f0080..2d8db61ee7 100644 --- a/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs +++ b/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs @@ -7,10 +7,8 @@ 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.Primitives; using osu.Framework.Graphics.UserInterface; -using osu.Framework.Utils; using osu.Game.Extensions; using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets.Edit; @@ -31,148 +29,16 @@ namespace osu.Game.Overlays.SkinEditor UpdatePosition = updateDrawablePosition }; - private bool allSelectedSupportManualSizing(Axes axis) => SelectedItems.All(b => (b as CompositeDrawable)?.AutoSizeAxes.HasFlagFast(axis) == false); - - public override bool HandleScale(Vector2 scale, Anchor anchor) + public override SelectionScaleHandler CreateScaleHandler() { - Axes adjustAxis; - - switch (anchor) + var scaleHandler = new SkinSelectionScaleHandler { - // for corners, adjust scale. - case Anchor.TopLeft: - case Anchor.TopRight: - case Anchor.BottomLeft: - case Anchor.BottomRight: - adjustAxis = Axes.Both; - break; + UpdatePosition = updateDrawablePosition + }; - // for edges, adjust size. - // autosize elements can't be easily handled so just disable sizing for now. - case Anchor.TopCentre: - case Anchor.BottomCentre: - if (!allSelectedSupportManualSizing(Axes.Y)) - return false; + scaleHandler.PerformFlipFromScaleHandles += a => SelectionBox.PerformFlipFromScaleHandles(a); - adjustAxis = Axes.Y; - break; - - case Anchor.CentreLeft: - case Anchor.CentreRight: - if (!allSelectedSupportManualSizing(Axes.X)) - return false; - - adjustAxis = Axes.X; - break; - - default: - throw new ArgumentOutOfRangeException(nameof(anchor), anchor, null); - } - - // convert scale to screen space - scale = ToScreenSpace(scale) - ToScreenSpace(Vector2.Zero); - - adjustScaleFromAnchor(ref scale, anchor); - - // the selection quad is always upright, so use an AABB rect to make mutating the values easier. - var selectionRect = getSelectionQuad().AABBFloat; - - // If the selection has no area we cannot scale it - if (selectionRect.Area == 0) - return false; - - // copy to mutate, as we will need to compare to the original later on. - var adjustedRect = selectionRect; - bool isRotated = false; - - // for now aspect lock scale adjustments that occur at corners.. - if (!anchor.HasFlagFast(Anchor.x1) && !anchor.HasFlagFast(Anchor.y1)) - { - // project scale vector along diagonal - Vector2 diag = (selectionRect.TopLeft - selectionRect.BottomRight).Normalized(); - scale = Vector2.Dot(scale, diag) * diag; - } - // ..or if any of the selection have been rotated. - // this is to avoid requiring skew logic (which would likely not be the user's expected transform anyway). - else if (SelectedBlueprints.Any(b => !Precision.AlmostEquals(((Drawable)b.Item).Rotation % 90, 0))) - { - isRotated = true; - if (anchor.HasFlagFast(Anchor.x1)) - // if dragging from the horizontal centre, only a vertical component is available. - scale.X = scale.Y / selectionRect.Height * selectionRect.Width; - else - // in all other cases (arbitrarily) use the horizontal component for aspect lock. - scale.Y = scale.X / selectionRect.Width * selectionRect.Height; - } - - if (anchor.HasFlagFast(Anchor.x0)) adjustedRect.X -= scale.X; - if (anchor.HasFlagFast(Anchor.y0)) adjustedRect.Y -= scale.Y; - - // Maintain the selection's centre position if dragging from the centre anchors and selection is rotated. - if (isRotated && anchor.HasFlagFast(Anchor.x1)) adjustedRect.X -= scale.X / 2; - if (isRotated && anchor.HasFlagFast(Anchor.y1)) adjustedRect.Y -= scale.Y / 2; - - adjustedRect.Width += scale.X; - adjustedRect.Height += scale.Y; - - if (adjustedRect.Width <= 0 || adjustedRect.Height <= 0) - { - Axes toFlip = Axes.None; - - if (adjustedRect.Width <= 0) toFlip |= Axes.X; - if (adjustedRect.Height <= 0) toFlip |= Axes.Y; - - SelectionBox.PerformFlipFromScaleHandles(toFlip); - return true; - } - - // scale adjust applied to each individual item should match that of the quad itself. - var scaledDelta = new Vector2( - adjustedRect.Width / selectionRect.Width, - adjustedRect.Height / selectionRect.Height - ); - - foreach (var b in SelectedBlueprints) - { - var drawableItem = (Drawable)b.Item; - - // each drawable's relative position should be maintained in the scaled quad. - var screenPosition = b.ScreenSpaceSelectionPoint; - - var relativePositionInOriginal = - new Vector2( - (screenPosition.X - selectionRect.TopLeft.X) / selectionRect.Width, - (screenPosition.Y - selectionRect.TopLeft.Y) / selectionRect.Height - ); - - var newPositionInAdjusted = new Vector2( - adjustedRect.TopLeft.X + adjustedRect.Width * relativePositionInOriginal.X, - adjustedRect.TopLeft.Y + adjustedRect.Height * relativePositionInOriginal.Y - ); - - updateDrawablePosition(drawableItem, newPositionInAdjusted); - - var currentScaledDelta = scaledDelta; - if (Precision.AlmostEquals(MathF.Abs(drawableItem.Rotation) % 180, 90)) - currentScaledDelta = new Vector2(scaledDelta.Y, scaledDelta.X); - - switch (adjustAxis) - { - case Axes.X: - drawableItem.Width *= currentScaledDelta.X; - break; - - case Axes.Y: - drawableItem.Height *= currentScaledDelta.Y; - break; - - case Axes.Both: - drawableItem.Scale *= currentScaledDelta; - break; - } - } - - return true; + return scaleHandler; } public override bool HandleFlip(Direction direction, bool flipOverOrigin) @@ -410,16 +276,5 @@ namespace osu.Game.Overlays.SkinEditor drawable.Anchor = anchor; drawable.Position -= drawable.AnchorPosition - previousAnchor; } - - private static void adjustScaleFromAnchor(ref Vector2 scale, Anchor reference) - { - // cancel out scale in axes we don't care about (based on which drag handle was used). - if ((reference & Anchor.x1) > 0) scale.X = 0; - if ((reference & Anchor.y1) > 0) scale.Y = 0; - - // reverse the scale direction if dragging from top or left. - if ((reference & Anchor.x0) > 0) scale.X = -scale.X; - if ((reference & Anchor.y0) > 0) scale.Y = -scale.Y; - } } } diff --git a/osu.Game/Overlays/SkinEditor/SkinSelectionScaleHandler.cs b/osu.Game/Overlays/SkinEditor/SkinSelectionScaleHandler.cs new file mode 100644 index 0000000000..46b39645b2 --- /dev/null +++ b/osu.Game/Overlays/SkinEditor/SkinSelectionScaleHandler.cs @@ -0,0 +1,198 @@ +// 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.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Extensions.EnumExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Utils; +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 partial class SkinSelectionScaleHandler : SelectionScaleHandler + { + public Action UpdatePosition { get; init; } = null!; + + public event Action? PerformFlipFromScaleHandles; + + [Resolved] + private IEditorChangeHandler? changeHandler { get; set; } + + private BindableList selectedItems { get; } = new BindableList(); + + [BackgroundDependencyLoader] + private void load(SkinEditor skinEditor) + { + selectedItems.BindTo(skinEditor.SelectedComponents); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + selectedItems.CollectionChanged += (_, __) => updateState(); + updateState(); + } + + private void updateState() + { + CanScaleX.Value = allSelectedSupportManualSizing(Axes.X); + CanScaleY.Value = allSelectedSupportManualSizing(Axes.Y); + CanScaleDiagonally.Value = true; + } + + private bool allSelectedSupportManualSizing(Axes axis) => selectedItems.All(b => (b as CompositeDrawable)?.AutoSizeAxes.HasFlagFast(axis) == false); + + private Drawable[]? objectsInScale; + + private Vector2? defaultOrigin; + private Dictionary? originalWidths; + private Dictionary? originalHeights; + private Dictionary? originalScales; + private Dictionary? originalPositions; + + public override void Begin() + { + if (objectsInScale != null) + throw new InvalidOperationException($"Cannot {nameof(Begin)} a scale operation while another is in progress!"); + + changeHandler?.BeginChange(); + + objectsInScale = selectedItems.Cast().ToArray(); + originalWidths = objectsInScale.ToDictionary(d => d, d => d.Width); + originalHeights = objectsInScale.ToDictionary(d => d, d => d.Height); + originalScales = objectsInScale.ToDictionary(d => d, d => d.Scale); + originalPositions = objectsInScale.ToDictionary(d => d, d => d.ToScreenSpace(d.OriginPosition)); + OriginalSurroundingQuad = GeometryUtils.GetSurroundingQuad(objectsInScale.SelectMany(d => d.ScreenSpaceDrawQuad.GetVertices().ToArray())); + defaultOrigin = OriginalSurroundingQuad.Value.Centre; + } + + public override void Update(Vector2 scale, Vector2? origin = null) + { + if (objectsInScale == null) + throw new InvalidOperationException($"Cannot {nameof(Update)} a scale operation without calling {nameof(Begin)} first!"); + + Debug.Assert(originalWidths != null && originalHeights != null && originalScales != null && originalPositions != null && defaultOrigin != null && OriginalSurroundingQuad != null); + + var actualOrigin = origin ?? defaultOrigin.Value; + + Axes adjustAxis = scale.X == 0 ? Axes.Y : scale.Y == 0 ? Axes.X : Axes.Both; + + if ((adjustAxis == Axes.Y && !allSelectedSupportManualSizing(Axes.Y)) || + (adjustAxis == Axes.X && !allSelectedSupportManualSizing(Axes.X))) + return; + + // the selection quad is always upright, so use an AABB rect to make mutating the values easier. + var selectionRect = OriginalSurroundingQuad.Value.AABBFloat; + + // If the selection has no area we cannot scale it + if (selectionRect.Area == 0) + return; + + // copy to mutate, as we will need to compare to the original later on. + var adjustedRect = selectionRect; + + // for now aspect lock scale adjustments that occur at corners.. + if (adjustAxis == Axes.Both) + { + // project scale vector along diagonal + Vector2 diag = new Vector2(1, 1).Normalized(); + scale = Vector2.Dot(scale, diag) * diag; + } + // ..or if any of the selection have been rotated. + // this is to avoid requiring skew logic (which would likely not be the user's expected transform anyway). + else if (objectsInScale.Any(b => !Precision.AlmostEquals(b.Rotation % 90, 0))) + { + if (adjustAxis == Axes.Y) + // if dragging from the horizontal centre, only a vertical component is available. + scale.X = scale.Y / selectionRect.Height * selectionRect.Width; + else + // in all other cases (arbitrarily) use the horizontal component for aspect lock. + scale.Y = scale.X / selectionRect.Width * selectionRect.Height; + } + + adjustedRect.Location = GeometryUtils.GetScaledPosition(scale, actualOrigin, OriginalSurroundingQuad!.Value.TopLeft); + adjustedRect.Size = OriginalSurroundingQuad!.Value.Size * scale; + + if (adjustedRect.Width <= 0 || adjustedRect.Height <= 0) + { + Axes toFlip = Axes.None; + + if (adjustedRect.Width <= 0) toFlip |= Axes.X; + if (adjustedRect.Height <= 0) toFlip |= Axes.Y; + + PerformFlipFromScaleHandles?.Invoke(toFlip); + return; + } + + // scale adjust applied to each individual item should match that of the quad itself. + var scaledDelta = new Vector2( + adjustedRect.Width / selectionRect.Width, + adjustedRect.Height / selectionRect.Height + ); + + foreach (var b in objectsInScale) + { + // each drawable's relative position should be maintained in the scaled quad. + var screenPosition = originalPositions[b]; + + var relativePositionInOriginal = + new Vector2( + (screenPosition.X - selectionRect.TopLeft.X) / selectionRect.Width, + (screenPosition.Y - selectionRect.TopLeft.Y) / selectionRect.Height + ); + + var newPositionInAdjusted = new Vector2( + adjustedRect.TopLeft.X + adjustedRect.Width * relativePositionInOriginal.X, + adjustedRect.TopLeft.Y + adjustedRect.Height * relativePositionInOriginal.Y + ); + + UpdatePosition(b, newPositionInAdjusted); + + var currentScaledDelta = scaledDelta; + if (Precision.AlmostEquals(MathF.Abs(b.Rotation) % 180, 90)) + currentScaledDelta = new Vector2(scaledDelta.Y, scaledDelta.X); + + switch (adjustAxis) + { + case Axes.X: + b.Width = originalWidths[b] * currentScaledDelta.X; + break; + + case Axes.Y: + b.Height = originalHeights[b] * currentScaledDelta.Y; + break; + + case Axes.Both: + b.Scale = originalScales[b] * currentScaledDelta; + break; + } + } + } + + public override void Commit() + { + if (objectsInScale == null) + throw new InvalidOperationException($"Cannot {nameof(Commit)} a scale operation without calling {nameof(Begin)} first!"); + + changeHandler?.EndChange(); + + objectsInScale = null; + originalPositions = null; + originalWidths = null; + originalHeights = null; + originalScales = null; + defaultOrigin = null; + } + } +} From 6a57be0a50c8ddc20356f237dd80bc219226ba59 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Sat, 20 Jan 2024 13:04:05 +0100 Subject: [PATCH 0181/2493] clean up code and fix flipping --- .../SkinEditor/SkinSelectionScaleHandler.cs | 74 ++++++++----------- .../Components/SelectionBoxScaleHandle.cs | 19 +++-- 2 files changed, 43 insertions(+), 50 deletions(-) diff --git a/osu.Game/Overlays/SkinEditor/SkinSelectionScaleHandler.cs b/osu.Game/Overlays/SkinEditor/SkinSelectionScaleHandler.cs index 46b39645b2..c2f788a9e8 100644 --- a/osu.Game/Overlays/SkinEditor/SkinSelectionScaleHandler.cs +++ b/osu.Game/Overlays/SkinEditor/SkinSelectionScaleHandler.cs @@ -61,6 +61,9 @@ namespace osu.Game.Overlays.SkinEditor private Dictionary? originalScales; private Dictionary? originalPositions; + private bool isFlippedX; + private bool isFlippedY; + public override void Begin() { if (objectsInScale != null) @@ -75,6 +78,9 @@ namespace osu.Game.Overlays.SkinEditor originalPositions = objectsInScale.ToDictionary(d => d, d => d.ToScreenSpace(d.OriginPosition)); OriginalSurroundingQuad = GeometryUtils.GetSurroundingQuad(objectsInScale.SelectMany(d => d.ScreenSpaceDrawQuad.GetVertices().ToArray())); defaultOrigin = OriginalSurroundingQuad.Value.Centre; + + isFlippedX = false; + isFlippedY = false; } public override void Update(Vector2 scale, Vector2? origin = null) @@ -85,29 +91,21 @@ namespace osu.Game.Overlays.SkinEditor Debug.Assert(originalWidths != null && originalHeights != null && originalScales != null && originalPositions != null && defaultOrigin != null && OriginalSurroundingQuad != null); var actualOrigin = origin ?? defaultOrigin.Value; - Axes adjustAxis = scale.X == 0 ? Axes.Y : scale.Y == 0 ? Axes.X : Axes.Both; if ((adjustAxis == Axes.Y && !allSelectedSupportManualSizing(Axes.Y)) || (adjustAxis == Axes.X && !allSelectedSupportManualSizing(Axes.X))) return; - // the selection quad is always upright, so use an AABB rect to make mutating the values easier. - var selectionRect = OriginalSurroundingQuad.Value.AABBFloat; - // If the selection has no area we cannot scale it - if (selectionRect.Area == 0) + if (OriginalSurroundingQuad.Value.Width == 0 || OriginalSurroundingQuad.Value.Height == 0) return; - // copy to mutate, as we will need to compare to the original later on. - var adjustedRect = selectionRect; - // for now aspect lock scale adjustments that occur at corners.. if (adjustAxis == Axes.Both) { // project scale vector along diagonal - Vector2 diag = new Vector2(1, 1).Normalized(); - scale = Vector2.Dot(scale, diag) * diag; + scale = new Vector2((scale.X + scale.Y) * 0.5f); } // ..or if any of the selection have been rotated. // this is to avoid requiring skew logic (which would likely not be the user's expected transform anyway). @@ -115,66 +113,54 @@ namespace osu.Game.Overlays.SkinEditor { if (adjustAxis == Axes.Y) // if dragging from the horizontal centre, only a vertical component is available. - scale.X = scale.Y / selectionRect.Height * selectionRect.Width; + scale.X = scale.Y; else // in all other cases (arbitrarily) use the horizontal component for aspect lock. - scale.Y = scale.X / selectionRect.Width * selectionRect.Height; + scale.Y = scale.X; } - adjustedRect.Location = GeometryUtils.GetScaledPosition(scale, actualOrigin, OriginalSurroundingQuad!.Value.TopLeft); - adjustedRect.Size = OriginalSurroundingQuad!.Value.Size * scale; + bool flippedX = scale.X < 0; + bool flippedY = scale.Y < 0; + Axes toFlip = Axes.None; - if (adjustedRect.Width <= 0 || adjustedRect.Height <= 0) + if (flippedX != isFlippedX) { - Axes toFlip = Axes.None; + isFlippedX = flippedX; + toFlip |= Axes.X; + } - if (adjustedRect.Width <= 0) toFlip |= Axes.X; - if (adjustedRect.Height <= 0) toFlip |= Axes.Y; + if (flippedY != isFlippedY) + { + isFlippedY = flippedY; + toFlip |= Axes.Y; + } + if (toFlip != Axes.None) + { PerformFlipFromScaleHandles?.Invoke(toFlip); return; } - // scale adjust applied to each individual item should match that of the quad itself. - var scaledDelta = new Vector2( - adjustedRect.Width / selectionRect.Width, - adjustedRect.Height / selectionRect.Height - ); - foreach (var b in objectsInScale) { - // each drawable's relative position should be maintained in the scaled quad. - var screenPosition = originalPositions[b]; + UpdatePosition(b, GeometryUtils.GetScaledPosition(scale, actualOrigin, originalPositions[b])); - var relativePositionInOriginal = - new Vector2( - (screenPosition.X - selectionRect.TopLeft.X) / selectionRect.Width, - (screenPosition.Y - selectionRect.TopLeft.Y) / selectionRect.Height - ); - - var newPositionInAdjusted = new Vector2( - adjustedRect.TopLeft.X + adjustedRect.Width * relativePositionInOriginal.X, - adjustedRect.TopLeft.Y + adjustedRect.Height * relativePositionInOriginal.Y - ); - - UpdatePosition(b, newPositionInAdjusted); - - var currentScaledDelta = scaledDelta; + var currentScale = scale; if (Precision.AlmostEquals(MathF.Abs(b.Rotation) % 180, 90)) - currentScaledDelta = new Vector2(scaledDelta.Y, scaledDelta.X); + currentScale = new Vector2(scale.Y, scale.X); switch (adjustAxis) { case Axes.X: - b.Width = originalWidths[b] * currentScaledDelta.X; + b.Width = originalWidths[b] * currentScale.X; break; case Axes.Y: - b.Height = originalHeights[b] * currentScaledDelta.Y; + b.Height = originalHeights[b] * currentScale.Y; break; case Axes.Both: - b.Scale = originalScales[b] * currentScaledDelta; + b.Scale = originalScales[b] * currentScale; break; } } diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionBoxScaleHandle.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionBoxScaleHandle.cs index 56c5585ae7..6179be1d4f 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionBoxScaleHandle.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionBoxScaleHandle.cs @@ -5,6 +5,7 @@ using System; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Input.Events; +using osu.Framework.Logging; using osuTK; using osuTK.Input; @@ -24,6 +25,8 @@ namespace osu.Game.Screens.Edit.Compose.Components Size = new Vector2(10); } + private Anchor originalAnchor; + protected override bool OnDragStart(DragStartEvent e) { if (e.Button != MouseButton.Left) @@ -31,6 +34,8 @@ namespace osu.Game.Screens.Edit.Compose.Components if (scaleHandler == null) return false; + originalAnchor = Anchor; + scaleHandler.Begin(); return true; } @@ -40,10 +45,10 @@ namespace osu.Game.Screens.Edit.Compose.Components var quad = scaleHandler!.OriginalSurroundingQuad!.Value; Vector2 origin = quad.TopLeft; - if ((Anchor & Anchor.x0) > 0) + if ((originalAnchor & Anchor.x0) > 0) origin.X += quad.Width; - if ((Anchor & Anchor.y0) > 0) + if ((originalAnchor & Anchor.y0) > 0) origin.Y += quad.Height; return origin; @@ -89,6 +94,7 @@ namespace osu.Game.Screens.Edit.Compose.Components private Vector2 convertDragEventToScaleMultiplier(DragEvent e) { Vector2 scale = e.MousePosition - e.MouseDownPosition; + Logger.Log($"Raw scale {scale}"); adjustScaleFromAnchor(ref scale); return Vector2.Divide(scale, scaleHandler!.OriginalSurroundingQuad!.Value.Size) + Vector2.One; } @@ -96,12 +102,12 @@ namespace osu.Game.Screens.Edit.Compose.Components private void adjustScaleFromAnchor(ref Vector2 scale) { // cancel out scale in axes we don't care about (based on which drag handle was used). - if ((Anchor & Anchor.x1) > 0) scale.X = 1; - if ((Anchor & Anchor.y1) > 0) scale.Y = 1; + if ((originalAnchor & Anchor.x1) > 0) scale.X = 1; + if ((originalAnchor & Anchor.y1) > 0) scale.Y = 1; // reverse the scale direction if dragging from top or left. - if ((Anchor & Anchor.x0) > 0) scale.X = -scale.X; - if ((Anchor & Anchor.y0) > 0) scale.Y = -scale.Y; + if ((originalAnchor & Anchor.x0) > 0) scale.X = -scale.X; + if ((originalAnchor & Anchor.y0) > 0) scale.Y = -scale.Y; } private void applyScale(bool shouldKeepAspectRatio) @@ -110,6 +116,7 @@ namespace osu.Game.Screens.Edit.Compose.Components ? new Vector2(MathF.Max(rawScale.X, rawScale.Y)) : rawScale; + Logger.Log($"Raw scale adjusted {newScale}, origin {getOriginPosition()}"); scaleHandler!.Update(newScale, getOriginPosition()); } } From fcaa5ec20e3fe43948bb1bd9d898d45dcf9b50cf Mon Sep 17 00:00:00 2001 From: OliBomby Date: Sat, 20 Jan 2024 13:26:08 +0100 Subject: [PATCH 0182/2493] remove debug logs --- .../Screens/Edit/Compose/Components/SelectionBoxScaleHandle.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionBoxScaleHandle.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionBoxScaleHandle.cs index 6179be1d4f..e0b41fd8e2 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionBoxScaleHandle.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionBoxScaleHandle.cs @@ -5,7 +5,6 @@ using System; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Input.Events; -using osu.Framework.Logging; using osuTK; using osuTK.Input; @@ -94,7 +93,6 @@ namespace osu.Game.Screens.Edit.Compose.Components private Vector2 convertDragEventToScaleMultiplier(DragEvent e) { Vector2 scale = e.MousePosition - e.MouseDownPosition; - Logger.Log($"Raw scale {scale}"); adjustScaleFromAnchor(ref scale); return Vector2.Divide(scale, scaleHandler!.OriginalSurroundingQuad!.Value.Size) + Vector2.One; } @@ -116,7 +114,6 @@ namespace osu.Game.Screens.Edit.Compose.Components ? new Vector2(MathF.Max(rawScale.X, rawScale.Y)) : rawScale; - Logger.Log($"Raw scale adjusted {newScale}, origin {getOriginPosition()}"); scaleHandler!.Update(newScale, getOriginPosition()); } } From e1f3f7d988194e2f48df3aba184f095f59d2623b Mon Sep 17 00:00:00 2001 From: OliBomby Date: Sat, 20 Jan 2024 14:49:47 +0100 Subject: [PATCH 0183/2493] fix possible NaN in clamped scale --- .../Edit/OsuSelectionScaleHandler.cs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs index 7b0ae947e7..3c4818a533 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs @@ -177,10 +177,14 @@ namespace osu.Game.Rulesets.Osu.Edit var br1 = Vector2.Divide(-origin, selectionQuad.BottomRight - origin); var br2 = Vector2.Divide(OsuPlayfield.BASE_SIZE - origin, selectionQuad.BottomRight - origin); - scale.X = selectionQuad.TopLeft.X - origin.X < 0 ? MathHelper.Clamp(scale.X, tl2.X, tl1.X) : MathHelper.Clamp(scale.X, tl1.X, tl2.X); - scale.Y = selectionQuad.TopLeft.Y - origin.Y < 0 ? MathHelper.Clamp(scale.Y, tl2.Y, tl1.Y) : MathHelper.Clamp(scale.Y, tl1.Y, tl2.Y); - scale.X = selectionQuad.BottomRight.X - origin.X < 0 ? MathHelper.Clamp(scale.X, br2.X, br1.X) : MathHelper.Clamp(scale.X, br1.X, br2.X); - scale.Y = selectionQuad.BottomRight.Y - origin.Y < 0 ? MathHelper.Clamp(scale.Y, br2.Y, br1.Y) : MathHelper.Clamp(scale.Y, br1.Y, br2.Y); + if (!Precision.AlmostEquals(selectionQuad.TopLeft.X - origin.X, 0)) + scale.X = selectionQuad.TopLeft.X - origin.X < 0 ? MathHelper.Clamp(scale.X, tl2.X, tl1.X) : MathHelper.Clamp(scale.X, tl1.X, tl2.X); + if (!Precision.AlmostEquals(selectionQuad.TopLeft.Y - origin.Y, 0)) + scale.Y = selectionQuad.TopLeft.Y - origin.Y < 0 ? MathHelper.Clamp(scale.Y, tl2.Y, tl1.Y) : MathHelper.Clamp(scale.Y, tl1.Y, tl2.Y); + if (!Precision.AlmostEquals(selectionQuad.BottomRight.X - origin.X, 0)) + scale.X = selectionQuad.BottomRight.X - origin.X < 0 ? MathHelper.Clamp(scale.X, br2.X, br1.X) : MathHelper.Clamp(scale.X, br1.X, br2.X); + if (!Precision.AlmostEquals(selectionQuad.BottomRight.Y - origin.Y, 0)) + scale.Y = selectionQuad.BottomRight.Y - origin.Y < 0 ? MathHelper.Clamp(scale.Y, br2.Y, br1.Y) : MathHelper.Clamp(scale.Y, br1.Y, br2.Y); return scale; } From 6a4129dad880e839b033d77ac2bdb00f22dc1c0d Mon Sep 17 00:00:00 2001 From: OliBomby Date: Sat, 20 Jan 2024 15:11:35 +0100 Subject: [PATCH 0184/2493] fix aspect ratio transform --- .../Screens/Edit/Compose/Components/SelectionBoxScaleHandle.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionBoxScaleHandle.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionBoxScaleHandle.cs index e0b41fd8e2..ea98ac573c 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionBoxScaleHandle.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionBoxScaleHandle.cs @@ -111,7 +111,7 @@ namespace osu.Game.Screens.Edit.Compose.Components private void applyScale(bool shouldKeepAspectRatio) { var newScale = shouldKeepAspectRatio - ? new Vector2(MathF.Max(rawScale.X, rawScale.Y)) + ? new Vector2((rawScale.X + rawScale.Y) * 0.5f) : rawScale; scaleHandler!.Update(newScale, getOriginPosition()); From 0fc448f4f3a31643903017a9881b81749561e0eb Mon Sep 17 00:00:00 2001 From: OliBomby Date: Sat, 20 Jan 2024 15:12:48 +0100 Subject: [PATCH 0185/2493] fix adjusting scale from anchor --- osu.Game/Overlays/SkinEditor/SkinSelectionScaleHandler.cs | 2 +- .../Edit/Compose/Components/SelectionBoxScaleHandle.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Overlays/SkinEditor/SkinSelectionScaleHandler.cs b/osu.Game/Overlays/SkinEditor/SkinSelectionScaleHandler.cs index c2f788a9e8..bf75469d7a 100644 --- a/osu.Game/Overlays/SkinEditor/SkinSelectionScaleHandler.cs +++ b/osu.Game/Overlays/SkinEditor/SkinSelectionScaleHandler.cs @@ -91,7 +91,7 @@ namespace osu.Game.Overlays.SkinEditor Debug.Assert(originalWidths != null && originalHeights != null && originalScales != null && originalPositions != null && defaultOrigin != null && OriginalSurroundingQuad != null); var actualOrigin = origin ?? defaultOrigin.Value; - Axes adjustAxis = scale.X == 0 ? Axes.Y : scale.Y == 0 ? Axes.X : Axes.Both; + Axes adjustAxis = scale.X == 1 ? Axes.Y : scale.Y == 1 ? Axes.X : Axes.Both; if ((adjustAxis == Axes.Y && !allSelectedSupportManualSizing(Axes.Y)) || (adjustAxis == Axes.X && !allSelectedSupportManualSizing(Axes.X))) diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionBoxScaleHandle.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionBoxScaleHandle.cs index ea98ac573c..60fbeb9fff 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionBoxScaleHandle.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionBoxScaleHandle.cs @@ -100,8 +100,8 @@ namespace osu.Game.Screens.Edit.Compose.Components private void adjustScaleFromAnchor(ref Vector2 scale) { // cancel out scale in axes we don't care about (based on which drag handle was used). - if ((originalAnchor & Anchor.x1) > 0) scale.X = 1; - if ((originalAnchor & Anchor.y1) > 0) scale.Y = 1; + if ((originalAnchor & Anchor.x1) > 0) scale.X = 0; + if ((originalAnchor & Anchor.y1) > 0) scale.Y = 0; // reverse the scale direction if dragging from top or left. if ((originalAnchor & Anchor.x0) > 0) scale.X = -scale.X; From 1596776a81b91db1850bf3325b6d2992ed5eaf6c Mon Sep 17 00:00:00 2001 From: OliBomby Date: Sat, 20 Jan 2024 15:15:49 +0100 Subject: [PATCH 0186/2493] fix imports --- osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs | 1 + .../Screens/Edit/Compose/Components/SelectionBoxScaleHandle.cs | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs index 3c4818a533..1e3e22e34a 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs @@ -8,6 +8,7 @@ using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics.Primitives; +using osu.Framework.Utils; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionBoxScaleHandle.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionBoxScaleHandle.cs index 60fbeb9fff..3dde97657f 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionBoxScaleHandle.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionBoxScaleHandle.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 osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Input.Events; From 9b9485f656807570afd91bd3b25923147a2075f2 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Sat, 20 Jan 2024 15:39:38 +0100 Subject: [PATCH 0187/2493] fix adjust axes detection --- .../Edit/OsuSelectionScaleHandler.cs | 3 +- .../SkinEditor/SkinSelectionScaleHandler.cs | 3 +- .../Components/SelectionBoxScaleHandle.cs | 47 +++++++++++++------ .../Components/SelectionScaleHandler.cs | 8 ++-- 4 files changed, 40 insertions(+), 21 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs index 1e3e22e34a..7d5240fb69 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs @@ -7,6 +7,7 @@ using System.Diagnostics; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Graphics; using osu.Framework.Graphics.Primitives; using osu.Framework.Utils; using osu.Game.Rulesets.Edit; @@ -82,7 +83,7 @@ namespace osu.Game.Rulesets.Osu.Edit obj => obj.Path.ControlPoints.Select(p => p.Type).ToArray()); } - public override void Update(Vector2 scale, Vector2? origin = null) + public override void Update(Vector2 scale, Vector2? origin = null, Axes adjustAxis = Axes.Both) { if (objectsInScale == null) throw new InvalidOperationException($"Cannot {nameof(Update)} a scale operation without calling {nameof(Begin)} first!"); diff --git a/osu.Game/Overlays/SkinEditor/SkinSelectionScaleHandler.cs b/osu.Game/Overlays/SkinEditor/SkinSelectionScaleHandler.cs index bf75469d7a..0bd146a0a1 100644 --- a/osu.Game/Overlays/SkinEditor/SkinSelectionScaleHandler.cs +++ b/osu.Game/Overlays/SkinEditor/SkinSelectionScaleHandler.cs @@ -83,7 +83,7 @@ namespace osu.Game.Overlays.SkinEditor isFlippedY = false; } - public override void Update(Vector2 scale, Vector2? origin = null) + public override void Update(Vector2 scale, Vector2? origin = null, Axes adjustAxis = Axes.Both) { if (objectsInScale == null) throw new InvalidOperationException($"Cannot {nameof(Update)} a scale operation without calling {nameof(Begin)} first!"); @@ -91,7 +91,6 @@ namespace osu.Game.Overlays.SkinEditor Debug.Assert(originalWidths != null && originalHeights != null && originalScales != null && originalPositions != null && defaultOrigin != null && OriginalSurroundingQuad != null); var actualOrigin = origin ?? defaultOrigin.Value; - Axes adjustAxis = scale.X == 1 ? Axes.Y : scale.Y == 1 ? Axes.X : Axes.Both; if ((adjustAxis == Axes.Y && !allSelectedSupportManualSizing(Axes.Y)) || (adjustAxis == Axes.X && !allSelectedSupportManualSizing(Axes.X))) diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionBoxScaleHandle.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionBoxScaleHandle.cs index 3dde97657f..d433e4e860 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionBoxScaleHandle.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionBoxScaleHandle.cs @@ -38,20 +38,6 @@ namespace osu.Game.Screens.Edit.Compose.Components return true; } - private Vector2 getOriginPosition() - { - var quad = scaleHandler!.OriginalSurroundingQuad!.Value; - Vector2 origin = quad.TopLeft; - - if ((originalAnchor & Anchor.x0) > 0) - origin.X += quad.Width; - - if ((originalAnchor & Anchor.y0) > 0) - origin.Y += quad.Height; - - return origin; - } - private Vector2 rawScale; protected override void OnDrag(DragEvent e) @@ -113,7 +99,38 @@ namespace osu.Game.Screens.Edit.Compose.Components ? new Vector2((rawScale.X + rawScale.Y) * 0.5f) : rawScale; - scaleHandler!.Update(newScale, getOriginPosition()); + scaleHandler!.Update(newScale, getOriginPosition(), getAdjustAxis()); + } + + private Vector2 getOriginPosition() + { + var quad = scaleHandler!.OriginalSurroundingQuad!.Value; + Vector2 origin = quad.TopLeft; + + if ((originalAnchor & Anchor.x0) > 0) + origin.X += quad.Width; + + if ((originalAnchor & Anchor.y0) > 0) + origin.Y += quad.Height; + + return origin; + } + + private Axes getAdjustAxis() + { + switch (originalAnchor) + { + case Anchor.TopCentre: + case Anchor.BottomCentre: + return Axes.Y; + + case Anchor.CentreLeft: + case Anchor.CentreRight: + return Axes.X; + + default: + return Axes.Both; + } } } } diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionScaleHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionScaleHandler.cs index 59406b3184..a96f627e56 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionScaleHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionScaleHandler.cs @@ -46,10 +46,11 @@ namespace osu.Game.Screens.Edit.Compose.Components /// The origin point to scale from. /// If the default value is supplied, a sane implementation-defined default will be used. /// - public void ScaleSelection(Vector2 scale, Vector2? origin = null) + /// The axes to adjust the scale in. + public void ScaleSelection(Vector2 scale, Vector2? origin = null, Axes adjustAxis = Axes.Both) { Begin(); - Update(scale, origin); + Update(scale, origin, adjustAxis); Commit(); } @@ -83,7 +84,8 @@ namespace osu.Game.Screens.Edit.Compose.Components /// The origin point to scale from. /// If the default value is supplied, a sane implementation-defined default will be used. /// - public virtual void Update(Vector2 scale, Vector2? origin = null) + /// The axes to adjust the scale in. + public virtual void Update(Vector2 scale, Vector2? origin = null, Axes adjustAxis = Axes.Both) { } From ac76af5cc8f894dfb87ae4d4987172b9f5a85934 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Sat, 20 Jan 2024 15:43:47 +0100 Subject: [PATCH 0188/2493] fix skin scale coordinate system --- osu.Game/Overlays/SkinEditor/SkinSelectionScaleHandler.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/SkinEditor/SkinSelectionScaleHandler.cs b/osu.Game/Overlays/SkinEditor/SkinSelectionScaleHandler.cs index 0bd146a0a1..e87952efa0 100644 --- a/osu.Game/Overlays/SkinEditor/SkinSelectionScaleHandler.cs +++ b/osu.Game/Overlays/SkinEditor/SkinSelectionScaleHandler.cs @@ -76,7 +76,7 @@ namespace osu.Game.Overlays.SkinEditor originalHeights = objectsInScale.ToDictionary(d => d, d => d.Height); originalScales = objectsInScale.ToDictionary(d => d, d => d.Scale); originalPositions = objectsInScale.ToDictionary(d => d, d => d.ToScreenSpace(d.OriginPosition)); - OriginalSurroundingQuad = GeometryUtils.GetSurroundingQuad(objectsInScale.SelectMany(d => d.ScreenSpaceDrawQuad.GetVertices().ToArray())); + OriginalSurroundingQuad = ToLocalSpace(GeometryUtils.GetSurroundingQuad(objectsInScale.SelectMany(d => d.ScreenSpaceDrawQuad.GetVertices().ToArray()))); defaultOrigin = OriginalSurroundingQuad.Value.Centre; isFlippedX = false; @@ -90,7 +90,7 @@ namespace osu.Game.Overlays.SkinEditor Debug.Assert(originalWidths != null && originalHeights != null && originalScales != null && originalPositions != null && defaultOrigin != null && OriginalSurroundingQuad != null); - var actualOrigin = origin ?? defaultOrigin.Value; + var actualOrigin = ToScreenSpace(origin ?? defaultOrigin.Value); if ((adjustAxis == Axes.Y && !allSelectedSupportManualSizing(Axes.Y)) || (adjustAxis == Axes.X && !allSelectedSupportManualSizing(Axes.X))) From 9459c66981a022905283b603f9bfb0d7e3cf6e77 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Sat, 20 Jan 2024 15:53:08 +0100 Subject: [PATCH 0189/2493] fix test --- osu.Game.Tests/Visual/Editing/TestSceneComposeSelectBox.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneComposeSelectBox.cs b/osu.Game.Tests/Visual/Editing/TestSceneComposeSelectBox.cs index 680a76f9b8..4c60ecf5db 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneComposeSelectBox.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneComposeSelectBox.cs @@ -130,7 +130,7 @@ namespace osu.Game.Tests.Visual.Editing OriginalSurroundingQuad = new Quad(targetContainer!.X, targetContainer.Y, targetContainer.Width, targetContainer.Height); } - public override void Update(Vector2 scale, Vector2? origin = null) + public override void Update(Vector2 scale, Vector2? origin = null, Axes adjustAxis = Axes.Both) { if (targetContainer == null) throw new InvalidOperationException($"Cannot {nameof(Update)} a scale operation without calling {nameof(Begin)} first!"); From a155b315bf8ad9060ae214ccc8763e4deebdae6c Mon Sep 17 00:00:00 2001 From: OliBomby Date: Sat, 20 Jan 2024 16:10:17 +0100 Subject: [PATCH 0190/2493] Fix negative width or height skin drawables --- osu.Game/Overlays/SkinEditor/SkinSelectionScaleHandler.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/SkinEditor/SkinSelectionScaleHandler.cs b/osu.Game/Overlays/SkinEditor/SkinSelectionScaleHandler.cs index e87952efa0..8daf0043da 100644 --- a/osu.Game/Overlays/SkinEditor/SkinSelectionScaleHandler.cs +++ b/osu.Game/Overlays/SkinEditor/SkinSelectionScaleHandler.cs @@ -151,11 +151,11 @@ namespace osu.Game.Overlays.SkinEditor switch (adjustAxis) { case Axes.X: - b.Width = originalWidths[b] * currentScale.X; + b.Width = MathF.Abs(originalWidths[b] * currentScale.X); break; case Axes.Y: - b.Height = originalHeights[b] * currentScale.Y; + b.Height = MathF.Abs(originalHeights[b] * currentScale.Y); break; case Axes.Both: From 5f40d3aed9ca859535c75d8f9e927d5cc7ad1581 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Sat, 20 Jan 2024 16:29:26 +0100 Subject: [PATCH 0191/2493] rename variable --- .../Edit/Compose/Components/SelectionBoxScaleHandle.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionBoxScaleHandle.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionBoxScaleHandle.cs index d433e4e860..74629a5384 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionBoxScaleHandle.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionBoxScaleHandle.cs @@ -48,14 +48,14 @@ namespace osu.Game.Screens.Edit.Compose.Components rawScale = convertDragEventToScaleMultiplier(e); - applyScale(shouldKeepAspectRatio: e.ShiftPressed); + applyScale(shouldLockAspectRatio: e.ShiftPressed); } protected override bool OnKeyDown(KeyDownEvent e) { if (IsDragged && (e.Key == Key.ShiftLeft || e.Key == Key.ShiftRight)) { - applyScale(shouldKeepAspectRatio: true); + applyScale(shouldLockAspectRatio: true); return true; } @@ -67,7 +67,7 @@ namespace osu.Game.Screens.Edit.Compose.Components base.OnKeyUp(e); if (IsDragged && (e.Key == Key.ShiftLeft || e.Key == Key.ShiftRight)) - applyScale(shouldKeepAspectRatio: false); + applyScale(shouldLockAspectRatio: false); } protected override void OnDragEnd(DragEndEvent e) @@ -93,9 +93,9 @@ namespace osu.Game.Screens.Edit.Compose.Components if ((originalAnchor & Anchor.y0) > 0) scale.Y = -scale.Y; } - private void applyScale(bool shouldKeepAspectRatio) + private void applyScale(bool shouldLockAspectRatio) { - var newScale = shouldKeepAspectRatio + var newScale = shouldLockAspectRatio ? new Vector2((rawScale.X + rawScale.Y) * 0.5f) : rawScale; From 2f924b33686ff7d7cb3080c2cc16f891d27cbc2e Mon Sep 17 00:00:00 2001 From: OliBomby Date: Sat, 20 Jan 2024 16:33:03 +0100 Subject: [PATCH 0192/2493] fix skewed single axis scale --- .../SkinEditor/SkinSelectionScaleHandler.cs | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/osu.Game/Overlays/SkinEditor/SkinSelectionScaleHandler.cs b/osu.Game/Overlays/SkinEditor/SkinSelectionScaleHandler.cs index 8daf0043da..0c2ee6aae3 100644 --- a/osu.Game/Overlays/SkinEditor/SkinSelectionScaleHandler.cs +++ b/osu.Game/Overlays/SkinEditor/SkinSelectionScaleHandler.cs @@ -100,23 +100,15 @@ namespace osu.Game.Overlays.SkinEditor if (OriginalSurroundingQuad.Value.Width == 0 || OriginalSurroundingQuad.Value.Height == 0) return; - // for now aspect lock scale adjustments that occur at corners.. + // for now aspect lock scale adjustments that occur at corners. if (adjustAxis == Axes.Both) { // project scale vector along diagonal scale = new Vector2((scale.X + scale.Y) * 0.5f); } - // ..or if any of the selection have been rotated. - // this is to avoid requiring skew logic (which would likely not be the user's expected transform anyway). - else if (objectsInScale.Any(b => !Precision.AlmostEquals(b.Rotation % 90, 0))) - { - if (adjustAxis == Axes.Y) - // if dragging from the horizontal centre, only a vertical component is available. - scale.X = scale.Y; - else - // in all other cases (arbitrarily) use the horizontal component for aspect lock. - scale.Y = scale.X; - } + // If any of the selection have been rotated and the adjust axis is not both, + // we would require skew logic to achieve a correct image editor-like scale. + // For now we just ignore, because it would likely not be the user's expected transform anyway. bool flippedX = scale.X < 0; bool flippedY = scale.Y < 0; From 78e87d379b760b9ebd5d567610423b607013b16d Mon Sep 17 00:00:00 2001 From: OliBomby Date: Sat, 20 Jan 2024 16:49:10 +0100 Subject: [PATCH 0193/2493] fix divide by zero --- .../Edit/Compose/Components/SelectionBoxScaleHandle.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionBoxScaleHandle.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionBoxScaleHandle.cs index 74629a5384..a1f6a1732a 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionBoxScaleHandle.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionBoxScaleHandle.cs @@ -4,6 +4,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Input.Events; +using osu.Framework.Utils; using osuTK; using osuTK.Input; @@ -79,7 +80,12 @@ namespace osu.Game.Screens.Edit.Compose.Components { Vector2 scale = e.MousePosition - e.MouseDownPosition; adjustScaleFromAnchor(ref scale); - return Vector2.Divide(scale, scaleHandler!.OriginalSurroundingQuad!.Value.Size) + Vector2.One; + + var surroundingQuad = scaleHandler!.OriginalSurroundingQuad!.Value; + scale.X = Precision.AlmostEquals(surroundingQuad.Width, 0) ? 0 : scale.X / surroundingQuad.Width; + scale.Y = Precision.AlmostEquals(surroundingQuad.Height, 0) ? 0 : scale.Y / surroundingQuad.Height; + + return scale + Vector2.One; } private void adjustScaleFromAnchor(ref Vector2 scale) From e1f8bc96924b104665782aacfd223eb2ba9dafed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=C3=AAn=20Minh=20H=E1=BB=93?= Date: Thu, 25 Jan 2024 12:09:39 +0700 Subject: [PATCH 0194/2493] Rename CanRotate property of SelectionRotationHandler to a more descriptive name --- osu.Game.Rulesets.Osu/Edit/OsuSelectionRotationHandler.cs | 4 +++- osu.Game.Rulesets.Osu/Edit/TransformToolboxGroup.cs | 2 +- osu.Game.Tests/Visual/Editing/TestSceneComposeSelectBox.cs | 2 +- osu.Game/Overlays/SkinEditor/SkinSelectionRotationHandler.cs | 2 +- osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs | 2 +- .../Edit/Compose/Components/SelectionRotationHandler.cs | 5 +++-- 6 files changed, 10 insertions(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionRotationHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionRotationHandler.cs index 21fb8a67de..0ce78e4f61 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionRotationHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionRotationHandler.cs @@ -19,6 +19,7 @@ namespace osu.Game.Rulesets.Osu.Edit { public partial class OsuSelectionRotationHandler : SelectionRotationHandler { + public BindableBool CanRotatePlayfieldOrigin { get; private set; } = new(); [Resolved] private IEditorChangeHandler? changeHandler { get; set; } @@ -41,7 +42,8 @@ namespace osu.Game.Rulesets.Osu.Edit private void updateState() { var quad = GeometryUtils.GetSurroundingQuad(selectedMovableObjects); - CanRotate.Value = quad.Width > 0 || quad.Height > 0; + CanRotateSelectionOrigin.Value = quad.Width > 0 || quad.Height > 0; + CanRotatePlayfieldOrigin.Value = selectedItems.Any(); } private OsuHitObject[]? objectsInRotation; diff --git a/osu.Game.Rulesets.Osu/Edit/TransformToolboxGroup.cs b/osu.Game.Rulesets.Osu/Edit/TransformToolboxGroup.cs index 3da9f5b69b..291c79e613 100644 --- a/osu.Game.Rulesets.Osu/Edit/TransformToolboxGroup.cs +++ b/osu.Game.Rulesets.Osu/Edit/TransformToolboxGroup.cs @@ -53,7 +53,7 @@ namespace osu.Game.Rulesets.Osu.Edit // bindings to `Enabled` on the buttons are decoupled on purpose // due to the weird `OsuButton` behaviour of resetting `Enabled` to `false` when `Action` is set. - canRotate.BindTo(RotationHandler.CanRotate); + canRotate.BindTo(RotationHandler.CanRotateSelectionOrigin); canRotate.BindValueChanged(_ => rotateButton.Enabled.Value = canRotate.Value, true); } diff --git a/osu.Game.Tests/Visual/Editing/TestSceneComposeSelectBox.cs b/osu.Game.Tests/Visual/Editing/TestSceneComposeSelectBox.cs index f6637d0e80..8e4f4a1cfd 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneComposeSelectBox.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneComposeSelectBox.cs @@ -89,7 +89,7 @@ namespace osu.Game.Tests.Visual.Editing { this.getTargetContainer = getTargetContainer; - CanRotate.Value = true; + CanRotateSelectionOrigin.Value = true; } [CanBeNull] diff --git a/osu.Game/Overlays/SkinEditor/SkinSelectionRotationHandler.cs b/osu.Game/Overlays/SkinEditor/SkinSelectionRotationHandler.cs index 60f69000a2..7ecf116b68 100644 --- a/osu.Game/Overlays/SkinEditor/SkinSelectionRotationHandler.cs +++ b/osu.Game/Overlays/SkinEditor/SkinSelectionRotationHandler.cs @@ -41,7 +41,7 @@ namespace osu.Game.Overlays.SkinEditor private void updateState() { - CanRotate.Value = selectedItems.Count > 0; + CanRotateSelectionOrigin.Value = selectedItems.Count > 0; } private Drawable[]? objectsInRotation; diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs index 0b16941bc4..85ea7364e8 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs @@ -174,7 +174,7 @@ namespace osu.Game.Screens.Edit.Compose.Components private void load() { if (rotationHandler != null) - canRotate.BindTo(rotationHandler.CanRotate); + canRotate.BindTo(rotationHandler.CanRotateSelectionOrigin); canRotate.BindValueChanged(_ => recreate(), true); } diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionRotationHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionRotationHandler.cs index 5faa4a108d..749e1aab17 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionRotationHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionRotationHandler.cs @@ -13,9 +13,10 @@ namespace osu.Game.Screens.Edit.Compose.Components public partial class SelectionRotationHandler : Component { /// - /// Whether the rotation can currently be performed. + /// Whether rotation anchored by the selection origin can currently be performed. + /// This is in constrast to rotation anchored by the entire field. /// - public Bindable CanRotate { get; private set; } = new BindableBool(); + public Bindable CanRotateSelectionOrigin { get; private set; } = new BindableBool(); /// /// Performs a single, instant, atomic rotation operation. From 601ba9f194ee029714d85a73f4e0078a7401f910 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=C3=AAn=20Minh=20H=E1=BB=93?= Date: Thu, 25 Jan 2024 12:16:35 +0700 Subject: [PATCH 0195/2493] Change rotate tool button to be enabled on single circle. Inject osu ruleset specific rotate handler instead of generic handler. --- osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs | 5 ++++- osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs | 2 +- osu.Game.Rulesets.Osu/Edit/TransformToolboxGroup.cs | 5 ++--- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs index 448cfaf84c..a0fb0b06c3 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs @@ -101,7 +101,10 @@ namespace osu.Game.Rulesets.Osu.Edit RightToolbox.AddRange(new EditorToolboxGroup[] { - new TransformToolboxGroup { RotationHandler = BlueprintContainer.SelectionHandler.RotationHandler, }, + new TransformToolboxGroup + { + RotationHandler = (OsuSelectionRotationHandler)BlueprintContainer.SelectionHandler.RotationHandler, + }, FreehandlSliderToolboxGroup } ); diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs index cea2adc6e2..7e645bc670 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs @@ -164,7 +164,7 @@ namespace osu.Game.Rulesets.Osu.Edit if ((reference & Anchor.y0) > 0) scale.Y = -scale.Y; } - public override SelectionRotationHandler CreateRotationHandler() => new OsuSelectionRotationHandler(); + public override OsuSelectionRotationHandler CreateRotationHandler() => new OsuSelectionRotationHandler(); private void scaleSlider(Slider slider, Vector2 scale) { diff --git a/osu.Game.Rulesets.Osu/Edit/TransformToolboxGroup.cs b/osu.Game.Rulesets.Osu/Edit/TransformToolboxGroup.cs index 291c79e613..c70f35c6fb 100644 --- a/osu.Game.Rulesets.Osu/Edit/TransformToolboxGroup.cs +++ b/osu.Game.Rulesets.Osu/Edit/TransformToolboxGroup.cs @@ -11,7 +11,6 @@ using osu.Framework.Input.Events; using osu.Game.Input.Bindings; using osu.Game.Rulesets.Edit; using osu.Game.Screens.Edit.Components; -using osu.Game.Screens.Edit.Compose.Components; using osuTK; namespace osu.Game.Rulesets.Osu.Edit @@ -22,7 +21,7 @@ namespace osu.Game.Rulesets.Osu.Edit private EditorToolButton rotateButton = null!; - public SelectionRotationHandler RotationHandler { get; init; } = null!; + public OsuSelectionRotationHandler RotationHandler { get; init; } = null!; public TransformToolboxGroup() : base("transform") @@ -53,7 +52,7 @@ namespace osu.Game.Rulesets.Osu.Edit // bindings to `Enabled` on the buttons are decoupled on purpose // due to the weird `OsuButton` behaviour of resetting `Enabled` to `false` when `Action` is set. - canRotate.BindTo(RotationHandler.CanRotateSelectionOrigin); + canRotate.BindTo(RotationHandler.CanRotatePlayfieldOrigin); canRotate.BindValueChanged(_ => rotateButton.Enabled.Value = canRotate.Value, true); } From 500bed01215f6b99eeb3a6a717563b243c18ccd2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=C3=AAn=20Minh=20H=E1=BB=93?= Date: Thu, 25 Jan 2024 14:24:35 +0700 Subject: [PATCH 0196/2493] Split editor toolbox radio button disabling logic from EditorRadioButton, then add disabling logic for rotate popover --- osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs | 8 +++++++- osu.Game/Rulesets/Edit/HitObjectComposer.cs | 8 ++++++++ .../Edit/Components/RadioButtons/EditorRadioButton.cs | 2 -- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs b/osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs index f09d6b78e6..fdab84f38d 100644 --- a/osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs +++ b/osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs @@ -24,6 +24,7 @@ namespace osu.Game.Rulesets.Osu.Edit private SliderWithTextBoxInput angleInput = null!; private EditorRadioButtonCollection rotationOrigin = null!; + private RadioButton selectionCentreButton = null!; public PreciseRotationPopover(SelectionRotationHandler rotationHandler) { this.rotationHandler = rotationHandler; @@ -59,7 +60,7 @@ namespace osu.Game.Rulesets.Osu.Edit new RadioButton("Playfield centre", () => rotationInfo.Value = rotationInfo.Value with { Origin = RotationOrigin.PlayfieldCentre }, () => new SpriteIcon { Icon = FontAwesome.Regular.Square }), - new RadioButton("Selection centre", + selectionCentreButton = new RadioButton("Selection centre", () => rotationInfo.Value = rotationInfo.Value with { Origin = RotationOrigin.SelectionCentre }, () => new SpriteIcon { Icon = FontAwesome.Solid.VectorSquare }) } @@ -76,6 +77,11 @@ namespace osu.Game.Rulesets.Osu.Edit angleInput.Current.BindValueChanged(angle => rotationInfo.Value = rotationInfo.Value with { Degrees = angle.NewValue }); rotationOrigin.Items.First().Select(); + rotationHandler.CanRotateSelectionOrigin.BindValueChanged(e => + { + selectionCentreButton.Selected.Disabled = !e.NewValue; + }, true); + rotationInfo.BindValueChanged(rotation => { rotationHandler.Update(rotation.NewValue.Degrees, rotation.NewValue.Origin == RotationOrigin.PlayfieldCentre ? OsuPlayfield.BASE_SIZE / 2 : null); diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs index 50e6393895..6abc6cb95b 100644 --- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs @@ -244,6 +244,14 @@ namespace osu.Game.Rulesets.Edit if (!timing.NewValue) setSelectTool(); }); + + EditorBeatmap.HasTiming.BindValueChanged(hasTiming => + { + foreach (var item in toolboxCollection.Items) + { + item.Selected.Disabled = !hasTiming.NewValue; + } + }, true); } protected override void Update() diff --git a/osu.Game/Screens/Edit/Components/RadioButtons/EditorRadioButton.cs b/osu.Game/Screens/Edit/Components/RadioButtons/EditorRadioButton.cs index 65f3e41c13..5549095639 100644 --- a/osu.Game/Screens/Edit/Components/RadioButtons/EditorRadioButton.cs +++ b/osu.Game/Screens/Edit/Components/RadioButtons/EditorRadioButton.cs @@ -76,8 +76,6 @@ namespace osu.Game.Screens.Edit.Components.RadioButtons Selected?.Invoke(Button); }; - editorBeatmap?.HasTiming.BindValueChanged(hasTiming => Button.Selected.Disabled = !hasTiming.NewValue, true); - Button.Selected.BindDisabledChanged(disabled => Enabled.Value = !disabled, true); updateSelectionState(); } From 94ada87cbad0828fcb669d0e4133f850b12a07b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=C3=AAn=20Minh=20H=E1=BB=93?= Date: Thu, 25 Jan 2024 14:24:45 +0700 Subject: [PATCH 0197/2493] Un-hardcode tooltip from EditorRadioButton and add disabled tooltip for rotation popover --- osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs | 2 ++ osu.Game/Rulesets/Edit/HitObjectComposer.cs | 5 +++++ .../Edit/Components/RadioButtons/EditorRadioButton.cs | 2 +- .../Edit/Components/RadioButtons/RadioButton.cs | 11 +++++++++++ 4 files changed, 19 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs b/osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs index fdab84f38d..2cf6799279 100644 --- a/osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs +++ b/osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs @@ -25,6 +25,7 @@ namespace osu.Game.Rulesets.Osu.Edit private EditorRadioButtonCollection rotationOrigin = null!; private RadioButton selectionCentreButton = null!; + public PreciseRotationPopover(SelectionRotationHandler rotationHandler) { this.rotationHandler = rotationHandler; @@ -67,6 +68,7 @@ namespace osu.Game.Rulesets.Osu.Edit } } }; + selectionCentreButton.TooltipTextWhenDisabled = "We can't rotate a circle around itself! Can we?"; } protected override void LoadComplete() diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs index 6abc6cb95b..bc8de7f4b2 100644 --- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs @@ -212,6 +212,11 @@ namespace osu.Game.Rulesets.Edit .Select(t => new RadioButton(t.Name, () => toolSelected(t), t.CreateIcon)) .ToList(); + foreach (var item in toolboxCollection.Items) + { + item.TooltipTextWhenDisabled = "Add at least one timing point first!"; + } + TernaryStates = CreateTernaryButtons().ToArray(); togglesCollection.AddRange(TernaryStates.Select(b => new DrawableTernaryButton(b))); diff --git a/osu.Game/Screens/Edit/Components/RadioButtons/EditorRadioButton.cs b/osu.Game/Screens/Edit/Components/RadioButtons/EditorRadioButton.cs index 5549095639..601548fadd 100644 --- a/osu.Game/Screens/Edit/Components/RadioButtons/EditorRadioButton.cs +++ b/osu.Game/Screens/Edit/Components/RadioButtons/EditorRadioButton.cs @@ -97,6 +97,6 @@ namespace osu.Game.Screens.Edit.Components.RadioButtons X = 40f }; - public LocalisableString TooltipText => Enabled.Value ? string.Empty : "Add at least one timing point first!"; + public LocalisableString TooltipText => Enabled.Value ? Button.TooltipTextWhenEnabled : Button.TooltipTextWhenDisabled; } } diff --git a/osu.Game/Screens/Edit/Components/RadioButtons/RadioButton.cs b/osu.Game/Screens/Edit/Components/RadioButtons/RadioButton.cs index 9dcd29bf83..1b47c028ab 100644 --- a/osu.Game/Screens/Edit/Components/RadioButtons/RadioButton.cs +++ b/osu.Game/Screens/Edit/Components/RadioButtons/RadioButton.cs @@ -4,6 +4,7 @@ using System; using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Framework.Localisation; namespace osu.Game.Screens.Edit.Components.RadioButtons { @@ -11,9 +12,19 @@ namespace osu.Game.Screens.Edit.Components.RadioButtons { /// /// Whether this is selected. + /// Disable this bindable to disable the button. /// public readonly BindableBool Selected; + /// + /// Tooltip text that will be shown on hover if button is enabled. + /// + public LocalisableString TooltipTextWhenEnabled { get; set; } = string.Empty; + /// + /// Tooltip text that will be shown on hover if button is disabled. + /// + public LocalisableString TooltipTextWhenDisabled { get; set; } = string.Empty; + /// /// The item related to this button. /// From b87ff4db0d5d69e7f8e787eb1135745c491c074e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=C3=AAn=20Minh=20H=E1=BB=93?= Date: Thu, 25 Jan 2024 15:33:48 +0700 Subject: [PATCH 0198/2493] Edit test for precise rotation popover --- .../Editor/TestScenePreciseRotation.cs | 41 +++++++++++++++++-- 1 file changed, 37 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePreciseRotation.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePreciseRotation.cs index d7dd30d608..67283f40da 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePreciseRotation.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePreciseRotation.cs @@ -24,14 +24,38 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor [Test] public void TestHotkeyHandling() { - AddStep("select single circle", () => EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects.OfType().First())); + AddStep("deselect everything", () => EditorBeatmap.SelectedHitObjects.Clear()); AddStep("press rotate hotkey", () => { InputManager.PressKey(Key.ControlLeft); InputManager.Key(Key.R); InputManager.ReleaseKey(Key.ControlLeft); }); - AddUntilStep("no popover present", () => this.ChildrenOfType().Count(), () => Is.Zero); + AddUntilStep("no popover present", getPopover, () => Is.Null); + + AddStep("select single circle", + () => EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects.OfType().First())); + AddStep("press rotate hotkey", () => + { + InputManager.PressKey(Key.ControlLeft); + InputManager.Key(Key.R); + InputManager.ReleaseKey(Key.ControlLeft); + }); + AddUntilStep("popover present", getPopover, () => Is.Not.Null); + AddAssert("only playfield centre origin rotation available", () => + { + var popover = getPopover(); + var buttons = popover.ChildrenOfType(); + return buttons.Any(btn => btn.Text == "Selection centre" && btn.Enabled.Value is false) && + buttons.Any(btn => btn.Text == "Playfield centre" && btn.Enabled.Value is true); + }); + AddStep("press rotate hotkey", () => + { + InputManager.PressKey(Key.ControlLeft); + InputManager.Key(Key.R); + InputManager.ReleaseKey(Key.ControlLeft); + }); + AddUntilStep("no popover present", getPopover, () => Is.Null); AddStep("select first three objects", () => { @@ -44,14 +68,23 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor InputManager.Key(Key.R); InputManager.ReleaseKey(Key.ControlLeft); }); - AddUntilStep("popover present", () => this.ChildrenOfType().Count(), () => Is.EqualTo(1)); + AddUntilStep("popover present", getPopover, () => Is.Not.Null); + AddAssert("both origin rotation available", () => + { + var popover = getPopover(); + var buttons = popover.ChildrenOfType(); + return buttons.Any(btn => btn.Text == "Selection centre" && btn.Enabled.Value is true) && + buttons.Any(btn => btn.Text == "Playfield centre" && btn.Enabled.Value is true); + }); AddStep("press rotate hotkey", () => { InputManager.PressKey(Key.ControlLeft); InputManager.Key(Key.R); InputManager.ReleaseKey(Key.ControlLeft); }); - AddUntilStep("no popover present", () => this.ChildrenOfType().Count(), () => Is.Zero); + AddUntilStep("no popover present", getPopover, () => Is.Null); + + PreciseRotationPopover? getPopover() => this.ChildrenOfType().SingleOrDefault(); } [Test] From 2fa52de87a07a7bbdbbde850ccd47d5722d1cc55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=C3=AAn=20Minh=20H=E1=BB=93?= Date: Thu, 25 Jan 2024 15:52:57 +0700 Subject: [PATCH 0199/2493] Fix formatting --- .../Editor/TestScenePreciseRotation.cs | 8 ++++---- osu.Game.Rulesets.Osu/Edit/OsuSelectionRotationHandler.cs | 3 ++- .../Edit/Components/RadioButtons/EditorRadioButton.cs | 3 --- .../Screens/Edit/Components/RadioButtons/RadioButton.cs | 1 + 4 files changed, 7 insertions(+), 8 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePreciseRotation.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePreciseRotation.cs index 67283f40da..30e0dbbf2e 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePreciseRotation.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePreciseRotation.cs @@ -46,8 +46,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor { var popover = getPopover(); var buttons = popover.ChildrenOfType(); - return buttons.Any(btn => btn.Text == "Selection centre" && btn.Enabled.Value is false) && - buttons.Any(btn => btn.Text == "Playfield centre" && btn.Enabled.Value is true); + return buttons.Any(btn => btn.Text == "Selection centre" && !btn.Enabled.Value) + && buttons.Any(btn => btn.Text == "Playfield centre" && btn.Enabled.Value); }); AddStep("press rotate hotkey", () => { @@ -73,8 +73,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor { var popover = getPopover(); var buttons = popover.ChildrenOfType(); - return buttons.Any(btn => btn.Text == "Selection centre" && btn.Enabled.Value is true) && - buttons.Any(btn => btn.Text == "Playfield centre" && btn.Enabled.Value is true); + return buttons.Any(btn => btn.Text == "Selection centre" && btn.Enabled.Value) + && buttons.Any(btn => btn.Text == "Playfield centre" && btn.Enabled.Value); }); AddStep("press rotate hotkey", () => { diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionRotationHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionRotationHandler.cs index 0ce78e4f61..cd01fc9f4d 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionRotationHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionRotationHandler.cs @@ -19,7 +19,8 @@ namespace osu.Game.Rulesets.Osu.Edit { public partial class OsuSelectionRotationHandler : SelectionRotationHandler { - public BindableBool CanRotatePlayfieldOrigin { get; private set; } = new(); + public BindableBool CanRotatePlayfieldOrigin { get; private set; } = new BindableBool(); + [Resolved] private IEditorChangeHandler? changeHandler { get; set; } diff --git a/osu.Game/Screens/Edit/Components/RadioButtons/EditorRadioButton.cs b/osu.Game/Screens/Edit/Components/RadioButtons/EditorRadioButton.cs index 601548fadd..9d1f87e1e0 100644 --- a/osu.Game/Screens/Edit/Components/RadioButtons/EditorRadioButton.cs +++ b/osu.Game/Screens/Edit/Components/RadioButtons/EditorRadioButton.cs @@ -33,9 +33,6 @@ namespace osu.Game.Screens.Edit.Components.RadioButtons private Drawable icon = null!; - [Resolved] - private EditorBeatmap? editorBeatmap { get; set; } - public EditorRadioButton(RadioButton button) { Button = button; diff --git a/osu.Game/Screens/Edit/Components/RadioButtons/RadioButton.cs b/osu.Game/Screens/Edit/Components/RadioButtons/RadioButton.cs index 1b47c028ab..2d1416c9c6 100644 --- a/osu.Game/Screens/Edit/Components/RadioButtons/RadioButton.cs +++ b/osu.Game/Screens/Edit/Components/RadioButtons/RadioButton.cs @@ -20,6 +20,7 @@ namespace osu.Game.Screens.Edit.Components.RadioButtons /// Tooltip text that will be shown on hover if button is enabled. /// public LocalisableString TooltipTextWhenEnabled { get; set; } = string.Empty; + /// /// Tooltip text that will be shown on hover if button is disabled. /// From d5b70ed09a68c8c99484df97dd4fbd245c233e8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=C3=AAn=20Minh=20H=E1=BB=93?= Date: Thu, 25 Jan 2024 16:56:59 +0700 Subject: [PATCH 0200/2493] Move CanRotatePlayfieldOrigin bindable to generic rotation handler --- osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs | 5 +---- osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs | 2 +- osu.Game.Rulesets.Osu/Edit/OsuSelectionRotationHandler.cs | 2 -- osu.Game.Rulesets.Osu/Edit/TransformToolboxGroup.cs | 3 ++- .../Edit/Compose/Components/SelectionRotationHandler.cs | 6 +++++- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs index a0fb0b06c3..448cfaf84c 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs @@ -101,10 +101,7 @@ namespace osu.Game.Rulesets.Osu.Edit RightToolbox.AddRange(new EditorToolboxGroup[] { - new TransformToolboxGroup - { - RotationHandler = (OsuSelectionRotationHandler)BlueprintContainer.SelectionHandler.RotationHandler, - }, + new TransformToolboxGroup { RotationHandler = BlueprintContainer.SelectionHandler.RotationHandler, }, FreehandlSliderToolboxGroup } ); diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs index 7e645bc670..cea2adc6e2 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs @@ -164,7 +164,7 @@ namespace osu.Game.Rulesets.Osu.Edit if ((reference & Anchor.y0) > 0) scale.Y = -scale.Y; } - public override OsuSelectionRotationHandler CreateRotationHandler() => new OsuSelectionRotationHandler(); + public override SelectionRotationHandler CreateRotationHandler() => new OsuSelectionRotationHandler(); 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 index cd01fc9f4d..1998e02a5c 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionRotationHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionRotationHandler.cs @@ -19,8 +19,6 @@ namespace osu.Game.Rulesets.Osu.Edit { public partial class OsuSelectionRotationHandler : SelectionRotationHandler { - public BindableBool CanRotatePlayfieldOrigin { get; private set; } = new BindableBool(); - [Resolved] private IEditorChangeHandler? changeHandler { get; set; } diff --git a/osu.Game.Rulesets.Osu/Edit/TransformToolboxGroup.cs b/osu.Game.Rulesets.Osu/Edit/TransformToolboxGroup.cs index c70f35c6fb..19590e9b6e 100644 --- a/osu.Game.Rulesets.Osu/Edit/TransformToolboxGroup.cs +++ b/osu.Game.Rulesets.Osu/Edit/TransformToolboxGroup.cs @@ -11,6 +11,7 @@ using osu.Framework.Input.Events; using osu.Game.Input.Bindings; using osu.Game.Rulesets.Edit; using osu.Game.Screens.Edit.Components; +using osu.Game.Screens.Edit.Compose.Components; using osuTK; namespace osu.Game.Rulesets.Osu.Edit @@ -21,7 +22,7 @@ namespace osu.Game.Rulesets.Osu.Edit private EditorToolButton rotateButton = null!; - public OsuSelectionRotationHandler RotationHandler { get; init; } = null!; + public SelectionRotationHandler RotationHandler { get; init; } = null!; public TransformToolboxGroup() : base("transform") diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionRotationHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionRotationHandler.cs index 749e1aab17..459e4b0c41 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionRotationHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionRotationHandler.cs @@ -14,10 +14,14 @@ namespace osu.Game.Screens.Edit.Compose.Components { /// /// Whether rotation anchored by the selection origin can currently be performed. - /// This is in constrast to rotation anchored by the entire field. /// public Bindable CanRotateSelectionOrigin { get; private set; } = new BindableBool(); + /// + /// Whether rotation anchored by the center of the playfield can currently be performed. + /// + public Bindable CanRotatePlayfieldOrigin { get; private set; } = new BindableBool(); + /// /// Performs a single, instant, atomic rotation operation. /// From 3f9c2b41f7080639d3f4d94097cfeb3238ee503a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 25 Jan 2024 21:28:08 +0900 Subject: [PATCH 0201/2493] Adjust `BeatSyncContainer`'s early animate offset based on source's rate --- osu.Game/Graphics/Containers/BeatSyncedContainer.cs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/osu.Game/Graphics/Containers/BeatSyncedContainer.cs b/osu.Game/Graphics/Containers/BeatSyncedContainer.cs index f911311a09..a14dfd4d64 100644 --- a/osu.Game/Graphics/Containers/BeatSyncedContainer.cs +++ b/osu.Game/Graphics/Containers/BeatSyncedContainer.cs @@ -91,7 +91,17 @@ namespace osu.Game.Graphics.Containers if (IsBeatSyncedWithTrack) { - currentTrackTime = BeatSyncSource.Clock.CurrentTime + EarlyActivationMilliseconds; + double early = EarlyActivationMilliseconds; + + // In the case of gameplay, we are usually within a hierarchy with the correct rate applied to our `Drawable.Clock`. + // This means that the amount of early adjustment is adjusted in line with audio track rate changes. + // But other cases like the osu! logo at the main menu won't correctly have this rate information. + // + // So for cases where the rate of the source isn't in sync with our hierarchy, let's assume we need to account for it locally. + if (Clock.Rate == 1 && BeatSyncSource.Clock.Rate != Clock.Rate) + early *= BeatSyncSource.Clock.Rate; + + currentTrackTime = BeatSyncSource.Clock.CurrentTime + early; timingPoint = BeatSyncSource.ControlPoints?.TimingPointAt(currentTrackTime) ?? TimingControlPoint.DEFAULT; effectPoint = BeatSyncSource.ControlPoints?.EffectPointAt(currentTrackTime) ?? EffectControlPoint.DEFAULT; From a77db5d837bb41f57b533c7dff1b893eaa5148a1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 26 Jan 2024 13:36:50 +0900 Subject: [PATCH 0202/2493] Add failing test coverage of editor metadata not saving --- .../Editing/TestSceneMetadataSection.cs | 105 ++++++++++++++++-- 1 file changed, 94 insertions(+), 11 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneMetadataSection.cs b/osu.Game.Tests/Visual/Editing/TestSceneMetadataSection.cs index a9f8e19e30..f767d9f7a3 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneMetadataSection.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneMetadataSection.cs @@ -3,17 +3,22 @@ #nullable disable +using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; +using osu.Framework.Graphics.UserInterface; +using osu.Framework.Input; +using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Rulesets.Osu; using osu.Game.Screens.Edit; using osu.Game.Screens.Edit.Setup; +using osuTK.Input; namespace osu.Game.Tests.Visual.Editing { - public partial class TestSceneMetadataSection : OsuTestScene + public partial class TestSceneMetadataSection : OsuManualInputManagerTestScene { [Cached] private EditorBeatmap editorBeatmap = new EditorBeatmap(new Beatmap @@ -26,6 +31,81 @@ namespace osu.Game.Tests.Visual.Editing private TestMetadataSection metadataSection; + [Test] + public void TestUpdateViaTextBoxOnFocusLoss() + { + AddStep("set metadata", () => + { + editorBeatmap.Metadata.Artist = "Example Artist"; + editorBeatmap.Metadata.ArtistUnicode = string.Empty; + }); + + createSection(); + + TextBox textbox = null!; + + AddStep("focus first textbox", () => + { + textbox = metadataSection.ChildrenOfType().First(); + InputManager.MoveMouseTo(textbox); + InputManager.Click(MouseButton.Left); + }); + + AddStep("simulate changing textbox", () => + { + // Can't simulate text input but this should work. + InputManager.Keys(PlatformAction.SelectAll); + InputManager.Keys(PlatformAction.Copy); + InputManager.Keys(PlatformAction.Paste); + InputManager.Keys(PlatformAction.Paste); + }); + + assertArtistMetadata("Example Artist"); + + // It's important values are committed immediately on focus loss so the editor exit sequence detects them. + AddAssert("value immediately changed on focus loss", () => + { + InputManager.TriggerFocusContention(metadataSection); + return editorBeatmap.Metadata.Artist; + }, () => Is.EqualTo("Example ArtistExample Artist")); + } + + [Test] + public void TestUpdateViaTextBoxOnCommit() + { + AddStep("set metadata", () => + { + editorBeatmap.Metadata.Artist = "Example Artist"; + editorBeatmap.Metadata.ArtistUnicode = string.Empty; + }); + + createSection(); + + TextBox textbox = null!; + + AddStep("focus first textbox", () => + { + textbox = metadataSection.ChildrenOfType().First(); + InputManager.MoveMouseTo(textbox); + InputManager.Click(MouseButton.Left); + }); + + AddStep("simulate changing textbox", () => + { + // Can't simulate text input but this should work. + InputManager.Keys(PlatformAction.SelectAll); + InputManager.Keys(PlatformAction.Copy); + InputManager.Keys(PlatformAction.Paste); + InputManager.Keys(PlatformAction.Paste); + }); + + assertArtistMetadata("Example Artist"); + + AddStep("commit", () => InputManager.Key(Key.Enter)); + + assertArtistMetadata("Example ArtistExample Artist"); + } + [Test] public void TestMinimalMetadata() { @@ -40,7 +120,7 @@ namespace osu.Game.Tests.Visual.Editing createSection(); - assertArtist("Example Artist"); + assertArtistTextBox("Example Artist"); assertRomanisedArtist("Example Artist", false); assertTitle("Example Title"); @@ -61,7 +141,7 @@ namespace osu.Game.Tests.Visual.Editing createSection(); - assertArtist("*なみりん"); + assertArtistTextBox("*なみりん"); assertRomanisedArtist(string.Empty, true); assertTitle("コイシテイク・プラネット"); @@ -82,7 +162,7 @@ namespace osu.Game.Tests.Visual.Editing createSection(); - assertArtist("*なみりん"); + assertArtistTextBox("*なみりん"); assertRomanisedArtist("*namirin", true); assertTitle("コイシテイク・プラネット"); @@ -104,11 +184,11 @@ namespace osu.Game.Tests.Visual.Editing createSection(); AddStep("set romanised artist name", () => metadataSection.ArtistTextBox.Current.Value = "*namirin"); - assertArtist("*namirin"); + assertArtistTextBox("*namirin"); assertRomanisedArtist("*namirin", false); AddStep("set native artist name", () => metadataSection.ArtistTextBox.Current.Value = "*なみりん"); - assertArtist("*なみりん"); + assertArtistTextBox("*なみりん"); assertRomanisedArtist("*namirin", true); AddStep("set romanised title", () => metadataSection.TitleTextBox.Current.Value = "Hitokoto no kyori"); @@ -123,21 +203,24 @@ namespace osu.Game.Tests.Visual.Editing private void createSection() => AddStep("create metadata section", () => Child = metadataSection = new TestMetadataSection()); - private void assertArtist(string expected) - => AddAssert($"artist is {expected}", () => metadataSection.ArtistTextBox.Current.Value == expected); + private void assertArtistMetadata(string expected) + => AddAssert($"artist metadata is {expected}", () => editorBeatmap.Metadata.Artist, () => Is.EqualTo(expected)); + + private void assertArtistTextBox(string expected) + => AddAssert($"artist textbox is {expected}", () => metadataSection.ArtistTextBox.Current.Value, () => Is.EqualTo(expected)); private void assertRomanisedArtist(string expected, bool editable) { - AddAssert($"romanised artist is {expected}", () => metadataSection.RomanisedArtistTextBox.Current.Value == expected); + AddAssert($"romanised artist is {expected}", () => metadataSection.RomanisedArtistTextBox.Current.Value, () => Is.EqualTo(expected)); AddAssert($"romanised artist is {(editable ? "" : "not ")}editable", () => metadataSection.RomanisedArtistTextBox.ReadOnly == !editable); } private void assertTitle(string expected) - => AddAssert($"title is {expected}", () => metadataSection.TitleTextBox.Current.Value == expected); + => AddAssert($"title is {expected}", () => metadataSection.TitleTextBox.Current.Value, () => Is.EqualTo(expected)); private void assertRomanisedTitle(string expected, bool editable) { - AddAssert($"romanised title is {expected}", () => metadataSection.RomanisedTitleTextBox.Current.Value == expected); + AddAssert($"romanised title is {expected}", () => metadataSection.RomanisedTitleTextBox.Current.Value, () => Is.EqualTo(expected)); AddAssert($"romanised title is {(editable ? "" : "not ")}editable", () => metadataSection.RomanisedTitleTextBox.ReadOnly == !editable); } From e54502eef1bd0d99e3a3b5c29476b0addc7a5c13 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 26 Jan 2024 13:59:28 +0900 Subject: [PATCH 0203/2493] Add full editor save test coverage --- .../TestSceneBeatmapEditorNavigation.cs | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneBeatmapEditorNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneBeatmapEditorNavigation.cs index 9930349b1b..370c40222e 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneBeatmapEditorNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneBeatmapEditorNavigation.cs @@ -7,6 +7,7 @@ using osu.Framework.Extensions; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics.UserInterface; +using osu.Framework.Input; using osu.Framework.Screens; using osu.Framework.Testing; using osu.Game.Beatmaps; @@ -17,6 +18,7 @@ using osu.Game.Rulesets.Mania; using osu.Game.Rulesets.Osu; using osu.Game.Screens.Edit; using osu.Game.Screens.Edit.GameplayTest; +using osu.Game.Screens.Edit.Setup; using osu.Game.Screens.Menu; using osu.Game.Screens.Select; using osu.Game.Screens.Select.Filter; @@ -27,6 +29,59 @@ namespace osu.Game.Tests.Visual.Navigation { public partial class TestSceneBeatmapEditorNavigation : OsuGameTestScene { + [Test] + public void TestChangeMetadataExitWhileTextboxFocusedPromptsSave() + { + BeatmapSetInfo beatmapSet = null!; + + AddStep("import test beatmap", () => Game.BeatmapManager.Import(TestResources.GetTestBeatmapForImport()).WaitSafely()); + AddStep("retrieve beatmap", () => beatmapSet = Game.BeatmapManager.QueryBeatmapSet(set => !set.Protected).AsNonNull().Value.Detach()); + + AddStep("present beatmap", () => Game.PresentBeatmap(beatmapSet)); + AddUntilStep("wait for song select", + () => Game.Beatmap.Value.BeatmapSetInfo.Equals(beatmapSet) + && Game.ScreenStack.CurrentScreen is PlaySongSelect songSelect + && songSelect.IsLoaded); + AddStep("switch ruleset", () => Game.Ruleset.Value = new ManiaRuleset().RulesetInfo); + + AddStep("open editor", () => ((PlaySongSelect)Game.ScreenStack.CurrentScreen).Edit(beatmapSet.Beatmaps.First(beatmap => beatmap.Ruleset.OnlineID == 0))); + AddUntilStep("wait for editor open", () => Game.ScreenStack.CurrentScreen is Editor editor && editor.ReadyForUse); + + AddStep("change to song setup", () => InputManager.Key(Key.F4)); + + TextBox textbox = null!; + + AddUntilStep("wait for metadata section", () => + { + var t = Game.ChildrenOfType().SingleOrDefault().ChildrenOfType().FirstOrDefault(); + + if (t == null) + return false; + + textbox = t; + return true; + }); + + AddStep("focus textbox", () => + { + InputManager.MoveMouseTo(textbox); + InputManager.Click(MouseButton.Left); + }); + + AddStep("simulate changing textbox", () => + { + // Can't simulate text input but this should work. + InputManager.Keys(PlatformAction.SelectAll); + InputManager.Keys(PlatformAction.Copy); + InputManager.Keys(PlatformAction.Paste); + InputManager.Keys(PlatformAction.Paste); + }); + + AddStep("exit", () => Game.ChildrenOfType().Single().Exit()); + + AddAssert("save dialog displayed", () => Game.ChildrenOfType().Single().CurrentDialog is PromptForSaveDialog); + } + [Test] public void TestEditorGameplayTestAlwaysUsesOriginalRuleset() { From 45f2980dc099ba8c421395dd4258cdf3df4a2e55 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 26 Jan 2024 13:23:02 +0900 Subject: [PATCH 0204/2493] Fix potential editor data loss if exiting while a textbox is focused --- osu.Game/Screens/Edit/Editor.cs | 6 ++++++ osu.Game/Screens/Edit/Setup/MetadataSection.cs | 10 +++++----- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index c1f6c02301..bc376f6165 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -719,6 +719,12 @@ namespace osu.Game.Screens.Edit public override bool OnExiting(ScreenExitEvent e) { + // Before exiting, trigger a focus loss. + // + // This is important to ensure that if the user is still editing a textbox, it will commit + // (and potentially block the exit procedure for save). + GetContainingInputManager().TriggerFocusContention(this); + if (!ExitConfirmed) { // dialog overlay may not be available in visual tests. diff --git a/osu.Game/Screens/Edit/Setup/MetadataSection.cs b/osu.Game/Screens/Edit/Setup/MetadataSection.cs index 752f590308..b51c03b796 100644 --- a/osu.Game/Screens/Edit/Setup/MetadataSection.cs +++ b/osu.Game/Screens/Edit/Setup/MetadataSection.cs @@ -53,9 +53,6 @@ namespace osu.Game.Screens.Edit.Setup sourceTextBox = createTextBox(BeatmapsetsStrings.ShowInfoSource, metadata.Source), tagsTextBox = createTextBox(BeatmapsetsStrings.ShowInfoTags, metadata.Tags) }; - - foreach (var item in Children.OfType()) - item.OnCommit += onCommit; } private TTextBox createTextBox(LocalisableString label, string initialValue) @@ -77,6 +74,10 @@ namespace osu.Game.Screens.Edit.Setup ArtistTextBox.Current.BindValueChanged(artist => transferIfRomanised(artist.NewValue, RomanisedArtistTextBox)); TitleTextBox.Current.BindValueChanged(title => transferIfRomanised(title.NewValue, RomanisedTitleTextBox)); + + foreach (var item in Children.OfType()) + item.OnCommit += onCommit; + updateReadOnlyState(); } @@ -86,7 +87,6 @@ namespace osu.Game.Screens.Edit.Setup target.Current.Value = value; updateReadOnlyState(); - Scheduler.AddOnce(updateMetadata); } private void updateReadOnlyState() @@ -101,7 +101,7 @@ namespace osu.Game.Screens.Edit.Setup // for now, update on commit rather than making BeatmapMetadata bindables. // after switching database engines we can reconsider if switching to bindables is a good direction. - Scheduler.AddOnce(updateMetadata); + updateMetadata(); } private void updateMetadata() From b5dbf24d2787569b4f813bf5631507492cdb0269 Mon Sep 17 00:00:00 2001 From: Sheepposu Date: Thu, 1 Feb 2024 10:19:09 -0500 Subject: [PATCH 0205/2493] early replay analysis settings version committing early version for others in the discussion to do their own testing with it --- osu.Game.Rulesets.Osu/OsuRuleset.cs | 3 + .../UI/HitMarkerContainer.cs | 134 ++++++++++++++++++ .../UI/OsuAnalysisSettings.cs | 81 +++++++++++ osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs | 4 + .../PlayerSettingsOverlayStrings.cs | 15 ++ osu.Game/Rulesets/Ruleset.cs | 3 + osu.Game/Screens/Play/Player.cs | 2 +- .../Play/PlayerSettings/AnalysisSettings.cs | 18 +++ osu.Game/Screens/Play/ReplayPlayer.cs | 5 + 9 files changed, 264 insertions(+), 1 deletion(-) create mode 100644 osu.Game.Rulesets.Osu/UI/HitMarkerContainer.cs create mode 100644 osu.Game.Rulesets.Osu/UI/OsuAnalysisSettings.cs create mode 100644 osu.Game/Screens/Play/PlayerSettings/AnalysisSettings.cs diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index 6752712be1..358553ac59 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -38,6 +38,7 @@ using osu.Game.Rulesets.Scoring.Legacy; using osu.Game.Rulesets.UI; using osu.Game.Scoring; using osu.Game.Screens.Edit.Setup; +using osu.Game.Screens.Play.PlayerSettings; using osu.Game.Screens.Ranking.Statistics; using osu.Game.Skinning; @@ -356,5 +357,7 @@ namespace osu.Game.Rulesets.Osu return adjustedDifficulty; } + + public override AnalysisSettings? CreateAnalysisSettings(DrawableRuleset drawableRuleset) => new OsuAnalysisSettings(drawableRuleset); } } diff --git a/osu.Game.Rulesets.Osu/UI/HitMarkerContainer.cs b/osu.Game.Rulesets.Osu/UI/HitMarkerContainer.cs new file mode 100644 index 0000000000..a9fc42e596 --- /dev/null +++ b/osu.Game.Rulesets.Osu/UI/HitMarkerContainer.cs @@ -0,0 +1,134 @@ +// 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; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Input; +using osu.Framework.Input.Bindings; +using osu.Framework.Input.Events; +using osuTK; + +namespace osu.Game.Rulesets.Osu.UI +{ + public partial class HitMarkerContainer : Container, IRequireHighFrequencyMousePosition, IKeyBindingHandler + { + private Vector2 lastMousePosition; + + public Bindable HitMarkerEnabled = new BindableBool(); + public Bindable AimMarkersEnabled = new BindableBool(); + + public override bool ReceivePositionalInputAt(Vector2 _) => true; + + public bool OnPressed(KeyBindingPressEvent e) + { + if (HitMarkerEnabled.Value && (e.Action == OsuAction.LeftButton || e.Action == OsuAction.RightButton)) + { + AddMarker(e.Action); + } + + return false; + } + + public void OnReleased(KeyBindingReleaseEvent e) { } + + protected override bool OnMouseMove(MouseMoveEvent e) + { + lastMousePosition = e.MousePosition; + + if (AimMarkersEnabled.Value) + { + AddMarker(null); + } + + return base.OnMouseMove(e); + } + + private void AddMarker(OsuAction? action) + { + Add(new HitMarkerDrawable(action) { Position = lastMousePosition }); + } + + private partial class HitMarkerDrawable : CompositeDrawable + { + private const double lifetime_duration = 1000; + private const double fade_out_time = 400; + + public override bool RemoveWhenNotAlive => true; + + public HitMarkerDrawable(OsuAction? action) + { + var colour = Colour4.Gray.Opacity(0.5F); + var length = 8; + var depth = float.MaxValue; + switch (action) + { + case OsuAction.LeftButton: + colour = Colour4.Orange; + length = 20; + depth = float.MinValue; + break; + case OsuAction.RightButton: + colour = Colour4.LightGreen; + length = 20; + depth = float.MinValue; + break; + } + + this.Depth = depth; + + InternalChildren = new Drawable[] + { + new Box + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(3, length), + Rotation = 45, + Colour = Colour4.Black.Opacity(0.5F) + }, + new Box + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(3, length), + Rotation = 135, + Colour = Colour4.Black.Opacity(0.5F) + }, + new Box + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(1, length), + Rotation = 45, + Colour = colour + }, + new Box + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(1, length), + Rotation = 135, + Colour = colour + } + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + LifetimeStart = Time.Current; + LifetimeEnd = LifetimeStart + lifetime_duration; + + Scheduler.AddDelayed(() => + { + this.FadeOut(fade_out_time); + }, lifetime_duration - fade_out_time); + } + } + } +} \ No newline at end of file diff --git a/osu.Game.Rulesets.Osu/UI/OsuAnalysisSettings.cs b/osu.Game.Rulesets.Osu/UI/OsuAnalysisSettings.cs new file mode 100644 index 0000000000..4694c1a560 --- /dev/null +++ b/osu.Game.Rulesets.Osu/UI/OsuAnalysisSettings.cs @@ -0,0 +1,81 @@ +// 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.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Logging; +using osu.Game.Localisation; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Osu.Mods; +using osu.Game.Rulesets.UI; +using osu.Game.Screens.Play.PlayerSettings; + +namespace osu.Game.Rulesets.Osu.UI +{ + public partial class OsuAnalysisSettings : AnalysisSettings + { + private static readonly Logger logger = Logger.GetLogger("osu-analysis-settings"); + + protected new DrawableOsuRuleset drawableRuleset => (DrawableOsuRuleset)base.drawableRuleset; + + private readonly PlayerCheckbox hitMarkerToggle; + private readonly PlayerCheckbox aimMarkerToggle; + private readonly PlayerCheckbox hideCursorToggle; + private readonly PlayerCheckbox? hiddenToggle; + + public OsuAnalysisSettings(DrawableRuleset drawableRuleset) + : base(drawableRuleset) + { + Children = new Drawable[] + { + hitMarkerToggle = new PlayerCheckbox { LabelText = PlayerSettingsOverlayStrings.HitMarkers }, + aimMarkerToggle = new PlayerCheckbox { LabelText = PlayerSettingsOverlayStrings.AimMarkers }, + hideCursorToggle = new PlayerCheckbox { LabelText = PlayerSettingsOverlayStrings.HideCursor } + }; + + // hidden stuff is just here for testing at the moment; to create the mod disabling functionality + + foreach (var mod in drawableRuleset.Mods) + { + if (mod is OsuModHidden) + { + logger.Add("Hidden is enabled", LogLevel.Debug); + Add(hiddenToggle = new PlayerCheckbox { LabelText = "Disable hidden" }); + break; + } + } + } + + protected override void LoadComplete() + { + drawableRuleset.Playfield.MarkersContainer.HitMarkerEnabled.BindTo(hitMarkerToggle.Current); + drawableRuleset.Playfield.MarkersContainer.AimMarkersEnabled.BindTo(aimMarkerToggle.Current); + hideCursorToggle.Current.BindValueChanged(onCursorToggle); + hiddenToggle?.Current.BindValueChanged(onHiddenToggle); + } + + private void onCursorToggle(ValueChangedEvent hide) + { + // this only hides half the cursor + if (hide.NewValue) + { + drawableRuleset.Playfield.Cursor.Hide(); + } else + { + drawableRuleset.Playfield.Cursor.Show(); + } + } + + private void onHiddenToggle(ValueChangedEvent off) + { + if (off.NewValue) + { + logger.Add("Hidden off", LogLevel.Debug); + } else + { + logger.Add("Hidden on", LogLevel.Debug); + } + } + } +} \ No newline at end of file diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs index 411a02c5af..d7e1732175 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs @@ -36,6 +36,9 @@ namespace osu.Game.Rulesets.Osu.UI private readonly JudgementPooler judgementPooler; public SmokeContainer Smoke { get; } + + public HitMarkerContainer MarkersContainer { get; } + public FollowPointRenderer FollowPoints { get; } public static readonly Vector2 BASE_SIZE = new Vector2(512, 384); @@ -59,6 +62,7 @@ namespace osu.Game.Rulesets.Osu.UI HitObjectContainer, judgementAboveHitObjectLayer = new Container { RelativeSizeAxes = Axes.Both }, approachCircles = new ProxyContainer { RelativeSizeAxes = Axes.Both }, + MarkersContainer = new HitMarkerContainer { RelativeSizeAxes = Axes.Both } }; HitPolicy = new StartTimeOrderedHitPolicy(); diff --git a/osu.Game/Localisation/PlayerSettingsOverlayStrings.cs b/osu.Game/Localisation/PlayerSettingsOverlayStrings.cs index 60874da561..f829fb4ac1 100644 --- a/osu.Game/Localisation/PlayerSettingsOverlayStrings.cs +++ b/osu.Game/Localisation/PlayerSettingsOverlayStrings.cs @@ -19,6 +19,21 @@ namespace osu.Game.Localisation /// public static LocalisableString StepForward => new TranslatableString(getKey(@"step_forward_frame"), @"Step forward one frame"); + /// + /// "Hit markers" + /// + public static LocalisableString HitMarkers => new TranslatableString(getKey(@"hit_markers"), @"Hit markers"); + + /// + /// "Aim markers" + /// + public static LocalisableString AimMarkers => new TranslatableString(getKey(@"aim_markers"), @"Aim markers"); + + /// + /// "Hide cursor" + /// + public static LocalisableString HideCursor => new TranslatableString(getKey(@"hide_cursor"), @"Hide cursor"); + /// /// "Seek backward {0} seconds" /// diff --git a/osu.Game/Rulesets/Ruleset.cs b/osu.Game/Rulesets/Ruleset.cs index 37a35fd3ae..5fbb23f094 100644 --- a/osu.Game/Rulesets/Ruleset.cs +++ b/osu.Game/Rulesets/Ruleset.cs @@ -27,6 +27,7 @@ using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI; using osu.Game.Scoring; using osu.Game.Screens.Edit.Setup; +using osu.Game.Screens.Play.PlayerSettings; using osu.Game.Screens.Ranking.Statistics; using osu.Game.Skinning; using osu.Game.Users; @@ -402,5 +403,7 @@ namespace osu.Game.Rulesets /// Can be overridden to alter the difficulty section to the editor beatmap setup screen. /// public virtual DifficultySection? CreateEditorDifficultySection() => null; + + public virtual AnalysisSettings? CreateAnalysisSettings(DrawableRuleset drawableRuleset) => null; } } diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index ad1f9ec897..0fa7143693 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -115,7 +115,7 @@ namespace osu.Game.Screens.Play public GameplayState GameplayState { get; private set; } - private Ruleset ruleset; + protected Ruleset ruleset; public BreakOverlay BreakOverlay; diff --git a/osu.Game/Screens/Play/PlayerSettings/AnalysisSettings.cs b/osu.Game/Screens/Play/PlayerSettings/AnalysisSettings.cs new file mode 100644 index 0000000000..122ca29142 --- /dev/null +++ b/osu.Game/Screens/Play/PlayerSettings/AnalysisSettings.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.Game.Rulesets.UI; + +namespace osu.Game.Screens.Play.PlayerSettings +{ + public partial class AnalysisSettings : PlayerSettingsGroup + { + protected DrawableRuleset drawableRuleset; + + public AnalysisSettings(DrawableRuleset drawableRuleset) + : base("Analysis Settings") + { + this.drawableRuleset = drawableRuleset; + } + } +} \ No newline at end of file diff --git a/osu.Game/Screens/Play/ReplayPlayer.cs b/osu.Game/Screens/Play/ReplayPlayer.cs index 3c5b85662a..0d877785e7 100644 --- a/osu.Game/Screens/Play/ReplayPlayer.cs +++ b/osu.Game/Screens/Play/ReplayPlayer.cs @@ -71,6 +71,11 @@ namespace osu.Game.Screens.Play playbackSettings.UserPlaybackRate.BindTo(master.UserPlaybackRate); HUDOverlay.PlayerSettingsOverlay.AddAtStart(playbackSettings); + + var analysisSettings = ruleset.CreateAnalysisSettings(DrawableRuleset); + if (analysisSettings != null) { + HUDOverlay.PlayerSettingsOverlay.AddAtStart(analysisSettings); + } } protected override void PrepareReplay() From 1428cbfbc34f29eb869d8867a5d4f76453fd1cfd Mon Sep 17 00:00:00 2001 From: OliBomby Date: Thu, 1 Feb 2024 16:56:57 +0100 Subject: [PATCH 0206/2493] Remove Masking from PositionSnapGrid This caused issues in rendering the outline of the grid because the outline was getting masked at some resolutions. --- .../Components/LinedPositionSnapGrid.cs | 128 +++++++++++++++--- .../Compose/Components/PositionSnapGrid.cs | 2 - 2 files changed, 106 insertions(+), 24 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/LinedPositionSnapGrid.cs b/osu.Game/Screens/Edit/Compose/Components/LinedPositionSnapGrid.cs index ebdd76a4e2..8a7f6b5344 100644 --- a/osu.Game/Screens/Edit/Compose/Components/LinedPositionSnapGrid.cs +++ b/osu.Game/Screens/Edit/Compose/Components/LinedPositionSnapGrid.cs @@ -15,18 +15,29 @@ namespace osu.Game.Screens.Edit.Compose.Components { protected void GenerateGridLines(Vector2 step, Vector2 drawSize) { + if (Precision.AlmostEquals(step, Vector2.Zero)) + return; + int index = 0; - var currentPosition = StartPosition.Value; // Make lines the same width independent of display resolution. float lineWidth = DrawWidth / ScreenSpaceDrawQuad.Width; - float lineLength = drawSize.Length * 2; + float rotation = MathHelper.RadiansToDegrees(MathF.Atan2(step.Y, step.X)); List generatedLines = new List(); - while (lineDefinitelyIntersectsBox(currentPosition, step.PerpendicularLeft, drawSize) || - isMovingTowardsBox(currentPosition, step, drawSize)) + while (true) { + Vector2 currentPosition = StartPosition.Value + index++ * step; + + if (!lineDefinitelyIntersectsBox(currentPosition, step.PerpendicularLeft, drawSize, out var p1, out var p2)) + { + if (!isMovingTowardsBox(currentPosition, step, drawSize)) + break; + + continue; + } + var gridLine = new Box { Colour = Colour4.White, @@ -34,15 +45,12 @@ namespace osu.Game.Screens.Edit.Compose.Components Origin = Anchor.Centre, RelativeSizeAxes = Axes.None, Width = lineWidth, - Height = lineLength, - Position = currentPosition, - Rotation = MathHelper.RadiansToDegrees(MathF.Atan2(step.Y, step.X)), + Height = Vector2.Distance(p1, p2), + Position = (p1 + p2) / 2, + Rotation = rotation, }; generatedLines.Add(gridLine); - - index += 1; - currentPosition = StartPosition.Value + index * step; } if (generatedLines.Count == 0) @@ -59,23 +67,99 @@ namespace osu.Game.Screens.Edit.Compose.Components (currentPosition + step - box).LengthSquared < (currentPosition - box).LengthSquared; } - private bool lineDefinitelyIntersectsBox(Vector2 lineStart, Vector2 lineDir, Vector2 box) + /// + /// Determines if the line starting at and going in the direction of + /// definitely intersects the box on (0, 0) with the given width and height and returns the intersection points if it does. + /// + /// The start point of the line. + /// The direction of the line. + /// The width and height of the box. + /// The first intersection point. + /// The second intersection point. + /// Whether the line definitely intersects the box. + private bool lineDefinitelyIntersectsBox(Vector2 lineStart, Vector2 lineDir, Vector2 box, out Vector2 p1, out Vector2 p2) { - var p2 = lineStart + lineDir; + p1 = Vector2.Zero; + p2 = Vector2.Zero; - double d1 = det(Vector2.Zero); - double d2 = det(new Vector2(box.X, 0)); - double d3 = det(new Vector2(0, box.Y)); - double d4 = det(box); + if (Precision.AlmostEquals(lineDir.X, 0)) + { + // If the line is vertical, we only need to check if the X coordinate of the line is within the box. + if (!Precision.DefinitelyBigger(lineStart.X, 0) || !Precision.DefinitelyBigger(box.X, lineStart.X)) + return false; - return definitelyDifferentSign(d1, d2) || definitelyDifferentSign(d3, d4) || - definitelyDifferentSign(d1, d3) || definitelyDifferentSign(d2, d4); + p1 = new Vector2(lineStart.X, 0); + p2 = new Vector2(lineStart.X, box.Y); + return true; + } - double det(Vector2 p) => (p.X - lineStart.X) * (p2.Y - lineStart.Y) - (p.Y - lineStart.Y) * (p2.X - lineStart.X); + if (Precision.AlmostEquals(lineDir.Y, 0)) + { + // If the line is horizontal, we only need to check if the Y coordinate of the line is within the box. + if (!Precision.DefinitelyBigger(lineStart.Y, 0) || !Precision.DefinitelyBigger(box.Y, lineStart.Y)) + return false; - bool definitelyDifferentSign(double a, double b) => !Precision.AlmostEquals(a, 0) && - !Precision.AlmostEquals(b, 0) && - Math.Sign(a) != Math.Sign(b); + p1 = new Vector2(0, lineStart.Y); + p2 = new Vector2(box.X, lineStart.Y); + return true; + } + + float m = lineDir.Y / lineDir.X; + float mInv = lineDir.X / lineDir.Y; // Use this to improve numerical stability if X is close to zero. + float b = lineStart.Y - m * lineStart.X; + + // Calculate intersection points with the sides of the box. + var p = new List(4); + + if (0 <= b && b <= box.Y) + p.Add(new Vector2(0, b)); + if (0 <= (box.Y - b) * mInv && (box.Y - b) * mInv <= box.X) + p.Add(new Vector2((box.Y - b) * mInv, box.Y)); + if (0 <= m * box.X + b && m * box.X + b <= box.Y) + p.Add(new Vector2(box.X, m * box.X + b)); + if (0 <= -b * mInv && -b * mInv <= box.X) + p.Add(new Vector2(-b * mInv, 0)); + + switch (p.Count) + { + case 4: + // If there are 4 intersection points, the line is a diagonal of the box. + if (m > 0) + { + p1 = Vector2.Zero; + p2 = box; + } + else + { + p1 = new Vector2(0, box.Y); + p2 = new Vector2(box.X, 0); + } + + break; + + case 3: + // If there are 3 intersection points, the line goes through a corner of the box. + if (p[0] == p[1]) + { + p1 = p[0]; + p2 = p[2]; + } + else + { + p1 = p[0]; + p2 = p[1]; + } + + break; + + case 2: + p1 = p[0]; + p2 = p[1]; + + break; + } + + return !Precision.AlmostEquals(p1, p2); } } } diff --git a/osu.Game/Screens/Edit/Compose/Components/PositionSnapGrid.cs b/osu.Game/Screens/Edit/Compose/Components/PositionSnapGrid.cs index 36687ef73a..e576ac1e49 100644 --- a/osu.Game/Screens/Edit/Compose/Components/PositionSnapGrid.cs +++ b/osu.Game/Screens/Edit/Compose/Components/PositionSnapGrid.cs @@ -21,8 +21,6 @@ namespace osu.Game.Screens.Edit.Compose.Components protected PositionSnapGrid() { - Masking = true; - StartPosition.BindValueChanged(_ => GridCache.Invalidate()); AddLayout(GridCache); From f807a3fd971527ba62938b674946989c44a4966c Mon Sep 17 00:00:00 2001 From: OliBomby Date: Thu, 1 Feb 2024 16:56:57 +0100 Subject: [PATCH 0207/2493] Remove Masking from PositionSnapGrid This caused issues in rendering the outline of the grid because the outline was getting masked at some resolutions. --- .../Components/LinedPositionSnapGrid.cs | 128 +++++++++++++++--- .../Compose/Components/PositionSnapGrid.cs | 2 - 2 files changed, 106 insertions(+), 24 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/LinedPositionSnapGrid.cs b/osu.Game/Screens/Edit/Compose/Components/LinedPositionSnapGrid.cs index ebdd76a4e2..8a7f6b5344 100644 --- a/osu.Game/Screens/Edit/Compose/Components/LinedPositionSnapGrid.cs +++ b/osu.Game/Screens/Edit/Compose/Components/LinedPositionSnapGrid.cs @@ -15,18 +15,29 @@ namespace osu.Game.Screens.Edit.Compose.Components { protected void GenerateGridLines(Vector2 step, Vector2 drawSize) { + if (Precision.AlmostEquals(step, Vector2.Zero)) + return; + int index = 0; - var currentPosition = StartPosition.Value; // Make lines the same width independent of display resolution. float lineWidth = DrawWidth / ScreenSpaceDrawQuad.Width; - float lineLength = drawSize.Length * 2; + float rotation = MathHelper.RadiansToDegrees(MathF.Atan2(step.Y, step.X)); List generatedLines = new List(); - while (lineDefinitelyIntersectsBox(currentPosition, step.PerpendicularLeft, drawSize) || - isMovingTowardsBox(currentPosition, step, drawSize)) + while (true) { + Vector2 currentPosition = StartPosition.Value + index++ * step; + + if (!lineDefinitelyIntersectsBox(currentPosition, step.PerpendicularLeft, drawSize, out var p1, out var p2)) + { + if (!isMovingTowardsBox(currentPosition, step, drawSize)) + break; + + continue; + } + var gridLine = new Box { Colour = Colour4.White, @@ -34,15 +45,12 @@ namespace osu.Game.Screens.Edit.Compose.Components Origin = Anchor.Centre, RelativeSizeAxes = Axes.None, Width = lineWidth, - Height = lineLength, - Position = currentPosition, - Rotation = MathHelper.RadiansToDegrees(MathF.Atan2(step.Y, step.X)), + Height = Vector2.Distance(p1, p2), + Position = (p1 + p2) / 2, + Rotation = rotation, }; generatedLines.Add(gridLine); - - index += 1; - currentPosition = StartPosition.Value + index * step; } if (generatedLines.Count == 0) @@ -59,23 +67,99 @@ namespace osu.Game.Screens.Edit.Compose.Components (currentPosition + step - box).LengthSquared < (currentPosition - box).LengthSquared; } - private bool lineDefinitelyIntersectsBox(Vector2 lineStart, Vector2 lineDir, Vector2 box) + /// + /// Determines if the line starting at and going in the direction of + /// definitely intersects the box on (0, 0) with the given width and height and returns the intersection points if it does. + /// + /// The start point of the line. + /// The direction of the line. + /// The width and height of the box. + /// The first intersection point. + /// The second intersection point. + /// Whether the line definitely intersects the box. + private bool lineDefinitelyIntersectsBox(Vector2 lineStart, Vector2 lineDir, Vector2 box, out Vector2 p1, out Vector2 p2) { - var p2 = lineStart + lineDir; + p1 = Vector2.Zero; + p2 = Vector2.Zero; - double d1 = det(Vector2.Zero); - double d2 = det(new Vector2(box.X, 0)); - double d3 = det(new Vector2(0, box.Y)); - double d4 = det(box); + if (Precision.AlmostEquals(lineDir.X, 0)) + { + // If the line is vertical, we only need to check if the X coordinate of the line is within the box. + if (!Precision.DefinitelyBigger(lineStart.X, 0) || !Precision.DefinitelyBigger(box.X, lineStart.X)) + return false; - return definitelyDifferentSign(d1, d2) || definitelyDifferentSign(d3, d4) || - definitelyDifferentSign(d1, d3) || definitelyDifferentSign(d2, d4); + p1 = new Vector2(lineStart.X, 0); + p2 = new Vector2(lineStart.X, box.Y); + return true; + } - double det(Vector2 p) => (p.X - lineStart.X) * (p2.Y - lineStart.Y) - (p.Y - lineStart.Y) * (p2.X - lineStart.X); + if (Precision.AlmostEquals(lineDir.Y, 0)) + { + // If the line is horizontal, we only need to check if the Y coordinate of the line is within the box. + if (!Precision.DefinitelyBigger(lineStart.Y, 0) || !Precision.DefinitelyBigger(box.Y, lineStart.Y)) + return false; - bool definitelyDifferentSign(double a, double b) => !Precision.AlmostEquals(a, 0) && - !Precision.AlmostEquals(b, 0) && - Math.Sign(a) != Math.Sign(b); + p1 = new Vector2(0, lineStart.Y); + p2 = new Vector2(box.X, lineStart.Y); + return true; + } + + float m = lineDir.Y / lineDir.X; + float mInv = lineDir.X / lineDir.Y; // Use this to improve numerical stability if X is close to zero. + float b = lineStart.Y - m * lineStart.X; + + // Calculate intersection points with the sides of the box. + var p = new List(4); + + if (0 <= b && b <= box.Y) + p.Add(new Vector2(0, b)); + if (0 <= (box.Y - b) * mInv && (box.Y - b) * mInv <= box.X) + p.Add(new Vector2((box.Y - b) * mInv, box.Y)); + if (0 <= m * box.X + b && m * box.X + b <= box.Y) + p.Add(new Vector2(box.X, m * box.X + b)); + if (0 <= -b * mInv && -b * mInv <= box.X) + p.Add(new Vector2(-b * mInv, 0)); + + switch (p.Count) + { + case 4: + // If there are 4 intersection points, the line is a diagonal of the box. + if (m > 0) + { + p1 = Vector2.Zero; + p2 = box; + } + else + { + p1 = new Vector2(0, box.Y); + p2 = new Vector2(box.X, 0); + } + + break; + + case 3: + // If there are 3 intersection points, the line goes through a corner of the box. + if (p[0] == p[1]) + { + p1 = p[0]; + p2 = p[2]; + } + else + { + p1 = p[0]; + p2 = p[1]; + } + + break; + + case 2: + p1 = p[0]; + p2 = p[1]; + + break; + } + + return !Precision.AlmostEquals(p1, p2); } } } diff --git a/osu.Game/Screens/Edit/Compose/Components/PositionSnapGrid.cs b/osu.Game/Screens/Edit/Compose/Components/PositionSnapGrid.cs index 36687ef73a..e576ac1e49 100644 --- a/osu.Game/Screens/Edit/Compose/Components/PositionSnapGrid.cs +++ b/osu.Game/Screens/Edit/Compose/Components/PositionSnapGrid.cs @@ -21,8 +21,6 @@ namespace osu.Game.Screens.Edit.Compose.Components protected PositionSnapGrid() { - Masking = true; - StartPosition.BindValueChanged(_ => GridCache.Invalidate()); AddLayout(GridCache); From 236f029dad22722ebf73c02d40dc19b312b16642 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Thu, 1 Feb 2024 16:56:57 +0100 Subject: [PATCH 0208/2493] Remove Masking from PositionSnapGrid This caused issues in rendering the outline of the grid because the outline was getting masked at some resolutions. --- .../Components/LinedPositionSnapGrid.cs | 128 +++++++++++++++--- .../Compose/Components/PositionSnapGrid.cs | 2 - 2 files changed, 106 insertions(+), 24 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/LinedPositionSnapGrid.cs b/osu.Game/Screens/Edit/Compose/Components/LinedPositionSnapGrid.cs index ebdd76a4e2..8a7f6b5344 100644 --- a/osu.Game/Screens/Edit/Compose/Components/LinedPositionSnapGrid.cs +++ b/osu.Game/Screens/Edit/Compose/Components/LinedPositionSnapGrid.cs @@ -15,18 +15,29 @@ namespace osu.Game.Screens.Edit.Compose.Components { protected void GenerateGridLines(Vector2 step, Vector2 drawSize) { + if (Precision.AlmostEquals(step, Vector2.Zero)) + return; + int index = 0; - var currentPosition = StartPosition.Value; // Make lines the same width independent of display resolution. float lineWidth = DrawWidth / ScreenSpaceDrawQuad.Width; - float lineLength = drawSize.Length * 2; + float rotation = MathHelper.RadiansToDegrees(MathF.Atan2(step.Y, step.X)); List generatedLines = new List(); - while (lineDefinitelyIntersectsBox(currentPosition, step.PerpendicularLeft, drawSize) || - isMovingTowardsBox(currentPosition, step, drawSize)) + while (true) { + Vector2 currentPosition = StartPosition.Value + index++ * step; + + if (!lineDefinitelyIntersectsBox(currentPosition, step.PerpendicularLeft, drawSize, out var p1, out var p2)) + { + if (!isMovingTowardsBox(currentPosition, step, drawSize)) + break; + + continue; + } + var gridLine = new Box { Colour = Colour4.White, @@ -34,15 +45,12 @@ namespace osu.Game.Screens.Edit.Compose.Components Origin = Anchor.Centre, RelativeSizeAxes = Axes.None, Width = lineWidth, - Height = lineLength, - Position = currentPosition, - Rotation = MathHelper.RadiansToDegrees(MathF.Atan2(step.Y, step.X)), + Height = Vector2.Distance(p1, p2), + Position = (p1 + p2) / 2, + Rotation = rotation, }; generatedLines.Add(gridLine); - - index += 1; - currentPosition = StartPosition.Value + index * step; } if (generatedLines.Count == 0) @@ -59,23 +67,99 @@ namespace osu.Game.Screens.Edit.Compose.Components (currentPosition + step - box).LengthSquared < (currentPosition - box).LengthSquared; } - private bool lineDefinitelyIntersectsBox(Vector2 lineStart, Vector2 lineDir, Vector2 box) + /// + /// Determines if the line starting at and going in the direction of + /// definitely intersects the box on (0, 0) with the given width and height and returns the intersection points if it does. + /// + /// The start point of the line. + /// The direction of the line. + /// The width and height of the box. + /// The first intersection point. + /// The second intersection point. + /// Whether the line definitely intersects the box. + private bool lineDefinitelyIntersectsBox(Vector2 lineStart, Vector2 lineDir, Vector2 box, out Vector2 p1, out Vector2 p2) { - var p2 = lineStart + lineDir; + p1 = Vector2.Zero; + p2 = Vector2.Zero; - double d1 = det(Vector2.Zero); - double d2 = det(new Vector2(box.X, 0)); - double d3 = det(new Vector2(0, box.Y)); - double d4 = det(box); + if (Precision.AlmostEquals(lineDir.X, 0)) + { + // If the line is vertical, we only need to check if the X coordinate of the line is within the box. + if (!Precision.DefinitelyBigger(lineStart.X, 0) || !Precision.DefinitelyBigger(box.X, lineStart.X)) + return false; - return definitelyDifferentSign(d1, d2) || definitelyDifferentSign(d3, d4) || - definitelyDifferentSign(d1, d3) || definitelyDifferentSign(d2, d4); + p1 = new Vector2(lineStart.X, 0); + p2 = new Vector2(lineStart.X, box.Y); + return true; + } - double det(Vector2 p) => (p.X - lineStart.X) * (p2.Y - lineStart.Y) - (p.Y - lineStart.Y) * (p2.X - lineStart.X); + if (Precision.AlmostEquals(lineDir.Y, 0)) + { + // If the line is horizontal, we only need to check if the Y coordinate of the line is within the box. + if (!Precision.DefinitelyBigger(lineStart.Y, 0) || !Precision.DefinitelyBigger(box.Y, lineStart.Y)) + return false; - bool definitelyDifferentSign(double a, double b) => !Precision.AlmostEquals(a, 0) && - !Precision.AlmostEquals(b, 0) && - Math.Sign(a) != Math.Sign(b); + p1 = new Vector2(0, lineStart.Y); + p2 = new Vector2(box.X, lineStart.Y); + return true; + } + + float m = lineDir.Y / lineDir.X; + float mInv = lineDir.X / lineDir.Y; // Use this to improve numerical stability if X is close to zero. + float b = lineStart.Y - m * lineStart.X; + + // Calculate intersection points with the sides of the box. + var p = new List(4); + + if (0 <= b && b <= box.Y) + p.Add(new Vector2(0, b)); + if (0 <= (box.Y - b) * mInv && (box.Y - b) * mInv <= box.X) + p.Add(new Vector2((box.Y - b) * mInv, box.Y)); + if (0 <= m * box.X + b && m * box.X + b <= box.Y) + p.Add(new Vector2(box.X, m * box.X + b)); + if (0 <= -b * mInv && -b * mInv <= box.X) + p.Add(new Vector2(-b * mInv, 0)); + + switch (p.Count) + { + case 4: + // If there are 4 intersection points, the line is a diagonal of the box. + if (m > 0) + { + p1 = Vector2.Zero; + p2 = box; + } + else + { + p1 = new Vector2(0, box.Y); + p2 = new Vector2(box.X, 0); + } + + break; + + case 3: + // If there are 3 intersection points, the line goes through a corner of the box. + if (p[0] == p[1]) + { + p1 = p[0]; + p2 = p[2]; + } + else + { + p1 = p[0]; + p2 = p[1]; + } + + break; + + case 2: + p1 = p[0]; + p2 = p[1]; + + break; + } + + return !Precision.AlmostEquals(p1, p2); } } } diff --git a/osu.Game/Screens/Edit/Compose/Components/PositionSnapGrid.cs b/osu.Game/Screens/Edit/Compose/Components/PositionSnapGrid.cs index 36687ef73a..e576ac1e49 100644 --- a/osu.Game/Screens/Edit/Compose/Components/PositionSnapGrid.cs +++ b/osu.Game/Screens/Edit/Compose/Components/PositionSnapGrid.cs @@ -21,8 +21,6 @@ namespace osu.Game.Screens.Edit.Compose.Components protected PositionSnapGrid() { - Masking = true; - StartPosition.BindValueChanged(_ => GridCache.Invalidate()); AddLayout(GridCache); From 576d6ff7990a7bbb4842cc943c8842c3ed562a3b Mon Sep 17 00:00:00 2001 From: OliBomby Date: Thu, 1 Feb 2024 17:07:03 +0100 Subject: [PATCH 0209/2493] Fix masking in circular snap grid --- .../Edit/Compose/Components/CircularPositionSnapGrid.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/CircularPositionSnapGrid.cs b/osu.Game/Screens/Edit/Compose/Components/CircularPositionSnapGrid.cs index 403a270359..791cb33439 100644 --- a/osu.Game/Screens/Edit/Compose/Components/CircularPositionSnapGrid.cs +++ b/osu.Game/Screens/Edit/Compose/Components/CircularPositionSnapGrid.cs @@ -82,7 +82,12 @@ namespace osu.Game.Screens.Edit.Compose.Components generatedCircles.First().Alpha = 0.8f; - AddRangeInternal(generatedCircles); + AddInternal(new Container + { + Masking = true, + RelativeSizeAxes = Axes.Both, + Children = generatedCircles, + }); } public override Vector2 GetSnappedPosition(Vector2 original) From b4b5cdfcf2d84527ec4d5cb9e504152be59b7e19 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Thu, 1 Feb 2024 17:07:03 +0100 Subject: [PATCH 0210/2493] Fix masking in circular snap grid --- .../Edit/Compose/Components/CircularPositionSnapGrid.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/CircularPositionSnapGrid.cs b/osu.Game/Screens/Edit/Compose/Components/CircularPositionSnapGrid.cs index 403a270359..791cb33439 100644 --- a/osu.Game/Screens/Edit/Compose/Components/CircularPositionSnapGrid.cs +++ b/osu.Game/Screens/Edit/Compose/Components/CircularPositionSnapGrid.cs @@ -82,7 +82,12 @@ namespace osu.Game.Screens.Edit.Compose.Components generatedCircles.First().Alpha = 0.8f; - AddRangeInternal(generatedCircles); + AddInternal(new Container + { + Masking = true, + RelativeSizeAxes = Axes.Both, + Children = generatedCircles, + }); } public override Vector2 GetSnappedPosition(Vector2 original) From 5a1b0004db37148191ccbc349b8bd6fe4ca9e502 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 2 Feb 2024 01:17:28 +0300 Subject: [PATCH 0211/2493] Add failing test case --- .../Visual/Gameplay/TestScenePlayerLoader.cs | 44 ++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs index f97372e9b6..b2e607d9a3 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs @@ -24,9 +24,11 @@ using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; +using osu.Game.Screens.Menu; using osu.Game.Screens.Play; using osu.Game.Screens.Play.PlayerSettings; using osu.Game.Utils; +using osuTK; using osuTK.Input; namespace osu.Game.Tests.Visual.Gameplay @@ -53,6 +55,9 @@ namespace osu.Game.Tests.Visual.Gameplay [Cached] private readonly VolumeOverlay volumeOverlay; + [Cached] + private readonly OsuLogo logo; + [Cached(typeof(BatteryInfo))] private readonly LocalBatteryInfo batteryInfo = new LocalBatteryInfo(); @@ -76,7 +81,14 @@ namespace osu.Game.Tests.Visual.Gameplay Anchor = Anchor.TopLeft, Origin = Anchor.TopLeft, }, - changelogOverlay = new ChangelogOverlay() + changelogOverlay = new ChangelogOverlay(), + logo = new OsuLogo + { + Anchor = Anchor.BottomRight, + Origin = Anchor.BottomRight, + Scale = new Vector2(0.5f), + Position = new Vector2(128f), + }, }); } @@ -204,6 +216,36 @@ namespace osu.Game.Tests.Visual.Gameplay AddUntilStep("loads after idle", () => !loader.IsCurrentScreen()); } + [Test] + public void TestLoadNotBlockedOnOsuLogo() + { + AddStep("load dummy beatmap", () => resetPlayer(false)); + AddUntilStep("wait for current", () => loader.IsCurrentScreen()); + + AddUntilStep("wait for load ready", () => + { + moveMouse(); + return player?.LoadState == LoadState.Ready; + }); + + // move mouse in logo while waiting for load to still proceed (it shouldn't be blocked when hovering logo). + AddUntilStep("move mouse in logo", () => + { + moveMouse(); + return !loader.IsCurrentScreen(); + }); + + void moveMouse() + { + notificationOverlay.State.Value = Visibility.Hidden; + + InputManager.MoveMouseTo( + logo.ScreenSpaceDrawQuad.TopLeft + + (logo.ScreenSpaceDrawQuad.BottomRight - logo.ScreenSpaceDrawQuad.TopLeft) + * RNG.NextSingle(0.3f, 0.7f)); + } + } + [Test] public void TestLoadContinuation() { From 0502997ae958e0697fcdbcad2555e80c52b6a22d Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 2 Feb 2024 01:02:13 +0300 Subject: [PATCH 0212/2493] Stop blocking player loader when hovering over osu! logo --- osu.Game/Screens/Play/PlayerLoader.cs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs index 232de53ac3..201511529e 100644 --- a/osu.Game/Screens/Play/PlayerLoader.cs +++ b/osu.Game/Screens/Play/PlayerLoader.cs @@ -106,8 +106,8 @@ namespace osu.Game.Screens.Play && ReadyForGameplay; protected virtual bool ReadyForGameplay => - // not ready if the user is hovering one of the panes, unless they are idle. - (IsHovered || idleTracker.IsIdle.Value) + // not ready if the user is hovering one of the panes (logo is excluded), unless they are idle. + (IsHovered || osuLogo?.IsHovered == true || idleTracker.IsIdle.Value) // not ready if the user is dragging a slider or otherwise. && inputManager.DraggedDrawable == null // not ready if a focused overlay is visible, like settings. @@ -306,10 +306,14 @@ namespace osu.Game.Screens.Play return base.OnExiting(e); } + private OsuLogo? osuLogo; + protected override void LogoArriving(OsuLogo logo, bool resuming) { base.LogoArriving(logo, resuming); + osuLogo = logo; + const double duration = 300; if (!resuming) logo.MoveTo(new Vector2(0.5f), duration, Easing.OutQuint); @@ -328,6 +332,7 @@ namespace osu.Game.Screens.Play { base.LogoExiting(logo); content.StopTracking(); + osuLogo = null; } protected override void LogoSuspending(OsuLogo logo) @@ -338,6 +343,8 @@ namespace osu.Game.Screens.Play logo .FadeOut(CONTENT_OUT_DURATION / 2, Easing.OutQuint) .ScaleTo(logo.Scale * 0.8f, CONTENT_OUT_DURATION * 2, Easing.OutQuint); + + osuLogo = null; } #endregion From fe0433e6ecd1a69132fee41626562590c4b688ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 2 Feb 2024 13:24:59 +0100 Subject: [PATCH 0213/2493] Remove redundant default value assignments --- osu.Game.Tests/Visual/Editing/TestSceneMetadataSection.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneMetadataSection.cs b/osu.Game.Tests/Visual/Editing/TestSceneMetadataSection.cs index f767d9f7a3..5930c077a4 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneMetadataSection.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneMetadataSection.cs @@ -42,7 +42,7 @@ namespace osu.Game.Tests.Visual.Editing createSection(); - TextBox textbox = null!; + TextBox textbox; AddStep("focus first textbox", () => { @@ -81,7 +81,7 @@ namespace osu.Game.Tests.Visual.Editing createSection(); - TextBox textbox = null!; + TextBox textbox; AddStep("focus first textbox", () => { From dbd4397bef2419ea4d9b1b8fdc44068358d59159 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 2 Feb 2024 13:32:16 +0100 Subject: [PATCH 0214/2493] Attempt to salvage test by using until step --- .../Visual/Navigation/TestSceneBeatmapEditorNavigation.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneBeatmapEditorNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneBeatmapEditorNavigation.cs index 370c40222e..e3a8e575f8 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneBeatmapEditorNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneBeatmapEditorNavigation.cs @@ -79,7 +79,7 @@ namespace osu.Game.Tests.Visual.Navigation AddStep("exit", () => Game.ChildrenOfType().Single().Exit()); - AddAssert("save dialog displayed", () => Game.ChildrenOfType().Single().CurrentDialog is PromptForSaveDialog); + AddUntilStep("save dialog displayed", () => Game.ChildrenOfType().SingleOrDefault()?.CurrentDialog is PromptForSaveDialog); } [Test] From 8ccb14f19f1363c42eaf80718ed50c1a68338185 Mon Sep 17 00:00:00 2001 From: tsunyoku Date: Tue, 6 Feb 2024 13:08:17 +0000 Subject: [PATCH 0215/2493] include slider count in accuracy pp if slider head accuracy is in use --- osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index b31f4ff519..7f4c5f6c46 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -192,6 +192,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty // This percentage only considers HitCircles of any value - in this part of the calculation we focus on hitting the timing hit window. double betterAccuracyPercentage; int amountHitObjectsWithAccuracy = attributes.HitCircleCount; + if (!score.Mods.Any(h => h is OsuModClassic cl && cl.NoSliderHeadAccuracy.Value)) + amountHitObjectsWithAccuracy += attributes.SliderCount; if (amountHitObjectsWithAccuracy > 0) betterAccuracyPercentage = ((countGreat - (totalHits - amountHitObjectsWithAccuracy)) * 6 + countOk * 2 + countMeh) / (double)(amountHitObjectsWithAccuracy * 6); From 5850d6a57807aee271aae79532964bc85d345e1a Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 6 Feb 2024 20:06:51 +0900 Subject: [PATCH 0216/2493] Show near-misses on the results-screen heatmap --- osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs | 7 +- .../Objects/Drawables/DrawableHitCircle.cs | 74 +++++++++++-------- .../Statistics/AccuracyHeatmap.cs | 60 +++++++++------ 3 files changed, 81 insertions(+), 60 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs b/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs index 10d7af5e58..0e3f972d41 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs @@ -95,12 +95,7 @@ namespace osu.Game.Rulesets.Osu.Mods /// private static void blockInputToObjectsUnderSliderHead(DrawableSliderHead slider) { - var oldHitAction = slider.HitArea.Hit; - slider.HitArea.Hit = () => - { - oldHitAction?.Invoke(); - return !slider.DrawableSlider.AllJudged; - }; + slider.HitArea.CanBeHit = () => !slider.DrawableSlider.AllJudged; } private void applyEarlyFading(DrawableHitCircle circle) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs index b1c9bef6c4..3727e78d01 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs @@ -10,7 +10,6 @@ using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Input; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Game.Graphics.Containers; @@ -43,7 +42,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables Drawable IHasApproachCircle.ApproachCircle => ApproachCircle; private Container scaleContainer; - private InputManager inputManager; public DrawableHitCircle() : this(null) @@ -73,14 +71,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { HitArea = new HitReceptor { - Hit = () => - { - if (AllJudged) - return false; - - UpdateResult(true); - return true; - }, + CanBeHit = () => !AllJudged, + Hit = () => UpdateResult(true) }, shakeContainer = new ShakeContainer { @@ -114,13 +106,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables ScaleBindable.BindValueChanged(scale => scaleContainer.Scale = new Vector2(scale.NewValue)); } - protected override void LoadComplete() - { - base.LoadComplete(); - - inputManager = GetContainingInputManager(); - } - public override double LifetimeStart { get => base.LifetimeStart; @@ -155,7 +140,15 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables if (!userTriggered) { if (!HitObject.HitWindows.CanBeHit(timeOffset)) - ApplyMinResult(); + { + ApplyResult((r, position) => + { + var circleResult = (OsuHitCircleJudgementResult)r; + + circleResult.Type = r.Judgement.MinResult; + circleResult.CursorPositionAtHit = position; + }, computeHitPosition()); + } return; } @@ -169,22 +162,21 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables if (result == HitResult.None || clickAction != ClickAction.Hit) return; - Vector2? hitPosition = null; - - // Todo: This should also consider misses, but they're a little more interesting to handle, since we don't necessarily know the position at the time of a miss. - if (result.IsHit()) - { - var localMousePosition = ToLocalSpace(inputManager.CurrentState.Mouse.Position); - hitPosition = HitObject.StackedPosition + (localMousePosition - DrawSize / 2); - } - ApplyResult<(HitResult result, Vector2? position)>((r, state) => { var circleResult = (OsuHitCircleJudgementResult)r; circleResult.Type = state.result; circleResult.CursorPositionAtHit = state.position; - }, (result, hitPosition)); + }, (result, computeHitPosition())); + } + + private Vector2? computeHitPosition() + { + if (HitArea.ClosestPressPosition is Vector2 screenSpaceHitPosition) + return HitObject.StackedPosition + (ToLocalSpace(screenSpaceHitPosition) - DrawSize / 2); + + return null; } /// @@ -227,6 +219,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables break; case ArmedState.Idle: + HitArea.ClosestPressPosition = null; HitArea.HitAction = null; break; @@ -247,9 +240,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables // IsHovered is used public override bool HandlePositionalInput => true; - public Func Hit; + public Func CanBeHit; + public Action Hit; public OsuAction? HitAction; + public Vector2? ClosestPressPosition; public HitReceptor() { @@ -264,12 +259,31 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables public bool OnPressed(KeyBindingPressEvent e) { + if (!(CanBeHit?.Invoke() ?? false)) + return false; + switch (e.Action) { case OsuAction.LeftButton: case OsuAction.RightButton: - if (IsHovered && (Hit?.Invoke() ?? false)) + // Only update closest press position while the object hasn't been hit yet. + if (HitAction == null) { + if (ClosestPressPosition is Vector2 curClosest) + { + float oldDist = Vector2.DistanceSquared(curClosest, ScreenSpaceDrawQuad.Centre); + float newDist = Vector2.DistanceSquared(e.ScreenSpaceMousePosition, ScreenSpaceDrawQuad.Centre); + + if (newDist < oldDist) + ClosestPressPosition = e.ScreenSpaceMousePosition; + } + else + ClosestPressPosition = e.ScreenSpaceMousePosition; + } + + if (IsHovered) + { + Hit?.Invoke(); HitAction ??= e.Action; return true; } diff --git a/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs b/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs index f9d4a3b325..813f3b2e7a 100644 --- a/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs +++ b/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs @@ -197,7 +197,9 @@ namespace osu.Game.Rulesets.Osu.Statistics var point = new HitPoint(pointType, this) { - BaseColour = pointType == HitPointType.Hit ? new Color4(102, 255, 204, 255) : new Color4(255, 102, 102, 255) + BaseColour = pointType == HitPointType.Hit + ? new Color4(102, 255, 204, 255) + : new Color4(255, 102, 102, 255) }; points[r][c] = point; @@ -250,12 +252,15 @@ namespace osu.Game.Rulesets.Osu.Statistics var rotatedCoordinate = -1 * new Vector2((float)Math.Cos(rotatedAngle), (float)Math.Sin(rotatedAngle)); Vector2 localCentre = new Vector2(points_per_dimension - 1) / 2; - float localRadius = localCentre.X * inner_portion * normalisedDistance; // The radius inside the inner portion which of the heatmap which the closest point lies. + float localRadius = localCentre.X * inner_portion * normalisedDistance; Vector2 localPoint = localCentre + localRadius * rotatedCoordinate; // Find the most relevant hit point. - int r = Math.Clamp((int)Math.Round(localPoint.Y), 0, points_per_dimension - 1); - int c = Math.Clamp((int)Math.Round(localPoint.X), 0, points_per_dimension - 1); + int r = (int)Math.Round(localPoint.Y); + int c = (int)Math.Round(localPoint.X); + + if (r < 0 || r >= points_per_dimension || c < 0 || c >= points_per_dimension) + return; PeakValue = Math.Max(PeakValue, ((HitPoint)pointGrid.Content[r][c]).Increment()); @@ -298,28 +303,35 @@ namespace osu.Game.Rulesets.Osu.Statistics { base.Update(); - // the point at which alpha is saturated and we begin to adjust colour lightness. - const float lighten_cutoff = 0.95f; - - // the amount of lightness to attribute regardless of relative value to peak point. - const float non_relative_portion = 0.2f; - - float amount = 0; - - // give some amount of alpha regardless of relative count - amount += non_relative_portion * Math.Min(1, count / 10f); - - // add relative portion - amount += (1 - non_relative_portion) * (count / heatmap.PeakValue); - - // apply easing - amount = (float)Interpolation.ApplyEasing(Easing.OutQuint, Math.Min(1, amount)); - - Debug.Assert(amount <= 1); - - Alpha = Math.Min(amount / lighten_cutoff, 1); if (pointType == HitPointType.Hit) + { + // the point at which alpha is saturated and we begin to adjust colour lightness. + const float lighten_cutoff = 0.95f; + + // the amount of lightness to attribute regardless of relative value to peak point. + const float non_relative_portion = 0.2f; + + float amount = 0; + + // give some amount of alpha regardless of relative count + amount += non_relative_portion * Math.Min(1, count / 10f); + + // add relative portion + amount += (1 - non_relative_portion) * (count / heatmap.PeakValue); + + // apply easing + amount = (float)Interpolation.ApplyEasing(Easing.OutQuint, Math.Min(1, amount)); + + Debug.Assert(amount <= 1); + + Alpha = Math.Min(amount / lighten_cutoff, 1); Colour = BaseColour.Lighten(Math.Max(0, amount - lighten_cutoff)); + } + else + { + Alpha = 0.8f; + Colour = BaseColour; + } } } From 99d716f8fc62aaaf09ca65fa7da6f8f876bd6b8a Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 6 Feb 2024 22:23:30 +0900 Subject: [PATCH 0217/2493] Make miss points into crosses --- .../Statistics/AccuracyHeatmap.cs | 123 ++++++++++-------- 1 file changed, 68 insertions(+), 55 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs b/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs index 813f3b2e7a..2120b929a7 100644 --- a/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs +++ b/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs @@ -9,6 +9,7 @@ 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.Utils; using osu.Game.Beatmaps; using osu.Game.Graphics; @@ -191,18 +192,22 @@ namespace osu.Game.Rulesets.Osu.Statistics for (int c = 0; c < points_per_dimension; c++) { - HitPointType pointType = Vector2.Distance(new Vector2(c + 0.5f, r + 0.5f), centre) <= innerRadius - ? HitPointType.Hit - : HitPointType.Miss; + bool isHit = Vector2.Distance(new Vector2(c + 0.5f, r + 0.5f), centre) <= innerRadius; - var point = new HitPoint(pointType, this) + if (isHit) { - BaseColour = pointType == HitPointType.Hit - ? new Color4(102, 255, 204, 255) - : new Color4(255, 102, 102, 255) - }; - - points[r][c] = point; + points[r][c] = new HitPoint(this) + { + BaseColour = new Color4(102, 255, 204, 255) + }; + } + else + { + points[r][c] = new MissPoint + { + BaseColour = new Color4(255, 102, 102, 255) + }; + } } } @@ -262,33 +267,21 @@ namespace osu.Game.Rulesets.Osu.Statistics if (r < 0 || r >= points_per_dimension || c < 0 || c >= points_per_dimension) return; - PeakValue = Math.Max(PeakValue, ((HitPoint)pointGrid.Content[r][c]).Increment()); + PeakValue = Math.Max(PeakValue, ((GridPoint)pointGrid.Content[r][c]).Increment()); bufferedGrid.ForceRedraw(); } - private partial class HitPoint : Circle + private abstract partial class GridPoint : CompositeDrawable { /// /// The base colour which will be lightened/darkened depending on the value of this . /// public Color4 BaseColour; - private readonly HitPointType pointType; - private readonly AccuracyHeatmap heatmap; + public override bool IsPresent => Count > 0; - public override bool IsPresent => count > 0; - - public HitPoint(HitPointType pointType, AccuracyHeatmap heatmap) - { - this.pointType = pointType; - this.heatmap = heatmap; - - RelativeSizeAxes = Axes.Both; - Alpha = 1; - } - - private int count; + protected int Count { get; private set; } /// /// Increment the value of this point by one. @@ -296,49 +289,69 @@ namespace osu.Game.Rulesets.Osu.Statistics /// The value after incrementing. public int Increment() { - return ++count; + return ++Count; + } + } + + private partial class MissPoint : GridPoint + { + public MissPoint() + { + RelativeSizeAxes = Axes.Both; + + InternalChild = new SpriteIcon + { + RelativeSizeAxes = Axes.Both, + Icon = FontAwesome.Solid.Times + }; + } + + protected override void Update() + { + Alpha = 0.8f; + Colour = BaseColour; + } + } + + private partial class HitPoint : GridPoint + { + private readonly AccuracyHeatmap heatmap; + + public HitPoint(AccuracyHeatmap heatmap) + { + this.heatmap = heatmap; + + RelativeSizeAxes = Axes.Both; + + InternalChild = new Circle { RelativeSizeAxes = Axes.Both }; } protected override void Update() { base.Update(); - if (pointType == HitPointType.Hit) - { - // the point at which alpha is saturated and we begin to adjust colour lightness. - const float lighten_cutoff = 0.95f; + // the point at which alpha is saturated and we begin to adjust colour lightness. + const float lighten_cutoff = 0.95f; - // the amount of lightness to attribute regardless of relative value to peak point. - const float non_relative_portion = 0.2f; + // the amount of lightness to attribute regardless of relative value to peak point. + const float non_relative_portion = 0.2f; - float amount = 0; + float amount = 0; - // give some amount of alpha regardless of relative count - amount += non_relative_portion * Math.Min(1, count / 10f); + // give some amount of alpha regardless of relative count + amount += non_relative_portion * Math.Min(1, Count / 10f); - // add relative portion - amount += (1 - non_relative_portion) * (count / heatmap.PeakValue); + // add relative portion + amount += (1 - non_relative_portion) * (Count / heatmap.PeakValue); - // apply easing - amount = (float)Interpolation.ApplyEasing(Easing.OutQuint, Math.Min(1, amount)); + // apply easing + amount = (float)Interpolation.ApplyEasing(Easing.OutQuint, Math.Min(1, amount)); - Debug.Assert(amount <= 1); + Debug.Assert(amount <= 1); - Alpha = Math.Min(amount / lighten_cutoff, 1); - Colour = BaseColour.Lighten(Math.Max(0, amount - lighten_cutoff)); - } - else - { - Alpha = 0.8f; - Colour = BaseColour; - } + Alpha = Math.Min(amount / lighten_cutoff, 1); + Colour = BaseColour.Lighten(Math.Max(0, amount - lighten_cutoff)); } } - - private enum HitPointType - { - Hit, - Miss - } } } From 44b1515cc5395486963615d238f2f77c16b5888c Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 7 Feb 2024 03:28:40 +0900 Subject: [PATCH 0218/2493] Remove LangVersion redefintions --- osu.Game.Rulesets.Osu/osu.Game.Rulesets.Osu.csproj | 1 - osu.Game/osu.Game.csproj | 1 - 2 files changed, 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/osu.Game.Rulesets.Osu.csproj b/osu.Game.Rulesets.Osu/osu.Game.Rulesets.Osu.csproj index 518ab362ca..7817d55f57 100644 --- a/osu.Game.Rulesets.Osu/osu.Game.Rulesets.Osu.csproj +++ b/osu.Game.Rulesets.Osu/osu.Game.Rulesets.Osu.csproj @@ -4,7 +4,6 @@ Library true click the circles. to the beat. - 10 diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 935b759e4d..9bcdebc347 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -3,7 +3,6 @@ net8.0 Library true - 10 osu! From 9b8f2064867472009e3c5c621c1fbf5823ad7592 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 7 Feb 2024 03:38:07 +0900 Subject: [PATCH 0219/2493] Enable NRT for DrawableHitCircle to clean up --- .../Objects/Drawables/DrawableHitCircle.cs | 46 +++++++++---------- 1 file changed, 22 insertions(+), 24 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs index 3727e78d01..b950ef4bbb 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs @@ -1,12 +1,9 @@ // 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.Collections.Generic; using System.Diagnostics; -using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -27,34 +24,30 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { public partial class DrawableHitCircle : DrawableOsuHitObject, IHasApproachCircle { - public OsuAction? HitAction => HitArea?.HitAction; + public OsuAction? HitAction => HitArea.HitAction; protected virtual OsuSkinComponents CirclePieceComponent => OsuSkinComponents.HitCircle; - public SkinnableDrawable ApproachCircle { get; private set; } - public HitReceptor HitArea { get; private set; } - public SkinnableDrawable CirclePiece { get; private set; } + public SkinnableDrawable ApproachCircle { get; private set; } = null!; + public HitReceptor HitArea { get; private set; } = null!; + public SkinnableDrawable CirclePiece { get; private set; } = null!; - protected override IEnumerable DimmablePieces => new[] - { - CirclePiece, - }; + protected override IEnumerable DimmablePieces => new[] { CirclePiece }; Drawable IHasApproachCircle.ApproachCircle => ApproachCircle; - private Container scaleContainer; + private Container scaleContainer = null!; + private ShakeContainer shakeContainer = null!; public DrawableHitCircle() : this(null) { } - public DrawableHitCircle([CanBeNull] HitCircle h = null) + public DrawableHitCircle(HitCircle? h = null) : base(h) { } - private ShakeContainer shakeContainer; - [BackgroundDependencyLoader] private void load() { @@ -219,8 +212,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables break; case ArmedState.Idle: - HitArea.ClosestPressPosition = null; - HitArea.HitAction = null; + HitArea.Reset(); break; case ArmedState.Miss: @@ -240,11 +232,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables // IsHovered is used public override bool HandlePositionalInput => true; - public Func CanBeHit; - public Action Hit; + public required Func CanBeHit { get; set; } + public required Action Hit { get; set; } - public OsuAction? HitAction; - public Vector2? ClosestPressPosition; + public OsuAction? HitAction { get; private set; } + public Vector2? ClosestPressPosition { get; private set; } public HitReceptor() { @@ -259,7 +251,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables public bool OnPressed(KeyBindingPressEvent e) { - if (!(CanBeHit?.Invoke() ?? false)) + if (CanBeHit()) return false; switch (e.Action) @@ -283,7 +275,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables if (IsHovered) { - Hit?.Invoke(); + Hit(); HitAction ??= e.Action; return true; } @@ -297,13 +289,19 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables public void OnReleased(KeyBindingReleaseEvent e) { } + + public void Reset() + { + HitAction = null; + ClosestPressPosition = null; + } } private partial class ProxyableSkinnableDrawable : SkinnableDrawable { public override bool RemoveWhenNotAlive => false; - public ProxyableSkinnableDrawable(ISkinComponentLookup lookup, Func defaultImplementation = null, ConfineMode confineMode = ConfineMode.NoScaling) + public ProxyableSkinnableDrawable(ISkinComponentLookup lookup, Func? defaultImplementation = null, ConfineMode confineMode = ConfineMode.NoScaling) : base(lookup, defaultImplementation, confineMode) { } From 38f7913b31bbc64f99882668b7d8cb89de4d51fd Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 7 Feb 2024 03:50:56 +0900 Subject: [PATCH 0220/2493] Fix inverted condition --- osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs index b950ef4bbb..c9f2983b1e 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs @@ -251,7 +251,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables public bool OnPressed(KeyBindingPressEvent e) { - if (CanBeHit()) + if (!CanBeHit()) return false; switch (e.Action) From 2fc06f16b50c0112522b3c2f164de6aa2aa36608 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 7 Feb 2024 03:51:28 +0900 Subject: [PATCH 0221/2493] Fix inspections --- osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs index c624fbbe73..9d79cb0db4 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs @@ -91,11 +91,11 @@ namespace osu.Game.Rulesets.Osu.Tests var skinnable = firstObject.ApproachCircle; - if (skin == null && skinnable?.Drawable is DefaultApproachCircle) + if (skin == null && skinnable.Drawable is DefaultApproachCircle) // check for default skin provider return true; - var text = skinnable?.Drawable as SpriteText; + var text = skinnable.Drawable as SpriteText; return text?.Text == skin; }); From e2867986c591f042b63c6da36e1f27d40cb89341 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 7 Feb 2024 03:47:36 +0900 Subject: [PATCH 0222/2493] Add xmldocs --- .../Objects/Drawables/DrawableHitCircle.cs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs index c9f2983b1e..f7237d4c03 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs @@ -232,10 +232,24 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables // IsHovered is used public override bool HandlePositionalInput => true; + /// + /// Whether the hitobject can still be hit at the current point in time. + /// public required Func CanBeHit { get; set; } + + /// + /// An action that's invoked to perform the hit. + /// public required Action Hit { get; set; } + /// + /// The with which the hit was attempted. + /// public OsuAction? HitAction { get; private set; } + + /// + /// The closest position to the hit receptor at the point where the hit was attempted. + /// public Vector2? ClosestPressPosition { get; private set; } public HitReceptor() @@ -290,6 +304,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { } + /// + /// Resets to a fresh state. + /// public void Reset() { HitAction = null; From 1f13124b3834b846281c5d63acc65275c356258e Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 7 Feb 2024 03:52:51 +0900 Subject: [PATCH 0223/2493] Always process position as long as it's hittable For example... If a hitobject is pressed but the result is a shake, this will stop processing hits. --- .../Objects/Drawables/DrawableHitCircle.cs | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs index f7237d4c03..c3ce6acce9 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs @@ -272,20 +272,16 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { case OsuAction.LeftButton: case OsuAction.RightButton: - // Only update closest press position while the object hasn't been hit yet. - if (HitAction == null) + if (ClosestPressPosition is Vector2 curClosest) { - if (ClosestPressPosition is Vector2 curClosest) - { - float oldDist = Vector2.DistanceSquared(curClosest, ScreenSpaceDrawQuad.Centre); - float newDist = Vector2.DistanceSquared(e.ScreenSpaceMousePosition, ScreenSpaceDrawQuad.Centre); + float oldDist = Vector2.DistanceSquared(curClosest, ScreenSpaceDrawQuad.Centre); + float newDist = Vector2.DistanceSquared(e.ScreenSpaceMousePosition, ScreenSpaceDrawQuad.Centre); - if (newDist < oldDist) - ClosestPressPosition = e.ScreenSpaceMousePosition; - } - else + if (newDist < oldDist) ClosestPressPosition = e.ScreenSpaceMousePosition; } + else + ClosestPressPosition = e.ScreenSpaceMousePosition; if (IsHovered) { From dcb195f3c813c72d2bd694d1488e0c37c67f1764 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 8 Feb 2024 02:16:08 +0900 Subject: [PATCH 0224/2493] Add delayed resume for taiko/catch/mania --- .../UI/DrawableCatchRuleset.cs | 3 ++ .../UI/DrawableManiaRuleset.cs | 3 ++ .../UI/DrawableTaikoRuleset.cs | 3 ++ osu.Game/Screens/Play/DelayedResumeOverlay.cs | 32 +++++++++++++++++++ 4 files changed, 41 insertions(+) create mode 100644 osu.Game/Screens/Play/DelayedResumeOverlay.cs diff --git a/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs b/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs index f0a327d7ac..580c90bcb4 100644 --- a/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs +++ b/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs @@ -16,6 +16,7 @@ using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Scoring; +using osu.Game.Screens.Play; namespace osu.Game.Rulesets.Catch.UI { @@ -52,5 +53,7 @@ namespace osu.Game.Rulesets.Catch.UI protected override PassThroughInputManager CreateInputManager() => new CatchInputManager(Ruleset.RulesetInfo); public override DrawableHitObject? CreateDrawableRepresentation(CatchHitObject h) => null; + + protected override ResumeOverlay CreateResumeOverlay() => new DelayedResumeOverlay(); } } diff --git a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs index decf670c5d..275b1311de 100644 --- a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs @@ -26,6 +26,7 @@ using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Scoring; +using osu.Game.Screens.Play; using osu.Game.Skinning; namespace osu.Game.Rulesets.Mania.UI @@ -164,6 +165,8 @@ namespace osu.Game.Rulesets.Mania.UI protected override ReplayRecorder CreateReplayRecorder(Score score) => new ManiaReplayRecorder(score); + protected override ResumeOverlay CreateResumeOverlay() => new DelayedResumeOverlay(); + protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); diff --git a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs index 77b2b06c0e..cd9ed399e6 100644 --- a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs @@ -22,6 +22,7 @@ using osu.Game.Rulesets.Timing; using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Scoring; +using osu.Game.Screens.Play; using osu.Game.Skinning; using osuTK; @@ -101,5 +102,7 @@ namespace osu.Game.Rulesets.Taiko.UI protected override ReplayInputHandler CreateReplayInputHandler(Replay replay) => new TaikoFramedReplayInputHandler(replay); protected override ReplayRecorder CreateReplayRecorder(Score score) => new TaikoReplayRecorder(score); + + protected override ResumeOverlay CreateResumeOverlay() => new DelayedResumeOverlay(); } } diff --git a/osu.Game/Screens/Play/DelayedResumeOverlay.cs b/osu.Game/Screens/Play/DelayedResumeOverlay.cs new file mode 100644 index 0000000000..ef39c8eb76 --- /dev/null +++ b/osu.Game/Screens/Play/DelayedResumeOverlay.cs @@ -0,0 +1,32 @@ +// 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; +using osu.Framework.Threading; + +namespace osu.Game.Screens.Play +{ + /// + /// Simple that resumes after 800ms. + /// + public partial class DelayedResumeOverlay : ResumeOverlay + { + protected override LocalisableString Message => string.Empty; + + private ScheduledDelegate? scheduledResume; + + protected override void PopIn() + { + base.PopIn(); + + scheduledResume?.Cancel(); + scheduledResume = Scheduler.AddDelayed(Resume, 800); + } + + protected override void PopOut() + { + base.PopOut(); + scheduledResume?.Cancel(); + } + } +} From 288eed53df439c594ffab544d3b83d9b2ea9d343 Mon Sep 17 00:00:00 2001 From: Sheepposu Date: Sat, 10 Feb 2024 02:02:26 -0500 Subject: [PATCH 0225/2493] new features + improvements Hit & aim markers are skinnable. Hidden can be toggled off. Aim line with skinnable color was added. The fadeout time is based on the approach rate. Cursor hide fixed. --- osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs | 77 ++++++- osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs | 12 +- osu.Game.Rulesets.Osu/OsuSkinComponents.cs | 3 + .../Skinning/Default/DefaultHitMarker.cs | 71 +++++++ .../Skinning/Default/HitMarker.cs | 33 +++ .../Legacy/OsuLegacySkinTransformer.cs | 19 ++ .../Skinning/OsuSkinColour.cs | 1 + .../UI/HitMarkerContainer.cs | 188 ++++++++++++------ .../UI/OsuAnalysisSettings.cs | 68 +++++-- osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs | 2 +- .../PlayerSettingsOverlayStrings.cs | 5 + .../Rulesets/Mods/IToggleableVisibility.cs | 11 + 12 files changed, 393 insertions(+), 97 deletions(-) create mode 100644 osu.Game.Rulesets.Osu/Skinning/Default/DefaultHitMarker.cs create mode 100644 osu.Game.Rulesets.Osu/Skinning/Default/HitMarker.cs create mode 100644 osu.Game/Rulesets/Mods/IToggleableVisibility.cs diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs b/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs index 6dc0d5d522..a69bed6fb2 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs @@ -2,9 +2,11 @@ // 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.Graphics; +using osu.Framework.Graphics.Transforms; using osu.Framework.Bindables; using osu.Framework.Localisation; using osu.Game.Configuration; @@ -12,13 +14,14 @@ using osu.Game.Beatmaps; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.UI; using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Skinning; namespace osu.Game.Rulesets.Osu.Mods { - public class OsuModHidden : ModHidden, IHidesApproachCircles + public class OsuModHidden : ModHidden, IHidesApproachCircles, IToggleableVisibility { [SettingSource("Only fade approach circles", "The main object body will not fade when enabled.")] public Bindable OnlyFadeApproachCircles { get; } = new BindableBool(); @@ -28,24 +31,41 @@ namespace osu.Game.Rulesets.Osu.Mods public override Type[] IncompatibleMods => new[] { typeof(IRequiresApproachCircles), typeof(OsuModSpinIn), typeof(OsuModDepth) }; + private bool toggledOff = false; + private IBeatmap? appliedBeatmap; + public const double FADE_IN_DURATION_MULTIPLIER = 0.4; public const double FADE_OUT_DURATION_MULTIPLIER = 0.3; protected override bool IsFirstAdjustableObject(HitObject hitObject) => !(hitObject is Spinner || hitObject is SpinnerTick); - public override void ApplyToBeatmap(IBeatmap beatmap) + public override void ApplyToBeatmap(IBeatmap? beatmap = null) { + if (beatmap is not null) + appliedBeatmap = beatmap; + else if (appliedBeatmap is null) + return; + else + beatmap = appliedBeatmap; + base.ApplyToBeatmap(beatmap); foreach (var obj in beatmap.HitObjects.OfType()) applyFadeInAdjustment(obj); + } - static void applyFadeInAdjustment(OsuHitObject osuObject) - { - osuObject.TimeFadeIn = osuObject.TimePreempt * FADE_IN_DURATION_MULTIPLIER; - foreach (var nested in osuObject.NestedHitObjects.OfType()) - applyFadeInAdjustment(nested); - } + private static void applyFadeInAdjustment(OsuHitObject osuObject) + { + osuObject.TimeFadeIn = osuObject.TimePreempt * FADE_IN_DURATION_MULTIPLIER; + foreach (var nested in osuObject.NestedHitObjects.OfType()) + applyFadeInAdjustment(nested); + } + + private static void revertFadeInAdjustment(OsuHitObject osuObject) + { + osuObject.TimeFadeIn = OsuHitObject.CalculateTimeFadeIn(osuObject.TimePreempt); + foreach (var nested in osuObject.NestedHitObjects.OfType()) + revertFadeInAdjustment(nested); } protected override void ApplyIncreasedVisibilityState(DrawableHitObject hitObject, ArmedState state) @@ -60,7 +80,7 @@ namespace osu.Game.Rulesets.Osu.Mods private void applyHiddenState(DrawableHitObject drawableObject, bool increaseVisibility) { - if (!(drawableObject is DrawableOsuHitObject drawableOsuObject)) + if (!(drawableObject is DrawableOsuHitObject drawableOsuObject) || toggledOff) return; OsuHitObject hitObject = drawableOsuObject.HitObject; @@ -183,8 +203,11 @@ namespace osu.Game.Rulesets.Osu.Mods } } - private static void hideSpinnerApproachCircle(DrawableSpinner spinner) + private void hideSpinnerApproachCircle(DrawableSpinner spinner) { + if (toggledOff) + return; + var approachCircle = (spinner.Body.Drawable as IHasApproachCircle)?.ApproachCircle; if (approachCircle == null) return; @@ -192,5 +215,39 @@ namespace osu.Game.Rulesets.Osu.Mods using (spinner.BeginAbsoluteSequence(spinner.HitObject.StartTime - spinner.HitObject.TimePreempt)) approachCircle.Hide(); } + + public void ToggleOffVisibility(Playfield playfield) + { + if (toggledOff) + return; + + toggledOff = true; + + if (appliedBeatmap is not null) + foreach (var obj in appliedBeatmap.HitObjects.OfType()) + revertFadeInAdjustment(obj); + + foreach (var dho in playfield.AllHitObjects) + { + dho.RefreshStateTransforms(); + } + } + + public void ToggleOnVisibility(Playfield playfield) + { + if (!toggledOff) + return; + + toggledOff = false; + + if (appliedBeatmap is not null) + ApplyToBeatmap(); + + foreach (var dho in playfield.AllHitObjects) + { + dho.RefreshStateTransforms(); + ApplyToDrawableHitObject(dho); + } + } } } diff --git a/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs b/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs index 74631400ca..60305ed953 100644 --- a/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs +++ b/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs @@ -154,17 +154,19 @@ namespace osu.Game.Rulesets.Osu.Objects }); } + // Preempt time can go below 450ms. Normally, this is achieved via the DT mod which uniformly speeds up all animations game wide regardless of AR. + // This uniform speedup is hard to match 1:1, however we can at least make AR>10 (via mods) feel good by extending the upper linear function above. + // Note that this doesn't exactly match the AR>10 visuals as they're classically known, but it feels good. + // This adjustment is necessary for AR>10, otherwise TimePreempt can become smaller leading to hitcircles not fully fading in. + static public double CalculateTimeFadeIn(double timePreempt) => 400 * Math.Min(1, timePreempt / PREEMPT_MIN); + protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty) { base.ApplyDefaultsToSelf(controlPointInfo, difficulty); TimePreempt = (float)IBeatmapDifficultyInfo.DifficultyRange(difficulty.ApproachRate, PREEMPT_MAX, PREEMPT_MID, PREEMPT_MIN); - // Preempt time can go below 450ms. Normally, this is achieved via the DT mod which uniformly speeds up all animations game wide regardless of AR. - // This uniform speedup is hard to match 1:1, however we can at least make AR>10 (via mods) feel good by extending the upper linear function above. - // Note that this doesn't exactly match the AR>10 visuals as they're classically known, but it feels good. - // This adjustment is necessary for AR>10, otherwise TimePreempt can become smaller leading to hitcircles not fully fading in. - TimeFadeIn = 400 * Math.Min(1, TimePreempt / PREEMPT_MIN); + TimeFadeIn = CalculateTimeFadeIn(TimePreempt); Scale = LegacyRulesetExtensions.CalculateScaleFromCircleSize(difficulty.CircleSize, true); } diff --git a/osu.Game.Rulesets.Osu/OsuSkinComponents.cs b/osu.Game.Rulesets.Osu/OsuSkinComponents.cs index 52fdfea95f..75db18d345 100644 --- a/osu.Game.Rulesets.Osu/OsuSkinComponents.cs +++ b/osu.Game.Rulesets.Osu/OsuSkinComponents.cs @@ -22,5 +22,8 @@ namespace osu.Game.Rulesets.Osu SpinnerBody, CursorSmoke, ApproachCircle, + HitMarkerLeft, + HitMarkerRight, + AimMarker } } diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/DefaultHitMarker.cs b/osu.Game.Rulesets.Osu/Skinning/Default/DefaultHitMarker.cs new file mode 100644 index 0000000000..f4c11e6c8a --- /dev/null +++ b/osu.Game.Rulesets.Osu/Skinning/Default/DefaultHitMarker.cs @@ -0,0 +1,71 @@ +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osuTK; + +namespace osu.Game.Rulesets.Osu.Skinning.Default +{ + public partial class DefaultHitMarker : CompositeDrawable + { + public DefaultHitMarker(OsuAction? action) + { + var (colour, length, hasBorder) = getConfig(action); + + if (hasBorder) + { + InternalChildren = new Drawable[] + { + new Box + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(3, length), + Rotation = 45, + Colour = Colour4.Black.Opacity(0.5F) + }, + new Box + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(3, length), + Rotation = 135, + Colour = Colour4.Black.Opacity(0.5F) + } + }; + } + + AddRangeInternal(new Drawable[] + { + new Box + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(1, length), + Rotation = 45, + Colour = colour + }, + new Box + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(1, length), + Rotation = 135, + Colour = colour + } + }); + } + + private (Colour4 colour, float length, bool hasBorder) getConfig(OsuAction? action) + { + switch (action) + { + case OsuAction.LeftButton: + return (Colour4.Orange, 20, true); + case OsuAction.RightButton: + return (Colour4.LightGreen, 20, true); + default: + return (Colour4.Gray.Opacity(0.3F), 8, false); + } + } + } +} \ No newline at end of file diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/HitMarker.cs b/osu.Game.Rulesets.Osu/Skinning/Default/HitMarker.cs new file mode 100644 index 0000000000..d86e3d4f79 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Skinning/Default/HitMarker.cs @@ -0,0 +1,33 @@ +using osu.Framework.Allocation; +using osu.Framework.Graphics.Sprites; +using osu.Game.Skinning; + +namespace osu.Game.Rulesets.Osu.Skinning.Default +{ + public partial class HitMarker : Sprite + { + private readonly OsuAction? action; + + public HitMarker(OsuAction? action = null) + { + this.action = action; + } + + [BackgroundDependencyLoader] + private void load(ISkinSource skin) + { + switch (action) + { + case OsuAction.LeftButton: + Texture = skin.GetTexture(@"hitmarker-left"); + break; + case OsuAction.RightButton: + Texture = skin.GetTexture(@"hitmarker-right"); + break; + default: + Texture = skin.GetTexture(@"aimmarker"); + break; + } + } + } +} \ No newline at end of file diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs index d2ebc68c52..84c055fe5e 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs @@ -5,6 +5,7 @@ using System; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Osu.Skinning.Default; using osu.Game.Skinning; using osuTK; @@ -168,6 +169,24 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy return null; + case OsuSkinComponents.HitMarkerLeft: + if (GetTexture(@"hitmarker-left") != null) + return new HitMarker(OsuAction.LeftButton); + + return null; + + case OsuSkinComponents.HitMarkerRight: + if (GetTexture(@"hitmarker-right") != null) + return new HitMarker(OsuAction.RightButton); + + return null; + + case OsuSkinComponents.AimMarker: + if (GetTexture(@"aimmarker") != null) + return new HitMarker(); + + return null; + default: throw new UnsupportedSkinComponentException(lookup); } diff --git a/osu.Game.Rulesets.Osu/Skinning/OsuSkinColour.cs b/osu.Game.Rulesets.Osu/Skinning/OsuSkinColour.cs index 24f9217a5f..5c864fb6c2 100644 --- a/osu.Game.Rulesets.Osu/Skinning/OsuSkinColour.cs +++ b/osu.Game.Rulesets.Osu/Skinning/OsuSkinColour.cs @@ -10,5 +10,6 @@ namespace osu.Game.Rulesets.Osu.Skinning SliderBall, SpinnerBackground, StarBreakAdditive, + ReplayAimLine, } } diff --git a/osu.Game.Rulesets.Osu/UI/HitMarkerContainer.cs b/osu.Game.Rulesets.Osu/UI/HitMarkerContainer.cs index a9fc42e596..01877a9185 100644 --- a/osu.Game.Rulesets.Osu/UI/HitMarkerContainer.cs +++ b/osu.Game.Rulesets.Osu/UI/HitMarkerContainer.cs @@ -10,111 +10,128 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Input; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Osu.Objects.Drawables; +using osu.Game.Rulesets.Osu.Skinning; +using osu.Game.Rulesets.Osu.Skinning.Default; +using osu.Game.Rulesets.UI; +using osu.Game.Skinning; using osuTK; +using osuTK.Graphics; namespace osu.Game.Rulesets.Osu.UI { public partial class HitMarkerContainer : Container, IRequireHighFrequencyMousePosition, IKeyBindingHandler { private Vector2 lastMousePosition; + private Vector2? lastlastMousePosition; + private double? timePreempt; + private double TimePreempt + { + get => timePreempt ?? default_time_preempt; + set => timePreempt = value; + } public Bindable HitMarkerEnabled = new BindableBool(); public Bindable AimMarkersEnabled = new BindableBool(); + public Bindable AimLinesEnabled = new BindableBool(); + + private const double default_time_preempt = 1000; + + private readonly HitObjectContainer hitObjectContainer; public override bool ReceivePositionalInputAt(Vector2 _) => true; + public HitMarkerContainer(HitObjectContainer hitObjectContainer) + { + this.hitObjectContainer = hitObjectContainer; + } + public bool OnPressed(KeyBindingPressEvent e) { if (HitMarkerEnabled.Value && (e.Action == OsuAction.LeftButton || e.Action == OsuAction.RightButton)) { + updateTimePreempt(); AddMarker(e.Action); } return false; } - public void OnReleased(KeyBindingReleaseEvent e) { } + public void OnReleased(KeyBindingReleaseEvent e) + { + } protected override bool OnMouseMove(MouseMoveEvent e) { + lastlastMousePosition = lastMousePosition; lastMousePosition = e.MousePosition; if (AimMarkersEnabled.Value) { + updateTimePreempt(); AddMarker(null); } + if (AimLinesEnabled.Value && lastlastMousePosition != null && lastlastMousePosition != lastMousePosition) + { + if (!AimMarkersEnabled.Value) + updateTimePreempt(); + Add(new AimLineDrawable((Vector2)lastlastMousePosition, lastMousePosition, TimePreempt)); + } + return base.OnMouseMove(e); } private void AddMarker(OsuAction? action) { - Add(new HitMarkerDrawable(action) { Position = lastMousePosition }); + var component = OsuSkinComponents.AimMarker; + switch(action) + { + case OsuAction.LeftButton: + component = OsuSkinComponents.HitMarkerLeft; + break; + case OsuAction.RightButton: + component = OsuSkinComponents.HitMarkerRight; + break; + } + + Add(new HitMarkerDrawable(action, component, TimePreempt) + { + Position = lastMousePosition, + Origin = Anchor.Centre, + Depth = action == null ? float.MaxValue : float.MinValue + }); } - private partial class HitMarkerDrawable : CompositeDrawable + private void updateTimePreempt() { - private const double lifetime_duration = 1000; - private const double fade_out_time = 400; + var hitObject = getHitObject(); + if (hitObject == null) + return; + + TimePreempt = hitObject.TimePreempt; + } + + private OsuHitObject? getHitObject() + { + foreach (var dho in hitObjectContainer.Objects) + return (dho as DrawableOsuHitObject)?.HitObject; + return null; + } + + private partial class HitMarkerDrawable : SkinnableDrawable + { + private readonly double lifetimeDuration; + private readonly double fadeOutTime; public override bool RemoveWhenNotAlive => true; - public HitMarkerDrawable(OsuAction? action) + public HitMarkerDrawable(OsuAction? action, OsuSkinComponents componenet, double timePreempt) + : base(new OsuSkinComponentLookup(componenet), _ => new DefaultHitMarker(action)) { - var colour = Colour4.Gray.Opacity(0.5F); - var length = 8; - var depth = float.MaxValue; - switch (action) - { - case OsuAction.LeftButton: - colour = Colour4.Orange; - length = 20; - depth = float.MinValue; - break; - case OsuAction.RightButton: - colour = Colour4.LightGreen; - length = 20; - depth = float.MinValue; - break; - } - - this.Depth = depth; - - InternalChildren = new Drawable[] - { - new Box - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Size = new Vector2(3, length), - Rotation = 45, - Colour = Colour4.Black.Opacity(0.5F) - }, - new Box - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Size = new Vector2(3, length), - Rotation = 135, - Colour = Colour4.Black.Opacity(0.5F) - }, - new Box - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Size = new Vector2(1, length), - Rotation = 45, - Colour = colour - }, - new Box - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Size = new Vector2(1, length), - Rotation = 135, - Colour = colour - } - }; + fadeOutTime = timePreempt / 2; + lifetimeDuration = timePreempt + fadeOutTime; } protected override void LoadComplete() @@ -122,12 +139,57 @@ namespace osu.Game.Rulesets.Osu.UI base.LoadComplete(); LifetimeStart = Time.Current; - LifetimeEnd = LifetimeStart + lifetime_duration; + LifetimeEnd = LifetimeStart + lifetimeDuration; Scheduler.AddDelayed(() => { - this.FadeOut(fade_out_time); - }, lifetime_duration - fade_out_time); + this.FadeOut(fadeOutTime); + }, lifetimeDuration - fadeOutTime); + } + } + + private partial class AimLineDrawable : CompositeDrawable + { + private readonly double lifetimeDuration; + private readonly double fadeOutTime; + + public override bool RemoveWhenNotAlive => true; + + public AimLineDrawable(Vector2 fromP, Vector2 toP, double timePreempt) + { + fadeOutTime = timePreempt / 2; + lifetimeDuration = timePreempt + fadeOutTime; + + float distance = Vector2.Distance(fromP, toP); + Vector2 direction = (toP - fromP); + InternalChild = new Box + { + Position = fromP + (direction / 2), + Size = new Vector2(distance, 1), + Rotation = (float)(Math.Atan(direction.Y / direction.X) * (180 / Math.PI)), + Origin = Anchor.Centre + }; + } + + [BackgroundDependencyLoader] + private void load(ISkinSource skin) + { + var color = skin.GetConfig(OsuSkinColour.ReplayAimLine)?.Value ?? Color4.White; + color.A = 127; + InternalChild.Colour = color; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + LifetimeStart = Time.Current; + LifetimeEnd = LifetimeStart + lifetimeDuration; + + Scheduler.AddDelayed(() => + { + this.FadeOut(fadeOutTime); + }, lifetimeDuration - fadeOutTime); } } } diff --git a/osu.Game.Rulesets.Osu/UI/OsuAnalysisSettings.cs b/osu.Game.Rulesets.Osu/UI/OsuAnalysisSettings.cs index 4694c1a560..050675d970 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuAnalysisSettings.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuAnalysisSettings.cs @@ -4,7 +4,9 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; -using osu.Framework.Logging; +using osu.Framework.Graphics.Containers; +using osu.Framework.Input.Events; +using osu.Game.Graphics.Containers; using osu.Game.Localisation; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.Mods; @@ -15,14 +17,13 @@ namespace osu.Game.Rulesets.Osu.UI { public partial class OsuAnalysisSettings : AnalysisSettings { - private static readonly Logger logger = Logger.GetLogger("osu-analysis-settings"); - protected new DrawableOsuRuleset drawableRuleset => (DrawableOsuRuleset)base.drawableRuleset; private readonly PlayerCheckbox hitMarkerToggle; private readonly PlayerCheckbox aimMarkerToggle; private readonly PlayerCheckbox hideCursorToggle; - private readonly PlayerCheckbox? hiddenToggle; + private readonly PlayerCheckbox aimLinesToggle; + private readonly FillFlowContainer modTogglesContainer; public OsuAnalysisSettings(DrawableRuleset drawableRuleset) : base(drawableRuleset) @@ -31,18 +32,36 @@ namespace osu.Game.Rulesets.Osu.UI { hitMarkerToggle = new PlayerCheckbox { LabelText = PlayerSettingsOverlayStrings.HitMarkers }, aimMarkerToggle = new PlayerCheckbox { LabelText = PlayerSettingsOverlayStrings.AimMarkers }, - hideCursorToggle = new PlayerCheckbox { LabelText = PlayerSettingsOverlayStrings.HideCursor } + aimLinesToggle = new PlayerCheckbox { LabelText = PlayerSettingsOverlayStrings.AimLines }, + hideCursorToggle = new PlayerCheckbox { LabelText = PlayerSettingsOverlayStrings.HideCursor }, + new OsuScrollContainer(Direction.Horizontal) + { + RelativeSizeAxes = Axes.X, + Height = ModSwitchSmall.DEFAULT_SIZE, + ScrollbarOverlapsContent = false, + Child = modTogglesContainer = new FillFlowContainer + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Direction = FillDirection.Horizontal, + RelativeSizeAxes = Axes.Y, + AutoSizeAxes = Axes.X + } + } }; - - // hidden stuff is just here for testing at the moment; to create the mod disabling functionality foreach (var mod in drawableRuleset.Mods) { - if (mod is OsuModHidden) + if (mod is IToggleableVisibility toggleableMod) { - logger.Add("Hidden is enabled", LogLevel.Debug); - Add(hiddenToggle = new PlayerCheckbox { LabelText = "Disable hidden" }); - break; + var modSwitch = new SelectableModSwitchSmall(mod) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Active = { Value = true } + }; + modSwitch.Active.BindValueChanged((v) => onModToggle(toggleableMod, v)); + modTogglesContainer.Add(modSwitch); } } } @@ -51,8 +70,8 @@ namespace osu.Game.Rulesets.Osu.UI { drawableRuleset.Playfield.MarkersContainer.HitMarkerEnabled.BindTo(hitMarkerToggle.Current); drawableRuleset.Playfield.MarkersContainer.AimMarkersEnabled.BindTo(aimMarkerToggle.Current); + drawableRuleset.Playfield.MarkersContainer.AimLinesEnabled.BindTo(aimLinesToggle.Current); hideCursorToggle.Current.BindValueChanged(onCursorToggle); - hiddenToggle?.Current.BindValueChanged(onHiddenToggle); } private void onCursorToggle(ValueChangedEvent hide) @@ -60,21 +79,34 @@ namespace osu.Game.Rulesets.Osu.UI // this only hides half the cursor if (hide.NewValue) { - drawableRuleset.Playfield.Cursor.Hide(); + drawableRuleset.Playfield.Cursor.FadeOut(); } else { - drawableRuleset.Playfield.Cursor.Show(); + drawableRuleset.Playfield.Cursor.FadeIn(); } } - private void onHiddenToggle(ValueChangedEvent off) + private void onModToggle(IToggleableVisibility mod, ValueChangedEvent toggled) { - if (off.NewValue) + if (toggled.NewValue) { - logger.Add("Hidden off", LogLevel.Debug); + mod.ToggleOnVisibility(drawableRuleset.Playfield); } else { - logger.Add("Hidden on", LogLevel.Debug); + mod.ToggleOffVisibility(drawableRuleset.Playfield); + } + } + + private partial class SelectableModSwitchSmall : ModSwitchSmall + { + public SelectableModSwitchSmall(IMod mod) + : base(mod) + {} + + protected override bool OnClick(ClickEvent e) + { + Active.Value = !Active.Value; + return true; } } } diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs index d7e1732175..3cb3c50ef7 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs @@ -62,7 +62,7 @@ namespace osu.Game.Rulesets.Osu.UI HitObjectContainer, judgementAboveHitObjectLayer = new Container { RelativeSizeAxes = Axes.Both }, approachCircles = new ProxyContainer { RelativeSizeAxes = Axes.Both }, - MarkersContainer = new HitMarkerContainer { RelativeSizeAxes = Axes.Both } + MarkersContainer = new HitMarkerContainer(HitObjectContainer) { RelativeSizeAxes = Axes.Both } }; HitPolicy = new StartTimeOrderedHitPolicy(); diff --git a/osu.Game/Localisation/PlayerSettingsOverlayStrings.cs b/osu.Game/Localisation/PlayerSettingsOverlayStrings.cs index f829fb4ac1..017cc9bf82 100644 --- a/osu.Game/Localisation/PlayerSettingsOverlayStrings.cs +++ b/osu.Game/Localisation/PlayerSettingsOverlayStrings.cs @@ -34,6 +34,11 @@ namespace osu.Game.Localisation /// public static LocalisableString HideCursor => new TranslatableString(getKey(@"hide_cursor"), @"Hide cursor"); + /// + /// "Aim lines" + /// + public static LocalisableString AimLines => new TranslatableString(getKey(@"aim_lines"), @"Aim lines"); + /// /// "Seek backward {0} seconds" /// diff --git a/osu.Game/Rulesets/Mods/IToggleableVisibility.cs b/osu.Game/Rulesets/Mods/IToggleableVisibility.cs new file mode 100644 index 0000000000..5d90c24b9e --- /dev/null +++ b/osu.Game/Rulesets/Mods/IToggleableVisibility.cs @@ -0,0 +1,11 @@ +using osu.Game.Rulesets.UI; + +namespace osu.Game.Rulesets.Mods +{ + public interface IToggleableVisibility + { + public void ToggleOffVisibility(Playfield playfield); + + public void ToggleOnVisibility(Playfield playfield); + } +} \ No newline at end of file From 6402f23f0245ecac17f82f1a9e5f06b9bd7898b2 Mon Sep 17 00:00:00 2001 From: Givikap120 Date: Mon, 12 Feb 2024 21:00:15 +0200 Subject: [PATCH 0226/2493] Added Traceable support for pp --- .../Difficulty/OsuPerformanceCalculator.cs | 12 ++++++++++++ osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs | 1 + 2 files changed, 13 insertions(+) diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index b31f4ff519..4771bce280 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -116,6 +116,11 @@ namespace osu.Game.Rulesets.Osu.Difficulty // We want to give more reward for lower AR when it comes to aim and HD. This nerfs high AR and buffs lower AR. aimValue *= 1.0 + 0.04 * (12.0 - attributes.ApproachRate); } + else if (score.Mods.Any(h => h is OsuModTraceable)) + { + // Default 2% increase and another is scaled by AR + aimValue *= 1.02 + 0.02 * (12.0 - attributes.ApproachRate); + } // We assume 15% of sliders in a map are difficult since there's no way to tell from the performance calculator. double estimateDifficultSliders = attributes.SliderCount * 0.15; @@ -167,6 +172,11 @@ namespace osu.Game.Rulesets.Osu.Difficulty // We want to give more reward for lower AR when it comes to aim and HD. This nerfs high AR and buffs lower AR. speedValue *= 1.0 + 0.04 * (12.0 - attributes.ApproachRate); } + else if (score.Mods.Any(h => h is OsuModTraceable)) + { + // More reward for speed because speed on Traceable is annoying + speedValue *= 1.04 + 0.06 * (12.0 - attributes.ApproachRate); + } // Calculate accuracy assuming the worst case scenario double relevantTotalDiff = totalHits - attributes.SpeedNoteCount; @@ -214,6 +224,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty accuracyValue *= 1.14; else if (score.Mods.Any(m => m is OsuModHidden)) accuracyValue *= 1.08; + else if (score.Mods.Any(m => m is OsuModTraceable)) + accuracyValue *= 1.02 + 0.01 * (12.0 - attributes.ApproachRate); if (score.Mods.Any(m => m is OsuModFlashlight)) accuracyValue *= 1.02; diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs index 9671f53bea..320c0a7040 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs @@ -19,6 +19,7 @@ namespace osu.Game.Rulesets.Osu.Mods public override ModType Type => ModType.Fun; public override LocalisableString Description => "Put your faith in the approach circles..."; public override double ScoreMultiplier => 1; + public override bool Ranked => UsesDefaultConfiguration; public override Type[] IncompatibleMods => new[] { typeof(IHidesApproachCircles), typeof(OsuModDepth) }; From 56391550096a19486e06d98a20e1f5bb1421c090 Mon Sep 17 00:00:00 2001 From: Givikap120 Date: Mon, 12 Feb 2024 23:31:00 +0200 Subject: [PATCH 0227/2493] Update OsuModTraceable.cs --- osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs index 320c0a7040..9671f53bea 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs @@ -19,7 +19,6 @@ namespace osu.Game.Rulesets.Osu.Mods public override ModType Type => ModType.Fun; public override LocalisableString Description => "Put your faith in the approach circles..."; public override double ScoreMultiplier => 1; - public override bool Ranked => UsesDefaultConfiguration; public override Type[] IncompatibleMods => new[] { typeof(IHidesApproachCircles), typeof(OsuModDepth) }; From 1d552e7c90b8d3f7350fecaa43f5759b0154eaa7 Mon Sep 17 00:00:00 2001 From: Sheepposu Date: Tue, 20 Feb 2024 22:28:25 -0500 Subject: [PATCH 0228/2493] move skin component logic --- .../Default/OsuTrianglesSkinTransformer.cs | 22 +++++++++++++++++++ .../Legacy/OsuLegacySkinTransformer.cs | 22 ------------------- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/OsuTrianglesSkinTransformer.cs b/osu.Game.Rulesets.Osu/Skinning/Default/OsuTrianglesSkinTransformer.cs index 7a4c768aa2..69ef1d9dc0 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Default/OsuTrianglesSkinTransformer.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Default/OsuTrianglesSkinTransformer.cs @@ -29,6 +29,28 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default return new DefaultJudgementPieceSliderTickMiss(result); } + break; + case OsuSkinComponentLookup osuComponent: + switch (osuComponent.Component) + { + case OsuSkinComponents.HitMarkerLeft: + if (GetTexture(@"hitmarker-left") != null) + return new HitMarker(OsuAction.LeftButton); + + return null; + + case OsuSkinComponents.HitMarkerRight: + if (GetTexture(@"hitmarker-right") != null) + return new HitMarker(OsuAction.RightButton); + + return null; + + case OsuSkinComponents.AimMarker: + if (GetTexture(@"aimmarker") != null) + return new HitMarker(); + + return null; + } break; } diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs index 84c055fe5e..a931d3ecbf 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs @@ -5,7 +5,6 @@ using System; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Rulesets.Osu.Objects; -using osu.Game.Rulesets.Osu.Skinning.Default; using osu.Game.Skinning; using osuTK; @@ -168,27 +167,6 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy return new LegacyApproachCircle(); return null; - - case OsuSkinComponents.HitMarkerLeft: - if (GetTexture(@"hitmarker-left") != null) - return new HitMarker(OsuAction.LeftButton); - - return null; - - case OsuSkinComponents.HitMarkerRight: - if (GetTexture(@"hitmarker-right") != null) - return new HitMarker(OsuAction.RightButton); - - return null; - - case OsuSkinComponents.AimMarker: - if (GetTexture(@"aimmarker") != null) - return new HitMarker(); - - return null; - - default: - throw new UnsupportedSkinComponentException(lookup); } } From 7d34542c12a8e1db707d101fc7988e32e9da74d8 Mon Sep 17 00:00:00 2001 From: TextAdventurer12 Date: Thu, 22 Feb 2024 15:14:56 +1300 Subject: [PATCH 0229/2493] use difficulty instead of topstrain --- osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs index 918e702b45..13d89cf2b8 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs @@ -37,6 +37,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills protected virtual double DifficultyMultiplier => DEFAULT_DIFFICULTY_MULTIPLIER; protected List objectStrains = new List(); + protected double difficulty; protected OsuStrainSkill(Mod[] mods) : base(mods) @@ -45,7 +46,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills public override double DifficultyValue() { - double difficulty = 0; + difficulty = 0; double weight = 1; // Sections with 0 strain are excluded to avoid worst-case time complexity of the following sort (e.g. /b/2351871). @@ -78,9 +79,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills /// public double CountDifficultStrains() { - double topStrain = objectStrains.Max(); + double consistentTopStrain = difficulty / 10; // What would the top strain be if all strain values were identical - return objectStrains.Sum(s => Math.Pow(s / topStrain, 4)); + return objectStrains.Sum(s => Math.Pow(1, s / consistentTopStrain, 5)); } } } From 0db910deb90b792f41203ee9410ef6dc05231308 Mon Sep 17 00:00:00 2001 From: TextAdventurer12 Date: Thu, 22 Feb 2024 15:20:32 +1300 Subject: [PATCH 0230/2493] cap each note at adding 1 difficult strain count --- osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs index 13d89cf2b8..84bf8e3bf6 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs @@ -81,7 +81,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills { double consistentTopStrain = difficulty / 10; // What would the top strain be if all strain values were identical - return objectStrains.Sum(s => Math.Pow(1, s / consistentTopStrain, 5)); + return objectStrains.Sum(s => Math.Pow(Math.Min(1, s / consistentTopStrain), 5)); } } } From 35b89966bccc2455034c7aed973cf2fefbf87ca7 Mon Sep 17 00:00:00 2001 From: Sheepposu Date: Wed, 21 Feb 2024 23:23:40 -0500 Subject: [PATCH 0231/2493] revert mod visibility toggle --- osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs | 79 +++---------------- osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs | 12 ++- .../UI/OsuAnalysisSettings.cs | 57 +------------ .../Rulesets/Mods/IToggleableVisibility.cs | 11 --- 4 files changed, 17 insertions(+), 142 deletions(-) delete mode 100644 osu.Game/Rulesets/Mods/IToggleableVisibility.cs diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs b/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs index a69bed6fb2..e45daed919 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs @@ -2,11 +2,9 @@ // 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.Graphics; -using osu.Framework.Graphics.Transforms; using osu.Framework.Bindables; using osu.Framework.Localisation; using osu.Game.Configuration; @@ -14,14 +12,13 @@ using osu.Game.Beatmaps; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; -using osu.Game.Rulesets.UI; using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Skinning; namespace osu.Game.Rulesets.Osu.Mods { - public class OsuModHidden : ModHidden, IHidesApproachCircles, IToggleableVisibility + public class OsuModHidden : ModHidden, IHidesApproachCircles { [SettingSource("Only fade approach circles", "The main object body will not fade when enabled.")] public Bindable OnlyFadeApproachCircles { get; } = new BindableBool(); @@ -31,41 +28,24 @@ namespace osu.Game.Rulesets.Osu.Mods public override Type[] IncompatibleMods => new[] { typeof(IRequiresApproachCircles), typeof(OsuModSpinIn), typeof(OsuModDepth) }; - private bool toggledOff = false; - private IBeatmap? appliedBeatmap; - public const double FADE_IN_DURATION_MULTIPLIER = 0.4; public const double FADE_OUT_DURATION_MULTIPLIER = 0.3; protected override bool IsFirstAdjustableObject(HitObject hitObject) => !(hitObject is Spinner || hitObject is SpinnerTick); - public override void ApplyToBeatmap(IBeatmap? beatmap = null) + public override void ApplyToBeatmap(IBeatmap beatmap) { - if (beatmap is not null) - appliedBeatmap = beatmap; - else if (appliedBeatmap is null) - return; - else - beatmap = appliedBeatmap; - base.ApplyToBeatmap(beatmap); foreach (var obj in beatmap.HitObjects.OfType()) applyFadeInAdjustment(obj); - } - private static void applyFadeInAdjustment(OsuHitObject osuObject) - { - osuObject.TimeFadeIn = osuObject.TimePreempt * FADE_IN_DURATION_MULTIPLIER; - foreach (var nested in osuObject.NestedHitObjects.OfType()) - applyFadeInAdjustment(nested); - } - - private static void revertFadeInAdjustment(OsuHitObject osuObject) - { - osuObject.TimeFadeIn = OsuHitObject.CalculateTimeFadeIn(osuObject.TimePreempt); - foreach (var nested in osuObject.NestedHitObjects.OfType()) - revertFadeInAdjustment(nested); + static void applyFadeInAdjustment(OsuHitObject osuObject) + { + osuObject.TimeFadeIn = osuObject.TimePreempt * FADE_IN_DURATION_MULTIPLIER; + foreach (var nested in osuObject.NestedHitObjects.OfType()) + applyFadeInAdjustment(nested); + } } protected override void ApplyIncreasedVisibilityState(DrawableHitObject hitObject, ArmedState state) @@ -80,7 +60,7 @@ namespace osu.Game.Rulesets.Osu.Mods private void applyHiddenState(DrawableHitObject drawableObject, bool increaseVisibility) { - if (!(drawableObject is DrawableOsuHitObject drawableOsuObject) || toggledOff) + if (!(drawableObject is DrawableOsuHitObject drawableOsuObject)) return; OsuHitObject hitObject = drawableOsuObject.HitObject; @@ -203,11 +183,8 @@ namespace osu.Game.Rulesets.Osu.Mods } } - private void hideSpinnerApproachCircle(DrawableSpinner spinner) + private static void hideSpinnerApproachCircle(DrawableSpinner spinner) { - if (toggledOff) - return; - var approachCircle = (spinner.Body.Drawable as IHasApproachCircle)?.ApproachCircle; if (approachCircle == null) return; @@ -215,39 +192,5 @@ namespace osu.Game.Rulesets.Osu.Mods using (spinner.BeginAbsoluteSequence(spinner.HitObject.StartTime - spinner.HitObject.TimePreempt)) approachCircle.Hide(); } - - public void ToggleOffVisibility(Playfield playfield) - { - if (toggledOff) - return; - - toggledOff = true; - - if (appliedBeatmap is not null) - foreach (var obj in appliedBeatmap.HitObjects.OfType()) - revertFadeInAdjustment(obj); - - foreach (var dho in playfield.AllHitObjects) - { - dho.RefreshStateTransforms(); - } - } - - public void ToggleOnVisibility(Playfield playfield) - { - if (!toggledOff) - return; - - toggledOff = false; - - if (appliedBeatmap is not null) - ApplyToBeatmap(); - - foreach (var dho in playfield.AllHitObjects) - { - dho.RefreshStateTransforms(); - ApplyToDrawableHitObject(dho); - } - } } -} +} \ No newline at end of file diff --git a/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs b/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs index 60305ed953..74631400ca 100644 --- a/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs +++ b/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs @@ -154,19 +154,17 @@ namespace osu.Game.Rulesets.Osu.Objects }); } - // Preempt time can go below 450ms. Normally, this is achieved via the DT mod which uniformly speeds up all animations game wide regardless of AR. - // This uniform speedup is hard to match 1:1, however we can at least make AR>10 (via mods) feel good by extending the upper linear function above. - // Note that this doesn't exactly match the AR>10 visuals as they're classically known, but it feels good. - // This adjustment is necessary for AR>10, otherwise TimePreempt can become smaller leading to hitcircles not fully fading in. - static public double CalculateTimeFadeIn(double timePreempt) => 400 * Math.Min(1, timePreempt / PREEMPT_MIN); - protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty) { base.ApplyDefaultsToSelf(controlPointInfo, difficulty); TimePreempt = (float)IBeatmapDifficultyInfo.DifficultyRange(difficulty.ApproachRate, PREEMPT_MAX, PREEMPT_MID, PREEMPT_MIN); - TimeFadeIn = CalculateTimeFadeIn(TimePreempt); + // Preempt time can go below 450ms. Normally, this is achieved via the DT mod which uniformly speeds up all animations game wide regardless of AR. + // This uniform speedup is hard to match 1:1, however we can at least make AR>10 (via mods) feel good by extending the upper linear function above. + // Note that this doesn't exactly match the AR>10 visuals as they're classically known, but it feels good. + // This adjustment is necessary for AR>10, otherwise TimePreempt can become smaller leading to hitcircles not fully fading in. + TimeFadeIn = 400 * Math.Min(1, TimePreempt / PREEMPT_MIN); Scale = LegacyRulesetExtensions.CalculateScaleFromCircleSize(difficulty.CircleSize, true); } diff --git a/osu.Game.Rulesets.Osu/UI/OsuAnalysisSettings.cs b/osu.Game.Rulesets.Osu/UI/OsuAnalysisSettings.cs index 050675d970..319ae3e1f5 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuAnalysisSettings.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuAnalysisSettings.cs @@ -8,7 +8,6 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Input.Events; using osu.Game.Graphics.Containers; using osu.Game.Localisation; -using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.UI; using osu.Game.Screens.Play.PlayerSettings; @@ -23,7 +22,6 @@ namespace osu.Game.Rulesets.Osu.UI private readonly PlayerCheckbox aimMarkerToggle; private readonly PlayerCheckbox hideCursorToggle; private readonly PlayerCheckbox aimLinesToggle; - private readonly FillFlowContainer modTogglesContainer; public OsuAnalysisSettings(DrawableRuleset drawableRuleset) : base(drawableRuleset) @@ -33,37 +31,8 @@ namespace osu.Game.Rulesets.Osu.UI hitMarkerToggle = new PlayerCheckbox { LabelText = PlayerSettingsOverlayStrings.HitMarkers }, aimMarkerToggle = new PlayerCheckbox { LabelText = PlayerSettingsOverlayStrings.AimMarkers }, aimLinesToggle = new PlayerCheckbox { LabelText = PlayerSettingsOverlayStrings.AimLines }, - hideCursorToggle = new PlayerCheckbox { LabelText = PlayerSettingsOverlayStrings.HideCursor }, - new OsuScrollContainer(Direction.Horizontal) - { - RelativeSizeAxes = Axes.X, - Height = ModSwitchSmall.DEFAULT_SIZE, - ScrollbarOverlapsContent = false, - Child = modTogglesContainer = new FillFlowContainer - { - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - Direction = FillDirection.Horizontal, - RelativeSizeAxes = Axes.Y, - AutoSizeAxes = Axes.X - } - } + hideCursorToggle = new PlayerCheckbox { LabelText = PlayerSettingsOverlayStrings.HideCursor } }; - - foreach (var mod in drawableRuleset.Mods) - { - if (mod is IToggleableVisibility toggleableMod) - { - var modSwitch = new SelectableModSwitchSmall(mod) - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Active = { Value = true } - }; - modSwitch.Active.BindValueChanged((v) => onModToggle(toggleableMod, v)); - modTogglesContainer.Add(modSwitch); - } - } } protected override void LoadComplete() @@ -85,29 +54,5 @@ namespace osu.Game.Rulesets.Osu.UI drawableRuleset.Playfield.Cursor.FadeIn(); } } - - private void onModToggle(IToggleableVisibility mod, ValueChangedEvent toggled) - { - if (toggled.NewValue) - { - mod.ToggleOnVisibility(drawableRuleset.Playfield); - } else - { - mod.ToggleOffVisibility(drawableRuleset.Playfield); - } - } - - private partial class SelectableModSwitchSmall : ModSwitchSmall - { - public SelectableModSwitchSmall(IMod mod) - : base(mod) - {} - - protected override bool OnClick(ClickEvent e) - { - Active.Value = !Active.Value; - return true; - } - } } } \ No newline at end of file diff --git a/osu.Game/Rulesets/Mods/IToggleableVisibility.cs b/osu.Game/Rulesets/Mods/IToggleableVisibility.cs deleted file mode 100644 index 5d90c24b9e..0000000000 --- a/osu.Game/Rulesets/Mods/IToggleableVisibility.cs +++ /dev/null @@ -1,11 +0,0 @@ -using osu.Game.Rulesets.UI; - -namespace osu.Game.Rulesets.Mods -{ - public interface IToggleableVisibility - { - public void ToggleOffVisibility(Playfield playfield); - - public void ToggleOnVisibility(Playfield playfield); - } -} \ No newline at end of file From f9d9df30b2104322920eb8326ad01ea946f87f06 Mon Sep 17 00:00:00 2001 From: Sheepposu Date: Thu, 22 Feb 2024 07:31:15 -0500 Subject: [PATCH 0232/2493] add test for HitMarkerContainer --- .../Resources/special-skin/aimmarker@2x.png | Bin 0 -> 648 bytes .../special-skin/hitmarker-left@2x.png | Bin 0 -> 2127 bytes .../special-skin/hitmarker-right@2x.png | Bin 0 -> 1742 bytes .../Resources/special-skin/skin.ini | 5 +- .../TestSceneHitMarker.cs | 134 ++++++++++++++++++ 5 files changed, 138 insertions(+), 1 deletion(-) create mode 100644 osu.Game.Rulesets.Osu.Tests/Resources/special-skin/aimmarker@2x.png create mode 100644 osu.Game.Rulesets.Osu.Tests/Resources/special-skin/hitmarker-left@2x.png create mode 100644 osu.Game.Rulesets.Osu.Tests/Resources/special-skin/hitmarker-right@2x.png create mode 100644 osu.Game.Rulesets.Osu.Tests/TestSceneHitMarker.cs diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/aimmarker@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/aimmarker@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..0b2a554193f08f7984568980956a3db8a6b39800 GIT binary patch literal 648 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 FV1jr`?veli literal 0 HcmV?d00001 diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/hitmarker-right@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/hitmarker-right@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..66be9a80ee2e2ae4c6dc6915d2b7ca5cf8ab589f GIT binary patch literal 1742 zcmV;<1~K`GP)EX>4Tx04R}tkv&MmKpe$iQ?)8p2Rn#3WT;NoK}8%(6^me@v=v%)FuC+YXws0R zxHt-~1qVMCs}3&Cx;nTDg5U>;vxAeOiUKFoAxFnR+6_CX>@2HM@dakSAh-}000D~Nkl1L9Yb|fH_ftE7RjjO?hFdLVhHsD6;Mi3NQZON`8V+<3BQA3EPF^Lyy zBFH5ssJYD~PVPNjt#956lhOc~_xg%spW#euk$w}6}R9gK=vhuEEC9o1$c~`Rf z(Ry2wlf>QYeKL21@Rg4>x`?ys*smS!IAyn7e8E7rV7}_2!Kl|YJi?#x1%84Syp!In zkY2Z>ZC|ASeH8V&R&~)}wqQQKV8CuUoJ!|IpZh27F_`+c=GVn>f!toVMTXssZ0rPq-2F z7)4*YhF6Y6=fW|AnK={TxGQO->3NOv?ZHqu?n-9PL^x&;=hC%on8f+eXCO0UEIMoi zC$QrUyo)<T*3ZV=R?f-~Sw)q8_7}ITNXzj(Ynz8XY#0nKO|Ffd3lJ+SQw? zo^T_5u}|1KI1!G!Qa#~D8k*bxlSMfdpVSn!s$M%F!q0GjixAm?KUED#k}2O6e=Ub! zzT9@~mdieLFWo14Y(4bY?{@Z~d#M9B__8N*HojnoWl8a{S^UD*#Oe5q57df^KXo6y zFS^)9_p=4_sqNTj>tdf)v)O`qjqaxoT%0!kCVZ*Rs)f?;&EU6Nn8-Z~eiR+BtjUAq zuj+95Y2#Stj`7sR9`G*v)Lv2)QZ=X0g)O!$}Y)kHjB1^&ZygL zaa;~xW2Fp;tl;b7xLnk27M%fmLZ@PB*b@vZ9}Jg6605gAXe zstYZ)p{)uZh6ZtROM+Y(5y>UDycZfo_&zMvtXfj*ar#}oX-JZ!IZ2XB>92W{iM*rM zPniAbKjTyQRE^sNDlJm64K~qIM5Tc?-B3Fj<2yzt(TD^gFHQ4B!vebI{%dk!P-X kiyU)`Hc@tO_2Ah*0YN2jCf7K0Z2$lO07*qoM6N<$f~iM7hyVZp literal 0 HcmV?d00001 diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/skin.ini b/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/skin.ini index 9d16267d73..2952948f45 100644 --- a/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/skin.ini +++ b/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/skin.ini @@ -1,4 +1,7 @@ [General] Version: latest HitCircleOverlayAboveNumber: 0 -HitCirclePrefix: display \ No newline at end of file +HitCirclePrefix: display + +[Colours] +ReplayAimLine: 0,0,255 \ No newline at end of file diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneHitMarker.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneHitMarker.cs new file mode 100644 index 0000000000..a0748e31af --- /dev/null +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneHitMarker.cs @@ -0,0 +1,134 @@ +// 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 NUnit.Framework; +using osu.Framework.Graphics; +using osu.Framework.Input.Events; +using osu.Framework.Input.States; +using osu.Framework.Logging; +using osu.Framework.Testing.Input; +using osu.Game.Rulesets.Osu.UI; +using osu.Game.Rulesets.UI; +using osuTK; + +namespace osu.Game.Rulesets.Osu.Tests +{ + public partial class TestSceneHitMarker : OsuSkinnableTestScene + { + [Test] + public void TestHitMarkers() + { + var markerContainers = new List(); + + AddStep("Create hit markers", () => + { + markerContainers.Clear(); + SetContents(_ => { + markerContainers.Add(new TestHitMarkerContainer(new HitObjectContainer()) + { + HitMarkerEnabled = { Value = true }, + AimMarkersEnabled = { Value = true }, + AimLinesEnabled = { Value = true }, + RelativeSizeAxes = Axes.Both + }); + + return new HitMarkerInputManager + { + RelativeSizeAxes = Axes.Both, + Size = new Vector2(0.95f), + Child = markerContainers[^1], + }; + }); + }); + + AddUntilStep("Until skinnable expires", () => + { + if (markerContainers.Count == 0) + return false; + + Logger.Log("How many: " + markerContainers.Count); + + foreach (var markerContainer in markerContainers) + { + if (markerContainer.Children.Count != 0) + return false; + } + + return true; + }); + } + + private partial class HitMarkerInputManager : ManualInputManager + { + private double? startTime; + + public HitMarkerInputManager() + { + UseParentInput = false; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + MoveMouseTo(ToScreenSpace(DrawSize / 2)); + } + + protected override void Update() + { + base.Update(); + + const float spin_angle = 4 * MathF.PI; + + startTime ??= Time.Current; + + float fraction = (float)((Time.Current - startTime) / 5_000); + + float angle = fraction * spin_angle; + float radius = fraction * Math.Min(DrawSize.X, DrawSize.Y) / 2; + + Vector2 pos = radius * new Vector2(MathF.Cos(angle), MathF.Sin(angle)) + DrawSize / 2; + MoveMouseTo(ToScreenSpace(pos)); + } + } + + private partial class TestHitMarkerContainer : HitMarkerContainer + { + private double? lastClick; + private double? startTime; + private bool finishedDrawing = false; + private bool leftOrRight = false; + + public TestHitMarkerContainer(HitObjectContainer hitObjectContainer) + : base(hitObjectContainer) + { + } + + protected override void Update() + { + base.Update(); + + if (finishedDrawing) + return; + + startTime ??= lastClick ??= Time.Current; + + if (startTime + 5_000 <= Time.Current) + { + finishedDrawing = true; + HitMarkerEnabled.Value = AimMarkersEnabled.Value = AimLinesEnabled.Value = false; + return; + } + + if (lastClick + 400 <= Time.Current) + { + OnPressed(new KeyBindingPressEvent(new InputState(), leftOrRight ? OsuAction.LeftButton : OsuAction.RightButton)); + leftOrRight = !leftOrRight; + lastClick = Time.Current; + } + } + } + } +} \ No newline at end of file From af13389a9e8fc58ef549cb8e9920375c22dc99ef Mon Sep 17 00:00:00 2001 From: Sheepposu Date: Thu, 22 Feb 2024 07:31:56 -0500 Subject: [PATCH 0233/2493] fix hit marker skinnables --- .../Default/OsuTrianglesSkinTransformer.cs | 22 ------------------- .../Legacy/OsuLegacySkinTransformer.cs | 19 ++++++++++++++++ 2 files changed, 19 insertions(+), 22 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/OsuTrianglesSkinTransformer.cs b/osu.Game.Rulesets.Osu/Skinning/Default/OsuTrianglesSkinTransformer.cs index 69ef1d9dc0..7a4c768aa2 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Default/OsuTrianglesSkinTransformer.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Default/OsuTrianglesSkinTransformer.cs @@ -29,28 +29,6 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default return new DefaultJudgementPieceSliderTickMiss(result); } - break; - case OsuSkinComponentLookup osuComponent: - switch (osuComponent.Component) - { - case OsuSkinComponents.HitMarkerLeft: - if (GetTexture(@"hitmarker-left") != null) - return new HitMarker(OsuAction.LeftButton); - - return null; - - case OsuSkinComponents.HitMarkerRight: - if (GetTexture(@"hitmarker-right") != null) - return new HitMarker(OsuAction.RightButton); - - return null; - - case OsuSkinComponents.AimMarker: - if (GetTexture(@"aimmarker") != null) - return new HitMarker(); - - return null; - } break; } diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs index a931d3ecbf..097f1732e2 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs @@ -6,6 +6,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Skinning; +using osu.Game.Rulesets.Osu.Skinning.Default; using osuTK; namespace osu.Game.Rulesets.Osu.Skinning.Legacy @@ -167,6 +168,24 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy return new LegacyApproachCircle(); return null; + + case OsuSkinComponents.HitMarkerLeft: + if (GetTexture(@"hitmarker-left") != null) + return new HitMarker(OsuAction.LeftButton); + + return null; + + case OsuSkinComponents.HitMarkerRight: + if (GetTexture(@"hitmarker-right") != null) + return new HitMarker(OsuAction.RightButton); + + return null; + + case OsuSkinComponents.AimMarker: + if (GetTexture(@"aimmarker") != null) + return new HitMarker(); + + return null; } } From 45444b39d461ca9c497cc7ffdac8402b98e26e03 Mon Sep 17 00:00:00 2001 From: Sheepposu Date: Thu, 22 Feb 2024 19:01:52 -0500 Subject: [PATCH 0234/2493] fix formatting issues --- .../TestSceneHitMarker.cs | 11 ++++--- osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs | 2 +- osu.Game.Rulesets.Osu/OsuRuleset.cs | 2 +- .../Skinning/Default/DefaultHitMarker.cs | 7 ++++- .../Skinning/Default/HitMarker.cs | 7 ++++- .../Legacy/OsuLegacySkinTransformer.cs | 4 +-- .../UI/HitMarkerContainer.cs | 31 +++++++++---------- .../UI/OsuAnalysisSettings.cs | 22 ++++++------- osu.Game/Rulesets/Ruleset.cs | 2 +- osu.Game/Screens/Play/Player.cs | 2 +- .../Play/PlayerSettings/AnalysisSettings.cs | 6 ++-- osu.Game/Screens/Play/ReplayPlayer.cs | 5 ++- 12 files changed, 53 insertions(+), 48 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneHitMarker.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneHitMarker.cs index a0748e31af..7ba681b50f 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneHitMarker.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneHitMarker.cs @@ -25,8 +25,9 @@ namespace osu.Game.Rulesets.Osu.Tests AddStep("Create hit markers", () => { markerContainers.Clear(); - SetContents(_ => { - markerContainers.Add(new TestHitMarkerContainer(new HitObjectContainer()) + SetContents(_ => + { + markerContainers.Add(new TestHitMarkerContainer(new HitObjectContainer()) { HitMarkerEnabled = { Value = true }, AimMarkersEnabled = { Value = true }, @@ -98,8 +99,8 @@ namespace osu.Game.Rulesets.Osu.Tests { private double? lastClick; private double? startTime; - private bool finishedDrawing = false; - private bool leftOrRight = false; + private bool finishedDrawing; + private bool leftOrRight; public TestHitMarkerContainer(HitObjectContainer hitObjectContainer) : base(hitObjectContainer) @@ -131,4 +132,4 @@ namespace osu.Game.Rulesets.Osu.Tests } } } -} \ No newline at end of file +} diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs b/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs index e45daed919..6dc0d5d522 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs @@ -193,4 +193,4 @@ namespace osu.Game.Rulesets.Osu.Mods approachCircle.Hide(); } } -} \ No newline at end of file +} diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index 358553ac59..224d08cbd5 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -358,6 +358,6 @@ namespace osu.Game.Rulesets.Osu return adjustedDifficulty; } - public override AnalysisSettings? CreateAnalysisSettings(DrawableRuleset drawableRuleset) => new OsuAnalysisSettings(drawableRuleset); + public override AnalysisSettings CreateAnalysisSettings(DrawableRuleset drawableRuleset) => new OsuAnalysisSettings(drawableRuleset); } } diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/DefaultHitMarker.cs b/osu.Game.Rulesets.Osu/Skinning/Default/DefaultHitMarker.cs index f4c11e6c8a..7dabb5182f 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Default/DefaultHitMarker.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Default/DefaultHitMarker.cs @@ -1,3 +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 osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; @@ -61,11 +64,13 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default { case OsuAction.LeftButton: return (Colour4.Orange, 20, true); + case OsuAction.RightButton: return (Colour4.LightGreen, 20, true); + default: return (Colour4.Gray.Opacity(0.3F), 8, false); } } } -} \ No newline at end of file +} diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/HitMarker.cs b/osu.Game.Rulesets.Osu/Skinning/Default/HitMarker.cs index d86e3d4f79..28877345d0 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Default/HitMarker.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Default/HitMarker.cs @@ -1,3 +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 osu.Framework.Allocation; using osu.Framework.Graphics.Sprites; using osu.Game.Skinning; @@ -21,13 +24,15 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default case OsuAction.LeftButton: Texture = skin.GetTexture(@"hitmarker-left"); break; + case OsuAction.RightButton: Texture = skin.GetTexture(@"hitmarker-right"); break; + default: Texture = skin.GetTexture(@"aimmarker"); break; } } } -} \ No newline at end of file +} diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs index 097f1732e2..61f9eebd86 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs @@ -168,7 +168,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy return new LegacyApproachCircle(); return null; - + case OsuSkinComponents.HitMarkerLeft: if (GetTexture(@"hitmarker-left") != null) return new HitMarker(OsuAction.LeftButton); @@ -184,7 +184,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy case OsuSkinComponents.AimMarker: if (GetTexture(@"aimmarker") != null) return new HitMarker(); - + return null; } } diff --git a/osu.Game.Rulesets.Osu/UI/HitMarkerContainer.cs b/osu.Game.Rulesets.Osu/UI/HitMarkerContainer.cs index 01877a9185..e8916ea545 100644 --- a/osu.Game.Rulesets.Osu/UI/HitMarkerContainer.cs +++ b/osu.Game.Rulesets.Osu/UI/HitMarkerContainer.cs @@ -25,12 +25,7 @@ namespace osu.Game.Rulesets.Osu.UI { private Vector2 lastMousePosition; private Vector2? lastlastMousePosition; - private double? timePreempt; - private double TimePreempt - { - get => timePreempt ?? default_time_preempt; - set => timePreempt = value; - } + private double timePreempt; public Bindable HitMarkerEnabled = new BindableBool(); public Bindable AimMarkersEnabled = new BindableBool(); @@ -45,6 +40,7 @@ namespace osu.Game.Rulesets.Osu.UI public HitMarkerContainer(HitObjectContainer hitObjectContainer) { this.hitObjectContainer = hitObjectContainer; + timePreempt = default_time_preempt; } public bool OnPressed(KeyBindingPressEvent e) @@ -52,7 +48,7 @@ namespace osu.Game.Rulesets.Osu.UI if (HitMarkerEnabled.Value && (e.Action == OsuAction.LeftButton || e.Action == OsuAction.RightButton)) { updateTimePreempt(); - AddMarker(e.Action); + addMarker(e.Action); } return false; @@ -70,33 +66,35 @@ namespace osu.Game.Rulesets.Osu.UI if (AimMarkersEnabled.Value) { updateTimePreempt(); - AddMarker(null); + addMarker(null); } if (AimLinesEnabled.Value && lastlastMousePosition != null && lastlastMousePosition != lastMousePosition) { if (!AimMarkersEnabled.Value) updateTimePreempt(); - Add(new AimLineDrawable((Vector2)lastlastMousePosition, lastMousePosition, TimePreempt)); + Add(new AimLineDrawable((Vector2)lastlastMousePosition, lastMousePosition, timePreempt)); } return base.OnMouseMove(e); } - private void AddMarker(OsuAction? action) + private void addMarker(OsuAction? action) { var component = OsuSkinComponents.AimMarker; - switch(action) + + switch (action) { case OsuAction.LeftButton: component = OsuSkinComponents.HitMarkerLeft; break; + case OsuAction.RightButton: component = OsuSkinComponents.HitMarkerRight; break; } - Add(new HitMarkerDrawable(action, component, TimePreempt) + Add(new HitMarkerDrawable(action, component, timePreempt) { Position = lastMousePosition, Origin = Anchor.Centre, @@ -110,13 +108,14 @@ namespace osu.Game.Rulesets.Osu.UI if (hitObject == null) return; - TimePreempt = hitObject.TimePreempt; + timePreempt = hitObject.TimePreempt; } private OsuHitObject? getHitObject() { foreach (var dho in hitObjectContainer.Objects) return (dho as DrawableOsuHitObject)?.HitObject; + return null; } @@ -141,7 +140,7 @@ namespace osu.Game.Rulesets.Osu.UI LifetimeStart = Time.Current; LifetimeEnd = LifetimeStart + lifetimeDuration; - Scheduler.AddDelayed(() => + Scheduler.AddDelayed(() => { this.FadeOut(fadeOutTime); }, lifetimeDuration - fadeOutTime); @@ -186,11 +185,11 @@ namespace osu.Game.Rulesets.Osu.UI LifetimeStart = Time.Current; LifetimeEnd = LifetimeStart + lifetimeDuration; - Scheduler.AddDelayed(() => + Scheduler.AddDelayed(() => { this.FadeOut(fadeOutTime); }, lifetimeDuration - fadeOutTime); } } } -} \ No newline at end of file +} diff --git a/osu.Game.Rulesets.Osu/UI/OsuAnalysisSettings.cs b/osu.Game.Rulesets.Osu/UI/OsuAnalysisSettings.cs index 319ae3e1f5..51fd835dc5 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuAnalysisSettings.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuAnalysisSettings.cs @@ -1,14 +1,9 @@ // 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.Bindables; using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Input.Events; -using osu.Game.Graphics.Containers; using osu.Game.Localisation; -using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.UI; using osu.Game.Screens.Play.PlayerSettings; @@ -16,7 +11,7 @@ namespace osu.Game.Rulesets.Osu.UI { public partial class OsuAnalysisSettings : AnalysisSettings { - protected new DrawableOsuRuleset drawableRuleset => (DrawableOsuRuleset)base.drawableRuleset; + protected new DrawableOsuRuleset DrawableRuleset => (DrawableOsuRuleset)base.DrawableRuleset; private readonly PlayerCheckbox hitMarkerToggle; private readonly PlayerCheckbox aimMarkerToggle; @@ -37,9 +32,9 @@ namespace osu.Game.Rulesets.Osu.UI protected override void LoadComplete() { - drawableRuleset.Playfield.MarkersContainer.HitMarkerEnabled.BindTo(hitMarkerToggle.Current); - drawableRuleset.Playfield.MarkersContainer.AimMarkersEnabled.BindTo(aimMarkerToggle.Current); - drawableRuleset.Playfield.MarkersContainer.AimLinesEnabled.BindTo(aimLinesToggle.Current); + DrawableRuleset.Playfield.MarkersContainer.HitMarkerEnabled.BindTo(hitMarkerToggle.Current); + DrawableRuleset.Playfield.MarkersContainer.AimMarkersEnabled.BindTo(aimMarkerToggle.Current); + DrawableRuleset.Playfield.MarkersContainer.AimLinesEnabled.BindTo(aimLinesToggle.Current); hideCursorToggle.Current.BindValueChanged(onCursorToggle); } @@ -48,11 +43,12 @@ namespace osu.Game.Rulesets.Osu.UI // this only hides half the cursor if (hide.NewValue) { - drawableRuleset.Playfield.Cursor.FadeOut(); - } else + DrawableRuleset.Playfield.Cursor.FadeOut(); + } + else { - drawableRuleset.Playfield.Cursor.FadeIn(); + DrawableRuleset.Playfield.Cursor.FadeIn(); } } } -} \ No newline at end of file +} diff --git a/osu.Game/Rulesets/Ruleset.cs b/osu.Game/Rulesets/Ruleset.cs index 5fbb23f094..84177f26b0 100644 --- a/osu.Game/Rulesets/Ruleset.cs +++ b/osu.Game/Rulesets/Ruleset.cs @@ -403,7 +403,7 @@ namespace osu.Game.Rulesets /// Can be overridden to alter the difficulty section to the editor beatmap setup screen. /// public virtual DifficultySection? CreateEditorDifficultySection() => null; - + public virtual AnalysisSettings? CreateAnalysisSettings(DrawableRuleset drawableRuleset) => null; } } diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 0fa7143693..ad1f9ec897 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -115,7 +115,7 @@ namespace osu.Game.Screens.Play public GameplayState GameplayState { get; private set; } - protected Ruleset ruleset; + private Ruleset ruleset; public BreakOverlay BreakOverlay; diff --git a/osu.Game/Screens/Play/PlayerSettings/AnalysisSettings.cs b/osu.Game/Screens/Play/PlayerSettings/AnalysisSettings.cs index 122ca29142..e30d2b2d42 100644 --- a/osu.Game/Screens/Play/PlayerSettings/AnalysisSettings.cs +++ b/osu.Game/Screens/Play/PlayerSettings/AnalysisSettings.cs @@ -7,12 +7,12 @@ namespace osu.Game.Screens.Play.PlayerSettings { public partial class AnalysisSettings : PlayerSettingsGroup { - protected DrawableRuleset drawableRuleset; + protected DrawableRuleset DrawableRuleset; public AnalysisSettings(DrawableRuleset drawableRuleset) : base("Analysis Settings") { - this.drawableRuleset = drawableRuleset; + DrawableRuleset = drawableRuleset; } } -} \ No newline at end of file +} diff --git a/osu.Game/Screens/Play/ReplayPlayer.cs b/osu.Game/Screens/Play/ReplayPlayer.cs index 0d877785e7..65e99731dc 100644 --- a/osu.Game/Screens/Play/ReplayPlayer.cs +++ b/osu.Game/Screens/Play/ReplayPlayer.cs @@ -72,10 +72,9 @@ namespace osu.Game.Screens.Play HUDOverlay.PlayerSettingsOverlay.AddAtStart(playbackSettings); - var analysisSettings = ruleset.CreateAnalysisSettings(DrawableRuleset); - if (analysisSettings != null) { + var analysisSettings = DrawableRuleset.Ruleset.CreateAnalysisSettings(DrawableRuleset); + if (analysisSettings != null) HUDOverlay.PlayerSettingsOverlay.AddAtStart(analysisSettings); - } } protected override void PrepareReplay() From 2a1fa8ccacd0ece5f6a098bd4951cefcd9a862c7 Mon Sep 17 00:00:00 2001 From: Sheepposu Date: Thu, 22 Feb 2024 22:34:38 -0500 Subject: [PATCH 0235/2493] fix OsuAnalysisSettings text not updating --- osu.Game.Rulesets.Osu/UI/OsuAnalysisSettings.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/UI/OsuAnalysisSettings.cs b/osu.Game.Rulesets.Osu/UI/OsuAnalysisSettings.cs index 51fd835dc5..0411882d2a 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuAnalysisSettings.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuAnalysisSettings.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.Bindables; using osu.Framework.Graphics; using osu.Game.Localisation; @@ -30,7 +31,8 @@ namespace osu.Game.Rulesets.Osu.UI }; } - protected override void LoadComplete() + [BackgroundDependencyLoader] + private void load() { DrawableRuleset.Playfield.MarkersContainer.HitMarkerEnabled.BindTo(hitMarkerToggle.Current); DrawableRuleset.Playfield.MarkersContainer.AimMarkersEnabled.BindTo(aimMarkerToggle.Current); From 0113fce02f5888dd775402ed4c15550826a5e999 Mon Sep 17 00:00:00 2001 From: Hivie Date: Fri, 23 Feb 2024 11:27:12 +0100 Subject: [PATCH 0236/2493] Add osu!taiko `Constant Speed` mod --- .../Mods/TaikoModConstantSpeed.cs | 31 +++++++++++++++++++ osu.Game.Rulesets.Taiko/TaikoRuleset.cs | 1 + 2 files changed, 32 insertions(+) create mode 100644 osu.Game.Rulesets.Taiko/Mods/TaikoModConstantSpeed.cs diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModConstantSpeed.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModConstantSpeed.cs new file mode 100644 index 0000000000..28de360eee --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModConstantSpeed.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.Localisation; +using osu.Framework.Graphics.Sprites; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Taiko.Beatmaps; + +namespace osu.Game.Rulesets.Taiko.Mods +{ + public class TaikoModConstantSpeed : Mod, IApplicableToBeatmap + { + public override string Name => "Constant Speed"; + public override string Acronym => "CS"; + public override double ScoreMultiplier => 0.8; + public override LocalisableString Description => "No more tricky speed changes!"; + public override IconUsage? Icon => FontAwesome.Solid.Equals; + public override ModType Type => ModType.Conversion; + + public void ApplyToBeatmap(IBeatmap beatmap) + { + var taikoBeatmap = (TaikoBeatmap)beatmap; + + foreach (var effectControlPoint in taikoBeatmap.ControlPointInfo.EffectPoints) + { + effectControlPoint.ScrollSpeed = 1; + } + } + } +} diff --git a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs index 24b0ec5d57..b701d3c25a 100644 --- a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs @@ -150,6 +150,7 @@ namespace osu.Game.Rulesets.Taiko new TaikoModClassic(), new TaikoModSwap(), new TaikoModSingleTap(), + new TaikoModConstantSpeed(), }; case ModType.Automation: From 1cbc2f07ab039c54d3788063e5028ab34577937a Mon Sep 17 00:00:00 2001 From: Hivie Date: Fri, 23 Feb 2024 14:01:12 +0100 Subject: [PATCH 0237/2493] use more correct implementation --- .../Mods/TaikoModConstantSpeed.cs | 20 +++++++++---------- .../UI/DrawableTaikoRuleset.cs | 7 ++++++- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModConstantSpeed.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModConstantSpeed.cs index 28de360eee..4ecb94467e 100644 --- a/osu.Game.Rulesets.Taiko/Mods/TaikoModConstantSpeed.cs +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModConstantSpeed.cs @@ -1,15 +1,17 @@ // 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; using osu.Framework.Graphics.Sprites; -using osu.Game.Beatmaps; +using osu.Framework.Localisation; +using osu.Game.Configuration; +using osu.Game.Rulesets.Taiko.Objects; +using osu.Game.Rulesets.Taiko.UI; using osu.Game.Rulesets.Mods; -using osu.Game.Rulesets.Taiko.Beatmaps; +using osu.Game.Rulesets.UI; namespace osu.Game.Rulesets.Taiko.Mods { - public class TaikoModConstantSpeed : Mod, IApplicableToBeatmap + public class TaikoModConstantSpeed : Mod, IApplicableToDrawableRuleset { public override string Name => "Constant Speed"; public override string Acronym => "CS"; @@ -18,14 +20,10 @@ namespace osu.Game.Rulesets.Taiko.Mods public override IconUsage? Icon => FontAwesome.Solid.Equals; public override ModType Type => ModType.Conversion; - public void ApplyToBeatmap(IBeatmap beatmap) + public void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset) { - var taikoBeatmap = (TaikoBeatmap)beatmap; - - foreach (var effectControlPoint in taikoBeatmap.ControlPointInfo.EffectPoints) - { - effectControlPoint.ScrollSpeed = 1; - } + var taikoRuleset = (DrawableTaikoRuleset)drawableRuleset; + taikoRuleset.VisualisationMethod = ScrollVisualisationMethod.Constant; } } } diff --git a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs index 77b2b06c0e..a476634fb8 100644 --- a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs @@ -69,7 +69,12 @@ namespace osu.Game.Rulesets.Taiko.UI TimeRange.Value = ComputeTimeRange(); } - protected virtual double ComputeTimeRange() => PlayfieldAdjustmentContainer.ComputeTimeRange(); + protected virtual double ComputeTimeRange() + { + // Adjust when we're using constant algorithm to not be sluggish. + double multiplier = VisualisationMethod == ScrollVisualisationMethod.Overlapping ? 1 : 4; + return PlayfieldAdjustmentContainer.ComputeTimeRange() / multiplier; + } protected override void UpdateAfterChildren() { From 14b0c41937f39906ddaf86d4919baadf2d5c2174 Mon Sep 17 00:00:00 2001 From: Hivie Date: Fri, 23 Feb 2024 14:22:56 +0100 Subject: [PATCH 0238/2493] remove unnecessary `ComputeTimeRange` override --- osu.Game.Rulesets.Taiko/Edit/DrawableTaikoEditorRuleset.cs | 7 ------- 1 file changed, 7 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Edit/DrawableTaikoEditorRuleset.cs b/osu.Game.Rulesets.Taiko/Edit/DrawableTaikoEditorRuleset.cs index 3c7a97c864..217bb8139c 100644 --- a/osu.Game.Rulesets.Taiko/Edit/DrawableTaikoEditorRuleset.cs +++ b/osu.Game.Rulesets.Taiko/Edit/DrawableTaikoEditorRuleset.cs @@ -26,12 +26,5 @@ namespace osu.Game.Rulesets.Taiko.Edit ShowSpeedChanges.BindValueChanged(showChanges => VisualisationMethod = showChanges.NewValue ? ScrollVisualisationMethod.Overlapping : ScrollVisualisationMethod.Constant, true); } - - protected override double ComputeTimeRange() - { - // Adjust when we're using constant algorithm to not be sluggish. - double multiplier = ShowSpeedChanges.Value ? 1 : 4; - return base.ComputeTimeRange() / multiplier; - } } } From 7762d2469b59c4a349d5a05119f0c7a007cd0c5e Mon Sep 17 00:00:00 2001 From: Hivie Date: Fri, 23 Feb 2024 14:24:26 +0100 Subject: [PATCH 0239/2493] exclude EZ/HR for now --- osu.Game.Rulesets.Taiko/Mods/TaikoModConstantSpeed.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModConstantSpeed.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModConstantSpeed.cs index 4ecb94467e..117dc0ebd2 100644 --- a/osu.Game.Rulesets.Taiko/Mods/TaikoModConstantSpeed.cs +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModConstantSpeed.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 System; +using System.Linq; using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; using osu.Game.Configuration; @@ -19,6 +21,7 @@ namespace osu.Game.Rulesets.Taiko.Mods public override LocalisableString Description => "No more tricky speed changes!"; public override IconUsage? Icon => FontAwesome.Solid.Equals; public override ModType Type => ModType.Conversion; + public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(TaikoModEasy), typeof(TaikoModHardRock) }).ToArray(); public void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset) { From 65c0b73dd5c0b8a74deeab5aa737fb5d90dce82f Mon Sep 17 00:00:00 2001 From: Hivie Date: Fri, 23 Feb 2024 17:55:49 +0100 Subject: [PATCH 0240/2493] mark `TaikoModConstantSpeed` as incompatible with EZ/HR --- osu.Game.Rulesets.Taiko/Mods/TaikoModEasy.cs | 3 +++ osu.Game.Rulesets.Taiko/Mods/TaikoModHardRock.cs | 3 +++ 2 files changed, 6 insertions(+) diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModEasy.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModEasy.cs index 009f2854f8..59d0563f1f 100644 --- a/osu.Game.Rulesets.Taiko/Mods/TaikoModEasy.cs +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModEasy.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 System; +using System.Linq; using osu.Framework.Localisation; using osu.Game.Beatmaps; using osu.Game.Rulesets.Mods; @@ -10,6 +12,7 @@ namespace osu.Game.Rulesets.Taiko.Mods public class TaikoModEasy : ModEasy { public override LocalisableString Description => @"Beats move slower, and less accuracy required!"; + public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(TaikoModConstantSpeed) }).ToArray(); /// /// Multiplier factor added to the scrolling speed. diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModHardRock.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModHardRock.cs index ba41175461..fa948507c8 100644 --- a/osu.Game.Rulesets.Taiko/Mods/TaikoModHardRock.cs +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModHardRock.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 System; +using System.Linq; using osu.Game.Beatmaps; using osu.Game.Rulesets.Mods; @@ -8,6 +10,7 @@ namespace osu.Game.Rulesets.Taiko.Mods { public class TaikoModHardRock : ModHardRock { + public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(TaikoModConstantSpeed) }).ToArray(); public override double ScoreMultiplier => UsesDefaultConfiguration ? 1.06 : 1; /// From 46a1f5267f40f03b566c4feb713de3a08a135ee8 Mon Sep 17 00:00:00 2001 From: Hivie Date: Fri, 23 Feb 2024 21:15:18 +0100 Subject: [PATCH 0241/2493] account for beatmap base scroll speed in constant visualisation method --- osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs index a476634fb8..c88bbec9bc 100644 --- a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs @@ -72,7 +72,7 @@ namespace osu.Game.Rulesets.Taiko.UI protected virtual double ComputeTimeRange() { // Adjust when we're using constant algorithm to not be sluggish. - double multiplier = VisualisationMethod == ScrollVisualisationMethod.Overlapping ? 1 : 4; + double multiplier = VisualisationMethod == ScrollVisualisationMethod.Overlapping ? 1 : 4 * Beatmap.Difficulty.SliderMultiplier; return PlayfieldAdjustmentContainer.ComputeTimeRange() / multiplier; } From 4ea9519db839762a088b2ef15fc8f770b0c87905 Mon Sep 17 00:00:00 2001 From: Hivie Date: Fri, 23 Feb 2024 21:15:52 +0100 Subject: [PATCH 0242/2493] revert changes to `IncompatibleMods` --- osu.Game.Rulesets.Taiko/Mods/TaikoModConstantSpeed.cs | 3 --- osu.Game.Rulesets.Taiko/Mods/TaikoModEasy.cs | 3 --- osu.Game.Rulesets.Taiko/Mods/TaikoModHardRock.cs | 3 --- 3 files changed, 9 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModConstantSpeed.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModConstantSpeed.cs index 117dc0ebd2..4ecb94467e 100644 --- a/osu.Game.Rulesets.Taiko/Mods/TaikoModConstantSpeed.cs +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModConstantSpeed.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. -using System; -using System.Linq; using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; using osu.Game.Configuration; @@ -21,7 +19,6 @@ namespace osu.Game.Rulesets.Taiko.Mods public override LocalisableString Description => "No more tricky speed changes!"; public override IconUsage? Icon => FontAwesome.Solid.Equals; public override ModType Type => ModType.Conversion; - public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(TaikoModEasy), typeof(TaikoModHardRock) }).ToArray(); public void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset) { diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModEasy.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModEasy.cs index 59d0563f1f..009f2854f8 100644 --- a/osu.Game.Rulesets.Taiko/Mods/TaikoModEasy.cs +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModEasy.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. -using System; -using System.Linq; using osu.Framework.Localisation; using osu.Game.Beatmaps; using osu.Game.Rulesets.Mods; @@ -12,7 +10,6 @@ namespace osu.Game.Rulesets.Taiko.Mods public class TaikoModEasy : ModEasy { public override LocalisableString Description => @"Beats move slower, and less accuracy required!"; - public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(TaikoModConstantSpeed) }).ToArray(); /// /// Multiplier factor added to the scrolling speed. diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModHardRock.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModHardRock.cs index fa948507c8..ba41175461 100644 --- a/osu.Game.Rulesets.Taiko/Mods/TaikoModHardRock.cs +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModHardRock.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. -using System; -using System.Linq; using osu.Game.Beatmaps; using osu.Game.Rulesets.Mods; @@ -10,7 +8,6 @@ namespace osu.Game.Rulesets.Taiko.Mods { public class TaikoModHardRock : ModHardRock { - public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(TaikoModConstantSpeed) }).ToArray(); public override double ScoreMultiplier => UsesDefaultConfiguration ? 1.06 : 1; /// From 4d669c56a2adb102031e1ea0c63538409ace3ba8 Mon Sep 17 00:00:00 2001 From: Sheepposu Date: Fri, 23 Feb 2024 23:32:35 -0500 Subject: [PATCH 0243/2493] implement pooling Uses pooling for all analysis objects and creates the lifetime entries from replay data when the analysis container is constructed. --- .../UI/OsuAnalysisContainer.cs | 242 ++++++++++++++++++ .../UI/OsuAnalysisSettings.cs | 18 +- osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs | 5 +- osu.Game/Rulesets/UI/AnalysisContainer.cs | 18 ++ osu.Game/Rulesets/UI/Playfield.cs | 6 + .../Play/PlayerSettings/AnalysisSettings.cs | 5 +- osu.Game/Screens/Play/ReplayPlayer.cs | 3 + 7 files changed, 284 insertions(+), 13 deletions(-) create mode 100644 osu.Game.Rulesets.Osu/UI/OsuAnalysisContainer.cs create mode 100644 osu.Game/Rulesets/UI/AnalysisContainer.cs diff --git a/osu.Game.Rulesets.Osu/UI/OsuAnalysisContainer.cs b/osu.Game.Rulesets.Osu/UI/OsuAnalysisContainer.cs new file mode 100644 index 0000000000..a637eddd05 --- /dev/null +++ b/osu.Game.Rulesets.Osu/UI/OsuAnalysisContainer.cs @@ -0,0 +1,242 @@ +// 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 osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Lines; +using osu.Framework.Graphics.Performance; +using osu.Framework.Graphics.Pooling; +using osu.Game.Replays; +using osu.Game.Rulesets.Objects.Pooling; +using osu.Game.Rulesets.Osu.Replays; +using osu.Game.Rulesets.Osu.Skinning; +using osu.Game.Rulesets.Osu.Skinning.Default; +using osu.Game.Rulesets.UI; +using osu.Game.Skinning; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Rulesets.Osu.UI +{ + public partial class OsuAnalysisContainer : AnalysisContainer + { + public Bindable HitMarkerEnabled = new BindableBool(); + public Bindable AimMarkersEnabled = new BindableBool(); + public Bindable AimLinesEnabled = new BindableBool(); + + private HitMarkersContainer hitMarkersContainer; + private AimMarkersContainer aimMarkersContainer; + private AimLinesContainer aimLinesContainer; + + public OsuAnalysisContainer(Replay replay) + : base(replay) + { + InternalChildren = new Drawable[] + { + hitMarkersContainer = new HitMarkersContainer(), + aimMarkersContainer = new AimMarkersContainer() { Depth = float.MinValue }, + aimLinesContainer = new AimLinesContainer() { Depth = float.MaxValue } + }; + + HitMarkerEnabled.ValueChanged += e => hitMarkersContainer.FadeTo(e.NewValue ? 1 : 0); + AimMarkersEnabled.ValueChanged += e => aimMarkersContainer.FadeTo(e.NewValue ? 1 : 0); + AimLinesEnabled.ValueChanged += e => aimLinesContainer.FadeTo(e.NewValue ? 1 : 0); + } + + [BackgroundDependencyLoader] + private void load(ISkinSource skin) + { + var aimLineColor = skin.GetConfig(OsuSkinColour.ReplayAimLine)?.Value ?? Color4.White; + aimLineColor.A = 127; + + hitMarkersContainer.Hide(); + aimMarkersContainer.Hide(); + aimLinesContainer.Hide(); + + bool leftHeld = false; + bool rightHeld = false; + foreach (var frame in Replay.Frames) + { + var osuFrame = (OsuReplayFrame)frame; + + aimMarkersContainer.Add(new AimPointEntry(osuFrame.Time, osuFrame.Position)); + aimLinesContainer.Add(new AimPointEntry(osuFrame.Time, osuFrame.Position)); + + bool leftButton = osuFrame.Actions.Contains(OsuAction.LeftButton); + bool rightButton = osuFrame.Actions.Contains(OsuAction.RightButton); + + if (leftHeld && !leftButton) + leftHeld = false; + else if (!leftHeld && leftButton) + { + hitMarkersContainer.Add(new HitMarkerEntry(osuFrame.Time, osuFrame.Position, true)); + leftHeld = true; + } + + if (rightHeld && !rightButton) + rightHeld = false; + else if (!rightHeld && rightButton) + { + hitMarkersContainer.Add(new HitMarkerEntry(osuFrame.Time, osuFrame.Position, false)); + rightHeld = true; + } + } + } + + private partial class HitMarkersContainer : PooledDrawableWithLifetimeContainer + { + private readonly HitMarkerPool leftPool; + private readonly HitMarkerPool rightPool; + + public HitMarkersContainer() + { + AddInternal(leftPool = new HitMarkerPool(OsuSkinComponents.HitMarkerLeft, OsuAction.LeftButton, 15)); + AddInternal(rightPool = new HitMarkerPool(OsuSkinComponents.HitMarkerRight, OsuAction.RightButton, 15)); + } + + protected override HitMarkerDrawable GetDrawable(HitMarkerEntry entry) => (entry.IsLeftMarker ? leftPool : rightPool).Get(d => d.Apply(entry)); + } + + private partial class AimMarkersContainer : PooledDrawableWithLifetimeContainer + { + private readonly HitMarkerPool pool; + + public AimMarkersContainer() + { + AddInternal(pool = new HitMarkerPool(OsuSkinComponents.AimMarker, null, 80)); + } + + protected override HitMarkerDrawable GetDrawable(AimPointEntry entry) => pool.Get(d => d.Apply(entry)); + } + + private partial class AimLinesContainer : Path + { + private LifetimeEntryManager lifetimeManager = new LifetimeEntryManager(); + private SortedSet aliveEntries = new SortedSet(new AimLinePointComparator()); + + public AimLinesContainer() + { + lifetimeManager.EntryBecameAlive += entryBecameAlive; + lifetimeManager.EntryBecameDead += entryBecameDead; + lifetimeManager.EntryCrossedBoundary += entryCrossedBoundary; + + PathRadius = 1f; + Colour = new Color4(255, 255, 255, 127); + } + + protected override void Update() + { + base.Update(); + + lifetimeManager.Update(Time.Current); + } + + public void Add(AimPointEntry entry) => lifetimeManager.AddEntry(entry); + + private void entryBecameAlive(LifetimeEntry entry) + { + aliveEntries.Add((AimPointEntry)entry); + updateVertices(); + } + + private void entryBecameDead(LifetimeEntry entry) + { + aliveEntries.Remove((AimPointEntry)entry); + updateVertices(); + } + + private void updateVertices() + { + ClearVertices(); + foreach (var entry in aliveEntries) + { + AddVertex(entry.Position); + } + } + + private void entryCrossedBoundary(LifetimeEntry entry, LifetimeBoundaryKind kind, LifetimeBoundaryCrossingDirection direction) + { + + } + + private sealed class AimLinePointComparator : IComparer + { + public int Compare(AimPointEntry? x, AimPointEntry? y) + { + ArgumentNullException.ThrowIfNull(x); + ArgumentNullException.ThrowIfNull(y); + + return x.LifetimeStart.CompareTo(y.LifetimeStart); + } + } + } + + private partial class HitMarkerDrawable : PoolableDrawableWithLifetime + { + /// + /// This constructor only exists to meet the new() type constraint of . + /// + public HitMarkerDrawable() + { + } + + public HitMarkerDrawable(OsuSkinComponents component, OsuAction? action) + { + Origin = Anchor.Centre; + InternalChild = new SkinnableDrawable(new OsuSkinComponentLookup(component), _ => new DefaultHitMarker(action)); + } + + protected override void OnApply(AimPointEntry entry) + { + Position = entry.Position; + + using (BeginAbsoluteSequence(LifetimeStart)) + Show(); + + using (BeginAbsoluteSequence(LifetimeEnd - 200)) + this.FadeOut(200); + } + } + + private partial class HitMarkerPool : DrawablePool + { + private readonly OsuSkinComponents component; + private readonly OsuAction? action; + + public HitMarkerPool(OsuSkinComponents component, OsuAction? action, int initialSize) + : base(initialSize) + { + this.component = component; + this.action = action; + } + + protected override HitMarkerDrawable CreateNewDrawable() => new HitMarkerDrawable(component, action); + } + + private partial class AimPointEntry : LifetimeEntry + { + public Vector2 Position { get; } + + public AimPointEntry(double time, Vector2 position) + { + LifetimeStart = time; + LifetimeEnd = time + 1_000; + Position = position; + } + } + + private partial class HitMarkerEntry : AimPointEntry + { + public bool IsLeftMarker { get; } + + public HitMarkerEntry(double lifetimeStart, Vector2 position, bool isLeftMarker) + : base(lifetimeStart, position) + { + IsLeftMarker = isLeftMarker; + } + } + } +} diff --git a/osu.Game.Rulesets.Osu/UI/OsuAnalysisSettings.cs b/osu.Game.Rulesets.Osu/UI/OsuAnalysisSettings.cs index 0411882d2a..fd9cb67995 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuAnalysisSettings.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuAnalysisSettings.cs @@ -1,10 +1,10 @@ // 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.Bindables; using osu.Framework.Graphics; using osu.Game.Localisation; +using osu.Game.Replays; using osu.Game.Rulesets.UI; using osu.Game.Screens.Play.PlayerSettings; @@ -29,14 +29,7 @@ namespace osu.Game.Rulesets.Osu.UI aimLinesToggle = new PlayerCheckbox { LabelText = PlayerSettingsOverlayStrings.AimLines }, hideCursorToggle = new PlayerCheckbox { LabelText = PlayerSettingsOverlayStrings.HideCursor } }; - } - [BackgroundDependencyLoader] - private void load() - { - DrawableRuleset.Playfield.MarkersContainer.HitMarkerEnabled.BindTo(hitMarkerToggle.Current); - DrawableRuleset.Playfield.MarkersContainer.AimMarkersEnabled.BindTo(aimMarkerToggle.Current); - DrawableRuleset.Playfield.MarkersContainer.AimLinesEnabled.BindTo(aimLinesToggle.Current); hideCursorToggle.Current.BindValueChanged(onCursorToggle); } @@ -52,5 +45,14 @@ namespace osu.Game.Rulesets.Osu.UI DrawableRuleset.Playfield.Cursor.FadeIn(); } } + + public override AnalysisContainer CreateAnalysisContainer(Replay replay) + { + var analysisContainer = new OsuAnalysisContainer(replay); + analysisContainer.HitMarkerEnabled.BindTo(hitMarkerToggle.Current); + analysisContainer.AimMarkersEnabled.BindTo(aimMarkerToggle.Current); + analysisContainer.AimLinesEnabled.BindTo(aimLinesToggle.Current); + return analysisContainer; + } } } diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs index 3cb3c50ef7..ea336e6067 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs @@ -37,8 +37,6 @@ namespace osu.Game.Rulesets.Osu.UI public SmokeContainer Smoke { get; } - public HitMarkerContainer MarkersContainer { get; } - public FollowPointRenderer FollowPoints { get; } public static readonly Vector2 BASE_SIZE = new Vector2(512, 384); @@ -61,8 +59,7 @@ namespace osu.Game.Rulesets.Osu.UI judgementLayer = new JudgementContainer { RelativeSizeAxes = Axes.Both }, HitObjectContainer, judgementAboveHitObjectLayer = new Container { RelativeSizeAxes = Axes.Both }, - approachCircles = new ProxyContainer { RelativeSizeAxes = Axes.Both }, - MarkersContainer = new HitMarkerContainer(HitObjectContainer) { RelativeSizeAxes = Axes.Both } + approachCircles = new ProxyContainer { RelativeSizeAxes = Axes.Both } }; HitPolicy = new StartTimeOrderedHitPolicy(); diff --git a/osu.Game/Rulesets/UI/AnalysisContainer.cs b/osu.Game/Rulesets/UI/AnalysisContainer.cs new file mode 100644 index 0000000000..62d54374e7 --- /dev/null +++ b/osu.Game/Rulesets/UI/AnalysisContainer.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.Graphics.Containers; +using osu.Game.Replays; + +namespace osu.Game.Rulesets.UI +{ + public partial class AnalysisContainer : Container + { + protected Replay Replay; + + public AnalysisContainer(Replay replay) + { + Replay = replay; + } + } +} diff --git a/osu.Game/Rulesets/UI/Playfield.cs b/osu.Game/Rulesets/UI/Playfield.cs index 90a2f63faa..e116acdc19 100644 --- a/osu.Game/Rulesets/UI/Playfield.cs +++ b/osu.Game/Rulesets/UI/Playfield.cs @@ -291,6 +291,12 @@ namespace osu.Game.Rulesets.UI /// protected virtual HitObjectContainer CreateHitObjectContainer() => new HitObjectContainer(); + /// + /// Adds an analysis container to internal children for replays. + /// + /// + public virtual void AddAnalysisContainer(AnalysisContainer analysisContainer) => AddInternal(analysisContainer); + #region Pooling support private readonly Dictionary pools = new Dictionary(); diff --git a/osu.Game/Screens/Play/PlayerSettings/AnalysisSettings.cs b/osu.Game/Screens/Play/PlayerSettings/AnalysisSettings.cs index e30d2b2d42..3752b2b900 100644 --- a/osu.Game/Screens/Play/PlayerSettings/AnalysisSettings.cs +++ b/osu.Game/Screens/Play/PlayerSettings/AnalysisSettings.cs @@ -1,11 +1,12 @@ // 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.Replays; using osu.Game.Rulesets.UI; namespace osu.Game.Screens.Play.PlayerSettings { - public partial class AnalysisSettings : PlayerSettingsGroup + public abstract partial class AnalysisSettings : PlayerSettingsGroup { protected DrawableRuleset DrawableRuleset; @@ -14,5 +15,7 @@ namespace osu.Game.Screens.Play.PlayerSettings { DrawableRuleset = drawableRuleset; } + + public abstract AnalysisContainer CreateAnalysisContainer(Replay replay); } } diff --git a/osu.Game/Screens/Play/ReplayPlayer.cs b/osu.Game/Screens/Play/ReplayPlayer.cs index 65e99731dc..ce6cb5124a 100644 --- a/osu.Game/Screens/Play/ReplayPlayer.cs +++ b/osu.Game/Screens/Play/ReplayPlayer.cs @@ -74,7 +74,10 @@ namespace osu.Game.Screens.Play var analysisSettings = DrawableRuleset.Ruleset.CreateAnalysisSettings(DrawableRuleset); if (analysisSettings != null) + { HUDOverlay.PlayerSettingsOverlay.AddAtStart(analysisSettings); + DrawableRuleset.Playfield.AddAnalysisContainer(analysisSettings.CreateAnalysisContainer(GameplayState.Score.Replay)); + } } protected override void PrepareReplay() From c95e85368fab71e8c38e10f3dcf809f47f0986e3 Mon Sep 17 00:00:00 2001 From: Sheepposu Date: Fri, 23 Feb 2024 23:45:58 -0500 Subject: [PATCH 0244/2493] remove old files --- .../TestSceneHitMarker.cs | 135 ------------ .../UI/HitMarkerContainer.cs | 195 ------------------ 2 files changed, 330 deletions(-) delete mode 100644 osu.Game.Rulesets.Osu.Tests/TestSceneHitMarker.cs delete mode 100644 osu.Game.Rulesets.Osu/UI/HitMarkerContainer.cs diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneHitMarker.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneHitMarker.cs deleted file mode 100644 index 7ba681b50f..0000000000 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneHitMarker.cs +++ /dev/null @@ -1,135 +0,0 @@ -// 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 NUnit.Framework; -using osu.Framework.Graphics; -using osu.Framework.Input.Events; -using osu.Framework.Input.States; -using osu.Framework.Logging; -using osu.Framework.Testing.Input; -using osu.Game.Rulesets.Osu.UI; -using osu.Game.Rulesets.UI; -using osuTK; - -namespace osu.Game.Rulesets.Osu.Tests -{ - public partial class TestSceneHitMarker : OsuSkinnableTestScene - { - [Test] - public void TestHitMarkers() - { - var markerContainers = new List(); - - AddStep("Create hit markers", () => - { - markerContainers.Clear(); - SetContents(_ => - { - markerContainers.Add(new TestHitMarkerContainer(new HitObjectContainer()) - { - HitMarkerEnabled = { Value = true }, - AimMarkersEnabled = { Value = true }, - AimLinesEnabled = { Value = true }, - RelativeSizeAxes = Axes.Both - }); - - return new HitMarkerInputManager - { - RelativeSizeAxes = Axes.Both, - Size = new Vector2(0.95f), - Child = markerContainers[^1], - }; - }); - }); - - AddUntilStep("Until skinnable expires", () => - { - if (markerContainers.Count == 0) - return false; - - Logger.Log("How many: " + markerContainers.Count); - - foreach (var markerContainer in markerContainers) - { - if (markerContainer.Children.Count != 0) - return false; - } - - return true; - }); - } - - private partial class HitMarkerInputManager : ManualInputManager - { - private double? startTime; - - public HitMarkerInputManager() - { - UseParentInput = false; - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - MoveMouseTo(ToScreenSpace(DrawSize / 2)); - } - - protected override void Update() - { - base.Update(); - - const float spin_angle = 4 * MathF.PI; - - startTime ??= Time.Current; - - float fraction = (float)((Time.Current - startTime) / 5_000); - - float angle = fraction * spin_angle; - float radius = fraction * Math.Min(DrawSize.X, DrawSize.Y) / 2; - - Vector2 pos = radius * new Vector2(MathF.Cos(angle), MathF.Sin(angle)) + DrawSize / 2; - MoveMouseTo(ToScreenSpace(pos)); - } - } - - private partial class TestHitMarkerContainer : HitMarkerContainer - { - private double? lastClick; - private double? startTime; - private bool finishedDrawing; - private bool leftOrRight; - - public TestHitMarkerContainer(HitObjectContainer hitObjectContainer) - : base(hitObjectContainer) - { - } - - protected override void Update() - { - base.Update(); - - if (finishedDrawing) - return; - - startTime ??= lastClick ??= Time.Current; - - if (startTime + 5_000 <= Time.Current) - { - finishedDrawing = true; - HitMarkerEnabled.Value = AimMarkersEnabled.Value = AimLinesEnabled.Value = false; - return; - } - - if (lastClick + 400 <= Time.Current) - { - OnPressed(new KeyBindingPressEvent(new InputState(), leftOrRight ? OsuAction.LeftButton : OsuAction.RightButton)); - leftOrRight = !leftOrRight; - lastClick = Time.Current; - } - } - } - } -} diff --git a/osu.Game.Rulesets.Osu/UI/HitMarkerContainer.cs b/osu.Game.Rulesets.Osu/UI/HitMarkerContainer.cs deleted file mode 100644 index e8916ea545..0000000000 --- a/osu.Game.Rulesets.Osu/UI/HitMarkerContainer.cs +++ /dev/null @@ -1,195 +0,0 @@ -// 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; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Input; -using osu.Framework.Input.Bindings; -using osu.Framework.Input.Events; -using osu.Game.Rulesets.Osu.Objects; -using osu.Game.Rulesets.Osu.Objects.Drawables; -using osu.Game.Rulesets.Osu.Skinning; -using osu.Game.Rulesets.Osu.Skinning.Default; -using osu.Game.Rulesets.UI; -using osu.Game.Skinning; -using osuTK; -using osuTK.Graphics; - -namespace osu.Game.Rulesets.Osu.UI -{ - public partial class HitMarkerContainer : Container, IRequireHighFrequencyMousePosition, IKeyBindingHandler - { - private Vector2 lastMousePosition; - private Vector2? lastlastMousePosition; - private double timePreempt; - - public Bindable HitMarkerEnabled = new BindableBool(); - public Bindable AimMarkersEnabled = new BindableBool(); - public Bindable AimLinesEnabled = new BindableBool(); - - private const double default_time_preempt = 1000; - - private readonly HitObjectContainer hitObjectContainer; - - public override bool ReceivePositionalInputAt(Vector2 _) => true; - - public HitMarkerContainer(HitObjectContainer hitObjectContainer) - { - this.hitObjectContainer = hitObjectContainer; - timePreempt = default_time_preempt; - } - - public bool OnPressed(KeyBindingPressEvent e) - { - if (HitMarkerEnabled.Value && (e.Action == OsuAction.LeftButton || e.Action == OsuAction.RightButton)) - { - updateTimePreempt(); - addMarker(e.Action); - } - - return false; - } - - public void OnReleased(KeyBindingReleaseEvent e) - { - } - - protected override bool OnMouseMove(MouseMoveEvent e) - { - lastlastMousePosition = lastMousePosition; - lastMousePosition = e.MousePosition; - - if (AimMarkersEnabled.Value) - { - updateTimePreempt(); - addMarker(null); - } - - if (AimLinesEnabled.Value && lastlastMousePosition != null && lastlastMousePosition != lastMousePosition) - { - if (!AimMarkersEnabled.Value) - updateTimePreempt(); - Add(new AimLineDrawable((Vector2)lastlastMousePosition, lastMousePosition, timePreempt)); - } - - return base.OnMouseMove(e); - } - - private void addMarker(OsuAction? action) - { - var component = OsuSkinComponents.AimMarker; - - switch (action) - { - case OsuAction.LeftButton: - component = OsuSkinComponents.HitMarkerLeft; - break; - - case OsuAction.RightButton: - component = OsuSkinComponents.HitMarkerRight; - break; - } - - Add(new HitMarkerDrawable(action, component, timePreempt) - { - Position = lastMousePosition, - Origin = Anchor.Centre, - Depth = action == null ? float.MaxValue : float.MinValue - }); - } - - private void updateTimePreempt() - { - var hitObject = getHitObject(); - if (hitObject == null) - return; - - timePreempt = hitObject.TimePreempt; - } - - private OsuHitObject? getHitObject() - { - foreach (var dho in hitObjectContainer.Objects) - return (dho as DrawableOsuHitObject)?.HitObject; - - return null; - } - - private partial class HitMarkerDrawable : SkinnableDrawable - { - private readonly double lifetimeDuration; - private readonly double fadeOutTime; - - public override bool RemoveWhenNotAlive => true; - - public HitMarkerDrawable(OsuAction? action, OsuSkinComponents componenet, double timePreempt) - : base(new OsuSkinComponentLookup(componenet), _ => new DefaultHitMarker(action)) - { - fadeOutTime = timePreempt / 2; - lifetimeDuration = timePreempt + fadeOutTime; - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - LifetimeStart = Time.Current; - LifetimeEnd = LifetimeStart + lifetimeDuration; - - Scheduler.AddDelayed(() => - { - this.FadeOut(fadeOutTime); - }, lifetimeDuration - fadeOutTime); - } - } - - private partial class AimLineDrawable : CompositeDrawable - { - private readonly double lifetimeDuration; - private readonly double fadeOutTime; - - public override bool RemoveWhenNotAlive => true; - - public AimLineDrawable(Vector2 fromP, Vector2 toP, double timePreempt) - { - fadeOutTime = timePreempt / 2; - lifetimeDuration = timePreempt + fadeOutTime; - - float distance = Vector2.Distance(fromP, toP); - Vector2 direction = (toP - fromP); - InternalChild = new Box - { - Position = fromP + (direction / 2), - Size = new Vector2(distance, 1), - Rotation = (float)(Math.Atan(direction.Y / direction.X) * (180 / Math.PI)), - Origin = Anchor.Centre - }; - } - - [BackgroundDependencyLoader] - private void load(ISkinSource skin) - { - var color = skin.GetConfig(OsuSkinColour.ReplayAimLine)?.Value ?? Color4.White; - color.A = 127; - InternalChild.Colour = color; - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - LifetimeStart = Time.Current; - LifetimeEnd = LifetimeStart + lifetimeDuration; - - Scheduler.AddDelayed(() => - { - this.FadeOut(fadeOutTime); - }, lifetimeDuration - fadeOutTime); - } - } - } -} From 8cdd9c9ddcec526dcf4866f2c0b0ce1b98dbffd4 Mon Sep 17 00:00:00 2001 From: Sheepposu Date: Fri, 23 Feb 2024 23:46:50 -0500 Subject: [PATCH 0245/2493] skinning changes improve default design --- .../Skinning/Default/DefaultHitMarker.cs | 8 +++----- osu.Game.Rulesets.Osu/Skinning/OsuSkinColour.cs | 1 - osu.Game.Rulesets.Osu/UI/OsuAnalysisContainer.cs | 6 +----- 3 files changed, 4 insertions(+), 11 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/DefaultHitMarker.cs b/osu.Game.Rulesets.Osu/Skinning/Default/DefaultHitMarker.cs index 7dabb5182f..dc890d4d63 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Default/DefaultHitMarker.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Default/DefaultHitMarker.cs @@ -23,7 +23,6 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default Anchor = Anchor.Centre, Origin = Anchor.Centre, Size = new Vector2(3, length), - Rotation = 45, Colour = Colour4.Black.Opacity(0.5F) }, new Box @@ -31,7 +30,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default Anchor = Anchor.Centre, Origin = Anchor.Centre, Size = new Vector2(3, length), - Rotation = 135, + Rotation = 90, Colour = Colour4.Black.Opacity(0.5F) } }; @@ -44,7 +43,6 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default Anchor = Anchor.Centre, Origin = Anchor.Centre, Size = new Vector2(1, length), - Rotation = 45, Colour = colour }, new Box @@ -52,7 +50,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default Anchor = Anchor.Centre, Origin = Anchor.Centre, Size = new Vector2(1, length), - Rotation = 135, + Rotation = 90, Colour = colour } }); @@ -69,7 +67,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default return (Colour4.LightGreen, 20, true); default: - return (Colour4.Gray.Opacity(0.3F), 8, false); + return (Colour4.Gray.Opacity(0.5F), 8, false); } } } diff --git a/osu.Game.Rulesets.Osu/Skinning/OsuSkinColour.cs b/osu.Game.Rulesets.Osu/Skinning/OsuSkinColour.cs index 5c864fb6c2..24f9217a5f 100644 --- a/osu.Game.Rulesets.Osu/Skinning/OsuSkinColour.cs +++ b/osu.Game.Rulesets.Osu/Skinning/OsuSkinColour.cs @@ -10,6 +10,5 @@ namespace osu.Game.Rulesets.Osu.Skinning SliderBall, SpinnerBackground, StarBreakAdditive, - ReplayAimLine, } } diff --git a/osu.Game.Rulesets.Osu/UI/OsuAnalysisContainer.cs b/osu.Game.Rulesets.Osu/UI/OsuAnalysisContainer.cs index a637eddd05..68686f835d 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuAnalysisContainer.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuAnalysisContainer.cs @@ -12,7 +12,6 @@ using osu.Framework.Graphics.Pooling; using osu.Game.Replays; using osu.Game.Rulesets.Objects.Pooling; using osu.Game.Rulesets.Osu.Replays; -using osu.Game.Rulesets.Osu.Skinning; using osu.Game.Rulesets.Osu.Skinning.Default; using osu.Game.Rulesets.UI; using osu.Game.Skinning; @@ -47,11 +46,8 @@ namespace osu.Game.Rulesets.Osu.UI } [BackgroundDependencyLoader] - private void load(ISkinSource skin) + private void load() { - var aimLineColor = skin.GetConfig(OsuSkinColour.ReplayAimLine)?.Value ?? Color4.White; - aimLineColor.A = 127; - hitMarkersContainer.Hide(); aimMarkersContainer.Hide(); aimLinesContainer.Hide(); From 822ecb71068b77e071fb590983ff2834b0f9b2e3 Mon Sep 17 00:00:00 2001 From: Sheepposu Date: Fri, 23 Feb 2024 23:52:17 -0500 Subject: [PATCH 0246/2493] remove unnecessary changes --- osu.Game.Rulesets.Osu.Tests/Resources/special-skin/skin.ini | 5 +---- osu.Game.Rulesets.Osu/UI/OsuAnalysisContainer.cs | 6 ------ osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs | 2 +- 3 files changed, 2 insertions(+), 11 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/skin.ini b/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/skin.ini index 2952948f45..9d16267d73 100644 --- a/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/skin.ini +++ b/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/skin.ini @@ -1,7 +1,4 @@ [General] Version: latest HitCircleOverlayAboveNumber: 0 -HitCirclePrefix: display - -[Colours] -ReplayAimLine: 0,0,255 \ No newline at end of file +HitCirclePrefix: display \ No newline at end of file diff --git a/osu.Game.Rulesets.Osu/UI/OsuAnalysisContainer.cs b/osu.Game.Rulesets.Osu/UI/OsuAnalysisContainer.cs index 68686f835d..c4b5135ca2 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuAnalysisContainer.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuAnalysisContainer.cs @@ -117,7 +117,6 @@ namespace osu.Game.Rulesets.Osu.UI { lifetimeManager.EntryBecameAlive += entryBecameAlive; lifetimeManager.EntryBecameDead += entryBecameDead; - lifetimeManager.EntryCrossedBoundary += entryCrossedBoundary; PathRadius = 1f; Colour = new Color4(255, 255, 255, 127); @@ -153,11 +152,6 @@ namespace osu.Game.Rulesets.Osu.UI } } - private void entryCrossedBoundary(LifetimeEntry entry, LifetimeBoundaryKind kind, LifetimeBoundaryCrossingDirection direction) - { - - } - private sealed class AimLinePointComparator : IComparer { public int Compare(AimPointEntry? x, AimPointEntry? y) diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs index ea336e6067..a801e3d2f7 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs @@ -59,7 +59,7 @@ namespace osu.Game.Rulesets.Osu.UI judgementLayer = new JudgementContainer { RelativeSizeAxes = Axes.Both }, HitObjectContainer, judgementAboveHitObjectLayer = new Container { RelativeSizeAxes = Axes.Both }, - approachCircles = new ProxyContainer { RelativeSizeAxes = Axes.Both } + approachCircles = new ProxyContainer { RelativeSizeAxes = Axes.Both }, }; HitPolicy = new StartTimeOrderedHitPolicy(); From 29e5f409bac928bc49e1940a2e44b8ee4b8f2a70 Mon Sep 17 00:00:00 2001 From: Sheepposu Date: Tue, 27 Feb 2024 21:51:54 -0500 Subject: [PATCH 0247/2493] remove skinnable better to add skinnable elements later --- .../Resources/special-skin/aimmarker@2x.png | Bin 648 -> 0 bytes .../special-skin/hitmarker-left@2x.png | Bin 2127 -> 0 bytes .../special-skin/hitmarker-right@2x.png | Bin 1742 -> 0 bytes osu.Game.Rulesets.Osu/OsuSkinComponents.cs | 3 - .../Skinning/Default/DefaultHitMarker.cs | 74 ------------------ .../Skinning/Default/HitMarker.cs | 68 ++++++++++++---- .../Legacy/OsuLegacySkinTransformer.cs | 20 +---- .../UI/OsuAnalysisContainer.cs | 69 ++++++++-------- 8 files changed, 88 insertions(+), 146 deletions(-) delete mode 100644 osu.Game.Rulesets.Osu.Tests/Resources/special-skin/aimmarker@2x.png delete mode 100644 osu.Game.Rulesets.Osu.Tests/Resources/special-skin/hitmarker-left@2x.png delete mode 100644 osu.Game.Rulesets.Osu.Tests/Resources/special-skin/hitmarker-right@2x.png delete mode 100644 osu.Game.Rulesets.Osu/Skinning/Default/DefaultHitMarker.cs diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/aimmarker@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/aimmarker@2x.png deleted file mode 100644 index 0b2a554193f08f7984568980956a3db8a6b39800..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 648 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 FV1jr`?veli diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/hitmarker-right@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/hitmarker-right@2x.png deleted file mode 100644 index 66be9a80ee2e2ae4c6dc6915d2b7ca5cf8ab589f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1742 zcmV;<1~K`GP)EX>4Tx04R}tkv&MmKpe$iQ?)8p2Rn#3WT;NoK}8%(6^me@v=v%)FuC+YXws0R zxHt-~1qVMCs}3&Cx;nTDg5U>;vxAeOiUKFoAxFnR+6_CX>@2HM@dakSAh-}000D~Nkl1L9Yb|fH_ftE7RjjO?hFdLVhHsD6;Mi3NQZON`8V+<3BQA3EPF^Lyy zBFH5ssJYD~PVPNjt#956lhOc~_xg%spW#euk$w}6}R9gK=vhuEEC9o1$c~`Rf z(Ry2wlf>QYeKL21@Rg4>x`?ys*smS!IAyn7e8E7rV7}_2!Kl|YJi?#x1%84Syp!In zkY2Z>ZC|ASeH8V&R&~)}wqQQKV8CuUoJ!|IpZh27F_`+c=GVn>f!toVMTXssZ0rPq-2F z7)4*YhF6Y6=fW|AnK={TxGQO->3NOv?ZHqu?n-9PL^x&;=hC%on8f+eXCO0UEIMoi zC$QrUyo)<T*3ZV=R?f-~Sw)q8_7}ITNXzj(Ynz8XY#0nKO|Ffd3lJ+SQw? zo^T_5u}|1KI1!G!Qa#~D8k*bxlSMfdpVSn!s$M%F!q0GjixAm?KUED#k}2O6e=Ub! zzT9@~mdieLFWo14Y(4bY?{@Z~d#M9B__8N*HojnoWl8a{S^UD*#Oe5q57df^KXo6y zFS^)9_p=4_sqNTj>tdf)v)O`qjqaxoT%0!kCVZ*Rs)f?;&EU6Nn8-Z~eiR+BtjUAq zuj+95Y2#Stj`7sR9`G*v)Lv2)QZ=X0g)O!$}Y)kHjB1^&ZygL zaa;~xW2Fp;tl;b7xLnk27M%fmLZ@PB*b@vZ9}Jg6605gAXe zstYZ)p{)uZh6ZtROM+Y(5y>UDycZfo_&zMvtXfj*ar#}oX-JZ!IZ2XB>92W{iM*rM zPniAbKjTyQRE^sNDlJm64K~qIM5Tc?-B3Fj<2yzt(TD^gFHQ4B!vebI{%dk!P-X kiyU)`Hc@tO_2Ah*0YN2jCf7K0Z2$lO07*qoM6N<$f~iM7hyVZp diff --git a/osu.Game.Rulesets.Osu/OsuSkinComponents.cs b/osu.Game.Rulesets.Osu/OsuSkinComponents.cs index 75db18d345..52fdfea95f 100644 --- a/osu.Game.Rulesets.Osu/OsuSkinComponents.cs +++ b/osu.Game.Rulesets.Osu/OsuSkinComponents.cs @@ -22,8 +22,5 @@ namespace osu.Game.Rulesets.Osu SpinnerBody, CursorSmoke, ApproachCircle, - HitMarkerLeft, - HitMarkerRight, - AimMarker } } diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/DefaultHitMarker.cs b/osu.Game.Rulesets.Osu/Skinning/Default/DefaultHitMarker.cs deleted file mode 100644 index dc890d4d63..0000000000 --- a/osu.Game.Rulesets.Osu/Skinning/Default/DefaultHitMarker.cs +++ /dev/null @@ -1,74 +0,0 @@ -// 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.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; -using osuTK; - -namespace osu.Game.Rulesets.Osu.Skinning.Default -{ - public partial class DefaultHitMarker : CompositeDrawable - { - public DefaultHitMarker(OsuAction? action) - { - var (colour, length, hasBorder) = getConfig(action); - - if (hasBorder) - { - InternalChildren = new Drawable[] - { - new Box - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Size = new Vector2(3, length), - Colour = Colour4.Black.Opacity(0.5F) - }, - new Box - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Size = new Vector2(3, length), - Rotation = 90, - Colour = Colour4.Black.Opacity(0.5F) - } - }; - } - - AddRangeInternal(new Drawable[] - { - new Box - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Size = new Vector2(1, length), - Colour = colour - }, - new Box - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Size = new Vector2(1, length), - Rotation = 90, - Colour = colour - } - }); - } - - private (Colour4 colour, float length, bool hasBorder) getConfig(OsuAction? action) - { - switch (action) - { - case OsuAction.LeftButton: - return (Colour4.Orange, 20, true); - - case OsuAction.RightButton: - return (Colour4.LightGreen, 20, true); - - default: - return (Colour4.Gray.Opacity(0.5F), 8, false); - } - } - } -} diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/HitMarker.cs b/osu.Game.Rulesets.Osu/Skinning/Default/HitMarker.cs index 28877345d0..fe662470bc 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Default/HitMarker.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Default/HitMarker.cs @@ -1,37 +1,73 @@ // 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.Sprites; -using osu.Game.Skinning; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osuTK; namespace osu.Game.Rulesets.Osu.Skinning.Default { - public partial class HitMarker : Sprite + public partial class HitMarker : CompositeDrawable { - private readonly OsuAction? action; - - public HitMarker(OsuAction? action = null) + public HitMarker(OsuAction? action) { - this.action = action; + var (colour, length, hasBorder) = getConfig(action); + + if (hasBorder) + { + InternalChildren = new Drawable[] + { + new Box + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(3, length), + Colour = Colour4.Black.Opacity(0.5F) + }, + new Box + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(3, length), + Rotation = 90, + Colour = Colour4.Black.Opacity(0.5F) + } + }; + } + + AddRangeInternal(new Drawable[] + { + new Box + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(1, length), + Colour = colour + }, + new Box + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(1, length), + Rotation = 90, + Colour = colour + } + }); } - [BackgroundDependencyLoader] - private void load(ISkinSource skin) + private (Colour4 colour, float length, bool hasBorder) getConfig(OsuAction? action) { switch (action) { case OsuAction.LeftButton: - Texture = skin.GetTexture(@"hitmarker-left"); - break; + return (Colour4.Orange, 20, true); case OsuAction.RightButton: - Texture = skin.GetTexture(@"hitmarker-right"); - break; + return (Colour4.LightGreen, 20, true); default: - Texture = skin.GetTexture(@"aimmarker"); - break; + return (Colour4.Gray.Opacity(0.5F), 8, false); } } } diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs index 61f9eebd86..d2ebc68c52 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs @@ -6,7 +6,6 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Skinning; -using osu.Game.Rulesets.Osu.Skinning.Default; using osuTK; namespace osu.Game.Rulesets.Osu.Skinning.Legacy @@ -169,23 +168,8 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy return null; - case OsuSkinComponents.HitMarkerLeft: - if (GetTexture(@"hitmarker-left") != null) - return new HitMarker(OsuAction.LeftButton); - - return null; - - case OsuSkinComponents.HitMarkerRight: - if (GetTexture(@"hitmarker-right") != null) - return new HitMarker(OsuAction.RightButton); - - return null; - - case OsuSkinComponents.AimMarker: - if (GetTexture(@"aimmarker") != null) - return new HitMarker(); - - return null; + default: + throw new UnsupportedSkinComponentException(lookup); } } diff --git a/osu.Game.Rulesets.Osu/UI/OsuAnalysisContainer.cs b/osu.Game.Rulesets.Osu/UI/OsuAnalysisContainer.cs index c4b5135ca2..7d8ae6980c 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuAnalysisContainer.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuAnalysisContainer.cs @@ -14,7 +14,6 @@ using osu.Game.Rulesets.Objects.Pooling; using osu.Game.Rulesets.Osu.Replays; using osu.Game.Rulesets.Osu.Skinning.Default; using osu.Game.Rulesets.UI; -using osu.Game.Skinning; using osuTK; using osuTK.Graphics; @@ -26,40 +25,41 @@ namespace osu.Game.Rulesets.Osu.UI public Bindable AimMarkersEnabled = new BindableBool(); public Bindable AimLinesEnabled = new BindableBool(); - private HitMarkersContainer hitMarkersContainer; - private AimMarkersContainer aimMarkersContainer; - private AimLinesContainer aimLinesContainer; + protected HitMarkersContainer HitMarkers; + protected AimMarkersContainer AimMarkers; + protected AimLinesContainer AimLines; public OsuAnalysisContainer(Replay replay) : base(replay) { InternalChildren = new Drawable[] { - hitMarkersContainer = new HitMarkersContainer(), - aimMarkersContainer = new AimMarkersContainer() { Depth = float.MinValue }, - aimLinesContainer = new AimLinesContainer() { Depth = float.MaxValue } + HitMarkers = new HitMarkersContainer(), + AimMarkers = new AimMarkersContainer { Depth = float.MinValue }, + AimLines = new AimLinesContainer { Depth = float.MaxValue } }; - HitMarkerEnabled.ValueChanged += e => hitMarkersContainer.FadeTo(e.NewValue ? 1 : 0); - AimMarkersEnabled.ValueChanged += e => aimMarkersContainer.FadeTo(e.NewValue ? 1 : 0); - AimLinesEnabled.ValueChanged += e => aimLinesContainer.FadeTo(e.NewValue ? 1 : 0); + HitMarkerEnabled.ValueChanged += e => HitMarkers.FadeTo(e.NewValue ? 1 : 0); + AimMarkersEnabled.ValueChanged += e => AimMarkers.FadeTo(e.NewValue ? 1 : 0); + AimLinesEnabled.ValueChanged += e => AimLines.FadeTo(e.NewValue ? 1 : 0); } [BackgroundDependencyLoader] private void load() { - hitMarkersContainer.Hide(); - aimMarkersContainer.Hide(); - aimLinesContainer.Hide(); + HitMarkers.Hide(); + AimMarkers.Hide(); + AimLines.Hide(); bool leftHeld = false; bool rightHeld = false; + foreach (var frame in Replay.Frames) { var osuFrame = (OsuReplayFrame)frame; - aimMarkersContainer.Add(new AimPointEntry(osuFrame.Time, osuFrame.Position)); - aimLinesContainer.Add(new AimPointEntry(osuFrame.Time, osuFrame.Position)); + AimMarkers.Add(new AimPointEntry(osuFrame.Time, osuFrame.Position)); + AimLines.Add(new AimPointEntry(osuFrame.Time, osuFrame.Position)); bool leftButton = osuFrame.Actions.Contains(OsuAction.LeftButton); bool rightButton = osuFrame.Actions.Contains(OsuAction.RightButton); @@ -68,7 +68,7 @@ namespace osu.Game.Rulesets.Osu.UI leftHeld = false; else if (!leftHeld && leftButton) { - hitMarkersContainer.Add(new HitMarkerEntry(osuFrame.Time, osuFrame.Position, true)); + HitMarkers.Add(new HitMarkerEntry(osuFrame.Time, osuFrame.Position, true)); leftHeld = true; } @@ -76,42 +76,42 @@ namespace osu.Game.Rulesets.Osu.UI rightHeld = false; else if (!rightHeld && rightButton) { - hitMarkersContainer.Add(new HitMarkerEntry(osuFrame.Time, osuFrame.Position, false)); + HitMarkers.Add(new HitMarkerEntry(osuFrame.Time, osuFrame.Position, false)); rightHeld = true; } } } - private partial class HitMarkersContainer : PooledDrawableWithLifetimeContainer + protected partial class HitMarkersContainer : PooledDrawableWithLifetimeContainer { private readonly HitMarkerPool leftPool; private readonly HitMarkerPool rightPool; public HitMarkersContainer() { - AddInternal(leftPool = new HitMarkerPool(OsuSkinComponents.HitMarkerLeft, OsuAction.LeftButton, 15)); - AddInternal(rightPool = new HitMarkerPool(OsuSkinComponents.HitMarkerRight, OsuAction.RightButton, 15)); + AddInternal(leftPool = new HitMarkerPool(OsuAction.LeftButton, 15)); + AddInternal(rightPool = new HitMarkerPool(OsuAction.RightButton, 15)); } protected override HitMarkerDrawable GetDrawable(HitMarkerEntry entry) => (entry.IsLeftMarker ? leftPool : rightPool).Get(d => d.Apply(entry)); } - private partial class AimMarkersContainer : PooledDrawableWithLifetimeContainer + protected partial class AimMarkersContainer : PooledDrawableWithLifetimeContainer { private readonly HitMarkerPool pool; public AimMarkersContainer() { - AddInternal(pool = new HitMarkerPool(OsuSkinComponents.AimMarker, null, 80)); + AddInternal(pool = new HitMarkerPool(null, 80)); } protected override HitMarkerDrawable GetDrawable(AimPointEntry entry) => pool.Get(d => d.Apply(entry)); } - private partial class AimLinesContainer : Path + protected partial class AimLinesContainer : Path { - private LifetimeEntryManager lifetimeManager = new LifetimeEntryManager(); - private SortedSet aliveEntries = new SortedSet(new AimLinePointComparator()); + private readonly LifetimeEntryManager lifetimeManager = new LifetimeEntryManager(); + private readonly SortedSet aliveEntries = new SortedSet(new AimLinePointComparator()); public AimLinesContainer() { @@ -146,6 +146,7 @@ namespace osu.Game.Rulesets.Osu.UI private void updateVertices() { ClearVertices(); + foreach (var entry in aliveEntries) { AddVertex(entry.Position); @@ -164,7 +165,7 @@ namespace osu.Game.Rulesets.Osu.UI } } - private partial class HitMarkerDrawable : PoolableDrawableWithLifetime + protected partial class HitMarkerDrawable : PoolableDrawableWithLifetime { /// /// This constructor only exists to meet the new() type constraint of . @@ -173,10 +174,10 @@ namespace osu.Game.Rulesets.Osu.UI { } - public HitMarkerDrawable(OsuSkinComponents component, OsuAction? action) + public HitMarkerDrawable(OsuAction? action) { Origin = Anchor.Centre; - InternalChild = new SkinnableDrawable(new OsuSkinComponentLookup(component), _ => new DefaultHitMarker(action)); + InternalChild = new HitMarker(action); } protected override void OnApply(AimPointEntry entry) @@ -191,22 +192,20 @@ namespace osu.Game.Rulesets.Osu.UI } } - private partial class HitMarkerPool : DrawablePool + protected partial class HitMarkerPool : DrawablePool { - private readonly OsuSkinComponents component; private readonly OsuAction? action; - public HitMarkerPool(OsuSkinComponents component, OsuAction? action, int initialSize) + public HitMarkerPool(OsuAction? action, int initialSize) : base(initialSize) { - this.component = component; this.action = action; } - protected override HitMarkerDrawable CreateNewDrawable() => new HitMarkerDrawable(component, action); + protected override HitMarkerDrawable CreateNewDrawable() => new HitMarkerDrawable(action); } - private partial class AimPointEntry : LifetimeEntry + protected partial class AimPointEntry : LifetimeEntry { public Vector2 Position { get; } @@ -218,7 +217,7 @@ namespace osu.Game.Rulesets.Osu.UI } } - private partial class HitMarkerEntry : AimPointEntry + protected partial class HitMarkerEntry : AimPointEntry { public bool IsLeftMarker { get; } From cefc8357bbdb732f805e848b065afcf40619b9c0 Mon Sep 17 00:00:00 2001 From: Sheepposu Date: Tue, 27 Feb 2024 21:52:10 -0500 Subject: [PATCH 0248/2493] test scene for OsuAnalysisContainer --- .../TestSceneHitMarker.cs | 91 +++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 osu.Game.Rulesets.Osu.Tests/TestSceneHitMarker.cs diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneHitMarker.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneHitMarker.cs new file mode 100644 index 0000000000..e4c48f96b8 --- /dev/null +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneHitMarker.cs @@ -0,0 +1,91 @@ +// 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.Collections.Generic; +using System.Linq; +using NUnit.Framework; +using osu.Game.Replays; +using osu.Game.Rulesets.Osu.Replays; +using osu.Game.Rulesets.Osu.UI; +using osu.Game.Rulesets.Replays; +using osu.Game.Tests.Visual; +using osuTK; + +namespace osu.Game.Rulesets.Osu.Tests +{ + public partial class TestSceneHitMarker : OsuTestScene + { + private TestOsuAnalysisContainer analysisContainer; + + [Test] + public void TestHitMarkers() + { + createAnalysisContainer(); + AddStep("enable hit markers", () => analysisContainer.HitMarkerEnabled.Value = true); + AddAssert("hit markers visible", () => analysisContainer.HitMarkersVisible); + AddStep("disable hit markers", () => analysisContainer.HitMarkerEnabled.Value = false); + AddAssert("hit markers not visible", () => !analysisContainer.HitMarkersVisible); + } + + [Test] + public void TestAimMarker() + { + createAnalysisContainer(); + AddStep("enable aim markers", () => analysisContainer.AimMarkersEnabled.Value = true); + AddAssert("aim markers visible", () => analysisContainer.AimMarkersVisible); + AddStep("disable aim markers", () => analysisContainer.AimMarkersEnabled.Value = false); + AddAssert("aim markers not visible", () => !analysisContainer.AimMarkersVisible); + } + + [Test] + public void TestAimLines() + { + createAnalysisContainer(); + AddStep("enable aim lines", () => analysisContainer.AimLinesEnabled.Value = true); + AddAssert("aim lines visible", () => analysisContainer.AimLinesVisible); + AddStep("disable aim lines", () => analysisContainer.AimLinesEnabled.Value = false); + AddAssert("aim lines not visible", () => !analysisContainer.AimLinesVisible); + } + + private void createAnalysisContainer() + { + AddStep("create new analysis container", () => Child = analysisContainer = new TestOsuAnalysisContainer(fabricateReplay())); + } + + private Replay fabricateReplay() + { + var frames = new List(); + + for (int i = 0; i < 50; i++) + { + frames.Add(new OsuReplayFrame + { + Time = Time.Current + i * 15, + Position = new Vector2(20 + i * 10, 20), + Actions = + { + i % 2 == 0 ? OsuAction.LeftButton : OsuAction.RightButton + } + }); + } + + return new Replay { Frames = frames }; + } + + private partial class TestOsuAnalysisContainer : OsuAnalysisContainer + { + public TestOsuAnalysisContainer(Replay replay) + : base(replay) + { + } + + public bool HitMarkersVisible => HitMarkers.Alpha > 0 && HitMarkers.Entries.Any(); + + public bool AimMarkersVisible => AimMarkers.Alpha > 0 && AimMarkers.Entries.Any(); + + public bool AimLinesVisible => AimLines.Alpha > 0 && AimLines.Vertices.Count > 1; + } + } +} From 7687ab63edd2b49b4e1a5a2c30be2b9e2b8ca328 Mon Sep 17 00:00:00 2001 From: Sheepposu Date: Tue, 27 Feb 2024 22:03:45 -0500 Subject: [PATCH 0249/2493] fix code formatting --- osu.Game.Rulesets.Osu/UI/OsuAnalysisSettings.cs | 3 ++- osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs | 1 - osu.Game/Screens/Play/PlayerSettings/AnalysisSettings.cs | 2 +- osu.Game/Screens/Play/ReplayPlayer.cs | 1 + 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/UI/OsuAnalysisSettings.cs b/osu.Game.Rulesets.Osu/UI/OsuAnalysisSettings.cs index fd9cb67995..b3d5231ade 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuAnalysisSettings.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuAnalysisSettings.cs @@ -16,12 +16,13 @@ namespace osu.Game.Rulesets.Osu.UI private readonly PlayerCheckbox hitMarkerToggle; private readonly PlayerCheckbox aimMarkerToggle; - private readonly PlayerCheckbox hideCursorToggle; private readonly PlayerCheckbox aimLinesToggle; public OsuAnalysisSettings(DrawableRuleset drawableRuleset) : base(drawableRuleset) { + PlayerCheckbox hideCursorToggle; + Children = new Drawable[] { hitMarkerToggle = new PlayerCheckbox { LabelText = PlayerSettingsOverlayStrings.HitMarkers }, diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs index a801e3d2f7..411a02c5af 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs @@ -36,7 +36,6 @@ namespace osu.Game.Rulesets.Osu.UI private readonly JudgementPooler judgementPooler; public SmokeContainer Smoke { get; } - public FollowPointRenderer FollowPoints { get; } public static readonly Vector2 BASE_SIZE = new Vector2(512, 384); diff --git a/osu.Game/Screens/Play/PlayerSettings/AnalysisSettings.cs b/osu.Game/Screens/Play/PlayerSettings/AnalysisSettings.cs index 3752b2b900..91531b28b4 100644 --- a/osu.Game/Screens/Play/PlayerSettings/AnalysisSettings.cs +++ b/osu.Game/Screens/Play/PlayerSettings/AnalysisSettings.cs @@ -10,7 +10,7 @@ namespace osu.Game.Screens.Play.PlayerSettings { protected DrawableRuleset DrawableRuleset; - public AnalysisSettings(DrawableRuleset drawableRuleset) + protected AnalysisSettings(DrawableRuleset drawableRuleset) : base("Analysis Settings") { DrawableRuleset = drawableRuleset; diff --git a/osu.Game/Screens/Play/ReplayPlayer.cs b/osu.Game/Screens/Play/ReplayPlayer.cs index ce6cb5124a..81cc44d889 100644 --- a/osu.Game/Screens/Play/ReplayPlayer.cs +++ b/osu.Game/Screens/Play/ReplayPlayer.cs @@ -73,6 +73,7 @@ namespace osu.Game.Screens.Play HUDOverlay.PlayerSettingsOverlay.AddAtStart(playbackSettings); var analysisSettings = DrawableRuleset.Ruleset.CreateAnalysisSettings(DrawableRuleset); + if (analysisSettings != null) { HUDOverlay.PlayerSettingsOverlay.AddAtStart(analysisSettings); From f49aa4d8157eb6368263f919c662dd0afdaca1d0 Mon Sep 17 00:00:00 2001 From: Huo Yaoyuan Date: Wed, 28 Feb 2024 22:01:39 +0800 Subject: [PATCH 0250/2493] Parse points and segments for path string --- .../Objects/Legacy/ConvertHitObjectParser.cs | 57 ++++++++++++------- 1 file changed, 38 insertions(+), 19 deletions(-) diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs index f9e32fe26f..30e4101a84 100644 --- a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs @@ -266,30 +266,49 @@ namespace osu.Game.Rulesets.Objects.Legacy // This code takes on the responsibility of handling explicit segments of the path ("X" & "Y" from above). Implicit segments are handled by calls to convertPoints(). string[] pointSplit = pointString.Split('|'); - var controlPoints = new List>(); - int startIndex = 0; - int endIndex = 0; - bool first = true; + Span points = stackalloc Vector2[pointSplit.Length]; + Span<(PathType Type, int StartIndex)> segments = stackalloc (PathType Type, int StartIndex)[pointSplit.Length]; + int pointsCount = 0; + int segmentsCount = 0; - while (++endIndex < pointSplit.Length) + foreach (string s in pointSplit) { - // Keep incrementing endIndex while it's not the start of a new segment (indicated by having an alpha character at position 0). - if (!char.IsLetter(pointSplit[endIndex][0])) - continue; - - // Multi-segmented sliders DON'T contain the end point as part of the current segment as it's assumed to be the start of the next segment. - // The start of the next segment is the index after the type descriptor. - string endPoint = endIndex < pointSplit.Length - 1 ? pointSplit[endIndex + 1] : null; - - controlPoints.AddRange(convertPoints(pointSplit.AsMemory().Slice(startIndex, endIndex - startIndex), endPoint, first, offset)); - startIndex = endIndex; - first = false; + if (char.IsLetter(s[0])) + { + // The start of a new segment(indicated by having an alpha character at position 0). + var pathType = convertPathType(s); + segments[segmentsCount++] = (pathType, pointsCount); + } + else + { + points[pointsCount++] = readPoint(s, offset); + } } - if (endIndex > startIndex) - controlPoints.AddRange(convertPoints(pointSplit.AsMemory().Slice(startIndex, endIndex - startIndex), null, first, offset)); + var controlPoints = new List(pointsCount); - return mergePointsLists(controlPoints); + for (int i = 0; i < segmentsCount; i++) + { + int startIndex = segments[i].StartIndex; + int endIndex = i < segmentsCount - 1 ? segments[i + 1].StartIndex : pointsCount; + Vector2? endPoint = i < segmentsCount - 1 ? points[endIndex] : null; + controlPoints.AddRange(convertPoints(segments[i].Type, points[startIndex..endIndex], endPoint)); + } + + return controlPoints.ToArray(); + + static Vector2 readPoint(string value, Vector2 startPos) + { + string[] vertexSplit = value.Split(':'); + + Vector2 pos = new Vector2((int)Parsing.ParseDouble(vertexSplit[0], Parsing.MAX_COORDINATE_VALUE), (int)Parsing.ParseDouble(vertexSplit[1], Parsing.MAX_COORDINATE_VALUE)) - startPos; + return pos; + } + } + + private IEnumerable convertPoints(PathType type, ReadOnlySpan points, Vector2? endPoint) + { + throw new NotImplementedException(); } /// From 4bff54d35dbd60aab7cd8a255977fe95e5c3bde7 Mon Sep 17 00:00:00 2001 From: Huo Yaoyuan Date: Wed, 28 Feb 2024 22:37:14 +0800 Subject: [PATCH 0251/2493] Add ToString on PathControlPoint for debugging --- osu.Game/Rulesets/Objects/PathControlPoint.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/Rulesets/Objects/PathControlPoint.cs b/osu.Game/Rulesets/Objects/PathControlPoint.cs index ae9fa08085..1f8e63b269 100644 --- a/osu.Game/Rulesets/Objects/PathControlPoint.cs +++ b/osu.Game/Rulesets/Objects/PathControlPoint.cs @@ -76,5 +76,9 @@ namespace osu.Game.Rulesets.Objects } public bool Equals(PathControlPoint other) => Position == other?.Position && Type == other.Type; + + public override string ToString() => type is null + ? $"Position={Position}" + : $"Position={Position}, Type={type}"; } } From fe34577ee2938c45d295800405a309ad3d55ac3e Mon Sep 17 00:00:00 2001 From: Huo Yaoyuan Date: Wed, 28 Feb 2024 22:42:08 +0800 Subject: [PATCH 0252/2493] Update parsing. --- .../Objects/Legacy/ConvertHitObjectParser.cs | 86 ++++++------------- 1 file changed, 24 insertions(+), 62 deletions(-) diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs index 30e4101a84..2d4d2865e6 100644 --- a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs @@ -278,6 +278,10 @@ namespace osu.Game.Rulesets.Objects.Legacy // The start of a new segment(indicated by having an alpha character at position 0). var pathType = convertPathType(s); segments[segmentsCount++] = (pathType, pointsCount); + + // First segment is prepended by an extra zero point + if (pointsCount == 0) + points[pointsCount++] = Vector2.Zero; } else { @@ -306,47 +310,30 @@ namespace osu.Game.Rulesets.Objects.Legacy } } - private IEnumerable convertPoints(PathType type, ReadOnlySpan points, Vector2? endPoint) - { - throw new NotImplementedException(); - } - /// /// Converts a given point list into a set of path segments. /// + /// The path type of the point list. /// The point list. /// Any extra endpoint to consider as part of the points. This will NOT be returned. - /// Whether this is the first segment in the set. If true the first of the returned segments will contain a zero point. - /// The positional offset to apply to the control points. - /// The set of points contained by as one or more segments of the path, prepended by an extra zero point if is true. - private IEnumerable> convertPoints(ReadOnlyMemory points, string endPoint, bool first, Vector2 offset) + /// The set of points contained by as one or more segments of the path. + private IEnumerable convertPoints(PathType type, ReadOnlySpan points, Vector2? endPoint) { - PathType type = convertPathType(points.Span[0]); - - int readOffset = first ? 1 : 0; // First control point is zero for the first segment. - int readablePoints = points.Length - 1; // Total points readable from the base point span. - int endPointLength = endPoint != null ? 1 : 0; // Extra length if an endpoint is given that lies outside the base point span. - - var vertices = new PathControlPoint[readOffset + readablePoints + endPointLength]; - - // Fill any non-read points. - for (int i = 0; i < readOffset; i++) - vertices[i] = new PathControlPoint(); + var vertices = new PathControlPoint[points.Length]; + var result = new List(); // Parse into control points. - for (int i = 1; i < points.Length; i++) - readPoint(points.Span[i], offset, out vertices[readOffset + i - 1]); - - // If an endpoint is given, add it to the end. - if (endPoint != null) - readPoint(endPoint, offset, out vertices[^1]); + for (int i = 0; i < points.Length; i++) + vertices[i] = new PathControlPoint { Position = points[i] }; // Edge-case rules (to match stable). if (type == PathType.PERFECT_CURVE) { - if (vertices.Length != 3) + int endPointLength = endPoint is null ? 0 : 1; + + if (vertices.Length + endPointLength != 3) type = PathType.BEZIER; - else if (isLinear(vertices)) + else if (isLinear(stackalloc[] { points[0], points[1], endPoint ?? points[2] })) { // osu-stable special-cased colinear perfect curves to a linear path type = PathType.LINEAR; @@ -365,7 +352,7 @@ namespace osu.Game.Rulesets.Objects.Legacy int startIndex = 0; int endIndex = 0; - while (++endIndex < vertices.Length - endPointLength) + while (++endIndex < vertices.Length) { // Keep incrementing while an implicit segment doesn't need to be started. if (vertices[endIndex].Position != vertices[endIndex - 1].Position) @@ -378,50 +365,25 @@ namespace osu.Game.Rulesets.Objects.Legacy continue; // The last control point of each segment is not allowed to start a new implicit segment. - if (endIndex == vertices.Length - endPointLength - 1) + if (endIndex == vertices.Length - 1) continue; // Force a type on the last point, and return the current control point set as a segment. vertices[endIndex - 1].Type = type; - yield return vertices.AsMemory().Slice(startIndex, endIndex - startIndex); + for (int i = startIndex; i < endIndex; i++) + result.Add(vertices[i]); // Skip the current control point - as it's the same as the one that's just been returned. startIndex = endIndex + 1; } - if (endIndex > startIndex) - yield return vertices.AsMemory().Slice(startIndex, endIndex - startIndex); + for (int i = startIndex; i < endIndex; i++) + result.Add(vertices[i]); - static void readPoint(string value, Vector2 startPos, out PathControlPoint point) - { - string[] vertexSplit = value.Split(':'); + return result; - Vector2 pos = new Vector2((int)Parsing.ParseDouble(vertexSplit[0], Parsing.MAX_COORDINATE_VALUE), (int)Parsing.ParseDouble(vertexSplit[1], Parsing.MAX_COORDINATE_VALUE)) - startPos; - point = new PathControlPoint { Position = pos }; - } - - static bool isLinear(PathControlPoint[] p) => Precision.AlmostEquals(0, (p[1].Position.Y - p[0].Position.Y) * (p[2].Position.X - p[0].Position.X) - - (p[1].Position.X - p[0].Position.X) * (p[2].Position.Y - p[0].Position.Y)); - } - - private PathControlPoint[] mergePointsLists(List> controlPointList) - { - int totalCount = 0; - - foreach (var arr in controlPointList) - totalCount += arr.Length; - - var mergedArray = new PathControlPoint[totalCount]; - var mergedArrayMemory = mergedArray.AsMemory(); - int copyIndex = 0; - - foreach (var arr in controlPointList) - { - arr.CopyTo(mergedArrayMemory.Slice(copyIndex)); - copyIndex += arr.Length; - } - - return mergedArray; + static bool isLinear(ReadOnlySpan p) => Precision.AlmostEquals(0, (p[1].Y - p[0].Y) * (p[2].X - p[0].X) + - (p[1].X - p[0].X) * (p[2].Y - p[0].Y)); } /// From bcb91f348d746bd4470240c23c72ec812fc54113 Mon Sep 17 00:00:00 2001 From: Huo Yaoyuan Date: Wed, 28 Feb 2024 22:51:36 +0800 Subject: [PATCH 0253/2493] Use ArrayPool instead of stackalloc --- .../Objects/Legacy/ConvertHitObjectParser.cs | 82 ++++++++++--------- 1 file changed, 44 insertions(+), 38 deletions(-) diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs index 2d4d2865e6..a3ca719ff9 100644 --- a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs @@ -18,6 +18,7 @@ using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.Legacy; using osu.Game.Skinning; using osu.Game.Utils; +using System.Buffers; namespace osu.Game.Rulesets.Objects.Legacy { @@ -266,41 +267,49 @@ namespace osu.Game.Rulesets.Objects.Legacy // This code takes on the responsibility of handling explicit segments of the path ("X" & "Y" from above). Implicit segments are handled by calls to convertPoints(). string[] pointSplit = pointString.Split('|'); - Span points = stackalloc Vector2[pointSplit.Length]; - Span<(PathType Type, int StartIndex)> segments = stackalloc (PathType Type, int StartIndex)[pointSplit.Length]; + var points = ArrayPool.Shared.Rent(pointSplit.Length); + var segments = ArrayPool<(PathType Type, int StartIndex)>.Shared.Rent(pointSplit.Length); int pointsCount = 0; int segmentsCount = 0; - - foreach (string s in pointSplit) + try { - if (char.IsLetter(s[0])) - { - // The start of a new segment(indicated by having an alpha character at position 0). - var pathType = convertPathType(s); - segments[segmentsCount++] = (pathType, pointsCount); - // First segment is prepended by an extra zero point - if (pointsCount == 0) - points[pointsCount++] = Vector2.Zero; - } - else + foreach (string s in pointSplit) { - points[pointsCount++] = readPoint(s, offset); + if (char.IsLetter(s[0])) + { + // The start of a new segment(indicated by having an alpha character at position 0). + var pathType = convertPathType(s); + segments[segmentsCount++] = (pathType, pointsCount); + + // First segment is prepended by an extra zero point + if (pointsCount == 0) + points[pointsCount++] = Vector2.Zero; + } + else + { + points[pointsCount++] = readPoint(s, offset); + } } + + var controlPoints = new List>(pointsCount); + + for (int i = 0; i < segmentsCount; i++) + { + int startIndex = segments[i].StartIndex; + int endIndex = i < segmentsCount - 1 ? segments[i + 1].StartIndex : pointsCount; + Vector2? endPoint = i < segmentsCount - 1 ? points[endIndex] : null; + controlPoints.AddRange(convertPoints(segments[i].Type, new ArraySegment(points, startIndex, endIndex - startIndex), endPoint)); + } + + return controlPoints.SelectMany(s => s).ToArray(); } - - var controlPoints = new List(pointsCount); - - for (int i = 0; i < segmentsCount; i++) + finally { - int startIndex = segments[i].StartIndex; - int endIndex = i < segmentsCount - 1 ? segments[i + 1].StartIndex : pointsCount; - Vector2? endPoint = i < segmentsCount - 1 ? points[endIndex] : null; - controlPoints.AddRange(convertPoints(segments[i].Type, points[startIndex..endIndex], endPoint)); + ArrayPool.Shared.Return(points); + ArrayPool<(PathType Type, int StartIndex)>.Shared.Return(segments); } - return controlPoints.ToArray(); - static Vector2 readPoint(string value, Vector2 startPos) { string[] vertexSplit = value.Split(':'); @@ -317,13 +326,12 @@ namespace osu.Game.Rulesets.Objects.Legacy /// The point list. /// Any extra endpoint to consider as part of the points. This will NOT be returned. /// The set of points contained by as one or more segments of the path. - private IEnumerable convertPoints(PathType type, ReadOnlySpan points, Vector2? endPoint) + private IEnumerable> convertPoints(PathType type, ArraySegment points, Vector2? endPoint) { - var vertices = new PathControlPoint[points.Length]; - var result = new List(); + var vertices = new PathControlPoint[points.Count]; // Parse into control points. - for (int i = 0; i < points.Length; i++) + for (int i = 0; i < points.Count; i++) vertices[i] = new PathControlPoint { Position = points[i] }; // Edge-case rules (to match stable). @@ -333,7 +341,7 @@ namespace osu.Game.Rulesets.Objects.Legacy if (vertices.Length + endPointLength != 3) type = PathType.BEZIER; - else if (isLinear(stackalloc[] { points[0], points[1], endPoint ?? points[2] })) + else if (isLinear(points[0], points[1], endPoint ?? points[2])) { // osu-stable special-cased colinear perfect curves to a linear path type = PathType.LINEAR; @@ -370,20 +378,18 @@ namespace osu.Game.Rulesets.Objects.Legacy // Force a type on the last point, and return the current control point set as a segment. vertices[endIndex - 1].Type = type; - for (int i = startIndex; i < endIndex; i++) - result.Add(vertices[i]); + yield return new ArraySegment(vertices, startIndex, endIndex - startIndex); // Skip the current control point - as it's the same as the one that's just been returned. startIndex = endIndex + 1; } - for (int i = startIndex; i < endIndex; i++) - result.Add(vertices[i]); + if (startIndex < endIndex) + yield return new ArraySegment(vertices, startIndex, endIndex - startIndex); - return result; - - static bool isLinear(ReadOnlySpan p) => Precision.AlmostEquals(0, (p[1].Y - p[0].Y) * (p[2].X - p[0].X) - - (p[1].X - p[0].X) * (p[2].Y - p[0].Y)); + static bool isLinear(Vector2 p0, Vector2 p1, Vector2 p2) + => Precision.AlmostEquals(0, (p1.Y - p0.Y) * (p2.X - p0.X) + - (p1.X - p0.X) * (p2.Y - p0.Y)); } /// From 470d2be2e1b4043953e067ef91e4c997138cc0ef Mon Sep 17 00:00:00 2001 From: Huo Yaoyuan Date: Thu, 29 Feb 2024 00:07:00 +0800 Subject: [PATCH 0254/2493] The overhead of LINQ is not ignorable --- .../Objects/Legacy/ConvertHitObjectParser.cs | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs index a3ca719ff9..72053648a0 100644 --- a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs @@ -302,7 +302,7 @@ namespace osu.Game.Rulesets.Objects.Legacy controlPoints.AddRange(convertPoints(segments[i].Type, new ArraySegment(points, startIndex, endIndex - startIndex), endPoint)); } - return controlPoints.SelectMany(s => s).ToArray(); + return mergePointsLists(controlPoints); } finally { @@ -392,6 +392,25 @@ namespace osu.Game.Rulesets.Objects.Legacy - (p1.X - p0.X) * (p2.Y - p0.Y)); } + private PathControlPoint[] mergePointsLists(List> controlPointList) + { + int totalCount = 0; + + foreach (var arr in controlPointList) + totalCount += arr.Count; + + var mergedArray = new PathControlPoint[totalCount]; + int copyIndex = 0; + + foreach (var arr in controlPointList) + { + arr.AsSpan().CopyTo(mergedArray.AsSpan(copyIndex)); + copyIndex += arr.Count; + } + + return mergedArray; + } + /// /// Creates a legacy Hit-type hit object. /// From e86ebd6cdba85e506a771aeab0fdeb0a71d74fab Mon Sep 17 00:00:00 2001 From: Huo Yaoyuan Date: Thu, 29 Feb 2024 00:24:24 +0800 Subject: [PATCH 0255/2493] Fix formatting --- osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs index 72053648a0..f042e6ba26 100644 --- a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs @@ -271,9 +271,9 @@ namespace osu.Game.Rulesets.Objects.Legacy var segments = ArrayPool<(PathType Type, int StartIndex)>.Shared.Rent(pointSplit.Length); int pointsCount = 0; int segmentsCount = 0; + try { - foreach (string s in pointSplit) { if (char.IsLetter(s[0])) From f28f19ed7eeee1ecacc909f0f4f35edb07c7b8cb Mon Sep 17 00:00:00 2001 From: Huo Yaoyuan Date: Thu, 29 Feb 2024 10:47:16 +0800 Subject: [PATCH 0256/2493] Fix method indent size Co-authored-by: Salman Ahmed --- osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs index f042e6ba26..db1fbe3fa4 100644 --- a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs @@ -389,7 +389,7 @@ namespace osu.Game.Rulesets.Objects.Legacy static bool isLinear(Vector2 p0, Vector2 p1, Vector2 p2) => Precision.AlmostEquals(0, (p1.Y - p0.Y) * (p2.X - p0.X) - - (p1.X - p0.X) * (p2.Y - p0.Y)); + - (p1.X - p0.X) * (p2.Y - p0.Y)); } private PathControlPoint[] mergePointsLists(List> controlPointList) From a11e63b184acc5030e3d167f13d85a3691f0b7dc Mon Sep 17 00:00:00 2001 From: Huo Yaoyuan Date: Thu, 29 Feb 2024 20:02:04 +0800 Subject: [PATCH 0257/2493] Make the code more clear --- .../Objects/Legacy/ConvertHitObjectParser.cs | 42 ++++++++++++------- 1 file changed, 26 insertions(+), 16 deletions(-) diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs index db1fbe3fa4..2b058d5e1f 100644 --- a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs @@ -265,49 +265,59 @@ namespace osu.Game.Rulesets.Objects.Legacy private PathControlPoint[] convertPathString(string pointString, Vector2 offset) { // This code takes on the responsibility of handling explicit segments of the path ("X" & "Y" from above). Implicit segments are handled by calls to convertPoints(). - string[] pointSplit = pointString.Split('|'); + string[] pointStringSplit = pointString.Split('|'); - var points = ArrayPool.Shared.Rent(pointSplit.Length); - var segments = ArrayPool<(PathType Type, int StartIndex)>.Shared.Rent(pointSplit.Length); - int pointsCount = 0; - int segmentsCount = 0; + var pointsBuffer = ArrayPool.Shared.Rent(pointStringSplit.Length); + var segmentsBuffer = ArrayPool<(PathType Type, int StartIndex)>.Shared.Rent(pointStringSplit.Length); + int currentPointsIndex = 0; + int currentSegmentsIndex = 0; try { - foreach (string s in pointSplit) + foreach (string s in pointStringSplit) { if (char.IsLetter(s[0])) { // The start of a new segment(indicated by having an alpha character at position 0). var pathType = convertPathType(s); - segments[segmentsCount++] = (pathType, pointsCount); + segmentsBuffer[currentSegmentsIndex++] = (pathType, currentPointsIndex); // First segment is prepended by an extra zero point - if (pointsCount == 0) - points[pointsCount++] = Vector2.Zero; + if (currentPointsIndex == 0) + pointsBuffer[currentPointsIndex++] = Vector2.Zero; } else { - points[pointsCount++] = readPoint(s, offset); + pointsBuffer[currentPointsIndex++] = readPoint(s, offset); } } + int pointsCount = currentPointsIndex; + int segmentsCount = currentSegmentsIndex; var controlPoints = new List>(pointsCount); + var allPoints = new ArraySegment(pointsBuffer, 0, pointsCount); for (int i = 0; i < segmentsCount; i++) { - int startIndex = segments[i].StartIndex; - int endIndex = i < segmentsCount - 1 ? segments[i + 1].StartIndex : pointsCount; - Vector2? endPoint = i < segmentsCount - 1 ? points[endIndex] : null; - controlPoints.AddRange(convertPoints(segments[i].Type, new ArraySegment(points, startIndex, endIndex - startIndex), endPoint)); + if (i < segmentsCount - 1) + { + int startIndex = segmentsBuffer[i].StartIndex; + int endIndex = segmentsBuffer[i + 1].StartIndex; + controlPoints.AddRange(convertPoints(segmentsBuffer[i].Type, allPoints[startIndex..endIndex], pointsBuffer[endIndex])); + } + else + { + int startIndex = segmentsBuffer[i].StartIndex; + controlPoints.AddRange(convertPoints(segmentsBuffer[i].Type, allPoints[startIndex..], null)); + } } return mergePointsLists(controlPoints); } finally { - ArrayPool.Shared.Return(points); - ArrayPool<(PathType Type, int StartIndex)>.Shared.Return(segments); + ArrayPool.Shared.Return(pointsBuffer); + ArrayPool<(PathType, int)>.Shared.Return(segmentsBuffer); } static Vector2 readPoint(string value, Vector2 startPos) From 060e17e9898256128632ab1a524ab33c87d0b9c2 Mon Sep 17 00:00:00 2001 From: jvyden Date: Thu, 29 Feb 2024 19:57:32 -0500 Subject: [PATCH 0258/2493] Support Discord game invites in multiplayer lobbies --- osu.Desktop/DiscordRichPresence.cs | 83 +++++++++++++++++++++++++++++- 1 file changed, 81 insertions(+), 2 deletions(-) diff --git a/osu.Desktop/DiscordRichPresence.cs b/osu.Desktop/DiscordRichPresence.cs index f0da708766..b85abdb4fe 100644 --- a/osu.Desktop/DiscordRichPresence.cs +++ b/osu.Desktop/DiscordRichPresence.cs @@ -9,10 +9,13 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Logging; +using osu.Game; using osu.Game.Configuration; using osu.Game.Extensions; using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; +using osu.Game.Online.Multiplayer; +using osu.Game.Online.Rooms; using osu.Game.Rulesets; using osu.Game.Users; using LogLevel = osu.Framework.Logging.LogLevel; @@ -22,6 +25,7 @@ namespace osu.Desktop internal partial class DiscordRichPresence : Component { private const string client_id = "367827983903490050"; + public const string DISCORD_PROTOCOL = $"discord-{client_id}://"; private DiscordRpcClient client = null!; @@ -33,6 +37,12 @@ namespace osu.Desktop [Resolved] private IAPIProvider api { get; set; } = null!; + [Resolved] + private OsuGame game { get; set; } = null!; + + [Resolved] + private MultiplayerClient multiplayerClient { get; set; } = null!; + private readonly IBindable status = new Bindable(); private readonly IBindable activity = new Bindable(); @@ -40,7 +50,12 @@ namespace osu.Desktop private readonly RichPresence presence = new RichPresence { - Assets = new Assets { LargeImageKey = "osu_logo_lazer", } + Assets = new Assets { LargeImageKey = "osu_logo_lazer" }, + Secrets = new Secrets + { + JoinSecret = null, + SpectateSecret = null, + }, }; [BackgroundDependencyLoader] @@ -52,8 +67,14 @@ namespace osu.Desktop }; client.OnReady += onReady; + client.OnError += (_, e) => Logger.Log($"An error occurred with Discord RPC Client: {e.Code} {e.Message}", LoggingTarget.Network, LogLevel.Error); - client.OnError += (_, e) => Logger.Log($"An error occurred with Discord RPC Client: {e.Code} {e.Message}", LoggingTarget.Network); + // set up stuff for spectate/join + // first, we register a uri scheme for when osu! isn't running and a user clicks join/spectate + // the rpc library we use also happens to _require_ that we do this + client.RegisterUriScheme(); + client.Subscribe(EventType.Join); // we have to explicitly tell discord to send us join events. + client.OnJoin += onJoin; config.BindWith(OsuSetting.DiscordRichPresence, privacyMode); @@ -114,6 +135,28 @@ namespace osu.Desktop { presence.Buttons = null; } + + if (!hideIdentifiableInformation && multiplayerClient.Room != null) + { + MultiplayerRoom room = multiplayerClient.Room; + presence.Party = new Party + { + Privacy = string.IsNullOrEmpty(room.Settings.Password) ? Party.PrivacySetting.Public : Party.PrivacySetting.Private, + ID = room.RoomID.ToString(), + // technically lobbies can have infinite users, but Discord needs this to be set to something. + // 1024 just happens to look nice. + // https://discord.com/channels/188630481301012481/188630652340404224/1212967974793642034 + Max = 1024, + Size = room.Users.Count, + }; + + presence.Secrets.JoinSecret = $"{room.RoomID}:{room.Settings.Password}"; + } + else + { + presence.Party = null; + presence.Secrets.JoinSecret = null; + } } else { @@ -139,6 +182,22 @@ namespace osu.Desktop client.SetPresence(presence); } + private void onJoin(object sender, JoinMessage args) + { + game.Window?.Raise(); // users will expect to be brought back to osu! when joining a lobby from discord + + if (!tryParseRoomSecret(args.Secret, out long roomId, out string? password)) + Logger.Log("Failed to parse the room secret Discord gave us", LoggingTarget.Network, LogLevel.Error); + + var request = new GetRoomRequest(roomId); + request.Success += room => Schedule(() => + { + game.PresentMultiplayerMatch(room, password); + }); + request.Failure += _ => Logger.Log("Couldn't find the room Discord gave us", LoggingTarget.Network, LogLevel.Error); + api.Queue(request); + } + private static readonly int ellipsis_length = Encoding.UTF8.GetByteCount(new[] { '…' }); private string truncate(string str) @@ -160,6 +219,26 @@ namespace osu.Desktop }); } + private static bool tryParseRoomSecret(ReadOnlySpan secret, out long roomId, out string? password) + { + roomId = 0; + password = null; + + int roomSecretSplitIndex = secret.IndexOf(':'); + + if (roomSecretSplitIndex == -1) + return false; + + if (!long.TryParse(secret[..roomSecretSplitIndex], out roomId)) + return false; + + // just convert to string here, we're going to have to alloc it later anyways + password = secret[(roomSecretSplitIndex + 1)..].ToString(); + if (password.Length == 0) password = null; + + return true; + } + private int? getBeatmapID(UserActivity activity) { switch (activity) From 92235e7789271da1080f6f8f635e52f5f8490002 Mon Sep 17 00:00:00 2001 From: jvyden Date: Fri, 1 Mar 2024 00:02:20 -0500 Subject: [PATCH 0259/2493] Make truncate and getBeatmapID static Code quality was complaining about hidden variables so I opted for this solution. --- osu.Desktop/DiscordRichPresence.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Desktop/DiscordRichPresence.cs b/osu.Desktop/DiscordRichPresence.cs index b85abdb4fe..91f7f6e1da 100644 --- a/osu.Desktop/DiscordRichPresence.cs +++ b/osu.Desktop/DiscordRichPresence.cs @@ -200,7 +200,7 @@ namespace osu.Desktop private static readonly int ellipsis_length = Encoding.UTF8.GetByteCount(new[] { '…' }); - private string truncate(string str) + private static string truncate(string str) { if (Encoding.UTF8.GetByteCount(str) <= 128) return str; @@ -239,7 +239,7 @@ namespace osu.Desktop return true; } - private int? getBeatmapID(UserActivity activity) + private static int? getBeatmapID(UserActivity activity) { switch (activity) { From 37e7a4dea7f957231a4eb2aaf9e95654ab4d711e Mon Sep 17 00:00:00 2001 From: Jayden Date: Fri, 1 Mar 2024 14:32:44 -0500 Subject: [PATCH 0260/2493] Fix yapping Co-authored-by: Salman Ahmed --- osu.Desktop/DiscordRichPresence.cs | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/osu.Desktop/DiscordRichPresence.cs b/osu.Desktop/DiscordRichPresence.cs index 91f7f6e1da..035add8044 100644 --- a/osu.Desktop/DiscordRichPresence.cs +++ b/osu.Desktop/DiscordRichPresence.cs @@ -25,7 +25,6 @@ namespace osu.Desktop internal partial class DiscordRichPresence : Component { private const string client_id = "367827983903490050"; - public const string DISCORD_PROTOCOL = $"discord-{client_id}://"; private DiscordRpcClient client = null!; @@ -69,11 +68,9 @@ namespace osu.Desktop client.OnReady += onReady; client.OnError += (_, e) => Logger.Log($"An error occurred with Discord RPC Client: {e.Code} {e.Message}", LoggingTarget.Network, LogLevel.Error); - // set up stuff for spectate/join - // first, we register a uri scheme for when osu! isn't running and a user clicks join/spectate - // the rpc library we use also happens to _require_ that we do this + // A URI scheme is required to support game invitations, as well as informing Discord of the game executable path to support launching the game when a user clicks on join/spectate. client.RegisterUriScheme(); - client.Subscribe(EventType.Join); // we have to explicitly tell discord to send us join events. + client.Subscribe(EventType.Join); client.OnJoin += onJoin; config.BindWith(OsuSetting.DiscordRichPresence, privacyMode); @@ -184,7 +181,7 @@ namespace osu.Desktop private void onJoin(object sender, JoinMessage args) { - game.Window?.Raise(); // users will expect to be brought back to osu! when joining a lobby from discord + game.Window?.Raise(); if (!tryParseRoomSecret(args.Secret, out long roomId, out string? password)) Logger.Log("Failed to parse the room secret Discord gave us", LoggingTarget.Network, LogLevel.Error); @@ -232,7 +229,6 @@ namespace osu.Desktop if (!long.TryParse(secret[..roomSecretSplitIndex], out roomId)) return false; - // just convert to string here, we're going to have to alloc it later anyways password = secret[(roomSecretSplitIndex + 1)..].ToString(); if (password.Length == 0) password = null; From c05007804fb715b5d542535bcb224d5bd67b8b58 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Sun, 3 Mar 2024 20:46:58 +0300 Subject: [PATCH 0261/2493] Use more direct way to apply transforms --- .../Formats/LegacyStoryboardDecoder.cs | 2 +- osu.Game/Storyboards/CommandTimeline.cs | 10 ++- osu.Game/Storyboards/CommandTimelineGroup.cs | 23 +++--- osu.Game/Storyboards/StoryboardSprite.cs | 77 +++++++++++++------ 4 files changed, 75 insertions(+), 37 deletions(-) diff --git a/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs index cf4700bf85..a9a4d9cc49 100644 --- a/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs @@ -212,7 +212,7 @@ namespace osu.Game.Beatmaps.Formats { float startValue = Parsing.ParseFloat(split[4]); float endValue = split.Length > 5 ? Parsing.ParseFloat(split[5]) : startValue; - timelineGroup?.Scale.Add(easing, startTime, endTime, startValue, endValue); + timelineGroup?.Scale.Add(easing, startTime, endTime, new Vector2(startValue), new Vector2(endValue)); break; } diff --git a/osu.Game/Storyboards/CommandTimeline.cs b/osu.Game/Storyboards/CommandTimeline.cs index 0650c97165..2bb606d2bb 100644 --- a/osu.Game/Storyboards/CommandTimeline.cs +++ b/osu.Game/Storyboards/CommandTimeline.cs @@ -24,6 +24,13 @@ namespace osu.Game.Storyboards public T StartValue { get; private set; } public T EndValue { get; private set; } + public string PropertyName { get; } + + public CommandTimeline(string propertyName) + { + PropertyName = propertyName; + } + public void Add(Easing easing, double startTime, double endTime, T startValue, T endValue) { if (endTime < startTime) @@ -31,7 +38,7 @@ namespace osu.Game.Storyboards endTime = startTime; } - commands.Add(new TypedCommand { Easing = easing, StartTime = startTime, EndTime = endTime, StartValue = startValue, EndValue = endValue }); + commands.Add(new TypedCommand { Easing = easing, StartTime = startTime, EndTime = endTime, StartValue = startValue, EndValue = endValue, PropertyName = PropertyName }); if (startTime < StartTime) { @@ -55,6 +62,7 @@ namespace osu.Game.Storyboards public double StartTime { get; set; } public double EndTime { get; set; } public double Duration => EndTime - StartTime; + public string PropertyName { get; set; } public T StartValue; public T EndValue; diff --git a/osu.Game/Storyboards/CommandTimelineGroup.cs b/osu.Game/Storyboards/CommandTimelineGroup.cs index 0b96db6861..cb795e0ffe 100644 --- a/osu.Game/Storyboards/CommandTimelineGroup.cs +++ b/osu.Game/Storyboards/CommandTimelineGroup.cs @@ -3,11 +3,11 @@ using System; using osuTK; -using osuTK.Graphics; using osu.Framework.Graphics; using System.Collections.Generic; using System.Linq; using Newtonsoft.Json; +using osu.Framework.Graphics.Colour; namespace osu.Game.Storyboards { @@ -15,16 +15,16 @@ namespace osu.Game.Storyboards public class CommandTimelineGroup { - public CommandTimeline X = new CommandTimeline(); - public CommandTimeline Y = new CommandTimeline(); - public CommandTimeline Scale = new CommandTimeline(); - public CommandTimeline VectorScale = new CommandTimeline(); - public CommandTimeline Rotation = new CommandTimeline(); - public CommandTimeline Colour = new CommandTimeline(); - public CommandTimeline Alpha = new CommandTimeline(); - public CommandTimeline BlendingParameters = new CommandTimeline(); - public CommandTimeline FlipH = new CommandTimeline(); - public CommandTimeline FlipV = new CommandTimeline(); + public CommandTimeline X = new CommandTimeline("X"); + public CommandTimeline Y = new CommandTimeline("Y"); + public CommandTimeline Scale = new CommandTimeline("Scale"); + public CommandTimeline VectorScale = new CommandTimeline("VectorScale"); + public CommandTimeline Rotation = new CommandTimeline("Rotation"); + public CommandTimeline Colour = new CommandTimeline("Colour"); + public CommandTimeline Alpha = new CommandTimeline("Alpha"); + public CommandTimeline BlendingParameters = new CommandTimeline("Blending"); + public CommandTimeline FlipH = new CommandTimeline("FlipH"); + public CommandTimeline FlipV = new CommandTimeline("FlipV"); private readonly ICommandTimeline[] timelines; @@ -109,6 +109,7 @@ namespace osu.Game.Storyboards EndTime = offset + command.EndTime, StartValue = command.StartValue, EndValue = command.EndValue, + PropertyName = command.PropertyName }); } diff --git a/osu.Game/Storyboards/StoryboardSprite.cs b/osu.Game/Storyboards/StoryboardSprite.cs index 982185d51b..4d3f1c158f 100644 --- a/osu.Game/Storyboards/StoryboardSprite.cs +++ b/osu.Game/Storyboards/StoryboardSprite.cs @@ -98,8 +98,6 @@ namespace osu.Game.Storyboards private delegate void DrawablePropertyInitializer(Drawable drawable, T value); - private delegate void DrawableTransformer(Drawable drawable, T value, double duration, Easing easing); - public StoryboardSprite(string path, Anchor origin, Vector2 initialPosition) { Path = path; @@ -132,27 +130,23 @@ namespace osu.Game.Storyboards List generated = new List(); - generateCommands(generated, getCommands(g => g.X, triggeredGroups), (d, value) => d.X = value, (d, value, duration, easing) => d.MoveToX(value, duration, easing)); - generateCommands(generated, getCommands(g => g.Y, triggeredGroups), (d, value) => d.Y = value, (d, value, duration, easing) => d.MoveToY(value, duration, easing)); - generateCommands(generated, getCommands(g => g.Scale, triggeredGroups), (d, value) => d.Scale = new Vector2(value), (d, value, duration, easing) => d.ScaleTo(value, duration, easing)); - generateCommands(generated, getCommands(g => g.Rotation, triggeredGroups), (d, value) => d.Rotation = value, (d, value, duration, easing) => d.RotateTo(value, duration, easing)); - generateCommands(generated, getCommands(g => g.Colour, triggeredGroups), (d, value) => d.Colour = value, (d, value, duration, easing) => d.FadeColour(value, duration, easing)); - generateCommands(generated, getCommands(g => g.Alpha, triggeredGroups), (d, value) => d.Alpha = value, (d, value, duration, easing) => d.FadeTo(value, duration, easing)); - generateCommands(generated, getCommands(g => g.BlendingParameters, triggeredGroups), (d, value) => d.Blending = value, (d, value, duration, _) => d.TransformBlendingMode(value, duration), - false); + generateCommands(generated, getCommands(g => g.X, triggeredGroups), (d, value) => d.X = value); + generateCommands(generated, getCommands(g => g.Y, triggeredGroups), (d, value) => d.Y = value); + generateCommands(generated, getCommands(g => g.Scale, triggeredGroups), (d, value) => d.Scale = value); + generateCommands(generated, getCommands(g => g.Rotation, triggeredGroups), (d, value) => d.Rotation = value); + generateCommands(generated, getCommands(g => g.Colour, triggeredGroups), (d, value) => d.Colour = value); + generateCommands(generated, getCommands(g => g.Alpha, triggeredGroups), (d, value) => d.Alpha = value); + generateCommands(generated, getCommands(g => g.BlendingParameters, triggeredGroups), (d, value) => d.Blending = value, false); if (drawable is IVectorScalable vectorScalable) { - generateCommands(generated, getCommands(g => g.VectorScale, triggeredGroups), (_, value) => vectorScalable.VectorScale = value, - (_, value, duration, easing) => vectorScalable.VectorScaleTo(value, duration, easing)); + generateCommands(generated, getCommands(g => g.VectorScale, triggeredGroups), (_, value) => vectorScalable.VectorScale = value); } if (drawable is IFlippable flippable) { - generateCommands(generated, getCommands(g => g.FlipH, triggeredGroups), (_, value) => flippable.FlipH = value, (_, value, duration, _) => flippable.TransformFlipH(value, duration), - false); - generateCommands(generated, getCommands(g => g.FlipV, triggeredGroups), (_, value) => flippable.FlipV = value, (_, value, duration, _) => flippable.TransformFlipV(value, duration), - false); + generateCommands(generated, getCommands(g => g.FlipH, triggeredGroups), (_, value) => flippable.FlipH = value, false); + generateCommands(generated, getCommands(g => g.FlipV, triggeredGroups), (_, value) => flippable.FlipV = value, false); } foreach (var command in generated.OrderBy(g => g.StartTime)) @@ -160,7 +154,7 @@ namespace osu.Game.Storyboards } private void generateCommands(List resultList, IEnumerable.TypedCommand> commands, - DrawablePropertyInitializer initializeProperty, DrawableTransformer transform, bool alwaysInitialize = true) + DrawablePropertyInitializer initializeProperty, bool alwaysInitialize = true) { bool initialized = false; @@ -175,7 +169,7 @@ namespace osu.Game.Storyboards initialized = true; } - resultList.Add(new GeneratedCommand(command, initFunc, transform)); + resultList.Add(new GeneratedCommand(command, initFunc)); } } @@ -209,24 +203,59 @@ namespace osu.Game.Storyboards public double StartTime => command.StartTime; private readonly DrawablePropertyInitializer? initializeProperty; - private readonly DrawableTransformer transform; private readonly CommandTimeline.TypedCommand command; - public GeneratedCommand(CommandTimeline.TypedCommand command, DrawablePropertyInitializer? initializeProperty, DrawableTransformer transform) + public GeneratedCommand(CommandTimeline.TypedCommand command, DrawablePropertyInitializer? initializeProperty) { this.command = command; this.initializeProperty = initializeProperty; - this.transform = transform; } public void ApplyTo(Drawable drawable) { initializeProperty?.Invoke(drawable, command.StartValue); - using (drawable.BeginAbsoluteSequence(command.StartTime)) + switch (command.PropertyName) { - transform(drawable, command.StartValue, 0, Easing.None); - transform(drawable, command.EndValue, command.Duration, command.Easing); + case "VectorScale": + using (drawable.BeginAbsoluteSequence(command.StartTime)) + { + ((IVectorScalable)drawable).TransformTo(command.PropertyName, command.StartValue).Then().TransformTo(command.PropertyName, command.EndValue, command.Duration, command.Easing); + } + + break; + + case "FlipH": + using (drawable.BeginAbsoluteSequence(command.StartTime)) + { + ((IFlippable)drawable).TransformTo(command.PropertyName, command.StartValue).Delay(command.Duration).TransformTo(command.PropertyName, command.EndValue); + } + + break; + + case "FlipV": + using (drawable.BeginAbsoluteSequence(command.StartTime)) + { + ((IFlippable)drawable).TransformTo(command.PropertyName, command.StartValue).Delay(command.Duration).TransformTo(command.PropertyName, command.EndValue); + } + + break; + + case "Blending": + using (drawable.BeginAbsoluteSequence(command.StartTime)) + { + drawable.TransformTo(command.PropertyName, command.StartValue).Delay(command.Duration).TransformTo(command.PropertyName, command.EndValue); + } + + break; + + default: + using (drawable.BeginAbsoluteSequence(command.StartTime)) + { + drawable.TransformTo(command.PropertyName, command.StartValue).Then().TransformTo(command.PropertyName, command.EndValue, command.Duration, command.Easing); + } + + break; } } } From 7193ec66a4dee4abe516bd8984d3ed50559df35d Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Sun, 3 Mar 2024 21:30:46 +0300 Subject: [PATCH 0262/2493] Make use of framework's transform loops --- osu.Game/Storyboards/CommandLoop.cs | 18 ++++-- osu.Game/Storyboards/CommandTimeline.cs | 2 + osu.Game/Storyboards/StoryboardSprite.cs | 75 +++++++++++++++++++----- 3 files changed, 76 insertions(+), 19 deletions(-) diff --git a/osu.Game/Storyboards/CommandLoop.cs b/osu.Game/Storyboards/CommandLoop.cs index 480d69c12f..a912daea44 100644 --- a/osu.Game/Storyboards/CommandLoop.cs +++ b/osu.Game/Storyboards/CommandLoop.cs @@ -38,11 +38,21 @@ namespace osu.Game.Storyboards public override IEnumerable.TypedCommand> GetCommands(CommandTimelineSelector timelineSelector, double offset = 0) { - for (int loop = 0; loop < TotalIterations; loop++) + double fullLoopDuration = CommandsEndTime - CommandsStartTime; + + foreach (var command in timelineSelector(this).Commands) { - double loopOffset = LoopStartTime + loop * CommandsDuration; - foreach (var command in base.GetCommands(timelineSelector, offset + loopOffset)) - yield return command; + yield return new CommandTimeline.TypedCommand + { + Easing = command.Easing, + StartTime = offset + LoopStartTime + command.StartTime, + EndTime = offset + LoopStartTime + command.EndTime, + StartValue = command.StartValue, + EndValue = command.EndValue, + PropertyName = command.PropertyName, + LoopCount = TotalIterations, + Delay = fullLoopDuration - command.EndTime + command.StartTime + }; } } diff --git a/osu.Game/Storyboards/CommandTimeline.cs b/osu.Game/Storyboards/CommandTimeline.cs index 2bb606d2bb..ce25bfe25b 100644 --- a/osu.Game/Storyboards/CommandTimeline.cs +++ b/osu.Game/Storyboards/CommandTimeline.cs @@ -63,6 +63,8 @@ namespace osu.Game.Storyboards public double EndTime { get; set; } public double Duration => EndTime - StartTime; public string PropertyName { get; set; } + public int LoopCount { get; set; } + public double Delay { get; set; } public T StartValue; public T EndValue; diff --git a/osu.Game/Storyboards/StoryboardSprite.cs b/osu.Game/Storyboards/StoryboardSprite.cs index 4d3f1c158f..2c04e4c983 100644 --- a/osu.Game/Storyboards/StoryboardSprite.cs +++ b/osu.Game/Storyboards/StoryboardSprite.cs @@ -218,41 +218,86 @@ namespace osu.Game.Storyboards switch (command.PropertyName) { case "VectorScale": - using (drawable.BeginAbsoluteSequence(command.StartTime)) + if (command.LoopCount == 0) { - ((IVectorScalable)drawable).TransformTo(command.PropertyName, command.StartValue).Then().TransformTo(command.PropertyName, command.EndValue, command.Duration, command.Easing); + using (drawable.BeginAbsoluteSequence(command.StartTime)) + { + ((IVectorScalable)drawable).TransformTo(command.PropertyName, command.StartValue).Then() + .TransformTo(command.PropertyName, command.EndValue, command.Duration, command.Easing); + } + } + else + { + using (drawable.BeginAbsoluteSequence(command.StartTime)) + { + ((IVectorScalable)drawable).TransformTo(command.PropertyName, command.StartValue).Then() + .TransformTo(command.PropertyName, command.EndValue, command.Duration, command.Easing) + .Loop(command.Delay, command.LoopCount); + } } break; case "FlipH": - using (drawable.BeginAbsoluteSequence(command.StartTime)) - { - ((IFlippable)drawable).TransformTo(command.PropertyName, command.StartValue).Delay(command.Duration).TransformTo(command.PropertyName, command.EndValue); - } - - break; - case "FlipV": - using (drawable.BeginAbsoluteSequence(command.StartTime)) + if (command.LoopCount == 0) { - ((IFlippable)drawable).TransformTo(command.PropertyName, command.StartValue).Delay(command.Duration).TransformTo(command.PropertyName, command.EndValue); + using (drawable.BeginAbsoluteSequence(command.StartTime)) + { + ((IFlippable)drawable).TransformTo(command.PropertyName, command.StartValue).Delay(command.Duration) + .TransformTo(command.PropertyName, command.EndValue); + } + } + else + { + using (drawable.BeginAbsoluteSequence(command.StartTime)) + { + ((IFlippable)drawable).TransformTo(command.PropertyName, command.StartValue).Delay(command.Duration) + .TransformTo(command.PropertyName, command.EndValue) + .Loop(command.Delay, command.LoopCount); + } } break; case "Blending": - using (drawable.BeginAbsoluteSequence(command.StartTime)) + if (command.LoopCount == 0) { - drawable.TransformTo(command.PropertyName, command.StartValue).Delay(command.Duration).TransformTo(command.PropertyName, command.EndValue); + using (drawable.BeginAbsoluteSequence(command.StartTime)) + { + drawable.TransformTo(command.PropertyName, command.StartValue).Delay(command.Duration) + .TransformTo(command.PropertyName, command.EndValue); + } + } + else + { + using (drawable.BeginAbsoluteSequence(command.StartTime)) + { + drawable.TransformTo(command.PropertyName, command.StartValue).Delay(command.Duration) + .TransformTo(command.PropertyName, command.EndValue) + .Loop(command.Delay, command.LoopCount); + } } break; default: - using (drawable.BeginAbsoluteSequence(command.StartTime)) + if (command.LoopCount == 0) { - drawable.TransformTo(command.PropertyName, command.StartValue).Then().TransformTo(command.PropertyName, command.EndValue, command.Duration, command.Easing); + using (drawable.BeginAbsoluteSequence(command.StartTime)) + { + drawable.TransformTo(command.PropertyName, command.StartValue).Then() + .TransformTo(command.PropertyName, command.EndValue, command.Duration, command.Easing); + } + } + else + { + using (drawable.BeginAbsoluteSequence(command.StartTime)) + { + drawable.TransformTo(command.PropertyName, command.StartValue).Then() + .TransformTo(command.PropertyName, command.EndValue, command.Duration, command.Easing) + .Loop(command.Delay, command.LoopCount); + } } break; From f5d24e6804e64ffa720adc57116323cd682107e3 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Sun, 3 Mar 2024 21:54:37 +0300 Subject: [PATCH 0263/2493] Remove unused transform extensions --- .../Drawables/DrawablesExtensions.cs | 30 ------------- osu.Game/Storyboards/Drawables/IFlippable.cs | 42 ------------------- .../Storyboards/Drawables/IVectorScalable.cs | 8 ---- 3 files changed, 80 deletions(-) delete mode 100644 osu.Game/Storyboards/Drawables/DrawablesExtensions.cs diff --git a/osu.Game/Storyboards/Drawables/DrawablesExtensions.cs b/osu.Game/Storyboards/Drawables/DrawablesExtensions.cs deleted file mode 100644 index bbc55a336d..0000000000 --- a/osu.Game/Storyboards/Drawables/DrawablesExtensions.cs +++ /dev/null @@ -1,30 +0,0 @@ -// 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.Graphics; -using osu.Framework.Graphics.Transforms; - -namespace osu.Game.Storyboards.Drawables -{ - public static class DrawablesExtensions - { - /// - /// Adjusts after a delay. - /// - /// A to which further transforms can be added. - public static TransformSequence TransformBlendingMode(this T drawable, BlendingParameters newValue, double delay = 0) - where T : Drawable - => drawable.TransformTo(drawable.PopulateTransform(new TransformBlendingParameters(), newValue, delay)); - } - - public class TransformBlendingParameters : Transform - { - private BlendingParameters valueAt(double time) - => time < EndTime ? StartValue : EndValue; - - public override string TargetMember => nameof(Drawable.Blending); - - protected override void Apply(Drawable d, double time) => d.Blending = valueAt(time); - protected override void ReadIntoStartValue(Drawable d) => StartValue = d.Blending; - } -} diff --git a/osu.Game/Storyboards/Drawables/IFlippable.cs b/osu.Game/Storyboards/Drawables/IFlippable.cs index 165b3d97cc..2a931875ea 100644 --- a/osu.Game/Storyboards/Drawables/IFlippable.cs +++ b/osu.Game/Storyboards/Drawables/IFlippable.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 osu.Framework.Graphics; using osu.Framework.Graphics.Transforms; namespace osu.Game.Storyboards.Drawables @@ -11,45 +10,4 @@ namespace osu.Game.Storyboards.Drawables bool FlipH { get; set; } bool FlipV { get; set; } } - - internal class TransformFlipH : Transform - { - private bool valueAt(double time) - => time < EndTime ? StartValue : EndValue; - - public override string TargetMember => nameof(IFlippable.FlipH); - - protected override void Apply(IFlippable d, double time) => d.FlipH = valueAt(time); - protected override void ReadIntoStartValue(IFlippable d) => StartValue = d.FlipH; - } - - internal class TransformFlipV : Transform - { - private bool valueAt(double time) - => time < EndTime ? StartValue : EndValue; - - public override string TargetMember => nameof(IFlippable.FlipV); - - protected override void Apply(IFlippable d, double time) => d.FlipV = valueAt(time); - protected override void ReadIntoStartValue(IFlippable d) => StartValue = d.FlipV; - } - - internal static class FlippableExtensions - { - /// - /// Adjusts after a delay. - /// - /// A to which further transforms can be added. - public static TransformSequence TransformFlipH(this T flippable, bool newValue, double delay = 0) - where T : class, IFlippable - => flippable.TransformTo(flippable.PopulateTransform(new TransformFlipH(), newValue, delay)); - - /// - /// Adjusts after a delay. - /// - /// A to which further transforms can be added. - public static TransformSequence TransformFlipV(this T flippable, bool newValue, double delay = 0) - where T : class, IFlippable - => flippable.TransformTo(flippable.PopulateTransform(new TransformFlipV(), newValue, delay)); - } } diff --git a/osu.Game/Storyboards/Drawables/IVectorScalable.cs b/osu.Game/Storyboards/Drawables/IVectorScalable.cs index 60a297e126..ab0452df80 100644 --- a/osu.Game/Storyboards/Drawables/IVectorScalable.cs +++ b/osu.Game/Storyboards/Drawables/IVectorScalable.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 osu.Framework.Graphics; using osu.Framework.Graphics.Transforms; using osuTK; @@ -11,11 +10,4 @@ namespace osu.Game.Storyboards.Drawables { Vector2 VectorScale { get; set; } } - - internal static class VectorScalableExtensions - { - public static TransformSequence VectorScaleTo(this T target, Vector2 newVectorScale, double duration = 0, Easing easing = Easing.None) - where T : class, IVectorScalable - => target.TransformTo(nameof(IVectorScalable.VectorScale), newVectorScale, duration, easing); - } } From 4a7635e488169bf09dbcdfc7bf4da489a82d2b1c Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Sun, 3 Mar 2024 23:04:06 +0300 Subject: [PATCH 0264/2493] Fix broken tests --- osu.Game.Tests/Visual/Gameplay/TestSceneLeadIn.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneLeadIn.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneLeadIn.cs index dadf3ca65f..dae3119ea4 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneLeadIn.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneLeadIn.cs @@ -73,9 +73,9 @@ namespace osu.Game.Tests.Visual.Gameplay var sprite = new StoryboardSprite("unknown", Anchor.TopLeft, Vector2.Zero); // these should be ignored as we have an alpha visibility blocker proceeding this command. - sprite.TimelineGroup.Scale.Add(Easing.None, loop_start_time, -18000, 0, 1); + sprite.TimelineGroup.Scale.Add(Easing.None, loop_start_time, -18000, Vector2.Zero, Vector2.One); var loopGroup = sprite.AddLoop(loop_start_time, 50); - loopGroup.Scale.Add(Easing.None, loop_start_time, -18000, 0, 1); + loopGroup.Scale.Add(Easing.None, loop_start_time, -18000, Vector2.Zero, Vector2.One); var target = addEventToLoop ? loopGroup : sprite.TimelineGroup; double loopRelativeOffset = addEventToLoop ? -loop_start_time : 0; From bce3bd55e5a863e52f41598c306a248a79638843 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 4 Mar 2024 16:08:17 +0900 Subject: [PATCH 0265/2493] Fix catch by moving cursor-specific handling local --- .../TestSceneResumeOverlay.cs | 16 +++++++++++++--- osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs | 7 +++++++ osu.Game/Rulesets/UI/DrawableRuleset.cs | 2 +- osu.Game/Screens/Play/ResumeOverlay.cs | 3 ++- 4 files changed, 23 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneResumeOverlay.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneResumeOverlay.cs index 25d0b0a3d3..b35984a2fc 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneResumeOverlay.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneResumeOverlay.cs @@ -6,11 +6,11 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Cursor; using osu.Framework.Testing; using osu.Game.Configuration; using osu.Game.Rulesets.Osu.UI; using osu.Game.Rulesets.Osu.UI.Cursor; +using osu.Game.Rulesets.UI; using osu.Game.Screens.Play; using osu.Game.Tests.Gameplay; using osu.Game.Tests.Visual; @@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Osu.Tests public partial class TestSceneResumeOverlay : OsuManualInputManagerTestScene { private ManualOsuInputManager osuInputManager = null!; - private CursorContainer cursor = null!; + private GameplayCursorContainer cursor = null!; private ResumeOverlay resume = null!; private bool resumeFired; @@ -99,7 +99,17 @@ namespace osu.Game.Rulesets.Osu.Tests private void loadContent() { - Child = osuInputManager = new ManualOsuInputManager(new OsuRuleset().RulesetInfo) { Children = new Drawable[] { cursor = new CursorContainer(), resume = new OsuResumeOverlay { GameplayCursor = cursor }, } }; + Child = osuInputManager = new ManualOsuInputManager(new OsuRuleset().RulesetInfo) + { + Children = new Drawable[] + { + cursor = new GameplayCursorContainer(), + resume = new OsuResumeOverlay + { + GameplayCursor = cursor + }, + } + }; resumeFired = false; resume.ResumeAction = () => resumeFired = true; diff --git a/osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs b/osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs index 8a84fe14e5..adc7bd97ff 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs @@ -39,6 +39,13 @@ namespace osu.Game.Rulesets.Osu.UI protected override void PopIn() { + // Can't display if the cursor is outside the window. + if (GameplayCursor.LastFrameState == Visibility.Hidden || !Contains(GameplayCursor.ActiveCursor.ScreenSpaceDrawQuad.Centre)) + { + Resume(); + return; + } + base.PopIn(); GameplayCursor.ActiveCursor.Hide(); diff --git a/osu.Game/Rulesets/UI/DrawableRuleset.cs b/osu.Game/Rulesets/UI/DrawableRuleset.cs index 4aeb3d4862..218fdf5b86 100644 --- a/osu.Game/Rulesets/UI/DrawableRuleset.cs +++ b/osu.Game/Rulesets/UI/DrawableRuleset.cs @@ -227,7 +227,7 @@ namespace osu.Game.Rulesets.UI public override void RequestResume(Action continueResume) { - if (ResumeOverlay != null && UseResumeOverlay && (Cursor == null || (Cursor.LastFrameState == Visibility.Visible && Contains(Cursor.ActiveCursor.ScreenSpaceDrawQuad.Centre)))) + if (ResumeOverlay != null && UseResumeOverlay) { ResumeOverlay.GameplayCursor = Cursor; ResumeOverlay.ResumeAction = continueResume; diff --git a/osu.Game/Screens/Play/ResumeOverlay.cs b/osu.Game/Screens/Play/ResumeOverlay.cs index fae406bd6b..a33dd79888 100644 --- a/osu.Game/Screens/Play/ResumeOverlay.cs +++ b/osu.Game/Screens/Play/ResumeOverlay.cs @@ -11,6 +11,7 @@ using osu.Framework.Graphics.Cursor; using osu.Framework.Localisation; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; +using osu.Game.Rulesets.UI; using osuTK; using osuTK.Graphics; @@ -21,7 +22,7 @@ namespace osu.Game.Screens.Play /// public abstract partial class ResumeOverlay : VisibilityContainer { - public CursorContainer GameplayCursor { get; set; } + public GameplayCursorContainer GameplayCursor { get; set; } /// /// The action to be performed to complete resuming. From 6635d9be04952b43b41ff8e3f2596999a069ff74 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 4 Mar 2024 16:08:25 +0900 Subject: [PATCH 0266/2493] Add countdown display --- .../UI/DrawableCatchRuleset.cs | 3 +- .../Gameplay/TestSceneDelayedResumeOverlay.cs | 44 ++++++ osu.Game/Screens/Play/DelayedResumeOverlay.cs | 135 +++++++++++++++++- 3 files changed, 178 insertions(+), 4 deletions(-) create mode 100644 osu.Game.Tests/Visual/Gameplay/TestSceneDelayedResumeOverlay.cs diff --git a/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs b/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs index 580c90bcb4..32ebdc1159 100644 --- a/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs +++ b/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs @@ -17,6 +17,7 @@ using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Scoring; using osu.Game.Screens.Play; +using osuTK; namespace osu.Game.Rulesets.Catch.UI { @@ -54,6 +55,6 @@ namespace osu.Game.Rulesets.Catch.UI public override DrawableHitObject? CreateDrawableRepresentation(CatchHitObject h) => null; - protected override ResumeOverlay CreateResumeOverlay() => new DelayedResumeOverlay(); + protected override ResumeOverlay CreateResumeOverlay() => new DelayedResumeOverlay { Scale = new Vector2(0.65f) }; } } diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneDelayedResumeOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneDelayedResumeOverlay.cs new file mode 100644 index 0000000000..241a78b6b8 --- /dev/null +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneDelayedResumeOverlay.cs @@ -0,0 +1,44 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Graphics.Containers; +using osu.Game.Rulesets.Osu; +using osu.Game.Screens.Play; +using osu.Game.Tests.Gameplay; + +namespace osu.Game.Tests.Visual.Gameplay +{ + public partial class TestSceneDelayedResumeOverlay : OsuTestScene + { + private ResumeOverlay resume = null!; + private bool resumeFired; + + [Cached] + private GameplayState gameplayState; + + public TestSceneDelayedResumeOverlay() + { + gameplayState = TestGameplayState.Create(new OsuRuleset()); + } + + [SetUp] + public void SetUp() => Schedule(loadContent); + + [Test] + public void TestResume() + { + AddStep("show", () => resume.Show()); + AddUntilStep("dismissed", () => resumeFired && resume.State.Value == Visibility.Hidden); + } + + private void loadContent() + { + Child = resume = new DelayedResumeOverlay(); + + resumeFired = false; + resume.ResumeAction = () => resumeFired = true; + } + } +} diff --git a/osu.Game/Screens/Play/DelayedResumeOverlay.cs b/osu.Game/Screens/Play/DelayedResumeOverlay.cs index ef39c8eb76..6f70a914f0 100644 --- a/osu.Game/Screens/Play/DelayedResumeOverlay.cs +++ b/osu.Game/Screens/Play/DelayedResumeOverlay.cs @@ -1,8 +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 System; +using osu.Framework.Allocation; +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.Framework.Threading; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osuTK; +using osuTK.Graphics; namespace osu.Game.Screens.Play { @@ -11,21 +21,140 @@ namespace osu.Game.Screens.Play /// public partial class DelayedResumeOverlay : ResumeOverlay { + private const double countdown_time = 800; + protected override LocalisableString Message => string.Empty; + [Resolved] + private OsuColour colours { get; set; } = null!; + private ScheduledDelegate? scheduledResume; + private int countdownCount = 3; + private double countdownStartTime; + + private Drawable content = null!; + private Drawable background = null!; + private SpriteText countdown = null!; + + public DelayedResumeOverlay() + { + Anchor = Anchor.Centre; + Origin = Anchor.Centre; + } + + [BackgroundDependencyLoader] + private void load() + { + Add(content = new CircularContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + AutoSizeAxes = Axes.Both, + Masking = true, + BorderColour = colours.Yellow, + BorderThickness = 1, + Children = new[] + { + background = new Box + { + Size = new Vector2(250, 40), + Colour = Color4.Black, + Alpha = 0.8f + }, + new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + AutoSizeAxes = Axes.Both, + Children = new Drawable[] + { + new FillFlowContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + AutoSizeAxes = Axes.Both, + Spacing = new Vector2(5), + Colour = colours.Yellow, + Children = new Drawable[] + { + // new Box + // { + // Anchor = Anchor.Centre, + // Origin = Anchor.Centre, + // Size = new Vector2(40, 3) + // }, + countdown = new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + UseFullGlyphHeight = false, + AlwaysPresent = true, + Font = OsuFont.Numeric.With(size: 20, fixedWidth: true) + }, + // new Box + // { + // Anchor = Anchor.Centre, + // Origin = Anchor.Centre, + // Size = new Vector2(40, 3) + // } + } + } + } + } + } + }); + } protected override void PopIn() { - base.PopIn(); + this.FadeIn(); + + content.FadeInFromZero(150, Easing.OutQuint); + content.ScaleTo(new Vector2(1.5f, 1)).Then().ScaleTo(1, 150, Easing.OutElasticQuarter); + + countdownCount = 3; + countdownStartTime = Time.Current; scheduledResume?.Cancel(); - scheduledResume = Scheduler.AddDelayed(Resume, 800); + scheduledResume = Scheduler.AddDelayed(Resume, countdown_time); + } + + protected override void Update() + { + base.Update(); + updateCountdown(); + } + + private void updateCountdown() + { + double amountTimePassed = Math.Min(countdown_time, Time.Current - countdownStartTime) / countdown_time; + int newCount = 3 - (int)Math.Floor(amountTimePassed * 3); + + if (newCount > 0) + { + countdown.Alpha = 1; + countdown.Text = newCount.ToString(); + } + else + countdown.Alpha = 0; + + if (newCount != countdownCount) + { + if (newCount == 0) + content.ScaleTo(new Vector2(1.5f, 1), 150, Easing.OutQuint); + else + content.ScaleTo(new Vector2(1.05f, 1), 50, Easing.OutQuint).Then().ScaleTo(1, 50, Easing.Out); + } + + countdownCount = newCount; } protected override void PopOut() { - base.PopOut(); + this.Delay(150).FadeOut(); + + content.FadeOut(150, Easing.OutQuint); + scheduledResume?.Cancel(); } } From cceb616a18cc862f975da533bed42b49a89d2fa9 Mon Sep 17 00:00:00 2001 From: Jayden Date: Mon, 4 Mar 2024 22:25:36 -0500 Subject: [PATCH 0267/2493] Update failure messages Co-authored-by: Salman Ahmed --- osu.Desktop/DiscordRichPresence.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Desktop/DiscordRichPresence.cs b/osu.Desktop/DiscordRichPresence.cs index 035add8044..85b6129043 100644 --- a/osu.Desktop/DiscordRichPresence.cs +++ b/osu.Desktop/DiscordRichPresence.cs @@ -184,14 +184,14 @@ namespace osu.Desktop game.Window?.Raise(); if (!tryParseRoomSecret(args.Secret, out long roomId, out string? password)) - Logger.Log("Failed to parse the room secret Discord gave us", LoggingTarget.Network, LogLevel.Error); + Logger.Log("Could not parse room from Discord RPC Client", LoggingTarget.Network, LogLevel.Important); var request = new GetRoomRequest(roomId); request.Success += room => Schedule(() => { game.PresentMultiplayerMatch(room, password); }); - request.Failure += _ => Logger.Log("Couldn't find the room Discord gave us", LoggingTarget.Network, LogLevel.Error); + request.Failure += _ => Logger.Log($"Could not find room {roomId} from Discord RPC Client", LoggingTarget.Network, LogLevel.Important); api.Queue(request); } From e6f1a722cbb54a905a20c91fe30a8be072ba357c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 5 Mar 2024 18:48:58 +0100 Subject: [PATCH 0268/2493] Remove unused field and commented-out code --- osu.Game/Screens/Play/DelayedResumeOverlay.cs | 34 +++++-------------- 1 file changed, 9 insertions(+), 25 deletions(-) diff --git a/osu.Game/Screens/Play/DelayedResumeOverlay.cs b/osu.Game/Screens/Play/DelayedResumeOverlay.cs index 6f70a914f0..08d00f8ac2 100644 --- a/osu.Game/Screens/Play/DelayedResumeOverlay.cs +++ b/osu.Game/Screens/Play/DelayedResumeOverlay.cs @@ -33,7 +33,6 @@ namespace osu.Game.Screens.Play private double countdownStartTime; private Drawable content = null!; - private Drawable background = null!; private SpriteText countdown = null!; public DelayedResumeOverlay() @@ -53,9 +52,9 @@ namespace osu.Game.Screens.Play Masking = true, BorderColour = colours.Yellow, BorderThickness = 1, - Children = new[] + Children = new Drawable[] { - background = new Box + new Box { Size = new Vector2(250, 40), Colour = Color4.Black, @@ -75,29 +74,14 @@ namespace osu.Game.Screens.Play AutoSizeAxes = Axes.Both, Spacing = new Vector2(5), Colour = colours.Yellow, - Children = new Drawable[] + Child = countdown = new OsuSpriteText { - // new Box - // { - // Anchor = Anchor.Centre, - // Origin = Anchor.Centre, - // Size = new Vector2(40, 3) - // }, - countdown = new OsuSpriteText - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - UseFullGlyphHeight = false, - AlwaysPresent = true, - Font = OsuFont.Numeric.With(size: 20, fixedWidth: true) - }, - // new Box - // { - // Anchor = Anchor.Centre, - // Origin = Anchor.Centre, - // Size = new Vector2(40, 3) - // } - } + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + UseFullGlyphHeight = false, + AlwaysPresent = true, + Font = OsuFont.Numeric.With(size: 20, fixedWidth: true) + }, } } } From 8b03acd27bdbdd6a997dbd70a00a7b4568e6b8bc Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Wed, 6 Mar 2024 00:05:56 +0300 Subject: [PATCH 0269/2493] Implement StoryboardElementWithDuration --- .../Formats/LegacyStoryboardDecoder.cs | 2 +- osu.Game/Storyboards/CommandLoop.cs | 1 + osu.Game/Storyboards/CommandTimeline.cs | 4 +- osu.Game/Storyboards/CommandTimelineGroup.cs | 9 +- .../Drawables/DrawableStoryboardAnimation.cs | 2 +- .../Drawables/DrawableStoryboardSprite.cs | 2 +- ...pable.cs => IDrawableStoryboardElement.cs} | 6 +- .../Storyboards/Drawables/IVectorScalable.cs | 13 - osu.Game/Storyboards/StoryboardAnimation.cs | 5 +- .../StoryboardElementWithDuration.cs | 261 +++++++++++++++ osu.Game/Storyboards/StoryboardSprite.cs | 297 +----------------- 11 files changed, 284 insertions(+), 318 deletions(-) rename osu.Game/Storyboards/Drawables/{IFlippable.cs => IDrawableStoryboardElement.cs} (54%) delete mode 100644 osu.Game/Storyboards/Drawables/IVectorScalable.cs create mode 100644 osu.Game/Storyboards/StoryboardElementWithDuration.cs diff --git a/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs index a9a4d9cc49..ba328b2dbd 100644 --- a/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs @@ -17,7 +17,7 @@ namespace osu.Game.Beatmaps.Formats { public class LegacyStoryboardDecoder : LegacyDecoder { - private StoryboardSprite? storyboardSprite; + private StoryboardElementWithDuration? storyboardSprite; private CommandTimelineGroup? timelineGroup; private Storyboard storyboard = null!; diff --git a/osu.Game/Storyboards/CommandLoop.cs b/osu.Game/Storyboards/CommandLoop.cs index a912daea44..6dd782cb7f 100644 --- a/osu.Game/Storyboards/CommandLoop.cs +++ b/osu.Game/Storyboards/CommandLoop.cs @@ -50,6 +50,7 @@ namespace osu.Game.Storyboards StartValue = command.StartValue, EndValue = command.EndValue, PropertyName = command.PropertyName, + IsParameterCommand = command.IsParameterCommand, LoopCount = TotalIterations, Delay = fullLoopDuration - command.EndTime + command.StartTime }; diff --git a/osu.Game/Storyboards/CommandTimeline.cs b/osu.Game/Storyboards/CommandTimeline.cs index ce25bfe25b..4ad31d88c2 100644 --- a/osu.Game/Storyboards/CommandTimeline.cs +++ b/osu.Game/Storyboards/CommandTimeline.cs @@ -25,6 +25,7 @@ namespace osu.Game.Storyboards public T EndValue { get; private set; } public string PropertyName { get; } + public bool IsParameterTimeline { get; set; } public CommandTimeline(string propertyName) { @@ -38,7 +39,7 @@ namespace osu.Game.Storyboards endTime = startTime; } - commands.Add(new TypedCommand { Easing = easing, StartTime = startTime, EndTime = endTime, StartValue = startValue, EndValue = endValue, PropertyName = PropertyName }); + commands.Add(new TypedCommand { Easing = easing, StartTime = startTime, EndTime = endTime, StartValue = startValue, EndValue = endValue, PropertyName = PropertyName, IsParameterCommand = IsParameterTimeline }); if (startTime < StartTime) { @@ -65,6 +66,7 @@ namespace osu.Game.Storyboards public string PropertyName { get; set; } public int LoopCount { get; set; } public double Delay { get; set; } + public bool IsParameterCommand { get; set; } public T StartValue; public T EndValue; diff --git a/osu.Game/Storyboards/CommandTimelineGroup.cs b/osu.Game/Storyboards/CommandTimelineGroup.cs index cb795e0ffe..0362925619 100644 --- a/osu.Game/Storyboards/CommandTimelineGroup.cs +++ b/osu.Game/Storyboards/CommandTimelineGroup.cs @@ -22,9 +22,9 @@ namespace osu.Game.Storyboards public CommandTimeline Rotation = new CommandTimeline("Rotation"); public CommandTimeline Colour = new CommandTimeline("Colour"); public CommandTimeline Alpha = new CommandTimeline("Alpha"); - public CommandTimeline BlendingParameters = new CommandTimeline("Blending"); - public CommandTimeline FlipH = new CommandTimeline("FlipH"); - public CommandTimeline FlipV = new CommandTimeline("FlipV"); + public CommandTimeline BlendingParameters = new CommandTimeline("Blending") { IsParameterTimeline = true }; + public CommandTimeline FlipH = new CommandTimeline("FlipH") { IsParameterTimeline = true }; + public CommandTimeline FlipV = new CommandTimeline("FlipV") { IsParameterTimeline = true }; private readonly ICommandTimeline[] timelines; @@ -109,7 +109,8 @@ namespace osu.Game.Storyboards EndTime = offset + command.EndTime, StartValue = command.StartValue, EndValue = command.EndValue, - PropertyName = command.PropertyName + PropertyName = command.PropertyName, + IsParameterCommand = command.IsParameterCommand }); } diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs index fae9ec7f2e..8e1a8ce949 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs @@ -16,7 +16,7 @@ using osuTK; namespace osu.Game.Storyboards.Drawables { - public partial class DrawableStoryboardAnimation : TextureAnimation, IFlippable, IVectorScalable + public partial class DrawableStoryboardAnimation : TextureAnimation, IDrawableStoryboardElement { public StoryboardAnimation Animation { get; } diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs index ec875219b6..6772178e85 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs @@ -13,7 +13,7 @@ using osuTK; namespace osu.Game.Storyboards.Drawables { - public partial class DrawableStoryboardSprite : Sprite, IFlippable, IVectorScalable + public partial class DrawableStoryboardSprite : Sprite, IDrawableStoryboardElement { public StoryboardSprite Sprite { get; } diff --git a/osu.Game/Storyboards/Drawables/IFlippable.cs b/osu.Game/Storyboards/Drawables/IDrawableStoryboardElement.cs similarity index 54% rename from osu.Game/Storyboards/Drawables/IFlippable.cs rename to osu.Game/Storyboards/Drawables/IDrawableStoryboardElement.cs index 2a931875ea..6652b5509c 100644 --- a/osu.Game/Storyboards/Drawables/IFlippable.cs +++ b/osu.Game/Storyboards/Drawables/IDrawableStoryboardElement.cs @@ -1,13 +1,15 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. using osu.Framework.Graphics.Transforms; +using osuTK; namespace osu.Game.Storyboards.Drawables { - internal interface IFlippable : ITransformable + public interface IDrawableStoryboardElement : ITransformable { bool FlipH { get; set; } bool FlipV { get; set; } + Vector2 VectorScale { get; set; } } } diff --git a/osu.Game/Storyboards/Drawables/IVectorScalable.cs b/osu.Game/Storyboards/Drawables/IVectorScalable.cs deleted file mode 100644 index ab0452df80..0000000000 --- a/osu.Game/Storyboards/Drawables/IVectorScalable.cs +++ /dev/null @@ -1,13 +0,0 @@ -// 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.Graphics.Transforms; -using osuTK; - -namespace osu.Game.Storyboards.Drawables -{ - internal interface IVectorScalable : ITransformable - { - Vector2 VectorScale { get; set; } - } -} diff --git a/osu.Game/Storyboards/StoryboardAnimation.cs b/osu.Game/Storyboards/StoryboardAnimation.cs index 1a4b6bb923..173acf7ff1 100644 --- a/osu.Game/Storyboards/StoryboardAnimation.cs +++ b/osu.Game/Storyboards/StoryboardAnimation.cs @@ -7,7 +7,7 @@ using osu.Game.Storyboards.Drawables; namespace osu.Game.Storyboards { - public class StoryboardAnimation : StoryboardSprite + public class StoryboardAnimation : StoryboardElementWithDuration { public int FrameCount; public double FrameDelay; @@ -21,8 +21,7 @@ namespace osu.Game.Storyboards LoopType = loopType; } - public override Drawable CreateDrawable() - => new DrawableStoryboardAnimation(this); + public override DrawableStoryboardAnimation CreateStoryboardDrawable() => new DrawableStoryboardAnimation(this); } public enum AnimationLoopType diff --git a/osu.Game/Storyboards/StoryboardElementWithDuration.cs b/osu.Game/Storyboards/StoryboardElementWithDuration.cs new file mode 100644 index 0000000000..0c89b40c36 --- /dev/null +++ b/osu.Game/Storyboards/StoryboardElementWithDuration.cs @@ -0,0 +1,261 @@ +// 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.Graphics; +using osu.Game.Storyboards.Drawables; +using osuTK; + +namespace osu.Game.Storyboards +{ + public abstract class StoryboardElementWithDuration : IStoryboardElementWithDuration + { + protected readonly List Loops = new List(); + private readonly List triggers = new List(); + + public string Path { get; } + public bool IsDrawable => HasCommands; + + public Anchor Origin; + public Vector2 InitialPosition; + + public readonly CommandTimelineGroup TimelineGroup = new CommandTimelineGroup(); + + public double StartTime + { + get + { + // To get the initial start time, we need to check whether the first alpha command to exist (across all loops) has a StartValue of zero. + // A StartValue of zero governs, above all else, the first valid display time of a sprite. + // + // You can imagine that the first command of each type decides that type's start value, so if the initial alpha is zero, + // anything before that point can be ignored (the sprite is not visible after all). + var alphaCommands = new List<(double startTime, bool isZeroStartValue)>(); + + var command = TimelineGroup.Alpha.Commands.FirstOrDefault(); + if (command != null) alphaCommands.Add((command.StartTime, command.StartValue == 0)); + + foreach (var loop in Loops) + { + command = loop.Alpha.Commands.FirstOrDefault(); + if (command != null) alphaCommands.Add((command.StartTime + loop.LoopStartTime, command.StartValue == 0)); + } + + if (alphaCommands.Count > 0) + { + var firstAlpha = alphaCommands.MinBy(t => t.startTime); + + if (firstAlpha.isZeroStartValue) + return firstAlpha.startTime; + } + + return EarliestTransformTime; + } + } + + public double EarliestTransformTime + { + get + { + // If we got to this point, either no alpha commands were present, or the earliest had a non-zero start value. + // The sprite's StartTime will be determined by the earliest command, regardless of type. + double earliestStartTime = TimelineGroup.StartTime; + foreach (var l in Loops) + earliestStartTime = Math.Min(earliestStartTime, l.StartTime); + return earliestStartTime; + } + } + + public double EndTime + { + get + { + double latestEndTime = TimelineGroup.EndTime; + + foreach (var l in Loops) + latestEndTime = Math.Max(latestEndTime, l.EndTime); + + return latestEndTime; + } + } + + public double EndTimeForDisplay + { + get + { + double latestEndTime = TimelineGroup.EndTime; + + foreach (var l in Loops) + latestEndTime = Math.Max(latestEndTime, l.StartTime + l.CommandsDuration * l.TotalIterations); + + return latestEndTime; + } + } + + public bool HasCommands => TimelineGroup.HasCommands || Loops.Any(l => l.HasCommands); + + protected StoryboardElementWithDuration(string path, Anchor origin, Vector2 initialPosition) + { + Path = path; + Origin = origin; + InitialPosition = initialPosition; + } + + public abstract Drawable CreateDrawable(); + + public CommandLoop AddLoop(double startTime, int repeatCount) + { + var loop = new CommandLoop(startTime, repeatCount); + Loops.Add(loop); + return loop; + } + + public CommandTrigger AddTrigger(string triggerName, double startTime, double endTime, int groupNumber) + { + var trigger = new CommandTrigger(triggerName, startTime, endTime, groupNumber); + triggers.Add(trigger); + return trigger; + } + + protected IEnumerable.TypedCommand> GetCommands(CommandTimelineSelector timelineSelector, IEnumerable>? triggeredGroups) + { + var commands = TimelineGroup.GetCommands(timelineSelector); + foreach (var loop in Loops) + commands = commands.Concat(loop.GetCommands(timelineSelector)); + + if (triggeredGroups != null) + { + foreach (var pair in triggeredGroups) + commands = commands.Concat(pair.Item1.GetCommands(timelineSelector, pair.Item2)); + } + + return commands; + } + + public override string ToString() + => $"{Path}, {Origin}, {InitialPosition}"; + } + + public abstract class StoryboardElementWithDuration : StoryboardElementWithDuration + where U : Drawable, IDrawableStoryboardElement + { + private delegate void DrawablePropertyInitializer(U drawable, T value); + + protected StoryboardElementWithDuration(string path, Anchor origin, Vector2 initialPosition) + : base(path, origin, initialPosition) + { + } + + public override Drawable CreateDrawable() => CreateStoryboardDrawable(); + + public abstract U CreateStoryboardDrawable(); + + public void ApplyTransforms(U drawable, IEnumerable>? triggeredGroups = null) + { + // For performance reasons, we need to apply the commands in order by start time. Not doing so will cause many functions to be interleaved, resulting in O(n^2) complexity. + // To achieve this, commands are "generated" as pairs of (command, initFunc, transformFunc) and batched into a contiguous list + // The list is then stably-sorted (to preserve command order), and applied to the drawable sequentially. + + List> generated = new List>(); + + generateCommands(generated, GetCommands(g => g.X, triggeredGroups), (d, value) => d.X = value); + generateCommands(generated, GetCommands(g => g.Y, triggeredGroups), (d, value) => d.Y = value); + generateCommands(generated, GetCommands(g => g.Scale, triggeredGroups), (d, value) => d.Scale = value); + generateCommands(generated, GetCommands(g => g.Rotation, triggeredGroups), (d, value) => d.Rotation = value); + generateCommands(generated, GetCommands(g => g.Colour, triggeredGroups), (d, value) => d.Colour = value); + generateCommands(generated, GetCommands(g => g.Alpha, triggeredGroups), (d, value) => d.Alpha = value); + generateCommands(generated, GetCommands(g => g.BlendingParameters, triggeredGroups), (d, value) => d.Blending = value, false); + generateCommands(generated, GetCommands(g => g.VectorScale, triggeredGroups), (d, value) => d.VectorScale = value); + generateCommands(generated, GetCommands(g => g.FlipH, triggeredGroups), (d, value) => d.FlipH = value, false); + generateCommands(generated, GetCommands(g => g.FlipV, triggeredGroups), (d, value) => d.FlipV = value, false); + + foreach (var command in generated.OrderBy(g => g.StartTime)) + command.ApplyTo(drawable); + } + + private void generateCommands(List> resultList, IEnumerable.TypedCommand> commands, + DrawablePropertyInitializer initializeProperty, bool alwaysInitialize = true) + { + bool initialized = false; + + foreach (var command in commands) + { + DrawablePropertyInitializer? initFunc = null; + + if (!initialized) + { + if (alwaysInitialize || command.StartTime == command.EndTime) + initFunc = initializeProperty; + initialized = true; + } + + resultList.Add(new GeneratedCommand(command, initFunc)); + } + } + + private interface IGeneratedCommand + where TDrawable : U + { + double StartTime { get; } + + void ApplyTo(TDrawable drawable); + } + + private readonly struct GeneratedCommand : IGeneratedCommand + where TDrawable : U + { + public double StartTime => command.StartTime; + + private readonly DrawablePropertyInitializer? initializeProperty; + private readonly CommandTimeline.TypedCommand command; + + public GeneratedCommand(CommandTimeline.TypedCommand command, DrawablePropertyInitializer? initializeProperty) + { + this.command = command; + this.initializeProperty = initializeProperty; + } + + public void ApplyTo(TDrawable drawable) + { + initializeProperty?.Invoke(drawable, command.StartValue); + + using (drawable.BeginAbsoluteSequence(command.StartTime)) + transform(drawable); + } + + private void transform(TDrawable drawable) + { + if (command.IsParameterCommand) + { + if (command.LoopCount == 0) + { + drawable.TransformTo(command.PropertyName, command.StartValue).Delay(command.Duration) + .TransformTo(command.PropertyName, command.EndValue); + } + else + { + drawable.TransformTo(command.PropertyName, command.StartValue).Delay(command.Duration) + .TransformTo(command.PropertyName, command.EndValue) + .Loop(command.Delay, command.LoopCount); + } + } + else + { + if (command.LoopCount == 0) + { + drawable.TransformTo(command.PropertyName, command.StartValue).Then() + .TransformTo(command.PropertyName, command.EndValue, command.Duration, command.Easing); + } + else + { + drawable.TransformTo(command.PropertyName, command.StartValue).Then() + .TransformTo(command.PropertyName, command.EndValue, command.Duration, command.Easing) + .Loop(command.Delay, command.LoopCount); + } + } + } + } + } +} diff --git a/osu.Game/Storyboards/StoryboardSprite.cs b/osu.Game/Storyboards/StoryboardSprite.cs index 2c04e4c983..69994f77a4 100644 --- a/osu.Game/Storyboards/StoryboardSprite.cs +++ b/osu.Game/Storyboards/StoryboardSprite.cs @@ -1,308 +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.Graphics; using osu.Game.Storyboards.Drawables; using osuTK; namespace osu.Game.Storyboards { - public class StoryboardSprite : IStoryboardElementWithDuration + public class StoryboardSprite : StoryboardElementWithDuration { - private readonly List loops = new List(); - private readonly List triggers = new List(); - - public string Path { get; } - public bool IsDrawable => HasCommands; - - public Anchor Origin; - public Vector2 InitialPosition; - - public readonly CommandTimelineGroup TimelineGroup = new CommandTimelineGroup(); - - public double StartTime - { - get - { - // To get the initial start time, we need to check whether the first alpha command to exist (across all loops) has a StartValue of zero. - // A StartValue of zero governs, above all else, the first valid display time of a sprite. - // - // You can imagine that the first command of each type decides that type's start value, so if the initial alpha is zero, - // anything before that point can be ignored (the sprite is not visible after all). - var alphaCommands = new List<(double startTime, bool isZeroStartValue)>(); - - var command = TimelineGroup.Alpha.Commands.FirstOrDefault(); - if (command != null) alphaCommands.Add((command.StartTime, command.StartValue == 0)); - - foreach (var loop in loops) - { - command = loop.Alpha.Commands.FirstOrDefault(); - if (command != null) alphaCommands.Add((command.StartTime + loop.LoopStartTime, command.StartValue == 0)); - } - - if (alphaCommands.Count > 0) - { - var firstAlpha = alphaCommands.MinBy(t => t.startTime); - - if (firstAlpha.isZeroStartValue) - return firstAlpha.startTime; - } - - return EarliestTransformTime; - } - } - - public double EarliestTransformTime - { - get - { - // If we got to this point, either no alpha commands were present, or the earliest had a non-zero start value. - // The sprite's StartTime will be determined by the earliest command, regardless of type. - double earliestStartTime = TimelineGroup.StartTime; - foreach (var l in loops) - earliestStartTime = Math.Min(earliestStartTime, l.StartTime); - return earliestStartTime; - } - } - - public double EndTime - { - get - { - double latestEndTime = TimelineGroup.EndTime; - - foreach (var l in loops) - latestEndTime = Math.Max(latestEndTime, l.EndTime); - - return latestEndTime; - } - } - - public double EndTimeForDisplay - { - get - { - double latestEndTime = TimelineGroup.EndTime; - - foreach (var l in loops) - latestEndTime = Math.Max(latestEndTime, l.StartTime + l.CommandsDuration * l.TotalIterations); - - return latestEndTime; - } - } - - public bool HasCommands => TimelineGroup.HasCommands || loops.Any(l => l.HasCommands); - - private delegate void DrawablePropertyInitializer(Drawable drawable, T value); - public StoryboardSprite(string path, Anchor origin, Vector2 initialPosition) + : base(path, origin, initialPosition) { - Path = path; - Origin = origin; - InitialPosition = initialPosition; } - public CommandLoop AddLoop(double startTime, int repeatCount) - { - var loop = new CommandLoop(startTime, repeatCount); - loops.Add(loop); - return loop; - } - - public CommandTrigger AddTrigger(string triggerName, double startTime, double endTime, int groupNumber) - { - var trigger = new CommandTrigger(triggerName, startTime, endTime, groupNumber); - triggers.Add(trigger); - return trigger; - } - - public virtual Drawable CreateDrawable() - => new DrawableStoryboardSprite(this); - - public void ApplyTransforms(Drawable drawable, IEnumerable>? triggeredGroups = null) - { - // For performance reasons, we need to apply the commands in order by start time. Not doing so will cause many functions to be interleaved, resulting in O(n^2) complexity. - // To achieve this, commands are "generated" as pairs of (command, initFunc, transformFunc) and batched into a contiguous list - // The list is then stably-sorted (to preserve command order), and applied to the drawable sequentially. - - List generated = new List(); - - generateCommands(generated, getCommands(g => g.X, triggeredGroups), (d, value) => d.X = value); - generateCommands(generated, getCommands(g => g.Y, triggeredGroups), (d, value) => d.Y = value); - generateCommands(generated, getCommands(g => g.Scale, triggeredGroups), (d, value) => d.Scale = value); - generateCommands(generated, getCommands(g => g.Rotation, triggeredGroups), (d, value) => d.Rotation = value); - generateCommands(generated, getCommands(g => g.Colour, triggeredGroups), (d, value) => d.Colour = value); - generateCommands(generated, getCommands(g => g.Alpha, triggeredGroups), (d, value) => d.Alpha = value); - generateCommands(generated, getCommands(g => g.BlendingParameters, triggeredGroups), (d, value) => d.Blending = value, false); - - if (drawable is IVectorScalable vectorScalable) - { - generateCommands(generated, getCommands(g => g.VectorScale, triggeredGroups), (_, value) => vectorScalable.VectorScale = value); - } - - if (drawable is IFlippable flippable) - { - generateCommands(generated, getCommands(g => g.FlipH, triggeredGroups), (_, value) => flippable.FlipH = value, false); - generateCommands(generated, getCommands(g => g.FlipV, triggeredGroups), (_, value) => flippable.FlipV = value, false); - } - - foreach (var command in generated.OrderBy(g => g.StartTime)) - command.ApplyTo(drawable); - } - - private void generateCommands(List resultList, IEnumerable.TypedCommand> commands, - DrawablePropertyInitializer initializeProperty, bool alwaysInitialize = true) - { - bool initialized = false; - - foreach (var command in commands) - { - DrawablePropertyInitializer? initFunc = null; - - if (!initialized) - { - if (alwaysInitialize || command.StartTime == command.EndTime) - initFunc = initializeProperty; - initialized = true; - } - - resultList.Add(new GeneratedCommand(command, initFunc)); - } - } - - private IEnumerable.TypedCommand> getCommands(CommandTimelineSelector timelineSelector, IEnumerable>? triggeredGroups) - { - var commands = TimelineGroup.GetCommands(timelineSelector); - foreach (var loop in loops) - commands = commands.Concat(loop.GetCommands(timelineSelector)); - - if (triggeredGroups != null) - { - foreach (var pair in triggeredGroups) - commands = commands.Concat(pair.Item1.GetCommands(timelineSelector, pair.Item2)); - } - - return commands; - } - - public override string ToString() - => $"{Path}, {Origin}, {InitialPosition}"; - - private interface IGeneratedCommand - { - double StartTime { get; } - - void ApplyTo(Drawable drawable); - } - - private readonly struct GeneratedCommand : IGeneratedCommand - { - public double StartTime => command.StartTime; - - private readonly DrawablePropertyInitializer? initializeProperty; - private readonly CommandTimeline.TypedCommand command; - - public GeneratedCommand(CommandTimeline.TypedCommand command, DrawablePropertyInitializer? initializeProperty) - { - this.command = command; - this.initializeProperty = initializeProperty; - } - - public void ApplyTo(Drawable drawable) - { - initializeProperty?.Invoke(drawable, command.StartValue); - - switch (command.PropertyName) - { - case "VectorScale": - if (command.LoopCount == 0) - { - using (drawable.BeginAbsoluteSequence(command.StartTime)) - { - ((IVectorScalable)drawable).TransformTo(command.PropertyName, command.StartValue).Then() - .TransformTo(command.PropertyName, command.EndValue, command.Duration, command.Easing); - } - } - else - { - using (drawable.BeginAbsoluteSequence(command.StartTime)) - { - ((IVectorScalable)drawable).TransformTo(command.PropertyName, command.StartValue).Then() - .TransformTo(command.PropertyName, command.EndValue, command.Duration, command.Easing) - .Loop(command.Delay, command.LoopCount); - } - } - - break; - - case "FlipH": - case "FlipV": - if (command.LoopCount == 0) - { - using (drawable.BeginAbsoluteSequence(command.StartTime)) - { - ((IFlippable)drawable).TransformTo(command.PropertyName, command.StartValue).Delay(command.Duration) - .TransformTo(command.PropertyName, command.EndValue); - } - } - else - { - using (drawable.BeginAbsoluteSequence(command.StartTime)) - { - ((IFlippable)drawable).TransformTo(command.PropertyName, command.StartValue).Delay(command.Duration) - .TransformTo(command.PropertyName, command.EndValue) - .Loop(command.Delay, command.LoopCount); - } - } - - break; - - case "Blending": - if (command.LoopCount == 0) - { - using (drawable.BeginAbsoluteSequence(command.StartTime)) - { - drawable.TransformTo(command.PropertyName, command.StartValue).Delay(command.Duration) - .TransformTo(command.PropertyName, command.EndValue); - } - } - else - { - using (drawable.BeginAbsoluteSequence(command.StartTime)) - { - drawable.TransformTo(command.PropertyName, command.StartValue).Delay(command.Duration) - .TransformTo(command.PropertyName, command.EndValue) - .Loop(command.Delay, command.LoopCount); - } - } - - break; - - default: - if (command.LoopCount == 0) - { - using (drawable.BeginAbsoluteSequence(command.StartTime)) - { - drawable.TransformTo(command.PropertyName, command.StartValue).Then() - .TransformTo(command.PropertyName, command.EndValue, command.Duration, command.Easing); - } - } - else - { - using (drawable.BeginAbsoluteSequence(command.StartTime)) - { - drawable.TransformTo(command.PropertyName, command.StartValue).Then() - .TransformTo(command.PropertyName, command.EndValue, command.Duration, command.Easing) - .Loop(command.Delay, command.LoopCount); - } - } - - break; - } - } - } + public override DrawableStoryboardSprite CreateStoryboardDrawable() => new DrawableStoryboardSprite(this); } } + + From 1c8ede854d5ba97deafeea83abee96c61cb367bb Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Wed, 6 Mar 2024 00:17:56 +0300 Subject: [PATCH 0270/2493] Remove blank lines --- osu.Game/Storyboards/StoryboardSprite.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game/Storyboards/StoryboardSprite.cs b/osu.Game/Storyboards/StoryboardSprite.cs index 69994f77a4..8eaab9428d 100644 --- a/osu.Game/Storyboards/StoryboardSprite.cs +++ b/osu.Game/Storyboards/StoryboardSprite.cs @@ -17,5 +17,3 @@ namespace osu.Game.Storyboards public override DrawableStoryboardSprite CreateStoryboardDrawable() => new DrawableStoryboardSprite(this); } } - - From 07392a4d3eb7ff9c08c863d0aaf27cb33161dac7 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Wed, 6 Mar 2024 01:10:22 +0300 Subject: [PATCH 0271/2493] Further simplify transform application --- .../StoryboardElementWithDuration.cs | 37 +++---------------- 1 file changed, 6 insertions(+), 31 deletions(-) diff --git a/osu.Game/Storyboards/StoryboardElementWithDuration.cs b/osu.Game/Storyboards/StoryboardElementWithDuration.cs index 0c89b40c36..06924a26ef 100644 --- a/osu.Game/Storyboards/StoryboardElementWithDuration.cs +++ b/osu.Game/Storyboards/StoryboardElementWithDuration.cs @@ -222,38 +222,13 @@ namespace osu.Game.Storyboards initializeProperty?.Invoke(drawable, command.StartValue); using (drawable.BeginAbsoluteSequence(command.StartTime)) - transform(drawable); - } + { + var sequence = command.IsParameterCommand + ? drawable.TransformTo(command.PropertyName, command.StartValue).Delay(command.Duration).TransformTo(command.PropertyName, command.EndValue) + : drawable.TransformTo(command.PropertyName, command.StartValue).Then().TransformTo(command.PropertyName, command.EndValue, command.Duration, command.Easing); - private void transform(TDrawable drawable) - { - if (command.IsParameterCommand) - { - if (command.LoopCount == 0) - { - drawable.TransformTo(command.PropertyName, command.StartValue).Delay(command.Duration) - .TransformTo(command.PropertyName, command.EndValue); - } - else - { - drawable.TransformTo(command.PropertyName, command.StartValue).Delay(command.Duration) - .TransformTo(command.PropertyName, command.EndValue) - .Loop(command.Delay, command.LoopCount); - } - } - else - { - if (command.LoopCount == 0) - { - drawable.TransformTo(command.PropertyName, command.StartValue).Then() - .TransformTo(command.PropertyName, command.EndValue, command.Duration, command.Easing); - } - else - { - drawable.TransformTo(command.PropertyName, command.StartValue).Then() - .TransformTo(command.PropertyName, command.EndValue, command.Duration, command.Easing) - .Loop(command.Delay, command.LoopCount); - } + if (command.LoopCount > 0) + sequence.Loop(command.Delay, command.LoopCount); } } } From b53777c2a42bab857664cb5c2887e5cced0c0625 Mon Sep 17 00:00:00 2001 From: jvyden Date: Tue, 5 Mar 2024 18:15:53 -0500 Subject: [PATCH 0272/2493] Refactor room secret handling to use JSON Also log room secrets for debugging purposes --- osu.Desktop/DiscordRichPresence.cs | 41 ++++++++++++++++++++++++------ 1 file changed, 33 insertions(+), 8 deletions(-) diff --git a/osu.Desktop/DiscordRichPresence.cs b/osu.Desktop/DiscordRichPresence.cs index 85b6129043..7315ee0c17 100644 --- a/osu.Desktop/DiscordRichPresence.cs +++ b/osu.Desktop/DiscordRichPresence.cs @@ -5,6 +5,7 @@ using System; using System.Text; using DiscordRPC; using DiscordRPC.Message; +using Newtonsoft.Json; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -147,7 +148,13 @@ namespace osu.Desktop Size = room.Users.Count, }; - presence.Secrets.JoinSecret = $"{room.RoomID}:{room.Settings.Password}"; + RoomSecret roomSecret = new RoomSecret + { + RoomID = room.RoomID, + Password = room.Settings.Password, + }; + + presence.Secrets.JoinSecret = JsonConvert.SerializeObject(roomSecret); } else { @@ -182,9 +189,13 @@ namespace osu.Desktop private void onJoin(object sender, JoinMessage args) { game.Window?.Raise(); + Logger.Log($"Received room secret from Discord RPC Client: {args.Secret}", LoggingTarget.Network, LogLevel.Debug); if (!tryParseRoomSecret(args.Secret, out long roomId, out string? password)) + { Logger.Log("Could not parse room from Discord RPC Client", LoggingTarget.Network, LogLevel.Important); + return; + } var request = new GetRoomRequest(roomId); request.Success += room => Schedule(() => @@ -216,21 +227,26 @@ namespace osu.Desktop }); } - private static bool tryParseRoomSecret(ReadOnlySpan secret, out long roomId, out string? password) + private static bool tryParseRoomSecret(string secretJson, out long roomId, out string? password) { roomId = 0; password = null; - int roomSecretSplitIndex = secret.IndexOf(':'); + RoomSecret? roomSecret; - if (roomSecretSplitIndex == -1) + try + { + roomSecret = JsonConvert.DeserializeObject(secretJson); + } + catch + { return false; + } - if (!long.TryParse(secret[..roomSecretSplitIndex], out roomId)) - return false; + if (roomSecret == null) return false; - password = secret[(roomSecretSplitIndex + 1)..].ToString(); - if (password.Length == 0) password = null; + roomId = roomSecret.RoomID; + password = roomSecret.Password; return true; } @@ -254,5 +270,14 @@ namespace osu.Desktop client.Dispose(); base.Dispose(isDisposing); } + + private class RoomSecret + { + [JsonProperty(@"roomId", Required = Required.Always)] + public long RoomID { get; set; } + + [JsonProperty(@"password", Required = Required.AllowNull)] + public string? Password { get; set; } + } } } From 98713003176da6b09bafeea7392564959098956b Mon Sep 17 00:00:00 2001 From: Jayden Date: Tue, 5 Mar 2024 18:22:39 -0500 Subject: [PATCH 0273/2493] Improve language of user-facing errors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bartłomiej Dach --- osu.Desktop/DiscordRichPresence.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Desktop/DiscordRichPresence.cs b/osu.Desktop/DiscordRichPresence.cs index 7315ee0c17..8fecd015d4 100644 --- a/osu.Desktop/DiscordRichPresence.cs +++ b/osu.Desktop/DiscordRichPresence.cs @@ -193,7 +193,7 @@ namespace osu.Desktop if (!tryParseRoomSecret(args.Secret, out long roomId, out string? password)) { - Logger.Log("Could not parse room from Discord RPC Client", LoggingTarget.Network, LogLevel.Important); + Logger.Log("Could not join multiplayer room.", LoggingTarget.Network, LogLevel.Important); return; } From 98ca021e6628d94df508709482f047a2cff7cdde Mon Sep 17 00:00:00 2001 From: jvyden Date: Wed, 6 Mar 2024 01:17:11 -0500 Subject: [PATCH 0274/2493] Catch and warn about osu!stable lobbies --- osu.Desktop/DiscordRichPresence.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/osu.Desktop/DiscordRichPresence.cs b/osu.Desktop/DiscordRichPresence.cs index 8fecd015d4..b4a7e80d48 100644 --- a/osu.Desktop/DiscordRichPresence.cs +++ b/osu.Desktop/DiscordRichPresence.cs @@ -191,6 +191,15 @@ namespace osu.Desktop game.Window?.Raise(); Logger.Log($"Received room secret from Discord RPC Client: {args.Secret}", LoggingTarget.Network, LogLevel.Debug); + // Stable and Lazer share the same Discord client ID, meaning they can accept join requests from each other. + // Since they aren't compatible in multi, see if stable's format is being used and log to avoid confusion. + // https://discord.com/channels/188630481301012481/188630652340404224/1214697229063946291 + if (args.Secret[0] != '{') + { + Logger.Log("osu!stable rooms are not compatible with lazer.", LoggingTarget.Network, LogLevel.Important); + return; + } + if (!tryParseRoomSecret(args.Secret, out long roomId, out string? password)) { Logger.Log("Could not join multiplayer room.", LoggingTarget.Network, LogLevel.Important); From 1cafb0997713ee387f8994e972d64ad908aa4181 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 7 Mar 2024 17:22:38 +0900 Subject: [PATCH 0275/2493] Increase border thickness --- osu.Game/Screens/Play/DelayedResumeOverlay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/DelayedResumeOverlay.cs b/osu.Game/Screens/Play/DelayedResumeOverlay.cs index 08d00f8ac2..ba49810b2b 100644 --- a/osu.Game/Screens/Play/DelayedResumeOverlay.cs +++ b/osu.Game/Screens/Play/DelayedResumeOverlay.cs @@ -51,7 +51,7 @@ namespace osu.Game.Screens.Play AutoSizeAxes = Axes.Both, Masking = true, BorderColour = colours.Yellow, - BorderThickness = 1, + BorderThickness = 2, Children = new Drawable[] { new Box From 585ab5976877281be70cb6f52721382c5a86a861 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 6 Mar 2024 19:39:28 +0300 Subject: [PATCH 0276/2493] Apply major refactor to the storyboard commands flow structrure --- .../TestSceneDrawableStoryboardSprite.cs | 3 +- .../Visual/Gameplay/TestSceneLeadIn.cs | 16 +- .../Gameplay/TestSceneStoryboardWithOutro.cs | 2 +- .../TestSceneMultiSpectatorScreen.cs | 2 +- .../Formats/LegacyStoryboardDecoder.cs | 39 +-- osu.Game/Storyboards/CommandLoop.cs | 63 ----- osu.Game/Storyboards/CommandTimeline.cs | 101 -------- osu.Game/Storyboards/CommandTimelineGroup.cs | 120 --------- .../Commands/IStoryboardCommand.cs | 28 +++ .../Commands/StoryboardAlphaCommand.cs | 19 ++ .../StoryboardBlendingParametersCommand.cs | 21 ++ .../Commands/StoryboardColourCommand.cs | 20 ++ .../Storyboards/Commands/StoryboardCommand.cs | 54 ++++ .../Commands/StoryboardCommandGroup.cs | 115 +++++++++ .../Commands/StoryboardCommandList.cs | 41 +++ .../Commands/StoryboardFlipHCommand.cs | 23 ++ .../Commands/StoryboardFlipVCommand.cs | 23 ++ .../Commands/StoryboardLoopingGroup.cs | 71 ++++++ .../Commands/StoryboardRotationCommand.cs | 21 ++ .../Commands/StoryboardScaleCommand.cs | 22 ++ .../StoryboardTriggerGroup.cs} | 7 +- .../Commands/StoryboardVectorScaleCommand.cs | 24 ++ .../Commands/StoryboardXCommand.cs | 21 ++ .../Commands/StoryboardYCommand.cs | 21 ++ .../Drawables/IDrawableStoryboardElement.cs | 4 +- osu.Game/Storyboards/StoryboardAnimation.cs | 4 +- .../StoryboardElementWithDuration.cs | 236 ------------------ osu.Game/Storyboards/StoryboardSprite.cs | 153 +++++++++++- 28 files changed, 713 insertions(+), 561 deletions(-) delete mode 100644 osu.Game/Storyboards/CommandLoop.cs delete mode 100644 osu.Game/Storyboards/CommandTimeline.cs delete mode 100644 osu.Game/Storyboards/CommandTimelineGroup.cs create mode 100644 osu.Game/Storyboards/Commands/IStoryboardCommand.cs create mode 100644 osu.Game/Storyboards/Commands/StoryboardAlphaCommand.cs create mode 100644 osu.Game/Storyboards/Commands/StoryboardBlendingParametersCommand.cs create mode 100644 osu.Game/Storyboards/Commands/StoryboardColourCommand.cs create mode 100644 osu.Game/Storyboards/Commands/StoryboardCommand.cs create mode 100644 osu.Game/Storyboards/Commands/StoryboardCommandGroup.cs create mode 100644 osu.Game/Storyboards/Commands/StoryboardCommandList.cs create mode 100644 osu.Game/Storyboards/Commands/StoryboardFlipHCommand.cs create mode 100644 osu.Game/Storyboards/Commands/StoryboardFlipVCommand.cs create mode 100644 osu.Game/Storyboards/Commands/StoryboardLoopingGroup.cs create mode 100644 osu.Game/Storyboards/Commands/StoryboardRotationCommand.cs create mode 100644 osu.Game/Storyboards/Commands/StoryboardScaleCommand.cs rename osu.Game/Storyboards/{CommandTrigger.cs => Commands/StoryboardTriggerGroup.cs} (68%) create mode 100644 osu.Game/Storyboards/Commands/StoryboardVectorScaleCommand.cs create mode 100644 osu.Game/Storyboards/Commands/StoryboardXCommand.cs create mode 100644 osu.Game/Storyboards/Commands/StoryboardYCommand.cs delete mode 100644 osu.Game/Storyboards/StoryboardElementWithDuration.cs diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableStoryboardSprite.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableStoryboardSprite.cs index 32693c2bb2..6209b42cbb 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableStoryboardSprite.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableStoryboardSprite.cs @@ -175,7 +175,8 @@ namespace osu.Game.Tests.Visual.Gameplay var layer = storyboard.GetLayer("Background"); var sprite = new StoryboardSprite(lookupName, origin, initialPosition); - sprite.AddLoop(Time.Current, 100).Alpha.Add(Easing.None, 0, 10000, 1, 1); + var loop = sprite.AddLoopingGroup(Time.Current, 100); + loop.AddAlpha(0, 10000, 1, 1, Easing.None); layer.Elements.Clear(); layer.Add(sprite); diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneLeadIn.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneLeadIn.cs index dae3119ea4..c3eef4da9b 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneLeadIn.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneLeadIn.cs @@ -47,7 +47,7 @@ namespace osu.Game.Tests.Visual.Gameplay var sprite = new StoryboardSprite("unknown", Anchor.TopLeft, Vector2.Zero); - sprite.TimelineGroup.Alpha.Add(Easing.None, firstStoryboardEvent, firstStoryboardEvent + 500, 0, 1); + sprite.Group.AddAlpha(firstStoryboardEvent, firstStoryboardEvent + 500, 0, 1, Easing.None); storyboard.GetLayer("Background").Add(sprite); @@ -73,17 +73,17 @@ namespace osu.Game.Tests.Visual.Gameplay var sprite = new StoryboardSprite("unknown", Anchor.TopLeft, Vector2.Zero); // these should be ignored as we have an alpha visibility blocker proceeding this command. - sprite.TimelineGroup.Scale.Add(Easing.None, loop_start_time, -18000, Vector2.Zero, Vector2.One); - var loopGroup = sprite.AddLoop(loop_start_time, 50); - loopGroup.Scale.Add(Easing.None, loop_start_time, -18000, Vector2.Zero, Vector2.One); + sprite.Group.AddScale(loop_start_time, -18000, 0, 1, Easing.None); + var loopGroup = sprite.AddLoopingGroup(loop_start_time, 50); + loopGroup.AddScale(loop_start_time, -18000, 0, 1, Easing.None); - var target = addEventToLoop ? loopGroup : sprite.TimelineGroup; + var target = addEventToLoop ? loopGroup : sprite.Group; double loopRelativeOffset = addEventToLoop ? -loop_start_time : 0; - target.Alpha.Add(Easing.None, loopRelativeOffset + firstStoryboardEvent, loopRelativeOffset + firstStoryboardEvent + 500, 0, 1); + target.AddAlpha(loopRelativeOffset + firstStoryboardEvent, loopRelativeOffset + firstStoryboardEvent + 500, 0, 1, Easing.None); // these should be ignored due to being in the future. - sprite.TimelineGroup.Alpha.Add(Easing.None, 18000, 20000, 0, 1); - loopGroup.Alpha.Add(Easing.None, 38000, 40000, 0, 1); + sprite.Group.AddAlpha(18000, 20000, 0, 1, Easing.None); + loopGroup.AddAlpha(38000, 40000, 0, 1, Easing.None); storyboard.GetLayer("Background").Add(sprite); diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardWithOutro.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardWithOutro.cs index f532921d63..9269c3f4ae 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardWithOutro.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardWithOutro.cs @@ -216,7 +216,7 @@ namespace osu.Game.Tests.Visual.Gameplay { var storyboard = new Storyboard(); var sprite = new StoryboardSprite("unknown", Anchor.TopLeft, Vector2.Zero); - sprite.TimelineGroup.Alpha.Add(Easing.None, 0, duration, 1, 0); + sprite.Group.AddAlpha(0, duration, 1, 0, Easing.None); storyboard.GetLayer("Background").Add(sprite); return storyboard; } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs index cebc75f90c..62a2bfeaab 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs @@ -424,7 +424,7 @@ namespace osu.Game.Tests.Visual.Multiplayer public void TestIntroStoryboardElement() => testLeadIn(b => { var sprite = new StoryboardSprite("unknown", Anchor.TopLeft, Vector2.Zero); - sprite.TimelineGroup.Alpha.Add(Easing.None, -2000, 0, 0, 1); + sprite.Group.AddAlpha(-2000, 0, 0, 1, Easing.None); b.Storyboard.GetLayer("Background").Add(sprite); }); diff --git a/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs index ba328b2dbd..33cdaa085e 100644 --- a/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs @@ -10,6 +10,7 @@ using osu.Framework.Utils; using osu.Game.Beatmaps.Legacy; using osu.Game.IO; using osu.Game.Storyboards; +using osu.Game.Storyboards.Commands; using osuTK; using osuTK.Graphics; @@ -17,8 +18,8 @@ namespace osu.Game.Beatmaps.Formats { public class LegacyStoryboardDecoder : LegacyDecoder { - private StoryboardElementWithDuration? storyboardSprite; - private CommandTimelineGroup? timelineGroup; + private StoryboardSprite? storyboardSprite; + private StoryboardCommandGroup? currentGroup; private Storyboard storyboard = null!; @@ -165,7 +166,7 @@ namespace osu.Game.Beatmaps.Formats else { if (depth < 2) - timelineGroup = storyboardSprite?.TimelineGroup; + currentGroup = storyboardSprite?.Group; string commandType = split[0]; @@ -177,7 +178,7 @@ namespace osu.Game.Beatmaps.Formats double startTime = split.Length > 2 ? Parsing.ParseDouble(split[2]) : double.MinValue; double endTime = split.Length > 3 ? Parsing.ParseDouble(split[3]) : double.MaxValue; int groupNumber = split.Length > 4 ? Parsing.ParseInt(split[4]) : 0; - timelineGroup = storyboardSprite?.AddTrigger(triggerName, startTime, endTime, groupNumber); + currentGroup = storyboardSprite?.AddTriggerGroup(triggerName, startTime, endTime, groupNumber); break; } @@ -185,7 +186,7 @@ namespace osu.Game.Beatmaps.Formats { double startTime = Parsing.ParseDouble(split[1]); int repeatCount = Parsing.ParseInt(split[2]); - timelineGroup = storyboardSprite?.AddLoop(startTime, Math.Max(0, repeatCount - 1)); + currentGroup = storyboardSprite?.AddLoopingGroup(startTime, Math.Max(0, repeatCount - 1)); break; } @@ -204,7 +205,7 @@ namespace osu.Game.Beatmaps.Formats { float startValue = Parsing.ParseFloat(split[4]); float endValue = split.Length > 5 ? Parsing.ParseFloat(split[5]) : startValue; - timelineGroup?.Alpha.Add(easing, startTime, endTime, startValue, endValue); + currentGroup?.AddAlpha(startTime, endTime, startValue, endValue, easing); break; } @@ -212,7 +213,7 @@ namespace osu.Game.Beatmaps.Formats { float startValue = Parsing.ParseFloat(split[4]); float endValue = split.Length > 5 ? Parsing.ParseFloat(split[5]) : startValue; - timelineGroup?.Scale.Add(easing, startTime, endTime, new Vector2(startValue), new Vector2(endValue)); + currentGroup?.AddScale(startTime, endTime, startValue, endValue, easing); break; } @@ -222,7 +223,7 @@ namespace osu.Game.Beatmaps.Formats float startY = Parsing.ParseFloat(split[5]); float endX = split.Length > 6 ? Parsing.ParseFloat(split[6]) : startX; float endY = split.Length > 7 ? Parsing.ParseFloat(split[7]) : startY; - timelineGroup?.VectorScale.Add(easing, startTime, endTime, new Vector2(startX, startY), new Vector2(endX, endY)); + currentGroup?.AddVectorScale(startTime, endTime, new Vector2(startX, startY), new Vector2(endX, endY), easing); break; } @@ -230,7 +231,7 @@ namespace osu.Game.Beatmaps.Formats { float startValue = Parsing.ParseFloat(split[4]); float endValue = split.Length > 5 ? Parsing.ParseFloat(split[5]) : startValue; - timelineGroup?.Rotation.Add(easing, startTime, endTime, MathUtils.RadiansToDegrees(startValue), MathUtils.RadiansToDegrees(endValue)); + currentGroup?.AddRotation(startTime, endTime, MathUtils.RadiansToDegrees(startValue), MathUtils.RadiansToDegrees(endValue), easing); break; } @@ -240,8 +241,8 @@ namespace osu.Game.Beatmaps.Formats float startY = Parsing.ParseFloat(split[5]); float endX = split.Length > 6 ? Parsing.ParseFloat(split[6]) : startX; float endY = split.Length > 7 ? Parsing.ParseFloat(split[7]) : startY; - timelineGroup?.X.Add(easing, startTime, endTime, startX, endX); - timelineGroup?.Y.Add(easing, startTime, endTime, startY, endY); + currentGroup?.AddX(startTime, endTime, startX, endX, easing); + currentGroup?.AddY(startTime, endTime, startY, endY, easing); break; } @@ -249,7 +250,7 @@ namespace osu.Game.Beatmaps.Formats { float startValue = Parsing.ParseFloat(split[4]); float endValue = split.Length > 5 ? Parsing.ParseFloat(split[5]) : startValue; - timelineGroup?.X.Add(easing, startTime, endTime, startValue, endValue); + currentGroup?.AddX(startTime, endTime, startValue, endValue, easing); break; } @@ -257,7 +258,7 @@ namespace osu.Game.Beatmaps.Formats { float startValue = Parsing.ParseFloat(split[4]); float endValue = split.Length > 5 ? Parsing.ParseFloat(split[5]) : startValue; - timelineGroup?.Y.Add(easing, startTime, endTime, startValue, endValue); + currentGroup?.AddY(startTime, endTime, startValue, endValue, easing); break; } @@ -269,9 +270,9 @@ namespace osu.Game.Beatmaps.Formats float endRed = split.Length > 7 ? Parsing.ParseFloat(split[7]) : startRed; float endGreen = split.Length > 8 ? Parsing.ParseFloat(split[8]) : startGreen; float endBlue = split.Length > 9 ? Parsing.ParseFloat(split[9]) : startBlue; - timelineGroup?.Colour.Add(easing, startTime, endTime, + currentGroup?.AddColour(startTime, endTime, new Color4(startRed / 255f, startGreen / 255f, startBlue / 255f, 1), - new Color4(endRed / 255f, endGreen / 255f, endBlue / 255f, 1)); + new Color4(endRed / 255f, endGreen / 255f, endBlue / 255f, 1), easing); break; } @@ -282,16 +283,16 @@ namespace osu.Game.Beatmaps.Formats switch (type) { case "A": - timelineGroup?.BlendingParameters.Add(easing, startTime, endTime, BlendingParameters.Additive, - startTime == endTime ? BlendingParameters.Additive : BlendingParameters.Inherit); + currentGroup?.AddBlendingParameters(startTime, endTime, BlendingParameters.Additive, + startTime == endTime ? BlendingParameters.Additive : BlendingParameters.Inherit, easing); break; case "H": - timelineGroup?.FlipH.Add(easing, startTime, endTime, true, startTime == endTime); + currentGroup?.AddFlipH(startTime, endTime, true, startTime == endTime, easing); break; case "V": - timelineGroup?.FlipV.Add(easing, startTime, endTime, true, startTime == endTime); + currentGroup?.AddFlipV(startTime, endTime, true, startTime == endTime, easing); break; } diff --git a/osu.Game/Storyboards/CommandLoop.cs b/osu.Game/Storyboards/CommandLoop.cs deleted file mode 100644 index 6dd782cb7f..0000000000 --- a/osu.Game/Storyboards/CommandLoop.cs +++ /dev/null @@ -1,63 +0,0 @@ -// 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; - -namespace osu.Game.Storyboards -{ - public class CommandLoop : CommandTimelineGroup - { - public double LoopStartTime; - - /// - /// The total number of times this loop is played back. Always greater than zero. - /// - public readonly int TotalIterations; - - public override double StartTime => LoopStartTime + CommandsStartTime; - - public override double EndTime => - // In an ideal world, we would multiply the command duration by TotalIterations here. - // Unfortunately this would clash with how stable handled end times, and results in some storyboards playing outro - // sequences for minutes or hours. - StartTime + CommandsDuration; - - /// - /// Construct a new command loop. - /// - /// The start time of the loop. - /// The number of times the loop should repeat. Should be greater than zero. Zero means a single playback. - public CommandLoop(double startTime, int repeatCount) - { - if (repeatCount < 0) throw new ArgumentException("Repeat count must be zero or above.", nameof(repeatCount)); - - LoopStartTime = startTime; - TotalIterations = repeatCount + 1; - } - - public override IEnumerable.TypedCommand> GetCommands(CommandTimelineSelector timelineSelector, double offset = 0) - { - double fullLoopDuration = CommandsEndTime - CommandsStartTime; - - foreach (var command in timelineSelector(this).Commands) - { - yield return new CommandTimeline.TypedCommand - { - Easing = command.Easing, - StartTime = offset + LoopStartTime + command.StartTime, - EndTime = offset + LoopStartTime + command.EndTime, - StartValue = command.StartValue, - EndValue = command.EndValue, - PropertyName = command.PropertyName, - IsParameterCommand = command.IsParameterCommand, - LoopCount = TotalIterations, - Delay = fullLoopDuration - command.EndTime + command.StartTime - }; - } - } - - public override string ToString() - => $"{LoopStartTime} x{TotalIterations}"; - } -} diff --git a/osu.Game/Storyboards/CommandTimeline.cs b/osu.Game/Storyboards/CommandTimeline.cs deleted file mode 100644 index 4ad31d88c2..0000000000 --- a/osu.Game/Storyboards/CommandTimeline.cs +++ /dev/null @@ -1,101 +0,0 @@ -// 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.Graphics; -using System; -using System.Collections.Generic; -using System.Linq; - -namespace osu.Game.Storyboards -{ - public class CommandTimeline : ICommandTimeline - { - private readonly List commands = new List(); - - public IEnumerable Commands => commands.OrderBy(c => c.StartTime); - - public bool HasCommands => commands.Count > 0; - - public double StartTime { get; private set; } = double.MaxValue; - public double EndTime { get; private set; } = double.MinValue; - - public T StartValue { get; private set; } - public T EndValue { get; private set; } - - public string PropertyName { get; } - public bool IsParameterTimeline { get; set; } - - public CommandTimeline(string propertyName) - { - PropertyName = propertyName; - } - - public void Add(Easing easing, double startTime, double endTime, T startValue, T endValue) - { - if (endTime < startTime) - { - endTime = startTime; - } - - commands.Add(new TypedCommand { Easing = easing, StartTime = startTime, EndTime = endTime, StartValue = startValue, EndValue = endValue, PropertyName = PropertyName, IsParameterCommand = IsParameterTimeline }); - - if (startTime < StartTime) - { - StartValue = startValue; - StartTime = startTime; - } - - if (endTime > EndTime) - { - EndValue = endValue; - EndTime = endTime; - } - } - - public override string ToString() - => $"{commands.Count} command(s)"; - - public class TypedCommand : ICommand - { - public Easing Easing { get; set; } - public double StartTime { get; set; } - public double EndTime { get; set; } - public double Duration => EndTime - StartTime; - public string PropertyName { get; set; } - public int LoopCount { get; set; } - public double Delay { get; set; } - public bool IsParameterCommand { get; set; } - - public T StartValue; - public T EndValue; - - public int CompareTo(ICommand other) - { - int result = StartTime.CompareTo(other.StartTime); - if (result != 0) return result; - - return EndTime.CompareTo(other.EndTime); - } - - public override string ToString() - => $"{StartTime} -> {EndTime}, {StartValue} -> {EndValue} {Easing}"; - } - } - - public interface ICommandTimeline - { - double StartTime { get; } - double EndTime { get; } - bool HasCommands { get; } - } - - public interface ICommand : IComparable - { - Easing Easing { get; set; } - double StartTime { get; set; } - double EndTime { get; set; } - double Duration { get; } - } -} diff --git a/osu.Game/Storyboards/CommandTimelineGroup.cs b/osu.Game/Storyboards/CommandTimelineGroup.cs deleted file mode 100644 index 0362925619..0000000000 --- a/osu.Game/Storyboards/CommandTimelineGroup.cs +++ /dev/null @@ -1,120 +0,0 @@ -// 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 osuTK; -using osu.Framework.Graphics; -using System.Collections.Generic; -using System.Linq; -using Newtonsoft.Json; -using osu.Framework.Graphics.Colour; - -namespace osu.Game.Storyboards -{ - public delegate CommandTimeline CommandTimelineSelector(CommandTimelineGroup commandTimelineGroup); - - public class CommandTimelineGroup - { - public CommandTimeline X = new CommandTimeline("X"); - public CommandTimeline Y = new CommandTimeline("Y"); - public CommandTimeline Scale = new CommandTimeline("Scale"); - public CommandTimeline VectorScale = new CommandTimeline("VectorScale"); - public CommandTimeline Rotation = new CommandTimeline("Rotation"); - public CommandTimeline Colour = new CommandTimeline("Colour"); - public CommandTimeline Alpha = new CommandTimeline("Alpha"); - public CommandTimeline BlendingParameters = new CommandTimeline("Blending") { IsParameterTimeline = true }; - public CommandTimeline FlipH = new CommandTimeline("FlipH") { IsParameterTimeline = true }; - public CommandTimeline FlipV = new CommandTimeline("FlipV") { IsParameterTimeline = true }; - - private readonly ICommandTimeline[] timelines; - - public CommandTimelineGroup() - { - timelines = new ICommandTimeline[] - { - X, - Y, - Scale, - VectorScale, - Rotation, - Colour, - Alpha, - BlendingParameters, - FlipH, - FlipV - }; - } - - [JsonIgnore] - public double CommandsStartTime - { - get - { - double min = double.MaxValue; - - for (int i = 0; i < timelines.Length; i++) - min = Math.Min(min, timelines[i].StartTime); - - return min; - } - } - - [JsonIgnore] - public double CommandsEndTime - { - get - { - double max = double.MinValue; - - for (int i = 0; i < timelines.Length; i++) - max = Math.Max(max, timelines[i].EndTime); - - return max; - } - } - - [JsonIgnore] - public double CommandsDuration => CommandsEndTime - CommandsStartTime; - - [JsonIgnore] - public virtual double StartTime => CommandsStartTime; - - [JsonIgnore] - public virtual double EndTime => CommandsEndTime; - - [JsonIgnore] - public bool HasCommands - { - get - { - for (int i = 0; i < timelines.Length; i++) - { - if (timelines[i].HasCommands) - return true; - } - - return false; - } - } - - public virtual IEnumerable.TypedCommand> GetCommands(CommandTimelineSelector timelineSelector, double offset = 0) - { - if (offset != 0) - { - return timelineSelector(this).Commands.Select(command => - new CommandTimeline.TypedCommand - { - Easing = command.Easing, - StartTime = offset + command.StartTime, - EndTime = offset + command.EndTime, - StartValue = command.StartValue, - EndValue = command.EndValue, - PropertyName = command.PropertyName, - IsParameterCommand = command.IsParameterCommand - }); - } - - return timelineSelector(this).Commands; - } - } -} diff --git a/osu.Game/Storyboards/Commands/IStoryboardCommand.cs b/osu.Game/Storyboards/Commands/IStoryboardCommand.cs new file mode 100644 index 0000000000..848dcab575 --- /dev/null +++ b/osu.Game/Storyboards/Commands/IStoryboardCommand.cs @@ -0,0 +1,28 @@ +// 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.Graphics; +using osu.Framework.Graphics.Transforms; + +namespace osu.Game.Storyboards.Commands +{ + public interface IStoryboardCommand + { + /// + /// The start time of the storyboard command. + /// + double StartTime { get; } + + /// + /// The end time of the storyboard command. + /// + double EndTime { get; } + + /// + /// Applies the transforms described by this storyboard command to the target drawable. + /// + /// The target drawable. + /// The sequence of transforms applied to the target drawable. + TransformSequence ApplyTransform(Drawable d); + } +} diff --git a/osu.Game/Storyboards/Commands/StoryboardAlphaCommand.cs b/osu.Game/Storyboards/Commands/StoryboardAlphaCommand.cs new file mode 100644 index 0000000000..729ecd72a7 --- /dev/null +++ b/osu.Game/Storyboards/Commands/StoryboardAlphaCommand.cs @@ -0,0 +1,19 @@ +// 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.Graphics; +using osu.Framework.Graphics.Transforms; + +namespace osu.Game.Storyboards.Commands +{ + public class StoryboardAlphaCommand : StoryboardCommand + { + public StoryboardAlphaCommand(double startTime, double endTime, float startValue, float endValue, Easing easing) + : base(startTime, endTime, startValue, endValue, easing) + { + } + + public override void SetInitialValue(Drawable d) => d.Alpha = StartValue; + public override TransformSequence ApplyTransform(Drawable d) => d.FadeTo(StartValue).Then().FadeTo(EndValue, Duration, Easing); + } +} diff --git a/osu.Game/Storyboards/Commands/StoryboardBlendingParametersCommand.cs b/osu.Game/Storyboards/Commands/StoryboardBlendingParametersCommand.cs new file mode 100644 index 0000000000..cc54909837 --- /dev/null +++ b/osu.Game/Storyboards/Commands/StoryboardBlendingParametersCommand.cs @@ -0,0 +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 osu.Framework.Graphics; +using osu.Framework.Graphics.Transforms; + +namespace osu.Game.Storyboards.Commands +{ + public class StoryboardBlendingParametersCommand : StoryboardCommand + { + public StoryboardBlendingParametersCommand(double startTime, double endTime, BlendingParameters startValue, BlendingParameters endValue, Easing easing) + : base(startTime, endTime, startValue, endValue, easing) + { + } + + public override void SetInitialValue(Drawable d) => d.Blending = StartValue; + + public override TransformSequence ApplyTransform(Drawable d) + => d.TransformTo(nameof(d.Blending), StartValue).Delay(Duration).TransformTo(nameof(d.Blending), EndValue); + } +} diff --git a/osu.Game/Storyboards/Commands/StoryboardColourCommand.cs b/osu.Game/Storyboards/Commands/StoryboardColourCommand.cs new file mode 100644 index 0000000000..be56a1d71b --- /dev/null +++ b/osu.Game/Storyboards/Commands/StoryboardColourCommand.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.Graphics; +using osu.Framework.Graphics.Transforms; +using osuTK.Graphics; + +namespace osu.Game.Storyboards.Commands +{ + public class StoryboardColourCommand : StoryboardCommand + { + public StoryboardColourCommand(double startTime, double endTime, Color4 startValue, Color4 endValue, Easing easing) + : base(startTime, endTime, startValue, endValue, easing) + { + } + + public override void SetInitialValue(Drawable d) => d.Colour = StartValue; + public override TransformSequence ApplyTransform(Drawable d) => d.FadeColour(StartValue).Then().FadeColour(EndValue, Duration, Easing); + } +} diff --git a/osu.Game/Storyboards/Commands/StoryboardCommand.cs b/osu.Game/Storyboards/Commands/StoryboardCommand.cs new file mode 100644 index 0000000000..883b9f57c1 --- /dev/null +++ b/osu.Game/Storyboards/Commands/StoryboardCommand.cs @@ -0,0 +1,54 @@ +// 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.Graphics; +using osu.Framework.Graphics.Transforms; + +namespace osu.Game.Storyboards.Commands +{ + public abstract class StoryboardCommand : IStoryboardCommand + { + public double StartTime { get; } + public double EndTime { get; } + public double Duration => EndTime - StartTime; + + protected StoryboardCommand(double startTime, double endTime, T startValue, T endValue, Easing easing) + { + if (endTime < startTime) + endTime = startTime; + + StartTime = startTime; + StartValue = startValue; + EndTime = endTime; + EndValue = endValue; + Easing = easing; + } + + public Easing Easing { get; set; } + public int LoopCount { get; set; } + public double Delay { get; set; } + + public T StartValue; + public T EndValue; + + /// + /// Sets the value of the corresponding property in to the start value of this command. + /// + public abstract void SetInitialValue(Drawable d); + + /// + /// Transforms a corresponding property in that corresponds to this command group with the specified parameters. + /// + public abstract TransformSequence ApplyTransform(Drawable d); + + public int CompareTo(IStoryboardCommand other) + { + int result = StartTime.CompareTo(other.StartTime); + if (result != 0) return result; + + return EndTime.CompareTo(other.EndTime); + } + + public override string ToString() => $"{StartTime} -> {EndTime}, {StartValue} -> {EndValue} {Easing}"; + } +} diff --git a/osu.Game/Storyboards/Commands/StoryboardCommandGroup.cs b/osu.Game/Storyboards/Commands/StoryboardCommandGroup.cs new file mode 100644 index 0000000000..02c43c9f60 --- /dev/null +++ b/osu.Game/Storyboards/Commands/StoryboardCommandGroup.cs @@ -0,0 +1,115 @@ +// 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 Newtonsoft.Json; +using osu.Framework.Graphics; +using osu.Framework.Lists; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Storyboards.Commands +{ + public class StoryboardCommandGroup + { + public SortedList> X; + public SortedList> Y; + public SortedList> Scale; + public SortedList> VectorScale; + public SortedList> Rotation; + public SortedList> Colour; + public SortedList> Alpha; + public SortedList> BlendingParameters; + public SortedList> FlipH; + public SortedList> FlipV; + + private readonly IReadOnlyList[] lists; + + /// + /// Returns the earliest start time of the commands added to this group. + /// + [JsonIgnore] + public double StartTime { get; private set; } + + /// + /// Returns the latest end time of the commands added to this group. + /// + [JsonIgnore] + public double EndTime { get; private set; } + + [JsonIgnore] + public double Duration => EndTime - StartTime; + + [JsonIgnore] + public bool HasCommands { get; private set; } + + public StoryboardCommandGroup() + { + lists = new IReadOnlyList[] + { + X = new SortedList>(), + Y = new SortedList>(), + Scale = new SortedList>(), + VectorScale = new SortedList>(), + Rotation = new SortedList>(), + Colour = new SortedList>(), + Alpha = new SortedList>(), + BlendingParameters = new SortedList>(), + FlipH = new SortedList>(), + FlipV = new SortedList>() + }; + } + + /// + /// Returns all commands contained by this group unsorted. + /// + public virtual IEnumerable GetAllCommands() => lists.SelectMany(l => l); + + public void AddX(double startTime, double endTime, float startValue, float endValue, Easing easing) + => AddCommand(X, new StoryboardXCommand(startTime, endTime, startValue, endValue, easing)); + + public void AddY(double startTime, double endTime, float startValue, float endValue, Easing easing) + => AddCommand(Y, new StoryboardYCommand(startTime, endTime, startValue, endValue, easing)); + + public void AddScale(double startTime, double endTime, float startValue, float endValue, Easing easing) + => AddCommand(Scale, new StoryboardScaleCommand(startTime, endTime, startValue, endValue, easing)); + + public void AddVectorScale(double startTime, double endTime, Vector2 startValue, Vector2 endValue, Easing easing) + => AddCommand(VectorScale, new StoryboardVectorScaleCommand(startTime, endTime, startValue, endValue, easing)); + + public void AddRotation(double startTime, double endTime, float startValue, float endValue, Easing easing) + => AddCommand(Rotation, new StoryboardRotationCommand(startTime, endTime, startValue, endValue, easing)); + + public void AddColour(double startTime, double endTime, Color4 startValue, Color4 endValue, Easing easing) + => AddCommand(Colour, new StoryboardColourCommand(startTime, endTime, startValue, endValue, easing)); + + public void AddAlpha(double startTime, double endTime, float startValue, float endValue, Easing easing) + => AddCommand(Alpha, new StoryboardAlphaCommand(startTime, endTime, startValue, endValue, easing)); + + public void AddBlendingParameters(double startTime, double endTime, BlendingParameters startValue, BlendingParameters endValue, Easing easing) + => AddCommand(BlendingParameters, new StoryboardBlendingParametersCommand(startTime, endTime, startValue, endValue, easing)); + + public void AddFlipH(double startTime, double endTime, bool startValue, bool endValue, Easing easing) + => AddCommand(FlipH, new StoryboardFlipHCommand(startTime, endTime, startValue, endValue, easing)); + + public void AddFlipV(double startTime, double endTime, bool startValue, bool endValue, Easing easing) + => AddCommand(FlipV, new StoryboardFlipVCommand(startTime, endTime, startValue, endValue, easing)); + + /// + /// Adds the given storyboard to the target . + /// Can be overriden to apply custom effects to the given command before adding it to the list (e.g. looping or time offsets). + /// + /// The value type of the target property affected by this storyboard command. + protected virtual void AddCommand(ICollection> list, StoryboardCommand command) + { + list.Add(command); + + if (command.StartTime < StartTime) + StartTime = command.StartTime; + + if (command.EndTime > EndTime) + EndTime = command.EndTime; + } + } +} diff --git a/osu.Game/Storyboards/Commands/StoryboardCommandList.cs b/osu.Game/Storyboards/Commands/StoryboardCommandList.cs new file mode 100644 index 0000000000..67012e9d49 --- /dev/null +++ b/osu.Game/Storyboards/Commands/StoryboardCommandList.cs @@ -0,0 +1,41 @@ +// 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.Storyboards.Commands +{ + // public class StoryboardCommandList : IStoryboardCommandList + // { + // // todo: change to sorted list and avoid enumerable type on exposed properties? + // private readonly List> commands = new List>(); + // + // public IEnumerable> Commands => commands.OrderBy(c => c.StartTime); + // + // IEnumerable IStoryboardCommandList.Commands => Commands; + // public bool HasCommands => commands.Count > 0; + // + // public double StartTime { get; private set; } = double.MaxValue; + // public double EndTime { get; private set; } = double.MinValue; + // + // public T? StartValue { get; private set; } + // public T? EndValue { get; private set; } + // + // public void Add(StoryboardCommand command) + // { + // commands.Add(command); + // + // if (command.StartTime < StartTime) + // { + // StartValue = command.StartValue; + // StartTime = command.StartTime; + // } + // + // if (command.EndTime > EndTime) + // { + // EndValue = command.EndValue; + // EndTime = command.EndTime; + // } + // } + // + // public override string ToString() => $"{commands.Count} command(s)"; + // } +} diff --git a/osu.Game/Storyboards/Commands/StoryboardFlipHCommand.cs b/osu.Game/Storyboards/Commands/StoryboardFlipHCommand.cs new file mode 100644 index 0000000000..9bcb687d3c --- /dev/null +++ b/osu.Game/Storyboards/Commands/StoryboardFlipHCommand.cs @@ -0,0 +1,23 @@ +// 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.Graphics; +using osu.Framework.Graphics.Transforms; +using osu.Game.Storyboards.Drawables; + +namespace osu.Game.Storyboards.Commands +{ + public class StoryboardFlipHCommand : StoryboardCommand + { + public StoryboardFlipHCommand(double startTime, double endTime, bool startValue, bool endValue, Easing easing) + : base(startTime, endTime, startValue, endValue, easing) + { + } + + public override void SetInitialValue(Drawable d) => ((IDrawableStoryboardElement)d).FlipH = StartValue; + + public override TransformSequence ApplyTransform(Drawable d) + => d.TransformTo(nameof(IDrawableStoryboardElement.FlipH), StartValue).Delay(Duration) + .TransformTo(nameof(IDrawableStoryboardElement.FlipH), EndValue); + } +} diff --git a/osu.Game/Storyboards/Commands/StoryboardFlipVCommand.cs b/osu.Game/Storyboards/Commands/StoryboardFlipVCommand.cs new file mode 100644 index 0000000000..9f1f5faa33 --- /dev/null +++ b/osu.Game/Storyboards/Commands/StoryboardFlipVCommand.cs @@ -0,0 +1,23 @@ +// 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.Graphics; +using osu.Framework.Graphics.Transforms; +using osu.Game.Storyboards.Drawables; + +namespace osu.Game.Storyboards.Commands +{ + public class StoryboardFlipVCommand : StoryboardCommand + { + public StoryboardFlipVCommand(double startTime, double endTime, bool startValue, bool endValue, Easing easing) + : base(startTime, endTime, startValue, endValue, easing) + { + } + + public override void SetInitialValue(Drawable d) => ((IDrawableStoryboardElement)d).FlipV = StartValue; + + public override TransformSequence ApplyTransform(Drawable d) + => d.TransformTo(nameof(IDrawableStoryboardElement.FlipV), StartValue).Delay(Duration) + .TransformTo(nameof(IDrawableStoryboardElement.FlipV), EndValue); + } +} diff --git a/osu.Game/Storyboards/Commands/StoryboardLoopingGroup.cs b/osu.Game/Storyboards/Commands/StoryboardLoopingGroup.cs new file mode 100644 index 0000000000..e520353bd6 --- /dev/null +++ b/osu.Game/Storyboards/Commands/StoryboardLoopingGroup.cs @@ -0,0 +1,71 @@ +// 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 osu.Framework.Graphics; +using osu.Framework.Graphics.Transforms; + +namespace osu.Game.Storyboards.Commands +{ + public class StoryboardLoopingGroup : StoryboardCommandGroup + { + public double LoopStartTime; + + /// + /// The total number of times this loop is played back. Always greater than zero. + /// + public readonly int TotalIterations; + + /// + /// Construct a new command loop. + /// + /// The start time of the loop. + /// The number of times the loop should repeat. Should be greater than zero. Zero means a single playback. + public StoryboardLoopingGroup(double startTime, int repeatCount) + { + if (repeatCount < 0) throw new ArgumentException("Repeat count must be zero or above.", nameof(repeatCount)); + + LoopStartTime = startTime; + TotalIterations = repeatCount + 1; + } + + protected override void AddCommand(ICollection> list, StoryboardCommand command) + { + // todo: this is broke! + double fullLoopDuration = EndTime - StartTime; + double loopDelay = fullLoopDuration - command.EndTime + command.StartTime; + base.AddCommand(list, new StoryboardLoopingCommand(command, LoopStartTime, TotalIterations, loopDelay)); + } + + public override string ToString() => $"{LoopStartTime} x{TotalIterations}"; + + private class StoryboardLoopingCommand : StoryboardCommand + { + private readonly StoryboardCommand command; + private readonly int loopCount; + private readonly double loopDelay; + + public StoryboardLoopingCommand(StoryboardCommand command, double loopStartTime, int loopCount, double loopDelay) + // In an ideal world, we would multiply the command duration by TotalIterations in command end time. + // Unfortunately this would clash with how stable handled end times, and results in some storyboards playing outro + // sequences for minutes or hours. + : base(loopStartTime + command.StartTime, loopStartTime + command.EndTime, command.StartValue, command.EndValue, command.Easing) + { + this.command = command; + this.loopCount = loopCount; + this.loopDelay = loopDelay; + } + + public override void SetInitialValue(Drawable d) => command.SetInitialValue(d); + + public override TransformSequence ApplyTransform(Drawable d) + { + if (loopCount == 0) + return command.ApplyTransform(d); + + return command.ApplyTransform(d).Loop(loopDelay, loopCount); + } + } + } +} diff --git a/osu.Game/Storyboards/Commands/StoryboardRotationCommand.cs b/osu.Game/Storyboards/Commands/StoryboardRotationCommand.cs new file mode 100644 index 0000000000..c56dcd130f --- /dev/null +++ b/osu.Game/Storyboards/Commands/StoryboardRotationCommand.cs @@ -0,0 +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 osu.Framework.Graphics; +using osu.Framework.Graphics.Transforms; + +namespace osu.Game.Storyboards.Commands +{ + public class StoryboardRotationCommand : StoryboardCommand + { + public StoryboardRotationCommand(double startTime, double endTime, float startValue, float endValue, Easing easing) + : base(startTime, endTime, startValue, endValue, easing) + { + } + + public override void SetInitialValue(Drawable d) => d.Rotation = StartValue; + + public override TransformSequence ApplyTransform(Drawable d) + => d.RotateTo(StartValue).Then().RotateTo(EndValue, Duration, Easing); + } +} diff --git a/osu.Game/Storyboards/Commands/StoryboardScaleCommand.cs b/osu.Game/Storyboards/Commands/StoryboardScaleCommand.cs new file mode 100644 index 0000000000..9dbdd6ebd8 --- /dev/null +++ b/osu.Game/Storyboards/Commands/StoryboardScaleCommand.cs @@ -0,0 +1,22 @@ +// 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.Graphics; +using osu.Framework.Graphics.Transforms; +using osuTK; + +namespace osu.Game.Storyboards.Commands +{ + public class StoryboardScaleCommand : StoryboardCommand + { + public StoryboardScaleCommand(double startTime, double endTime, float startValue, float endValue, Easing easing) + : base(startTime, endTime, startValue, endValue, easing) + { + } + + public override void SetInitialValue(Drawable d) => d.Scale = new Vector2(StartValue); + + public override TransformSequence ApplyTransform(Drawable d) + => d.ScaleTo(StartValue).Then().ScaleTo(EndValue, Duration, Easing); + } +} diff --git a/osu.Game/Storyboards/CommandTrigger.cs b/osu.Game/Storyboards/Commands/StoryboardTriggerGroup.cs similarity index 68% rename from osu.Game/Storyboards/CommandTrigger.cs rename to osu.Game/Storyboards/Commands/StoryboardTriggerGroup.cs index 011f345df2..dfb6f8cb1b 100644 --- a/osu.Game/Storyboards/CommandTrigger.cs +++ b/osu.Game/Storyboards/Commands/StoryboardTriggerGroup.cs @@ -1,16 +1,17 @@ // 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.Storyboards +namespace osu.Game.Storyboards.Commands { - public class CommandTrigger : CommandTimelineGroup + // todo: this is not implemented and has never been, keep that in mind. + public class StoryboardTriggerGroup : StoryboardCommandGroup { public string TriggerName; public double TriggerStartTime; public double TriggerEndTime; public int GroupNumber; - public CommandTrigger(string triggerName, double startTime, double endTime, int groupNumber) + public StoryboardTriggerGroup(string triggerName, double startTime, double endTime, int groupNumber) { TriggerName = triggerName; TriggerStartTime = startTime; diff --git a/osu.Game/Storyboards/Commands/StoryboardVectorScaleCommand.cs b/osu.Game/Storyboards/Commands/StoryboardVectorScaleCommand.cs new file mode 100644 index 0000000000..fefb21b257 --- /dev/null +++ b/osu.Game/Storyboards/Commands/StoryboardVectorScaleCommand.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 osu.Framework.Graphics; +using osu.Framework.Graphics.Transforms; +using osu.Game.Storyboards.Drawables; +using osuTK; + +namespace osu.Game.Storyboards.Commands +{ + public class StoryboardVectorScaleCommand : StoryboardCommand + { + public StoryboardVectorScaleCommand(double startTime, double endTime, Vector2 startValue, Vector2 endValue, Easing easing) + : base(startTime, endTime, startValue, endValue, easing) + { + } + + public override void SetInitialValue(Drawable d) => ((IDrawableStoryboardElement)d).VectorScale = StartValue; + + public override TransformSequence ApplyTransform(Drawable d) + => d.TransformTo(nameof(IDrawableStoryboardElement.VectorScale), StartValue).Then() + .TransformTo(nameof(IDrawableStoryboardElement.VectorScale), EndValue, Duration, Easing); + } +} diff --git a/osu.Game/Storyboards/Commands/StoryboardXCommand.cs b/osu.Game/Storyboards/Commands/StoryboardXCommand.cs new file mode 100644 index 0000000000..a9f9cd4f8f --- /dev/null +++ b/osu.Game/Storyboards/Commands/StoryboardXCommand.cs @@ -0,0 +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 osu.Framework.Graphics; +using osu.Framework.Graphics.Transforms; + +namespace osu.Game.Storyboards.Commands +{ + public class StoryboardXCommand : StoryboardCommand + { + public StoryboardXCommand(double startTime, double endTime, float startValue, float endValue, Easing easing) + : base(startTime, endTime, startValue, endValue, easing) + { + } + + public override void SetInitialValue(Drawable d) => d.X = StartValue; + + public override TransformSequence ApplyTransform(Drawable d) + => d.MoveToX(StartValue).Then().MoveToX(EndValue, Duration, Easing); + } +} diff --git a/osu.Game/Storyboards/Commands/StoryboardYCommand.cs b/osu.Game/Storyboards/Commands/StoryboardYCommand.cs new file mode 100644 index 0000000000..eb30b36720 --- /dev/null +++ b/osu.Game/Storyboards/Commands/StoryboardYCommand.cs @@ -0,0 +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 osu.Framework.Graphics; +using osu.Framework.Graphics.Transforms; + +namespace osu.Game.Storyboards.Commands +{ + public class StoryboardYCommand : StoryboardCommand + { + public StoryboardYCommand(double startTime, double endTime, float startValue, float endValue, Easing easing) + : base(startTime, endTime, startValue, endValue, easing) + { + } + + public override void SetInitialValue(Drawable d) => d.Y = StartValue; + + public override TransformSequence ApplyTransform(Drawable d) + => d.MoveToY(StartValue).Then().MoveToY(EndValue, Duration, Easing); + } +} diff --git a/osu.Game/Storyboards/Drawables/IDrawableStoryboardElement.cs b/osu.Game/Storyboards/Drawables/IDrawableStoryboardElement.cs index 6652b5509c..04bae88c76 100644 --- a/osu.Game/Storyboards/Drawables/IDrawableStoryboardElement.cs +++ b/osu.Game/Storyboards/Drawables/IDrawableStoryboardElement.cs @@ -1,12 +1,12 @@ // 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.Graphics.Transforms; +using osu.Framework.Graphics; using osuTK; namespace osu.Game.Storyboards.Drawables { - public interface IDrawableStoryboardElement : ITransformable + public interface IDrawableStoryboardElement : IDrawable { bool FlipH { get; set; } bool FlipV { get; set; } diff --git a/osu.Game/Storyboards/StoryboardAnimation.cs b/osu.Game/Storyboards/StoryboardAnimation.cs index 173acf7ff1..0b714633c9 100644 --- a/osu.Game/Storyboards/StoryboardAnimation.cs +++ b/osu.Game/Storyboards/StoryboardAnimation.cs @@ -7,7 +7,7 @@ using osu.Game.Storyboards.Drawables; namespace osu.Game.Storyboards { - public class StoryboardAnimation : StoryboardElementWithDuration + public class StoryboardAnimation : StoryboardSprite { public int FrameCount; public double FrameDelay; @@ -21,7 +21,7 @@ namespace osu.Game.Storyboards LoopType = loopType; } - public override DrawableStoryboardAnimation CreateStoryboardDrawable() => new DrawableStoryboardAnimation(this); + public override Drawable CreateDrawable() => new DrawableStoryboardAnimation(this); } public enum AnimationLoopType diff --git a/osu.Game/Storyboards/StoryboardElementWithDuration.cs b/osu.Game/Storyboards/StoryboardElementWithDuration.cs deleted file mode 100644 index 06924a26ef..0000000000 --- a/osu.Game/Storyboards/StoryboardElementWithDuration.cs +++ /dev/null @@ -1,236 +0,0 @@ -// 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.Graphics; -using osu.Game.Storyboards.Drawables; -using osuTK; - -namespace osu.Game.Storyboards -{ - public abstract class StoryboardElementWithDuration : IStoryboardElementWithDuration - { - protected readonly List Loops = new List(); - private readonly List triggers = new List(); - - public string Path { get; } - public bool IsDrawable => HasCommands; - - public Anchor Origin; - public Vector2 InitialPosition; - - public readonly CommandTimelineGroup TimelineGroup = new CommandTimelineGroup(); - - public double StartTime - { - get - { - // To get the initial start time, we need to check whether the first alpha command to exist (across all loops) has a StartValue of zero. - // A StartValue of zero governs, above all else, the first valid display time of a sprite. - // - // You can imagine that the first command of each type decides that type's start value, so if the initial alpha is zero, - // anything before that point can be ignored (the sprite is not visible after all). - var alphaCommands = new List<(double startTime, bool isZeroStartValue)>(); - - var command = TimelineGroup.Alpha.Commands.FirstOrDefault(); - if (command != null) alphaCommands.Add((command.StartTime, command.StartValue == 0)); - - foreach (var loop in Loops) - { - command = loop.Alpha.Commands.FirstOrDefault(); - if (command != null) alphaCommands.Add((command.StartTime + loop.LoopStartTime, command.StartValue == 0)); - } - - if (alphaCommands.Count > 0) - { - var firstAlpha = alphaCommands.MinBy(t => t.startTime); - - if (firstAlpha.isZeroStartValue) - return firstAlpha.startTime; - } - - return EarliestTransformTime; - } - } - - public double EarliestTransformTime - { - get - { - // If we got to this point, either no alpha commands were present, or the earliest had a non-zero start value. - // The sprite's StartTime will be determined by the earliest command, regardless of type. - double earliestStartTime = TimelineGroup.StartTime; - foreach (var l in Loops) - earliestStartTime = Math.Min(earliestStartTime, l.StartTime); - return earliestStartTime; - } - } - - public double EndTime - { - get - { - double latestEndTime = TimelineGroup.EndTime; - - foreach (var l in Loops) - latestEndTime = Math.Max(latestEndTime, l.EndTime); - - return latestEndTime; - } - } - - public double EndTimeForDisplay - { - get - { - double latestEndTime = TimelineGroup.EndTime; - - foreach (var l in Loops) - latestEndTime = Math.Max(latestEndTime, l.StartTime + l.CommandsDuration * l.TotalIterations); - - return latestEndTime; - } - } - - public bool HasCommands => TimelineGroup.HasCommands || Loops.Any(l => l.HasCommands); - - protected StoryboardElementWithDuration(string path, Anchor origin, Vector2 initialPosition) - { - Path = path; - Origin = origin; - InitialPosition = initialPosition; - } - - public abstract Drawable CreateDrawable(); - - public CommandLoop AddLoop(double startTime, int repeatCount) - { - var loop = new CommandLoop(startTime, repeatCount); - Loops.Add(loop); - return loop; - } - - public CommandTrigger AddTrigger(string triggerName, double startTime, double endTime, int groupNumber) - { - var trigger = new CommandTrigger(triggerName, startTime, endTime, groupNumber); - triggers.Add(trigger); - return trigger; - } - - protected IEnumerable.TypedCommand> GetCommands(CommandTimelineSelector timelineSelector, IEnumerable>? triggeredGroups) - { - var commands = TimelineGroup.GetCommands(timelineSelector); - foreach (var loop in Loops) - commands = commands.Concat(loop.GetCommands(timelineSelector)); - - if (triggeredGroups != null) - { - foreach (var pair in triggeredGroups) - commands = commands.Concat(pair.Item1.GetCommands(timelineSelector, pair.Item2)); - } - - return commands; - } - - public override string ToString() - => $"{Path}, {Origin}, {InitialPosition}"; - } - - public abstract class StoryboardElementWithDuration : StoryboardElementWithDuration - where U : Drawable, IDrawableStoryboardElement - { - private delegate void DrawablePropertyInitializer(U drawable, T value); - - protected StoryboardElementWithDuration(string path, Anchor origin, Vector2 initialPosition) - : base(path, origin, initialPosition) - { - } - - public override Drawable CreateDrawable() => CreateStoryboardDrawable(); - - public abstract U CreateStoryboardDrawable(); - - public void ApplyTransforms(U drawable, IEnumerable>? triggeredGroups = null) - { - // For performance reasons, we need to apply the commands in order by start time. Not doing so will cause many functions to be interleaved, resulting in O(n^2) complexity. - // To achieve this, commands are "generated" as pairs of (command, initFunc, transformFunc) and batched into a contiguous list - // The list is then stably-sorted (to preserve command order), and applied to the drawable sequentially. - - List> generated = new List>(); - - generateCommands(generated, GetCommands(g => g.X, triggeredGroups), (d, value) => d.X = value); - generateCommands(generated, GetCommands(g => g.Y, triggeredGroups), (d, value) => d.Y = value); - generateCommands(generated, GetCommands(g => g.Scale, triggeredGroups), (d, value) => d.Scale = value); - generateCommands(generated, GetCommands(g => g.Rotation, triggeredGroups), (d, value) => d.Rotation = value); - generateCommands(generated, GetCommands(g => g.Colour, triggeredGroups), (d, value) => d.Colour = value); - generateCommands(generated, GetCommands(g => g.Alpha, triggeredGroups), (d, value) => d.Alpha = value); - generateCommands(generated, GetCommands(g => g.BlendingParameters, triggeredGroups), (d, value) => d.Blending = value, false); - generateCommands(generated, GetCommands(g => g.VectorScale, triggeredGroups), (d, value) => d.VectorScale = value); - generateCommands(generated, GetCommands(g => g.FlipH, triggeredGroups), (d, value) => d.FlipH = value, false); - generateCommands(generated, GetCommands(g => g.FlipV, triggeredGroups), (d, value) => d.FlipV = value, false); - - foreach (var command in generated.OrderBy(g => g.StartTime)) - command.ApplyTo(drawable); - } - - private void generateCommands(List> resultList, IEnumerable.TypedCommand> commands, - DrawablePropertyInitializer initializeProperty, bool alwaysInitialize = true) - { - bool initialized = false; - - foreach (var command in commands) - { - DrawablePropertyInitializer? initFunc = null; - - if (!initialized) - { - if (alwaysInitialize || command.StartTime == command.EndTime) - initFunc = initializeProperty; - initialized = true; - } - - resultList.Add(new GeneratedCommand(command, initFunc)); - } - } - - private interface IGeneratedCommand - where TDrawable : U - { - double StartTime { get; } - - void ApplyTo(TDrawable drawable); - } - - private readonly struct GeneratedCommand : IGeneratedCommand - where TDrawable : U - { - public double StartTime => command.StartTime; - - private readonly DrawablePropertyInitializer? initializeProperty; - private readonly CommandTimeline.TypedCommand command; - - public GeneratedCommand(CommandTimeline.TypedCommand command, DrawablePropertyInitializer? initializeProperty) - { - this.command = command; - this.initializeProperty = initializeProperty; - } - - public void ApplyTo(TDrawable drawable) - { - initializeProperty?.Invoke(drawable, command.StartValue); - - using (drawable.BeginAbsoluteSequence(command.StartTime)) - { - var sequence = command.IsParameterCommand - ? drawable.TransformTo(command.PropertyName, command.StartValue).Delay(command.Duration).TransformTo(command.PropertyName, command.EndValue) - : drawable.TransformTo(command.PropertyName, command.StartValue).Then().TransformTo(command.PropertyName, command.EndValue, command.Duration, command.Easing); - - if (command.LoopCount > 0) - sequence.Loop(command.Delay, command.LoopCount); - } - } - } - } -} diff --git a/osu.Game/Storyboards/StoryboardSprite.cs b/osu.Game/Storyboards/StoryboardSprite.cs index 8eaab9428d..dfd184a909 100644 --- a/osu.Game/Storyboards/StoryboardSprite.cs +++ b/osu.Game/Storyboards/StoryboardSprite.cs @@ -1,19 +1,164 @@ // 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.Graphics; +using osu.Game.Storyboards.Commands; using osu.Game.Storyboards.Drawables; using osuTK; namespace osu.Game.Storyboards { - public class StoryboardSprite : StoryboardElementWithDuration + public class StoryboardSprite : IStoryboardElementWithDuration { - public StoryboardSprite(string path, Anchor origin, Vector2 initialPosition) - : base(path, origin, initialPosition) + private readonly List loopGroups = new List(); + private readonly List triggerGroups = new List(); + + public string Path { get; } + public bool IsDrawable => HasCommands; + + public Anchor Origin; + public Vector2 InitialPosition; + + public readonly StoryboardCommandGroup Group = new StoryboardCommandGroup(); + + public double StartTime { + get + { + // To get the initial start time, we need to check whether the first alpha command to exist (across all loops) has a StartValue of zero. + // A StartValue of zero governs, above all else, the first valid display time of a sprite. + // + // You can imagine that the first command of each type decides that type's start value, so if the initial alpha is zero, + // anything before that point can be ignored (the sprite is not visible after all). + var alphaCommands = new List<(double startTime, bool isZeroStartValue)>(); + + var command = Group.Alpha.FirstOrDefault(); + if (command != null) alphaCommands.Add((command.StartTime, command.StartValue == 0)); + + foreach (var loop in loopGroups) + { + command = loop.Alpha.FirstOrDefault(); + if (command != null) alphaCommands.Add((command.StartTime + loop.LoopStartTime, command.StartValue == 0)); + } + + if (alphaCommands.Count > 0) + { + var firstAlpha = alphaCommands.MinBy(t => t.startTime); + + if (firstAlpha.isZeroStartValue) + return firstAlpha.startTime; + } + + return EarliestTransformTime; + } } - public override DrawableStoryboardSprite CreateStoryboardDrawable() => new DrawableStoryboardSprite(this); + public double EarliestTransformTime + { + get + { + // If we got to this point, either no alpha commands were present, or the earliest had a non-zero start value. + // The sprite's StartTime will be determined by the earliest command, regardless of type. + double earliestStartTime = Group.StartTime; + foreach (var l in loopGroups) + earliestStartTime = Math.Min(earliestStartTime, l.StartTime); + return earliestStartTime; + } + } + + public double EndTime + { + get + { + double latestEndTime = Group.EndTime; + + foreach (var l in loopGroups) + latestEndTime = Math.Max(latestEndTime, l.EndTime); + + return latestEndTime; + } + } + + public double EndTimeForDisplay + { + get + { + double latestEndTime = Group.StartTime; + + foreach (var l in loopGroups) + latestEndTime = Math.Max(latestEndTime, l.StartTime + l.Duration * l.TotalIterations); + + return latestEndTime; + } + } + + public bool HasCommands => Group.HasCommands || loopGroups.Any(l => l.HasCommands); + + public StoryboardSprite(string path, Anchor origin, Vector2 initialPosition) + { + Path = path; + Origin = origin; + InitialPosition = initialPosition; + } + + public virtual Drawable CreateDrawable() => new DrawableStoryboardSprite(this); + + public StoryboardLoopingGroup AddLoopingGroup(double loopStartTime, int repeatCount) + { + var loop = new StoryboardLoopingGroup(loopStartTime, repeatCount); + loopGroups.Add(loop); + return loop; + } + + public StoryboardTriggerGroup AddTriggerGroup(string triggerName, double startTime, double endTime, int groupNumber) + { + var trigger = new StoryboardTriggerGroup(triggerName, startTime, endTime, groupNumber); + triggerGroups.Add(trigger); + return trigger; + } + + public override string ToString() => $"{Path}, {Origin}, {InitialPosition}"; + + public void ApplyTransforms(Drawable drawable, IEnumerable>? triggeredGroups = null) + { + // For performance reasons, we need to apply the commands in order by start time. Not doing so will cause many functions to be interleaved, resulting in O(n^2) complexity. + + var commands = Group.GetAllCommands(); + commands = commands.Concat(loopGroups.SelectMany(l => l.GetAllCommands())); + + // todo: triggers are not implemented yet. + // if (triggeredGroups != null) + // commands = commands.Concat(triggeredGroups.SelectMany(tuple => tuple.Item1.GetAllCommands(tuple.Item2))); + + foreach (var command in commands.OrderBy(c => c.StartTime)) + { + using (drawable.BeginAbsoluteSequence(command.StartTime)) + command.ApplyTransform(drawable); + } + } + + // todo: need to revisit property initialisation. apparently it has to be done per first command of every affected property (transforms are supposed to do that already?). + // private void generateCommands(List resultList, IEnumerable.TypedCommand> commands, + // DrawablePropertyInitializer initializeProperty, DrawableTransform transform, bool alwaysInitialize = true) + // { + // bool initialized = false; + // + // foreach (var command in commands) + // { + // DrawablePropertyInitializer? initFunc = null; + // + // if (!initialized) + // { + // if (alwaysInitialize || command.StartTime == command.EndTime) + // initFunc = initializeProperty; + // initialized = true; + // } + // + // resultList.Add(new GeneratedCommand(command, initFunc, transform)); + // } + // } } } From 9b77d8c972abefe0e7e0d524e26a90b457f41abf Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 8 Mar 2024 01:59:26 +0300 Subject: [PATCH 0277/2493] Fix group start/end time not calculating correctly --- osu.Game/Storyboards/Commands/StoryboardCommandGroup.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Storyboards/Commands/StoryboardCommandGroup.cs b/osu.Game/Storyboards/Commands/StoryboardCommandGroup.cs index 02c43c9f60..5bb7ee6acf 100644 --- a/osu.Game/Storyboards/Commands/StoryboardCommandGroup.cs +++ b/osu.Game/Storyboards/Commands/StoryboardCommandGroup.cs @@ -30,13 +30,13 @@ namespace osu.Game.Storyboards.Commands /// Returns the earliest start time of the commands added to this group. /// [JsonIgnore] - public double StartTime { get; private set; } + public double StartTime { get; private set; } = double.MaxValue; /// /// Returns the latest end time of the commands added to this group. /// [JsonIgnore] - public double EndTime { get; private set; } + public double EndTime { get; private set; } = double.MinValue; [JsonIgnore] public double Duration => EndTime - StartTime; From 6c257e515996d04a7e31c35ac9bf7219442e2f2e Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 8 Mar 2024 01:59:42 +0300 Subject: [PATCH 0278/2493] Fix `HasCommands` property not set at all --- osu.Game/Storyboards/Commands/StoryboardCommandGroup.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Storyboards/Commands/StoryboardCommandGroup.cs b/osu.Game/Storyboards/Commands/StoryboardCommandGroup.cs index 5bb7ee6acf..0d4a79079b 100644 --- a/osu.Game/Storyboards/Commands/StoryboardCommandGroup.cs +++ b/osu.Game/Storyboards/Commands/StoryboardCommandGroup.cs @@ -104,6 +104,7 @@ namespace osu.Game.Storyboards.Commands protected virtual void AddCommand(ICollection> list, StoryboardCommand command) { list.Add(command); + HasCommands = true; if (command.StartTime < StartTime) StartTime = command.StartTime; From 87b065b8c3c82a365be4a2b9274042480e417a36 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 8 Mar 2024 02:00:23 +0300 Subject: [PATCH 0279/2493] Fix incorrect start time calculations `LoopStartTime` is now baked into each `IStoryboardCommand`. --- osu.Game/Storyboards/StoryboardSprite.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Storyboards/StoryboardSprite.cs b/osu.Game/Storyboards/StoryboardSprite.cs index dfd184a909..fc3c5d343c 100644 --- a/osu.Game/Storyboards/StoryboardSprite.cs +++ b/osu.Game/Storyboards/StoryboardSprite.cs @@ -41,7 +41,7 @@ namespace osu.Game.Storyboards foreach (var loop in loopGroups) { command = loop.Alpha.FirstOrDefault(); - if (command != null) alphaCommands.Add((command.StartTime + loop.LoopStartTime, command.StartValue == 0)); + if (command != null) alphaCommands.Add((command.StartTime, command.StartValue == 0)); } if (alphaCommands.Count > 0) @@ -120,8 +120,6 @@ namespace osu.Game.Storyboards return trigger; } - public override string ToString() => $"{Path}, {Origin}, {InitialPosition}"; - public void ApplyTransforms(Drawable drawable, IEnumerable>? triggeredGroups = null) { // For performance reasons, we need to apply the commands in order by start time. Not doing so will cause many functions to be interleaved, resulting in O(n^2) complexity. @@ -140,6 +138,8 @@ namespace osu.Game.Storyboards } } + public override string ToString() => $"{Path}, {Origin}, {InitialPosition}"; + // todo: need to revisit property initialisation. apparently it has to be done per first command of every affected property (transforms are supposed to do that already?). // private void generateCommands(List resultList, IEnumerable.TypedCommand> commands, // DrawablePropertyInitializer initializeProperty, DrawableTransform transform, bool alwaysInitialize = true) From 3755dd059af7855219a1867d894aee803e722fa2 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 8 Mar 2024 01:58:58 +0300 Subject: [PATCH 0280/2493] Calculate loop delays at point of transform application --- .../Commands/StoryboardLoopingGroup.cs | 36 +++++++++---------- 1 file changed, 16 insertions(+), 20 deletions(-) diff --git a/osu.Game/Storyboards/Commands/StoryboardLoopingGroup.cs b/osu.Game/Storyboards/Commands/StoryboardLoopingGroup.cs index e520353bd6..39b81ead28 100644 --- a/osu.Game/Storyboards/Commands/StoryboardLoopingGroup.cs +++ b/osu.Game/Storyboards/Commands/StoryboardLoopingGroup.cs @@ -10,7 +10,7 @@ namespace osu.Game.Storyboards.Commands { public class StoryboardLoopingGroup : StoryboardCommandGroup { - public double LoopStartTime; + private readonly double loopStartTime; /// /// The total number of times this loop is played back. Always greater than zero. @@ -26,45 +26,41 @@ namespace osu.Game.Storyboards.Commands { if (repeatCount < 0) throw new ArgumentException("Repeat count must be zero or above.", nameof(repeatCount)); - LoopStartTime = startTime; + loopStartTime = startTime; TotalIterations = repeatCount + 1; } protected override void AddCommand(ICollection> list, StoryboardCommand command) - { - // todo: this is broke! - double fullLoopDuration = EndTime - StartTime; - double loopDelay = fullLoopDuration - command.EndTime + command.StartTime; - base.AddCommand(list, new StoryboardLoopingCommand(command, LoopStartTime, TotalIterations, loopDelay)); - } + => base.AddCommand(list, new StoryboardLoopingCommand(command, this)); - public override string ToString() => $"{LoopStartTime} x{TotalIterations}"; + public override string ToString() => $"{loopStartTime} x{TotalIterations}"; private class StoryboardLoopingCommand : StoryboardCommand { private readonly StoryboardCommand command; - private readonly int loopCount; - private readonly double loopDelay; + private readonly StoryboardLoopingGroup loopingGroup; - public StoryboardLoopingCommand(StoryboardCommand command, double loopStartTime, int loopCount, double loopDelay) + public StoryboardLoopingCommand(StoryboardCommand command, StoryboardLoopingGroup loopingGroup) // In an ideal world, we would multiply the command duration by TotalIterations in command end time. // Unfortunately this would clash with how stable handled end times, and results in some storyboards playing outro // sequences for minutes or hours. - : base(loopStartTime + command.StartTime, loopStartTime + command.EndTime, command.StartValue, command.EndValue, command.Easing) + : base(loopingGroup.loopStartTime + command.StartTime, loopingGroup.loopStartTime + command.EndTime, command.StartValue, command.EndValue, command.Easing) { this.command = command; - this.loopCount = loopCount; - this.loopDelay = loopDelay; + this.loopingGroup = loopingGroup; } - public override void SetInitialValue(Drawable d) => command.SetInitialValue(d); + public override string PropertyName => command.PropertyName; - public override TransformSequence ApplyTransform(Drawable d) + public override void ApplyInitialValue(Drawable d) => command.ApplyInitialValue(d); + + public override TransformSequence ApplyTransforms(Drawable d) { - if (loopCount == 0) - return command.ApplyTransform(d); + if (loopingGroup.TotalIterations == 0) + return command.ApplyTransforms(d); - return command.ApplyTransform(d).Loop(loopDelay, loopCount); + double loopingGroupDuration = loopingGroup.Duration; + return command.ApplyTransforms(d).Loop(loopingGroupDuration - Duration, loopingGroup.TotalIterations); } } } From 2ca36254f46c3501d97a8782bcc7a699104557d1 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 8 Mar 2024 02:02:22 +0300 Subject: [PATCH 0281/2493] Fix comparison interface not implemented on storyboard command classes --- osu.Game/Storyboards/Commands/StoryboardCommand.cs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/osu.Game/Storyboards/Commands/StoryboardCommand.cs b/osu.Game/Storyboards/Commands/StoryboardCommand.cs index 883b9f57c1..1647faf243 100644 --- a/osu.Game/Storyboards/Commands/StoryboardCommand.cs +++ b/osu.Game/Storyboards/Commands/StoryboardCommand.cs @@ -1,12 +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; using osu.Framework.Graphics; using osu.Framework.Graphics.Transforms; namespace osu.Game.Storyboards.Commands { - public abstract class StoryboardCommand : IStoryboardCommand + public abstract class StoryboardCommand : IStoryboardCommand, IComparable> { public double StartTime { get; } public double EndTime { get; } @@ -41,10 +42,14 @@ namespace osu.Game.Storyboards.Commands /// public abstract TransformSequence ApplyTransform(Drawable d); - public int CompareTo(IStoryboardCommand other) + public int CompareTo(StoryboardCommand? other) { + if (other == null) + return 1; + int result = StartTime.CompareTo(other.StartTime); - if (result != 0) return result; + if (result != 0) + return result; return EndTime.CompareTo(other.EndTime); } From b450abb687823132c9bb3c6b1041bb70bb05d8ea Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 8 Mar 2024 02:02:45 +0300 Subject: [PATCH 0282/2493] Support applying initial values of storyboard commands --- .../Storyboards/Commands/IStoryboardCommand.cs | 14 +++++++++++++- .../Storyboards/Commands/StoryboardAlphaCommand.cs | 8 ++++++-- .../StoryboardBlendingParametersCommand.cs | 9 ++++++--- .../Commands/StoryboardColourCommand.cs | 8 ++++++-- osu.Game/Storyboards/Commands/StoryboardCommand.cs | 12 ++++-------- .../Storyboards/Commands/StoryboardFlipHCommand.cs | 6 ++++-- .../Storyboards/Commands/StoryboardFlipVCommand.cs | 6 ++++-- .../Commands/StoryboardRotationCommand.cs | 6 ++++-- .../Storyboards/Commands/StoryboardScaleCommand.cs | 6 ++++-- .../Commands/StoryboardVectorScaleCommand.cs | 6 ++++-- .../Storyboards/Commands/StoryboardXCommand.cs | 6 ++++-- .../Storyboards/Commands/StoryboardYCommand.cs | 6 ++++-- osu.Game/Storyboards/StoryboardSprite.cs | 10 +++++++++- 13 files changed, 72 insertions(+), 31 deletions(-) diff --git a/osu.Game/Storyboards/Commands/IStoryboardCommand.cs b/osu.Game/Storyboards/Commands/IStoryboardCommand.cs index 848dcab575..05613a987d 100644 --- a/osu.Game/Storyboards/Commands/IStoryboardCommand.cs +++ b/osu.Game/Storyboards/Commands/IStoryboardCommand.cs @@ -18,11 +18,23 @@ namespace osu.Game.Storyboards.Commands /// double EndTime { get; } + /// + /// The name of the property affected by this storyboard command. + /// Used to apply initial property values based on the list of commands given in . + /// + string PropertyName { get; } + + /// + /// Sets the value of the corresponding property in to the start value of this command. + /// + /// The target drawable. + void ApplyInitialValue(Drawable d); + /// /// Applies the transforms described by this storyboard command to the target drawable. /// /// The target drawable. /// The sequence of transforms applied to the target drawable. - TransformSequence ApplyTransform(Drawable d); + TransformSequence ApplyTransforms(Drawable d); } } diff --git a/osu.Game/Storyboards/Commands/StoryboardAlphaCommand.cs b/osu.Game/Storyboards/Commands/StoryboardAlphaCommand.cs index 729ecd72a7..1d91d6ccc1 100644 --- a/osu.Game/Storyboards/Commands/StoryboardAlphaCommand.cs +++ b/osu.Game/Storyboards/Commands/StoryboardAlphaCommand.cs @@ -13,7 +13,11 @@ namespace osu.Game.Storyboards.Commands { } - public override void SetInitialValue(Drawable d) => d.Alpha = StartValue; - public override TransformSequence ApplyTransform(Drawable d) => d.FadeTo(StartValue).Then().FadeTo(EndValue, Duration, Easing); + public override string PropertyName => nameof(Drawable.Alpha); + + public override void ApplyInitialValue(Drawable d) => d.Alpha = StartValue; + + public override TransformSequence ApplyTransforms(Drawable d) + => d.FadeTo(StartValue).Then().FadeTo(EndValue, Duration, Easing); } } diff --git a/osu.Game/Storyboards/Commands/StoryboardBlendingParametersCommand.cs b/osu.Game/Storyboards/Commands/StoryboardBlendingParametersCommand.cs index cc54909837..3a2d372a66 100644 --- a/osu.Game/Storyboards/Commands/StoryboardBlendingParametersCommand.cs +++ b/osu.Game/Storyboards/Commands/StoryboardBlendingParametersCommand.cs @@ -13,9 +13,12 @@ namespace osu.Game.Storyboards.Commands { } - public override void SetInitialValue(Drawable d) => d.Blending = StartValue; + public override string PropertyName => nameof(Drawable.Blending); - public override TransformSequence ApplyTransform(Drawable d) - => d.TransformTo(nameof(d.Blending), StartValue).Delay(Duration).TransformTo(nameof(d.Blending), EndValue); + public override void ApplyInitialValue(Drawable d) => d.Blending = StartValue; + + public override TransformSequence ApplyTransforms(Drawable d) + => d.TransformTo(nameof(d.Blending), StartValue).Delay(Duration) + .TransformTo(nameof(d.Blending), EndValue); } } diff --git a/osu.Game/Storyboards/Commands/StoryboardColourCommand.cs b/osu.Game/Storyboards/Commands/StoryboardColourCommand.cs index be56a1d71b..fbde7e1af7 100644 --- a/osu.Game/Storyboards/Commands/StoryboardColourCommand.cs +++ b/osu.Game/Storyboards/Commands/StoryboardColourCommand.cs @@ -14,7 +14,11 @@ namespace osu.Game.Storyboards.Commands { } - public override void SetInitialValue(Drawable d) => d.Colour = StartValue; - public override TransformSequence ApplyTransform(Drawable d) => d.FadeColour(StartValue).Then().FadeColour(EndValue, Duration, Easing); + public override string PropertyName => nameof(Drawable.Colour); + + public override void ApplyInitialValue(Drawable d) => d.Colour = StartValue; + + public override TransformSequence ApplyTransforms(Drawable d) + => d.FadeColour(StartValue).Then().FadeColour(EndValue, Duration, Easing); } } diff --git a/osu.Game/Storyboards/Commands/StoryboardCommand.cs b/osu.Game/Storyboards/Commands/StoryboardCommand.cs index 1647faf243..58fcb148ff 100644 --- a/osu.Game/Storyboards/Commands/StoryboardCommand.cs +++ b/osu.Game/Storyboards/Commands/StoryboardCommand.cs @@ -32,15 +32,11 @@ namespace osu.Game.Storyboards.Commands public T StartValue; public T EndValue; - /// - /// Sets the value of the corresponding property in to the start value of this command. - /// - public abstract void SetInitialValue(Drawable d); + public abstract string PropertyName { get; } - /// - /// Transforms a corresponding property in that corresponds to this command group with the specified parameters. - /// - public abstract TransformSequence ApplyTransform(Drawable d); + public abstract void ApplyInitialValue(Drawable d); + + public abstract TransformSequence ApplyTransforms(Drawable d); public int CompareTo(StoryboardCommand? other) { diff --git a/osu.Game/Storyboards/Commands/StoryboardFlipHCommand.cs b/osu.Game/Storyboards/Commands/StoryboardFlipHCommand.cs index 9bcb687d3c..c381b0bb64 100644 --- a/osu.Game/Storyboards/Commands/StoryboardFlipHCommand.cs +++ b/osu.Game/Storyboards/Commands/StoryboardFlipHCommand.cs @@ -14,9 +14,11 @@ namespace osu.Game.Storyboards.Commands { } - public override void SetInitialValue(Drawable d) => ((IDrawableStoryboardElement)d).FlipH = StartValue; + public override string PropertyName => nameof(IDrawableStoryboardElement.FlipH); - public override TransformSequence ApplyTransform(Drawable d) + public override void ApplyInitialValue(Drawable d) => ((IDrawableStoryboardElement)d).FlipH = StartValue; + + public override TransformSequence ApplyTransforms(Drawable d) => d.TransformTo(nameof(IDrawableStoryboardElement.FlipH), StartValue).Delay(Duration) .TransformTo(nameof(IDrawableStoryboardElement.FlipH), EndValue); } diff --git a/osu.Game/Storyboards/Commands/StoryboardFlipVCommand.cs b/osu.Game/Storyboards/Commands/StoryboardFlipVCommand.cs index 9f1f5faa33..e43e5e9205 100644 --- a/osu.Game/Storyboards/Commands/StoryboardFlipVCommand.cs +++ b/osu.Game/Storyboards/Commands/StoryboardFlipVCommand.cs @@ -14,9 +14,11 @@ namespace osu.Game.Storyboards.Commands { } - public override void SetInitialValue(Drawable d) => ((IDrawableStoryboardElement)d).FlipV = StartValue; + public override string PropertyName => nameof(IDrawableStoryboardElement.FlipV); - public override TransformSequence ApplyTransform(Drawable d) + public override void ApplyInitialValue(Drawable d) => ((IDrawableStoryboardElement)d).FlipV = StartValue; + + public override TransformSequence ApplyTransforms(Drawable d) => d.TransformTo(nameof(IDrawableStoryboardElement.FlipV), StartValue).Delay(Duration) .TransformTo(nameof(IDrawableStoryboardElement.FlipV), EndValue); } diff --git a/osu.Game/Storyboards/Commands/StoryboardRotationCommand.cs b/osu.Game/Storyboards/Commands/StoryboardRotationCommand.cs index c56dcd130f..2a449af843 100644 --- a/osu.Game/Storyboards/Commands/StoryboardRotationCommand.cs +++ b/osu.Game/Storyboards/Commands/StoryboardRotationCommand.cs @@ -13,9 +13,11 @@ namespace osu.Game.Storyboards.Commands { } - public override void SetInitialValue(Drawable d) => d.Rotation = StartValue; + public override string PropertyName => nameof(Drawable.Rotation); - public override TransformSequence ApplyTransform(Drawable d) + public override void ApplyInitialValue(Drawable d) => d.Rotation = StartValue; + + public override TransformSequence ApplyTransforms(Drawable d) => d.RotateTo(StartValue).Then().RotateTo(EndValue, Duration, Easing); } } diff --git a/osu.Game/Storyboards/Commands/StoryboardScaleCommand.cs b/osu.Game/Storyboards/Commands/StoryboardScaleCommand.cs index 9dbdd6ebd8..bf9796148c 100644 --- a/osu.Game/Storyboards/Commands/StoryboardScaleCommand.cs +++ b/osu.Game/Storyboards/Commands/StoryboardScaleCommand.cs @@ -14,9 +14,11 @@ namespace osu.Game.Storyboards.Commands { } - public override void SetInitialValue(Drawable d) => d.Scale = new Vector2(StartValue); + public override string PropertyName => nameof(Drawable.Scale); - public override TransformSequence ApplyTransform(Drawable d) + public override void ApplyInitialValue(Drawable d) => d.Scale = new Vector2(StartValue); + + public override TransformSequence ApplyTransforms(Drawable d) => d.ScaleTo(StartValue).Then().ScaleTo(EndValue, Duration, Easing); } } diff --git a/osu.Game/Storyboards/Commands/StoryboardVectorScaleCommand.cs b/osu.Game/Storyboards/Commands/StoryboardVectorScaleCommand.cs index fefb21b257..638dc6a5ee 100644 --- a/osu.Game/Storyboards/Commands/StoryboardVectorScaleCommand.cs +++ b/osu.Game/Storyboards/Commands/StoryboardVectorScaleCommand.cs @@ -15,9 +15,11 @@ namespace osu.Game.Storyboards.Commands { } - public override void SetInitialValue(Drawable d) => ((IDrawableStoryboardElement)d).VectorScale = StartValue; + public override string PropertyName => nameof(IDrawableStoryboardElement.VectorScale); - public override TransformSequence ApplyTransform(Drawable d) + public override void ApplyInitialValue(Drawable d) => ((IDrawableStoryboardElement)d).VectorScale = StartValue; + + public override TransformSequence ApplyTransforms(Drawable d) => d.TransformTo(nameof(IDrawableStoryboardElement.VectorScale), StartValue).Then() .TransformTo(nameof(IDrawableStoryboardElement.VectorScale), EndValue, Duration, Easing); } diff --git a/osu.Game/Storyboards/Commands/StoryboardXCommand.cs b/osu.Game/Storyboards/Commands/StoryboardXCommand.cs index a9f9cd4f8f..809a77256c 100644 --- a/osu.Game/Storyboards/Commands/StoryboardXCommand.cs +++ b/osu.Game/Storyboards/Commands/StoryboardXCommand.cs @@ -13,9 +13,11 @@ namespace osu.Game.Storyboards.Commands { } - public override void SetInitialValue(Drawable d) => d.X = StartValue; + public override string PropertyName => nameof(Drawable.X); - public override TransformSequence ApplyTransform(Drawable d) + public override void ApplyInitialValue(Drawable d) => d.X = StartValue; + + public override TransformSequence ApplyTransforms(Drawable d) => d.MoveToX(StartValue).Then().MoveToX(EndValue, Duration, Easing); } } diff --git a/osu.Game/Storyboards/Commands/StoryboardYCommand.cs b/osu.Game/Storyboards/Commands/StoryboardYCommand.cs index eb30b36720..d054135878 100644 --- a/osu.Game/Storyboards/Commands/StoryboardYCommand.cs +++ b/osu.Game/Storyboards/Commands/StoryboardYCommand.cs @@ -13,9 +13,11 @@ namespace osu.Game.Storyboards.Commands { } - public override void SetInitialValue(Drawable d) => d.Y = StartValue; + public override string PropertyName => nameof(Drawable.Y); - public override TransformSequence ApplyTransform(Drawable d) + public override void ApplyInitialValue(Drawable d) => d.Y = StartValue; + + public override TransformSequence ApplyTransforms(Drawable d) => d.MoveToY(StartValue).Then().MoveToY(EndValue, Duration, Easing); } } diff --git a/osu.Game/Storyboards/StoryboardSprite.cs b/osu.Game/Storyboards/StoryboardSprite.cs index fc3c5d343c..9d7ab66692 100644 --- a/osu.Game/Storyboards/StoryboardSprite.cs +++ b/osu.Game/Storyboards/StoryboardSprite.cs @@ -131,10 +131,18 @@ namespace osu.Game.Storyboards // if (triggeredGroups != null) // commands = commands.Concat(triggeredGroups.SelectMany(tuple => tuple.Item1.GetAllCommands(tuple.Item2))); + HashSet appliedProperties = new HashSet(); + foreach (var command in commands.OrderBy(c => c.StartTime)) { + if (!appliedProperties.Contains(command.PropertyName)) + { + command.ApplyInitialValue(drawable); + appliedProperties.Add(command.PropertyName); + } + using (drawable.BeginAbsoluteSequence(command.StartTime)) - command.ApplyTransform(drawable); + command.ApplyTransforms(drawable); } } From fa9b2f0cd541bb4c80c895a0bbeced418ccd85e1 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 8 Mar 2024 03:06:49 +0300 Subject: [PATCH 0283/2493] Add generics to `ApplyInitialValue`/`ApplyTransforms` for ability to return custom transform sequences *sigh* --- .../Visual/Gameplay/TestSceneLeadIn.cs | 8 +-- .../Gameplay/TestSceneStoryboardWithOutro.cs | 2 +- .../TestSceneMultiSpectatorScreen.cs | 2 +- .../Formats/LegacyStoryboardDecoder.cs | 32 ++++----- .../Commands/IStoryboardCommand.cs | 7 +- .../Commands/StoryboardAlphaCommand.cs | 4 +- .../StoryboardBlendingParametersCommand.cs | 4 +- .../Commands/StoryboardColourCommand.cs | 4 +- .../Storyboards/Commands/StoryboardCommand.cs | 19 +++--- .../Commands/StoryboardCommandGroup.cs | 48 ++++--------- .../Commands/StoryboardCommandList.cs | 41 ------------ .../Commands/StoryboardFlipHCommand.cs | 10 +-- .../Commands/StoryboardFlipVCommand.cs | 10 +-- .../Commands/StoryboardLoopingGroup.cs | 5 +- .../Commands/StoryboardRotationCommand.cs | 4 +- .../Commands/StoryboardScaleCommand.cs | 4 +- .../Commands/StoryboardTriggerGroup.cs | 1 - .../Commands/StoryboardVectorScaleCommand.cs | 10 +-- .../Commands/StoryboardXCommand.cs | 4 +- .../Commands/StoryboardYCommand.cs | 4 +- .../Drawables/DrawableStoryboardAnimation.cs | 2 +- .../Drawables/DrawableStoryboardSprite.cs | 6 +- ...ableStoryboardElement.cs => IFlippable.cs} | 4 +- .../Storyboards/Drawables/IVectorScalable.cs | 13 ++++ osu.Game/Storyboards/StoryboardSprite.cs | 67 ++++++------------- 25 files changed, 122 insertions(+), 193 deletions(-) delete mode 100644 osu.Game/Storyboards/Commands/StoryboardCommandList.cs rename osu.Game/Storyboards/Drawables/{IDrawableStoryboardElement.cs => IFlippable.cs} (72%) create mode 100644 osu.Game/Storyboards/Drawables/IVectorScalable.cs diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneLeadIn.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneLeadIn.cs index c3eef4da9b..b2196e77b4 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneLeadIn.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneLeadIn.cs @@ -47,7 +47,7 @@ namespace osu.Game.Tests.Visual.Gameplay var sprite = new StoryboardSprite("unknown", Anchor.TopLeft, Vector2.Zero); - sprite.Group.AddAlpha(firstStoryboardEvent, firstStoryboardEvent + 500, 0, 1, Easing.None); + sprite.Commands.AddAlpha(firstStoryboardEvent, firstStoryboardEvent + 500, 0, 1, Easing.None); storyboard.GetLayer("Background").Add(sprite); @@ -73,16 +73,16 @@ namespace osu.Game.Tests.Visual.Gameplay var sprite = new StoryboardSprite("unknown", Anchor.TopLeft, Vector2.Zero); // these should be ignored as we have an alpha visibility blocker proceeding this command. - sprite.Group.AddScale(loop_start_time, -18000, 0, 1, Easing.None); + sprite.Commands.AddScale(loop_start_time, -18000, 0, 1, Easing.None); var loopGroup = sprite.AddLoopingGroup(loop_start_time, 50); loopGroup.AddScale(loop_start_time, -18000, 0, 1, Easing.None); - var target = addEventToLoop ? loopGroup : sprite.Group; + var target = addEventToLoop ? loopGroup : sprite.Commands; double loopRelativeOffset = addEventToLoop ? -loop_start_time : 0; target.AddAlpha(loopRelativeOffset + firstStoryboardEvent, loopRelativeOffset + firstStoryboardEvent + 500, 0, 1, Easing.None); // these should be ignored due to being in the future. - sprite.Group.AddAlpha(18000, 20000, 0, 1, Easing.None); + sprite.Commands.AddAlpha(18000, 20000, 0, 1, Easing.None); loopGroup.AddAlpha(38000, 40000, 0, 1, Easing.None); storyboard.GetLayer("Background").Add(sprite); diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardWithOutro.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardWithOutro.cs index 9269c3f4ae..8bdb7297fe 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardWithOutro.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardWithOutro.cs @@ -216,7 +216,7 @@ namespace osu.Game.Tests.Visual.Gameplay { var storyboard = new Storyboard(); var sprite = new StoryboardSprite("unknown", Anchor.TopLeft, Vector2.Zero); - sprite.Group.AddAlpha(0, duration, 1, 0, Easing.None); + sprite.Commands.AddAlpha(0, duration, 1, 0, Easing.None); storyboard.GetLayer("Background").Add(sprite); return storyboard; } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs index 62a2bfeaab..7cd2e2fdaa 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs @@ -424,7 +424,7 @@ namespace osu.Game.Tests.Visual.Multiplayer public void TestIntroStoryboardElement() => testLeadIn(b => { var sprite = new StoryboardSprite("unknown", Anchor.TopLeft, Vector2.Zero); - sprite.Group.AddAlpha(-2000, 0, 0, 1, Easing.None); + sprite.Commands.AddAlpha(-2000, 0, 0, 1, Easing.None); b.Storyboard.GetLayer("Background").Add(sprite); }); diff --git a/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs index 33cdaa085e..83277b71c8 100644 --- a/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs @@ -19,7 +19,7 @@ namespace osu.Game.Beatmaps.Formats public class LegacyStoryboardDecoder : LegacyDecoder { private StoryboardSprite? storyboardSprite; - private StoryboardCommandGroup? currentGroup; + private StoryboardCommandGroup? currentCommandsGroup; private Storyboard storyboard = null!; @@ -166,7 +166,7 @@ namespace osu.Game.Beatmaps.Formats else { if (depth < 2) - currentGroup = storyboardSprite?.Group; + currentCommandsGroup = storyboardSprite?.Commands; string commandType = split[0]; @@ -178,7 +178,7 @@ namespace osu.Game.Beatmaps.Formats double startTime = split.Length > 2 ? Parsing.ParseDouble(split[2]) : double.MinValue; double endTime = split.Length > 3 ? Parsing.ParseDouble(split[3]) : double.MaxValue; int groupNumber = split.Length > 4 ? Parsing.ParseInt(split[4]) : 0; - currentGroup = storyboardSprite?.AddTriggerGroup(triggerName, startTime, endTime, groupNumber); + currentCommandsGroup = storyboardSprite?.AddTriggerGroup(triggerName, startTime, endTime, groupNumber); break; } @@ -186,7 +186,7 @@ namespace osu.Game.Beatmaps.Formats { double startTime = Parsing.ParseDouble(split[1]); int repeatCount = Parsing.ParseInt(split[2]); - currentGroup = storyboardSprite?.AddLoopingGroup(startTime, Math.Max(0, repeatCount - 1)); + currentCommandsGroup = storyboardSprite?.AddLoopingGroup(startTime, Math.Max(0, repeatCount - 1)); break; } @@ -205,7 +205,7 @@ namespace osu.Game.Beatmaps.Formats { float startValue = Parsing.ParseFloat(split[4]); float endValue = split.Length > 5 ? Parsing.ParseFloat(split[5]) : startValue; - currentGroup?.AddAlpha(startTime, endTime, startValue, endValue, easing); + currentCommandsGroup?.AddAlpha(startTime, endTime, startValue, endValue, easing); break; } @@ -213,7 +213,7 @@ namespace osu.Game.Beatmaps.Formats { float startValue = Parsing.ParseFloat(split[4]); float endValue = split.Length > 5 ? Parsing.ParseFloat(split[5]) : startValue; - currentGroup?.AddScale(startTime, endTime, startValue, endValue, easing); + currentCommandsGroup?.AddScale(startTime, endTime, startValue, endValue, easing); break; } @@ -223,7 +223,7 @@ namespace osu.Game.Beatmaps.Formats float startY = Parsing.ParseFloat(split[5]); float endX = split.Length > 6 ? Parsing.ParseFloat(split[6]) : startX; float endY = split.Length > 7 ? Parsing.ParseFloat(split[7]) : startY; - currentGroup?.AddVectorScale(startTime, endTime, new Vector2(startX, startY), new Vector2(endX, endY), easing); + currentCommandsGroup?.AddVectorScale(startTime, endTime, new Vector2(startX, startY), new Vector2(endX, endY), easing); break; } @@ -231,7 +231,7 @@ namespace osu.Game.Beatmaps.Formats { float startValue = Parsing.ParseFloat(split[4]); float endValue = split.Length > 5 ? Parsing.ParseFloat(split[5]) : startValue; - currentGroup?.AddRotation(startTime, endTime, MathUtils.RadiansToDegrees(startValue), MathUtils.RadiansToDegrees(endValue), easing); + currentCommandsGroup?.AddRotation(startTime, endTime, MathUtils.RadiansToDegrees(startValue), MathUtils.RadiansToDegrees(endValue), easing); break; } @@ -241,8 +241,8 @@ namespace osu.Game.Beatmaps.Formats float startY = Parsing.ParseFloat(split[5]); float endX = split.Length > 6 ? Parsing.ParseFloat(split[6]) : startX; float endY = split.Length > 7 ? Parsing.ParseFloat(split[7]) : startY; - currentGroup?.AddX(startTime, endTime, startX, endX, easing); - currentGroup?.AddY(startTime, endTime, startY, endY, easing); + currentCommandsGroup?.AddX(startTime, endTime, startX, endX, easing); + currentCommandsGroup?.AddY(startTime, endTime, startY, endY, easing); break; } @@ -250,7 +250,7 @@ namespace osu.Game.Beatmaps.Formats { float startValue = Parsing.ParseFloat(split[4]); float endValue = split.Length > 5 ? Parsing.ParseFloat(split[5]) : startValue; - currentGroup?.AddX(startTime, endTime, startValue, endValue, easing); + currentCommandsGroup?.AddX(startTime, endTime, startValue, endValue, easing); break; } @@ -258,7 +258,7 @@ namespace osu.Game.Beatmaps.Formats { float startValue = Parsing.ParseFloat(split[4]); float endValue = split.Length > 5 ? Parsing.ParseFloat(split[5]) : startValue; - currentGroup?.AddY(startTime, endTime, startValue, endValue, easing); + currentCommandsGroup?.AddY(startTime, endTime, startValue, endValue, easing); break; } @@ -270,7 +270,7 @@ namespace osu.Game.Beatmaps.Formats float endRed = split.Length > 7 ? Parsing.ParseFloat(split[7]) : startRed; float endGreen = split.Length > 8 ? Parsing.ParseFloat(split[8]) : startGreen; float endBlue = split.Length > 9 ? Parsing.ParseFloat(split[9]) : startBlue; - currentGroup?.AddColour(startTime, endTime, + currentCommandsGroup?.AddColour(startTime, endTime, new Color4(startRed / 255f, startGreen / 255f, startBlue / 255f, 1), new Color4(endRed / 255f, endGreen / 255f, endBlue / 255f, 1), easing); break; @@ -283,16 +283,16 @@ namespace osu.Game.Beatmaps.Formats switch (type) { case "A": - currentGroup?.AddBlendingParameters(startTime, endTime, BlendingParameters.Additive, + currentCommandsGroup?.AddBlendingParameters(startTime, endTime, BlendingParameters.Additive, startTime == endTime ? BlendingParameters.Additive : BlendingParameters.Inherit, easing); break; case "H": - currentGroup?.AddFlipH(startTime, endTime, true, startTime == endTime, easing); + currentCommandsGroup?.AddFlipH(startTime, endTime, true, startTime == endTime, easing); break; case "V": - currentGroup?.AddFlipV(startTime, endTime, true, startTime == endTime, easing); + currentCommandsGroup?.AddFlipV(startTime, endTime, true, startTime == endTime, easing); break; } diff --git a/osu.Game/Storyboards/Commands/IStoryboardCommand.cs b/osu.Game/Storyboards/Commands/IStoryboardCommand.cs index 05613a987d..6efb19afe4 100644 --- a/osu.Game/Storyboards/Commands/IStoryboardCommand.cs +++ b/osu.Game/Storyboards/Commands/IStoryboardCommand.cs @@ -3,6 +3,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Transforms; +using osu.Game.Storyboards.Drawables; namespace osu.Game.Storyboards.Commands { @@ -28,13 +29,15 @@ namespace osu.Game.Storyboards.Commands /// Sets the value of the corresponding property in to the start value of this command. /// /// The target drawable. - void ApplyInitialValue(Drawable d); + void ApplyInitialValue(TDrawable d) + where TDrawable : Drawable, IFlippable, IVectorScalable; /// /// Applies the transforms described by this storyboard command to the target drawable. /// /// The target drawable. /// The sequence of transforms applied to the target drawable. - TransformSequence ApplyTransforms(Drawable d); + TransformSequence ApplyTransforms(TDrawable d) + where TDrawable : Drawable, IFlippable, IVectorScalable; } } diff --git a/osu.Game/Storyboards/Commands/StoryboardAlphaCommand.cs b/osu.Game/Storyboards/Commands/StoryboardAlphaCommand.cs index 1d91d6ccc1..f4a90b372a 100644 --- a/osu.Game/Storyboards/Commands/StoryboardAlphaCommand.cs +++ b/osu.Game/Storyboards/Commands/StoryboardAlphaCommand.cs @@ -15,9 +15,9 @@ namespace osu.Game.Storyboards.Commands public override string PropertyName => nameof(Drawable.Alpha); - public override void ApplyInitialValue(Drawable d) => d.Alpha = StartValue; + public override void ApplyInitialValue(TDrawable d) => d.Alpha = StartValue; - public override TransformSequence ApplyTransforms(Drawable d) + public override TransformSequence ApplyTransforms(TDrawable d) => d.FadeTo(StartValue).Then().FadeTo(EndValue, Duration, Easing); } } diff --git a/osu.Game/Storyboards/Commands/StoryboardBlendingParametersCommand.cs b/osu.Game/Storyboards/Commands/StoryboardBlendingParametersCommand.cs index 3a2d372a66..3e2e8bb0e8 100644 --- a/osu.Game/Storyboards/Commands/StoryboardBlendingParametersCommand.cs +++ b/osu.Game/Storyboards/Commands/StoryboardBlendingParametersCommand.cs @@ -15,9 +15,9 @@ namespace osu.Game.Storyboards.Commands public override string PropertyName => nameof(Drawable.Blending); - public override void ApplyInitialValue(Drawable d) => d.Blending = StartValue; + public override void ApplyInitialValue(TDrawable d) => d.Blending = StartValue; - public override TransformSequence ApplyTransforms(Drawable d) + public override TransformSequence ApplyTransforms(TDrawable d) => d.TransformTo(nameof(d.Blending), StartValue).Delay(Duration) .TransformTo(nameof(d.Blending), EndValue); } diff --git a/osu.Game/Storyboards/Commands/StoryboardColourCommand.cs b/osu.Game/Storyboards/Commands/StoryboardColourCommand.cs index fbde7e1af7..66390eb305 100644 --- a/osu.Game/Storyboards/Commands/StoryboardColourCommand.cs +++ b/osu.Game/Storyboards/Commands/StoryboardColourCommand.cs @@ -16,9 +16,9 @@ namespace osu.Game.Storyboards.Commands public override string PropertyName => nameof(Drawable.Colour); - public override void ApplyInitialValue(Drawable d) => d.Colour = StartValue; + public override void ApplyInitialValue(TDrawable d) => d.Colour = StartValue; - public override TransformSequence ApplyTransforms(Drawable d) + public override TransformSequence ApplyTransforms(TDrawable d) => d.FadeColour(StartValue).Then().FadeColour(EndValue, Duration, Easing); } } diff --git a/osu.Game/Storyboards/Commands/StoryboardCommand.cs b/osu.Game/Storyboards/Commands/StoryboardCommand.cs index 58fcb148ff..4f2f0f04a2 100644 --- a/osu.Game/Storyboards/Commands/StoryboardCommand.cs +++ b/osu.Game/Storyboards/Commands/StoryboardCommand.cs @@ -4,6 +4,7 @@ using System; using osu.Framework.Graphics; using osu.Framework.Graphics.Transforms; +using osu.Game.Storyboards.Drawables; namespace osu.Game.Storyboards.Commands { @@ -11,6 +12,11 @@ namespace osu.Game.Storyboards.Commands { public double StartTime { get; } public double EndTime { get; } + + public T StartValue { get; } + public T EndValue { get; } + public Easing Easing { get; } + public double Duration => EndTime - StartTime; protected StoryboardCommand(double startTime, double endTime, T startValue, T endValue, Easing easing) @@ -25,18 +31,13 @@ namespace osu.Game.Storyboards.Commands Easing = easing; } - public Easing Easing { get; set; } - public int LoopCount { get; set; } - public double Delay { get; set; } - - public T StartValue; - public T EndValue; - public abstract string PropertyName { get; } - public abstract void ApplyInitialValue(Drawable d); + public abstract void ApplyInitialValue(TDrawable d) + where TDrawable : Drawable, IFlippable, IVectorScalable; - public abstract TransformSequence ApplyTransforms(Drawable d); + public abstract TransformSequence ApplyTransforms(TDrawable d) + where TDrawable : Drawable, IFlippable, IVectorScalable; public int CompareTo(StoryboardCommand? other) { diff --git a/osu.Game/Storyboards/Commands/StoryboardCommandGroup.cs b/osu.Game/Storyboards/Commands/StoryboardCommandGroup.cs index 0d4a79079b..fb847d2e44 100644 --- a/osu.Game/Storyboards/Commands/StoryboardCommandGroup.cs +++ b/osu.Game/Storyboards/Commands/StoryboardCommandGroup.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 Newtonsoft.Json; using osu.Framework.Graphics; using osu.Framework.Lists; @@ -13,18 +12,20 @@ namespace osu.Game.Storyboards.Commands { public class StoryboardCommandGroup { - public SortedList> X; - public SortedList> Y; - public SortedList> Scale; - public SortedList> VectorScale; - public SortedList> Rotation; - public SortedList> Colour; - public SortedList> Alpha; - public SortedList> BlendingParameters; - public SortedList> FlipH; - public SortedList> FlipV; + public SortedList> X = new SortedList>(); + public SortedList> Y = new SortedList>(); + public SortedList> Scale = new SortedList>(); + public SortedList> VectorScale = new SortedList>(); + public SortedList> Rotation = new SortedList>(); + public SortedList> Colour = new SortedList>(); + public SortedList> Alpha = new SortedList>(); + public SortedList> BlendingParameters = new SortedList>(); + public SortedList> FlipH = new SortedList>(); + public SortedList> FlipV = new SortedList>(); - private readonly IReadOnlyList[] lists; + public IReadOnlyList AllCommands => allCommands; + + private readonly List allCommands = new List(); /// /// Returns the earliest start time of the commands added to this group. @@ -44,28 +45,6 @@ namespace osu.Game.Storyboards.Commands [JsonIgnore] public bool HasCommands { get; private set; } - public StoryboardCommandGroup() - { - lists = new IReadOnlyList[] - { - X = new SortedList>(), - Y = new SortedList>(), - Scale = new SortedList>(), - VectorScale = new SortedList>(), - Rotation = new SortedList>(), - Colour = new SortedList>(), - Alpha = new SortedList>(), - BlendingParameters = new SortedList>(), - FlipH = new SortedList>(), - FlipV = new SortedList>() - }; - } - - /// - /// Returns all commands contained by this group unsorted. - /// - public virtual IEnumerable GetAllCommands() => lists.SelectMany(l => l); - public void AddX(double startTime, double endTime, float startValue, float endValue, Easing easing) => AddCommand(X, new StoryboardXCommand(startTime, endTime, startValue, endValue, easing)); @@ -104,6 +83,7 @@ namespace osu.Game.Storyboards.Commands protected virtual void AddCommand(ICollection> list, StoryboardCommand command) { list.Add(command); + allCommands.Add(command); HasCommands = true; if (command.StartTime < StartTime) diff --git a/osu.Game/Storyboards/Commands/StoryboardCommandList.cs b/osu.Game/Storyboards/Commands/StoryboardCommandList.cs deleted file mode 100644 index 67012e9d49..0000000000 --- a/osu.Game/Storyboards/Commands/StoryboardCommandList.cs +++ /dev/null @@ -1,41 +0,0 @@ -// 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.Storyboards.Commands -{ - // public class StoryboardCommandList : IStoryboardCommandList - // { - // // todo: change to sorted list and avoid enumerable type on exposed properties? - // private readonly List> commands = new List>(); - // - // public IEnumerable> Commands => commands.OrderBy(c => c.StartTime); - // - // IEnumerable IStoryboardCommandList.Commands => Commands; - // public bool HasCommands => commands.Count > 0; - // - // public double StartTime { get; private set; } = double.MaxValue; - // public double EndTime { get; private set; } = double.MinValue; - // - // public T? StartValue { get; private set; } - // public T? EndValue { get; private set; } - // - // public void Add(StoryboardCommand command) - // { - // commands.Add(command); - // - // if (command.StartTime < StartTime) - // { - // StartValue = command.StartValue; - // StartTime = command.StartTime; - // } - // - // if (command.EndTime > EndTime) - // { - // EndValue = command.EndValue; - // EndTime = command.EndTime; - // } - // } - // - // public override string ToString() => $"{commands.Count} command(s)"; - // } -} diff --git a/osu.Game/Storyboards/Commands/StoryboardFlipHCommand.cs b/osu.Game/Storyboards/Commands/StoryboardFlipHCommand.cs index c381b0bb64..26aef23226 100644 --- a/osu.Game/Storyboards/Commands/StoryboardFlipHCommand.cs +++ b/osu.Game/Storyboards/Commands/StoryboardFlipHCommand.cs @@ -14,12 +14,12 @@ namespace osu.Game.Storyboards.Commands { } - public override string PropertyName => nameof(IDrawableStoryboardElement.FlipH); + public override string PropertyName => nameof(IFlippable.FlipH); - public override void ApplyInitialValue(Drawable d) => ((IDrawableStoryboardElement)d).FlipH = StartValue; + public override void ApplyInitialValue(TDrawable d) => d.FlipH = StartValue; - public override TransformSequence ApplyTransforms(Drawable d) - => d.TransformTo(nameof(IDrawableStoryboardElement.FlipH), StartValue).Delay(Duration) - .TransformTo(nameof(IDrawableStoryboardElement.FlipH), EndValue); + public override TransformSequence ApplyTransforms(TDrawable d) + => d.TransformTo(nameof(IFlippable.FlipH), StartValue).Delay(Duration) + .TransformTo(nameof(IFlippable.FlipH), EndValue); } } diff --git a/osu.Game/Storyboards/Commands/StoryboardFlipVCommand.cs b/osu.Game/Storyboards/Commands/StoryboardFlipVCommand.cs index e43e5e9205..88423da2af 100644 --- a/osu.Game/Storyboards/Commands/StoryboardFlipVCommand.cs +++ b/osu.Game/Storyboards/Commands/StoryboardFlipVCommand.cs @@ -14,12 +14,12 @@ namespace osu.Game.Storyboards.Commands { } - public override string PropertyName => nameof(IDrawableStoryboardElement.FlipV); + public override string PropertyName => nameof(IFlippable.FlipV); - public override void ApplyInitialValue(Drawable d) => ((IDrawableStoryboardElement)d).FlipV = StartValue; + public override void ApplyInitialValue(TDrawable d) => d.FlipV = StartValue; - public override TransformSequence ApplyTransforms(Drawable d) - => d.TransformTo(nameof(IDrawableStoryboardElement.FlipV), StartValue).Delay(Duration) - .TransformTo(nameof(IDrawableStoryboardElement.FlipV), EndValue); + public override TransformSequence ApplyTransforms(TDrawable d) + => d.TransformTo(nameof(IFlippable.FlipV), StartValue).Delay(Duration) + .TransformTo(nameof(IFlippable.FlipV), EndValue); } } diff --git a/osu.Game/Storyboards/Commands/StoryboardLoopingGroup.cs b/osu.Game/Storyboards/Commands/StoryboardLoopingGroup.cs index 39b81ead28..e97de84ab7 100644 --- a/osu.Game/Storyboards/Commands/StoryboardLoopingGroup.cs +++ b/osu.Game/Storyboards/Commands/StoryboardLoopingGroup.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using osu.Framework.Graphics; using osu.Framework.Graphics.Transforms; namespace osu.Game.Storyboards.Commands @@ -52,9 +51,9 @@ namespace osu.Game.Storyboards.Commands public override string PropertyName => command.PropertyName; - public override void ApplyInitialValue(Drawable d) => command.ApplyInitialValue(d); + public override void ApplyInitialValue(TDrawable d) => command.ApplyInitialValue(d); - public override TransformSequence ApplyTransforms(Drawable d) + public override TransformSequence ApplyTransforms(TDrawable d) { if (loopingGroup.TotalIterations == 0) return command.ApplyTransforms(d); diff --git a/osu.Game/Storyboards/Commands/StoryboardRotationCommand.cs b/osu.Game/Storyboards/Commands/StoryboardRotationCommand.cs index 2a449af843..4347dc9d77 100644 --- a/osu.Game/Storyboards/Commands/StoryboardRotationCommand.cs +++ b/osu.Game/Storyboards/Commands/StoryboardRotationCommand.cs @@ -15,9 +15,9 @@ namespace osu.Game.Storyboards.Commands public override string PropertyName => nameof(Drawable.Rotation); - public override void ApplyInitialValue(Drawable d) => d.Rotation = StartValue; + public override void ApplyInitialValue(TDrawable d) => d.Rotation = StartValue; - public override TransformSequence ApplyTransforms(Drawable d) + public override TransformSequence ApplyTransforms(TDrawable d) => d.RotateTo(StartValue).Then().RotateTo(EndValue, Duration, Easing); } } diff --git a/osu.Game/Storyboards/Commands/StoryboardScaleCommand.cs b/osu.Game/Storyboards/Commands/StoryboardScaleCommand.cs index bf9796148c..b0f33fd6b8 100644 --- a/osu.Game/Storyboards/Commands/StoryboardScaleCommand.cs +++ b/osu.Game/Storyboards/Commands/StoryboardScaleCommand.cs @@ -16,9 +16,9 @@ namespace osu.Game.Storyboards.Commands public override string PropertyName => nameof(Drawable.Scale); - public override void ApplyInitialValue(Drawable d) => d.Scale = new Vector2(StartValue); + public override void ApplyInitialValue(TDrawable d) => d.Scale = new Vector2(StartValue); - public override TransformSequence ApplyTransforms(Drawable d) + public override TransformSequence ApplyTransforms(TDrawable d) => d.ScaleTo(StartValue).Then().ScaleTo(EndValue, Duration, Easing); } } diff --git a/osu.Game/Storyboards/Commands/StoryboardTriggerGroup.cs b/osu.Game/Storyboards/Commands/StoryboardTriggerGroup.cs index dfb6f8cb1b..89a68e9ec0 100644 --- a/osu.Game/Storyboards/Commands/StoryboardTriggerGroup.cs +++ b/osu.Game/Storyboards/Commands/StoryboardTriggerGroup.cs @@ -3,7 +3,6 @@ namespace osu.Game.Storyboards.Commands { - // todo: this is not implemented and has never been, keep that in mind. public class StoryboardTriggerGroup : StoryboardCommandGroup { public string TriggerName; diff --git a/osu.Game/Storyboards/Commands/StoryboardVectorScaleCommand.cs b/osu.Game/Storyboards/Commands/StoryboardVectorScaleCommand.cs index 638dc6a5ee..5d3fef5948 100644 --- a/osu.Game/Storyboards/Commands/StoryboardVectorScaleCommand.cs +++ b/osu.Game/Storyboards/Commands/StoryboardVectorScaleCommand.cs @@ -15,12 +15,12 @@ namespace osu.Game.Storyboards.Commands { } - public override string PropertyName => nameof(IDrawableStoryboardElement.VectorScale); + public override string PropertyName => nameof(IVectorScalable.VectorScale); - public override void ApplyInitialValue(Drawable d) => ((IDrawableStoryboardElement)d).VectorScale = StartValue; + public override void ApplyInitialValue(TDrawable d) => d.VectorScale = StartValue; - public override TransformSequence ApplyTransforms(Drawable d) - => d.TransformTo(nameof(IDrawableStoryboardElement.VectorScale), StartValue).Then() - .TransformTo(nameof(IDrawableStoryboardElement.VectorScale), EndValue, Duration, Easing); + public override TransformSequence ApplyTransforms(TDrawable d) + => d.TransformTo(nameof(d.VectorScale), StartValue).Then() + .TransformTo(nameof(d.VectorScale), EndValue, Duration, Easing); } } diff --git a/osu.Game/Storyboards/Commands/StoryboardXCommand.cs b/osu.Game/Storyboards/Commands/StoryboardXCommand.cs index 809a77256c..7df9a75768 100644 --- a/osu.Game/Storyboards/Commands/StoryboardXCommand.cs +++ b/osu.Game/Storyboards/Commands/StoryboardXCommand.cs @@ -15,9 +15,9 @@ namespace osu.Game.Storyboards.Commands public override string PropertyName => nameof(Drawable.X); - public override void ApplyInitialValue(Drawable d) => d.X = StartValue; + public override void ApplyInitialValue(TDrawable d) => d.X = StartValue; - public override TransformSequence ApplyTransforms(Drawable d) + public override TransformSequence ApplyTransforms(TDrawable d) => d.MoveToX(StartValue).Then().MoveToX(EndValue, Duration, Easing); } } diff --git a/osu.Game/Storyboards/Commands/StoryboardYCommand.cs b/osu.Game/Storyboards/Commands/StoryboardYCommand.cs index d054135878..d7dc32a0f3 100644 --- a/osu.Game/Storyboards/Commands/StoryboardYCommand.cs +++ b/osu.Game/Storyboards/Commands/StoryboardYCommand.cs @@ -15,9 +15,9 @@ namespace osu.Game.Storyboards.Commands public override string PropertyName => nameof(Drawable.Y); - public override void ApplyInitialValue(Drawable d) => d.Y = StartValue; + public override void ApplyInitialValue(TDrawable d) => d.Y = StartValue; - public override TransformSequence ApplyTransforms(Drawable d) + public override TransformSequence ApplyTransforms(TDrawable d) => d.MoveToY(StartValue).Then().MoveToY(EndValue, Duration, Easing); } } diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs index 8e1a8ce949..fae9ec7f2e 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs @@ -16,7 +16,7 @@ using osuTK; namespace osu.Game.Storyboards.Drawables { - public partial class DrawableStoryboardAnimation : TextureAnimation, IDrawableStoryboardElement + public partial class DrawableStoryboardAnimation : TextureAnimation, IFlippable, IVectorScalable { public StoryboardAnimation Animation { get; } diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs index 6772178e85..507a51aca4 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Diagnostics; using osu.Framework.Allocation; using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; @@ -13,7 +14,7 @@ using osuTK; namespace osu.Game.Storyboards.Drawables { - public partial class DrawableStoryboardSprite : Sprite, IDrawableStoryboardElement + public partial class DrawableStoryboardSprite : Sprite, IFlippable, IVectorScalable { public StoryboardSprite Sprite { get; } @@ -101,6 +102,9 @@ namespace osu.Game.Storyboards.Drawables else Texture = textureStore.Get(Sprite.Path); + if (Sprite.Path == "SB/textbox.png") + Debugger.Break(); + Sprite.ApplyTransforms(this); } diff --git a/osu.Game/Storyboards/Drawables/IDrawableStoryboardElement.cs b/osu.Game/Storyboards/Drawables/IFlippable.cs similarity index 72% rename from osu.Game/Storyboards/Drawables/IDrawableStoryboardElement.cs rename to osu.Game/Storyboards/Drawables/IFlippable.cs index 04bae88c76..79f98ea6ef 100644 --- a/osu.Game/Storyboards/Drawables/IDrawableStoryboardElement.cs +++ b/osu.Game/Storyboards/Drawables/IFlippable.cs @@ -2,14 +2,12 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Graphics; -using osuTK; namespace osu.Game.Storyboards.Drawables { - public interface IDrawableStoryboardElement : IDrawable + public interface IFlippable : IDrawable { bool FlipH { get; set; } bool FlipV { get; set; } - Vector2 VectorScale { get; set; } } } diff --git a/osu.Game/Storyboards/Drawables/IVectorScalable.cs b/osu.Game/Storyboards/Drawables/IVectorScalable.cs new file mode 100644 index 0000000000..ce6047c8f6 --- /dev/null +++ b/osu.Game/Storyboards/Drawables/IVectorScalable.cs @@ -0,0 +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 osu.Framework.Graphics; +using osuTK; + +namespace osu.Game.Storyboards.Drawables +{ + public interface IVectorScalable : IDrawable + { + Vector2 VectorScale { get; set; } + } +} diff --git a/osu.Game/Storyboards/StoryboardSprite.cs b/osu.Game/Storyboards/StoryboardSprite.cs index 9d7ab66692..f2c011bfca 100644 --- a/osu.Game/Storyboards/StoryboardSprite.cs +++ b/osu.Game/Storyboards/StoryboardSprite.cs @@ -13,7 +13,7 @@ namespace osu.Game.Storyboards { public class StoryboardSprite : IStoryboardElementWithDuration { - private readonly List loopGroups = new List(); + private readonly List loopingGroups = new List(); private readonly List triggerGroups = new List(); public string Path { get; } @@ -22,7 +22,7 @@ namespace osu.Game.Storyboards public Anchor Origin; public Vector2 InitialPosition; - public readonly StoryboardCommandGroup Group = new StoryboardCommandGroup(); + public readonly StoryboardCommandGroup Commands = new StoryboardCommandGroup(); public double StartTime { @@ -35,10 +35,10 @@ namespace osu.Game.Storyboards // anything before that point can be ignored (the sprite is not visible after all). var alphaCommands = new List<(double startTime, bool isZeroStartValue)>(); - var command = Group.Alpha.FirstOrDefault(); + var command = Commands.Alpha.FirstOrDefault(); if (command != null) alphaCommands.Add((command.StartTime, command.StartValue == 0)); - foreach (var loop in loopGroups) + foreach (var loop in loopingGroups) { command = loop.Alpha.FirstOrDefault(); if (command != null) alphaCommands.Add((command.StartTime, command.StartValue == 0)); @@ -62,8 +62,8 @@ namespace osu.Game.Storyboards { // If we got to this point, either no alpha commands were present, or the earliest had a non-zero start value. // The sprite's StartTime will be determined by the earliest command, regardless of type. - double earliestStartTime = Group.StartTime; - foreach (var l in loopGroups) + double earliestStartTime = Commands.StartTime; + foreach (var l in loopingGroups) earliestStartTime = Math.Min(earliestStartTime, l.StartTime); return earliestStartTime; } @@ -73,9 +73,9 @@ namespace osu.Game.Storyboards { get { - double latestEndTime = Group.EndTime; + double latestEndTime = Commands.EndTime; - foreach (var l in loopGroups) + foreach (var l in loopingGroups) latestEndTime = Math.Max(latestEndTime, l.EndTime); return latestEndTime; @@ -86,16 +86,16 @@ namespace osu.Game.Storyboards { get { - double latestEndTime = Group.StartTime; + double latestEndTime = Commands.StartTime; - foreach (var l in loopGroups) + foreach (var l in loopingGroups) latestEndTime = Math.Max(latestEndTime, l.StartTime + l.Duration * l.TotalIterations); return latestEndTime; } } - public bool HasCommands => Group.HasCommands || loopGroups.Any(l => l.HasCommands); + public bool HasCommands => Commands.HasCommands || loopingGroups.Any(l => l.HasCommands); public StoryboardSprite(string path, Anchor origin, Vector2 initialPosition) { @@ -109,7 +109,7 @@ namespace osu.Game.Storyboards public StoryboardLoopingGroup AddLoopingGroup(double loopStartTime, int repeatCount) { var loop = new StoryboardLoopingGroup(loopStartTime, repeatCount); - loopGroups.Add(loop); + loopingGroups.Add(loop); return loop; } @@ -120,26 +120,20 @@ namespace osu.Game.Storyboards return trigger; } - public void ApplyTransforms(Drawable drawable, IEnumerable>? triggeredGroups = null) + public void ApplyTransforms(TDrawable drawable) + where TDrawable : Drawable, IFlippable, IVectorScalable { - // For performance reasons, we need to apply the commands in order by start time. Not doing so will cause many functions to be interleaved, resulting in O(n^2) complexity. - - var commands = Group.GetAllCommands(); - commands = commands.Concat(loopGroups.SelectMany(l => l.GetAllCommands())); - - // todo: triggers are not implemented yet. - // if (triggeredGroups != null) - // commands = commands.Concat(triggeredGroups.SelectMany(tuple => tuple.Item1.GetAllCommands(tuple.Item2))); - HashSet appliedProperties = new HashSet(); + // For performance reasons, we need to apply the commands in chronological order. + // Not doing so will cause many functions to be interleaved, resulting in O(n^2) complexity. + IEnumerable commands = Commands.AllCommands; + commands = commands.Concat(loopingGroups.SelectMany(l => l.AllCommands)); + foreach (var command in commands.OrderBy(c => c.StartTime)) { - if (!appliedProperties.Contains(command.PropertyName)) - { + if (appliedProperties.Add(command.PropertyName)) command.ApplyInitialValue(drawable); - appliedProperties.Add(command.PropertyName); - } using (drawable.BeginAbsoluteSequence(command.StartTime)) command.ApplyTransforms(drawable); @@ -147,26 +141,5 @@ namespace osu.Game.Storyboards } public override string ToString() => $"{Path}, {Origin}, {InitialPosition}"; - - // todo: need to revisit property initialisation. apparently it has to be done per first command of every affected property (transforms are supposed to do that already?). - // private void generateCommands(List resultList, IEnumerable.TypedCommand> commands, - // DrawablePropertyInitializer initializeProperty, DrawableTransform transform, bool alwaysInitialize = true) - // { - // bool initialized = false; - // - // foreach (var command in commands) - // { - // DrawablePropertyInitializer? initFunc = null; - // - // if (!initialized) - // { - // if (alwaysInitialize || command.StartTime == command.EndTime) - // initFunc = initializeProperty; - // initialized = true; - // } - // - // resultList.Add(new GeneratedCommand(command, initFunc, transform)); - // } - // } } } From 79da6d861326ca9d6d897d7a0059471a4548d8bf Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 8 Mar 2024 03:07:59 +0300 Subject: [PATCH 0284/2493] Fix refactor error on `EndTimeForDisplay` Not sure when this happened >.> --- osu.Game/Storyboards/StoryboardSprite.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Storyboards/StoryboardSprite.cs b/osu.Game/Storyboards/StoryboardSprite.cs index f2c011bfca..f961019883 100644 --- a/osu.Game/Storyboards/StoryboardSprite.cs +++ b/osu.Game/Storyboards/StoryboardSprite.cs @@ -86,7 +86,7 @@ namespace osu.Game.Storyboards { get { - double latestEndTime = Commands.StartTime; + double latestEndTime = Commands.EndTime; foreach (var l in loopingGroups) latestEndTime = Math.Max(latestEndTime, l.StartTime + l.Duration * l.TotalIterations); From 48c83195677736a41d1bb3b8e8d563247481de72 Mon Sep 17 00:00:00 2001 From: Hivie Date: Fri, 8 Mar 2024 16:01:57 +0100 Subject: [PATCH 0285/2493] change multiplier to 0.9x --- osu.Game.Rulesets.Taiko/Mods/TaikoModConstantSpeed.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModConstantSpeed.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModConstantSpeed.cs index 4ecb94467e..81973e65cc 100644 --- a/osu.Game.Rulesets.Taiko/Mods/TaikoModConstantSpeed.cs +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModConstantSpeed.cs @@ -15,7 +15,7 @@ namespace osu.Game.Rulesets.Taiko.Mods { public override string Name => "Constant Speed"; public override string Acronym => "CS"; - public override double ScoreMultiplier => 0.8; + public override double ScoreMultiplier => 0.9; public override LocalisableString Description => "No more tricky speed changes!"; public override IconUsage? Icon => FontAwesome.Solid.Equals; public override ModType Type => ModType.Conversion; From a8792b35850c53a5946e6e20176282dc33292075 Mon Sep 17 00:00:00 2001 From: Hivie Date: Fri, 8 Mar 2024 16:02:17 +0100 Subject: [PATCH 0286/2493] better assertion --- osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs index c88bbec9bc..ee7acec65c 100644 --- a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs @@ -72,7 +72,7 @@ namespace osu.Game.Rulesets.Taiko.UI protected virtual double ComputeTimeRange() { // Adjust when we're using constant algorithm to not be sluggish. - double multiplier = VisualisationMethod == ScrollVisualisationMethod.Overlapping ? 1 : 4 * Beatmap.Difficulty.SliderMultiplier; + double multiplier = VisualisationMethod == ScrollVisualisationMethod.Constant ? 4 * Beatmap.Difficulty.SliderMultiplier : 1; return PlayfieldAdjustmentContainer.ComputeTimeRange() / multiplier; } From a85be2a46d574fe62416e7313afa3bfdd3873dd0 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 8 Mar 2024 20:22:26 +0300 Subject: [PATCH 0287/2493] Fix merge conflicts --- osu.Game/Storyboards/StoryboardSprite.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Storyboards/StoryboardSprite.cs b/osu.Game/Storyboards/StoryboardSprite.cs index a6312ccb79..944d77e745 100644 --- a/osu.Game/Storyboards/StoryboardSprite.cs +++ b/osu.Game/Storyboards/StoryboardSprite.cs @@ -97,10 +97,10 @@ namespace osu.Game.Storyboards // If the logic above fails to find anything or discarded by the fact that there are loops present, latestEndTime will be double.MaxValue // and thus conservativeEndTime will be used. - double conservativeEndTime = TimelineGroup.EndTime; + double conservativeEndTime = Commands.EndTime; - foreach (var l in loops) - conservativeEndTime = Math.Max(conservativeEndTime, l.StartTime + l.CommandsDuration * l.TotalIterations); + foreach (var l in loopingGroups) + conservativeEndTime = Math.Max(conservativeEndTime, l.StartTime + l.Duration * l.TotalIterations); return Math.Min(latestEndTime, conservativeEndTime); } From c1649b76d638dbe0f32217be7027e7a98b5b6026 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 8 Mar 2024 21:33:46 +0300 Subject: [PATCH 0288/2493] Reorder command properties to match general format I had them shuffled around in the middle of the refactor. --- .../TestSceneDrawableStoryboardSprite.cs | 2 +- .../Visual/Gameplay/TestSceneLeadIn.cs | 12 +++--- .../Gameplay/TestSceneStoryboardWithOutro.cs | 2 +- .../TestSceneMultiSpectatorScreen.cs | 2 +- .../Formats/LegacyStoryboardDecoder.cs | 28 ++++++------- .../Commands/StoryboardAlphaCommand.cs | 4 +- .../StoryboardBlendingParametersCommand.cs | 4 +- .../Commands/StoryboardColourCommand.cs | 4 +- .../Storyboards/Commands/StoryboardCommand.cs | 2 +- .../Commands/StoryboardCommandGroup.cs | 40 +++++++++---------- .../Commands/StoryboardFlipHCommand.cs | 4 +- .../Commands/StoryboardFlipVCommand.cs | 4 +- .../Commands/StoryboardLoopingGroup.cs | 2 +- .../Commands/StoryboardRotationCommand.cs | 4 +- .../Commands/StoryboardScaleCommand.cs | 4 +- .../Commands/StoryboardVectorScaleCommand.cs | 4 +- .../Commands/StoryboardXCommand.cs | 4 +- .../Commands/StoryboardYCommand.cs | 4 +- 18 files changed, 65 insertions(+), 65 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableStoryboardSprite.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableStoryboardSprite.cs index 6209b42cbb..6bfa141d85 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableStoryboardSprite.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableStoryboardSprite.cs @@ -176,7 +176,7 @@ namespace osu.Game.Tests.Visual.Gameplay var sprite = new StoryboardSprite(lookupName, origin, initialPosition); var loop = sprite.AddLoopingGroup(Time.Current, 100); - loop.AddAlpha(0, 10000, 1, 1, Easing.None); + loop.AddAlpha(Easing.None, 0, 10000, 1, 1); layer.Elements.Clear(); layer.Add(sprite); diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneLeadIn.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneLeadIn.cs index b2196e77b4..5a71369976 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneLeadIn.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneLeadIn.cs @@ -47,7 +47,7 @@ namespace osu.Game.Tests.Visual.Gameplay var sprite = new StoryboardSprite("unknown", Anchor.TopLeft, Vector2.Zero); - sprite.Commands.AddAlpha(firstStoryboardEvent, firstStoryboardEvent + 500, 0, 1, Easing.None); + sprite.Commands.AddAlpha(Easing.None, firstStoryboardEvent, firstStoryboardEvent + 500, 0, 1); storyboard.GetLayer("Background").Add(sprite); @@ -73,17 +73,17 @@ namespace osu.Game.Tests.Visual.Gameplay var sprite = new StoryboardSprite("unknown", Anchor.TopLeft, Vector2.Zero); // these should be ignored as we have an alpha visibility blocker proceeding this command. - sprite.Commands.AddScale(loop_start_time, -18000, 0, 1, Easing.None); + sprite.Commands.AddScale(Easing.None, loop_start_time, -18000, 0, 1); var loopGroup = sprite.AddLoopingGroup(loop_start_time, 50); - loopGroup.AddScale(loop_start_time, -18000, 0, 1, Easing.None); + loopGroup.AddScale(Easing.None, loop_start_time, -18000, 0, 1); var target = addEventToLoop ? loopGroup : sprite.Commands; double loopRelativeOffset = addEventToLoop ? -loop_start_time : 0; - target.AddAlpha(loopRelativeOffset + firstStoryboardEvent, loopRelativeOffset + firstStoryboardEvent + 500, 0, 1, Easing.None); + target.AddAlpha(Easing.None, loopRelativeOffset + firstStoryboardEvent, loopRelativeOffset + firstStoryboardEvent + 500, 0, 1); // these should be ignored due to being in the future. - sprite.Commands.AddAlpha(18000, 20000, 0, 1, Easing.None); - loopGroup.AddAlpha(38000, 40000, 0, 1, Easing.None); + sprite.Commands.AddAlpha(Easing.None, 18000, 20000, 0, 1); + loopGroup.AddAlpha(Easing.None, 38000, 40000, 0, 1); storyboard.GetLayer("Background").Add(sprite); diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardWithOutro.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardWithOutro.cs index 8bdb7297fe..aff6139c08 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardWithOutro.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardWithOutro.cs @@ -216,7 +216,7 @@ namespace osu.Game.Tests.Visual.Gameplay { var storyboard = new Storyboard(); var sprite = new StoryboardSprite("unknown", Anchor.TopLeft, Vector2.Zero); - sprite.Commands.AddAlpha(0, duration, 1, 0, Easing.None); + sprite.Commands.AddAlpha(Easing.None, 0, duration, 1, 0); storyboard.GetLayer("Background").Add(sprite); return storyboard; } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs index 7cd2e2fdaa..2b17f91e68 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs @@ -424,7 +424,7 @@ namespace osu.Game.Tests.Visual.Multiplayer public void TestIntroStoryboardElement() => testLeadIn(b => { var sprite = new StoryboardSprite("unknown", Anchor.TopLeft, Vector2.Zero); - sprite.Commands.AddAlpha(-2000, 0, 0, 1, Easing.None); + sprite.Commands.AddAlpha(Easing.None, -2000, 0, 0, 1); b.Storyboard.GetLayer("Background").Add(sprite); }); diff --git a/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs index 739e81a2fe..195d59d0eb 100644 --- a/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs @@ -204,7 +204,7 @@ namespace osu.Game.Beatmaps.Formats { float startValue = Parsing.ParseFloat(split[4]); float endValue = split.Length > 5 ? Parsing.ParseFloat(split[5]) : startValue; - currentCommandsGroup?.AddAlpha(startTime, endTime, startValue, endValue, easing); + currentCommandsGroup?.AddAlpha(easing, startTime, endTime, startValue, endValue); break; } @@ -212,7 +212,7 @@ namespace osu.Game.Beatmaps.Formats { float startValue = Parsing.ParseFloat(split[4]); float endValue = split.Length > 5 ? Parsing.ParseFloat(split[5]) : startValue; - currentCommandsGroup?.AddScale(startTime, endTime, startValue, endValue, easing); + currentCommandsGroup?.AddScale(easing, startTime, endTime, startValue, endValue); break; } @@ -222,7 +222,7 @@ namespace osu.Game.Beatmaps.Formats float startY = Parsing.ParseFloat(split[5]); float endX = split.Length > 6 ? Parsing.ParseFloat(split[6]) : startX; float endY = split.Length > 7 ? Parsing.ParseFloat(split[7]) : startY; - currentCommandsGroup?.AddVectorScale(startTime, endTime, new Vector2(startX, startY), new Vector2(endX, endY), easing); + currentCommandsGroup?.AddVectorScale(easing, startTime, endTime, new Vector2(startX, startY), new Vector2(endX, endY)); break; } @@ -230,7 +230,7 @@ namespace osu.Game.Beatmaps.Formats { float startValue = Parsing.ParseFloat(split[4]); float endValue = split.Length > 5 ? Parsing.ParseFloat(split[5]) : startValue; - currentCommandsGroup?.AddRotation(startTime, endTime, float.RadiansToDegrees(startValue), float.RadiansToDegrees(endValue), easing); + currentCommandsGroup?.AddRotation(easing, startTime, endTime, float.RadiansToDegrees(startValue), float.RadiansToDegrees(endValue)); break; } @@ -240,8 +240,8 @@ namespace osu.Game.Beatmaps.Formats float startY = Parsing.ParseFloat(split[5]); float endX = split.Length > 6 ? Parsing.ParseFloat(split[6]) : startX; float endY = split.Length > 7 ? Parsing.ParseFloat(split[7]) : startY; - currentCommandsGroup?.AddX(startTime, endTime, startX, endX, easing); - currentCommandsGroup?.AddY(startTime, endTime, startY, endY, easing); + currentCommandsGroup?.AddX(easing, startTime, endTime, startX, endX); + currentCommandsGroup?.AddY(easing, startTime, endTime, startY, endY); break; } @@ -249,7 +249,7 @@ namespace osu.Game.Beatmaps.Formats { float startValue = Parsing.ParseFloat(split[4]); float endValue = split.Length > 5 ? Parsing.ParseFloat(split[5]) : startValue; - currentCommandsGroup?.AddX(startTime, endTime, startValue, endValue, easing); + currentCommandsGroup?.AddX(easing, startTime, endTime, startValue, endValue); break; } @@ -257,7 +257,7 @@ namespace osu.Game.Beatmaps.Formats { float startValue = Parsing.ParseFloat(split[4]); float endValue = split.Length > 5 ? Parsing.ParseFloat(split[5]) : startValue; - currentCommandsGroup?.AddY(startTime, endTime, startValue, endValue, easing); + currentCommandsGroup?.AddY(easing, startTime, endTime, startValue, endValue); break; } @@ -269,9 +269,9 @@ namespace osu.Game.Beatmaps.Formats float endRed = split.Length > 7 ? Parsing.ParseFloat(split[7]) : startRed; float endGreen = split.Length > 8 ? Parsing.ParseFloat(split[8]) : startGreen; float endBlue = split.Length > 9 ? Parsing.ParseFloat(split[9]) : startBlue; - currentCommandsGroup?.AddColour(startTime, endTime, + currentCommandsGroup?.AddColour(easing, startTime, endTime, new Color4(startRed / 255f, startGreen / 255f, startBlue / 255f, 1), - new Color4(endRed / 255f, endGreen / 255f, endBlue / 255f, 1), easing); + new Color4(endRed / 255f, endGreen / 255f, endBlue / 255f, 1)); break; } @@ -282,16 +282,16 @@ namespace osu.Game.Beatmaps.Formats switch (type) { case "A": - currentCommandsGroup?.AddBlendingParameters(startTime, endTime, BlendingParameters.Additive, - startTime == endTime ? BlendingParameters.Additive : BlendingParameters.Inherit, easing); + currentCommandsGroup?.AddBlendingParameters(easing, startTime, endTime, BlendingParameters.Additive, + startTime == endTime ? BlendingParameters.Additive : BlendingParameters.Inherit); break; case "H": - currentCommandsGroup?.AddFlipH(startTime, endTime, true, startTime == endTime, easing); + currentCommandsGroup?.AddFlipH(easing, startTime, endTime, true, startTime == endTime); break; case "V": - currentCommandsGroup?.AddFlipV(startTime, endTime, true, startTime == endTime, easing); + currentCommandsGroup?.AddFlipV(easing, startTime, endTime, true, startTime == endTime); break; } diff --git a/osu.Game/Storyboards/Commands/StoryboardAlphaCommand.cs b/osu.Game/Storyboards/Commands/StoryboardAlphaCommand.cs index f4a90b372a..1c17da7592 100644 --- a/osu.Game/Storyboards/Commands/StoryboardAlphaCommand.cs +++ b/osu.Game/Storyboards/Commands/StoryboardAlphaCommand.cs @@ -8,8 +8,8 @@ namespace osu.Game.Storyboards.Commands { public class StoryboardAlphaCommand : StoryboardCommand { - public StoryboardAlphaCommand(double startTime, double endTime, float startValue, float endValue, Easing easing) - : base(startTime, endTime, startValue, endValue, easing) + public StoryboardAlphaCommand(Easing easing, double startTime, double endTime, float startValue, float endValue) + : base(easing, startTime, endTime, startValue, endValue) { } diff --git a/osu.Game/Storyboards/Commands/StoryboardBlendingParametersCommand.cs b/osu.Game/Storyboards/Commands/StoryboardBlendingParametersCommand.cs index 3e2e8bb0e8..9ac6613708 100644 --- a/osu.Game/Storyboards/Commands/StoryboardBlendingParametersCommand.cs +++ b/osu.Game/Storyboards/Commands/StoryboardBlendingParametersCommand.cs @@ -8,8 +8,8 @@ namespace osu.Game.Storyboards.Commands { public class StoryboardBlendingParametersCommand : StoryboardCommand { - public StoryboardBlendingParametersCommand(double startTime, double endTime, BlendingParameters startValue, BlendingParameters endValue, Easing easing) - : base(startTime, endTime, startValue, endValue, easing) + public StoryboardBlendingParametersCommand(Easing easing, double startTime, double endTime, BlendingParameters startValue, BlendingParameters endValue) + : base(easing, startTime, endTime, startValue, endValue) { } diff --git a/osu.Game/Storyboards/Commands/StoryboardColourCommand.cs b/osu.Game/Storyboards/Commands/StoryboardColourCommand.cs index 66390eb305..da8a20647c 100644 --- a/osu.Game/Storyboards/Commands/StoryboardColourCommand.cs +++ b/osu.Game/Storyboards/Commands/StoryboardColourCommand.cs @@ -9,8 +9,8 @@ namespace osu.Game.Storyboards.Commands { public class StoryboardColourCommand : StoryboardCommand { - public StoryboardColourCommand(double startTime, double endTime, Color4 startValue, Color4 endValue, Easing easing) - : base(startTime, endTime, startValue, endValue, easing) + public StoryboardColourCommand(Easing easing, double startTime, double endTime, Color4 startValue, Color4 endValue) + : base(easing, startTime, endTime, startValue, endValue) { } diff --git a/osu.Game/Storyboards/Commands/StoryboardCommand.cs b/osu.Game/Storyboards/Commands/StoryboardCommand.cs index 4f2f0f04a2..60c28e7833 100644 --- a/osu.Game/Storyboards/Commands/StoryboardCommand.cs +++ b/osu.Game/Storyboards/Commands/StoryboardCommand.cs @@ -19,7 +19,7 @@ namespace osu.Game.Storyboards.Commands public double Duration => EndTime - StartTime; - protected StoryboardCommand(double startTime, double endTime, T startValue, T endValue, Easing easing) + protected StoryboardCommand(Easing easing, double startTime, double endTime, T startValue, T endValue) { if (endTime < startTime) endTime = startTime; diff --git a/osu.Game/Storyboards/Commands/StoryboardCommandGroup.cs b/osu.Game/Storyboards/Commands/StoryboardCommandGroup.cs index fb847d2e44..40dd8f78e6 100644 --- a/osu.Game/Storyboards/Commands/StoryboardCommandGroup.cs +++ b/osu.Game/Storyboards/Commands/StoryboardCommandGroup.cs @@ -45,35 +45,35 @@ namespace osu.Game.Storyboards.Commands [JsonIgnore] public bool HasCommands { get; private set; } - public void AddX(double startTime, double endTime, float startValue, float endValue, Easing easing) - => AddCommand(X, new StoryboardXCommand(startTime, endTime, startValue, endValue, easing)); + public void AddX(Easing easing, double startTime, double endTime, float startValue, float endValue) + => AddCommand(X, new StoryboardXCommand(easing, startTime, endTime, startValue, endValue)); - public void AddY(double startTime, double endTime, float startValue, float endValue, Easing easing) - => AddCommand(Y, new StoryboardYCommand(startTime, endTime, startValue, endValue, easing)); + public void AddY(Easing easing, double startTime, double endTime, float startValue, float endValue) + => AddCommand(Y, new StoryboardYCommand(easing, startTime, endTime, startValue, endValue)); - public void AddScale(double startTime, double endTime, float startValue, float endValue, Easing easing) - => AddCommand(Scale, new StoryboardScaleCommand(startTime, endTime, startValue, endValue, easing)); + public void AddScale(Easing easing, double startTime, double endTime, float startValue, float endValue) + => AddCommand(Scale, new StoryboardScaleCommand(easing, startTime, endTime, startValue, endValue)); - public void AddVectorScale(double startTime, double endTime, Vector2 startValue, Vector2 endValue, Easing easing) - => AddCommand(VectorScale, new StoryboardVectorScaleCommand(startTime, endTime, startValue, endValue, easing)); + public void AddVectorScale(Easing easing, double startTime, double endTime, Vector2 startValue, Vector2 endValue) + => AddCommand(VectorScale, new StoryboardVectorScaleCommand(easing, startTime, endTime, startValue, endValue)); - public void AddRotation(double startTime, double endTime, float startValue, float endValue, Easing easing) - => AddCommand(Rotation, new StoryboardRotationCommand(startTime, endTime, startValue, endValue, easing)); + public void AddRotation(Easing easing, double startTime, double endTime, float startValue, float endValue) + => AddCommand(Rotation, new StoryboardRotationCommand(easing, startTime, endTime, startValue, endValue)); - public void AddColour(double startTime, double endTime, Color4 startValue, Color4 endValue, Easing easing) - => AddCommand(Colour, new StoryboardColourCommand(startTime, endTime, startValue, endValue, easing)); + public void AddColour(Easing easing, double startTime, double endTime, Color4 startValue, Color4 endValue) + => AddCommand(Colour, new StoryboardColourCommand(easing, startTime, endTime, startValue, endValue)); - public void AddAlpha(double startTime, double endTime, float startValue, float endValue, Easing easing) - => AddCommand(Alpha, new StoryboardAlphaCommand(startTime, endTime, startValue, endValue, easing)); + public void AddAlpha(Easing easing, double startTime, double endTime, float startValue, float endValue) + => AddCommand(Alpha, new StoryboardAlphaCommand(easing, startTime, endTime, startValue, endValue)); - public void AddBlendingParameters(double startTime, double endTime, BlendingParameters startValue, BlendingParameters endValue, Easing easing) - => AddCommand(BlendingParameters, new StoryboardBlendingParametersCommand(startTime, endTime, startValue, endValue, easing)); + public void AddBlendingParameters(Easing easing, double startTime, double endTime, BlendingParameters startValue, BlendingParameters endValue) + => AddCommand(BlendingParameters, new StoryboardBlendingParametersCommand(easing, startTime, endTime, startValue, endValue)); - public void AddFlipH(double startTime, double endTime, bool startValue, bool endValue, Easing easing) - => AddCommand(FlipH, new StoryboardFlipHCommand(startTime, endTime, startValue, endValue, easing)); + public void AddFlipH(Easing easing, double startTime, double endTime, bool startValue, bool endValue) + => AddCommand(FlipH, new StoryboardFlipHCommand(easing, startTime, endTime, startValue, endValue)); - public void AddFlipV(double startTime, double endTime, bool startValue, bool endValue, Easing easing) - => AddCommand(FlipV, new StoryboardFlipVCommand(startTime, endTime, startValue, endValue, easing)); + public void AddFlipV(Easing easing, double startTime, double endTime, bool startValue, bool endValue) + => AddCommand(FlipV, new StoryboardFlipVCommand(easing, startTime, endTime, startValue, endValue)); /// /// Adds the given storyboard to the target . diff --git a/osu.Game/Storyboards/Commands/StoryboardFlipHCommand.cs b/osu.Game/Storyboards/Commands/StoryboardFlipHCommand.cs index 26aef23226..fa07ff6645 100644 --- a/osu.Game/Storyboards/Commands/StoryboardFlipHCommand.cs +++ b/osu.Game/Storyboards/Commands/StoryboardFlipHCommand.cs @@ -9,8 +9,8 @@ namespace osu.Game.Storyboards.Commands { public class StoryboardFlipHCommand : StoryboardCommand { - public StoryboardFlipHCommand(double startTime, double endTime, bool startValue, bool endValue, Easing easing) - : base(startTime, endTime, startValue, endValue, easing) + public StoryboardFlipHCommand(Easing easing, double startTime, double endTime, bool startValue, bool endValue) + : base(easing, startTime, endTime, startValue, endValue) { } diff --git a/osu.Game/Storyboards/Commands/StoryboardFlipVCommand.cs b/osu.Game/Storyboards/Commands/StoryboardFlipVCommand.cs index 88423da2af..fa6a170c25 100644 --- a/osu.Game/Storyboards/Commands/StoryboardFlipVCommand.cs +++ b/osu.Game/Storyboards/Commands/StoryboardFlipVCommand.cs @@ -9,8 +9,8 @@ namespace osu.Game.Storyboards.Commands { public class StoryboardFlipVCommand : StoryboardCommand { - public StoryboardFlipVCommand(double startTime, double endTime, bool startValue, bool endValue, Easing easing) - : base(startTime, endTime, startValue, endValue, easing) + public StoryboardFlipVCommand(Easing easing, double startTime, double endTime, bool startValue, bool endValue) + : base(easing, startTime, endTime, startValue, endValue) { } diff --git a/osu.Game/Storyboards/Commands/StoryboardLoopingGroup.cs b/osu.Game/Storyboards/Commands/StoryboardLoopingGroup.cs index e97de84ab7..a886998679 100644 --- a/osu.Game/Storyboards/Commands/StoryboardLoopingGroup.cs +++ b/osu.Game/Storyboards/Commands/StoryboardLoopingGroup.cs @@ -43,7 +43,7 @@ namespace osu.Game.Storyboards.Commands // In an ideal world, we would multiply the command duration by TotalIterations in command end time. // Unfortunately this would clash with how stable handled end times, and results in some storyboards playing outro // sequences for minutes or hours. - : base(loopingGroup.loopStartTime + command.StartTime, loopingGroup.loopStartTime + command.EndTime, command.StartValue, command.EndValue, command.Easing) + : base(command.Easing, loopingGroup.loopStartTime + command.StartTime, loopingGroup.loopStartTime + command.EndTime, command.StartValue, command.EndValue) { this.command = command; this.loopingGroup = loopingGroup; diff --git a/osu.Game/Storyboards/Commands/StoryboardRotationCommand.cs b/osu.Game/Storyboards/Commands/StoryboardRotationCommand.cs index 4347dc9d77..7e097fce25 100644 --- a/osu.Game/Storyboards/Commands/StoryboardRotationCommand.cs +++ b/osu.Game/Storyboards/Commands/StoryboardRotationCommand.cs @@ -8,8 +8,8 @@ namespace osu.Game.Storyboards.Commands { public class StoryboardRotationCommand : StoryboardCommand { - public StoryboardRotationCommand(double startTime, double endTime, float startValue, float endValue, Easing easing) - : base(startTime, endTime, startValue, endValue, easing) + public StoryboardRotationCommand(Easing easing, double startTime, double endTime, float startValue, float endValue) + : base(easing, startTime, endTime, startValue, endValue) { } diff --git a/osu.Game/Storyboards/Commands/StoryboardScaleCommand.cs b/osu.Game/Storyboards/Commands/StoryboardScaleCommand.cs index b0f33fd6b8..832533af5e 100644 --- a/osu.Game/Storyboards/Commands/StoryboardScaleCommand.cs +++ b/osu.Game/Storyboards/Commands/StoryboardScaleCommand.cs @@ -9,8 +9,8 @@ namespace osu.Game.Storyboards.Commands { public class StoryboardScaleCommand : StoryboardCommand { - public StoryboardScaleCommand(double startTime, double endTime, float startValue, float endValue, Easing easing) - : base(startTime, endTime, startValue, endValue, easing) + public StoryboardScaleCommand(Easing easing, double startTime, double endTime, float startValue, float endValue) + : base(easing, startTime, endTime, startValue, endValue) { } diff --git a/osu.Game/Storyboards/Commands/StoryboardVectorScaleCommand.cs b/osu.Game/Storyboards/Commands/StoryboardVectorScaleCommand.cs index 5d3fef5948..06983a1590 100644 --- a/osu.Game/Storyboards/Commands/StoryboardVectorScaleCommand.cs +++ b/osu.Game/Storyboards/Commands/StoryboardVectorScaleCommand.cs @@ -10,8 +10,8 @@ namespace osu.Game.Storyboards.Commands { public class StoryboardVectorScaleCommand : StoryboardCommand { - public StoryboardVectorScaleCommand(double startTime, double endTime, Vector2 startValue, Vector2 endValue, Easing easing) - : base(startTime, endTime, startValue, endValue, easing) + public StoryboardVectorScaleCommand(Easing easing, double startTime, double endTime, Vector2 startValue, Vector2 endValue) + : base(easing, startTime, endTime, startValue, endValue) { } diff --git a/osu.Game/Storyboards/Commands/StoryboardXCommand.cs b/osu.Game/Storyboards/Commands/StoryboardXCommand.cs index 7df9a75768..d52e9c8a05 100644 --- a/osu.Game/Storyboards/Commands/StoryboardXCommand.cs +++ b/osu.Game/Storyboards/Commands/StoryboardXCommand.cs @@ -8,8 +8,8 @@ namespace osu.Game.Storyboards.Commands { public class StoryboardXCommand : StoryboardCommand { - public StoryboardXCommand(double startTime, double endTime, float startValue, float endValue, Easing easing) - : base(startTime, endTime, startValue, endValue, easing) + public StoryboardXCommand(Easing easing, double startTime, double endTime, float startValue, float endValue) + : base(easing, startTime, endTime, startValue, endValue) { } diff --git a/osu.Game/Storyboards/Commands/StoryboardYCommand.cs b/osu.Game/Storyboards/Commands/StoryboardYCommand.cs index d7dc32a0f3..90dfe4d995 100644 --- a/osu.Game/Storyboards/Commands/StoryboardYCommand.cs +++ b/osu.Game/Storyboards/Commands/StoryboardYCommand.cs @@ -8,8 +8,8 @@ namespace osu.Game.Storyboards.Commands { public class StoryboardYCommand : StoryboardCommand { - public StoryboardYCommand(double startTime, double endTime, float startValue, float endValue, Easing easing) - : base(startTime, endTime, startValue, endValue, easing) + public StoryboardYCommand(Easing easing, double startTime, double endTime, float startValue, float endValue) + : base(easing, startTime, endTime, startValue, endValue) { } From 0efa12a86a5b3f19251e90b65f80f523a9ded5bc Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 8 Mar 2024 21:52:56 +0300 Subject: [PATCH 0289/2493] Fix parameter commands applying initial value before start time --- osu.Game/Storyboards/Commands/IStoryboardCommand.cs | 3 +++ .../Commands/StoryboardBlendingParametersCommand.cs | 6 +++++- osu.Game/Storyboards/Commands/StoryboardFlipHCommand.cs | 6 +++++- osu.Game/Storyboards/Commands/StoryboardFlipVCommand.cs | 6 +++++- 4 files changed, 18 insertions(+), 3 deletions(-) diff --git a/osu.Game/Storyboards/Commands/IStoryboardCommand.cs b/osu.Game/Storyboards/Commands/IStoryboardCommand.cs index 6efb19afe4..ea14f5fa40 100644 --- a/osu.Game/Storyboards/Commands/IStoryboardCommand.cs +++ b/osu.Game/Storyboards/Commands/IStoryboardCommand.cs @@ -28,6 +28,9 @@ namespace osu.Game.Storyboards.Commands /// /// Sets the value of the corresponding property in to the start value of this command. /// + /// + /// Parameter commands (e.g. / / ) only apply the start value if they have zero duration, i.e. take "permanent" effect regardless of time. + /// /// The target drawable. void ApplyInitialValue(TDrawable d) where TDrawable : Drawable, IFlippable, IVectorScalable; diff --git a/osu.Game/Storyboards/Commands/StoryboardBlendingParametersCommand.cs b/osu.Game/Storyboards/Commands/StoryboardBlendingParametersCommand.cs index 9ac6613708..cf9cadf1a7 100644 --- a/osu.Game/Storyboards/Commands/StoryboardBlendingParametersCommand.cs +++ b/osu.Game/Storyboards/Commands/StoryboardBlendingParametersCommand.cs @@ -15,7 +15,11 @@ namespace osu.Game.Storyboards.Commands public override string PropertyName => nameof(Drawable.Blending); - public override void ApplyInitialValue(TDrawable d) => d.Blending = StartValue; + public override void ApplyInitialValue(TDrawable d) + { + if (StartTime == EndTime) + d.Blending = StartValue; + } public override TransformSequence ApplyTransforms(TDrawable d) => d.TransformTo(nameof(d.Blending), StartValue).Delay(Duration) diff --git a/osu.Game/Storyboards/Commands/StoryboardFlipHCommand.cs b/osu.Game/Storyboards/Commands/StoryboardFlipHCommand.cs index fa07ff6645..fbf7295f15 100644 --- a/osu.Game/Storyboards/Commands/StoryboardFlipHCommand.cs +++ b/osu.Game/Storyboards/Commands/StoryboardFlipHCommand.cs @@ -16,7 +16,11 @@ namespace osu.Game.Storyboards.Commands public override string PropertyName => nameof(IFlippable.FlipH); - public override void ApplyInitialValue(TDrawable d) => d.FlipH = StartValue; + public override void ApplyInitialValue(TDrawable d) + { + if (StartTime == EndTime) + d.FlipH = StartValue; + } public override TransformSequence ApplyTransforms(TDrawable d) => d.TransformTo(nameof(IFlippable.FlipH), StartValue).Delay(Duration) diff --git a/osu.Game/Storyboards/Commands/StoryboardFlipVCommand.cs b/osu.Game/Storyboards/Commands/StoryboardFlipVCommand.cs index fa6a170c25..136bd52f1f 100644 --- a/osu.Game/Storyboards/Commands/StoryboardFlipVCommand.cs +++ b/osu.Game/Storyboards/Commands/StoryboardFlipVCommand.cs @@ -16,7 +16,11 @@ namespace osu.Game.Storyboards.Commands public override string PropertyName => nameof(IFlippable.FlipV); - public override void ApplyInitialValue(TDrawable d) => d.FlipV = StartValue; + public override void ApplyInitialValue(TDrawable d) + { + if (StartTime == EndTime) + d.FlipV = StartValue; + } public override TransformSequence ApplyTransforms(TDrawable d) => d.TransformTo(nameof(IFlippable.FlipV), StartValue).Delay(Duration) From 6861d9a302f2995ac05ff717e8911d9eabe3e25b Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 8 Mar 2024 22:28:13 +0300 Subject: [PATCH 0290/2493] Expose storyboard command lists as read-only and remove unnecessary memory footprint Mutation should be done only with the methods exposed by `StoryboardCommandGroup`. --- .../Commands/StoryboardCommandGroup.cs | 80 +++++++++++++------ 1 file changed, 57 insertions(+), 23 deletions(-) diff --git a/osu.Game/Storyboards/Commands/StoryboardCommandGroup.cs b/osu.Game/Storyboards/Commands/StoryboardCommandGroup.cs index 40dd8f78e6..0925231412 100644 --- a/osu.Game/Storyboards/Commands/StoryboardCommandGroup.cs +++ b/osu.Game/Storyboards/Commands/StoryboardCommandGroup.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 Newtonsoft.Json; using osu.Framework.Graphics; using osu.Framework.Lists; @@ -12,20 +13,45 @@ namespace osu.Game.Storyboards.Commands { public class StoryboardCommandGroup { - public SortedList> X = new SortedList>(); - public SortedList> Y = new SortedList>(); - public SortedList> Scale = new SortedList>(); - public SortedList> VectorScale = new SortedList>(); - public SortedList> Rotation = new SortedList>(); - public SortedList> Colour = new SortedList>(); - public SortedList> Alpha = new SortedList>(); - public SortedList> BlendingParameters = new SortedList>(); - public SortedList> FlipH = new SortedList>(); - public SortedList> FlipV = new SortedList>(); + private readonly SortedList> x = new SortedList>(); - public IReadOnlyList AllCommands => allCommands; + public IReadOnlyList> X => x; - private readonly List allCommands = new List(); + private readonly SortedList> y = new SortedList>(); + + public IReadOnlyList> Y => y; + + private readonly SortedList> scale = new SortedList>(); + + public IReadOnlyList> Scale => scale; + + private readonly SortedList> vectorScale = new SortedList>(); + + public IReadOnlyList> VectorScale => vectorScale; + + private readonly SortedList> rotation = new SortedList>(); + + public IReadOnlyList> Rotation => rotation; + + private readonly SortedList> colour = new SortedList>(); + + public IReadOnlyList> Colour => colour; + + private readonly SortedList> alpha = new SortedList>(); + + public IReadOnlyList> Alpha => alpha; + + private readonly SortedList> blendingParameters = new SortedList>(); + + public IReadOnlyList> BlendingParameters => blendingParameters; + + private readonly SortedList> flipH = new SortedList>(); + + public IReadOnlyList> FlipH => flipH; + + private readonly SortedList> flipV = new SortedList>(); + + public IReadOnlyList> FlipV => flipV; /// /// Returns the earliest start time of the commands added to this group. @@ -45,35 +71,44 @@ namespace osu.Game.Storyboards.Commands [JsonIgnore] public bool HasCommands { get; private set; } + private readonly IReadOnlyList[] lists; + + public IEnumerable AllCommands => lists.SelectMany(g => g); + + public StoryboardCommandGroup() + { + lists = new IReadOnlyList[] { X, Y, Scale, VectorScale, Rotation, Colour, Alpha, BlendingParameters, FlipH, FlipV }; + } + public void AddX(Easing easing, double startTime, double endTime, float startValue, float endValue) - => AddCommand(X, new StoryboardXCommand(easing, startTime, endTime, startValue, endValue)); + => AddCommand(x, new StoryboardXCommand(easing, startTime, endTime, startValue, endValue)); public void AddY(Easing easing, double startTime, double endTime, float startValue, float endValue) - => AddCommand(Y, new StoryboardYCommand(easing, startTime, endTime, startValue, endValue)); + => AddCommand(y, new StoryboardYCommand(easing, startTime, endTime, startValue, endValue)); public void AddScale(Easing easing, double startTime, double endTime, float startValue, float endValue) - => AddCommand(Scale, new StoryboardScaleCommand(easing, startTime, endTime, startValue, endValue)); + => AddCommand(scale, new StoryboardScaleCommand(easing, startTime, endTime, startValue, endValue)); public void AddVectorScale(Easing easing, double startTime, double endTime, Vector2 startValue, Vector2 endValue) - => AddCommand(VectorScale, new StoryboardVectorScaleCommand(easing, startTime, endTime, startValue, endValue)); + => AddCommand(vectorScale, new StoryboardVectorScaleCommand(easing, startTime, endTime, startValue, endValue)); public void AddRotation(Easing easing, double startTime, double endTime, float startValue, float endValue) - => AddCommand(Rotation, new StoryboardRotationCommand(easing, startTime, endTime, startValue, endValue)); + => AddCommand(rotation, new StoryboardRotationCommand(easing, startTime, endTime, startValue, endValue)); public void AddColour(Easing easing, double startTime, double endTime, Color4 startValue, Color4 endValue) - => AddCommand(Colour, new StoryboardColourCommand(easing, startTime, endTime, startValue, endValue)); + => AddCommand(colour, new StoryboardColourCommand(easing, startTime, endTime, startValue, endValue)); public void AddAlpha(Easing easing, double startTime, double endTime, float startValue, float endValue) - => AddCommand(Alpha, new StoryboardAlphaCommand(easing, startTime, endTime, startValue, endValue)); + => AddCommand(alpha, new StoryboardAlphaCommand(easing, startTime, endTime, startValue, endValue)); public void AddBlendingParameters(Easing easing, double startTime, double endTime, BlendingParameters startValue, BlendingParameters endValue) - => AddCommand(BlendingParameters, new StoryboardBlendingParametersCommand(easing, startTime, endTime, startValue, endValue)); + => AddCommand(blendingParameters, new StoryboardBlendingParametersCommand(easing, startTime, endTime, startValue, endValue)); public void AddFlipH(Easing easing, double startTime, double endTime, bool startValue, bool endValue) - => AddCommand(FlipH, new StoryboardFlipHCommand(easing, startTime, endTime, startValue, endValue)); + => AddCommand(flipH, new StoryboardFlipHCommand(easing, startTime, endTime, startValue, endValue)); public void AddFlipV(Easing easing, double startTime, double endTime, bool startValue, bool endValue) - => AddCommand(FlipV, new StoryboardFlipVCommand(easing, startTime, endTime, startValue, endValue)); + => AddCommand(flipV, new StoryboardFlipVCommand(easing, startTime, endTime, startValue, endValue)); /// /// Adds the given storyboard to the target . @@ -83,7 +118,6 @@ namespace osu.Game.Storyboards.Commands protected virtual void AddCommand(ICollection> list, StoryboardCommand command) { list.Add(command); - allCommands.Add(command); HasCommands = true; if (command.StartTime < StartTime) From 1942d46a38fac400e28dbe95a3d5a2d6c0a95cc2 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 8 Mar 2024 22:37:27 +0300 Subject: [PATCH 0291/2493] Remove leftover debugging code --- osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs index 507a51aca4..da4b5c641d 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs @@ -102,9 +102,6 @@ namespace osu.Game.Storyboards.Drawables else Texture = textureStore.Get(Sprite.Path); - if (Sprite.Path == "SB/textbox.png") - Debugger.Break(); - Sprite.ApplyTransforms(this); } From 8c92bb0595f6528c86c39a0851fd7e6edd8be5f6 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 8 Mar 2024 23:09:16 +0300 Subject: [PATCH 0292/2493] Remove unused using directive --- osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs index da4b5c641d..ec875219b6 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.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.Extensions.ObjectExtensions; using osu.Framework.Graphics; From 9f71eac1dbe26df48cdcdaff181a0ca6be71f219 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 8 Mar 2024 23:09:50 +0300 Subject: [PATCH 0293/2493] Remove extra end line --- osu.Game/Storyboards/StoryboardSprite.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Storyboards/StoryboardSprite.cs b/osu.Game/Storyboards/StoryboardSprite.cs index 944d77e745..80a76cd831 100644 --- a/osu.Game/Storyboards/StoryboardSprite.cs +++ b/osu.Game/Storyboards/StoryboardSprite.cs @@ -195,4 +195,3 @@ namespace osu.Game.Storyboards public override string ToString() => $"{Path}, {Origin}, {InitialPosition}"; } } - From db1c59475b0e31fd17d6944804c23a65b206c756 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Fri, 9 Feb 2024 15:10:45 -0800 Subject: [PATCH 0294/2493] Wrap beatmap listing filters and match web spacing --- .../BeatmapListingSearchControl.cs | 1 + .../BeatmapListing/BeatmapSearchFilterRow.cs | 37 ++++--------------- ...BeatmapSearchMultipleSelectionFilterRow.cs | 4 +- .../Overlays/BeatmapListing/FilterTabItem.cs | 2 - 4 files changed, 9 insertions(+), 35 deletions(-) diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchControl.cs b/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchControl.cs index 3fa0fc7a77..bab64165cb 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchControl.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchControl.cs @@ -128,6 +128,7 @@ namespace osu.Game.Overlays.BeatmapListing RelativeSizeAxes = Axes.X, Direction = FillDirection.Vertical, Padding = new MarginPadding { Horizontal = 10 }, + Spacing = new Vector2(5), Children = new Drawable[] { generalFilter = new BeatmapSearchGeneralFilterRow(), diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapSearchFilterRow.cs b/osu.Game/Overlays/BeatmapListing/BeatmapSearchFilterRow.cs index 6d75521cb0..f79695a123 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapSearchFilterRow.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapSearchFilterRow.cs @@ -1,14 +1,12 @@ // 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.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.UserInterface; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; -using osu.Game.Graphics.UserInterface; using osuTK; using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Localisation; @@ -49,8 +47,6 @@ namespace osu.Game.Overlays.BeatmapListing { new OsuSpriteText { - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, Font = OsuFont.GetFont(size: 13), Text = header }, @@ -69,10 +65,8 @@ namespace osu.Game.Overlays.BeatmapListing { public BeatmapSearchFilter() { - Anchor = Anchor.BottomLeft; - Origin = Anchor.BottomLeft; RelativeSizeAxes = Axes.X; - Height = 15; + AutoSizeAxes = Axes.Y; TabContainer.Spacing = new Vector2(10, 0); @@ -83,33 +77,16 @@ namespace osu.Game.Overlays.BeatmapListing } } - [BackgroundDependencyLoader] - private void load(OverlayColourProvider colourProvider) - { - if (Dropdown is FilterDropdown fd) - fd.AccentColour = colourProvider.Light2; - } - - protected override Dropdown CreateDropdown() => new FilterDropdown(); + protected override Dropdown CreateDropdown() => null!; protected override TabItem CreateTabItem(T value) => new FilterTabItem(value); - private partial class FilterDropdown : OsuTabDropdown + protected override TabFillFlowContainer CreateTabFlow() => new TabFillFlowContainer { - protected override DropdownHeader CreateHeader() => new FilterHeader - { - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight - }; - - private partial class FilterHeader : OsuTabDropdownHeader - { - public FilterHeader() - { - Background.Height = 1; - } - } - } + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + AllowMultiline = true, + }; } } } diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapSearchMultipleSelectionFilterRow.cs b/osu.Game/Overlays/BeatmapListing/BeatmapSearchMultipleSelectionFilterRow.cs index abd2643a41..e59beb43ff 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapSearchMultipleSelectionFilterRow.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapSearchMultipleSelectionFilterRow.cs @@ -52,10 +52,8 @@ namespace osu.Game.Overlays.BeatmapListing [BackgroundDependencyLoader] private void load() { - Anchor = Anchor.BottomLeft; - Origin = Anchor.BottomLeft; RelativeSizeAxes = Axes.X; - Height = 15; + AutoSizeAxes = Axes.Y; Spacing = new Vector2(10, 0); AddRange(GetValues().Select(CreateTabItem)); diff --git a/osu.Game/Overlays/BeatmapListing/FilterTabItem.cs b/osu.Game/Overlays/BeatmapListing/FilterTabItem.cs index c33d5056fa..831cf812ff 100644 --- a/osu.Game/Overlays/BeatmapListing/FilterTabItem.cs +++ b/osu.Game/Overlays/BeatmapListing/FilterTabItem.cs @@ -33,8 +33,6 @@ namespace osu.Game.Overlays.BeatmapListing private void load() { AutoSizeAxes = Axes.Both; - Anchor = Anchor.BottomLeft; - Origin = Anchor.BottomLeft; AddRangeInternal(new Drawable[] { text = new OsuSpriteText From 82048df9f16607c8b9887ddf91cf880fdb5927c3 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 9 Mar 2024 04:42:17 +0300 Subject: [PATCH 0295/2493] Add basic test scene for asserting storyboard commands behaviour Pending actual test coverage. --- .../Gameplay/TestSceneStoryboardCommands.cs | 166 ++++++++++++++++++ 1 file changed, 166 insertions(+) create mode 100644 osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardCommands.cs diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardCommands.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardCommands.cs new file mode 100644 index 0000000000..1893182b32 --- /dev/null +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardCommands.cs @@ -0,0 +1,166 @@ +// 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.IO; +using System.Threading; +using System.Threading.Tasks; +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Framework.IO.Stores; +using osu.Framework.Timing; +using osu.Game.Graphics.Sprites; +using osu.Game.Rulesets.Mods; +using osu.Game.Storyboards; +using osu.Game.Storyboards.Drawables; +using osu.Game.Tests.Resources; +using osuTK; + +namespace osu.Game.Tests.Visual.Gameplay +{ + public partial class TestSceneStoryboardCommands : OsuTestScene + { + [Cached(typeof(Storyboard))] + private TestStoryboard storyboard { get; set; } = new TestStoryboard + { + UseSkinSprites = false, + AlwaysProvideTexture = true, + }; + + private readonly ManualClock manualClock = new ManualClock { Rate = 1, IsRunning = true }; + private bool forward; + + private const string lookup_name = "hitcircleoverlay"; + private const double clock_limit = 2500; + + protected override Container Content => content; + + private Container content = null!; + private SpriteText timelineText = null!; + private Box timelineMarker = null!; + + [BackgroundDependencyLoader] + private void load() + { + base.Content.Children = new Drawable[] + { + content = new Container + { + RelativeSizeAxes = Axes.Both, + }, + timelineText = new OsuSpriteText + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Margin = new MarginPadding { Bottom = 60 }, + }, + timelineMarker = new Box + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomCentre, + RelativePositionAxes = Axes.X, + Size = new Vector2(2, 50), + }, + }; + } + + [SetUp] + public void SetUp() + { + manualClock.CurrentTime = 0; + forward = true; + } + + [Test] + public void TestLoop() + { + AddStep("create sprite", () => Child = createSprite(s => + { + var loop = s.AddLoopingGroup(600, 10); + loop.AddY(Easing.OutBounce, 0, 500, 100, 240); + loop.AddY(Easing.OutQuint, 700, 1000, 240, 100); + })); + } + + protected override void Update() + { + base.Update(); + + if (manualClock.CurrentTime > clock_limit || manualClock.CurrentTime < 0) + forward = !forward; + + manualClock.CurrentTime += Time.Elapsed * (forward ? 1 : -1); + timelineText.Text = $"Time: {manualClock.CurrentTime:0}ms"; + timelineMarker.X = (float)(manualClock.CurrentTime / clock_limit); + } + + private DrawableStoryboard createSprite(Action? addCommands = null) + { + var layer = storyboard.GetLayer("Background"); + + var sprite = new StoryboardSprite(lookup_name, Anchor.Centre, new Vector2(320, 240)); + sprite.Commands.AddScale(Easing.None, 0, clock_limit, 0.5f, 0.5f); + sprite.Commands.AddAlpha(Easing.None, 0, clock_limit, 1, 1); + addCommands?.Invoke(sprite); + + layer.Elements.Clear(); + layer.Add(sprite); + + return storyboard.CreateDrawable().With(c => c.Clock = new FramedClock(manualClock)); + } + + private partial class TestStoryboard : Storyboard + { + public override DrawableStoryboard CreateDrawable(IReadOnlyList? mods = null) + { + return new TestDrawableStoryboard(this, mods); + } + + public bool AlwaysProvideTexture { get; set; } + + public override string GetStoragePathFromStoryboardPath(string path) => AlwaysProvideTexture ? path : string.Empty; + + private partial class TestDrawableStoryboard : DrawableStoryboard + { + private readonly bool alwaysProvideTexture; + + public TestDrawableStoryboard(TestStoryboard storyboard, IReadOnlyList? mods) + : base(storyboard, mods) + { + alwaysProvideTexture = storyboard.AlwaysProvideTexture; + } + + protected override IResourceStore CreateResourceLookupStore() => alwaysProvideTexture + ? new AlwaysReturnsTextureStore() + : new ResourceStore(); + + internal class AlwaysReturnsTextureStore : IResourceStore + { + private const string test_image = "Resources/Textures/test-image.png"; + + private readonly DllResourceStore store; + + public AlwaysReturnsTextureStore() + { + store = TestResources.GetStore(); + } + + public void Dispose() => store.Dispose(); + + public byte[] Get(string name) => store.Get(test_image); + + public Task GetAsync(string name, CancellationToken cancellationToken = new CancellationToken()) => store.GetAsync(test_image, cancellationToken); + + public Stream GetStream(string name) => store.GetStream(test_image); + + public IEnumerable GetAvailableResources() => store.GetAvailableResources(); + } + } + } + } +} From 8a1c5a754763cb8cc25963ab0e5b3439683a357a Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sun, 10 Mar 2024 07:23:22 +0300 Subject: [PATCH 0296/2493] Adjust time values --- .../Visual/Gameplay/TestSceneStoryboardCommands.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardCommands.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardCommands.cs index 1893182b32..11c07824d3 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardCommands.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardCommands.cs @@ -81,9 +81,9 @@ namespace osu.Game.Tests.Visual.Gameplay { AddStep("create sprite", () => Child = createSprite(s => { - var loop = s.AddLoopingGroup(600, 10); - loop.AddY(Easing.OutBounce, 0, 500, 100, 240); - loop.AddY(Easing.OutQuint, 700, 1000, 240, 100); + var loop = s.AddLoopingGroup(500, 10); + loop.AddY(Easing.OutBounce, 0, 600, 100, 240); + loop.AddY(Easing.OutQuint, 800, 1200, 240, 100); })); } From d039b565621790c8df3b5eaf0243deaa0cd9d5bb Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sun, 10 Mar 2024 07:26:27 +0300 Subject: [PATCH 0297/2493] Add test case for running with high number of loops --- .../Visual/Gameplay/TestSceneStoryboardCommands.cs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardCommands.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardCommands.cs index 11c07824d3..1392cc3a21 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardCommands.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardCommands.cs @@ -87,6 +87,17 @@ namespace osu.Game.Tests.Visual.Gameplay })); } + [Test] + public void TestLoopManyTimes() + { + AddStep("create sprite", () => Child = createSprite(s => + { + var loop = s.AddLoopingGroup(500, 10000); + loop.AddY(Easing.OutBounce, 0, 60, 100, 240); + loop.AddY(Easing.OutQuint, 80, 120, 240, 100); + })); + } + protected override void Update() { base.Update(); From 99b06102b1bb67cd13c2e582c9796212d31c65d1 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sun, 10 Mar 2024 09:01:31 +0300 Subject: [PATCH 0298/2493] Add enough test coverage --- .../Gameplay/TestSceneStoryboardCommands.cs | 115 +++++++++++++++--- 1 file changed, 101 insertions(+), 14 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardCommands.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardCommands.cs index 1392cc3a21..4af3d23463 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardCommands.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardCommands.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Threading; using System.Threading.Tasks; using NUnit.Framework; @@ -13,6 +14,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.IO.Stores; +using osu.Framework.Testing; using osu.Framework.Timing; using osu.Game.Graphics.Sprites; using osu.Game.Rulesets.Mods; @@ -33,7 +35,7 @@ namespace osu.Game.Tests.Visual.Gameplay }; private readonly ManualClock manualClock = new ManualClock { Rate = 1, IsRunning = true }; - private bool forward; + private int clockDirection; private const string lookup_name = "hitcircleoverlay"; private const double clock_limit = 2500; @@ -69,28 +71,75 @@ namespace osu.Game.Tests.Visual.Gameplay }; } - [SetUp] - public void SetUp() + [SetUpSteps] + public void SetUpSteps() { - manualClock.CurrentTime = 0; - forward = true; + AddStep("start clock", () => clockDirection = 1); + AddStep("pause clock", () => clockDirection = 0); + AddStep("set clock = 0", () => manualClock.CurrentTime = 0); } [Test] - public void TestLoop() + public void TestNormalCommandPlayback() { - AddStep("create sprite", () => Child = createSprite(s => + AddStep("create storyboard", () => Child = createStoryboard(s => { - var loop = s.AddLoopingGroup(500, 10); - loop.AddY(Easing.OutBounce, 0, 600, 100, 240); - loop.AddY(Easing.OutQuint, 800, 1200, 240, 100); + s.Commands.AddY(Easing.OutBounce, 500, 900, 100, 240); + s.Commands.AddY(Easing.OutQuint, 1100, 1500, 240, 100); })); + + assert(0, 100); + assert(500, 100); + assert(1000, 240); + assert(1500, 100); + assert(clock_limit, 100); + assert(1500, 100); + assert(1000, 240); + assert(500, 100); + assert(0, 100); + + void assert(double time, double y) + { + AddStep($"set clock = {time}", () => manualClock.CurrentTime = time); + AddAssert($"sprite y = {y} at t = {time}", () => this.ChildrenOfType().Single().Y == y); + } + } + + [Test] + public void TestLoopingCommandsPlayback() + { + AddStep("create storyboard", () => Child = createStoryboard(s => + { + var loop = s.AddLoopingGroup(250, 1); + loop.AddY(Easing.OutBounce, 0, 400, 100, 240); + loop.AddY(Easing.OutQuint, 600, 1000, 240, 100); + })); + + assert(0, 100); + assert(250, 100); + assert(850, 240); + assert(1250, 100); + assert(1850, 240); + assert(2250, 100); + assert(clock_limit, 100); + assert(2250, 100); + assert(1850, 240); + assert(1250, 100); + assert(850, 240); + assert(250, 100); + assert(0, 100); + + void assert(double time, double y) + { + AddStep($"set clock = {time}", () => manualClock.CurrentTime = time); + AddAssert($"sprite y = {y} at t = {time}", () => this.ChildrenOfType().Single().Y == y); + } } [Test] public void TestLoopManyTimes() { - AddStep("create sprite", () => Child = createSprite(s => + AddStep("create storyboard", () => Child = createStoryboard(s => { var loop = s.AddLoopingGroup(500, 10000); loop.AddY(Easing.OutBounce, 0, 60, 100, 240); @@ -98,19 +147,57 @@ namespace osu.Game.Tests.Visual.Gameplay })); } + [Test] + public void TestParameterTemporaryEffect() + { + AddStep("create storyboard", () => Child = createStoryboard(s => + { + s.Commands.AddFlipV(Easing.None, 1000, 1500, true, false); + })); + + AddAssert("sprite not flipped at t = 0", () => !this.ChildrenOfType().Single().FlipV); + + AddStep("set clock = 1250", () => manualClock.CurrentTime = 1250); + AddAssert("sprite flipped at t = 1250", () => this.ChildrenOfType().Single().FlipV); + + AddStep("set clock = 2000", () => manualClock.CurrentTime = 2000); + AddAssert("sprite not flipped at t = 2000", () => !this.ChildrenOfType().Single().FlipV); + + AddStep("resume clock", () => clockDirection = 1); + } + + [Test] + public void TestParameterPermanentEffect() + { + AddStep("create storyboard", () => Child = createStoryboard(s => + { + s.Commands.AddFlipV(Easing.None, 1000, 1000, true, true); + })); + + AddAssert("sprite flipped at t = 0", () => this.ChildrenOfType().Single().FlipV); + + AddStep("set clock = 1250", () => manualClock.CurrentTime = 1250); + AddAssert("sprite flipped at t = 1250", () => this.ChildrenOfType().Single().FlipV); + + AddStep("set clock = 2000", () => manualClock.CurrentTime = 2000); + AddAssert("sprite flipped at t = 2000", () => this.ChildrenOfType().Single().FlipV); + + AddStep("resume clock", () => clockDirection = 1); + } + protected override void Update() { base.Update(); if (manualClock.CurrentTime > clock_limit || manualClock.CurrentTime < 0) - forward = !forward; + clockDirection = -clockDirection; - manualClock.CurrentTime += Time.Elapsed * (forward ? 1 : -1); + manualClock.CurrentTime += Time.Elapsed * clockDirection; timelineText.Text = $"Time: {manualClock.CurrentTime:0}ms"; timelineMarker.X = (float)(manualClock.CurrentTime / clock_limit); } - private DrawableStoryboard createSprite(Action? addCommands = null) + private DrawableStoryboard createStoryboard(Action? addCommands = null) { var layer = storyboard.GetLayer("Background"); From 6ff4b1d7e3d461adf4b67c676a62f39ecfd7ec21 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Sun, 10 Mar 2024 15:42:03 +0300 Subject: [PATCH 0299/2493] Don't update SubTreeMasking in OsuPlayfield --- osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs index 411a02c5af..80379094ae 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs @@ -9,6 +9,7 @@ using System.Linq; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Primitives; using osu.Game.Beatmaps; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects; @@ -35,6 +36,8 @@ namespace osu.Game.Rulesets.Osu.UI private readonly JudgementPooler judgementPooler; + public override bool UpdateSubTreeMasking(Drawable source, RectangleF maskingBounds) => false; + public SmokeContainer Smoke { get; } public FollowPointRenderer FollowPoints { get; } From 6ecef33fd7a9dcee06349e1075e10ed3bce9e7c7 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Sun, 10 Mar 2024 22:45:29 +0300 Subject: [PATCH 0300/2493] Fic incorrect ExtendableCircle gradient --- .../Components/Timeline/TimelineHitObjectBlueprint.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs index d4afcc0151..7e6e886ff7 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs @@ -471,6 +471,12 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline /// public partial class ExtendableCircle : CompositeDrawable { + public new ColourInfo Colour + { + get => Content.Colour; + set => Content.Colour = value; + } + protected readonly Circle Content; public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => Content.ReceivePositionalInputAt(screenSpacePos); From 283de215d37aca9605bbf5b0a11dc440455bb348 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 11 Mar 2024 01:01:26 +0300 Subject: [PATCH 0301/2493] Adjust log message and comment --- osu.Desktop/DiscordRichPresence.cs | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/osu.Desktop/DiscordRichPresence.cs b/osu.Desktop/DiscordRichPresence.cs index b4a7e80d48..2e5db2f5c1 100644 --- a/osu.Desktop/DiscordRichPresence.cs +++ b/osu.Desktop/DiscordRichPresence.cs @@ -189,20 +189,14 @@ namespace osu.Desktop private void onJoin(object sender, JoinMessage args) { game.Window?.Raise(); - Logger.Log($"Received room secret from Discord RPC Client: {args.Secret}", LoggingTarget.Network, LogLevel.Debug); - // Stable and Lazer share the same Discord client ID, meaning they can accept join requests from each other. + Logger.Log($"Received room secret from Discord RPC Client: \"{args.Secret}\"", LoggingTarget.Network, LogLevel.Debug); + + // Stable and lazer share the same Discord client ID, meaning they can accept join requests from each other. // Since they aren't compatible in multi, see if stable's format is being used and log to avoid confusion. - // https://discord.com/channels/188630481301012481/188630652340404224/1214697229063946291 - if (args.Secret[0] != '{') + if (args.Secret[0] != '{' || !tryParseRoomSecret(args.Secret, out long roomId, out string? password)) { - Logger.Log("osu!stable rooms are not compatible with lazer.", LoggingTarget.Network, LogLevel.Important); - return; - } - - if (!tryParseRoomSecret(args.Secret, out long roomId, out string? password)) - { - Logger.Log("Could not join multiplayer room.", LoggingTarget.Network, LogLevel.Important); + Logger.Log("Could not join multiplayer room, invitation is invalid or incompatible.", LoggingTarget.Network, LogLevel.Important); return; } @@ -211,7 +205,7 @@ namespace osu.Desktop { game.PresentMultiplayerMatch(room, password); }); - request.Failure += _ => Logger.Log($"Could not find room {roomId} from Discord RPC Client", LoggingTarget.Network, LogLevel.Important); + request.Failure += _ => Logger.Log($"Could not join multiplayer room, room could not be found (room ID: {roomId}).", LoggingTarget.Network, LogLevel.Important); api.Queue(request); } From 2be6d1f1c60f6e146b056f3fc42c1205cea22826 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 11 Mar 2024 11:26:03 +0800 Subject: [PATCH 0302/2493] Apply NRT to `OsuPlayfield` --- osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs index 80379094ae..9b8128a107 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.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.Diagnostics; using System.Linq; @@ -79,7 +77,7 @@ namespace osu.Game.Rulesets.Osu.UI NewResult += onNewResult; } - private IHitPolicy hitPolicy; + private IHitPolicy hitPolicy = null!; public IHitPolicy HitPolicy { @@ -119,12 +117,12 @@ namespace osu.Game.Rulesets.Osu.UI judgementAboveHitObjectLayer.Add(judgement.ProxiedAboveHitObjectsContent); } - [BackgroundDependencyLoader(true)] - private void load(OsuRulesetConfigManager config, IBeatmap beatmap) + [BackgroundDependencyLoader] + private void load(OsuRulesetConfigManager? config, IBeatmap? beatmap) { config?.BindWith(OsuRulesetSetting.PlayfieldBorderStyle, playfieldBorder.PlayfieldBorderStyle); - var osuBeatmap = (OsuBeatmap)beatmap; + var osuBeatmap = (OsuBeatmap?)beatmap; RegisterPool(20, 100); From f3d154a9955697cd25db6a2dd3393daf668acac2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 11 Mar 2024 11:28:15 +0800 Subject: [PATCH 0303/2493] Add inline comment explaining optimisation --- osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs index 9b8128a107..63030293ac 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs @@ -34,6 +34,8 @@ namespace osu.Game.Rulesets.Osu.UI private readonly JudgementPooler judgementPooler; + // For osu! gameplay, everything is always on screen. + // Skipping masking calculations improves performance in intense beatmaps (ie. https://osu.ppy.sh/beatmapsets/150945#osu/372245) public override bool UpdateSubTreeMasking(Drawable source, RectangleF maskingBounds) => false; public SmokeContainer Smoke { get; } From e3936930c7df32ccd53f6c8f4ea2dca14dec3934 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 11 Mar 2024 13:11:57 +0800 Subject: [PATCH 0304/2493] Remove local optimisation in `DrawableHitObject` --- osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index de05219212..28aa2ccb79 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -15,7 +15,6 @@ using osu.Framework.Extensions.ListExtensions; using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Extensions.TypeExtensions; using osu.Framework.Graphics; -using osu.Framework.Graphics.Primitives; using osu.Framework.Lists; using osu.Framework.Threading; using osu.Framework.Utils; @@ -632,8 +631,6 @@ namespace osu.Game.Rulesets.Objects.Drawables #endregion - public override bool UpdateSubTreeMasking(Drawable source, RectangleF maskingBounds) => false; - protected override void UpdateAfterChildren() { base.UpdateAfterChildren(); From d12b11e234f0e9d279e70c71ab595a063ec98e47 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 11 Mar 2024 13:14:29 +0800 Subject: [PATCH 0305/2493] Revert "Remove local optimisation in `DrawableHitObject`" This reverts commit e3936930c7df32ccd53f6c8f4ea2dca14dec3934. --- osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index 28aa2ccb79..de05219212 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -15,6 +15,7 @@ using osu.Framework.Extensions.ListExtensions; using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Extensions.TypeExtensions; using osu.Framework.Graphics; +using osu.Framework.Graphics.Primitives; using osu.Framework.Lists; using osu.Framework.Threading; using osu.Framework.Utils; @@ -631,6 +632,8 @@ namespace osu.Game.Rulesets.Objects.Drawables #endregion + public override bool UpdateSubTreeMasking(Drawable source, RectangleF maskingBounds) => false; + protected override void UpdateAfterChildren() { base.UpdateAfterChildren(); From 091425db3091104269531f28218b3b309e50c426 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 11 Mar 2024 14:43:54 +0800 Subject: [PATCH 0306/2493] Fix nullability hint --- osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs index 63030293ac..a55a55f760 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs @@ -43,7 +43,7 @@ namespace osu.Game.Rulesets.Osu.UI public static readonly Vector2 BASE_SIZE = new Vector2(512, 384); - protected override GameplayCursorContainer CreateCursor() => new OsuCursorContainer(); + protected override GameplayCursorContainer? CreateCursor() => new OsuCursorContainer(); private readonly Container judgementAboveHitObjectLayer; From fc05268fc33fc16f214501f3dffe1ec33a301e29 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 11 Mar 2024 14:45:52 +0800 Subject: [PATCH 0307/2493] Apply NRT to `DrawableOsuEditorRuleset` too --- osu.Game.Rulesets.Osu/Edit/DrawableOsuEditorRuleset.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditorRuleset.cs b/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditorRuleset.cs index 14e7b93f3a..68c565af4d 100644 --- a/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditorRuleset.cs +++ b/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditorRuleset.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.Collections.Generic; using osu.Game.Beatmaps; using osu.Game.Rulesets.Mods; @@ -25,7 +23,7 @@ namespace osu.Game.Rulesets.Osu.Edit private partial class OsuEditorPlayfield : OsuPlayfield { - protected override GameplayCursorContainer CreateCursor() => null; + protected override GameplayCursorContainer? CreateCursor() => null; public OsuEditorPlayfield() { From e0f1f70b820030452c2f32d1d7dfe6417230f9c7 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 11 Mar 2024 15:52:38 +0900 Subject: [PATCH 0308/2493] Adjust NRT to prevent future issues This way, it will yet at us if the setter is ever moved out of the ctor. --- osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs index a55a55f760..4933eb4041 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs @@ -3,6 +3,7 @@ using System; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Graphics; @@ -79,11 +80,12 @@ namespace osu.Game.Rulesets.Osu.UI NewResult += onNewResult; } - private IHitPolicy hitPolicy = null!; + private IHitPolicy hitPolicy; public IHitPolicy HitPolicy { get => hitPolicy; + [MemberNotNull(nameof(hitPolicy))] set { hitPolicy = value ?? throw new ArgumentNullException(nameof(value)); From 226df7163e1b34eb4a78f02f0038493b673a8dce Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 11 Mar 2024 16:55:49 +0800 Subject: [PATCH 0309/2493] Update client ID --- osu.Desktop/DiscordRichPresence.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Desktop/DiscordRichPresence.cs b/osu.Desktop/DiscordRichPresence.cs index 2e5db2f5c1..4e3db2db2d 100644 --- a/osu.Desktop/DiscordRichPresence.cs +++ b/osu.Desktop/DiscordRichPresence.cs @@ -25,7 +25,7 @@ namespace osu.Desktop { internal partial class DiscordRichPresence : Component { - private const string client_id = "367827983903490050"; + private const string client_id = "1216669957799018608"; private DiscordRpcClient client = null!; From 169e2e1b4e21c824e586cd1be88b33875e9a2e30 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 11 Mar 2024 09:54:49 +0300 Subject: [PATCH 0310/2493] Change maximum room number to closest powers of two --- osu.Desktop/DiscordRichPresence.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/osu.Desktop/DiscordRichPresence.cs b/osu.Desktop/DiscordRichPresence.cs index 4e3db2db2d..886038bcf0 100644 --- a/osu.Desktop/DiscordRichPresence.cs +++ b/osu.Desktop/DiscordRichPresence.cs @@ -142,9 +142,8 @@ namespace osu.Desktop Privacy = string.IsNullOrEmpty(room.Settings.Password) ? Party.PrivacySetting.Public : Party.PrivacySetting.Private, ID = room.RoomID.ToString(), // technically lobbies can have infinite users, but Discord needs this to be set to something. - // 1024 just happens to look nice. - // https://discord.com/channels/188630481301012481/188630652340404224/1212967974793642034 - Max = 1024, + // to make party display sensible, assign a powers of two above participants count (8 at minimum). + Max = (int)Math.Max(8, Math.Pow(2, Math.Ceiling(Math.Log2(room.Users.Count)))), Size = room.Users.Count, }; From 8b730acb082379174f9a48b5fd132b184b4e9a81 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 11 Mar 2024 11:18:59 +0300 Subject: [PATCH 0311/2493] Update presence on changes to multiplayer room --- osu.Desktop/DiscordRichPresence.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/osu.Desktop/DiscordRichPresence.cs b/osu.Desktop/DiscordRichPresence.cs index 886038bcf0..8de1a08e7a 100644 --- a/osu.Desktop/DiscordRichPresence.cs +++ b/osu.Desktop/DiscordRichPresence.cs @@ -8,6 +8,7 @@ using DiscordRPC.Message; using Newtonsoft.Json; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Logging; using osu.Game; @@ -91,6 +92,8 @@ namespace osu.Desktop activity.BindValueChanged(_ => updateStatus()); privacyMode.BindValueChanged(_ => updateStatus()); + multiplayerClient.RoomUpdated += updateStatus; + client.Initialize(); } @@ -269,6 +272,9 @@ namespace osu.Desktop protected override void Dispose(bool isDisposing) { + if (multiplayerClient.IsNotNull()) + multiplayerClient.RoomUpdated -= updateStatus; + client.Dispose(); base.Dispose(isDisposing); } From e5e7c8f26841654a6819f3c88c40a347184c086b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 11 Mar 2024 14:58:28 +0100 Subject: [PATCH 0312/2493] Wrap beatmap listing filter names too While we're here fixing... Addresses https://github.com/ppy/osu/discussions/15452#discussioncomment-2734237. --- osu.Game/Overlays/BeatmapListing/BeatmapSearchFilterRow.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapSearchFilterRow.cs b/osu.Game/Overlays/BeatmapListing/BeatmapSearchFilterRow.cs index f79695a123..0b54a921f5 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapSearchFilterRow.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapSearchFilterRow.cs @@ -6,10 +6,10 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.UserInterface; using osu.Game.Graphics; -using osu.Game.Graphics.Sprites; using osuTK; using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Localisation; +using osu.Game.Graphics.Containers; namespace osu.Game.Overlays.BeatmapListing { @@ -45,9 +45,10 @@ namespace osu.Game.Overlays.BeatmapListing { new[] { - new OsuSpriteText + new OsuTextFlowContainer(t => t.Font = OsuFont.GetFont(size: 13)) { - Font = OsuFont.GetFont(size: 13), + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, Text = header }, filter = CreateFilter() From f30dfcb728b216659bf80ff513f7c0cdb97d56f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 11 Mar 2024 21:34:10 +0100 Subject: [PATCH 0313/2493] Fix ruleset medals not displaying due to deserialisation failure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤦🤦🤦🤦🤦🤦🤦🤦🤦🤦🤦🤦🤦🤦🤦🤦🤦🤦 Reported in https://discord.com/channels/188630481301012481/188630652340404224/1216812697589518386. --- .../Notifications/WebSocket/Events/UserAchievementUnlock.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Online/Notifications/WebSocket/Events/UserAchievementUnlock.cs b/osu.Game/Online/Notifications/WebSocket/Events/UserAchievementUnlock.cs index 6c7c8af4f4..3f803a4fe5 100644 --- a/osu.Game/Online/Notifications/WebSocket/Events/UserAchievementUnlock.cs +++ b/osu.Game/Online/Notifications/WebSocket/Events/UserAchievementUnlock.cs @@ -14,7 +14,7 @@ namespace osu.Game.Online.Notifications.WebSocket.Events public uint AchievementId { get; set; } [JsonProperty("achievement_mode")] - public ushort? AchievementMode { get; set; } + public string? AchievementMode { get; set; } [JsonProperty("cover_url")] public string CoverUrl { get; set; } = string.Empty; From 5580ce31fa6c37c7c65a703cfe820705928453bd Mon Sep 17 00:00:00 2001 From: jvyden Date: Mon, 11 Mar 2024 18:15:18 -0400 Subject: [PATCH 0314/2493] Log Discord RPC updates --- osu.Desktop/DiscordRichPresence.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Desktop/DiscordRichPresence.cs b/osu.Desktop/DiscordRichPresence.cs index 8de1a08e7a..6c8bd26d3d 100644 --- a/osu.Desktop/DiscordRichPresence.cs +++ b/osu.Desktop/DiscordRichPresence.cs @@ -114,6 +114,8 @@ namespace osu.Desktop return; } + Logger.Log("Updating Discord RPC", LoggingTarget.Network, LogLevel.Debug); + if (activity.Value != null) { bool hideIdentifiableInformation = privacyMode.Value == DiscordRichPresenceMode.Limited || status.Value == UserStatus.DoNotDisturb; From e4e7dd14f30ee5c2d28ccc967a0a891fbbd076b7 Mon Sep 17 00:00:00 2001 From: jvyden Date: Mon, 11 Mar 2024 18:16:13 -0400 Subject: [PATCH 0315/2493] Revert "Update presence on changes to multiplayer room" This reverts commit 8b730acb082379174f9a48b5fd132b184b4e9a81. --- osu.Desktop/DiscordRichPresence.cs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/osu.Desktop/DiscordRichPresence.cs b/osu.Desktop/DiscordRichPresence.cs index 6c8bd26d3d..ca26cab0fd 100644 --- a/osu.Desktop/DiscordRichPresence.cs +++ b/osu.Desktop/DiscordRichPresence.cs @@ -8,7 +8,6 @@ using DiscordRPC.Message; using Newtonsoft.Json; using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Logging; using osu.Game; @@ -92,8 +91,6 @@ namespace osu.Desktop activity.BindValueChanged(_ => updateStatus()); privacyMode.BindValueChanged(_ => updateStatus()); - multiplayerClient.RoomUpdated += updateStatus; - client.Initialize(); } @@ -274,9 +271,6 @@ namespace osu.Desktop protected override void Dispose(bool isDisposing) { - if (multiplayerClient.IsNotNull()) - multiplayerClient.RoomUpdated -= updateStatus; - client.Dispose(); base.Dispose(isDisposing); } From 83052fe1d98174a832e4ed7e9c52b04c5eb7c6c3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 12 Mar 2024 15:41:50 +0800 Subject: [PATCH 0316/2493] Downgrade realm to work around crashes on latest release See https://github.com/ppy/osu/issues/27577. --- 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 55bd68dea0..0b70515abf 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 961058f5c8ca538b1a96a82651b30e49b92c90c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 12 Mar 2024 09:05:28 +0100 Subject: [PATCH 0317/2493] Add failing test case --- .../Archives/skin-with-subfolder-zip-entries.osk | Bin 0 -> 894 bytes osu.Game.Tests/Skins/IO/ImportSkinTest.cs | 10 ++++++++++ 2 files changed, 10 insertions(+) create mode 100644 osu.Game.Tests/Resources/Archives/skin-with-subfolder-zip-entries.osk diff --git a/osu.Game.Tests/Resources/Archives/skin-with-subfolder-zip-entries.osk b/osu.Game.Tests/Resources/Archives/skin-with-subfolder-zip-entries.osk new file mode 100644 index 0000000000000000000000000000000000000000..013bca3801866f95f7139fbed30f96fb2a6b2639 GIT binary patch literal 894 zcmWIWW@Zs#0D)fToCq)jN+aJ8o04|V8AWw2I zfK1XY?5o`hL%~fhOr?=4GDkw_SJ0K%k|1ep1|jmnGM2G-Y{9ME;m|BpsAa zdiX}EdTqz%zyg(Z44p4_Fr3YD|D0*^dB@$=T(75H%;>qrZuxLUl1|$^qZKpCQXYC3 z#yQ^GeEcET*{AILmN4?)b)Ee8k;Bi_RSP&LwpzKFwp97OxN&0g#UpQ@>CDl1clVL& zm59~7Cw(RLCr>>h`BUbIZ}-+&SvgO1Zgt--I_0?Z*!?H>cjm9{y*o)W$#hTc2C>C+ z_D=kJe3g2DHzSih1FjfU0eTMvKmi5@6ekwJnX*M{T1U&$?CAfWZSSn=z9F%;pB5t+Rm`J%J$Gz#u;>J*p6w-*6=rWE-9W ZZGa^l;!I{`18QMl0m2o)Fg*YcR{%bq?(hHr literal 0 HcmV?d00001 diff --git a/osu.Game.Tests/Skins/IO/ImportSkinTest.cs b/osu.Game.Tests/Skins/IO/ImportSkinTest.cs index 606a5afac2..62e7a80435 100644 --- a/osu.Game.Tests/Skins/IO/ImportSkinTest.cs +++ b/osu.Game.Tests/Skins/IO/ImportSkinTest.cs @@ -173,6 +173,16 @@ namespace osu.Game.Tests.Skins.IO assertCorrectMetadata(import1, "name 1 [my custom skin 1]", "author 1", 1.0m, osu); }); + [Test] + public Task TestImportWithSubfolder() => runSkinTest(async osu => + { + const string filename = "Archives/skin-with-subfolder-zip-entries.osk"; + var import = await loadSkinIntoOsu(osu, new ImportTask(TestResources.OpenResource(filename), filename)); + + assertCorrectMetadata(import, $"Totally fully features skin [Real Skin with Real Features] [{filename[..^4]}]", "Unknown", 2.7m, osu); + Assert.That(import.PerformRead(r => r.Files.Count), Is.EqualTo(3)); + }); + #endregion #region Cases where imports should be uniquely imported From 83e47b3c4c1c3a59c6ded7249797daa436d46572 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 12 Mar 2024 09:06:24 +0100 Subject: [PATCH 0318/2493] Fix exports containing zero byte files after import from specific ZIP archives Closes https://github.com/ppy/osu/issues/27540. As it turns out, some ZIP archivers (like 7zip) will decide to add fake entries for directories, and some (like windows zipfolders) won't. The "directory" entries aren't really properly supported using any actual data or attributes, they're detected by sharpcompress basically by heuristics: https://github.com/adamhathcock/sharpcompress/blob/ab5535eba365ec8fae58f92d53763ddf2dbf45af/src/SharpCompress/Common/Zip/Headers/ZipFileEntry.cs#L19-L31 When importing into realm we have thus far presumed that these directory entries will not be a thing. Having them be a thing breaks multiple things, like: - When importing from ZIPs with separate directory entries, a separate `RealmFile` is created for a directory entry even though it doesn't represent a real file - As a result, when re-exporting a model with files imported from such an archive, a zero-byte file would be created because to the database it looks like it was originally a zero-byte file. If you want to have fun, google "zip empty directories". You'll see a whole gamut of languages, libraries, and developers stepping on this rake. Yet another episode of underspecced mistakes from decades ago that were somebody's "good idea" but continue to wreak havoc forevermore because now there are two competing conventions you can't just pick one. --- osu.Game/IO/Archives/ZipArchiveReader.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/IO/Archives/ZipArchiveReader.cs b/osu.Game/IO/Archives/ZipArchiveReader.cs index 7d7ce858dd..cc5c65d184 100644 --- a/osu.Game/IO/Archives/ZipArchiveReader.cs +++ b/osu.Game/IO/Archives/ZipArchiveReader.cs @@ -46,7 +46,7 @@ namespace osu.Game.IO.Archives archiveStream.Dispose(); } - public override IEnumerable Filenames => archive.Entries.Select(e => e.Key).ExcludeSystemFileNames(); + public override IEnumerable Filenames => archive.Entries.Where(e => !e.IsDirectory).Select(e => e.Key).ExcludeSystemFileNames(); private class MemoryOwnerMemoryStream : Stream { From 88ec0cdbc704e041bd6b77ff24d1a4206d888896 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 12 Mar 2024 17:35:00 +0800 Subject: [PATCH 0319/2493] Fix seek ending too early in sample playback test --- .../TestSceneGameplaySamplePlayback.cs | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySamplePlayback.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySamplePlayback.cs index 057197e819..ad3fe7cb7e 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySamplePlayback.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySamplePlayback.cs @@ -18,6 +18,8 @@ namespace osu.Game.Tests.Visual.Gameplay { protected override bool AllowBackwardsSeeks => true; + private bool seek; + [Test] public void TestAllSamplesStopDuringSeek() { @@ -42,7 +44,7 @@ namespace osu.Game.Tests.Visual.Gameplay if (!samples.Any(s => s.Playing)) return false; - Player.ChildrenOfType().First().Seek(40000); + seek = true; return true; }); @@ -55,10 +57,27 @@ namespace osu.Game.Tests.Visual.Gameplay AddAssert("sample playback still disabled", () => sampleDisabler.SamplePlaybackDisabled.Value); + AddStep("stop seeking", () => seek = false); + AddUntilStep("seek finished, sample playback enabled", () => !sampleDisabler.SamplePlaybackDisabled.Value); AddUntilStep("any sample is playing", () => Player.ChildrenOfType().Any(s => s.IsPlaying)); } + protected override void Update() + { + base.Update(); + + if (seek) + { + // Frame stable playback is too fast to catch up these days. + // + // We want to keep seeking while asserting various test conditions, so + // continue to seek until we unset the flag. + var gameplayClockContainer = Player.ChildrenOfType().First(); + gameplayClockContainer.Seek(gameplayClockContainer.CurrentTime > 30000 ? 0 : 60000); + } + } + private IEnumerable allSounds => Player.ChildrenOfType(); private IEnumerable allLoopingSounds => allSounds.Where(sound => sound.Looping); From a4a433b92add36153d8f9b06ffa757480ff3b5ee Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 12 Mar 2024 20:14:50 +0800 Subject: [PATCH 0320/2493] Remember login by default Kinda what is expected from a user's perspective --- osu.Game/Configuration/OsuConfigManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index a71460ded7..f4a4c553d8 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -75,7 +75,7 @@ namespace osu.Game.Configuration #pragma warning restore CS0618 // Type or member is obsolete SetDefault(OsuSetting.AutomaticallyDownloadMissingBeatmaps, false); - SetDefault(OsuSetting.SavePassword, false).ValueChanged += enabled => + SetDefault(OsuSetting.SavePassword, true).ValueChanged += enabled => { if (enabled.NewValue) SetValue(OsuSetting.SaveUsername, true); From e2e99fc5cc392b57276a1c79f3b68c2831c64ac0 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Tue, 12 Mar 2024 21:07:21 -0700 Subject: [PATCH 0321/2493] Rearrange rankings overlay tabs to match web --- osu.Game/Overlays/Rankings/RankingsOverlayHeader.cs | 2 +- osu.Game/Overlays/Rankings/RankingsScope.cs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Overlays/Rankings/RankingsOverlayHeader.cs b/osu.Game/Overlays/Rankings/RankingsOverlayHeader.cs index cf132ed4da..0eaa6ce827 100644 --- a/osu.Game/Overlays/Rankings/RankingsOverlayHeader.cs +++ b/osu.Game/Overlays/Rankings/RankingsOverlayHeader.cs @@ -52,9 +52,9 @@ namespace osu.Game.Overlays.Rankings switch (scope) { case RankingsScope.Performance: - case RankingsScope.Spotlights: case RankingsScope.Score: case RankingsScope.Country: + case RankingsScope.Spotlights: return true; default: diff --git a/osu.Game/Overlays/Rankings/RankingsScope.cs b/osu.Game/Overlays/Rankings/RankingsScope.cs index 356a861764..0740c17e8c 100644 --- a/osu.Game/Overlays/Rankings/RankingsScope.cs +++ b/osu.Game/Overlays/Rankings/RankingsScope.cs @@ -11,15 +11,15 @@ namespace osu.Game.Overlays.Rankings [LocalisableDescription(typeof(RankingsStrings), nameof(RankingsStrings.TypePerformance))] Performance, - [LocalisableDescription(typeof(RankingsStrings), nameof(RankingsStrings.TypeCharts))] - Spotlights, - [LocalisableDescription(typeof(RankingsStrings), nameof(RankingsStrings.TypeScore))] Score, [LocalisableDescription(typeof(RankingsStrings), nameof(RankingsStrings.TypeCountry))] Country, + [LocalisableDescription(typeof(RankingsStrings), nameof(RankingsStrings.TypeCharts))] + Spotlights, + [LocalisableDescription(typeof(RankingsStrings), nameof(RankingsStrings.TypeKudosu))] Kudosu, } From 20c760835ab81c4a90c46656f69d8b59b0903fc1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 13 Mar 2024 13:55:49 +0100 Subject: [PATCH 0322/2493] Fix audio in video check crashing on unexpected failures --- osu.Game/Rulesets/Edit/Checks/CheckAudioInVideo.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/osu.Game/Rulesets/Edit/Checks/CheckAudioInVideo.cs b/osu.Game/Rulesets/Edit/Checks/CheckAudioInVideo.cs index f712a7867d..38976dd4b5 100644 --- a/osu.Game/Rulesets/Edit/Checks/CheckAudioInVideo.cs +++ b/osu.Game/Rulesets/Edit/Checks/CheckAudioInVideo.cs @@ -1,8 +1,10 @@ // 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.IO; +using osu.Framework.Logging; using osu.Game.Beatmaps; using osu.Game.IO.FileAbstraction; using osu.Game.Rulesets.Edit.Checks.Components; @@ -75,6 +77,11 @@ namespace osu.Game.Rulesets.Edit.Checks { issue = new IssueTemplateFileError(this).Create(filename, "Unsupported format"); } + catch (Exception ex) + { + issue = new IssueTemplateFileError(this).Create(filename, "Internal failure - see logs for more info"); + Logger.Log($"Failed when running {nameof(CheckAudioInVideo)}: {ex}"); + } yield return issue; } From d32f19b546e63e844acaa3c4c3911225cb757ace Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 13 Mar 2024 15:18:20 +0100 Subject: [PATCH 0323/2493] Fix first word bold not applying correctly after first language change Closes https://github.com/ppy/osu/issues/27549. I'm not entirely sure why the old solution failed exactly, but also think it's unimportant because I think past me was an idiot and was playing stupid games with the juggling of indices between two callbacks with no ordering guarantees and expecting not to win stupid prizes. I'm not sure this requires any follow-up reconsiderations of that entire text flow API, but if opinions differ, I'll re-examine. --- osu.Game/Overlays/Mods/ModSelectColumn.cs | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModSelectColumn.cs b/osu.Game/Overlays/Mods/ModSelectColumn.cs index b2c5a054e1..61b29ef65b 100644 --- a/osu.Game/Overlays/Mods/ModSelectColumn.cs +++ b/osu.Game/Overlays/Mods/ModSelectColumn.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 osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -8,9 +9,11 @@ using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Localisation; +using System.Linq; using osu.Game.Graphics; using osu.Game.Graphics.Backgrounds; using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Sprites; using osuTK; using osuTK.Graphics; @@ -181,17 +184,15 @@ namespace osu.Game.Overlays.Mods { headerText.Clear(); - int wordIndex = 0; + ITextPart part = headerText.AddText(text); + part.DrawablePartsRecreated += applySemiBoldToFirstWord; + applySemiBoldToFirstWord(part.Drawables); - ITextPart part = headerText.AddText(text, t => + void applySemiBoldToFirstWord(IEnumerable d) { - if (wordIndex == 0) - t.Font = t.Font.With(weight: FontWeight.SemiBold); - wordIndex += 1; - }); - - // Reset the index so that if the parts are refreshed (e.g. through changes in localisation) the correct word is re-emboldened. - part.DrawablePartsRecreated += _ => wordIndex = 0; + if (d.FirstOrDefault() is OsuSpriteText firstWord) + firstWord.Font = firstWord.Font.With(weight: FontWeight.SemiBold); + } } [BackgroundDependencyLoader] From 789a9f4dfa071dacc9157914e93792231d568923 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 14 Mar 2024 11:01:57 +0900 Subject: [PATCH 0324/2493] Initial redesign following flyte's design --- osu.Game/Screens/Play/DelayedResumeOverlay.cs | 125 +++++++++++------- 1 file changed, 79 insertions(+), 46 deletions(-) diff --git a/osu.Game/Screens/Play/DelayedResumeOverlay.cs b/osu.Game/Screens/Play/DelayedResumeOverlay.cs index ba49810b2b..e9dd26a06b 100644 --- a/osu.Game/Screens/Play/DelayedResumeOverlay.cs +++ b/osu.Game/Screens/Play/DelayedResumeOverlay.cs @@ -3,10 +3,12 @@ using System; using osu.Framework.Allocation; +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.Framework.Localisation; using osu.Framework.Threading; using osu.Game.Graphics; @@ -21,7 +23,12 @@ namespace osu.Game.Screens.Play /// public partial class DelayedResumeOverlay : ResumeOverlay { - private const double countdown_time = 800; + private const float outer_size = 200; + private const float inner_size = 150; + private const float progress_stroke_width = 7; + private const float progress_size = inner_size + progress_stroke_width / 2f; + + private const double countdown_time = 3000; protected override LocalisableString Message => string.Empty; @@ -31,9 +38,15 @@ namespace osu.Game.Screens.Play private ScheduledDelegate? scheduledResume; private int countdownCount = 3; private double countdownStartTime; + private bool countdownComplete; - private Drawable content = null!; - private SpriteText countdown = null!; + private Drawable outerContent = null!; + private Container innerContent = null!; + + private Container countdownComponents = null!; + private Drawable countdownBackground = null!; + private SpriteText countdownText = null!; + private CircularProgress countdownProgress = null!; public DelayedResumeOverlay() { @@ -44,44 +57,48 @@ namespace osu.Game.Screens.Play [BackgroundDependencyLoader] private void load() { - Add(content = new CircularContainer + Add(outerContent = new Circle { Anchor = Anchor.Centre, Origin = Anchor.Centre, - AutoSizeAxes = Axes.Both, - Masking = true, - BorderColour = colours.Yellow, - BorderThickness = 2, - Children = new Drawable[] + Size = new Vector2(outer_size), + Colour = Color4.Black.Opacity(0.25f) + }); + + Add(innerContent = new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Children = new[] { - new Box - { - Size = new Vector2(250, 40), - Colour = Color4.Black, - Alpha = 0.8f - }, - new Container + countdownBackground = new Circle { Anchor = Anchor.Centre, Origin = Anchor.Centre, - AutoSizeAxes = Axes.Both, + Size = new Vector2(inner_size), + Colour = Color4.Black.Opacity(0.25f) + }, + countdownComponents = new Container + { + RelativeSizeAxes = Axes.Both, Children = new Drawable[] { - new FillFlowContainer + countdownProgress = new CircularProgress { Anchor = Anchor.Centre, Origin = Anchor.Centre, - AutoSizeAxes = Axes.Both, - Spacing = new Vector2(5), - Colour = colours.Yellow, - Child = countdown = new OsuSpriteText - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - UseFullGlyphHeight = false, - AlwaysPresent = true, - Font = OsuFont.Numeric.With(size: 20, fixedWidth: true) - }, + Size = new Vector2(progress_size), + InnerRadius = progress_stroke_width / progress_size, + RoundedCaps = true + }, + countdownText = new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + UseFullGlyphHeight = false, + AlwaysPresent = true, + Font = OsuFont.Torus.With(size: 70, weight: FontWeight.Light) } } } @@ -93,14 +110,26 @@ namespace osu.Game.Screens.Play { this.FadeIn(); - content.FadeInFromZero(150, Easing.OutQuint); - content.ScaleTo(new Vector2(1.5f, 1)).Then().ScaleTo(1, 150, Easing.OutElasticQuarter); + // The transition effects. + outerContent.FadeIn().ScaleTo(Vector2.Zero).Then().ScaleTo(Vector2.One, 200, Easing.OutQuint); + innerContent.FadeIn().ScaleTo(Vector2.Zero).Then().ScaleTo(Vector2.One, 400, Easing.OutElasticHalf); + countdownComponents.FadeOut().Then().Delay(50).FadeTo(1, 100); + // Reset states for various components. + countdownBackground.FadeIn(); + countdownText.FadeIn(); + countdownProgress.FadeIn().ScaleTo(1); + + countdownComplete = false; countdownCount = 3; countdownStartTime = Time.Current; scheduledResume?.Cancel(); - scheduledResume = Scheduler.AddDelayed(Resume, countdown_time); + scheduledResume = Scheduler.AddDelayed(() => + { + countdownComplete = true; + Resume(); + }, countdown_time); } protected override void Update() @@ -114,20 +143,14 @@ namespace osu.Game.Screens.Play double amountTimePassed = Math.Min(countdown_time, Time.Current - countdownStartTime) / countdown_time; int newCount = 3 - (int)Math.Floor(amountTimePassed * 3); - if (newCount > 0) - { - countdown.Alpha = 1; - countdown.Text = newCount.ToString(); - } - else - countdown.Alpha = 0; + countdownProgress.Current.Value = amountTimePassed; + countdownText.Text = Math.Max(1, newCount).ToString(); + countdownProgress.InnerRadius = progress_stroke_width / progress_size / countdownProgress.Scale.X; - if (newCount != countdownCount) + if (countdownCount != newCount && newCount > 0) { - if (newCount == 0) - content.ScaleTo(new Vector2(1.5f, 1), 150, Easing.OutQuint); - else - content.ScaleTo(new Vector2(1.05f, 1), 50, Easing.OutQuint).Then().ScaleTo(1, 50, Easing.Out); + countdownText.ScaleTo(0.25f).Then().ScaleTo(1, 200, Easing.OutQuint); + outerContent.Delay(25).Then().ScaleTo(1.05f, 100).Then().ScaleTo(1f, 200, Easing.Out); } countdownCount = newCount; @@ -135,9 +158,19 @@ namespace osu.Game.Screens.Play protected override void PopOut() { - this.Delay(150).FadeOut(); + this.Delay(300).FadeOut(); - content.FadeOut(150, Easing.OutQuint); + outerContent.FadeOut(); + countdownBackground.FadeOut(); + countdownText.FadeOut(); + + if (countdownComplete) + { + countdownProgress.ScaleTo(2f, 300, Easing.OutQuint); + countdownProgress.Delay(200).FadeOut(100, Easing.Out); + } + else + countdownProgress.FadeOut(); scheduledResume?.Cancel(); } From b431bb11764c4c4088eea49f4cd7f5e7611e4aa9 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 14 Mar 2024 12:24:12 +0900 Subject: [PATCH 0325/2493] Resolve post-merge issues --- osu.Game/Screens/Play/DelayedResumeOverlay.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/osu.Game/Screens/Play/DelayedResumeOverlay.cs b/osu.Game/Screens/Play/DelayedResumeOverlay.cs index e9dd26a06b..196ca24358 100644 --- a/osu.Game/Screens/Play/DelayedResumeOverlay.cs +++ b/osu.Game/Screens/Play/DelayedResumeOverlay.cs @@ -32,9 +32,6 @@ namespace osu.Game.Screens.Play protected override LocalisableString Message => string.Empty; - [Resolved] - private OsuColour colours { get; set; } = null!; - private ScheduledDelegate? scheduledResume; private int countdownCount = 3; private double countdownStartTime; @@ -143,7 +140,7 @@ namespace osu.Game.Screens.Play double amountTimePassed = Math.Min(countdown_time, Time.Current - countdownStartTime) / countdown_time; int newCount = 3 - (int)Math.Floor(amountTimePassed * 3); - countdownProgress.Current.Value = amountTimePassed; + countdownProgress.Progress = amountTimePassed; countdownText.Text = Math.Max(1, newCount).ToString(); countdownProgress.InnerRadius = progress_stroke_width / progress_size / countdownProgress.Scale.X; From b309aad895b001e042ec85f2c6103457fdd801a1 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Fri, 8 Mar 2024 13:13:08 -0800 Subject: [PATCH 0326/2493] Fix noticeable masking artifact of beatmap cards when already downloaded MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Had a similar fix before seeing https://github.com/ppy/osu/pull/20743#discussion_r994955470, but using that diff instead so co-authoring. Co-Authored-By: Bartłomiej Dach --- .../Drawables/Cards/CollapsibleButtonContainer.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/osu.Game/Beatmaps/Drawables/Cards/CollapsibleButtonContainer.cs b/osu.Game/Beatmaps/Drawables/Cards/CollapsibleButtonContainer.cs index fe2ee8c7cc..32df1755a7 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/CollapsibleButtonContainer.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/CollapsibleButtonContainer.cs @@ -77,7 +77,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards downloadTracker, background = new Container { - RelativeSizeAxes = Axes.Both, + RelativeSizeAxes = Axes.Y, Anchor = Anchor.CentreRight, Origin = Anchor.CentreRight, Child = new Box @@ -165,9 +165,11 @@ namespace osu.Game.Beatmaps.Drawables.Cards private void updateState() { - float targetWidth = Width - (ShowDetails.Value ? ButtonsExpandedWidth : ButtonsCollapsedWidth); + float buttonAreaWidth = ShowDetails.Value ? ButtonsExpandedWidth : ButtonsCollapsedWidth; + float mainAreaWidth = Width - buttonAreaWidth; - mainArea.ResizeWidthTo(targetWidth, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); + mainArea.ResizeWidthTo(mainAreaWidth, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); + background.ResizeWidthTo(buttonAreaWidth + BeatmapCard.CORNER_RADIUS, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); background.FadeColour(downloadTracker.State.Value == DownloadState.LocallyAvailable ? colours.Lime0 : colourProvider.Background3, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); buttons.FadeTo(ShowDetails.Value ? 1 : 0, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); From b4cee12db96644a8364ed1525aea51f526c1dcb6 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 14 Mar 2024 09:21:29 +0300 Subject: [PATCH 0327/2493] Use defined colours for counter background --- osu.Game/Screens/Play/DelayedResumeOverlay.cs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Play/DelayedResumeOverlay.cs b/osu.Game/Screens/Play/DelayedResumeOverlay.cs index 196ca24358..c6cfeca142 100644 --- a/osu.Game/Screens/Play/DelayedResumeOverlay.cs +++ b/osu.Game/Screens/Play/DelayedResumeOverlay.cs @@ -3,7 +3,6 @@ using System; using osu.Framework.Allocation; -using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; @@ -13,8 +12,8 @@ using osu.Framework.Localisation; using osu.Framework.Threading; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; +using osu.Game.Overlays; using osuTK; -using osuTK.Graphics; namespace osu.Game.Screens.Play { @@ -54,12 +53,15 @@ namespace osu.Game.Screens.Play [BackgroundDependencyLoader] private void load() { + // todo: this shouldn't define its own colour provider, but nothing in Player screen does, so let's do that for now. + var colourProvider = new OverlayColourProvider(OverlayColourScheme.Blue); + Add(outerContent = new Circle { Anchor = Anchor.Centre, Origin = Anchor.Centre, Size = new Vector2(outer_size), - Colour = Color4.Black.Opacity(0.25f) + Colour = colourProvider.Background6, }); Add(innerContent = new Container @@ -74,7 +76,7 @@ namespace osu.Game.Screens.Play Anchor = Anchor.Centre, Origin = Anchor.Centre, Size = new Vector2(inner_size), - Colour = Color4.Black.Opacity(0.25f) + Colour = colourProvider.Background4, }, countdownComponents = new Container { From f8a841e907e9b2deb3ca08c425a0e665a5c07775 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Thu, 14 Mar 2024 00:17:22 -0700 Subject: [PATCH 0328/2493] Add comment explaining why width is limited to the button area Same comment as https://github.com/ppy/osu/blob/a47ccb8edd2392258b6b7e176b222a9ecd511fc0/osu.Game/Screens/Select/BeatmapInfoWedgeV2.cs#L91-L92. --- osu.Game/Beatmaps/Drawables/Cards/CollapsibleButtonContainer.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Beatmaps/Drawables/Cards/CollapsibleButtonContainer.cs b/osu.Game/Beatmaps/Drawables/Cards/CollapsibleButtonContainer.cs index 32df1755a7..a29724032e 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/CollapsibleButtonContainer.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/CollapsibleButtonContainer.cs @@ -169,6 +169,8 @@ namespace osu.Game.Beatmaps.Drawables.Cards float mainAreaWidth = Width - buttonAreaWidth; mainArea.ResizeWidthTo(mainAreaWidth, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); + + // By limiting the width we avoid this box showing up as an outline around the drawables that are on top of it. background.ResizeWidthTo(buttonAreaWidth + BeatmapCard.CORNER_RADIUS, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); background.FadeColour(downloadTracker.State.Value == DownloadState.LocallyAvailable ? colours.Lime0 : colourProvider.Background3, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); From 42bd558d7c159857ca7356b8485d710802685fc6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 14 Mar 2024 22:41:29 +0800 Subject: [PATCH 0329/2493] Only update text when necessary (reducing unnecessary string allocadtions) --- osu.Game/Screens/Play/DelayedResumeOverlay.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Play/DelayedResumeOverlay.cs b/osu.Game/Screens/Play/DelayedResumeOverlay.cs index c6cfeca142..468e67901d 100644 --- a/osu.Game/Screens/Play/DelayedResumeOverlay.cs +++ b/osu.Game/Screens/Play/DelayedResumeOverlay.cs @@ -32,7 +32,7 @@ namespace osu.Game.Screens.Play protected override LocalisableString Message => string.Empty; private ScheduledDelegate? scheduledResume; - private int countdownCount = 3; + private int? countdownCount; private double countdownStartTime; private bool countdownComplete; @@ -120,7 +120,7 @@ namespace osu.Game.Screens.Play countdownProgress.FadeIn().ScaleTo(1); countdownComplete = false; - countdownCount = 3; + countdownCount = null; countdownStartTime = Time.Current; scheduledResume?.Cancel(); @@ -143,11 +143,11 @@ namespace osu.Game.Screens.Play int newCount = 3 - (int)Math.Floor(amountTimePassed * 3); countdownProgress.Progress = amountTimePassed; - countdownText.Text = Math.Max(1, newCount).ToString(); countdownProgress.InnerRadius = progress_stroke_width / progress_size / countdownProgress.Scale.X; if (countdownCount != newCount && newCount > 0) { + countdownText.Text = Math.Max(1, newCount).ToString(); countdownText.ScaleTo(0.25f).Then().ScaleTo(1, 200, Easing.OutQuint); outerContent.Delay(25).Then().ScaleTo(1.05f, 100).Then().ScaleTo(1f, 200, Easing.Out); } From d7769ec3e25cc985fea44e6fb0d02ab668e2dd33 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 14 Mar 2024 22:38:59 +0800 Subject: [PATCH 0330/2493] Adjust animation --- osu.Game/Screens/Play/DelayedResumeOverlay.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Play/DelayedResumeOverlay.cs b/osu.Game/Screens/Play/DelayedResumeOverlay.cs index 468e67901d..7c34050bbf 100644 --- a/osu.Game/Screens/Play/DelayedResumeOverlay.cs +++ b/osu.Game/Screens/Play/DelayedResumeOverlay.cs @@ -112,7 +112,7 @@ namespace osu.Game.Screens.Play // The transition effects. outerContent.FadeIn().ScaleTo(Vector2.Zero).Then().ScaleTo(Vector2.One, 200, Easing.OutQuint); innerContent.FadeIn().ScaleTo(Vector2.Zero).Then().ScaleTo(Vector2.One, 400, Easing.OutElasticHalf); - countdownComponents.FadeOut().Then().Delay(50).FadeTo(1, 100); + countdownComponents.FadeOut().Delay(50).FadeTo(1, 100); // Reset states for various components. countdownBackground.FadeIn(); @@ -166,7 +166,7 @@ namespace osu.Game.Screens.Play if (countdownComplete) { countdownProgress.ScaleTo(2f, 300, Easing.OutQuint); - countdownProgress.Delay(200).FadeOut(100, Easing.Out); + countdownProgress.FadeOut(100, Easing.Out); } else countdownProgress.FadeOut(); From 888245b44fa13e46a7fb23b4a801f3fd36d38fee Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 14 Mar 2024 22:44:26 +0800 Subject: [PATCH 0331/2493] Reorder methods --- osu.Game/Screens/Play/DelayedResumeOverlay.cs | 38 +++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/osu.Game/Screens/Play/DelayedResumeOverlay.cs b/osu.Game/Screens/Play/DelayedResumeOverlay.cs index 7c34050bbf..4b703ec3cf 100644 --- a/osu.Game/Screens/Play/DelayedResumeOverlay.cs +++ b/osu.Game/Screens/Play/DelayedResumeOverlay.cs @@ -131,6 +131,25 @@ namespace osu.Game.Screens.Play }, countdown_time); } + protected override void PopOut() + { + this.Delay(300).FadeOut(); + + outerContent.FadeOut(); + countdownBackground.FadeOut(); + countdownText.FadeOut(); + + if (countdownComplete) + { + countdownProgress.ScaleTo(2f, 300, Easing.OutQuint); + countdownProgress.FadeOut(100, Easing.Out); + } + else + countdownProgress.FadeOut(); + + scheduledResume?.Cancel(); + } + protected override void Update() { base.Update(); @@ -154,24 +173,5 @@ namespace osu.Game.Screens.Play countdownCount = newCount; } - - protected override void PopOut() - { - this.Delay(300).FadeOut(); - - outerContent.FadeOut(); - countdownBackground.FadeOut(); - countdownText.FadeOut(); - - if (countdownComplete) - { - countdownProgress.ScaleTo(2f, 300, Easing.OutQuint); - countdownProgress.FadeOut(100, Easing.Out); - } - else - countdownProgress.FadeOut(); - - scheduledResume?.Cancel(); - } } } From 2845303a74986d4310d7fb4a77b56a1bd980a8d3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 14 Mar 2024 22:44:57 +0800 Subject: [PATCH 0332/2493] Fix non-matching scale outwards animation --- osu.Game/Screens/Play/DelayedResumeOverlay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/DelayedResumeOverlay.cs b/osu.Game/Screens/Play/DelayedResumeOverlay.cs index 4b703ec3cf..c3c98510e3 100644 --- a/osu.Game/Screens/Play/DelayedResumeOverlay.cs +++ b/osu.Game/Screens/Play/DelayedResumeOverlay.cs @@ -142,7 +142,7 @@ namespace osu.Game.Screens.Play if (countdownComplete) { countdownProgress.ScaleTo(2f, 300, Easing.OutQuint); - countdownProgress.FadeOut(100, Easing.Out); + countdownProgress.FadeOut(300, Easing.OutQuint); } else countdownProgress.FadeOut(); From 23975d4dd162b503ccb0e79f0c0d570c5606f901 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 14 Mar 2024 22:49:53 +0800 Subject: [PATCH 0333/2493] Add flash and reduce overall time for countdown to 2 seconds --- osu.Game/Screens/Play/DelayedResumeOverlay.cs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Play/DelayedResumeOverlay.cs b/osu.Game/Screens/Play/DelayedResumeOverlay.cs index c3c98510e3..fd1ce5d829 100644 --- a/osu.Game/Screens/Play/DelayedResumeOverlay.cs +++ b/osu.Game/Screens/Play/DelayedResumeOverlay.cs @@ -22,12 +22,15 @@ namespace osu.Game.Screens.Play /// public partial class DelayedResumeOverlay : ResumeOverlay { + // todo: this shouldn't define its own colour provider, but nothing in Player screen does, so let's do that for now. + private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Blue); + private const float outer_size = 200; private const float inner_size = 150; private const float progress_stroke_width = 7; private const float progress_size = inner_size + progress_stroke_width / 2f; - private const double countdown_time = 3000; + private const double countdown_time = 2000; protected override LocalisableString Message => string.Empty; @@ -53,9 +56,6 @@ namespace osu.Game.Screens.Play [BackgroundDependencyLoader] private void load() { - // todo: this shouldn't define its own colour provider, but nothing in Player screen does, so let's do that for now. - var colourProvider = new OverlayColourProvider(OverlayColourScheme.Blue); - Add(outerContent = new Circle { Anchor = Anchor.Centre, @@ -169,6 +169,8 @@ namespace osu.Game.Screens.Play countdownText.Text = Math.Max(1, newCount).ToString(); countdownText.ScaleTo(0.25f).Then().ScaleTo(1, 200, Easing.OutQuint); outerContent.Delay(25).Then().ScaleTo(1.05f, 100).Then().ScaleTo(1f, 200, Easing.Out); + + countdownBackground.FlashColour(colourProvider.Background3, 400, Easing.Out); } countdownCount = newCount; From 1aa695add9db68d3aef453f476486209a3aeb5f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 15 Mar 2024 09:14:17 +0100 Subject: [PATCH 0334/2493] Implement alternate design of player loader disclaimers --- .../Screens/Play/PlayerLoaderDisclaimer.cs | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 osu.Game/Screens/Play/PlayerLoaderDisclaimer.cs diff --git a/osu.Game/Screens/Play/PlayerLoaderDisclaimer.cs b/osu.Game/Screens/Play/PlayerLoaderDisclaimer.cs new file mode 100644 index 0000000000..c8112055c8 --- /dev/null +++ b/osu.Game/Screens/Play/PlayerLoaderDisclaimer.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 osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Localisation; +using osu.Game.Graphics; +using osu.Game.Overlays; +using osuTK; + +namespace osu.Game.Screens.Play +{ + public partial class PlayerLoaderDisclaimer : CompositeDrawable + { + private readonly LocalisableString title; + private readonly LocalisableString content; + + public PlayerLoaderDisclaimer(LocalisableString title, LocalisableString content) + { + this.title = title; + this.content = content; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + Width = SettingsToolboxGroup.CONTAINER_WIDTH; + AutoSizeAxes = Axes.Y; + + InternalChildren = new Drawable[] + { + new Circle + { + Width = 7, + Height = 15, + Margin = new MarginPadding { Top = 2 }, + Colour = colours.Orange1, + }, + new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding { Left = 12 }, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0, 2), + Children = new[] + { + new TextFlowContainer(t => t.Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 17)) + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Text = title, + }, + new TextFlowContainer(t => t.Font = OsuFont.GetFont(size: 16)) + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Text = content, + } + } + } + }; + } + } +} From 42ae18976ff4287567f1b69c27d27215353573a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 15 Mar 2024 09:27:33 +0100 Subject: [PATCH 0335/2493] Replace existing epilepsy warning with inline display --- .../Visual/Gameplay/TestScenePlayerLoader.cs | 35 +----- osu.Game/Screens/Play/EpilepsyWarning.cs | 102 ------------------ osu.Game/Screens/Play/PlayerLoader.cs | 67 +++++------- 3 files changed, 26 insertions(+), 178 deletions(-) delete mode 100644 osu.Game/Screens/Play/EpilepsyWarning.cs diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs index c6827d4197..dac8545729 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs @@ -334,13 +334,7 @@ namespace osu.Game.Tests.Visual.Gameplay AddUntilStep("wait for current", () => loader.IsCurrentScreen()); - AddAssert($"epilepsy warning {(warning ? "present" : "absent")}", () => (getWarning() != null) == warning); - - if (warning) - { - AddUntilStep("sound volume decreased", () => Beatmap.Value.Track.AggregateVolume.Value == 0.25); - AddUntilStep("sound volume restored", () => Beatmap.Value.Track.AggregateVolume.Value == 1); - } + AddAssert($"epilepsy warning {(warning ? "present" : "absent")}", () => this.ChildrenOfType().Count(), () => Is.EqualTo(warning ? 1 : 0)); restoreVolumes(); } @@ -357,30 +351,7 @@ namespace osu.Game.Tests.Visual.Gameplay AddUntilStep("wait for current", () => loader.IsCurrentScreen()); - AddUntilStep("epilepsy warning absent", () => getWarning() == null); - - restoreVolumes(); - } - - [Test] - public void TestEpilepsyWarningEarlyExit() - { - saveVolumes(); - setFullVolume(); - - AddStep("enable storyboards", () => config.SetValue(OsuSetting.ShowStoryboard, true)); - AddStep("set epilepsy warning", () => epilepsyWarning = true); - AddStep("load dummy beatmap", () => resetPlayer(false)); - - AddUntilStep("wait for current", () => loader.IsCurrentScreen()); - - AddUntilStep("wait for epilepsy warning", () => getWarning().Alpha > 0); - AddUntilStep("warning is shown", () => getWarning().State.Value == Visibility.Visible); - - AddStep("exit early", () => loader.Exit()); - - AddUntilStep("warning is hidden", () => getWarning().State.Value == Visibility.Hidden); - AddUntilStep("sound volume restored", () => Beatmap.Value.Track.AggregateVolume.Value == 1); + AddUntilStep("epilepsy warning absent", () => this.ChildrenOfType().Single().Alpha, () => Is.Zero); restoreVolumes(); } @@ -479,8 +450,6 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("click notification", () => notification.TriggerClick()); } - private EpilepsyWarning getWarning() => loader.ChildrenOfType().SingleOrDefault(w => w.IsAlive); - private partial class TestPlayerLoader : PlayerLoader { public new VisualSettings VisualSettings => base.VisualSettings; diff --git a/osu.Game/Screens/Play/EpilepsyWarning.cs b/osu.Game/Screens/Play/EpilepsyWarning.cs deleted file mode 100644 index 6316bbdb4e..0000000000 --- a/osu.Game/Screens/Play/EpilepsyWarning.cs +++ /dev/null @@ -1,102 +0,0 @@ -// 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; -using osu.Framework.Graphics.Sprites; -using osu.Game.Graphics; -using osu.Game.Graphics.Containers; -using osu.Game.Screens.Backgrounds; -using osuTK; - -namespace osu.Game.Screens.Play -{ - public partial class EpilepsyWarning : VisibilityContainer - { - public const double FADE_DURATION = 250; - - public EpilepsyWarning() - { - RelativeSizeAxes = Axes.Both; - Alpha = 0f; - } - - private BackgroundScreenBeatmap dimmableBackground; - - public BackgroundScreenBeatmap DimmableBackground - { - get => dimmableBackground; - set - { - dimmableBackground = value; - - if (IsLoaded) - updateBackgroundFade(); - } - } - - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { - Children = new Drawable[] - { - new FillFlowContainer - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Direction = FillDirection.Vertical, - Children = new Drawable[] - { - new SpriteIcon - { - Colour = colours.Yellow, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Icon = FontAwesome.Solid.ExclamationTriangle, - Size = new Vector2(50), - }, - new OsuTextFlowContainer(s => s.Font = OsuFont.GetFont(size: 25)) - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - TextAnchor = Anchor.Centre, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - }.With(tfc => - { - tfc.AddText("This beatmap contains scenes with "); - tfc.AddText("rapidly flashing colours", s => - { - s.Font = s.Font.With(weight: FontWeight.Bold); - s.Colour = colours.Yellow; - }); - tfc.AddText("."); - - tfc.NewParagraph(); - tfc.AddText("Please take caution if you are affected by epilepsy."); - }), - } - } - }; - } - - protected override void PopIn() - { - updateBackgroundFade(); - - this.FadeIn(FADE_DURATION, Easing.OutQuint); - } - - private void updateBackgroundFade() - { - DimmableBackground?.FadeColour(OsuColour.Gray(0.5f), FADE_DURATION, Easing.OutQuint); - } - - protected override void PopOut() => this.FadeOut(FADE_DURATION); - } -} diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs index aa730a8e0f..75240ba2ba 100644 --- a/osu.Game/Screens/Play/PlayerLoader.cs +++ b/osu.Game/Screens/Play/PlayerLoader.cs @@ -71,6 +71,7 @@ namespace osu.Game.Screens.Play protected Task? DisposalTask { get; private set; } + private FillFlowContainer disclaimers = null!; private OsuScrollContainer settingsScroll = null!; private Bindable showStoryboards = null!; @@ -137,7 +138,7 @@ namespace osu.Game.Screens.Play private ScheduledDelegate? scheduledPushPlayer; - private EpilepsyWarning? epilepsyWarning; + private PlayerLoaderDisclaimer? epilepsyWarning; private bool quickRestart; @@ -188,6 +189,16 @@ namespace osu.Game.Screens.Play Origin = Anchor.Centre, }, }), + disclaimers = new FillFlowContainer + { + Anchor = Anchor.TopLeft, + Origin = Anchor.TopLeft, + RelativeSizeAxes = Axes.Y, + AutoSizeAxes = Axes.X, + Direction = FillDirection.Vertical, + Padding = new MarginPadding(padding), + Spacing = new Vector2(padding), + }, settingsScroll = new OsuScrollContainer { Anchor = Anchor.TopRight, @@ -216,11 +227,7 @@ namespace osu.Game.Screens.Play if (Beatmap.Value.BeatmapInfo.EpilepsyWarning) { - AddInternal(epilepsyWarning = new EpilepsyWarning - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - }); + disclaimers.Add(epilepsyWarning = new PlayerLoaderDisclaimer("This beatmap contains scenes with rapidly flashing colours", "Please take caution if you are affected by epilepsy.")); } } @@ -229,6 +236,9 @@ namespace osu.Game.Screens.Play base.LoadComplete(); inputManager = GetContainingInputManager(); + + showStoryboards.BindValueChanged(val => epilepsyWarning?.FadeTo(val.NewValue ? 1 : 0, 250, Easing.OutQuint), true); + epilepsyWarning?.FinishTransforms(true); } #region Screen handling @@ -237,15 +247,10 @@ namespace osu.Game.Screens.Play { base.OnEntering(e); - ApplyToBackground(b => - { - if (epilepsyWarning != null) - epilepsyWarning.DimmableBackground = b; - }); - Beatmap.Value.Track.AddAdjustment(AdjustableProperty.Volume, volumeAdjustment); - // Start off-screen. + // Start side content off-screen. + disclaimers.MoveToX(-disclaimers.DrawWidth); settingsScroll.MoveToX(settingsScroll.DrawWidth); content.ScaleTo(0.7f); @@ -301,9 +306,6 @@ namespace osu.Game.Screens.Play cancelLoad(); ContentOut(); - // If the load sequence was interrupted, the epilepsy warning may already be displayed (or in the process of being displayed). - epilepsyWarning?.Hide(); - // Ensure the screen doesn't expire until all the outwards fade operations have completed. this.Delay(CONTENT_OUT_DURATION).FadeOut(); @@ -433,6 +435,8 @@ namespace osu.Game.Screens.Play content.FadeInFromZero(500, Easing.OutQuint); content.ScaleTo(1, 650, Easing.OutQuint).Then().Schedule(prepareNewPlayer); + disclaimers.FadeInFromZero(500, Easing.Out) + .MoveToX(0, 500, Easing.OutQuint); settingsScroll.FadeInFromZero(500, Easing.Out) .MoveToX(0, 500, Easing.OutQuint); @@ -466,6 +470,8 @@ namespace osu.Game.Screens.Play lowPassFilter = null; }); + disclaimers.FadeOut(CONTENT_OUT_DURATION, Easing.Out) + .MoveToX(-disclaimers.DrawWidth, CONTENT_OUT_DURATION * 2, Easing.OutQuint); settingsScroll.FadeOut(CONTENT_OUT_DURATION, Easing.OutQuint) .MoveToX(settingsScroll.DrawWidth, CONTENT_OUT_DURATION * 2, Easing.OutQuint); @@ -503,33 +509,8 @@ namespace osu.Game.Screens.Play TransformSequence pushSequence = this.Delay(0); - // only show if the warning was created (i.e. the beatmap needs it) - // and this is not a restart of the map (the warning expires after first load). - // - // note the late check of storyboard enable as the user may have just changed it - // from the settings on the loader screen. - if (epilepsyWarning?.IsAlive == true && showStoryboards.Value) - { - const double epilepsy_display_length = 3000; - - pushSequence - .Delay(CONTENT_OUT_DURATION) - .Schedule(() => epilepsyWarning.State.Value = Visibility.Visible) - .TransformBindableTo(volumeAdjustment, 0.25, EpilepsyWarning.FADE_DURATION, Easing.OutQuint) - .Delay(epilepsy_display_length) - .Schedule(() => - { - epilepsyWarning.Hide(); - epilepsyWarning.Expire(); - }) - .Delay(EpilepsyWarning.FADE_DURATION); - } - else - { - // This goes hand-in-hand with the restoration of low pass filter in contentOut(). - this.TransformBindableTo(volumeAdjustment, 0, CONTENT_OUT_DURATION, Easing.OutCubic); - epilepsyWarning?.Expire(); - } + // This goes hand-in-hand with the restoration of low pass filter in contentOut(). + this.TransformBindableTo(volumeAdjustment, 0, CONTENT_OUT_DURATION, Easing.OutCubic); pushSequence.Schedule(() => { From f3a444b7aca2295e92d473e7fe84a23aefd239b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 15 Mar 2024 09:41:37 +0100 Subject: [PATCH 0336/2493] Add disclaimer for loved/qualified status --- .../Visual/Gameplay/TestScenePlayerLoader.cs | 43 ++++++++++++++++++- osu.Game/Screens/Play/PlayerLoader.cs | 12 ++++++ 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs index dac8545729..91e35b067b 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs @@ -16,6 +16,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Screens; using osu.Framework.Testing; using osu.Framework.Utils; +using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.Overlays; using osu.Game.Overlays.Notifications; @@ -37,6 +38,7 @@ namespace osu.Game.Tests.Visual.Gameplay private TestPlayer player; private bool epilepsyWarning; + private BeatmapOnlineStatus onlineStatus; [Resolved] private AudioManager audioManager { get; set; } @@ -118,8 +120,9 @@ namespace osu.Game.Tests.Visual.Gameplay // Add intro time to test quick retry skipping (TestQuickRetry). workingBeatmap.BeatmapInfo.AudioLeadIn = 60000; - // Turn on epilepsy warning to test warning display (TestEpilepsyWarning). + // Set up data for testing disclaimer display. workingBeatmap.BeatmapInfo.EpilepsyWarning = epilepsyWarning; + workingBeatmap.BeatmapInfo.Status = onlineStatus; Beatmap.Value = workingBeatmap; @@ -356,6 +359,44 @@ namespace osu.Game.Tests.Visual.Gameplay restoreVolumes(); } + [TestCase(BeatmapOnlineStatus.Loved, 1)] + [TestCase(BeatmapOnlineStatus.Qualified, 1)] + [TestCase(BeatmapOnlineStatus.Graveyard, 0)] + public void TestStatusWarning(BeatmapOnlineStatus status, int expectedDisclaimerCount) + { + saveVolumes(); + setFullVolume(); + + AddStep("enable storyboards", () => config.SetValue(OsuSetting.ShowStoryboard, true)); + AddStep("disable epilepsy warning", () => epilepsyWarning = false); + AddStep("set beatmap status", () => onlineStatus = status); + AddStep("load dummy beatmap", () => resetPlayer(false)); + + AddUntilStep("wait for current", () => loader.IsCurrentScreen()); + + AddAssert($"disclaimer count is {expectedDisclaimerCount}", () => this.ChildrenOfType().Count(), () => Is.EqualTo(expectedDisclaimerCount)); + + restoreVolumes(); + } + + [Test] + public void TestCombinedWarnings() + { + saveVolumes(); + setFullVolume(); + + AddStep("enable storyboards", () => config.SetValue(OsuSetting.ShowStoryboard, true)); + AddStep("disable epilepsy warning", () => epilepsyWarning = true); + AddStep("set beatmap status", () => onlineStatus = BeatmapOnlineStatus.Loved); + AddStep("load dummy beatmap", () => resetPlayer(false)); + + AddUntilStep("wait for current", () => loader.IsCurrentScreen()); + + AddAssert($"disclaimer count is 2", () => this.ChildrenOfType().Count(), () => Is.EqualTo(2)); + + restoreVolumes(); + } + [TestCase(true, 1.0, false)] // on battery, above cutoff --> no warning [TestCase(false, 0.1, false)] // not on battery, below cutoff --> no warning [TestCase(true, 0.25, true)] // on battery, at cutoff --> warning diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs index 75240ba2ba..2d75b69b4a 100644 --- a/osu.Game/Screens/Play/PlayerLoader.cs +++ b/osu.Game/Screens/Play/PlayerLoader.cs @@ -18,6 +18,7 @@ using osu.Framework.Threading; using osu.Framework.Utils; using osu.Game.Audio; using osu.Game.Audio.Effects; +using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Graphics.Containers; @@ -229,6 +230,17 @@ namespace osu.Game.Screens.Play { disclaimers.Add(epilepsyWarning = new PlayerLoaderDisclaimer("This beatmap contains scenes with rapidly flashing colours", "Please take caution if you are affected by epilepsy.")); } + + switch (Beatmap.Value.BeatmapInfo.Status) + { + case BeatmapOnlineStatus.Loved: + disclaimers.Add(new PlayerLoaderDisclaimer("This beatmap is loved", "No performance points will be awarded.\nLeaderboards may be reset by the beatmap creator.")); + break; + + case BeatmapOnlineStatus.Qualified: + disclaimers.Add(new PlayerLoaderDisclaimer("This beatmap is qualified", "No performance points will be awarded.\nLeaderboards will be reset when the beatmap is ranked.")); + break; + } } protected override void LoadComplete() From 4688a53cf435861af05f8253d029f30a4e5db342 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 15 Mar 2024 09:43:09 +0100 Subject: [PATCH 0337/2493] Stay on player loader a bit longer if disclaimers are present Just to make reading the text easier. --- osu.Game/Screens/Play/PlayerLoader.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs index 2d75b69b4a..29ad6d2a47 100644 --- a/osu.Game/Screens/Play/PlayerLoader.cs +++ b/osu.Game/Screens/Play/PlayerLoader.cs @@ -43,7 +43,7 @@ namespace osu.Game.Screens.Play protected const double CONTENT_OUT_DURATION = 300; - protected virtual double PlayerPushDelay => 1800; + protected virtual double PlayerPushDelay => 1800 + disclaimers.Count * 500; public override bool HideOverlaysOnEnter => hideOverlays; From 4e2098adb86c73f8391255331c08dc5349567e67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 15 Mar 2024 09:46:18 +0100 Subject: [PATCH 0338/2493] Fix test crosstalk --- .../Visual/Gameplay/TestScenePlayerLoader.cs | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs index 91e35b067b..0ec4284a00 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs @@ -37,8 +37,8 @@ namespace osu.Game.Tests.Visual.Gameplay private TestPlayerLoader loader; private TestPlayer player; - private bool epilepsyWarning; - private BeatmapOnlineStatus onlineStatus; + private bool? epilepsyWarning; + private BeatmapOnlineStatus? onlineStatus; [Resolved] private AudioManager audioManager { get; set; } @@ -83,7 +83,12 @@ namespace osu.Game.Tests.Visual.Gameplay } [SetUp] - public void Setup() => Schedule(() => player = null); + public void Setup() => Schedule(() => + { + player = null; + epilepsyWarning = null; + onlineStatus = null; + }); [SetUpSteps] public override void SetUpSteps() @@ -121,8 +126,8 @@ namespace osu.Game.Tests.Visual.Gameplay workingBeatmap.BeatmapInfo.AudioLeadIn = 60000; // Set up data for testing disclaimer display. - workingBeatmap.BeatmapInfo.EpilepsyWarning = epilepsyWarning; - workingBeatmap.BeatmapInfo.Status = onlineStatus; + workingBeatmap.BeatmapInfo.EpilepsyWarning = epilepsyWarning ?? false; + workingBeatmap.BeatmapInfo.Status = onlineStatus ?? BeatmapOnlineStatus.Ranked; Beatmap.Value = workingBeatmap; From 49a087f7fcf764fe4764e4dff0c178287f2dc5f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 15 Mar 2024 10:58:00 +0100 Subject: [PATCH 0339/2493] Add localisation support --- osu.Game/Localisation/PlayerLoaderStrings.cs | 48 ++++++++++++++++++++ osu.Game/Screens/Play/PlayerLoader.cs | 6 +-- 2 files changed, 51 insertions(+), 3 deletions(-) create mode 100644 osu.Game/Localisation/PlayerLoaderStrings.cs diff --git a/osu.Game/Localisation/PlayerLoaderStrings.cs b/osu.Game/Localisation/PlayerLoaderStrings.cs new file mode 100644 index 0000000000..eba98c7aa7 --- /dev/null +++ b/osu.Game/Localisation/PlayerLoaderStrings.cs @@ -0,0 +1,48 @@ +// 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.Localisation +{ + public static class PlayerLoaderStrings + { + private const string prefix = @"osu.Game.Resources.Localisation.PlayerLoader"; + + /// + /// "This beatmap contains scenes with rapidly flashing colours" + /// + public static LocalisableString EpilepsyWarningTitle => new TranslatableString(getKey(@"epilepsy_warning_title"), @"This beatmap contains scenes with rapidly flashing colours"); + + /// + /// "Please take caution if you are affected by epilepsy." + /// + public static LocalisableString EpilepsyWarningContent => new TranslatableString(getKey(@"epilepsy_warning_content"), @"Please take caution if you are affected by epilepsy."); + + /// + /// "This beatmap is loved" + /// + public static LocalisableString LovedBeatmapDisclaimerTitle => new TranslatableString(getKey(@"loved_beatmap_disclaimer_title"), @"This beatmap is loved"); + + /// + /// "No performance points will be awarded. + /// Leaderboards may be reset by the beatmap creator." + /// + public static LocalisableString LovedBeatmapDisclaimerContent => new TranslatableString(getKey(@"loved_beatmap_disclaimer_content"), @"No performance points will be awarded. +Leaderboards may be reset by the beatmap creator."); + + /// + /// "This beatmap is qualified" + /// + public static LocalisableString QualifiedBeatmapDisclaimerTitle => new TranslatableString(getKey(@"qualified_beatmap_disclaimer_title"), @"This beatmap is qualified"); + + /// + /// "No performance points will be awarded. + /// Leaderboards will be reset when the beatmap is ranked." + /// + public static LocalisableString QualifiedBeatmapDisclaimerContent => new TranslatableString(getKey(@"qualified_beatmap_disclaimer_content"), @"No performance points will be awarded. +Leaderboards will be reset when the beatmap is ranked."); + + private static string getKey(string key) => $@"{prefix}:{key}"; + } +} diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs index 29ad6d2a47..ad9814e8c9 100644 --- a/osu.Game/Screens/Play/PlayerLoader.cs +++ b/osu.Game/Screens/Play/PlayerLoader.cs @@ -228,17 +228,17 @@ namespace osu.Game.Screens.Play if (Beatmap.Value.BeatmapInfo.EpilepsyWarning) { - disclaimers.Add(epilepsyWarning = new PlayerLoaderDisclaimer("This beatmap contains scenes with rapidly flashing colours", "Please take caution if you are affected by epilepsy.")); + disclaimers.Add(epilepsyWarning = new PlayerLoaderDisclaimer(PlayerLoaderStrings.EpilepsyWarningTitle, PlayerLoaderStrings.EpilepsyWarningContent)); } switch (Beatmap.Value.BeatmapInfo.Status) { case BeatmapOnlineStatus.Loved: - disclaimers.Add(new PlayerLoaderDisclaimer("This beatmap is loved", "No performance points will be awarded.\nLeaderboards may be reset by the beatmap creator.")); + disclaimers.Add(new PlayerLoaderDisclaimer(PlayerLoaderStrings.LovedBeatmapDisclaimerTitle, PlayerLoaderStrings.LovedBeatmapDisclaimerContent)); break; case BeatmapOnlineStatus.Qualified: - disclaimers.Add(new PlayerLoaderDisclaimer("This beatmap is qualified", "No performance points will be awarded.\nLeaderboards will be reset when the beatmap is ranked.")); + disclaimers.Add(new PlayerLoaderDisclaimer(PlayerLoaderStrings.QualifiedBeatmapDisclaimerTitle, PlayerLoaderStrings.QualifiedBeatmapDisclaimerContent)); break; } } From 5513a727488fb1825a2ca60514ab478b92623b30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 15 Mar 2024 10:33:32 +0100 Subject: [PATCH 0340/2493] Improve hiding transition when multiple disclaimers are visible --- osu.Game/Screens/Play/PlayerLoader.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs index ad9814e8c9..0cc8ed0f4b 100644 --- a/osu.Game/Screens/Play/PlayerLoader.cs +++ b/osu.Game/Screens/Play/PlayerLoader.cs @@ -194,8 +194,10 @@ namespace osu.Game.Screens.Play { Anchor = Anchor.TopLeft, Origin = Anchor.TopLeft, - RelativeSizeAxes = Axes.Y, - AutoSizeAxes = Axes.X, + Width = SettingsToolboxGroup.CONTAINER_WIDTH + padding * 2, + AutoSizeAxes = Axes.Y, + AutoSizeDuration = 250, + AutoSizeEasing = Easing.OutQuint, Direction = FillDirection.Vertical, Padding = new MarginPadding(padding), Spacing = new Vector2(padding), From 87682008fd0de936edb734c69d49cb0e7e4c4b78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 15 Mar 2024 11:26:48 +0100 Subject: [PATCH 0341/2493] Fix code quality inspection --- osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs index 0ec4284a00..4b00a86950 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs @@ -397,7 +397,7 @@ namespace osu.Game.Tests.Visual.Gameplay AddUntilStep("wait for current", () => loader.IsCurrentScreen()); - AddAssert($"disclaimer count is 2", () => this.ChildrenOfType().Count(), () => Is.EqualTo(2)); + AddAssert("disclaimer count is 2", () => this.ChildrenOfType().Count(), () => Is.EqualTo(2)); restoreVolumes(); } From 51568ba06a0465b3e3c3de13f4d9ce885c6e82ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 15 Mar 2024 11:31:45 +0100 Subject: [PATCH 0342/2493] Add background to disclaimers --- .../Screens/Play/PlayerLoaderDisclaimer.cs | 52 ++++++++++++------- 1 file changed, 34 insertions(+), 18 deletions(-) diff --git a/osu.Game/Screens/Play/PlayerLoaderDisclaimer.cs b/osu.Game/Screens/Play/PlayerLoaderDisclaimer.cs index c8112055c8..b41a2d3112 100644 --- a/osu.Game/Screens/Play/PlayerLoaderDisclaimer.cs +++ b/osu.Game/Screens/Play/PlayerLoaderDisclaimer.cs @@ -24,40 +24,56 @@ namespace osu.Game.Screens.Play } [BackgroundDependencyLoader] - private void load(OsuColour colours) + private void load(OsuColour colours, OverlayColourProvider colourProvider) { - Width = SettingsToolboxGroup.CONTAINER_WIDTH; + RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; + Masking = true; + CornerRadius = 5; InternalChildren = new Drawable[] { - new Circle + new Box { - Width = 7, - Height = 15, - Margin = new MarginPadding { Top = 2 }, - Colour = colours.Orange1, + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background4, }, - new FillFlowContainer + new Container { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Padding = new MarginPadding { Left = 12 }, - Direction = FillDirection.Vertical, - Spacing = new Vector2(0, 2), - Children = new[] + Padding = new MarginPadding(10), + Children = new Drawable[] { - new TextFlowContainer(t => t.Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 17)) + new Circle { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Text = title, + Width = 7, + Height = 15, + Margin = new MarginPadding { Top = 2 }, + Colour = colours.Orange1, }, - new TextFlowContainer(t => t.Font = OsuFont.GetFont(size: 16)) + new FillFlowContainer { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Text = content, + Padding = new MarginPadding { Left = 12 }, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0, 2), + Children = new[] + { + new TextFlowContainer(t => t.Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 17)) + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Text = title, + }, + new TextFlowContainer(t => t.Font = OsuFont.GetFont(size: 16)) + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Text = content, + } + } } } } From e6883b841802c5a8689ca80b13ca277022901b71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 15 Mar 2024 11:38:51 +0100 Subject: [PATCH 0343/2493] Tweak visuals further --- osu.Game/Screens/Play/PlayerLoader.cs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs index 0cc8ed0f4b..196180f3dd 100644 --- a/osu.Game/Screens/Play/PlayerLoader.cs +++ b/osu.Game/Screens/Play/PlayerLoader.cs @@ -200,7 +200,7 @@ namespace osu.Game.Screens.Play AutoSizeEasing = Easing.OutQuint, Direction = FillDirection.Vertical, Padding = new MarginPadding(padding), - Spacing = new Vector2(padding), + Spacing = new Vector2(20), }, settingsScroll = new OsuScrollContainer { @@ -269,9 +269,12 @@ namespace osu.Game.Screens.Play content.ScaleTo(0.7f); - contentIn(); + const double metadata_delay = 750; - MetadataInfo.Delay(750).FadeIn(500, Easing.OutQuint); + contentIn(); + MetadataInfo.Delay(metadata_delay).FadeIn(500, Easing.OutQuint); + disclaimers.Delay(metadata_delay).FadeInFromZero(500, Easing.Out) + .MoveToX(0, 500, Easing.OutQuint); // after an initial delay, start the debounced load check. // this will continue to execute even after resuming back on restart. @@ -449,8 +452,6 @@ namespace osu.Game.Screens.Play content.FadeInFromZero(500, Easing.OutQuint); content.ScaleTo(1, 650, Easing.OutQuint).Then().Schedule(prepareNewPlayer); - disclaimers.FadeInFromZero(500, Easing.Out) - .MoveToX(0, 500, Easing.OutQuint); settingsScroll.FadeInFromZero(500, Easing.Out) .MoveToX(0, 500, Easing.OutQuint); From a78210c88f6d3d519c4aa884033b3a30e8e82657 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 15 Mar 2024 11:45:27 +0100 Subject: [PATCH 0344/2493] Handle hover so that users can hover disclaimer to block load & read it --- osu.Game/Screens/Play/PlayerLoaderDisclaimer.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/Screens/Play/PlayerLoaderDisclaimer.cs b/osu.Game/Screens/Play/PlayerLoaderDisclaimer.cs index b41a2d3112..9dd5a3832c 100644 --- a/osu.Game/Screens/Play/PlayerLoaderDisclaimer.cs +++ b/osu.Game/Screens/Play/PlayerLoaderDisclaimer.cs @@ -5,6 +5,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Framework.Input.Events; using osu.Framework.Localisation; using osu.Game.Graphics; using osu.Game.Overlays; @@ -79,5 +80,8 @@ namespace osu.Game.Screens.Play } }; } + + // handle hover so that users can hover the disclaimer to delay load if they want to read it. + protected override bool OnHover(HoverEvent e) => true; } } From 0f8d526453b1ada099a52fa7d69340b9cb04963f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 16 Mar 2024 10:09:49 +0800 Subject: [PATCH 0345/2493] Adjust timings and delay disclaimers the same as settings --- osu.Game/Screens/Play/PlayerLoader.cs | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs index 196180f3dd..75c97d594d 100644 --- a/osu.Game/Screens/Play/PlayerLoader.cs +++ b/osu.Game/Screens/Play/PlayerLoader.cs @@ -269,12 +269,10 @@ namespace osu.Game.Screens.Play content.ScaleTo(0.7f); - const double metadata_delay = 750; + const double metadata_delay = 500; - contentIn(); MetadataInfo.Delay(metadata_delay).FadeIn(500, Easing.OutQuint); - disclaimers.Delay(metadata_delay).FadeInFromZero(500, Easing.Out) - .MoveToX(0, 500, Easing.OutQuint); + contentIn(metadata_delay + 250); // after an initial delay, start the debounced load check. // this will continue to execute even after resuming back on restart. @@ -352,7 +350,7 @@ namespace osu.Game.Screens.Play { if (this.IsCurrentScreen()) content.StartTracking(logo, resuming ? 0 : 500, Easing.InOutExpo); - }, resuming ? 0 : 500); + }, resuming ? 0 : 250); } protected override void LogoExiting(OsuLogo logo) @@ -445,15 +443,21 @@ namespace osu.Game.Screens.Play this.MakeCurrent(); } - private void contentIn() + private void contentIn(double delayBeforeSideDisplays = 0) { MetadataInfo.Loading = true; content.FadeInFromZero(500, Easing.OutQuint); content.ScaleTo(1, 650, Easing.OutQuint).Then().Schedule(prepareNewPlayer); - settingsScroll.FadeInFromZero(500, Easing.Out) - .MoveToX(0, 500, Easing.OutQuint); + using (BeginDelayedSequence(delayBeforeSideDisplays)) + { + settingsScroll.FadeInFromZero(500, Easing.Out) + .MoveToX(0, 500, Easing.OutQuint); + + disclaimers.FadeInFromZero(500, Easing.Out) + .MoveToX(0, 500, Easing.OutQuint); + } AddRangeInternal(new[] { From a49c4ebea634cff84243192ef59e1fa273f5042f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 16 Mar 2024 10:23:21 +0800 Subject: [PATCH 0346/2493] Match settings panels' backgrounds visually and behaviourally --- .../Screens/Play/PlayerLoaderDisclaimer.cs | 26 ++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Play/PlayerLoaderDisclaimer.cs b/osu.Game/Screens/Play/PlayerLoaderDisclaimer.cs index 9dd5a3832c..3637a4b210 100644 --- a/osu.Game/Screens/Play/PlayerLoaderDisclaimer.cs +++ b/osu.Game/Screens/Play/PlayerLoaderDisclaimer.cs @@ -18,6 +18,8 @@ namespace osu.Game.Screens.Play private readonly LocalisableString title; private readonly LocalisableString content; + private Box background = null!; + public PlayerLoaderDisclaimer(LocalisableString title, LocalisableString content) { this.title = title; @@ -34,10 +36,11 @@ namespace osu.Game.Screens.Play InternalChildren = new Drawable[] { - new Box + background = new Box { RelativeSizeAxes = Axes.Both, Colour = colourProvider.Background4, + Alpha = 0.1f, }, new Container { @@ -81,7 +84,24 @@ namespace osu.Game.Screens.Play }; } - // handle hover so that users can hover the disclaimer to delay load if they want to read it. - protected override bool OnHover(HoverEvent e) => true; + protected override bool OnHover(HoverEvent e) + { + updateFadeState(); + + // handle hover so that users can hover the disclaimer to delay load if they want to read it. + return true; + } + + protected override void OnHoverLost(HoverLostEvent e) + { + updateFadeState(); + base.OnHoverLost(e); + } + + private void updateFadeState() + { + // Matches SettingsToolboxGroup + background.FadeTo(IsHovered ? 1 : 0.1f, (float)500, Easing.OutQuint); + } } } From bde3da2746c4f80e33dd9a632a8a9d95e7bef186 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 16 Mar 2024 11:11:42 +0800 Subject: [PATCH 0347/2493] Fix failing test --- .../Visual/Background/TestSceneUserDimBackgrounds.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs b/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs index 204dea39b2..7d517b3155 100644 --- a/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs +++ b/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs @@ -9,6 +9,7 @@ using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Bindables; using osu.Framework.Extensions; +using osu.Framework.Extensions.PolygonExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Events; @@ -90,12 +91,13 @@ namespace osu.Game.Tests.Visual.Background AddStep("Start player loader", () => songSelect.Push(playerLoader = new TestPlayerLoader(player = new LoadBlockingTestPlayer { BlockLoad = true }))); AddUntilStep("Wait for Player Loader to load", () => playerLoader?.IsLoaded ?? false); AddAssert("Background retained from song select", () => songSelect.IsBackgroundCurrent()); - AddStep("Trigger background preview", () => + + AddUntilStep("Screen is dimmed and blur applied", () => { - InputManager.MoveMouseTo(playerLoader.ScreenPos); InputManager.MoveMouseTo(playerLoader.VisualSettingsPos); + return songSelect.IsBackgroundDimmed() && songSelect.IsUserBlurApplied(); }); - AddUntilStep("Screen is dimmed and blur applied", () => songSelect.IsBackgroundDimmed() && songSelect.IsUserBlurApplied()); + AddStep("Stop background preview", () => InputManager.MoveMouseTo(playerLoader.ScreenPos)); AddUntilStep("Screen is undimmed and user blur removed", () => songSelect.IsBackgroundUndimmed() && songSelect.CheckBackgroundBlur(playerLoader.ExpectedBackgroundBlur)); } From a598ea5b97fd467395e901f8dbe08de4b620120b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 16 Mar 2024 12:14:32 +0800 Subject: [PATCH 0348/2493] Remove unused using statement --- osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs b/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs index 7d517b3155..c5540eae88 100644 --- a/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs +++ b/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs @@ -9,7 +9,6 @@ using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Bindables; using osu.Framework.Extensions; -using osu.Framework.Extensions.PolygonExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Events; From c0ae94dc60d6ac9df97784760cee4b5e6baa046b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 16 Mar 2024 12:15:12 +0800 Subject: [PATCH 0349/2493] Attempt to fix test better --- .../Visual/Background/TestSceneUserDimBackgrounds.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs b/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs index c5540eae88..aac7689b1b 100644 --- a/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs +++ b/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs @@ -89,7 +89,11 @@ namespace osu.Game.Tests.Visual.Background setupUserSettings(); AddStep("Start player loader", () => songSelect.Push(playerLoader = new TestPlayerLoader(player = new LoadBlockingTestPlayer { BlockLoad = true }))); AddUntilStep("Wait for Player Loader to load", () => playerLoader?.IsLoaded ?? false); - AddAssert("Background retained from song select", () => songSelect.IsBackgroundCurrent()); + AddAssert("Background retained from song select", () => + { + InputManager.MoveMouseTo(playerLoader); + return songSelect.IsBackgroundCurrent(); + }); AddUntilStep("Screen is dimmed and blur applied", () => { From 15c0b1a2ec786dbdac2263655229430764f2a1d3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 16 Mar 2024 13:18:42 +0800 Subject: [PATCH 0350/2493] Remove redundant cast --- osu.Game/Screens/Play/PlayerLoaderDisclaimer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/PlayerLoaderDisclaimer.cs b/osu.Game/Screens/Play/PlayerLoaderDisclaimer.cs index 3637a4b210..592af166ba 100644 --- a/osu.Game/Screens/Play/PlayerLoaderDisclaimer.cs +++ b/osu.Game/Screens/Play/PlayerLoaderDisclaimer.cs @@ -101,7 +101,7 @@ namespace osu.Game.Screens.Play private void updateFadeState() { // Matches SettingsToolboxGroup - background.FadeTo(IsHovered ? 1 : 0.1f, (float)500, Easing.OutQuint); + background.FadeTo(IsHovered ? 1 : 0.1f, 500, Easing.OutQuint); } } } From ea3a9314f9dd3bb19fcc0aff6c1b212cf96cc560 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Wed, 13 Mar 2024 23:08:00 +0300 Subject: [PATCH 0351/2493] Improve TimelineControlPointDisplay performance --- .../Timeline/TimelineControlPointDisplay.cs | 104 +++++++++++++----- .../Components/Timeline/TopPointPiece.cs | 6 +- 2 files changed, 78 insertions(+), 32 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointDisplay.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointDisplay.cs index cd97b293ba..60d113ef58 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointDisplay.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointDisplay.cs @@ -1,10 +1,9 @@ // 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.Specialized; -using System.Diagnostics; -using System.Linq; +using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Caching; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Screens.Edit.Components.Timelines.Summary.Parts; @@ -15,6 +14,16 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline /// public partial class TimelineControlPointDisplay : TimelinePart { + [Resolved] + private Timeline? timeline { get; set; } + + /// + /// The visible time/position range of the timeline. + /// + private (float min, float max) visibleRange = (float.MinValue, float.MaxValue); + + private readonly Cached groupCache = new Cached(); + private readonly IBindableList controlPointGroups = new BindableList(); protected override void LoadBeatmap(EditorBeatmap beatmap) @@ -23,34 +32,71 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline controlPointGroups.UnbindAll(); controlPointGroups.BindTo(beatmap.ControlPointInfo.Groups); - controlPointGroups.BindCollectionChanged((_, args) => + controlPointGroups.BindCollectionChanged((_, _) => { - switch (args.Action) - { - case NotifyCollectionChangedAction.Reset: - Clear(); - break; - - case NotifyCollectionChangedAction.Add: - Debug.Assert(args.NewItems != null); - - foreach (var group in args.NewItems.OfType()) - Add(new TimelineControlPointGroup(group)); - break; - - case NotifyCollectionChangedAction.Remove: - Debug.Assert(args.OldItems != null); - - foreach (var group in args.OldItems.OfType()) - { - var matching = Children.SingleOrDefault(gv => ReferenceEquals(gv.Group, group)); - - matching?.Expire(); - } - - break; - } + invalidateGroups(); }, true); } + + protected override void Update() + { + base.Update(); + + if (timeline == null || DrawWidth <= 0) return; + + (float, float) newRange = ( + (ToLocalSpace(timeline.ScreenSpaceDrawQuad.TopLeft).X - TopPointPiece.WIDTH) / DrawWidth * Content.RelativeChildSize.X, + (ToLocalSpace(timeline.ScreenSpaceDrawQuad.TopRight).X) / DrawWidth * Content.RelativeChildSize.X); + + if (visibleRange != newRange) + { + visibleRange = newRange; + invalidateGroups(); + } + + if (!groupCache.IsValid) + recreateDrawableGroups(); + } + + private void invalidateGroups() => groupCache.Invalidate(); + + private void recreateDrawableGroups() + { + // Remove groups outside the visible range + for (int i = Count - 1; i >= 0; i--) + { + var g = Children[i]; + + if (!shouldBeVisible(g.Group)) + g.Expire(); + } + + // Add remaining ones + foreach (var group in controlPointGroups) + { + if (!shouldBeVisible(group)) + continue; + + bool alreadyVisible = false; + + foreach (var g in this) + { + if (ReferenceEquals(g.Group, group)) + { + alreadyVisible = true; + break; + } + } + + if (alreadyVisible) + continue; + + Add(new TimelineControlPointGroup(group)); + } + + groupCache.Validate(); + } + + private bool shouldBeVisible(ControlPointGroup group) => group.Time >= visibleRange.min && group.Time <= visibleRange.max; } } diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TopPointPiece.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TopPointPiece.cs index 243cdc6ddd..a40a805361 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TopPointPiece.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TopPointPiece.cs @@ -19,12 +19,12 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline protected OsuSpriteText Label { get; private set; } = null!; - private const float width = 80; + public const float WIDTH = 80; public TopPointPiece(ControlPoint point) { Point = point; - Width = width; + Width = WIDTH; Height = 16; Margin = new MarginPadding { Vertical = 4 }; @@ -65,7 +65,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline new Container { RelativeSizeAxes = Axes.Y, - Width = width - triangle_portion, + Width = WIDTH - triangle_portion, Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, Colour = Point.GetRepresentingColour(colours), From e825db61eeebc0e1f94b2f62b6dba30c478dd58a Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Sat, 16 Mar 2024 12:26:56 +0300 Subject: [PATCH 0352/2493] Fix enumerator allocation --- .../Components/Timeline/TimelineControlPointDisplay.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointDisplay.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointDisplay.cs index 60d113ef58..950b717ffb 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointDisplay.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointDisplay.cs @@ -72,8 +72,10 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline } // Add remaining ones - foreach (var group in controlPointGroups) + for (int i = 0; i < controlPointGroups.Count; i++) { + var group = controlPointGroups[i]; + if (!shouldBeVisible(group)) continue; From 981ee54cdca16653753c85a71192f404cb80e85c Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Sat, 16 Mar 2024 15:05:52 +0300 Subject: [PATCH 0353/2493] Fix transforms overhead in TimelineTickDisplay --- .../Edit/Compose/Components/Timeline/TimelineTickDisplay.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs index 7e7bef8cf2..5348d03a38 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs @@ -165,7 +165,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline // save a few drawables beyond the currently used for edge cases. while (drawableIndex < Math.Min(usedDrawables + 16, Count)) - Children[drawableIndex++].Hide(); + Children[drawableIndex++].Alpha = 0; // expire any excess while (drawableIndex < Count) @@ -182,7 +182,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline point = Children[drawableIndex]; drawableIndex++; - point.Show(); + point.Alpha = 1; return point; } From 34a5e2d606070ba043afc501030933bd4f158412 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Sat, 16 Mar 2024 15:20:37 +0300 Subject: [PATCH 0354/2493] Don't update subtree masking in TimelineTickDisplay --- .../Edit/Compose/Components/Timeline/TimelineTickDisplay.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs index 5348d03a38..c3adb43032 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs @@ -7,6 +7,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Caching; using osu.Framework.Graphics; +using osu.Framework.Graphics.Primitives; using osu.Framework.Logging; using osu.Game.Beatmaps; using osu.Game.Graphics; @@ -18,6 +19,9 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline { public partial class TimelineTickDisplay : TimelinePart { + // With current implementation every tick in the sub-tree should be visible, no need to check whether they are masked away. + public override bool UpdateSubTreeMasking(Drawable source, RectangleF maskingBounds) => false; + [Resolved] private EditorBeatmap beatmap { get; set; } = null!; From 0a6960296ec1947594b06623cfd925433e09d380 Mon Sep 17 00:00:00 2001 From: Vlad Frangu Date: Sat, 16 Mar 2024 21:32:55 +0200 Subject: [PATCH 0355/2493] feat: Support filtering for multiple statuses --- .../Filtering/FilterQueryParserTest.cs | 22 ++++++++++---- osu.Game/Screens/Select/FilterCriteria.cs | 17 ++++++++++- osu.Game/Screens/Select/FilterQueryParser.cs | 29 ++++++++++++++++++- 3 files changed, 60 insertions(+), 8 deletions(-) diff --git a/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs b/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs index bf888348ee..c109e5bad2 100644 --- a/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs +++ b/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs @@ -256,8 +256,9 @@ namespace osu.Game.Tests.NonVisual.Filtering const string query = "status=r"; var filterCriteria = new FilterCriteria(); FilterQueryParser.ApplyQueries(filterCriteria, query); - Assert.AreEqual(BeatmapOnlineStatus.Ranked, filterCriteria.OnlineStatus.Min); - Assert.AreEqual(BeatmapOnlineStatus.Ranked, filterCriteria.OnlineStatus.Max); + Assert.IsNotNull(filterCriteria.OnlineStatus.Values); + Assert.IsNotEmpty(filterCriteria.OnlineStatus.Values!); + Assert.Contains(BeatmapOnlineStatus.Ranked, filterCriteria.OnlineStatus.Values); } [Test] @@ -268,10 +269,19 @@ namespace osu.Game.Tests.NonVisual.Filtering FilterQueryParser.ApplyQueries(filterCriteria, query); Assert.AreEqual("I want the pp", filterCriteria.SearchText.Trim()); Assert.AreEqual(4, filterCriteria.SearchTerms.Length); - Assert.AreEqual(BeatmapOnlineStatus.Ranked, filterCriteria.OnlineStatus.Min); - Assert.IsTrue(filterCriteria.OnlineStatus.IsLowerInclusive); - Assert.AreEqual(BeatmapOnlineStatus.Ranked, filterCriteria.OnlineStatus.Max); - Assert.IsTrue(filterCriteria.OnlineStatus.IsUpperInclusive); + Assert.IsNotNull(filterCriteria.OnlineStatus.Values); + Assert.Contains(BeatmapOnlineStatus.Ranked, filterCriteria.OnlineStatus.Values); + } + + [Test] + public void TestApplyStatusMatches() + { + const string query = "status=ranked status=loved"; + var filterCriteria = new FilterCriteria(); + FilterQueryParser.ApplyQueries(filterCriteria, query); + Assert.IsNotNull(filterCriteria.OnlineStatus.Values); + Assert.Contains(BeatmapOnlineStatus.Ranked, filterCriteria.OnlineStatus.Values); + Assert.Contains(BeatmapOnlineStatus.Loved, filterCriteria.OnlineStatus.Values); } [TestCase("creator")] diff --git a/osu.Game/Screens/Select/FilterCriteria.cs b/osu.Game/Screens/Select/FilterCriteria.cs index 46083f7c88..dd6b602ade 100644 --- a/osu.Game/Screens/Select/FilterCriteria.cs +++ b/osu.Game/Screens/Select/FilterCriteria.cs @@ -34,7 +34,7 @@ namespace osu.Game.Screens.Select public OptionalRange Length; public OptionalRange BPM; public OptionalRange BeatDivisor; - public OptionalRange OnlineStatus; + public OptionalArray OnlineStatus; public OptionalRange LastPlayed; public OptionalTextFilter Creator; public OptionalTextFilter Artist; @@ -114,6 +114,21 @@ namespace osu.Game.Screens.Select public IRulesetFilterCriteria? RulesetCriteria { get; set; } + public struct OptionalArray : IEquatable> + where T : struct + { + public bool HasFilter => Values?.Length > 0; + + public bool IsInRange(T value) + { + return Values?.Contains(value) ?? false; + } + + public T[]? Values; + + public bool Equals(OptionalArray other) => Values?.SequenceEqual(other.Values ?? Array.Empty()) ?? false; + } + public struct OptionalRange : IEquatable> where T : struct { diff --git a/osu.Game/Screens/Select/FilterQueryParser.cs b/osu.Game/Screens/Select/FilterQueryParser.cs index 2c4077dacf..208e7f559e 100644 --- a/osu.Game/Screens/Select/FilterQueryParser.cs +++ b/osu.Game/Screens/Select/FilterQueryParser.cs @@ -69,7 +69,7 @@ namespace osu.Game.Screens.Select return TryUpdateCriteriaRange(ref criteria.BeatDivisor, op, value, tryParseInt); case "status": - return TryUpdateCriteriaRange(ref criteria.OnlineStatus, op, value, tryParseEnum); + return TryUpdateArrayRange(ref criteria.OnlineStatus, op, value, tryParseEnum); case "creator": case "author": @@ -300,6 +300,33 @@ namespace osu.Game.Screens.Select where T : struct => parseFunction.Invoke(val, out var converted) && tryUpdateCriteriaRange(ref range, op, converted); + /// + /// Attempts to parse a keyword filter of type , + /// from the specified and . + /// If can be parsed into using , the function returns true + /// and the resulting range constraint is stored into the 's expected values. + /// + /// The to store the parsed data into, if successful. + /// The operator for the keyword filter. Currently, only can be used. + /// The value of the keyword filter. + /// Function used to determine if can be converted to type . + public static bool TryUpdateArrayRange(ref FilterCriteria.OptionalArray range, Operator op, string val, TryParseFunction parseFunction) + where T : struct + => parseFunction.Invoke(val, out var converted) && tryUpdateArrayRange(ref range, op, converted); + + private static bool tryUpdateArrayRange(ref FilterCriteria.OptionalArray range, Operator op, T value) + where T : struct + { + if (op != Operator.Equal) + return false; + + range.Values ??= Array.Empty(); + + range.Values = range.Values.Append(value).ToArray(); + + return true; + } + private static bool tryUpdateCriteriaRange(ref FilterCriteria.OptionalRange range, Operator op, T value) where T : struct { From e1c1609271bee3b21c38383596f20f31c2a3573e Mon Sep 17 00:00:00 2001 From: Vlad Frangu Date: Sat, 16 Mar 2024 21:48:44 +0200 Subject: [PATCH 0356/2493] chore: correct equal logic --- osu.Game/Screens/Select/FilterCriteria.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Select/FilterCriteria.cs b/osu.Game/Screens/Select/FilterCriteria.cs index dd6b602ade..6cec7a387d 100644 --- a/osu.Game/Screens/Select/FilterCriteria.cs +++ b/osu.Game/Screens/Select/FilterCriteria.cs @@ -126,7 +126,13 @@ namespace osu.Game.Screens.Select public T[]? Values; - public bool Equals(OptionalArray other) => Values?.SequenceEqual(other.Values ?? Array.Empty()) ?? false; + public bool Equals(OptionalArray other) + { + if (Values is null && other.Values is null) + return true; + + return Values?.SequenceEqual(other.Values ?? Array.Empty()) ?? false; + } } public struct OptionalRange : IEquatable> From 63816adbc081065fe68b9bc20b49b9329c4abbd2 Mon Sep 17 00:00:00 2001 From: Arthur Araujo Date: Sat, 16 Mar 2024 21:20:12 -0300 Subject: [PATCH 0357/2493] Add verify checks to unused audio at the end --- .../Checks/CheckUnusedAudioAtEndTest.cs | 90 +++++++++++++++++++ .../CheckUnusedAudioAtEndStrings.cs | 19 ++++ osu.Game/Rulesets/Edit/BeatmapVerifier.cs | 2 + .../Edit/Checks/CheckUnusedAudioAtEnd.cs | 50 +++++++++++ 4 files changed, 161 insertions(+) create mode 100644 osu.Game.Tests/Editing/Checks/CheckUnusedAudioAtEndTest.cs create mode 100644 osu.Game/Localisation/CheckUnusedAudioAtEndStrings.cs create mode 100644 osu.Game/Rulesets/Edit/Checks/CheckUnusedAudioAtEnd.cs diff --git a/osu.Game.Tests/Editing/Checks/CheckUnusedAudioAtEndTest.cs b/osu.Game.Tests/Editing/Checks/CheckUnusedAudioAtEndTest.cs new file mode 100644 index 0000000000..687feae63d --- /dev/null +++ b/osu.Game.Tests/Editing/Checks/CheckUnusedAudioAtEndTest.cs @@ -0,0 +1,90 @@ +// 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 Moq; +using NUnit.Framework; +using osu.Framework.Timing; +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 static osu.Game.Tests.Visual.OsuTestScene.ClockBackedTestWorkingBeatmap; + +namespace osu.Game.Tests.Editing.Checks +{ + public class CheckUnusedAudioTest + { + private CheckUnusedAudioAtEnd check = null!; + + private IBeatmap beatmapNotFullyMapped = null!; + + private IBeatmap beatmapFullyMapped = null!; + + [SetUp] + public void Setup() + { + check = new CheckUnusedAudioAtEnd(); + beatmapNotFullyMapped = new Beatmap + { + HitObjects = + { + new HitCircle { StartTime = 0 }, + new HitCircle { StartTime = 1_298 }, + }, + BeatmapInfo = new BeatmapInfo + { + Metadata = new BeatmapMetadata { AudioFile = "abc123.jpg" } + } + }; + beatmapFullyMapped = new Beatmap + { + HitObjects = + { + new HitCircle { StartTime = 0 }, + new HitCircle { StartTime = 9000 }, + }, + BeatmapInfo = new BeatmapInfo + { + Metadata = new BeatmapMetadata { AudioFile = "abc123.jpg" } + } + }; + } + + [Test] + public void TestAudioNotFullyUsed() + { + var context = getContext(beatmapNotFullyMapped); + var issues = check.Run(context).ToList(); + + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues.Single().Template is CheckUnusedAudioAtEnd.IssueTemplateUnusedAudioAtEnd); + } + + [Test] + public void TestAudioFullyUsed() + { + var context = getContext(beatmapFullyMapped); + var issues = check.Run(context).ToList(); + + Assert.That(issues, Has.Count.EqualTo(0)); + } + + private BeatmapVerifierContext getContext(IBeatmap beatmap) + { + return new BeatmapVerifierContext(beatmap, getMockWorkingBeatmap(beatmap).Object); + } + + private Mock getMockWorkingBeatmap(IBeatmap beatmap) + { + var mockTrack = new TrackVirtualStore(new FramedClock()).GetVirtual(10000, "virtual"); + + var mockWorkingBeatmap = new Mock(); + mockWorkingBeatmap.SetupGet(w => w.Beatmap).Returns(beatmap); + mockWorkingBeatmap.SetupGet(w => w.Track).Returns(mockTrack); + + return mockWorkingBeatmap; + } + } +} diff --git a/osu.Game/Localisation/CheckUnusedAudioAtEndStrings.cs b/osu.Game/Localisation/CheckUnusedAudioAtEndStrings.cs new file mode 100644 index 0000000000..46f92237a9 --- /dev/null +++ b/osu.Game/Localisation/CheckUnusedAudioAtEndStrings.cs @@ -0,0 +1,19 @@ +// 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.Localisation +{ + public static class CheckUnusedAudioAtEndStrings + { + private const string prefix = @"osu.Game.Resources.Localisation.CheckUnusedAudioAtEnd"; + + /// + /// "{0}% of the audio is not mapped." + /// + public static LocalisableString OfTheAudioIsNot(double percentageLeft) => new TranslatableString(getKey(@"of_the_audio_is_not"), @"{0}% of the audio is not mapped.", percentageLeft); + + private static string getKey(string key) => $@"{prefix}:{key}"; + } +} diff --git a/osu.Game/Rulesets/Edit/BeatmapVerifier.cs b/osu.Game/Rulesets/Edit/BeatmapVerifier.cs index dcf5eb4da9..4bba72d828 100644 --- a/osu.Game/Rulesets/Edit/BeatmapVerifier.cs +++ b/osu.Game/Rulesets/Edit/BeatmapVerifier.cs @@ -36,12 +36,14 @@ namespace osu.Game.Rulesets.Edit new CheckConcurrentObjects(), new CheckZeroLengthObjects(), new CheckDrainLength(), + new CheckUnusedAudioAtEnd(), // Timing new CheckPreviewTime(), // Events new CheckBreaks() + }; public IEnumerable Run(BeatmapVerifierContext context) diff --git a/osu.Game/Rulesets/Edit/Checks/CheckUnusedAudioAtEnd.cs b/osu.Game/Rulesets/Edit/Checks/CheckUnusedAudioAtEnd.cs new file mode 100644 index 0000000000..c120e0993a --- /dev/null +++ b/osu.Game/Rulesets/Edit/Checks/CheckUnusedAudioAtEnd.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; +using System.Collections.Generic; +using System.Linq; +using osu.Game.Rulesets.Edit.Checks.Components; +using osu.Game.Rulesets.Objects; + +namespace osu.Game.Rulesets.Edit.Checks +{ + public class CheckUnusedAudioAtEnd : ICheck + { + public CheckMetadata Metadata => new CheckMetadata(CheckCategory.Compose, "More than 20% unused audio at the end"); + + public IEnumerable PossibleTemplates => new IssueTemplate[] + { + new IssueTemplateUnusedAudioAtEnd(this), + }; + + public IEnumerable Run(BeatmapVerifierContext context) + { + double mappedLength = context.Beatmap.HitObjects.Last().GetEndTime(); + double trackLength = context.WorkingBeatmap.Track.Length; + + double mappedPercentage = calculatePercentage(mappedLength, trackLength); + + if (mappedPercentage < 80) + { + yield return new IssueTemplateUnusedAudioAtEnd(this).Create(); + } + + } + + private double calculatePercentage(double mappedLenght, double trackLenght) + { + return Math.Round(mappedLenght / trackLenght * 100); + } + + public class IssueTemplateUnusedAudioAtEnd : IssueTemplate + { + public IssueTemplateUnusedAudioAtEnd(ICheck check) + : base(check, IssueType.Problem, "There is more than 20% unused audio at the end.") + { + } + + public Issue Create() => new Issue(this); + } + } +} From f7aff76592c108ff3df1b97b3d2b8e8d7ded6938 Mon Sep 17 00:00:00 2001 From: Arthur Araujo Date: Sat, 16 Mar 2024 23:03:06 -0300 Subject: [PATCH 0358/2493] Fix codefactor issues --- osu.Game/Rulesets/Edit/BeatmapVerifier.cs | 1 - osu.Game/Rulesets/Edit/Checks/CheckUnusedAudioAtEnd.cs | 1 - 2 files changed, 2 deletions(-) diff --git a/osu.Game/Rulesets/Edit/BeatmapVerifier.cs b/osu.Game/Rulesets/Edit/BeatmapVerifier.cs index 4bba72d828..4a316afd22 100644 --- a/osu.Game/Rulesets/Edit/BeatmapVerifier.cs +++ b/osu.Game/Rulesets/Edit/BeatmapVerifier.cs @@ -43,7 +43,6 @@ namespace osu.Game.Rulesets.Edit // Events new CheckBreaks() - }; public IEnumerable Run(BeatmapVerifierContext context) diff --git a/osu.Game/Rulesets/Edit/Checks/CheckUnusedAudioAtEnd.cs b/osu.Game/Rulesets/Edit/Checks/CheckUnusedAudioAtEnd.cs index c120e0993a..9c1f2748e9 100644 --- a/osu.Game/Rulesets/Edit/Checks/CheckUnusedAudioAtEnd.cs +++ b/osu.Game/Rulesets/Edit/Checks/CheckUnusedAudioAtEnd.cs @@ -29,7 +29,6 @@ namespace osu.Game.Rulesets.Edit.Checks { yield return new IssueTemplateUnusedAudioAtEnd(this).Create(); } - } private double calculatePercentage(double mappedLenght, double trackLenght) From 80f24a07916e9ae03fa4631e7f9018812139214d Mon Sep 17 00:00:00 2001 From: Arthur Araujo Date: Sat, 16 Mar 2024 23:30:59 -0300 Subject: [PATCH 0359/2493] Fix test class name not matching file name --- osu.Game.Tests/Editing/Checks/CheckUnusedAudioAtEndTest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Editing/Checks/CheckUnusedAudioAtEndTest.cs b/osu.Game.Tests/Editing/Checks/CheckUnusedAudioAtEndTest.cs index 687feae63d..29c5cb96fd 100644 --- a/osu.Game.Tests/Editing/Checks/CheckUnusedAudioAtEndTest.cs +++ b/osu.Game.Tests/Editing/Checks/CheckUnusedAudioAtEndTest.cs @@ -14,7 +14,7 @@ using static osu.Game.Tests.Visual.OsuTestScene.ClockBackedTestWorkingBeatmap; namespace osu.Game.Tests.Editing.Checks { - public class CheckUnusedAudioTest + public class CheckUnusedAudioAtEndTest { private CheckUnusedAudioAtEnd check = null!; From d0678bfbee7952e2b82c6d25bd2abc5952937421 Mon Sep 17 00:00:00 2001 From: Vlad Frangu Date: Mon, 18 Mar 2024 15:30:43 +0200 Subject: [PATCH 0360/2493] chore: requested changes --- .../Filtering/FilterQueryParserTest.cs | 20 ++++++-- osu.Game/Screens/Select/FilterCriteria.cs | 19 +++---- osu.Game/Screens/Select/FilterQueryParser.cs | 50 +++++++++++++++---- 3 files changed, 67 insertions(+), 22 deletions(-) diff --git a/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs b/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs index c109e5bad2..98d3286409 100644 --- a/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs +++ b/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs @@ -256,8 +256,7 @@ namespace osu.Game.Tests.NonVisual.Filtering const string query = "status=r"; var filterCriteria = new FilterCriteria(); FilterQueryParser.ApplyQueries(filterCriteria, query); - Assert.IsNotNull(filterCriteria.OnlineStatus.Values); - Assert.IsNotEmpty(filterCriteria.OnlineStatus.Values!); + Assert.IsNotEmpty(filterCriteria.OnlineStatus.Values); Assert.Contains(BeatmapOnlineStatus.Ranked, filterCriteria.OnlineStatus.Values); } @@ -269,7 +268,7 @@ namespace osu.Game.Tests.NonVisual.Filtering FilterQueryParser.ApplyQueries(filterCriteria, query); Assert.AreEqual("I want the pp", filterCriteria.SearchText.Trim()); Assert.AreEqual(4, filterCriteria.SearchTerms.Length); - Assert.IsNotNull(filterCriteria.OnlineStatus.Values); + Assert.IsNotEmpty(filterCriteria.OnlineStatus.Values); Assert.Contains(BeatmapOnlineStatus.Ranked, filterCriteria.OnlineStatus.Values); } @@ -279,11 +278,24 @@ namespace osu.Game.Tests.NonVisual.Filtering const string query = "status=ranked status=loved"; var filterCriteria = new FilterCriteria(); FilterQueryParser.ApplyQueries(filterCriteria, query); - Assert.IsNotNull(filterCriteria.OnlineStatus.Values); + Assert.IsNotEmpty(filterCriteria.OnlineStatus.Values); Assert.Contains(BeatmapOnlineStatus.Ranked, filterCriteria.OnlineStatus.Values); Assert.Contains(BeatmapOnlineStatus.Loved, filterCriteria.OnlineStatus.Values); } + [Test] + public void TestApplyRangeStatusMatches() + { + const string query = "status>=r"; + var filterCriteria = new FilterCriteria(); + FilterQueryParser.ApplyQueries(filterCriteria, query); + Assert.IsNotEmpty(filterCriteria.OnlineStatus.Values); + Assert.Contains(BeatmapOnlineStatus.Ranked, filterCriteria.OnlineStatus.Values); + Assert.Contains(BeatmapOnlineStatus.Approved, filterCriteria.OnlineStatus.Values); + Assert.Contains(BeatmapOnlineStatus.Qualified, filterCriteria.OnlineStatus.Values); + Assert.Contains(BeatmapOnlineStatus.Loved, filterCriteria.OnlineStatus.Values); + } + [TestCase("creator")] [TestCase("author")] [TestCase("mapper")] diff --git a/osu.Game/Screens/Select/FilterCriteria.cs b/osu.Game/Screens/Select/FilterCriteria.cs index 6cec7a387d..1d155574cf 100644 --- a/osu.Game/Screens/Select/FilterCriteria.cs +++ b/osu.Game/Screens/Select/FilterCriteria.cs @@ -34,7 +34,7 @@ namespace osu.Game.Screens.Select public OptionalRange Length; public OptionalRange BPM; public OptionalRange BeatDivisor; - public OptionalArray OnlineStatus; + public OptionalSet OnlineStatus = new OptionalSet(); public OptionalRange LastPlayed; public OptionalTextFilter Creator; public OptionalTextFilter Artist; @@ -114,24 +114,25 @@ namespace osu.Game.Screens.Select public IRulesetFilterCriteria? RulesetCriteria { get; set; } - public struct OptionalArray : IEquatable> + public struct OptionalSet : IEquatable> where T : struct { - public bool HasFilter => Values?.Length > 0; + public bool HasFilter => Values.Count > 0; public bool IsInRange(T value) { - return Values?.Contains(value) ?? false; + return Values.Contains(value); } - public T[]? Values; + public SortedSet Values = new SortedSet(); - public bool Equals(OptionalArray other) + public OptionalSet() { - if (Values is null && other.Values is null) - return true; + } - return Values?.SequenceEqual(other.Values ?? Array.Empty()) ?? false; + public bool Equals(OptionalSet other) + { + return Values.SetEquals(other.Values); } } diff --git a/osu.Game/Screens/Select/FilterQueryParser.cs b/osu.Game/Screens/Select/FilterQueryParser.cs index 208e7f559e..fbb09b93bd 100644 --- a/osu.Game/Screens/Select/FilterQueryParser.cs +++ b/osu.Game/Screens/Select/FilterQueryParser.cs @@ -69,7 +69,7 @@ namespace osu.Game.Screens.Select return TryUpdateCriteriaRange(ref criteria.BeatDivisor, op, value, tryParseInt); case "status": - return TryUpdateArrayRange(ref criteria.OnlineStatus, op, value, tryParseEnum); + return TryUpdateSetRange(ref criteria.OnlineStatus, op, value, tryParseEnum); case "creator": case "author": @@ -306,23 +306,55 @@ namespace osu.Game.Screens.Select /// If can be parsed into using , the function returns true /// and the resulting range constraint is stored into the 's expected values. /// - /// The to store the parsed data into, if successful. + /// The to store the parsed data into, if successful. /// The operator for the keyword filter. Currently, only can be used. /// The value of the keyword filter. /// Function used to determine if can be converted to type . - public static bool TryUpdateArrayRange(ref FilterCriteria.OptionalArray range, Operator op, string val, TryParseFunction parseFunction) + public static bool TryUpdateSetRange(ref FilterCriteria.OptionalSet range, Operator op, string val, TryParseFunction parseFunction) where T : struct - => parseFunction.Invoke(val, out var converted) && tryUpdateArrayRange(ref range, op, converted); + => parseFunction.Invoke(val, out var converted) && tryUpdateSetRange(ref range, op, converted); - private static bool tryUpdateArrayRange(ref FilterCriteria.OptionalArray range, Operator op, T value) + private static bool tryUpdateSetRange(ref FilterCriteria.OptionalSet range, Operator op, T value) where T : struct { - if (op != Operator.Equal) - return false; + var enumValues = (T[])Enum.GetValues(typeof(T)); - range.Values ??= Array.Empty(); + foreach (var enumValue in enumValues) + { + switch (op) + { + case Operator.Less: + if (Comparer.Default.Compare(enumValue, value) < 0) + range.Values.Add(enumValue); - range.Values = range.Values.Append(value).ToArray(); + break; + + case Operator.LessOrEqual: + if (Comparer.Default.Compare(enumValue, value) <= 0) + range.Values.Add(enumValue); + + break; + + case Operator.Equal: + range.Values.Add(value); + break; + + case Operator.GreaterOrEqual: + if (Comparer.Default.Compare(enumValue, value) >= 0) + range.Values.Add(enumValue); + + break; + + case Operator.Greater: + if (Comparer.Default.Compare(enumValue, value) > 0) + range.Values.Add(enumValue); + + break; + + default: + return false; + } + } return true; } From 77119b2cdb1c4f2b743aa9a99eac1f48f74064d1 Mon Sep 17 00:00:00 2001 From: Vlad Frangu Date: Mon, 18 Mar 2024 16:44:42 +0200 Subject: [PATCH 0361/2493] chore: correct doc string --- osu.Game/Screens/Select/FilterQueryParser.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Select/FilterQueryParser.cs b/osu.Game/Screens/Select/FilterQueryParser.cs index fbb09b93bd..76d58963e7 100644 --- a/osu.Game/Screens/Select/FilterQueryParser.cs +++ b/osu.Game/Screens/Select/FilterQueryParser.cs @@ -307,7 +307,7 @@ namespace osu.Game.Screens.Select /// and the resulting range constraint is stored into the 's expected values. /// /// The to store the parsed data into, if successful. - /// The operator for the keyword filter. Currently, only can be used. + /// The operator for the keyword filter. /// The value of the keyword filter. /// Function used to determine if can be converted to type . public static bool TryUpdateSetRange(ref FilterCriteria.OptionalSet range, Operator op, string val, TryParseFunction parseFunction) From a3f3dcf853b6148c6c2cbebe41b88711c01d468a Mon Sep 17 00:00:00 2001 From: Arthur Araujo Date: Mon, 18 Mar 2024 13:27:43 -0300 Subject: [PATCH 0362/2493] Inline percentage calculation --- osu.Game/Rulesets/Edit/Checks/CheckUnusedAudioAtEnd.cs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/osu.Game/Rulesets/Edit/Checks/CheckUnusedAudioAtEnd.cs b/osu.Game/Rulesets/Edit/Checks/CheckUnusedAudioAtEnd.cs index 9c1f2748e9..8795eeac2d 100644 --- a/osu.Game/Rulesets/Edit/Checks/CheckUnusedAudioAtEnd.cs +++ b/osu.Game/Rulesets/Edit/Checks/CheckUnusedAudioAtEnd.cs @@ -23,7 +23,7 @@ namespace osu.Game.Rulesets.Edit.Checks double mappedLength = context.Beatmap.HitObjects.Last().GetEndTime(); double trackLength = context.WorkingBeatmap.Track.Length; - double mappedPercentage = calculatePercentage(mappedLength, trackLength); + double mappedPercentage = Math.Round(mappedLength / trackLength * 100); if (mappedPercentage < 80) { @@ -31,11 +31,6 @@ namespace osu.Game.Rulesets.Edit.Checks } } - private double calculatePercentage(double mappedLenght, double trackLenght) - { - return Math.Round(mappedLenght / trackLenght * 100); - } - public class IssueTemplateUnusedAudioAtEnd : IssueTemplate { public IssueTemplateUnusedAudioAtEnd(ICheck check) From 915a9682b5be54b090d39ae2a44beae8ea2c9ce7 Mon Sep 17 00:00:00 2001 From: Arthur Araujo Date: Mon, 18 Mar 2024 13:51:36 -0300 Subject: [PATCH 0363/2493] Fix issue type and display percentage left --- osu.Game/Rulesets/Edit/Checks/CheckUnusedAudioAtEnd.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/osu.Game/Rulesets/Edit/Checks/CheckUnusedAudioAtEnd.cs b/osu.Game/Rulesets/Edit/Checks/CheckUnusedAudioAtEnd.cs index 8795eeac2d..d22303b7df 100644 --- a/osu.Game/Rulesets/Edit/Checks/CheckUnusedAudioAtEnd.cs +++ b/osu.Game/Rulesets/Edit/Checks/CheckUnusedAudioAtEnd.cs @@ -27,18 +27,19 @@ namespace osu.Game.Rulesets.Edit.Checks if (mappedPercentage < 80) { - yield return new IssueTemplateUnusedAudioAtEnd(this).Create(); + double percentageLeft = Math.Abs(mappedPercentage - 100); + yield return new IssueTemplateUnusedAudioAtEnd(this).Create(percentageLeft); } } public class IssueTemplateUnusedAudioAtEnd : IssueTemplate { public IssueTemplateUnusedAudioAtEnd(ICheck check) - : base(check, IssueType.Problem, "There is more than 20% unused audio at the end.") + : base(check, IssueType.Warning, "Currently there is {0}% unused audio at the end. Ensure the outro significantly contributes to the song, otherwise cut the outro.") { } - public Issue Create() => new Issue(this); + public Issue Create(double percentageLeft) => new Issue(this, percentageLeft); } } } From c23212f4efad0c58207265877a5130177f47d2e5 Mon Sep 17 00:00:00 2001 From: Arthur Araujo Date: Mon, 18 Mar 2024 14:02:33 -0300 Subject: [PATCH 0364/2493] Use `GetLastObjectTime` to calculate mapped length --- osu.Game/Rulesets/Edit/Checks/CheckUnusedAudioAtEnd.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/osu.Game/Rulesets/Edit/Checks/CheckUnusedAudioAtEnd.cs b/osu.Game/Rulesets/Edit/Checks/CheckUnusedAudioAtEnd.cs index d22303b7df..d9a675fd17 100644 --- a/osu.Game/Rulesets/Edit/Checks/CheckUnusedAudioAtEnd.cs +++ b/osu.Game/Rulesets/Edit/Checks/CheckUnusedAudioAtEnd.cs @@ -3,9 +3,8 @@ using System; using System.Collections.Generic; -using System.Linq; +using osu.Game.Beatmaps; using osu.Game.Rulesets.Edit.Checks.Components; -using osu.Game.Rulesets.Objects; namespace osu.Game.Rulesets.Edit.Checks { @@ -20,7 +19,7 @@ namespace osu.Game.Rulesets.Edit.Checks public IEnumerable Run(BeatmapVerifierContext context) { - double mappedLength = context.Beatmap.HitObjects.Last().GetEndTime(); + double mappedLength = context.Beatmap.GetLastObjectTime(); double trackLength = context.WorkingBeatmap.Track.Length; double mappedPercentage = Math.Round(mappedLength / trackLength * 100); From 0edc249637d56208bca655db17e274738df0b70e Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Mon, 18 Mar 2024 20:38:19 +0300 Subject: [PATCH 0365/2493] Make Timeline non-nullable --- .../Components/Timeline/TimelineControlPointDisplay.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointDisplay.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointDisplay.cs index 950b717ffb..1bf12e40d1 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointDisplay.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointDisplay.cs @@ -15,7 +15,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline public partial class TimelineControlPointDisplay : TimelinePart { [Resolved] - private Timeline? timeline { get; set; } + private Timeline timeline { get; set; } = null!; /// /// The visible time/position range of the timeline. @@ -42,7 +42,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline { base.Update(); - if (timeline == null || DrawWidth <= 0) return; + if (DrawWidth <= 0) return; (float, float) newRange = ( (ToLocalSpace(timeline.ScreenSpaceDrawQuad.TopLeft).X - TopPointPiece.WIDTH) / DrawWidth * Content.RelativeChildSize.X, From 7ca45c75b3348f66c0ec86e02a72c114abb27c8c Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Mon, 18 Mar 2024 20:46:38 +0300 Subject: [PATCH 0366/2493] Don't iterate backwards on children without a reason --- .../Components/Timeline/TimelineControlPointDisplay.cs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointDisplay.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointDisplay.cs index 1bf12e40d1..8e522fa715 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointDisplay.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointDisplay.cs @@ -63,12 +63,10 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline private void recreateDrawableGroups() { // Remove groups outside the visible range - for (int i = Count - 1; i >= 0; i--) + foreach (var drawableGroup in this) { - var g = Children[i]; - - if (!shouldBeVisible(g.Group)) - g.Expire(); + if (!shouldBeVisible(drawableGroup.Group)) + drawableGroup.Expire(); } // Add remaining ones From f6d7f18f2592071d98872e2cda18f8de9d20cfa1 Mon Sep 17 00:00:00 2001 From: Arthur Araujo Date: Mon, 18 Mar 2024 15:59:19 -0300 Subject: [PATCH 0367/2493] Remove unused localisation file --- .../CheckUnusedAudioAtEndStrings.cs | 19 ------------------- 1 file changed, 19 deletions(-) delete mode 100644 osu.Game/Localisation/CheckUnusedAudioAtEndStrings.cs diff --git a/osu.Game/Localisation/CheckUnusedAudioAtEndStrings.cs b/osu.Game/Localisation/CheckUnusedAudioAtEndStrings.cs deleted file mode 100644 index 46f92237a9..0000000000 --- a/osu.Game/Localisation/CheckUnusedAudioAtEndStrings.cs +++ /dev/null @@ -1,19 +0,0 @@ -// 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.Localisation -{ - public static class CheckUnusedAudioAtEndStrings - { - private const string prefix = @"osu.Game.Resources.Localisation.CheckUnusedAudioAtEnd"; - - /// - /// "{0}% of the audio is not mapped." - /// - public static LocalisableString OfTheAudioIsNot(double percentageLeft) => new TranslatableString(getKey(@"of_the_audio_is_not"), @"{0}% of the audio is not mapped.", percentageLeft); - - private static string getKey(string key) => $@"{prefix}:{key}"; - } -} From 5241c999c1f3300f36723a1d070c23240936c8da Mon Sep 17 00:00:00 2001 From: Arthur Araujo Date: Mon, 18 Mar 2024 16:08:41 -0300 Subject: [PATCH 0368/2493] Add different warning to maps with storyboard/video --- .../Checks/CheckUnusedAudioAtEndTest.cs | 51 +++++++++++++++++-- .../Edit/Checks/CheckUnusedAudioAtEnd.cs | 37 +++++++++++++- 2 files changed, 84 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Editing/Checks/CheckUnusedAudioAtEndTest.cs b/osu.Game.Tests/Editing/Checks/CheckUnusedAudioAtEndTest.cs index 29c5cb96fd..33d73a8086 100644 --- a/osu.Game.Tests/Editing/Checks/CheckUnusedAudioAtEndTest.cs +++ b/osu.Game.Tests/Editing/Checks/CheckUnusedAudioAtEndTest.cs @@ -4,12 +4,15 @@ using System.Linq; using Moq; using NUnit.Framework; +using osu.Framework.Graphics; using osu.Framework.Timing; 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.Storyboards; +using osuTK; using static osu.Game.Tests.Visual.OsuTestScene.ClockBackedTestWorkingBeatmap; namespace osu.Game.Tests.Editing.Checks @@ -47,7 +50,7 @@ namespace osu.Game.Tests.Editing.Checks }, BeatmapInfo = new BeatmapInfo { - Metadata = new BeatmapMetadata { AudioFile = "abc123.jpg" } + Metadata = new BeatmapMetadata { AudioFile = "abc123.jpg" }, } }; } @@ -62,6 +65,42 @@ namespace osu.Game.Tests.Editing.Checks Assert.That(issues.Single().Template is CheckUnusedAudioAtEnd.IssueTemplateUnusedAudioAtEnd); } + [Test] + public void TestAudioNotFullyUsedWithVideo() + { + var storyboard = new Storyboard(); + + var video = new StoryboardVideo("abc123.mp4", 0); + + storyboard.GetLayer("Video").Add(video); + + var mockWorkingBeatmap = getMockWorkingBeatmap(beatmapNotFullyMapped, storyboard); + + var context = getContext(beatmapNotFullyMapped, mockWorkingBeatmap); + var issues = check.Run(context).ToList(); + + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues.Single().Template is CheckUnusedAudioAtEnd.IssueTemplateUnusedAudioAtEndStoryboardOrVideo); + } + + [Test] + public void TestAudioNotFullyUsedWithStoryboardElement() + { + var storyboard = new Storyboard(); + + var sprite = new StoryboardSprite("unknown", Anchor.TopLeft, Vector2.Zero); + + storyboard.GetLayer("Background").Add(sprite); + + var mockWorkingBeatmap = getMockWorkingBeatmap(beatmapNotFullyMapped, storyboard); + + var context = getContext(beatmapNotFullyMapped, mockWorkingBeatmap); + var issues = check.Run(context).ToList(); + + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues.Single().Template is CheckUnusedAudioAtEnd.IssueTemplateUnusedAudioAtEndStoryboardOrVideo); + } + [Test] public void TestAudioFullyUsed() { @@ -73,16 +112,22 @@ namespace osu.Game.Tests.Editing.Checks private BeatmapVerifierContext getContext(IBeatmap beatmap) { - return new BeatmapVerifierContext(beatmap, getMockWorkingBeatmap(beatmap).Object); + return new BeatmapVerifierContext(beatmap, getMockWorkingBeatmap(beatmap, new Storyboard()).Object); } - private Mock getMockWorkingBeatmap(IBeatmap beatmap) + private BeatmapVerifierContext getContext(IBeatmap beatmap, Mock workingBeatmap) + { + return new BeatmapVerifierContext(beatmap, workingBeatmap.Object); + } + + private Mock getMockWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard) { var mockTrack = new TrackVirtualStore(new FramedClock()).GetVirtual(10000, "virtual"); var mockWorkingBeatmap = new Mock(); mockWorkingBeatmap.SetupGet(w => w.Beatmap).Returns(beatmap); mockWorkingBeatmap.SetupGet(w => w.Track).Returns(mockTrack); + mockWorkingBeatmap.SetupGet(w => w.Storyboard).Returns(storyboard); return mockWorkingBeatmap; } diff --git a/osu.Game/Rulesets/Edit/Checks/CheckUnusedAudioAtEnd.cs b/osu.Game/Rulesets/Edit/Checks/CheckUnusedAudioAtEnd.cs index d9a675fd17..2f768b6ffa 100644 --- a/osu.Game/Rulesets/Edit/Checks/CheckUnusedAudioAtEnd.cs +++ b/osu.Game/Rulesets/Edit/Checks/CheckUnusedAudioAtEnd.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using osu.Game.Beatmaps; using osu.Game.Rulesets.Edit.Checks.Components; +using osu.Game.Storyboards; namespace osu.Game.Rulesets.Edit.Checks { @@ -15,6 +16,7 @@ namespace osu.Game.Rulesets.Edit.Checks public IEnumerable PossibleTemplates => new IssueTemplate[] { new IssueTemplateUnusedAudioAtEnd(this), + new IssueTemplateUnusedAudioAtEndStoryboardOrVideo(this), }; public IEnumerable Run(BeatmapVerifierContext context) @@ -27,10 +29,33 @@ namespace osu.Game.Rulesets.Edit.Checks if (mappedPercentage < 80) { double percentageLeft = Math.Abs(mappedPercentage - 100); - yield return new IssueTemplateUnusedAudioAtEnd(this).Create(percentageLeft); + + bool storyboardIsPresent = isAnyStoryboardElementPresent(context.WorkingBeatmap.Storyboard); + + if (storyboardIsPresent) + { + yield return new IssueTemplateUnusedAudioAtEndStoryboardOrVideo(this).Create(percentageLeft); + } + else + { + yield return new IssueTemplateUnusedAudioAtEnd(this).Create(percentageLeft); + } } } + private bool isAnyStoryboardElementPresent(Storyboard storyboard) + { + foreach (var layer in storyboard.Layers) + { + foreach (var _ in layer.Elements) + { + return true; + } + } + + return false; + } + public class IssueTemplateUnusedAudioAtEnd : IssueTemplate { public IssueTemplateUnusedAudioAtEnd(ICheck check) @@ -40,5 +65,15 @@ namespace osu.Game.Rulesets.Edit.Checks public Issue Create(double percentageLeft) => new Issue(this, percentageLeft); } + + public class IssueTemplateUnusedAudioAtEndStoryboardOrVideo : IssueTemplate + { + public IssueTemplateUnusedAudioAtEndStoryboardOrVideo(ICheck check) + : base(check, IssueType.Warning, "Currently there is {0}% unused audio at the end. Ensure the outro significantly contributes to the song, or is being occupied by the video or storyboard, otherwise cut the outro.") + { + } + + public Issue Create(double percentageLeft) => new Issue(this, percentageLeft); + } } } From a8ce6a0bba5c65ab12360191d7589365d0996133 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 19 Mar 2024 02:43:34 +0300 Subject: [PATCH 0369/2493] Use `Slice` method instead of index range operators for readability --- .../Rulesets/Objects/Legacy/ConvertHitObjectParser.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs index 2b058d5e1f..283a59b7ed 100644 --- a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs @@ -303,16 +303,16 @@ namespace osu.Game.Rulesets.Objects.Legacy { int startIndex = segmentsBuffer[i].StartIndex; int endIndex = segmentsBuffer[i + 1].StartIndex; - controlPoints.AddRange(convertPoints(segmentsBuffer[i].Type, allPoints[startIndex..endIndex], pointsBuffer[endIndex])); + controlPoints.AddRange(convertPoints(segmentsBuffer[i].Type, allPoints.Slice(startIndex, endIndex - startIndex), pointsBuffer[endIndex])); } else { int startIndex = segmentsBuffer[i].StartIndex; - controlPoints.AddRange(convertPoints(segmentsBuffer[i].Type, allPoints[startIndex..], null)); + controlPoints.AddRange(convertPoints(segmentsBuffer[i].Type, allPoints.Slice(startIndex), null)); } } - return mergePointsLists(controlPoints); + return mergeControlPointsLists(controlPoints); } finally { @@ -402,7 +402,7 @@ namespace osu.Game.Rulesets.Objects.Legacy - (p1.X - p0.X) * (p2.Y - p0.Y)); } - private PathControlPoint[] mergePointsLists(List> controlPointList) + private PathControlPoint[] mergeControlPointsLists(List> controlPointList) { int totalCount = 0; From af713a78695f4abd3e8023a98245f034fbb55da8 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 19 Mar 2024 17:20:59 +0900 Subject: [PATCH 0370/2493] Fix incorrectly encoded score IsPerfect value --- .../Formats/LegacyScoreEncoderTest.cs | 38 ++++++++++++++++--- osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs | 7 +++- osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs | 2 +- 3 files changed, 40 insertions(+), 7 deletions(-) diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyScoreEncoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyScoreEncoderTest.cs index c0a7285f39..c0bf47dfc9 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyScoreEncoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyScoreEncoderTest.cs @@ -6,6 +6,7 @@ using System.IO; using NUnit.Framework; using osu.Game.Beatmaps.Formats; using osu.Game.Rulesets.Catch; +using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; using osu.Game.Scoring.Legacy; @@ -21,9 +22,9 @@ namespace osu.Game.Tests.Beatmaps.Formats public void CatchMergesFruitAndDropletMisses(int missCount, int largeTickMissCount) { var ruleset = new CatchRuleset().RulesetInfo; - var scoreInfo = TestResources.CreateTestScoreInfo(ruleset); var beatmap = new TestBeatmap(ruleset); + scoreInfo.Statistics = new Dictionary { [HitResult.Great] = 50, @@ -31,14 +32,41 @@ namespace osu.Game.Tests.Beatmaps.Formats [HitResult.Miss] = missCount, [HitResult.LargeTickMiss] = largeTickMissCount }; - var score = new Score { ScoreInfo = scoreInfo }; - var decodedAfterEncode = encodeThenDecode(LegacyBeatmapDecoder.LATEST_VERSION, score, beatmap); + var score = new Score { ScoreInfo = scoreInfo }; + var decodedAfterEncode = encodeThenDecode(LegacyBeatmapDecoder.LATEST_VERSION, score, beatmap, out _); Assert.That(decodedAfterEncode.ScoreInfo.GetCountMiss(), Is.EqualTo(missCount + largeTickMissCount)); } - private static Score encodeThenDecode(int beatmapVersion, Score score, TestBeatmap beatmap) + [Test] + public void ScoreWithMissIsNotPerfect() + { + var ruleset = new OsuRuleset().RulesetInfo; + var scoreInfo = TestResources.CreateTestScoreInfo(ruleset); + var beatmap = new TestBeatmap(ruleset); + + scoreInfo.Statistics = new Dictionary + { + [HitResult.Great] = 2, + [HitResult.Miss] = 1, + }; + + scoreInfo.MaximumStatistics = new Dictionary + { + [HitResult.Great] = 3 + }; + + // Hit -> Miss -> Hit + scoreInfo.Combo = 1; + scoreInfo.MaxCombo = 1; + + encodeThenDecode(LegacyBeatmapDecoder.LATEST_VERSION, new Score { ScoreInfo = scoreInfo }, beatmap, out var decoder); + + Assert.That(decoder.DecodedPerfectValue, Is.False); + } + + private static Score encodeThenDecode(int beatmapVersion, Score score, TestBeatmap beatmap, out LegacyScoreDecoderTest.TestLegacyScoreDecoder decoder) { var encodeStream = new MemoryStream(); @@ -47,7 +75,7 @@ namespace osu.Game.Tests.Beatmaps.Formats var decodeStream = new MemoryStream(encodeStream.GetBuffer()); - var decoder = new LegacyScoreDecoderTest.TestLegacyScoreDecoder(beatmapVersion); + decoder = new LegacyScoreDecoderTest.TestLegacyScoreDecoder(beatmapVersion); var decodedAfterEncode = decoder.Parse(decodeStream); return decodedAfterEncode; } diff --git a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs index 65e2c02655..f2c096da15 100644 --- a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs +++ b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs @@ -27,6 +27,11 @@ namespace osu.Game.Scoring.Legacy { public abstract class LegacyScoreDecoder { + /// + /// The decoded "IsPerfect" value. This isn't used by osu!lazer. + /// + public bool DecodedPerfectValue { get; private set; } + private IBeatmap currentBeatmap; private Ruleset currentRuleset; @@ -82,7 +87,7 @@ namespace osu.Game.Scoring.Legacy scoreInfo.MaxCombo = sr.ReadUInt16(); /* score.Perfect = */ - sr.ReadBoolean(); + DecodedPerfectValue = sr.ReadBoolean(); scoreInfo.Mods = currentRuleset.ConvertFromLegacyMods((LegacyMods)sr.ReadInt32()).ToArray(); diff --git a/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs index 4ee4231925..1df54565e9 100644 --- a/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs +++ b/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs @@ -93,7 +93,7 @@ namespace osu.Game.Scoring.Legacy sw.Write((ushort)(score.ScoreInfo.GetCountMiss() ?? 0)); sw.Write((int)(score.ScoreInfo.TotalScore)); sw.Write((ushort)score.ScoreInfo.MaxCombo); - sw.Write(score.ScoreInfo.Combo == score.ScoreInfo.MaxCombo); + sw.Write(score.ScoreInfo.Combo == score.ScoreInfo.GetMaximumAchievableCombo()); sw.Write((int)score.ScoreInfo.Ruleset.CreateInstance().ConvertToLegacyMods(score.ScoreInfo.Mods)); sw.Write(getHpGraphFormatted()); From 6e3350941749a87b21ed2c819c063e7a556509f0 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 19 Mar 2024 17:44:37 +0900 Subject: [PATCH 0371/2493] Remove added property, use local decoding instead --- .../Formats/LegacyScoreEncoderTest.cs | 34 ++++++++++++++++--- osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs | 7 +--- 2 files changed, 30 insertions(+), 11 deletions(-) diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyScoreEncoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyScoreEncoderTest.cs index c0bf47dfc9..806f538249 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyScoreEncoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyScoreEncoderTest.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.IO; using NUnit.Framework; using osu.Game.Beatmaps.Formats; +using osu.Game.IO.Legacy; using osu.Game.Rulesets.Catch; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Scoring; @@ -34,7 +35,7 @@ namespace osu.Game.Tests.Beatmaps.Formats }; var score = new Score { ScoreInfo = scoreInfo }; - var decodedAfterEncode = encodeThenDecode(LegacyBeatmapDecoder.LATEST_VERSION, score, beatmap, out _); + var decodedAfterEncode = encodeThenDecode(LegacyBeatmapDecoder.LATEST_VERSION, score, beatmap); Assert.That(decodedAfterEncode.ScoreInfo.GetCountMiss(), Is.EqualTo(missCount + largeTickMissCount)); } @@ -61,12 +62,35 @@ namespace osu.Game.Tests.Beatmaps.Formats scoreInfo.Combo = 1; scoreInfo.MaxCombo = 1; - encodeThenDecode(LegacyBeatmapDecoder.LATEST_VERSION, new Score { ScoreInfo = scoreInfo }, beatmap, out var decoder); + using (var ms = new MemoryStream()) + { + new LegacyScoreEncoder(new Score { ScoreInfo = scoreInfo }, beatmap).Encode(ms, true); - Assert.That(decoder.DecodedPerfectValue, Is.False); + ms.Seek(0, SeekOrigin.Begin); + + using (var sr = new SerializationReader(ms)) + { + sr.ReadByte(); // ruleset id + sr.ReadInt32(); // version + sr.ReadString(); // beatmap hash + sr.ReadString(); // username + sr.ReadString(); // score hash + sr.ReadInt16(); // count300 + sr.ReadInt16(); // count100 + sr.ReadInt16(); // count50 + sr.ReadInt16(); // countGeki + sr.ReadInt16(); // countKatu + sr.ReadInt16(); // countMiss + sr.ReadInt32(); // total score + sr.ReadInt16(); // max combo + bool isPerfect = sr.ReadBoolean(); // full combo + + Assert.That(isPerfect, Is.False); + } + } } - private static Score encodeThenDecode(int beatmapVersion, Score score, TestBeatmap beatmap, out LegacyScoreDecoderTest.TestLegacyScoreDecoder decoder) + private static Score encodeThenDecode(int beatmapVersion, Score score, TestBeatmap beatmap) { var encodeStream = new MemoryStream(); @@ -75,7 +99,7 @@ namespace osu.Game.Tests.Beatmaps.Formats var decodeStream = new MemoryStream(encodeStream.GetBuffer()); - decoder = new LegacyScoreDecoderTest.TestLegacyScoreDecoder(beatmapVersion); + var decoder = new LegacyScoreDecoderTest.TestLegacyScoreDecoder(beatmapVersion); var decodedAfterEncode = decoder.Parse(decodeStream); return decodedAfterEncode; } diff --git a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs index f2c096da15..65e2c02655 100644 --- a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs +++ b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs @@ -27,11 +27,6 @@ namespace osu.Game.Scoring.Legacy { public abstract class LegacyScoreDecoder { - /// - /// The decoded "IsPerfect" value. This isn't used by osu!lazer. - /// - public bool DecodedPerfectValue { get; private set; } - private IBeatmap currentBeatmap; private Ruleset currentRuleset; @@ -87,7 +82,7 @@ namespace osu.Game.Scoring.Legacy scoreInfo.MaxCombo = sr.ReadUInt16(); /* score.Perfect = */ - DecodedPerfectValue = sr.ReadBoolean(); + sr.ReadBoolean(); scoreInfo.Mods = currentRuleset.ConvertFromLegacyMods((LegacyMods)sr.ReadInt32()).ToArray(); From f6069d8d93b05c752b1aeaa36e4c269580880f26 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 19 Mar 2024 17:46:18 +0900 Subject: [PATCH 0372/2493] Compare against MaxCombo instead --- osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs index 1df54565e9..93f51ee74d 100644 --- a/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs +++ b/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs @@ -93,7 +93,7 @@ namespace osu.Game.Scoring.Legacy sw.Write((ushort)(score.ScoreInfo.GetCountMiss() ?? 0)); sw.Write((int)(score.ScoreInfo.TotalScore)); sw.Write((ushort)score.ScoreInfo.MaxCombo); - sw.Write(score.ScoreInfo.Combo == score.ScoreInfo.GetMaximumAchievableCombo()); + sw.Write(score.ScoreInfo.MaxCombo == score.ScoreInfo.GetMaximumAchievableCombo()); sw.Write((int)score.ScoreInfo.Ruleset.CreateInstance().ConvertToLegacyMods(score.ScoreInfo.Mods)); sw.Write(getHpGraphFormatted()); From 0de5ca8d2df633b044b41d121bb3ed97aee73e47 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 20 Mar 2024 01:26:39 +0800 Subject: [PATCH 0373/2493] Update incorrect xmldoc --- osu.Game/Screens/Play/DelayedResumeOverlay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/DelayedResumeOverlay.cs b/osu.Game/Screens/Play/DelayedResumeOverlay.cs index fd1ce5d829..8bb3ae8182 100644 --- a/osu.Game/Screens/Play/DelayedResumeOverlay.cs +++ b/osu.Game/Screens/Play/DelayedResumeOverlay.cs @@ -18,7 +18,7 @@ using osuTK; namespace osu.Game.Screens.Play { /// - /// Simple that resumes after 800ms. + /// Simple that resumes after a short delay. /// public partial class DelayedResumeOverlay : ResumeOverlay { From feaf59e15f701e1348122e17c4650420b8a581ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 19 Mar 2024 18:19:06 +0100 Subject: [PATCH 0374/2493] Use `HashSet` instead of `SortedSet` No need for it to be sorted. --- .../NonVisual/Filtering/FilterQueryParserTest.cs | 16 ++++++++-------- osu.Game/Screens/Select/FilterCriteria.cs | 12 +++--------- 2 files changed, 11 insertions(+), 17 deletions(-) diff --git a/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs b/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs index 98d3286409..bd706b5b4c 100644 --- a/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs +++ b/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs @@ -257,7 +257,7 @@ namespace osu.Game.Tests.NonVisual.Filtering var filterCriteria = new FilterCriteria(); FilterQueryParser.ApplyQueries(filterCriteria, query); Assert.IsNotEmpty(filterCriteria.OnlineStatus.Values); - Assert.Contains(BeatmapOnlineStatus.Ranked, filterCriteria.OnlineStatus.Values); + Assert.That(filterCriteria.OnlineStatus.Values, Contains.Item(BeatmapOnlineStatus.Ranked)); } [Test] @@ -269,7 +269,7 @@ namespace osu.Game.Tests.NonVisual.Filtering Assert.AreEqual("I want the pp", filterCriteria.SearchText.Trim()); Assert.AreEqual(4, filterCriteria.SearchTerms.Length); Assert.IsNotEmpty(filterCriteria.OnlineStatus.Values); - Assert.Contains(BeatmapOnlineStatus.Ranked, filterCriteria.OnlineStatus.Values); + Assert.That(filterCriteria.OnlineStatus.Values, Contains.Item(BeatmapOnlineStatus.Ranked)); } [Test] @@ -279,8 +279,8 @@ namespace osu.Game.Tests.NonVisual.Filtering var filterCriteria = new FilterCriteria(); FilterQueryParser.ApplyQueries(filterCriteria, query); Assert.IsNotEmpty(filterCriteria.OnlineStatus.Values); - Assert.Contains(BeatmapOnlineStatus.Ranked, filterCriteria.OnlineStatus.Values); - Assert.Contains(BeatmapOnlineStatus.Loved, filterCriteria.OnlineStatus.Values); + Assert.That(filterCriteria.OnlineStatus.Values, Contains.Item(BeatmapOnlineStatus.Ranked)); + Assert.That(filterCriteria.OnlineStatus.Values, Contains.Item(BeatmapOnlineStatus.Loved)); } [Test] @@ -290,10 +290,10 @@ namespace osu.Game.Tests.NonVisual.Filtering var filterCriteria = new FilterCriteria(); FilterQueryParser.ApplyQueries(filterCriteria, query); Assert.IsNotEmpty(filterCriteria.OnlineStatus.Values); - Assert.Contains(BeatmapOnlineStatus.Ranked, filterCriteria.OnlineStatus.Values); - Assert.Contains(BeatmapOnlineStatus.Approved, filterCriteria.OnlineStatus.Values); - Assert.Contains(BeatmapOnlineStatus.Qualified, filterCriteria.OnlineStatus.Values); - Assert.Contains(BeatmapOnlineStatus.Loved, filterCriteria.OnlineStatus.Values); + Assert.That(filterCriteria.OnlineStatus.Values, Contains.Item(BeatmapOnlineStatus.Ranked)); + Assert.That(filterCriteria.OnlineStatus.Values, Contains.Item(BeatmapOnlineStatus.Approved)); + Assert.That(filterCriteria.OnlineStatus.Values, Contains.Item(BeatmapOnlineStatus.Qualified)); + Assert.That(filterCriteria.OnlineStatus.Values, Contains.Item(BeatmapOnlineStatus.Loved)); } [TestCase("creator")] diff --git a/osu.Game/Screens/Select/FilterCriteria.cs b/osu.Game/Screens/Select/FilterCriteria.cs index 1d155574cf..6750b07aba 100644 --- a/osu.Game/Screens/Select/FilterCriteria.cs +++ b/osu.Game/Screens/Select/FilterCriteria.cs @@ -119,21 +119,15 @@ namespace osu.Game.Screens.Select { public bool HasFilter => Values.Count > 0; - public bool IsInRange(T value) - { - return Values.Contains(value); - } + public bool IsInRange(T value) => Values.Contains(value); - public SortedSet Values = new SortedSet(); + public HashSet Values = new HashSet(); public OptionalSet() { } - public bool Equals(OptionalSet other) - { - return Values.SetEquals(other.Values); - } + public bool Equals(OptionalSet other) => Values.SetEquals(other.Values); } public struct OptionalRange : IEquatable> From 320373e3e0a50c5b94a0f5a68a108ce24b3da2e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 19 Mar 2024 18:28:13 +0100 Subject: [PATCH 0375/2493] Rename method to match convention better --- osu.Game/Screens/Select/FilterQueryParser.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Select/FilterQueryParser.cs b/osu.Game/Screens/Select/FilterQueryParser.cs index 76d58963e7..194a288426 100644 --- a/osu.Game/Screens/Select/FilterQueryParser.cs +++ b/osu.Game/Screens/Select/FilterQueryParser.cs @@ -69,7 +69,7 @@ namespace osu.Game.Screens.Select return TryUpdateCriteriaRange(ref criteria.BeatDivisor, op, value, tryParseInt); case "status": - return TryUpdateSetRange(ref criteria.OnlineStatus, op, value, tryParseEnum); + return TryUpdateCriteriaSet(ref criteria.OnlineStatus, op, value, tryParseEnum); case "creator": case "author": @@ -310,11 +310,11 @@ namespace osu.Game.Screens.Select /// The operator for the keyword filter. /// The value of the keyword filter. /// Function used to determine if can be converted to type . - public static bool TryUpdateSetRange(ref FilterCriteria.OptionalSet range, Operator op, string val, TryParseFunction parseFunction) + public static bool TryUpdateCriteriaSet(ref FilterCriteria.OptionalSet range, Operator op, string val, TryParseFunction parseFunction) where T : struct - => parseFunction.Invoke(val, out var converted) && tryUpdateSetRange(ref range, op, converted); + => parseFunction.Invoke(val, out var converted) && tryUpdateCriteriaSet(ref range, op, converted); - private static bool tryUpdateSetRange(ref FilterCriteria.OptionalSet range, Operator op, T value) + private static bool tryUpdateCriteriaSet(ref FilterCriteria.OptionalSet range, Operator op, T value) where T : struct { var enumValues = (T[])Enum.GetValues(typeof(T)); From af3f7dcbbfb3e5b955f45e56815238eae71ccbdf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 19 Mar 2024 18:31:07 +0100 Subject: [PATCH 0376/2493] Retouch update criteria method --- osu.Game/Screens/Select/FilterQueryParser.cs | 30 ++++++++------------ 1 file changed, 12 insertions(+), 18 deletions(-) diff --git a/osu.Game/Screens/Select/FilterQueryParser.cs b/osu.Game/Screens/Select/FilterQueryParser.cs index 194a288426..32b533e18d 100644 --- a/osu.Game/Screens/Select/FilterQueryParser.cs +++ b/osu.Game/Screens/Select/FilterQueryParser.cs @@ -311,44 +311,38 @@ namespace osu.Game.Screens.Select /// The value of the keyword filter. /// Function used to determine if can be converted to type . public static bool TryUpdateCriteriaSet(ref FilterCriteria.OptionalSet range, Operator op, string val, TryParseFunction parseFunction) - where T : struct + where T : struct, Enum => parseFunction.Invoke(val, out var converted) && tryUpdateCriteriaSet(ref range, op, converted); - private static bool tryUpdateCriteriaSet(ref FilterCriteria.OptionalSet range, Operator op, T value) - where T : struct + private static bool tryUpdateCriteriaSet(ref FilterCriteria.OptionalSet range, Operator op, T pivotValue) + where T : struct, Enum { - var enumValues = (T[])Enum.GetValues(typeof(T)); + var allDefinedValues = Enum.GetValues(); - foreach (var enumValue in enumValues) + foreach (var val in allDefinedValues) { + int compareResult = Comparer.Default.Compare(val, pivotValue); + switch (op) { case Operator.Less: - if (Comparer.Default.Compare(enumValue, value) < 0) - range.Values.Add(enumValue); - + if (compareResult < 0) range.Values.Add(val); break; case Operator.LessOrEqual: - if (Comparer.Default.Compare(enumValue, value) <= 0) - range.Values.Add(enumValue); - + if (compareResult <= 0) range.Values.Add(val); break; case Operator.Equal: - range.Values.Add(value); + if (compareResult == 0) range.Values.Add(val); break; case Operator.GreaterOrEqual: - if (Comparer.Default.Compare(enumValue, value) >= 0) - range.Values.Add(enumValue); - + if (compareResult >= 0) range.Values.Add(val); break; case Operator.Greater: - if (Comparer.Default.Compare(enumValue, value) > 0) - range.Values.Add(enumValue); - + if (compareResult > 0) range.Values.Add(val); break; default: From 0211ae12adbed9b0d37881fbc87774ca901a0953 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 19 Mar 2024 19:16:33 +0100 Subject: [PATCH 0377/2493] Add failing test case for crash on empty beatmap --- .../Editing/Checks/CheckUnusedAudioAtEndTest.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/osu.Game.Tests/Editing/Checks/CheckUnusedAudioAtEndTest.cs b/osu.Game.Tests/Editing/Checks/CheckUnusedAudioAtEndTest.cs index 33d73a8086..bf996b06ea 100644 --- a/osu.Game.Tests/Editing/Checks/CheckUnusedAudioAtEndTest.cs +++ b/osu.Game.Tests/Editing/Checks/CheckUnusedAudioAtEndTest.cs @@ -55,6 +55,16 @@ namespace osu.Game.Tests.Editing.Checks }; } + [Test] + public void TestEmptyBeatmap() + { + var context = getContext(new Beatmap()); + var issues = check.Run(context).ToList(); + + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues.Single().Template is CheckUnusedAudioAtEnd.IssueTemplateUnusedAudioAtEnd); + } + [Test] public void TestAudioNotFullyUsed() { From 2b83e6bc4cc325375f1db483e84af25f73330087 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 19 Mar 2024 19:17:22 +0100 Subject: [PATCH 0378/2493] Fix check crash on empty beatmap --- osu.Game/Rulesets/Edit/Checks/CheckUnusedAudioAtEnd.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Edit/Checks/CheckUnusedAudioAtEnd.cs b/osu.Game/Rulesets/Edit/Checks/CheckUnusedAudioAtEnd.cs index 2f768b6ffa..2e97fbeb99 100644 --- a/osu.Game/Rulesets/Edit/Checks/CheckUnusedAudioAtEnd.cs +++ b/osu.Game/Rulesets/Edit/Checks/CheckUnusedAudioAtEnd.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Linq; using osu.Game.Beatmaps; using osu.Game.Rulesets.Edit.Checks.Components; using osu.Game.Storyboards; @@ -21,7 +22,7 @@ namespace osu.Game.Rulesets.Edit.Checks public IEnumerable Run(BeatmapVerifierContext context) { - double mappedLength = context.Beatmap.GetLastObjectTime(); + double mappedLength = context.Beatmap.HitObjects.Any() ? context.Beatmap.GetLastObjectTime() : 0; double trackLength = context.WorkingBeatmap.Track.Length; double mappedPercentage = Math.Round(mappedLength / trackLength * 100); From e4418547feba011e5461d2a9d5358d198d341427 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 19 Mar 2024 19:19:07 +0100 Subject: [PATCH 0379/2493] Document `GetLastObjectTime()` exception on empty beatmap --- osu.Game/Beatmaps/IBeatmap.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Beatmaps/IBeatmap.cs b/osu.Game/Beatmaps/IBeatmap.cs index b5bb6ccafc..6fe494ca0f 100644 --- a/osu.Game/Beatmaps/IBeatmap.cs +++ b/osu.Game/Beatmaps/IBeatmap.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 osu.Game.Beatmaps.ControlPoints; @@ -129,6 +130,7 @@ namespace osu.Game.Beatmaps /// /// It's not super efficient so calls should be kept to a minimum. /// + /// If has no objects. public static double GetLastObjectTime(this IBeatmap beatmap) => beatmap.HitObjects.Max(h => h.GetEndTime()); #region Helper methods From 808d6e09436468c8690311f235852ed0dd7834c9 Mon Sep 17 00:00:00 2001 From: jvyden Date: Tue, 19 Mar 2024 16:02:06 -0400 Subject: [PATCH 0380/2493] Prevent potential threading issues --- osu.Desktop/DiscordRichPresence.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Desktop/DiscordRichPresence.cs b/osu.Desktop/DiscordRichPresence.cs index ca26cab0fd..080032a298 100644 --- a/osu.Desktop/DiscordRichPresence.cs +++ b/osu.Desktop/DiscordRichPresence.cs @@ -97,11 +97,13 @@ namespace osu.Desktop private void onReady(object _, ReadyMessage __) { Logger.Log("Discord RPC Client ready.", LoggingTarget.Network, LogLevel.Debug); - updateStatus(); + Schedule(updateStatus); } private void updateStatus() { + Debug.Assert(ThreadSafety.IsUpdateThread); + if (!client.IsInitialized) return; From 0ecfa580d7a48d247ab9fa9907fdd0f1d739d732 Mon Sep 17 00:00:00 2001 From: jvyden Date: Tue, 19 Mar 2024 16:03:32 -0400 Subject: [PATCH 0381/2493] Move room code from activity code, prevent duplicate RPC updates --- osu.Desktop/DiscordRichPresence.cs | 68 +++++++++++++++++------------- 1 file changed, 39 insertions(+), 29 deletions(-) diff --git a/osu.Desktop/DiscordRichPresence.cs b/osu.Desktop/DiscordRichPresence.cs index 080032a298..633b6324b7 100644 --- a/osu.Desktop/DiscordRichPresence.cs +++ b/osu.Desktop/DiscordRichPresence.cs @@ -2,12 +2,14 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Diagnostics; using System.Text; using DiscordRPC; using DiscordRPC.Message; using Newtonsoft.Json; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Development; using osu.Framework.Graphics; using osu.Framework.Logging; using osu.Game; @@ -48,6 +50,8 @@ namespace osu.Desktop private readonly Bindable privacyMode = new Bindable(); + private int usersCurrentlyInLobby = 0; + private readonly RichPresence presence = new RichPresence { Assets = new Assets { LargeImageKey = "osu_logo_lazer" }, @@ -115,10 +119,10 @@ namespace osu.Desktop Logger.Log("Updating Discord RPC", LoggingTarget.Network, LogLevel.Debug); + bool hideIdentifiableInformation = privacyMode.Value == DiscordRichPresenceMode.Limited || status.Value == UserStatus.DoNotDisturb; + if (activity.Value != null) { - bool hideIdentifiableInformation = privacyMode.Value == DiscordRichPresenceMode.Limited || status.Value == UserStatus.DoNotDisturb; - presence.State = truncate(activity.Value.GetStatus(hideIdentifiableInformation)); presence.Details = truncate(activity.Value.GetDetails(hideIdentifiableInformation) ?? string.Empty); @@ -137,33 +141,6 @@ namespace osu.Desktop { presence.Buttons = null; } - - if (!hideIdentifiableInformation && multiplayerClient.Room != null) - { - MultiplayerRoom room = multiplayerClient.Room; - presence.Party = new Party - { - Privacy = string.IsNullOrEmpty(room.Settings.Password) ? Party.PrivacySetting.Public : Party.PrivacySetting.Private, - ID = room.RoomID.ToString(), - // technically lobbies can have infinite users, but Discord needs this to be set to something. - // to make party display sensible, assign a powers of two above participants count (8 at minimum). - Max = (int)Math.Max(8, Math.Pow(2, Math.Ceiling(Math.Log2(room.Users.Count)))), - Size = room.Users.Count, - }; - - RoomSecret roomSecret = new RoomSecret - { - RoomID = room.RoomID, - Password = room.Settings.Password, - }; - - presence.Secrets.JoinSecret = JsonConvert.SerializeObject(roomSecret); - } - else - { - presence.Party = null; - presence.Secrets.JoinSecret = null; - } } else { @@ -171,6 +148,39 @@ namespace osu.Desktop presence.Details = string.Empty; } + if (!hideIdentifiableInformation && multiplayerClient.Room != null) + { + MultiplayerRoom room = multiplayerClient.Room; + + if (room.Users.Count == usersCurrentlyInLobby) + return; + + presence.Party = new Party + { + Privacy = string.IsNullOrEmpty(room.Settings.Password) ? Party.PrivacySetting.Public : Party.PrivacySetting.Private, + ID = room.RoomID.ToString(), + // technically lobbies can have infinite users, but Discord needs this to be set to something. + // to make party display sensible, assign a powers of two above participants count (8 at minimum). + Max = (int)Math.Max(8, Math.Pow(2, Math.Ceiling(Math.Log2(room.Users.Count)))), + Size = room.Users.Count, + }; + + RoomSecret roomSecret = new RoomSecret + { + RoomID = room.RoomID, + Password = room.Settings.Password, + }; + + presence.Secrets.JoinSecret = JsonConvert.SerializeObject(roomSecret); + usersCurrentlyInLobby = room.Users.Count; + } + else + { + presence.Party = null; + presence.Secrets.JoinSecret = null; + usersCurrentlyInLobby = 0; + } + // update user information if (privacyMode.Value == DiscordRichPresenceMode.Limited) presence.Assets.LargeImageText = string.Empty; From c71daba4f663164a8180561bbadc8e0ee6f78a46 Mon Sep 17 00:00:00 2001 From: jvyden Date: Tue, 19 Mar 2024 16:05:13 -0400 Subject: [PATCH 0382/2493] Improve logging of RPC Co-authored-by: Salman Ahmed --- osu.Desktop/DiscordRichPresence.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Desktop/DiscordRichPresence.cs b/osu.Desktop/DiscordRichPresence.cs index 633b6324b7..f47b2eaba5 100644 --- a/osu.Desktop/DiscordRichPresence.cs +++ b/osu.Desktop/DiscordRichPresence.cs @@ -117,8 +117,6 @@ namespace osu.Desktop return; } - Logger.Log("Updating Discord RPC", LoggingTarget.Network, LogLevel.Debug); - bool hideIdentifiableInformation = privacyMode.Value == DiscordRichPresenceMode.Limited || status.Value == UserStatus.DoNotDisturb; if (activity.Value != null) @@ -181,6 +179,8 @@ namespace osu.Desktop usersCurrentlyInLobby = 0; } + Logger.Log($"Updating Discord RPC presence with activity status: {presence.State}, details: {presence.Details}", LoggingTarget.Network, LogLevel.Debug); + // update user information if (privacyMode.Value == DiscordRichPresenceMode.Limited) presence.Assets.LargeImageText = string.Empty; From 4305c3db5b70b70d1e28ba1deeb807b28742481e Mon Sep 17 00:00:00 2001 From: jvyden Date: Tue, 19 Mar 2024 16:15:22 -0400 Subject: [PATCH 0383/2493] Show login overlay when joining room while not logged in --- osu.Desktop/DiscordRichPresence.cs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/osu.Desktop/DiscordRichPresence.cs b/osu.Desktop/DiscordRichPresence.cs index f47b2eaba5..a2d7ace0e0 100644 --- a/osu.Desktop/DiscordRichPresence.cs +++ b/osu.Desktop/DiscordRichPresence.cs @@ -19,6 +19,7 @@ using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; +using osu.Game.Overlays; using osu.Game.Rulesets; using osu.Game.Users; using LogLevel = osu.Framework.Logging.LogLevel; @@ -42,6 +43,8 @@ namespace osu.Desktop [Resolved] private OsuGame game { get; set; } = null!; + private LoginOverlay? login { get; set; } + [Resolved] private MultiplayerClient multiplayerClient { get; set; } = null!; @@ -65,6 +68,8 @@ namespace osu.Desktop [BackgroundDependencyLoader] private void load(OsuConfigManager config) { + login = game.Dependencies.Get(); + client = new DiscordRpcClient(client_id) { SkipIdenticalPresence = false // handles better on discord IPC loss, see updateStatus call in onReady. @@ -203,6 +208,12 @@ namespace osu.Desktop { game.Window?.Raise(); + if (!api.IsLoggedIn) + { + Schedule(() => login?.Show()); + return; + } + Logger.Log($"Received room secret from Discord RPC Client: \"{args.Secret}\"", LoggingTarget.Network, LogLevel.Debug); // Stable and lazer share the same Discord client ID, meaning they can accept join requests from each other. From 1a08dbaa2ba929c26e3463163f3c8ac9523809c5 Mon Sep 17 00:00:00 2001 From: jvyden Date: Tue, 19 Mar 2024 17:03:30 -0400 Subject: [PATCH 0384/2493] Fix code style --- osu.Desktop/DiscordRichPresence.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Desktop/DiscordRichPresence.cs b/osu.Desktop/DiscordRichPresence.cs index a2d7ace0e0..d8013aabfe 100644 --- a/osu.Desktop/DiscordRichPresence.cs +++ b/osu.Desktop/DiscordRichPresence.cs @@ -53,7 +53,7 @@ namespace osu.Desktop private readonly Bindable privacyMode = new Bindable(); - private int usersCurrentlyInLobby = 0; + private int usersCurrentlyInLobby; private readonly RichPresence presence = new RichPresence { From d83a53fc944eea90ad8244e248ec762e6d6d6ad3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 20 Mar 2024 12:10:05 +0800 Subject: [PATCH 0385/2493] Remove unused `ScreenBreadcrumbControl` See https://github.com/ppy/osu-framework/pull/6218#discussion_r1529932798. --- .../TestSceneScreenBreadcrumbControl.cs | 138 ------------------ .../UserInterface/ScreenBreadcrumbControl.cs | 48 ------ 2 files changed, 186 deletions(-) delete mode 100644 osu.Game.Tests/Visual/UserInterface/TestSceneScreenBreadcrumbControl.cs delete mode 100644 osu.Game/Graphics/UserInterface/ScreenBreadcrumbControl.cs diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneScreenBreadcrumbControl.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneScreenBreadcrumbControl.cs deleted file mode 100644 index 968cf9f9db..0000000000 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneScreenBreadcrumbControl.cs +++ /dev/null @@ -1,138 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using NUnit.Framework; -using osu.Framework.Allocation; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Screens; -using osu.Game.Graphics; -using osu.Game.Graphics.Sprites; -using osu.Game.Graphics.UserInterface; -using osu.Game.Graphics.UserInterfaceV2; -using osu.Game.Screens; -using osuTK; - -namespace osu.Game.Tests.Visual.UserInterface -{ - [TestFixture] - public partial class TestSceneScreenBreadcrumbControl : OsuTestScene - { - private readonly ScreenBreadcrumbControl breadcrumbs; - private readonly OsuScreenStack screenStack; - - public TestSceneScreenBreadcrumbControl() - { - OsuSpriteText titleText; - - IScreen startScreen = new TestScreenOne(); - - screenStack = new OsuScreenStack { RelativeSizeAxes = Axes.Both }; - screenStack.Push(startScreen); - - Children = new Drawable[] - { - screenStack, - new FillFlowContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Direction = FillDirection.Vertical, - Spacing = new Vector2(10), - Children = new Drawable[] - { - breadcrumbs = new ScreenBreadcrumbControl(screenStack) - { - RelativeSizeAxes = Axes.X, - }, - titleText = new OsuSpriteText(), - }, - }, - }; - - breadcrumbs.Current.ValueChanged += screen => titleText.Text = $"Changed to {screen.NewValue}"; - breadcrumbs.Current.TriggerChange(); - - waitForCurrent(); - pushNext(); - waitForCurrent(); - pushNext(); - waitForCurrent(); - - AddStep(@"make start current", () => startScreen.MakeCurrent()); - - waitForCurrent(); - pushNext(); - waitForCurrent(); - AddAssert(@"only 2 items", () => breadcrumbs.Items.Count == 2); - AddStep(@"exit current", () => screenStack.CurrentScreen.Exit()); - AddAssert(@"current screen is first", () => startScreen == screenStack.CurrentScreen); - } - - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { - breadcrumbs.StripColour = colours.Blue; - } - - private void pushNext() => AddStep(@"push next screen", () => ((TestScreen)screenStack.CurrentScreen).PushNext()); - private void waitForCurrent() => AddUntilStep("current screen", () => screenStack.CurrentScreen.IsCurrentScreen()); - - private abstract partial class TestScreen : OsuScreen - { - protected abstract string NextTitle { get; } - protected abstract TestScreen CreateNextScreen(); - - public TestScreen PushNext() - { - TestScreen screen = CreateNextScreen(); - this.Push(screen); - - return screen; - } - - protected TestScreen() - { - InternalChild = new FillFlowContainer - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Vertical, - Spacing = new Vector2(10), - Children = new Drawable[] - { - new OsuSpriteText - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - Text = Title, - }, - new RoundedButton - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - Width = 100, - Text = $"Push {NextTitle}", - Action = () => PushNext(), - }, - }, - }; - } - } - - private partial class TestScreenOne : TestScreen - { - public override string Title => @"Screen One"; - protected override string NextTitle => @"Two"; - protected override TestScreen CreateNextScreen() => new TestScreenTwo(); - } - - private partial class TestScreenTwo : TestScreen - { - public override string Title => @"Screen Two"; - protected override string NextTitle => @"One"; - protected override TestScreen CreateNextScreen() => new TestScreenOne(); - } - } -} diff --git a/osu.Game/Graphics/UserInterface/ScreenBreadcrumbControl.cs b/osu.Game/Graphics/UserInterface/ScreenBreadcrumbControl.cs deleted file mode 100644 index 65dce422d6..0000000000 --- a/osu.Game/Graphics/UserInterface/ScreenBreadcrumbControl.cs +++ /dev/null @@ -1,48 +0,0 @@ -// 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.Linq; -using osu.Framework.Extensions.IEnumerableExtensions; -using osu.Framework.Graphics.UserInterface; -using osu.Framework.Screens; - -namespace osu.Game.Graphics.UserInterface -{ - /// - /// A which follows the active screen (and allows navigation) in a stack. - /// - public partial class ScreenBreadcrumbControl : BreadcrumbControl - { - public ScreenBreadcrumbControl(ScreenStack stack) - { - stack.ScreenPushed += onPushed; - stack.ScreenExited += onExited; - - if (stack.CurrentScreen != null) - onPushed(null, stack.CurrentScreen); - } - - protected override void SelectTab(TabItem tab) - { - // override base method to prevent current item from being changed on click. - // depend on screen push/exit to change current item instead. - tab.Value.MakeCurrent(); - } - - private void onPushed(IScreen lastScreen, IScreen newScreen) - { - AddItem(newScreen); - Current.Value = newScreen; - } - - private void onExited(IScreen lastScreen, IScreen newScreen) - { - if (newScreen != null) - Current.Value = newScreen; - - Items.ToList().SkipWhile(s => s != Current.Value).Skip(1).ForEach(RemoveItem); - } - } -} From b11ae1c5714b43d33ebbec569faf6569d0304c25 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 20 Mar 2024 06:40:18 +0300 Subject: [PATCH 0386/2493] Organise code, still hook up to `RoomChanged` to update room privacy mode, and use `SkipIdenticalPresence` + scheduling to avoid potential rate-limits --- osu.Desktop/DiscordRichPresence.cs | 87 ++++++++++++++++++------------ 1 file changed, 54 insertions(+), 33 deletions(-) diff --git a/osu.Desktop/DiscordRichPresence.cs b/osu.Desktop/DiscordRichPresence.cs index d8013aabfe..8e4af5c5b1 100644 --- a/osu.Desktop/DiscordRichPresence.cs +++ b/osu.Desktop/DiscordRichPresence.cs @@ -2,16 +2,16 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Diagnostics; using System.Text; using DiscordRPC; using DiscordRPC.Message; using Newtonsoft.Json; using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Framework.Development; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Logging; +using osu.Framework.Threading; using osu.Game; using osu.Game.Configuration; using osu.Game.Extensions; @@ -53,8 +53,6 @@ namespace osu.Desktop private readonly Bindable privacyMode = new Bindable(); - private int usersCurrentlyInLobby; - private readonly RichPresence presence = new RichPresence { Assets = new Assets { LargeImageKey = "osu_logo_lazer" }, @@ -72,7 +70,9 @@ namespace osu.Desktop client = new DiscordRpcClient(client_id) { - SkipIdenticalPresence = false // handles better on discord IPC loss, see updateStatus call in onReady. + // SkipIdenticalPresence allows us to fire SetPresence at any point and leave it to the underlying implementation + // to check whether a difference has actually occurred before sending a command to Discord (with a minor caveat that's handled in onReady). + SkipIdenticalPresence = true }; client.OnReady += onReady; @@ -95,10 +95,11 @@ namespace osu.Desktop activity.BindTo(u.NewValue.Activity); }, true); - ruleset.BindValueChanged(_ => updateStatus()); - status.BindValueChanged(_ => updateStatus()); - activity.BindValueChanged(_ => updateStatus()); - privacyMode.BindValueChanged(_ => updateStatus()); + ruleset.BindValueChanged(_ => updatePresence()); + status.BindValueChanged(_ => updatePresence()); + activity.BindValueChanged(_ => updatePresence()); + privacyMode.BindValueChanged(_ => updatePresence()); + multiplayerClient.RoomUpdated += onRoomUpdated; client.Initialize(); } @@ -106,24 +107,44 @@ namespace osu.Desktop private void onReady(object _, ReadyMessage __) { Logger.Log("Discord RPC Client ready.", LoggingTarget.Network, LogLevel.Debug); - Schedule(updateStatus); + + // when RPC is lost and reconnected, we have to clear presence state for updatePresence to work (see DiscordRpcClient.SkipIdenticalPresence). + if (client.CurrentPresence != null) + client.SetPresence(null); + + updatePresence(); } - private void updateStatus() + private void onRoomUpdated() => updatePresence(); + + private ScheduledDelegate? presenceUpdateDelegate; + + private void updatePresence() { - Debug.Assert(ThreadSafety.IsUpdateThread); - - if (!client.IsInitialized) - return; - - if (status.Value == UserStatus.Offline || privacyMode.Value == DiscordRichPresenceMode.Off) + presenceUpdateDelegate?.Cancel(); + presenceUpdateDelegate = Scheduler.AddDelayed(() => { - client.ClearPresence(); - return; - } + if (!client.IsInitialized) + return; - bool hideIdentifiableInformation = privacyMode.Value == DiscordRichPresenceMode.Limited || status.Value == UserStatus.DoNotDisturb; + if (status.Value == UserStatus.Offline || privacyMode.Value == DiscordRichPresenceMode.Off) + { + client.ClearPresence(); + return; + } + bool hideIdentifiableInformation = privacyMode.Value == DiscordRichPresenceMode.Limited || status.Value == UserStatus.DoNotDisturb; + + updatePresenceStatus(hideIdentifiableInformation); + updatePresenceParty(hideIdentifiableInformation); + updatePresenceAssets(); + + client.SetPresence(presence); + }, 200); + } + + private void updatePresenceStatus(bool hideIdentifiableInformation) + { if (activity.Value != null) { presence.State = truncate(activity.Value.GetStatus(hideIdentifiableInformation)); @@ -150,14 +171,14 @@ namespace osu.Desktop presence.State = "Idle"; presence.Details = string.Empty; } + } + private void updatePresenceParty(bool hideIdentifiableInformation) + { if (!hideIdentifiableInformation && multiplayerClient.Room != null) { MultiplayerRoom room = multiplayerClient.Room; - if (room.Users.Count == usersCurrentlyInLobby) - return; - presence.Party = new Party { Privacy = string.IsNullOrEmpty(room.Settings.Password) ? Party.PrivacySetting.Public : Party.PrivacySetting.Private, @@ -175,17 +196,16 @@ namespace osu.Desktop }; presence.Secrets.JoinSecret = JsonConvert.SerializeObject(roomSecret); - usersCurrentlyInLobby = room.Users.Count; } else { presence.Party = null; presence.Secrets.JoinSecret = null; - usersCurrentlyInLobby = 0; } + } - Logger.Log($"Updating Discord RPC presence with activity status: {presence.State}, details: {presence.Details}", LoggingTarget.Network, LogLevel.Debug); - + private void updatePresenceAssets() + { // update user information if (privacyMode.Value == DiscordRichPresenceMode.Limited) presence.Assets.LargeImageText = string.Empty; @@ -200,17 +220,15 @@ namespace osu.Desktop // update ruleset presence.Assets.SmallImageKey = ruleset.Value.IsLegacyRuleset() ? $"mode_{ruleset.Value.OnlineID}" : "mode_custom"; presence.Assets.SmallImageText = ruleset.Value.Name; - - client.SetPresence(presence); } - private void onJoin(object sender, JoinMessage args) + private void onJoin(object sender, JoinMessage args) => Scheduler.AddOnce(() => { game.Window?.Raise(); if (!api.IsLoggedIn) { - Schedule(() => login?.Show()); + login?.Show(); return; } @@ -231,7 +249,7 @@ namespace osu.Desktop }); request.Failure += _ => Logger.Log($"Could not join multiplayer room, room could not be found (room ID: {roomId}).", LoggingTarget.Network, LogLevel.Important); api.Queue(request); - } + }); private static readonly int ellipsis_length = Encoding.UTF8.GetByteCount(new[] { '…' }); @@ -294,6 +312,9 @@ namespace osu.Desktop protected override void Dispose(bool isDisposing) { + if (multiplayerClient.IsNotNull()) + multiplayerClient.RoomUpdated -= onRoomUpdated; + client.Dispose(); base.Dispose(isDisposing); } From 5f86b5a2fa2b109ec63d283a84356dc46efb67ac Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 20 Mar 2024 07:36:15 +0300 Subject: [PATCH 0387/2493] Use DI correctly --- osu.Desktop/DiscordRichPresence.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Desktop/DiscordRichPresence.cs b/osu.Desktop/DiscordRichPresence.cs index 8e4af5c5b1..d78459ff28 100644 --- a/osu.Desktop/DiscordRichPresence.cs +++ b/osu.Desktop/DiscordRichPresence.cs @@ -43,6 +43,7 @@ namespace osu.Desktop [Resolved] private OsuGame game { get; set; } = null!; + [Resolved] private LoginOverlay? login { get; set; } [Resolved] @@ -66,8 +67,6 @@ namespace osu.Desktop [BackgroundDependencyLoader] private void load(OsuConfigManager config) { - login = game.Dependencies.Get(); - client = new DiscordRpcClient(client_id) { // SkipIdenticalPresence allows us to fire SetPresence at any point and leave it to the underlying implementation From fd509c82f50a59293fee0978769bcaa02c28f852 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 20 Mar 2024 12:52:54 +0800 Subject: [PATCH 0388/2493] Adjust code structure slightly --- .../Timeline/TimelineControlPointDisplay.cs | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointDisplay.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointDisplay.cs index 8e522fa715..116a3ee105 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointDisplay.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointDisplay.cs @@ -32,10 +32,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline controlPointGroups.UnbindAll(); controlPointGroups.BindTo(beatmap.ControlPointInfo.Groups); - controlPointGroups.BindCollectionChanged((_, _) => - { - invalidateGroups(); - }, true); + controlPointGroups.BindCollectionChanged((_, _) => groupCache.Invalidate(), true); } protected override void Update() @@ -51,19 +48,20 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline if (visibleRange != newRange) { visibleRange = newRange; - invalidateGroups(); + groupCache.Invalidate(); } if (!groupCache.IsValid) + { recreateDrawableGroups(); + groupCache.Validate(); + } } - private void invalidateGroups() => groupCache.Invalidate(); - private void recreateDrawableGroups() { // Remove groups outside the visible range - foreach (var drawableGroup in this) + foreach (TimelineControlPointGroup drawableGroup in this) { if (!shouldBeVisible(drawableGroup.Group)) drawableGroup.Expire(); @@ -93,8 +91,6 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline Add(new TimelineControlPointGroup(group)); } - - groupCache.Validate(); } private bool shouldBeVisible(ControlPointGroup group) => group.Time >= visibleRange.min && group.Time <= visibleRange.max; From 1f343b75454c416b8c17d98428330096213c7fc6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 20 Mar 2024 08:27:49 +0100 Subject: [PATCH 0389/2493] Add extended logging when discarding online metadata lookup results Related to: https://github.com/ppy/osu/issues/27674 Relevant log output for that particular case: [network] 2024-03-20 07:25:30 [verbose]: Performing request osu.Game.Online.API.Requests.GetBeatmapRequest [network] 2024-03-20 07:25:30 [verbose]: Request to https://dev.ppy.sh/api/v2/beatmaps/lookup successfully completed! [network] 2024-03-20 07:25:30 [verbose]: GetBeatmapRequest finished with response size of 3,170 bytes [database] 2024-03-20 07:25:30 [verbose]: [4fe02] [APIBeatmapMetadataSource] Online retrieval mapped Tsukiyama Sae - Hana Saku Iro wa Koi no Gotoshi (Log Off Now) [Destiny] to 744883 / 1613507. [database] 2024-03-20 07:25:30 [verbose]: Discarding metadata lookup result due to mismatching online ID (expected: 1570982 actual: 1613507) [network] 2024-03-20 07:25:30 [verbose]: Performing request osu.Game.Online.API.Requests.GetBeatmapRequest [network] 2024-03-20 07:25:30 [verbose]: Request to https://dev.ppy.sh/api/v2/beatmaps/lookup successfully completed! [network] 2024-03-20 07:25:30 [verbose]: GetBeatmapRequest finished with response size of 2,924 bytes [database] 2024-03-20 07:25:30 [verbose]: [4fe02] [APIBeatmapMetadataSource] Online retrieval mapped Tsukiyama Sae - Hana Saku Iro wa Koi no Gotoshi (Log Off Now) [Easy] to 744883 / 1570982. [database] 2024-03-20 07:25:30 [verbose]: Discarding metadata lookup result due to mismatching online ID (expected: 1613507 actual: 1570982) Note that the online IDs are swapped. --- osu.Game/Beatmaps/BeatmapUpdaterMetadataLookup.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/osu.Game/Beatmaps/BeatmapUpdaterMetadataLookup.cs b/osu.Game/Beatmaps/BeatmapUpdaterMetadataLookup.cs index f395718a93..034ec31ee4 100644 --- a/osu.Game/Beatmaps/BeatmapUpdaterMetadataLookup.cs +++ b/osu.Game/Beatmaps/BeatmapUpdaterMetadataLookup.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using osu.Framework.Logging; using osu.Framework.Platform; using osu.Game.Online.API; @@ -85,10 +86,16 @@ namespace osu.Game.Beatmaps private bool shouldDiscardLookupResult(OnlineBeatmapMetadata result, BeatmapInfo beatmapInfo) { if (beatmapInfo.OnlineID > 0 && result.BeatmapID != beatmapInfo.OnlineID) + { + Logger.Log($"Discarding metadata lookup result due to mismatching online ID (expected: {beatmapInfo.OnlineID} actual: {result.BeatmapID})", LoggingTarget.Database); return true; + } if (beatmapInfo.OnlineID == -1 && result.MD5Hash != beatmapInfo.MD5Hash) + { + Logger.Log($"Discarding metadata lookup result due to mismatching hash (expected: {beatmapInfo.MD5Hash} actual: {result.MD5Hash})", LoggingTarget.Database); return true; + } return false; } From c78e203df5434495a79589559b973917a4088a3e Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 20 Mar 2024 17:30:34 +0900 Subject: [PATCH 0390/2493] Fix infinite health processor loop when no top-level objects This is unlikely to occur in actual gameplay, but occurs in the follow-up test. --- osu.Game/Rulesets/Scoring/LegacyDrainingHealthProcessor.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Rulesets/Scoring/LegacyDrainingHealthProcessor.cs b/osu.Game/Rulesets/Scoring/LegacyDrainingHealthProcessor.cs index 2bc3ea80ec..7cee5ebecf 100644 --- a/osu.Game/Rulesets/Scoring/LegacyDrainingHealthProcessor.cs +++ b/osu.Game/Rulesets/Scoring/LegacyDrainingHealthProcessor.cs @@ -108,6 +108,9 @@ namespace osu.Game.Rulesets.Scoring increaseHp(h); } + if (topLevelObjectCount == 0) + return testDrop; + if (!fail && currentHp < lowestHpEnd) { fail = true; From 66ace02e5872bd900acdaf7682a9178eea7dc168 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 20 Mar 2024 17:31:11 +0900 Subject: [PATCH 0391/2493] Add test for banana shower fail --- osu.Game.Rulesets.Catch.Tests/CatchHealthProcessorTest.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game.Rulesets.Catch.Tests/CatchHealthProcessorTest.cs b/osu.Game.Rulesets.Catch.Tests/CatchHealthProcessorTest.cs index d0a8ce4bbc..1b46be01fb 100644 --- a/osu.Game.Rulesets.Catch.Tests/CatchHealthProcessorTest.cs +++ b/osu.Game.Rulesets.Catch.Tests/CatchHealthProcessorTest.cs @@ -19,6 +19,7 @@ namespace osu.Game.Rulesets.Catch.Tests [new Droplet(), 0.01, true], [new TinyDroplet(), 0, false], [new Banana(), 0, false], + [new BananaShower(), 0, false] ]; [TestCaseSource(nameof(test_cases))] From bf5640049a73e2eb01c5cc02814aaea43d7c00e5 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 20 Mar 2024 17:31:31 +0900 Subject: [PATCH 0392/2493] Fix banana showers causing fails when hp is at 0 --- osu.Game.Rulesets.Catch/Scoring/CatchHealthProcessor.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game.Rulesets.Catch/Scoring/CatchHealthProcessor.cs b/osu.Game.Rulesets.Catch/Scoring/CatchHealthProcessor.cs index 2f55f9a85f..b2509091fe 100644 --- a/osu.Game.Rulesets.Catch/Scoring/CatchHealthProcessor.cs +++ b/osu.Game.Rulesets.Catch/Scoring/CatchHealthProcessor.cs @@ -32,6 +32,10 @@ namespace osu.Game.Rulesets.Catch.Scoring if (result.Type == HitResult.SmallTickMiss) return false; + // on stable, banana showers don't exist as concrete objects themselves, so they can't cause a fail. + if (result.HitObject is BananaShower) + return false; + return base.CheckDefaultFailCondition(result); } From 4904c49c38b418f3a151c34c114be96983bb03b7 Mon Sep 17 00:00:00 2001 From: Arthur Araujo Date: Wed, 20 Mar 2024 09:31:58 -0300 Subject: [PATCH 0393/2493] Add abnormal difficulty settings checks --- .../CheckAbnormalDifficultySettingsTest.cs | 93 +++++++++++++++++++ osu.Game/Rulesets/Edit/BeatmapVerifier.cs | 5 +- .../Checks/CheckAbnormalDifficultySettings.cs | 82 ++++++++++++++++ 3 files changed, 179 insertions(+), 1 deletion(-) create mode 100644 osu.Game.Tests/Editing/Checks/CheckAbnormalDifficultySettingsTest.cs create mode 100644 osu.Game/Rulesets/Edit/Checks/CheckAbnormalDifficultySettings.cs diff --git a/osu.Game.Tests/Editing/Checks/CheckAbnormalDifficultySettingsTest.cs b/osu.Game.Tests/Editing/Checks/CheckAbnormalDifficultySettingsTest.cs new file mode 100644 index 0000000000..94305755c4 --- /dev/null +++ b/osu.Game.Tests/Editing/Checks/CheckAbnormalDifficultySettingsTest.cs @@ -0,0 +1,93 @@ +// 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.Edit.Checks; +using NUnit.Framework; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Edit; +using osu.Game.Tests.Beatmaps; +using System.Linq; + +namespace osu.Game.Tests.Editing.Checks +{ + [TestFixture] + public class CheckAbnormalDifficultySettingsTest + { + private CheckAbnormalDifficultySettings check = null!; + + private IBeatmap beatmap = new Beatmap(); + + [SetUp] + public void Setup() + { + check = new CheckAbnormalDifficultySettings(); + beatmap.Difficulty = new(); + } + + [Test] + public void TestSettingsNormal() + { + var context = getContext(); + var issues = check.Run(context).ToList(); + + Assert.That(issues, Has.Count.EqualTo(0)); + } + + [Test] + public void TestAllSettingsMoreThanOneDecimal() + { + beatmap.Difficulty = new() + { + ApproachRate = 5.55f, + OverallDifficulty = 7.7777f, + CircleSize = 4.444f, + DrainRate = 1.1111111111f, + }; + + var context = getContext(); + var issues = check.Run(context).ToList(); + + Assert.That(issues, Has.Count.EqualTo(4)); + } + + [Test] + public void TestAllSettingsLessThanZero() + { + beatmap.Difficulty = new() + { + ApproachRate = -1, + OverallDifficulty = -20, + CircleSize = -11, + DrainRate = -34, + }; + + var context = getContext(); + var issues = check.Run(context).ToList(); + + Assert.That(issues, Has.Count.EqualTo(4)); + } + + [Test] + public void TestAllSettingsHigherThanTen() + { + beatmap.Difficulty = new() + { + ApproachRate = 14, + OverallDifficulty = 24, + CircleSize = 30, + DrainRate = 90, + }; + + var context = getContext(); + var issues = check.Run(context).ToList(); + + Assert.That(issues, Has.Count.EqualTo(4)); + } + + private BeatmapVerifierContext getContext() + { + return new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap)); + } + } +} diff --git a/osu.Game/Rulesets/Edit/BeatmapVerifier.cs b/osu.Game/Rulesets/Edit/BeatmapVerifier.cs index dcf5eb4da9..edabe941b7 100644 --- a/osu.Game/Rulesets/Edit/BeatmapVerifier.cs +++ b/osu.Game/Rulesets/Edit/BeatmapVerifier.cs @@ -41,7 +41,10 @@ namespace osu.Game.Rulesets.Edit new CheckPreviewTime(), // Events - new CheckBreaks() + new CheckBreaks(), + + // Settings + new CheckAbnormalDifficultySettings(), }; public IEnumerable Run(BeatmapVerifierContext context) diff --git a/osu.Game/Rulesets/Edit/Checks/CheckAbnormalDifficultySettings.cs b/osu.Game/Rulesets/Edit/Checks/CheckAbnormalDifficultySettings.cs new file mode 100644 index 0000000000..73049b323d --- /dev/null +++ b/osu.Game/Rulesets/Edit/Checks/CheckAbnormalDifficultySettings.cs @@ -0,0 +1,82 @@ +// 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.Rulesets.Edit.Checks.Components; + + +namespace osu.Game.Rulesets.Edit.Checks +{ + public class CheckAbnormalDifficultySettings : ICheck + { + public CheckMetadata Metadata => new CheckMetadata(CheckCategory.Settings, "Abnormal difficulty settings"); + + public IEnumerable PossibleTemplates => new IssueTemplate[] + { + new IssueTemplateMoreThanOneDecimal(this), + new IssueTemplateOutOfRange(this), + }; + + public IEnumerable Run(BeatmapVerifierContext context) + { + var diff = context.Beatmap.Difficulty; + + if (hasMoreThanOneDecimalPlace(diff.ApproachRate)) + yield return new IssueTemplateMoreThanOneDecimal(this).Create("Approach rate", diff.ApproachRate); + + if (isOutOfRange(diff.ApproachRate)) + yield return new IssueTemplateOutOfRange(this).Create("Approach rate", diff.ApproachRate); + + + if (hasMoreThanOneDecimalPlace(diff.OverallDifficulty)) + yield return new IssueTemplateMoreThanOneDecimal(this).Create("Overall difficulty", diff.OverallDifficulty); + + if (isOutOfRange(diff.OverallDifficulty)) + yield return new IssueTemplateOutOfRange(this).Create("Overall difficulty", diff.OverallDifficulty); + + + if (hasMoreThanOneDecimalPlace(diff.CircleSize)) + yield return new IssueTemplateMoreThanOneDecimal(this).Create("Circle size", diff.CircleSize); + + if (isOutOfRange(diff.CircleSize)) + yield return new IssueTemplateOutOfRange(this).Create("Circle size", diff.CircleSize); + + + if (hasMoreThanOneDecimalPlace(diff.DrainRate)) + yield return new IssueTemplateMoreThanOneDecimal(this).Create("Drain rate", diff.DrainRate); + + if (isOutOfRange(diff.DrainRate)) + yield return new IssueTemplateOutOfRange(this).Create("Drain rate", diff.DrainRate); + } + + private bool isOutOfRange(float setting) + { + return setting < 0f || setting > 10f; + } + + private bool hasMoreThanOneDecimalPlace(float setting) + { + return float.Round(setting, 1) != setting; + } + + public class IssueTemplateMoreThanOneDecimal : IssueTemplate + { + public IssueTemplateMoreThanOneDecimal(ICheck check) + : base(check, IssueType.Problem, "{0} {1} has more than one decimal place.") + { + } + + public Issue Create(string settingName, float settingValue) => new Issue(this, settingName, settingValue); + } + + public class IssueTemplateOutOfRange : IssueTemplate + { + public IssueTemplateOutOfRange(ICheck check) + : base(check, IssueType.Warning, "{0} is {1} although it is capped between 0 to 10 in-game.") + { + } + + public Issue Create(string settingName, float settingValue) => new Issue(this, settingName, settingValue); + } + } +} From ac7fca10d63e3f92db65572ea77ae0ab4bb58df6 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 21 Mar 2024 00:47:37 +0900 Subject: [PATCH 0394/2493] Warn about not using an official "deployed" build --- osu.Game/Localisation/NotificationsStrings.cs | 5 +++++ osu.Game/Updater/UpdateManager.cs | 6 ++++++ osu.Game/Utils/OfficialBuildAttribute.cs | 12 ++++++++++++ 3 files changed, 23 insertions(+) create mode 100644 osu.Game/Utils/OfficialBuildAttribute.cs diff --git a/osu.Game/Localisation/NotificationsStrings.cs b/osu.Game/Localisation/NotificationsStrings.cs index f4965e4ebe..5328bcd066 100644 --- a/osu.Game/Localisation/NotificationsStrings.cs +++ b/osu.Game/Localisation/NotificationsStrings.cs @@ -125,6 +125,11 @@ Click to see what's new!", version); /// public static LocalisableString UpdateReadyToInstall => new TranslatableString(getKey(@"update_ready_to_install"), @"Update ready to install. Click to restart!"); + /// + /// "This is not an official build of the game and scores will not be submitted." + /// + public static LocalisableString NotOfficialBuild => new TranslatableString(getKey(@"not_official_build"), @"This is not an official build of the game and scores will not be submitted."); + /// /// "Downloading update..." /// diff --git a/osu.Game/Updater/UpdateManager.cs b/osu.Game/Updater/UpdateManager.cs index 8f13e0f42a..bcb28d8b14 100644 --- a/osu.Game/Updater/UpdateManager.cs +++ b/osu.Game/Updater/UpdateManager.cs @@ -1,7 +1,9 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Reflection; using System.Threading.Tasks; +using osu.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -11,6 +13,7 @@ using osu.Game.Graphics; using osu.Game.Localisation; using osu.Game.Overlays; using osu.Game.Overlays.Notifications; +using osu.Game.Utils; using osuTK; namespace osu.Game.Updater @@ -51,6 +54,9 @@ namespace osu.Game.Updater // only show a notification if we've previously saved a version to the config file (ie. not the first run). if (!string.IsNullOrEmpty(lastVersion)) Notifications.Post(new UpdateCompleteNotification(version)); + + if (RuntimeInfo.EntryAssembly.GetCustomAttribute() == null) + Notifications.Post(new SimpleNotification { Text = NotificationsStrings.NotOfficialBuild }); } // debug / local compilations will reset to a non-release string. diff --git a/osu.Game/Utils/OfficialBuildAttribute.cs b/osu.Game/Utils/OfficialBuildAttribute.cs new file mode 100644 index 0000000000..66c1ef5591 --- /dev/null +++ b/osu.Game/Utils/OfficialBuildAttribute.cs @@ -0,0 +1,12 @@ +// 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 JetBrains.Annotations; + +namespace osu.Game.Utils +{ + [UsedImplicitly] + [AttributeUsage(AttributeTargets.Assembly)] + public class OfficialBuildAttribute : Attribute; +} From c605e463a41388f43a0493d1246a813f1f7d4ab1 Mon Sep 17 00:00:00 2001 From: Arthur Araujo Date: Wed, 20 Mar 2024 15:52:16 -0300 Subject: [PATCH 0395/2493] Add mania keycount check --- .../Editor/Checks/CheckKeyCountTest.cs | 75 +++++++++++++++++++ .../Edit/Checks/CheckKeyCount.cs | 55 ++++++++++++++ .../Edit/ManiaBeatmapVerifier.cs | 24 ++++++ osu.Game.Rulesets.Mania/ManiaRuleset.cs | 2 + .../Checks/CheckAbnormalDifficultySettings.cs | 4 +- 5 files changed, 158 insertions(+), 2 deletions(-) create mode 100644 osu.Game.Rulesets.Mania.Tests/Editor/Checks/CheckKeyCountTest.cs create mode 100644 osu.Game.Rulesets.Mania/Edit/Checks/CheckKeyCount.cs create mode 100644 osu.Game.Rulesets.Mania/Edit/ManiaBeatmapVerifier.cs diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/Checks/CheckKeyCountTest.cs b/osu.Game.Rulesets.Mania.Tests/Editor/Checks/CheckKeyCountTest.cs new file mode 100644 index 0000000000..91361e2a62 --- /dev/null +++ b/osu.Game.Rulesets.Mania.Tests/Editor/Checks/CheckKeyCountTest.cs @@ -0,0 +1,75 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Objects; +using osu.Game.Tests.Beatmaps; +using osu.Game.Rulesets.Mania.Edit.Checks; +using System.Linq; + +namespace osu.Game.Rulesets.Mania.Tests.Editor.Checks +{ + [TestFixture] + public class CheckKeyCountTest + { + private CheckKeyCount check = null!; + + private IBeatmap beatmap = null!; + + [SetUp] + public void Setup() + { + check = new CheckKeyCount(); + + beatmap = new Beatmap() + { + BeatmapInfo = new BeatmapInfo + { + Ruleset = new ManiaRuleset().RulesetInfo + } + }; + } + + [Test] + public void TestKeycountFour() + { + beatmap.Difficulty.CircleSize = 4; + + var context = getContext(); + var issues = check.Run(context).ToList(); + + Assert.That(issues, Has.Count.EqualTo(0)); + } + + [Test] + public void TestKeycountSmallerThanFour() + { + beatmap.Difficulty.CircleSize = 1; + + var context = getContext(); + var issues = check.Run(context).ToList(); + + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues.Single().Template is CheckKeyCount.IssueTemplateKeycountTooLow); + } + + [Test] + public void TestKeycountHigherThanTen() + { + beatmap.Difficulty.CircleSize = 11; + + var context = getContext(); + var issues = check.Run(context).ToList(); + + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues.Single().Template is CheckKeyCount.IssueTemplateKeycountNonStandard); + } + + private BeatmapVerifierContext getContext() + { + return new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap)); + } + } +} diff --git a/osu.Game.Rulesets.Mania/Edit/Checks/CheckKeyCount.cs b/osu.Game.Rulesets.Mania/Edit/Checks/CheckKeyCount.cs new file mode 100644 index 0000000000..57991bd3ff --- /dev/null +++ b/osu.Game.Rulesets.Mania/Edit/Checks/CheckKeyCount.cs @@ -0,0 +1,55 @@ +// 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.Rulesets.Edit; +using osu.Game.Rulesets.Edit.Checks.Components; + +namespace osu.Game.Rulesets.Mania.Edit.Checks +{ + public class CheckKeyCount : ICheck + { + public CheckMetadata Metadata => new CheckMetadata(CheckCategory.Settings, "Check mania keycount."); + + public IEnumerable PossibleTemplates => new IssueTemplate[] + { + new IssueTemplateKeycountTooLow(this), + new IssueTemplateKeycountNonStandard(this), + }; + + public IEnumerable Run(BeatmapVerifierContext context) + { + var diff = context.Beatmap.Difficulty; + + if (diff.CircleSize < 4) + { + yield return new IssueTemplateKeycountTooLow(this).Create(diff.CircleSize); + } + + if (diff.CircleSize > 10) + { + yield return new IssueTemplateKeycountNonStandard(this).Create(diff.CircleSize); + } + } + + public class IssueTemplateKeycountTooLow : IssueTemplate + { + public IssueTemplateKeycountTooLow(ICheck check) + : base(check, IssueType.Problem, "Key count is {0} and must be 4 or higher.") + { + } + + public Issue Create(float current) => new Issue(this, current); + } + + public class IssueTemplateKeycountNonStandard : IssueTemplate + { + public IssueTemplateKeycountNonStandard(ICheck check) + : base(check, IssueType.Warning, "Key count {0} is higher than 10.") + { + } + + public Issue Create(float current) => new Issue(this, current); + } + } +} diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaBeatmapVerifier.cs b/osu.Game.Rulesets.Mania/Edit/ManiaBeatmapVerifier.cs new file mode 100644 index 0000000000..778dcfc6e1 --- /dev/null +++ b/osu.Game.Rulesets.Mania/Edit/ManiaBeatmapVerifier.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.Edit; +using osu.Game.Rulesets.Edit.Checks.Components; +using osu.Game.Rulesets.Mania.Edit.Checks; + +namespace osu.Game.Rulesets.Mania.Edit +{ + public class ManiaBeatmapVerifier : IBeatmapVerifier + { + private readonly List checks = new List + { + new CheckKeyCount(), + }; + + public IEnumerable Run(BeatmapVerifierContext context) + { + return checks.SelectMany(check => check.Run(context)); + } + } +} diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs index 0b54fb3da0..3d4803f1e4 100644 --- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs @@ -65,6 +65,8 @@ namespace osu.Game.Rulesets.Mania public override HitObjectComposer CreateHitObjectComposer() => new ManiaHitObjectComposer(this); + public override IBeatmapVerifier CreateBeatmapVerifier() => new ManiaBeatmapVerifier(); + public override ISkin? CreateSkinTransformer(ISkin skin, IBeatmap beatmap) { switch (skin) diff --git a/osu.Game/Rulesets/Edit/Checks/CheckAbnormalDifficultySettings.cs b/osu.Game/Rulesets/Edit/Checks/CheckAbnormalDifficultySettings.cs index 73049b323d..3c454a57fe 100644 --- a/osu.Game/Rulesets/Edit/Checks/CheckAbnormalDifficultySettings.cs +++ b/osu.Game/Rulesets/Edit/Checks/CheckAbnormalDifficultySettings.cs @@ -4,7 +4,6 @@ using System.Collections.Generic; using osu.Game.Rulesets.Edit.Checks.Components; - namespace osu.Game.Rulesets.Edit.Checks { public class CheckAbnormalDifficultySettings : ICheck @@ -20,6 +19,7 @@ namespace osu.Game.Rulesets.Edit.Checks public IEnumerable Run(BeatmapVerifierContext context) { var diff = context.Beatmap.Difficulty; + string ruleset = context.Beatmap.BeatmapInfo.Ruleset.ShortName; if (hasMoreThanOneDecimalPlace(diff.ApproachRate)) yield return new IssueTemplateMoreThanOneDecimal(this).Create("Approach rate", diff.ApproachRate); @@ -38,7 +38,7 @@ namespace osu.Game.Rulesets.Edit.Checks if (hasMoreThanOneDecimalPlace(diff.CircleSize)) yield return new IssueTemplateMoreThanOneDecimal(this).Create("Circle size", diff.CircleSize); - if (isOutOfRange(diff.CircleSize)) + if (ruleset != "mania" && isOutOfRange(diff.CircleSize)) yield return new IssueTemplateOutOfRange(this).Create("Circle size", diff.CircleSize); From 2d6a3b8e2b5e3f0023957868a0706f5df162e110 Mon Sep 17 00:00:00 2001 From: Arthur Araujo Date: Wed, 20 Mar 2024 16:51:27 -0300 Subject: [PATCH 0396/2493] Remove warning for 10K+ --- .../Editor/Checks/CheckKeyCountTest.cs | 12 ------------ .../Edit/Checks/CheckKeyCount.cs | 16 ---------------- 2 files changed, 28 deletions(-) diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/Checks/CheckKeyCountTest.cs b/osu.Game.Rulesets.Mania.Tests/Editor/Checks/CheckKeyCountTest.cs index 91361e2a62..564c611548 100644 --- a/osu.Game.Rulesets.Mania.Tests/Editor/Checks/CheckKeyCountTest.cs +++ b/osu.Game.Rulesets.Mania.Tests/Editor/Checks/CheckKeyCountTest.cs @@ -55,18 +55,6 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor.Checks Assert.That(issues.Single().Template is CheckKeyCount.IssueTemplateKeycountTooLow); } - [Test] - public void TestKeycountHigherThanTen() - { - beatmap.Difficulty.CircleSize = 11; - - var context = getContext(); - var issues = check.Run(context).ToList(); - - Assert.That(issues, Has.Count.EqualTo(1)); - Assert.That(issues.Single().Template is CheckKeyCount.IssueTemplateKeycountNonStandard); - } - private BeatmapVerifierContext getContext() { return new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap)); diff --git a/osu.Game.Rulesets.Mania/Edit/Checks/CheckKeyCount.cs b/osu.Game.Rulesets.Mania/Edit/Checks/CheckKeyCount.cs index 57991bd3ff..51ead5f423 100644 --- a/osu.Game.Rulesets.Mania/Edit/Checks/CheckKeyCount.cs +++ b/osu.Game.Rulesets.Mania/Edit/Checks/CheckKeyCount.cs @@ -14,7 +14,6 @@ namespace osu.Game.Rulesets.Mania.Edit.Checks public IEnumerable PossibleTemplates => new IssueTemplate[] { new IssueTemplateKeycountTooLow(this), - new IssueTemplateKeycountNonStandard(this), }; public IEnumerable Run(BeatmapVerifierContext context) @@ -25,11 +24,6 @@ namespace osu.Game.Rulesets.Mania.Edit.Checks { yield return new IssueTemplateKeycountTooLow(this).Create(diff.CircleSize); } - - if (diff.CircleSize > 10) - { - yield return new IssueTemplateKeycountNonStandard(this).Create(diff.CircleSize); - } } public class IssueTemplateKeycountTooLow : IssueTemplate @@ -41,15 +35,5 @@ namespace osu.Game.Rulesets.Mania.Edit.Checks public Issue Create(float current) => new Issue(this, current); } - - public class IssueTemplateKeycountNonStandard : IssueTemplate - { - public IssueTemplateKeycountNonStandard(ICheck check) - : base(check, IssueType.Warning, "Key count {0} is higher than 10.") - { - } - - public Issue Create(float current) => new Issue(this, current); - } } } From b132734f9c429da05b976544b266528c765019e6 Mon Sep 17 00:00:00 2001 From: Arthur Araujo Date: Wed, 20 Mar 2024 18:54:38 -0300 Subject: [PATCH 0397/2493] Fix codefactor issues --- .../Rulesets/Edit/Checks/CheckAbnormalDifficultySettings.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/osu.Game/Rulesets/Edit/Checks/CheckAbnormalDifficultySettings.cs b/osu.Game/Rulesets/Edit/Checks/CheckAbnormalDifficultySettings.cs index 3c454a57fe..db154f4efc 100644 --- a/osu.Game/Rulesets/Edit/Checks/CheckAbnormalDifficultySettings.cs +++ b/osu.Game/Rulesets/Edit/Checks/CheckAbnormalDifficultySettings.cs @@ -27,21 +27,18 @@ namespace osu.Game.Rulesets.Edit.Checks if (isOutOfRange(diff.ApproachRate)) yield return new IssueTemplateOutOfRange(this).Create("Approach rate", diff.ApproachRate); - if (hasMoreThanOneDecimalPlace(diff.OverallDifficulty)) yield return new IssueTemplateMoreThanOneDecimal(this).Create("Overall difficulty", diff.OverallDifficulty); if (isOutOfRange(diff.OverallDifficulty)) yield return new IssueTemplateOutOfRange(this).Create("Overall difficulty", diff.OverallDifficulty); - if (hasMoreThanOneDecimalPlace(diff.CircleSize)) yield return new IssueTemplateMoreThanOneDecimal(this).Create("Circle size", diff.CircleSize); if (ruleset != "mania" && isOutOfRange(diff.CircleSize)) yield return new IssueTemplateOutOfRange(this).Create("Circle size", diff.CircleSize); - if (hasMoreThanOneDecimalPlace(diff.DrainRate)) yield return new IssueTemplateMoreThanOneDecimal(this).Create("Drain rate", diff.DrainRate); From a07d5115bfef749e2b766a554ceb420c82206dd0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 21 Mar 2024 11:42:22 +0800 Subject: [PATCH 0398/2493] 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 de7497d58e..0e091dbd37 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -36,7 +36,7 @@ - + From 55d66d4615e65bdbcc794d385da3210aa130d7cf Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 21 Mar 2024 11:45:46 +0800 Subject: [PATCH 0399/2493] Add sounds to countdown --- osu.Game/Screens/Play/DelayedResumeOverlay.cs | 29 +++++++++++++++---- 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/Play/DelayedResumeOverlay.cs b/osu.Game/Screens/Play/DelayedResumeOverlay.cs index 8bb3ae8182..147d48ae02 100644 --- a/osu.Game/Screens/Play/DelayedResumeOverlay.cs +++ b/osu.Game/Screens/Play/DelayedResumeOverlay.cs @@ -3,6 +3,8 @@ using System; using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Audio.Sample; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; @@ -47,6 +49,8 @@ namespace osu.Game.Screens.Play private SpriteText countdownText = null!; private CircularProgress countdownProgress = null!; + private Sample? sampleCountdown; + public DelayedResumeOverlay() { Anchor = Anchor.Centre; @@ -54,7 +58,7 @@ namespace osu.Game.Screens.Play } [BackgroundDependencyLoader] - private void load() + private void load(AudioManager audio) { Add(outerContent = new Circle { @@ -103,6 +107,8 @@ namespace osu.Game.Screens.Play } } }); + + sampleCountdown = audio.Samples.Get(@"Gameplay/resume-countdown"); } protected override void PopIn() @@ -164,13 +170,24 @@ namespace osu.Game.Screens.Play countdownProgress.Progress = amountTimePassed; countdownProgress.InnerRadius = progress_stroke_width / progress_size / countdownProgress.Scale.X; - if (countdownCount != newCount && newCount > 0) + if (countdownCount != newCount) { - countdownText.Text = Math.Max(1, newCount).ToString(); - countdownText.ScaleTo(0.25f).Then().ScaleTo(1, 200, Easing.OutQuint); - outerContent.Delay(25).Then().ScaleTo(1.05f, 100).Then().ScaleTo(1f, 200, Easing.Out); + if (newCount > 0) + { + countdownText.Text = Math.Max(1, newCount).ToString(); + countdownText.ScaleTo(0.25f).Then().ScaleTo(1, 200, Easing.OutQuint); + outerContent.Delay(25).Then().ScaleTo(1.05f, 100).Then().ScaleTo(1f, 200, Easing.Out); - countdownBackground.FlashColour(colourProvider.Background3, 400, Easing.Out); + countdownBackground.FlashColour(colourProvider.Background3, 400, Easing.Out); + } + + var chan = sampleCountdown?.GetChannel(); + + if (chan != null) + { + chan.Frequency.Value = newCount == 0 ? 0.5f : 1; + chan.Play(); + } } countdownCount = newCount; From b99b0337cffd33519b1323adbda98cb13b395f63 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 21 Mar 2024 13:06:25 +0800 Subject: [PATCH 0400/2493] Adjust text slightly --- osu.Game/Localisation/NotificationsStrings.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Localisation/NotificationsStrings.cs b/osu.Game/Localisation/NotificationsStrings.cs index 5328bcd066..3188ca5533 100644 --- a/osu.Game/Localisation/NotificationsStrings.cs +++ b/osu.Game/Localisation/NotificationsStrings.cs @@ -126,9 +126,9 @@ Click to see what's new!", version); public static LocalisableString UpdateReadyToInstall => new TranslatableString(getKey(@"update_ready_to_install"), @"Update ready to install. Click to restart!"); /// - /// "This is not an official build of the game and scores will not be submitted." + /// "This is not an official build of the game. Scores will not be submitted and other online systems may not work as intended." /// - public static LocalisableString NotOfficialBuild => new TranslatableString(getKey(@"not_official_build"), @"This is not an official build of the game and scores will not be submitted."); + public static LocalisableString NotOfficialBuild => new TranslatableString(getKey(@"not_official_build"), @"This is not an official build of the game. Scores will not be submitted and other online systems may not work as intended."); /// /// "Downloading update..." From 5f7028b5741f88e5c68e05cbd878ecc110d32eec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 21 Mar 2024 17:45:56 +0100 Subject: [PATCH 0401/2493] Add failing tests --- .../Formats/LegacyScoreDecoderTest.cs | 89 +++++++++++++++++++ 1 file changed, 89 insertions(+) diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs index 5dae86d9e9..050259c2fa 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs @@ -31,6 +31,7 @@ using osu.Game.Rulesets.Taiko; using osu.Game.Scoring; using osu.Game.Scoring.Legacy; using osu.Game.Tests.Resources; +using osuTK; namespace osu.Game.Tests.Beatmaps.Formats { @@ -178,6 +179,94 @@ namespace osu.Game.Tests.Beatmaps.Formats Assert.That(decodedAfterEncode.Replay.Frames[1].Time, Is.EqualTo(second_frame_time)); } + [Test] + public void TestNegativeFrameSkipped() + { + var ruleset = new OsuRuleset().RulesetInfo; + var scoreInfo = TestResources.CreateTestScoreInfo(ruleset); + var beatmap = new TestBeatmap(ruleset); + + var score = new Score + { + ScoreInfo = scoreInfo, + Replay = new Replay + { + Frames = new List + { + new OsuReplayFrame(0, new Vector2()), + new OsuReplayFrame(1000, OsuPlayfield.BASE_SIZE), + new OsuReplayFrame(500, OsuPlayfield.BASE_SIZE / 2), + new OsuReplayFrame(2000, OsuPlayfield.BASE_SIZE), + } + } + }; + + var decodedAfterEncode = encodeThenDecode(LegacyScoreEncoder.LATEST_VERSION, score, beatmap); + + Assert.That(decodedAfterEncode.Replay.Frames, Has.Count.EqualTo(3)); + Assert.That(decodedAfterEncode.Replay.Frames[0].Time, Is.EqualTo(0)); + Assert.That(decodedAfterEncode.Replay.Frames[1].Time, Is.EqualTo(1000)); + Assert.That(decodedAfterEncode.Replay.Frames[2].Time, Is.EqualTo(2000)); + } + + [Test] + public void FirstTwoFramesSwappedIfInWrongOrder() + { + var ruleset = new OsuRuleset().RulesetInfo; + var scoreInfo = TestResources.CreateTestScoreInfo(ruleset); + var beatmap = new TestBeatmap(ruleset); + + var score = new Score + { + ScoreInfo = scoreInfo, + Replay = new Replay + { + Frames = new List + { + new OsuReplayFrame(100, new Vector2()), + new OsuReplayFrame(50, OsuPlayfield.BASE_SIZE / 2), + new OsuReplayFrame(1000, OsuPlayfield.BASE_SIZE), + } + } + }; + + var decodedAfterEncode = encodeThenDecode(LegacyScoreEncoder.LATEST_VERSION, score, beatmap); + + Assert.That(decodedAfterEncode.Replay.Frames, Has.Count.EqualTo(3)); + Assert.That(decodedAfterEncode.Replay.Frames[0].Time, Is.EqualTo(0)); + Assert.That(decodedAfterEncode.Replay.Frames[1].Time, Is.EqualTo(100)); + Assert.That(decodedAfterEncode.Replay.Frames[2].Time, Is.EqualTo(1000)); + } + + [Test] + public void FirstTwoFramesPulledTowardThirdIfTheyAreAfterIt() + { + var ruleset = new OsuRuleset().RulesetInfo; + var scoreInfo = TestResources.CreateTestScoreInfo(ruleset); + var beatmap = new TestBeatmap(ruleset); + + var score = new Score + { + ScoreInfo = scoreInfo, + Replay = new Replay + { + Frames = new List + { + new OsuReplayFrame(0, new Vector2()), + new OsuReplayFrame(500, OsuPlayfield.BASE_SIZE / 2), + new OsuReplayFrame(-1500, OsuPlayfield.BASE_SIZE), + } + } + }; + + var decodedAfterEncode = encodeThenDecode(LegacyScoreEncoder.LATEST_VERSION, score, beatmap); + + Assert.That(decodedAfterEncode.Replay.Frames, Has.Count.EqualTo(3)); + Assert.That(decodedAfterEncode.Replay.Frames[0].Time, Is.EqualTo(-1500)); + Assert.That(decodedAfterEncode.Replay.Frames[1].Time, Is.EqualTo(-1500)); + Assert.That(decodedAfterEncode.Replay.Frames[2].Time, Is.EqualTo(-1500)); + } + [Test] public void TestCultureInvariance() { From 990a07af0eb7070b2e92ed37c033bf183721e299 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 21 Mar 2024 21:01:51 +0100 Subject: [PATCH 0402/2493] Rewrite handling of legacy replay frame quirks to match stable closer --- osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs | 77 +++++++++---------- 1 file changed, 37 insertions(+), 40 deletions(-) diff --git a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs index b16cdffe82..af514a4b59 100644 --- a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs +++ b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs @@ -21,6 +21,7 @@ using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Replays; using osu.Game.Rulesets.Scoring; +using osuTK; using SharpCompress.Compressors.LZMA; namespace osu.Game.Scoring.Legacy @@ -240,15 +241,7 @@ namespace osu.Game.Scoring.Legacy private void readLegacyReplay(Replay replay, StreamReader reader) { float lastTime = beatmapOffset; - bool negativeFrameEncounted = false; - ReplayFrame currentFrame = null; - - // the negative time amount that must be "paid back" by positive frames before we start including frames again. - // When a negative frame occurs in a replay, all future frames are skipped until the sum total of their times - // is equal to or greater than the time of that negative frame. - // This value will be negative if we are in a time deficit, ie we have a negative frame that must be paid back. - // Otherwise it will be 0. - float timeDeficit = 0; + var legacyFrames = new List(); string[] frames = reader.ReadToEnd().Split(','); @@ -271,40 +264,44 @@ namespace osu.Game.Scoring.Legacy lastTime += diff; - if (i < 2 && mouseX == 256 && mouseY == -500) - // at the start of the replay, stable places two replay frames, at time 0 and SkipBoundary - 1, respectively. - // both frames use a position of (256, -500). - // ignore these frames as they serve no real purpose (and can even mislead ruleset-specific handlers - see mania) - continue; - - // negative frames are only counted towards the deficit after the very beginning of the replay. - // When the two skip frames are present (see directly above), the third frame will have a large - // negative time roughly equal to SkipBoundary. This shouldn't be counted towards the deficit, otherwise - // any replay data before the skip would be, well, skipped. - // - // On testing against stable, it appears that stable ignores the negative time of only the first - // negative frame of the first three replay frames, regardless of if the skip frames are present. - // Hence the condition here. - // But there is a possibility this is incorrect and may need to be revisited later. - if (i > 2 || negativeFrameEncounted) - { - timeDeficit += diff; - timeDeficit = Math.Min(0, timeDeficit); - } - - if (diff < 0) - negativeFrameEncounted = true; - - // still paying back the deficit from a negative frame. Skip this frame. - if (timeDeficit < 0) - continue; - - currentFrame = convertFrame(new LegacyReplayFrame(lastTime, + legacyFrames.Add(new LegacyReplayFrame(lastTime, mouseX, mouseY, - (ReplayButtonState)Parsing.ParseInt(split[3])), currentFrame); + (ReplayButtonState)Parsing.ParseInt(split[3]))); + } - replay.Frames.Add(currentFrame); + // https://github.com/peppy/osu-stable-reference/blob/e53980dd76857ee899f66ce519ba1597e7874f28/osu!/GameModes/Play/ReplayWatcher.cs#L62-L67 + if (legacyFrames.Count >= 2 && legacyFrames[1].Time < legacyFrames[0].Time) + { + legacyFrames[1].Time = legacyFrames[0].Time; + legacyFrames[0].Time = 0; + } + + // https://github.com/peppy/osu-stable-reference/blob/e53980dd76857ee899f66ce519ba1597e7874f28/osu!/GameModes/Play/ReplayWatcher.cs#L69-L71 + if (legacyFrames.Count >= 3 && legacyFrames[0].Time > legacyFrames[2].Time) + legacyFrames[0].Time = legacyFrames[1].Time = legacyFrames[2].Time; + + // at the start of the replay, stable places two replay frames, at time 0 and SkipBoundary - 1, respectively. + // both frames use a position of (256, -500). + // ignore these frames as they serve no real purpose (and can even mislead ruleset-specific handlers - see mania) + if (legacyFrames.Count >= 2 && legacyFrames[1].Position == new Vector2(256, -500)) + legacyFrames.RemoveAt(1); + + if (legacyFrames.Count >= 1 && legacyFrames[0].Position == new Vector2(256, -500)) + legacyFrames.RemoveAt(0); + + ReplayFrame currentFrame = null; + + foreach (var legacyFrame in legacyFrames) + { + // never allow backwards time traversal in relation to the current frame. + // this handles frames with negative delta. + // this doesn't match stable 100% as stable will do something similar to adding an interpolated "intermediate frame" + // at the point wherein time flow changes from backwards to forwards, but it'll do for now. + if (currentFrame != null && legacyFrame.Time < currentFrame.Time) + continue; + + replay.Frames.Add(currentFrame = convertFrame(legacyFrame, currentFrame)); } } From a4822fd615ef983be0399182f98159fc8546e427 Mon Sep 17 00:00:00 2001 From: Arthur Araujo Date: Fri, 22 Mar 2024 01:37:06 -0300 Subject: [PATCH 0403/2493] Make `CheckAbnormalDifficultySettings` abstract --- .../CheckAbnormalDifficultySettingsTest.cs | 93 ------------------- osu.Game/Rulesets/Edit/BeatmapVerifier.cs | 3 - .../Checks/CheckAbnormalDifficultySettings.cs | 41 ++------ 3 files changed, 8 insertions(+), 129 deletions(-) delete mode 100644 osu.Game.Tests/Editing/Checks/CheckAbnormalDifficultySettingsTest.cs diff --git a/osu.Game.Tests/Editing/Checks/CheckAbnormalDifficultySettingsTest.cs b/osu.Game.Tests/Editing/Checks/CheckAbnormalDifficultySettingsTest.cs deleted file mode 100644 index 94305755c4..0000000000 --- a/osu.Game.Tests/Editing/Checks/CheckAbnormalDifficultySettingsTest.cs +++ /dev/null @@ -1,93 +0,0 @@ -// 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.Edit.Checks; -using NUnit.Framework; -using osu.Game.Beatmaps; -using osu.Game.Rulesets.Objects; -using osu.Game.Rulesets.Edit; -using osu.Game.Tests.Beatmaps; -using System.Linq; - -namespace osu.Game.Tests.Editing.Checks -{ - [TestFixture] - public class CheckAbnormalDifficultySettingsTest - { - private CheckAbnormalDifficultySettings check = null!; - - private IBeatmap beatmap = new Beatmap(); - - [SetUp] - public void Setup() - { - check = new CheckAbnormalDifficultySettings(); - beatmap.Difficulty = new(); - } - - [Test] - public void TestSettingsNormal() - { - var context = getContext(); - var issues = check.Run(context).ToList(); - - Assert.That(issues, Has.Count.EqualTo(0)); - } - - [Test] - public void TestAllSettingsMoreThanOneDecimal() - { - beatmap.Difficulty = new() - { - ApproachRate = 5.55f, - OverallDifficulty = 7.7777f, - CircleSize = 4.444f, - DrainRate = 1.1111111111f, - }; - - var context = getContext(); - var issues = check.Run(context).ToList(); - - Assert.That(issues, Has.Count.EqualTo(4)); - } - - [Test] - public void TestAllSettingsLessThanZero() - { - beatmap.Difficulty = new() - { - ApproachRate = -1, - OverallDifficulty = -20, - CircleSize = -11, - DrainRate = -34, - }; - - var context = getContext(); - var issues = check.Run(context).ToList(); - - Assert.That(issues, Has.Count.EqualTo(4)); - } - - [Test] - public void TestAllSettingsHigherThanTen() - { - beatmap.Difficulty = new() - { - ApproachRate = 14, - OverallDifficulty = 24, - CircleSize = 30, - DrainRate = 90, - }; - - var context = getContext(); - var issues = check.Run(context).ToList(); - - Assert.That(issues, Has.Count.EqualTo(4)); - } - - private BeatmapVerifierContext getContext() - { - return new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap)); - } - } -} diff --git a/osu.Game/Rulesets/Edit/BeatmapVerifier.cs b/osu.Game/Rulesets/Edit/BeatmapVerifier.cs index edabe941b7..2eb52e88c7 100644 --- a/osu.Game/Rulesets/Edit/BeatmapVerifier.cs +++ b/osu.Game/Rulesets/Edit/BeatmapVerifier.cs @@ -42,9 +42,6 @@ namespace osu.Game.Rulesets.Edit // Events new CheckBreaks(), - - // Settings - new CheckAbnormalDifficultySettings(), }; public IEnumerable Run(BeatmapVerifierContext context) diff --git a/osu.Game/Rulesets/Edit/Checks/CheckAbnormalDifficultySettings.cs b/osu.Game/Rulesets/Edit/Checks/CheckAbnormalDifficultySettings.cs index db154f4efc..93592a866b 100644 --- a/osu.Game/Rulesets/Edit/Checks/CheckAbnormalDifficultySettings.cs +++ b/osu.Game/Rulesets/Edit/Checks/CheckAbnormalDifficultySettings.cs @@ -6,9 +6,9 @@ using osu.Game.Rulesets.Edit.Checks.Components; namespace osu.Game.Rulesets.Edit.Checks { - public class CheckAbnormalDifficultySettings : ICheck + public abstract class CheckAbnormalDifficultySettings : ICheck { - public CheckMetadata Metadata => new CheckMetadata(CheckCategory.Settings, "Abnormal difficulty settings"); + public abstract CheckMetadata Metadata { get; } public IEnumerable PossibleTemplates => new IssueTemplate[] { @@ -16,42 +16,17 @@ namespace osu.Game.Rulesets.Edit.Checks new IssueTemplateOutOfRange(this), }; - public IEnumerable Run(BeatmapVerifierContext context) - { - var diff = context.Beatmap.Difficulty; - string ruleset = context.Beatmap.BeatmapInfo.Ruleset.ShortName; + public abstract IEnumerable Run(BeatmapVerifierContext context); - if (hasMoreThanOneDecimalPlace(diff.ApproachRate)) - yield return new IssueTemplateMoreThanOneDecimal(this).Create("Approach rate", diff.ApproachRate); - - if (isOutOfRange(diff.ApproachRate)) - yield return new IssueTemplateOutOfRange(this).Create("Approach rate", diff.ApproachRate); - - if (hasMoreThanOneDecimalPlace(diff.OverallDifficulty)) - yield return new IssueTemplateMoreThanOneDecimal(this).Create("Overall difficulty", diff.OverallDifficulty); - - if (isOutOfRange(diff.OverallDifficulty)) - yield return new IssueTemplateOutOfRange(this).Create("Overall difficulty", diff.OverallDifficulty); - - if (hasMoreThanOneDecimalPlace(diff.CircleSize)) - yield return new IssueTemplateMoreThanOneDecimal(this).Create("Circle size", diff.CircleSize); - - if (ruleset != "mania" && isOutOfRange(diff.CircleSize)) - yield return new IssueTemplateOutOfRange(this).Create("Circle size", diff.CircleSize); - - if (hasMoreThanOneDecimalPlace(diff.DrainRate)) - yield return new IssueTemplateMoreThanOneDecimal(this).Create("Drain rate", diff.DrainRate); - - if (isOutOfRange(diff.DrainRate)) - yield return new IssueTemplateOutOfRange(this).Create("Drain rate", diff.DrainRate); - } - - private bool isOutOfRange(float setting) + /// + /// If the setting is out of the boundaries set by the editor (0 - 10) + /// + protected bool OutOfRange(float setting) { return setting < 0f || setting > 10f; } - private bool hasMoreThanOneDecimalPlace(float setting) + protected bool HasMoreThanOneDecimalPlace(float setting) { return float.Round(setting, 1) != setting; } From e2df0981843c3428fa11a232d341d97ac3e0505f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 22 Mar 2024 08:25:03 +0100 Subject: [PATCH 0404/2493] Add failing test case for desired artist sort behaviour --- .../SongSelect/TestSceneBeatmapCarousel.cs | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs index de2ae3708f..c0102b238c 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs @@ -666,6 +666,56 @@ namespace osu.Game.Tests.Visual.SongSelect AddAssert($"Check {zzz_lowercase} is second last", () => carousel.BeatmapSets.SkipLast(1).Last().Metadata.Artist == zzz_lowercase); } + [Test] + public void TestSortByArtistUsesTitleAsTiebreaker() + { + var sets = new List(); + + AddStep("Populuate beatmap sets", () => + { + sets.Clear(); + + for (int i = 0; i < 20; i++) + { + var set = TestResources.CreateTestBeatmapSetInfo(); + + if (i == 4) + { + set.Beatmaps.ForEach(b => + { + b.Metadata.Artist = "ZZZ"; + b.Metadata.Title = "AAA"; + }); + } + + if (i == 8) + { + set.Beatmaps.ForEach(b => + { + b.Metadata.Artist = "ZZZ"; + b.Metadata.Title = "ZZZ"; + }); + } + + sets.Add(set); + } + }); + + loadBeatmaps(sets); + + AddStep("Sort by artist", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Artist }, false)); + AddAssert("Check last item", () => + { + var lastItem = carousel.BeatmapSets.Last(); + return lastItem.Metadata.Artist == "ZZZ" && lastItem.Metadata.Title == "ZZZ"; + }); + AddAssert("Check second last item", () => + { + var secondLastItem = carousel.BeatmapSets.SkipLast(1).Last(); + return secondLastItem.Metadata.Artist == "ZZZ" && secondLastItem.Metadata.Title == "AAA"; + }); + } + /// /// Ensures stability is maintained on different sort modes for items with equal properties. /// From a1880e89c299f55a8907050988b8200e854ed34b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 22 Mar 2024 08:31:59 +0100 Subject: [PATCH 0405/2493] Use title as tiebreaker when sorting beatmap carousel by artist Closes https://github.com/ppy/osu/issues/27548. Reference: https://github.com/peppy/osu-stable-reference/blob/e53980dd76857ee899f66ce519ba1597e7874f28/osu!/GameplayElements/Beatmaps/BeatmapTreeManager.cs#L341-L347 --- osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs b/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs index 43c9c621e8..7e15699804 100644 --- a/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs +++ b/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs @@ -69,6 +69,8 @@ namespace osu.Game.Screens.Select.Carousel default: case SortMode.Artist: comparison = OrdinalSortByCaseStringComparer.DEFAULT.Compare(BeatmapSet.Metadata.Artist, otherSet.BeatmapSet.Metadata.Artist); + if (comparison == 0) + goto case SortMode.Title; break; case SortMode.Title: From 6fa663c8cac732c6d705955f52565f1f78b7eddd Mon Sep 17 00:00:00 2001 From: Arthur Araujo Date: Fri, 22 Mar 2024 14:48:22 -0300 Subject: [PATCH 0406/2493] Make check ruleset specific --- ...heckCatchAbnormalDifficultySettingsTest.cs | 158 ++++++++++++++ .../Edit/CatchBeatmapVerifier.cs | 3 +- .../CheckCatchAbnormalDifficultySettings.cs | 38 ++++ ...heckManiaAbnormalDifficultySettingsTest.cs | 121 +++++++++++ .../CheckManiaAbnormalDifficultySettings.cs | 32 +++ .../Edit/ManiaBeatmapVerifier.cs | 2 + .../CheckOsuAbnormalDifficultySettingsTest.cs | 194 ++++++++++++++++++ .../CheckOsuAbnormalDifficultySettings.cs | 43 ++++ .../Edit/OsuBeatmapVerifier.cs | 3 + ...heckTaikoAbnormalDifficultySettingsTest.cs | 84 ++++++++ .../CheckTaikoAbnormalDifficultySettings.cs | 26 +++ .../Edit/TaikoBeatmapVerifier.cs | 24 +++ osu.Game.Rulesets.Taiko/TaikoRuleset.cs | 2 + 13 files changed, 729 insertions(+), 1 deletion(-) create mode 100644 osu.Game.Rulesets.Catch.Tests/Editor/Checks/CheckCatchAbnormalDifficultySettingsTest.cs create mode 100644 osu.Game.Rulesets.Catch/Edit/Checks/CheckCatchAbnormalDifficultySettings.cs create mode 100644 osu.Game.Rulesets.Mania.Tests/Editor/Checks/CheckManiaAbnormalDifficultySettingsTest.cs create mode 100644 osu.Game.Rulesets.Mania/Edit/Checks/CheckManiaAbnormalDifficultySettings.cs create mode 100644 osu.Game.Rulesets.Osu.Tests/Editor/Checks/CheckOsuAbnormalDifficultySettingsTest.cs create mode 100644 osu.Game.Rulesets.Osu/Edit/Checks/CheckOsuAbnormalDifficultySettings.cs create mode 100644 osu.Game.Rulesets.Taiko.Tests/Editor/Checks/CheckTaikoAbnormalDifficultySettingsTest.cs create mode 100644 osu.Game.Rulesets.Taiko/Edit/Checks/CheckTaikoAbnormalDifficultySettings.cs create mode 100644 osu.Game.Rulesets.Taiko/Edit/TaikoBeatmapVerifier.cs diff --git a/osu.Game.Rulesets.Catch.Tests/Editor/Checks/CheckCatchAbnormalDifficultySettingsTest.cs b/osu.Game.Rulesets.Catch.Tests/Editor/Checks/CheckCatchAbnormalDifficultySettingsTest.cs new file mode 100644 index 0000000000..2ae2e20215 --- /dev/null +++ b/osu.Game.Rulesets.Catch.Tests/Editor/Checks/CheckCatchAbnormalDifficultySettingsTest.cs @@ -0,0 +1,158 @@ +// 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.Catch.Edit.Checks; +using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Edit.Checks; +using osu.Game.Rulesets.Objects; +using osu.Game.Tests.Beatmaps; + +namespace osu.Game.Rulesets.Catch.Tests.Editor.Checks +{ + [TestFixture] + public class CheckCatchAbnormalDifficultySettingsTest + { + private CheckCatchAbnormalDifficultySettings check = null!; + + private IBeatmap beatmap = new Beatmap(); + + [SetUp] + public void Setup() + { + check = new CheckCatchAbnormalDifficultySettings(); + + beatmap.BeatmapInfo.Ruleset = new CatchRuleset().RulesetInfo; + beatmap.Difficulty = new BeatmapDifficulty() + { + ApproachRate = 5, + CircleSize = 5, + DrainRate = 5, + }; + } + + [Test] + public void TestNormalSettings() + { + var context = getContext(); + var issues = check.Run(context).ToList(); + + Assert.That(issues, Has.Count.EqualTo(0)); + } + + [Test] + public void TestApproachRateTwoDecimals() + { + beatmap.Difficulty.ApproachRate = 5.55f; + + var context = getContext(); + var issues = check.Run(context).ToList(); + + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues.Single().Template is CheckAbnormalDifficultySettings.IssueTemplateMoreThanOneDecimal); + } + + [Test] + public void TestCircleSizeTwoDecimals() + { + beatmap.Difficulty.CircleSize = 5.55f; + + var context = getContext(); + var issues = check.Run(context).ToList(); + + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues.Single().Template is CheckAbnormalDifficultySettings.IssueTemplateMoreThanOneDecimal); + } + + [Test] + public void TestDrainRateTwoDecimals() + { + beatmap.Difficulty.DrainRate = 5.55f; + + var context = getContext(); + var issues = check.Run(context).ToList(); + + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues.Single().Template is CheckAbnormalDifficultySettings.IssueTemplateMoreThanOneDecimal); + } + + [Test] + public void TestApproachRateUnder() + { + beatmap.Difficulty.ApproachRate = -10; + + var context = getContext(); + var issues = check.Run(context).ToList(); + + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues.Single().Template is CheckAbnormalDifficultySettings.IssueTemplateOutOfRange); + } + + [Test] + public void TestCircleSizeUnder() + { + beatmap.Difficulty.CircleSize = -10; + + var context = getContext(); + var issues = check.Run(context).ToList(); + + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues.Single().Template is CheckAbnormalDifficultySettings.IssueTemplateOutOfRange); + } + + [Test] + public void TestDrainRateUnder() + { + beatmap.Difficulty.DrainRate = -10; + + var context = getContext(); + var issues = check.Run(context).ToList(); + + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues.Single().Template is CheckAbnormalDifficultySettings.IssueTemplateOutOfRange); + } + + [Test] + public void TestApproachRateOver() + { + beatmap.Difficulty.ApproachRate = 20; + + var context = getContext(); + var issues = check.Run(context).ToList(); + + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues.Single().Template is CheckAbnormalDifficultySettings.IssueTemplateOutOfRange); + } + + [Test] + public void TestCircleSizeOver() + { + beatmap.Difficulty.CircleSize = 20; + + var context = getContext(); + var issues = check.Run(context).ToList(); + + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues.Single().Template is CheckAbnormalDifficultySettings.IssueTemplateOutOfRange); + } + + [Test] + public void TestDrainRateOver() + { + beatmap.Difficulty.DrainRate = 20; + + var context = getContext(); + var issues = check.Run(context).ToList(); + + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues.Single().Template is CheckAbnormalDifficultySettings.IssueTemplateOutOfRange); + } + + private BeatmapVerifierContext getContext() + { + return new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap)); + } + } +} diff --git a/osu.Game.Rulesets.Catch/Edit/CatchBeatmapVerifier.cs b/osu.Game.Rulesets.Catch/Edit/CatchBeatmapVerifier.cs index c7a41a4e22..71da6d5014 100644 --- a/osu.Game.Rulesets.Catch/Edit/CatchBeatmapVerifier.cs +++ b/osu.Game.Rulesets.Catch/Edit/CatchBeatmapVerifier.cs @@ -13,7 +13,8 @@ namespace osu.Game.Rulesets.Catch.Edit { private readonly List checks = new List { - new CheckBananaShowerGap() + new CheckBananaShowerGap(), + new CheckCatchAbnormalDifficultySettings(), }; public IEnumerable Run(BeatmapVerifierContext context) diff --git a/osu.Game.Rulesets.Catch/Edit/Checks/CheckCatchAbnormalDifficultySettings.cs b/osu.Game.Rulesets.Catch/Edit/Checks/CheckCatchAbnormalDifficultySettings.cs new file mode 100644 index 0000000000..8295795f00 --- /dev/null +++ b/osu.Game.Rulesets.Catch/Edit/Checks/CheckCatchAbnormalDifficultySettings.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.Rulesets.Edit; +using osu.Game.Rulesets.Edit.Checks; +using osu.Game.Rulesets.Edit.Checks.Components; + +namespace osu.Game.Rulesets.Catch.Edit.Checks +{ + public class CheckCatchAbnormalDifficultySettings : CheckAbnormalDifficultySettings + { + public override CheckMetadata Metadata => new CheckMetadata(CheckCategory.Settings, "Checks catch relevant settings"); + + public override IEnumerable Run(BeatmapVerifierContext context) + { + var diff = context.Beatmap.Difficulty; + + if (HasMoreThanOneDecimalPlace(diff.ApproachRate)) + yield return new IssueTemplateMoreThanOneDecimal(this).Create("Approach rate", diff.ApproachRate); + + if (OutOfRange(diff.ApproachRate)) + yield return new IssueTemplateOutOfRange(this).Create("Approach rate", diff.ApproachRate); + + if (HasMoreThanOneDecimalPlace(diff.CircleSize)) + yield return new IssueTemplateMoreThanOneDecimal(this).Create("Circle size", diff.CircleSize); + + if (OutOfRange(diff.CircleSize)) + yield return new IssueTemplateOutOfRange(this).Create("Circle size", diff.CircleSize); + + if (HasMoreThanOneDecimalPlace(diff.DrainRate)) + yield return new IssueTemplateMoreThanOneDecimal(this).Create("Drain rate", diff.DrainRate); + + if (OutOfRange(diff.DrainRate)) + yield return new IssueTemplateOutOfRange(this).Create("Drain rate", diff.DrainRate); + } + } +} diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/Checks/CheckManiaAbnormalDifficultySettingsTest.cs b/osu.Game.Rulesets.Mania.Tests/Editor/Checks/CheckManiaAbnormalDifficultySettingsTest.cs new file mode 100644 index 0000000000..6c585aace3 --- /dev/null +++ b/osu.Game.Rulesets.Mania.Tests/Editor/Checks/CheckManiaAbnormalDifficultySettingsTest.cs @@ -0,0 +1,121 @@ +// 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.Mania.Edit.Checks; +using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Edit.Checks; +using osu.Game.Rulesets.Objects; +using osu.Game.Tests.Beatmaps; + +namespace osu.Game.Rulesets.Mania.Tests.Editor.Checks +{ + [TestFixture] + public class CheckManiaAbnormalDifficultySettingsTest + { + private CheckManiaAbnormalDifficultySettings check = null!; + + private IBeatmap beatmap = new Beatmap(); + + [SetUp] + public void Setup() + { + check = new CheckManiaAbnormalDifficultySettings(); + + beatmap.BeatmapInfo.Ruleset = new ManiaRuleset().RulesetInfo; + beatmap.Difficulty = new BeatmapDifficulty() + { + OverallDifficulty = 5, + DrainRate = 5, + }; + } + + [Test] + public void TestNormalSettings() + { + var context = getContext(); + var issues = check.Run(context).ToList(); + + Assert.That(issues, Has.Count.EqualTo(0)); + } + + [Test] + public void TestOverallDifficultyTwoDecimals() + { + beatmap.Difficulty.OverallDifficulty = 5.55f; + + var context = getContext(); + var issues = check.Run(context).ToList(); + + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues.Single().Template is CheckAbnormalDifficultySettings.IssueTemplateMoreThanOneDecimal); + } + + [Test] + public void TestDrainRateTwoDecimals() + { + beatmap.Difficulty.DrainRate = 5.55f; + + var context = getContext(); + var issues = check.Run(context).ToList(); + + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues.Single().Template is CheckAbnormalDifficultySettings.IssueTemplateMoreThanOneDecimal); + } + + [Test] + public void TestOverallDifficultyUnder() + { + beatmap.Difficulty.OverallDifficulty = -10; + + var context = getContext(); + var issues = check.Run(context).ToList(); + + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues.Single().Template is CheckAbnormalDifficultySettings.IssueTemplateOutOfRange); + } + + [Test] + public void TestDrainRateUnder() + { + beatmap.Difficulty.DrainRate = -10; + + var context = getContext(); + var issues = check.Run(context).ToList(); + + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues.Single().Template is CheckAbnormalDifficultySettings.IssueTemplateOutOfRange); + } + + [Test] + public void TestOverallDifficultyOver() + { + beatmap.Difficulty.OverallDifficulty = 20; + + var context = getContext(); + var issues = check.Run(context).ToList(); + + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues.Single().Template is CheckAbnormalDifficultySettings.IssueTemplateOutOfRange); + } + + [Test] + public void TestDrainRateOver() + { + beatmap.Difficulty.DrainRate = 20; + + var context = getContext(); + var issues = check.Run(context).ToList(); + + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues.Single().Template is CheckAbnormalDifficultySettings.IssueTemplateOutOfRange); + } + + private BeatmapVerifierContext getContext() + { + return new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap)); + } + } +} diff --git a/osu.Game.Rulesets.Mania/Edit/Checks/CheckManiaAbnormalDifficultySettings.cs b/osu.Game.Rulesets.Mania/Edit/Checks/CheckManiaAbnormalDifficultySettings.cs new file mode 100644 index 0000000000..ae0cc3aa4c --- /dev/null +++ b/osu.Game.Rulesets.Mania/Edit/Checks/CheckManiaAbnormalDifficultySettings.cs @@ -0,0 +1,32 @@ +// 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.Rulesets.Edit; +using osu.Game.Rulesets.Edit.Checks; +using osu.Game.Rulesets.Edit.Checks.Components; + +namespace osu.Game.Rulesets.Mania.Edit.Checks +{ + public class CheckManiaAbnormalDifficultySettings : CheckAbnormalDifficultySettings + { + public override CheckMetadata Metadata => new CheckMetadata(CheckCategory.Settings, "Checks mania relevant settings"); + + public override IEnumerable Run(BeatmapVerifierContext context) + { + var diff = context.Beatmap.Difficulty; + + if (HasMoreThanOneDecimalPlace(diff.OverallDifficulty)) + yield return new IssueTemplateMoreThanOneDecimal(this).Create("Overall difficulty", diff.OverallDifficulty); + + if (OutOfRange(diff.OverallDifficulty)) + yield return new IssueTemplateOutOfRange(this).Create("Overall difficulty", diff.OverallDifficulty); + + if (HasMoreThanOneDecimalPlace(diff.DrainRate)) + yield return new IssueTemplateMoreThanOneDecimal(this).Create("Drain rate", diff.DrainRate); + + if (OutOfRange(diff.DrainRate)) + yield return new IssueTemplateOutOfRange(this).Create("Drain rate", diff.DrainRate); + } + } +} diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaBeatmapVerifier.cs b/osu.Game.Rulesets.Mania/Edit/ManiaBeatmapVerifier.cs index 778dcfc6e1..4adabfa4d7 100644 --- a/osu.Game.Rulesets.Mania/Edit/ManiaBeatmapVerifier.cs +++ b/osu.Game.Rulesets.Mania/Edit/ManiaBeatmapVerifier.cs @@ -13,7 +13,9 @@ namespace osu.Game.Rulesets.Mania.Edit { private readonly List checks = new List { + // Settings new CheckKeyCount(), + new CheckManiaAbnormalDifficultySettings(), }; public IEnumerable Run(BeatmapVerifierContext context) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/Checks/CheckOsuAbnormalDifficultySettingsTest.cs b/osu.Game.Rulesets.Osu.Tests/Editor/Checks/CheckOsuAbnormalDifficultySettingsTest.cs new file mode 100644 index 0000000000..53ccd3c7a7 --- /dev/null +++ b/osu.Game.Rulesets.Osu.Tests/Editor/Checks/CheckOsuAbnormalDifficultySettingsTest.cs @@ -0,0 +1,194 @@ +// 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.Osu.Edit.Checks; +using NUnit.Framework; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Edit.Checks; +using osu.Game.Rulesets.Edit; +using osu.Game.Tests.Beatmaps; +using System.Linq; + +namespace osu.Game.Rulesets.Osu.Tests.Editor.Checks +{ + [TestFixture] + public class CheckOsuAbnormalDifficultySettingsTest + { + private CheckOsuAbnormalDifficultySettings check = null!; + + private IBeatmap beatmap = new Beatmap(); + + [SetUp] + public void Setup() + { + check = new CheckOsuAbnormalDifficultySettings(); + + beatmap.Difficulty = new BeatmapDifficulty() + { + ApproachRate = 5, + CircleSize = 5, + DrainRate = 5, + OverallDifficulty = 5, + }; + } + + [Test] + public void TestNormalSettings() + { + var context = getContext(); + var issues = check.Run(context).ToList(); + + Assert.That(issues, Has.Count.EqualTo(0)); + } + + [Test] + public void TestApproachRateTwoDecimals() + { + beatmap.Difficulty.ApproachRate = 5.55f; + + var context = getContext(); + var issues = check.Run(context).ToList(); + + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues.Single().Template is CheckAbnormalDifficultySettings.IssueTemplateMoreThanOneDecimal); + } + + [Test] + public void TestCircleSizeTwoDecimals() + { + beatmap.Difficulty.CircleSize = 5.55f; + + var context = getContext(); + var issues = check.Run(context).ToList(); + + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues.Single().Template is CheckAbnormalDifficultySettings.IssueTemplateMoreThanOneDecimal); + } + + [Test] + public void TestDrainRateTwoDecimals() + { + beatmap.Difficulty.DrainRate = 5.55f; + + var context = getContext(); + var issues = check.Run(context).ToList(); + + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues.Single().Template is CheckAbnormalDifficultySettings.IssueTemplateMoreThanOneDecimal); + } + + [Test] + public void TestOverallDifficultyTwoDecimals() + { + beatmap.Difficulty.OverallDifficulty = 5.55f; + + var context = getContext(); + var issues = check.Run(context).ToList(); + + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues.Single().Template is CheckAbnormalDifficultySettings.IssueTemplateMoreThanOneDecimal); + } + + [Test] + public void TestApproachRateUnder() + { + beatmap.Difficulty.ApproachRate = -10; + + var context = getContext(); + var issues = check.Run(context).ToList(); + + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues.Single().Template is CheckAbnormalDifficultySettings.IssueTemplateOutOfRange); + } + + [Test] + public void TestCircleSizeUnder() + { + beatmap.Difficulty.CircleSize = -10; + + var context = getContext(); + var issues = check.Run(context).ToList(); + + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues.Single().Template is CheckAbnormalDifficultySettings.IssueTemplateOutOfRange); + } + + [Test] + public void TestDrainRateUnder() + { + beatmap.Difficulty.DrainRate = -10; + + var context = getContext(); + var issues = check.Run(context).ToList(); + + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues.Single().Template is CheckAbnormalDifficultySettings.IssueTemplateOutOfRange); + } + + [Test] + public void TestOverallDifficultyUnder() + { + beatmap.Difficulty.OverallDifficulty = -10; + + var context = getContext(); + var issues = check.Run(context).ToList(); + + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues.Single().Template is CheckAbnormalDifficultySettings.IssueTemplateOutOfRange); + } + + [Test] + public void TestApproachRateOver() + { + beatmap.Difficulty.ApproachRate = 20; + + var context = getContext(); + var issues = check.Run(context).ToList(); + + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues.Single().Template is CheckAbnormalDifficultySettings.IssueTemplateOutOfRange); + } + + [Test] + public void TestCircleSizeOver() + { + beatmap.Difficulty.CircleSize = 20; + + var context = getContext(); + var issues = check.Run(context).ToList(); + + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues.Single().Template is CheckAbnormalDifficultySettings.IssueTemplateOutOfRange); + } + + [Test] + public void TestDrainRateOver() + { + beatmap.Difficulty.DrainRate = 20; + + var context = getContext(); + var issues = check.Run(context).ToList(); + + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues.Single().Template is CheckAbnormalDifficultySettings.IssueTemplateOutOfRange); + } + + [Test] + public void TestOverallDifficultyOver() + { + beatmap.Difficulty.OverallDifficulty = 20; + + var context = getContext(); + var issues = check.Run(context).ToList(); + + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues.Single().Template is CheckAbnormalDifficultySettings.IssueTemplateOutOfRange); + } + + private BeatmapVerifierContext getContext() + { + return new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap)); + } + } +} diff --git a/osu.Game.Rulesets.Osu/Edit/Checks/CheckOsuAbnormalDifficultySettings.cs b/osu.Game.Rulesets.Osu/Edit/Checks/CheckOsuAbnormalDifficultySettings.cs new file mode 100644 index 0000000000..c1eca7fff7 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Edit/Checks/CheckOsuAbnormalDifficultySettings.cs @@ -0,0 +1,43 @@ +// 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.Rulesets.Edit; +using osu.Game.Rulesets.Edit.Checks; +using osu.Game.Rulesets.Edit.Checks.Components; + +namespace osu.Game.Rulesets.Osu.Edit.Checks +{ + public class CheckOsuAbnormalDifficultySettings : CheckAbnormalDifficultySettings + { + public override CheckMetadata Metadata => new CheckMetadata(CheckCategory.Settings, "Checks osu relevant settings"); + public override IEnumerable Run(BeatmapVerifierContext context) + { + var diff = context.Beatmap.Difficulty; + + if (HasMoreThanOneDecimalPlace(diff.ApproachRate)) + yield return new IssueTemplateMoreThanOneDecimal(this).Create("Approach rate", diff.ApproachRate); + + if (OutOfRange(diff.ApproachRate)) + yield return new IssueTemplateOutOfRange(this).Create("Approach rate", diff.ApproachRate); + + if (HasMoreThanOneDecimalPlace(diff.OverallDifficulty)) + yield return new IssueTemplateMoreThanOneDecimal(this).Create("Overall difficulty", diff.OverallDifficulty); + + if (OutOfRange(diff.OverallDifficulty)) + yield return new IssueTemplateOutOfRange(this).Create("Overall difficulty", diff.OverallDifficulty); + + if (HasMoreThanOneDecimalPlace(diff.CircleSize)) + yield return new IssueTemplateMoreThanOneDecimal(this).Create("Circle size", diff.CircleSize); + + if (OutOfRange(diff.CircleSize)) + yield return new IssueTemplateOutOfRange(this).Create("Circle size", diff.CircleSize); + + if (HasMoreThanOneDecimalPlace(diff.DrainRate)) + yield return new IssueTemplateMoreThanOneDecimal(this).Create("Drain rate", diff.DrainRate); + + if (OutOfRange(diff.DrainRate)) + yield return new IssueTemplateOutOfRange(this).Create("Drain rate", diff.DrainRate); + } + } +} diff --git a/osu.Game.Rulesets.Osu/Edit/OsuBeatmapVerifier.cs b/osu.Game.Rulesets.Osu/Edit/OsuBeatmapVerifier.cs index 325e9ed4cb..4b01a1fc39 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuBeatmapVerifier.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuBeatmapVerifier.cs @@ -21,6 +21,9 @@ namespace osu.Game.Rulesets.Osu.Edit new CheckTimeDistanceEquality(), new CheckLowDiffOverlaps(), new CheckTooShortSliders(), + + // Settings + new CheckOsuAbnormalDifficultySettings(), }; public IEnumerable Run(BeatmapVerifierContext context) diff --git a/osu.Game.Rulesets.Taiko.Tests/Editor/Checks/CheckTaikoAbnormalDifficultySettingsTest.cs b/osu.Game.Rulesets.Taiko.Tests/Editor/Checks/CheckTaikoAbnormalDifficultySettingsTest.cs new file mode 100644 index 0000000000..f10e62f3bf --- /dev/null +++ b/osu.Game.Rulesets.Taiko.Tests/Editor/Checks/CheckTaikoAbnormalDifficultySettingsTest.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 osu.Game.Rulesets.Taiko.Edit.Checks; +using NUnit.Framework; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Edit.Checks; +using osu.Game.Rulesets.Edit; +using osu.Game.Tests.Beatmaps; +using System.Linq; + +namespace osu.Game.Rulesets.Taiko.Tests.Editor.Checks +{ + [TestFixture] + public class CheckTaikoAbnormalDifficultySettingsTest + { + private CheckTaikoAbnormalDifficultySettings check = null!; + + private IBeatmap beatmap = new Beatmap(); + + [SetUp] + public void Setup() + { + check = new CheckTaikoAbnormalDifficultySettings(); + + beatmap.BeatmapInfo.Ruleset = new TaikoRuleset().RulesetInfo; + beatmap.Difficulty = new BeatmapDifficulty() + { + OverallDifficulty = 5, + }; + } + + [Test] + public void TestNormalSettings() + { + var context = getContext(); + var issues = check.Run(context).ToList(); + + Assert.That(issues, Has.Count.EqualTo(0)); + } + + [Test] + public void TestOverallDifficultyTwoDecimals() + { + beatmap.Difficulty.OverallDifficulty = 5.55f; + + var context = getContext(); + var issues = check.Run(context).ToList(); + + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues.Single().Template is CheckAbnormalDifficultySettings.IssueTemplateMoreThanOneDecimal); + } + + [Test] + public void TestOverallDifficultyUnder() + { + beatmap.Difficulty.OverallDifficulty = -10; + + var context = getContext(); + var issues = check.Run(context).ToList(); + + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues.Single().Template is CheckAbnormalDifficultySettings.IssueTemplateOutOfRange); + } + + [Test] + public void TestOverallDifficultyOver() + { + beatmap.Difficulty.OverallDifficulty = 20; + + var context = getContext(); + var issues = check.Run(context).ToList(); + + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues.Single().Template is CheckAbnormalDifficultySettings.IssueTemplateOutOfRange); + } + + private BeatmapVerifierContext getContext() + { + return new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap)); + } + } +} diff --git a/osu.Game.Rulesets.Taiko/Edit/Checks/CheckTaikoAbnormalDifficultySettings.cs b/osu.Game.Rulesets.Taiko/Edit/Checks/CheckTaikoAbnormalDifficultySettings.cs new file mode 100644 index 0000000000..ce35f21853 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Edit/Checks/CheckTaikoAbnormalDifficultySettings.cs @@ -0,0 +1,26 @@ +// 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.Rulesets.Edit; +using osu.Game.Rulesets.Edit.Checks; +using osu.Game.Rulesets.Edit.Checks.Components; + +namespace osu.Game.Rulesets.Taiko.Edit.Checks +{ + public class CheckTaikoAbnormalDifficultySettings : CheckAbnormalDifficultySettings + { + public override CheckMetadata Metadata => new CheckMetadata(CheckCategory.Settings, "Checks taiko relevant settings"); + + public override IEnumerable Run(BeatmapVerifierContext context) + { + var diff = context.Beatmap.Difficulty; + + if (HasMoreThanOneDecimalPlace(diff.OverallDifficulty)) + yield return new IssueTemplateMoreThanOneDecimal(this).Create("Overall difficulty", diff.OverallDifficulty); + + if (OutOfRange(diff.OverallDifficulty)) + yield return new IssueTemplateOutOfRange(this).Create("Overall difficulty", diff.OverallDifficulty); + } + } +} diff --git a/osu.Game.Rulesets.Taiko/Edit/TaikoBeatmapVerifier.cs b/osu.Game.Rulesets.Taiko/Edit/TaikoBeatmapVerifier.cs new file mode 100644 index 0000000000..f5c3f1846d --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Edit/TaikoBeatmapVerifier.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.Edit; +using osu.Game.Rulesets.Edit.Checks.Components; +using osu.Game.Rulesets.Taiko.Edit.Checks; + +namespace osu.Game.Rulesets.Taiko.Edit +{ + public class TaikoBeatmapVerifier : IBeatmapVerifier + { + private readonly List checks = new List + { + new CheckTaikoAbnormalDifficultySettings(), + }; + + public IEnumerable Run(BeatmapVerifierContext context) + { + return checks.SelectMany(check => check.Run(context)); + } + } +} diff --git a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs index 24b0ec5d57..d7184bce60 100644 --- a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs @@ -188,6 +188,8 @@ namespace osu.Game.Rulesets.Taiko public override HitObjectComposer CreateHitObjectComposer() => new TaikoHitObjectComposer(this); + public override IBeatmapVerifier CreateBeatmapVerifier() => new TaikoBeatmapVerifier(); + public override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => new TaikoDifficultyCalculator(RulesetInfo, beatmap); public override PerformanceCalculator CreatePerformanceCalculator() => new TaikoPerformanceCalculator(); From c7c03302653790668d0fdb49f25dfceb016eaf3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 22 Mar 2024 19:05:58 +0100 Subject: [PATCH 0407/2493] Attempt to disable rulesets that can be linked to an unhandled crash --- osu.Game/OsuGameBase.cs | 4 ++- osu.Game/Rulesets/RealmRulesetStore.cs | 48 ++++++++++++++++++++++++-- osu.Game/Rulesets/RulesetStore.cs | 24 +++++++++---- 3 files changed, 65 insertions(+), 11 deletions(-) diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 81e3d8bed8..8bda8fb6c2 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -678,12 +678,14 @@ namespace osu.Game /// /// Allows a maximum of one unhandled exception, per second of execution. /// - private bool onExceptionThrown(Exception _) + private bool onExceptionThrown(Exception ex) { bool continueExecution = Interlocked.Decrement(ref allowableExceptions) >= 0; Logger.Log($"Unhandled exception has been {(continueExecution ? $"allowed with {allowableExceptions} more allowable exceptions" : "denied")} ."); + RulesetStore.TryDisableCustomRulesetsCausing(ex); + // restore the stock of allowable exceptions after a short delay. Task.Delay(1000).ContinueWith(_ => Interlocked.Increment(ref allowableExceptions)); diff --git a/osu.Game/Rulesets/RealmRulesetStore.cs b/osu.Game/Rulesets/RealmRulesetStore.cs index ba6f4583d1..36eae7af2c 100644 --- a/osu.Game/Rulesets/RealmRulesetStore.cs +++ b/osu.Game/Rulesets/RealmRulesetStore.cs @@ -3,8 +3,11 @@ using System; using System.Collections.Generic; +using System.Diagnostics; +using System.IO; using System.Linq; using osu.Framework.Extensions.ObjectExtensions; +using osu.Framework.Logging; using osu.Framework.Platform; using osu.Game.Beatmaps; using osu.Game.Database; @@ -13,17 +16,20 @@ namespace osu.Game.Rulesets { public class RealmRulesetStore : RulesetStore { + private readonly RealmAccess realmAccess; public override IEnumerable AvailableRulesets => availableRulesets; private readonly List availableRulesets = new List(); - public RealmRulesetStore(RealmAccess realm, Storage? storage = null) + public RealmRulesetStore(RealmAccess realmAccess, Storage? storage = null) : base(storage) { - prepareDetachedRulesets(realm); + this.realmAccess = realmAccess; + prepareDetachedRulesets(); + informUserAboutBrokenRulesets(); } - private void prepareDetachedRulesets(RealmAccess realmAccess) + private void prepareDetachedRulesets() { realmAccess.Write(realm => { @@ -143,5 +149,41 @@ namespace osu.Game.Rulesets instance.CreateBeatmapProcessor(converter.Convert()); } + + private void informUserAboutBrokenRulesets() + { + if (RulesetStorage == null) + return; + + foreach (string brokenRulesetDll in RulesetStorage.GetFiles(@".", @"*.dll.broken")) + { + Logger.Log($"Ruleset '{Path.GetFileNameWithoutExtension(brokenRulesetDll)}' has been disabled due to causing a crash.\n\n" + + "Please update the ruleset or report the issue to the developers of the ruleset if no updates are available.", level: LogLevel.Important); + } + } + + internal void TryDisableCustomRulesetsCausing(Exception exception) + { + var stackTrace = new StackTrace(exception); + + foreach (var frame in stackTrace.GetFrames()) + { + var declaringAssembly = frame.GetMethod()?.DeclaringType?.Assembly; + if (declaringAssembly == null) + continue; + + if (UserRulesetAssemblies.Contains(declaringAssembly)) + { + string sourceLocation = declaringAssembly.Location; + string destinationLocation = Path.ChangeExtension(sourceLocation, @".dll.broken"); + + if (File.Exists(sourceLocation)) + { + Logger.Log($"Unhandled exception traced back to custom ruleset {Path.GetFileNameWithoutExtension(sourceLocation)}. Marking as broken."); + File.Move(sourceLocation, destinationLocation); + } + } + } + } } } diff --git a/osu.Game/Rulesets/RulesetStore.cs b/osu.Game/Rulesets/RulesetStore.cs index ac36ee6494..f33d42a53e 100644 --- a/osu.Game/Rulesets/RulesetStore.cs +++ b/osu.Game/Rulesets/RulesetStore.cs @@ -18,6 +18,8 @@ namespace osu.Game.Rulesets private const string ruleset_library_prefix = @"osu.Game.Rulesets"; protected readonly Dictionary LoadedAssemblies = new Dictionary(); + protected readonly HashSet UserRulesetAssemblies = new HashSet(); + protected readonly Storage? RulesetStorage; /// /// All available rulesets. @@ -41,9 +43,9 @@ namespace osu.Game.Rulesets // to load as unable to locate the game core assembly. AppDomain.CurrentDomain.AssemblyResolve += resolveRulesetDependencyAssembly; - var rulesetStorage = storage?.GetStorageForDirectory(@"rulesets"); - if (rulesetStorage != null) - loadUserRulesets(rulesetStorage); + RulesetStorage = storage?.GetStorageForDirectory(@"rulesets"); + if (RulesetStorage != null) + loadUserRulesets(RulesetStorage); } /// @@ -105,7 +107,11 @@ namespace osu.Game.Rulesets var rulesets = rulesetStorage.GetFiles(@".", @$"{ruleset_library_prefix}.*.dll"); foreach (string? ruleset in rulesets.Where(f => !f.Contains(@"Tests"))) - loadRulesetFromFile(rulesetStorage.GetFullPath(ruleset)); + { + var assembly = loadRulesetFromFile(rulesetStorage.GetFullPath(ruleset)); + if (assembly != null) + UserRulesetAssemblies.Add(assembly); + } } private void loadFromDisk() @@ -126,21 +132,25 @@ namespace osu.Game.Rulesets } } - private void loadRulesetFromFile(string file) + private Assembly? loadRulesetFromFile(string file) { string filename = Path.GetFileNameWithoutExtension(file); if (LoadedAssemblies.Values.Any(t => Path.GetFileNameWithoutExtension(t.Assembly.Location) == filename)) - return; + return null; try { - addRuleset(Assembly.LoadFrom(file)); + var assembly = Assembly.LoadFrom(file); + addRuleset(assembly); + return assembly; } catch (Exception e) { LogFailedLoad(filename, e); } + + return null; } private void addRuleset(Assembly assembly) From 6fbe1a5b8d1a80c6a5b1e277ee015162b7ea8083 Mon Sep 17 00:00:00 2001 From: Arthur Araujo Date: Sat, 23 Mar 2024 19:22:47 -0300 Subject: [PATCH 0408/2493] Add video resolution check --- .../Checks/CheckVideoResolutionTest.cs | 86 +++++++++++++ .../Videos/test-video-resolution-high.mp4 | Bin 0 -> 13655 bytes osu.Game/Rulesets/Edit/BeatmapVerifier.cs | 1 + .../Edit/Checks/CheckVideoResolution.cs | 117 ++++++++++++++++++ 4 files changed, 204 insertions(+) create mode 100644 osu.Game.Tests/Editing/Checks/CheckVideoResolutionTest.cs create mode 100644 osu.Game.Tests/Resources/Videos/test-video-resolution-high.mp4 create mode 100644 osu.Game/Rulesets/Edit/Checks/CheckVideoResolution.cs diff --git a/osu.Game.Tests/Editing/Checks/CheckVideoResolutionTest.cs b/osu.Game.Tests/Editing/Checks/CheckVideoResolutionTest.cs new file mode 100644 index 0000000000..ab677a15d4 --- /dev/null +++ b/osu.Game.Tests/Editing/Checks/CheckVideoResolutionTest.cs @@ -0,0 +1,86 @@ +using System.IO; +using System.Linq; +using Moq; +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.Storyboards; +using osu.Game.Tests.Beatmaps; +using osu.Game.Tests.Resources; + +namespace osu.Game.Tests.Editing.Checks +{ + [TestFixture] + public class CheckVideoResolutionTest + { + private CheckVideoResolution check = null!; + + private IBeatmap beatmap = null!; + + [SetUp] + public void Setup() + { + check = new CheckVideoResolution(); + beatmap = new Beatmap + { + BeatmapInfo = new BeatmapInfo + { + BeatmapSet = new BeatmapSetInfo + { + Files = + { + CheckTestHelpers.CreateMockFile("mp4"), + } + } + } + }; + } + + [Test] + public void TestNoVideo() + { + beatmap.BeatmapInfo.BeatmapSet?.Files.Clear(); + + var issues = check.Run(getContext(null)).ToList(); + + Assert.That(issues, Has.Count.EqualTo(0)); + } + + [Test] + public void TestVideoAcceptableResolution() + { + using (var resourceStream = TestResources.OpenResource("Videos/test-video.mp4")) + { + var issues = check.Run(getContext(resourceStream)).ToList(); + Assert.That(issues, Has.Count.EqualTo(0)); + } + } + + [Test] + public void TestVideoHighResolution() + { + using (var resourceStream = TestResources.OpenResource("Videos/test-video-resolution-high.mp4")) + { + var issues = check.Run(getContext(resourceStream)).ToList(); + + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues.Single().Template is CheckVideoResolution.IssueTemplateHighResolution); + } + } + + private BeatmapVerifierContext getContext(Stream? resourceStream) + { + var storyboard = new Storyboard(); + var layer = storyboard.GetLayer("Video"); + layer.Add(new StoryboardVideo("abc123.mp4", 0)); + + var mockWorkingBeatmap = new Mock(beatmap, null, null); + mockWorkingBeatmap.Setup(w => w.GetStream(It.IsAny())).Returns(resourceStream); + mockWorkingBeatmap.As().SetupGet(w => w.Storyboard).Returns(storyboard); + + return new BeatmapVerifierContext(beatmap, mockWorkingBeatmap.Object); + } + } +} diff --git a/osu.Game.Tests/Resources/Videos/test-video-resolution-high.mp4 b/osu.Game.Tests/Resources/Videos/test-video-resolution-high.mp4 new file mode 100644 index 0000000000000000000000000000000000000000..fbdb00d3ad464a7f2d9d223d5fa9edb67fdaa689 GIT binary patch literal 13655 zcmeHNeQX>@6@PczVL*m4tPP}$L-S}hg&bg*ePO=GZEeMbb5vo#p-P>LJ4)5Ns zch`~B8a%$qmwy`2jYNdw-vopmjfC`>E|#gtJsYT8{RYT1XfIi(P|$oB0*sq zpGtgdCop3W(sEt%T;z?q;d(ye&y`r0=D_UPkB{GRO@&2S21 z&}dfEgEGsv;N9w1a+;yDHdS9Tv$+8j7L1zyp!~F#Yx%lH&CyvKmRS9q5yz17<*GYe zFKwWF+V$LELs*+*Y}1gl#EwF5+qsKNF8ok7uW)6Fy!Ny38Jjq{as2fL>sSr>ss76H3uE$U3*>WF{yW;; zS7^sF$iI3!awdDWcf;49u{YO+Uj+8=_C5X|^ksaRZv88BzT$eSg+46Z3hUPkE&d4T z1?bT2pq~)cuLpgPXkjPl+eC|&f%Zdfmx3N8TKrznZWtB?^fJ+sEudc#E!_+{3U&Pe z=xM0)I?#8Cn&Y4+iCR{H9wSK@P;qSbeTP7$?k0{x8W<{l8{ zvt~W$5K$ZE({_NU^M24Ik&Ly=uMkBupmRjAH0U%@9P^2vAnLjU^gL0`iECI}{{Z+_ zo=GI1LQF|~ttkM0=7S}mr&MI6x3achDOuZ^zPGlvPft+Du4rvtYb3|B`WW7|lC`bA zT(GuvZr<7kZT|^NTm6HArM-WerOm-LVsrgeT6UP~7=o#?l+ay`DLq9dCMH(koK+!T z)Kkx(FgroBZ>$DVJ@26{=copy5EFID92VLDn{E#8&Rf55hC9wNv|}>~h>gXFy))Yt z;ME*n!{N0Y&TK}|el~~Sz~OT^d@hH-hr@5=a3+~xKJz#nN2Q1}+Zym?VhY+XmBZI?cpHbe zb2!{uaU2});yh&zk8(KEgFtRw9KM#r6C8dEhr=8d$A{@7rYn$3=yTKD0rZd9J}~{6 zAu%$T=9cDT-{!;gFM#P^0MlO&-KdA@UkF#Ua38h@`?Kg#m=2tghTU*SIIoN0!W%yc z`W4*KX3$Z%BeAh$M+~HoECS8%R*Y8ZoV8d3r?i6y$fAx}T#+ z&3t$cC*vr#%|^2*}0oF;OLOU>ZK-H zVOmerR8LKP=XZU7I(+_g|BoJS`;ByZ*`>dX#S&doyQEr56N-{R zMSlkxbbfeq-|g*PQty_HXsPKcDmGg7xS(;Fu?DN7h zlw61940bXu9U0Fw(8jX8td(=SB~?i)YBDO}6CxQ+NSdBDEOjUu?UuV`Ny#b3xT`1S zv3R#U7LCUxQ%{bV8Oa?`lrBn7e%2^GINT(D{u|2<(&MGcr zl67>fD=ux-vx6Bg4Q7_D=Td`~joLhDqr;FsjcsjzFMW01iHl#4H$78-=&7z- z4xjYDkRrje_QmgaED5ki<63Bk!l=n^X-e2CzC5w+{s z<0>4<%Q!UP3&&p^b*3;Qfwx5h!@V%cR_`OT+jo9+>Wx=Ks94RF--nr3P~jEgQFw(I zyauYL9p1w7Qi3SIZ& z%Mf(K7%uP7rKTTw9q2M{o*u$IJTOVZS6C0xX!(fObUFh3+6;^T0SSbM0*w|PoaGr8 z*^7+GZM?OX0CtN#FS<<~4c6f5rYbqvVZ z(nW@16+U&ug<=(SORYVicoAHevAaoT#n7$^<57MaF=7Y|I1}+YI4_; zWvcsMx&r+1wrR28bqL9r)&9TvrLxR$iVManI16#X*fgu{@3ydztX$xtgRx>%1PvD# nl9krXg0TnQ0l!rC9a$sfC&QIzd5O0gqJFh2Q*qv;=F9&8=0zb8 literal 0 HcmV?d00001 diff --git a/osu.Game/Rulesets/Edit/BeatmapVerifier.cs b/osu.Game/Rulesets/Edit/BeatmapVerifier.cs index 4a316afd22..95f79391ef 100644 --- a/osu.Game/Rulesets/Edit/BeatmapVerifier.cs +++ b/osu.Game/Rulesets/Edit/BeatmapVerifier.cs @@ -18,6 +18,7 @@ namespace osu.Game.Rulesets.Edit // Resources new CheckBackgroundPresence(), new CheckBackgroundQuality(), + new CheckVideoResolution(), // Audio new CheckAudioPresence(), diff --git a/osu.Game/Rulesets/Edit/Checks/CheckVideoResolution.cs b/osu.Game/Rulesets/Edit/Checks/CheckVideoResolution.cs new file mode 100644 index 0000000000..831962e2e9 --- /dev/null +++ b/osu.Game/Rulesets/Edit/Checks/CheckVideoResolution.cs @@ -0,0 +1,117 @@ +// 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.IO; +using osu.Framework.Logging; +using osu.Game.Beatmaps; +using osu.Game.IO.FileAbstraction; +using osu.Game.Rulesets.Edit.Checks.Components; +using osu.Game.Storyboards; +using TagLib; +using File = TagLib.File; + +namespace osu.Game.Rulesets.Edit.Checks +{ + public class CheckVideoResolution : ICheck + { + private const int max_video_width = 1280; + + private const int max_video_height = 720; + + public CheckMetadata Metadata => new CheckMetadata(CheckCategory.Resources, "Too high video resolution."); + + public IEnumerable PossibleTemplates => new IssueTemplate[] + { + new IssueTemplateHighResolution(this), + new IssueTemplateFileError(this), + }; + + public IEnumerable Run(BeatmapVerifierContext context) + { + var beatmapSet = context.Beatmap.BeatmapInfo.BeatmapSet; + var videoPaths = getVideoPaths(context.WorkingBeatmap.Storyboard); + + foreach (string filename in videoPaths) + { + string? storagePath = beatmapSet?.GetPathForFile(filename); + + // Don't report any issues for missing video here since another check is already doing that (CheckAudioInVideo) + if (storagePath == null) continue; + + Issue issue; + + try + { + using (Stream data = context.WorkingBeatmap.GetStream(storagePath)) + using (File tagFile = File.Create(new StreamFileAbstraction(filename, data))) + { + int height = tagFile.Properties.VideoHeight; + int width = tagFile.Properties.VideoWidth; + + if (height <= max_video_height || width <= max_video_width) + continue; + + issue = new IssueTemplateHighResolution(this).Create(filename, width, height); + } + } + catch (CorruptFileException) + { + issue = new IssueTemplateFileError(this).Create(filename, "Corrupt file"); + } + catch (UnsupportedFormatException) + { + issue = new IssueTemplateFileError(this).Create(filename, "Unsupported format"); + } + catch (Exception ex) + { + issue = new IssueTemplateFileError(this).Create(filename, "Internal failure - see logs for more info"); + Logger.Log($"Failed when running {nameof(CheckVideoResolution)}: {ex}"); + } + + yield return issue; + } + } + + private List getVideoPaths(Storyboard storyboard) + { + var videoPaths = new List(); + + foreach (var layer in storyboard.Layers) + { + foreach (var element in layer.Elements) + { + if (element is not StoryboardVideo video) + continue; + + if (!videoPaths.Contains(video.Path)) + videoPaths.Add(video.Path); + } + } + + return videoPaths; + } + + public class IssueTemplateHighResolution : IssueTemplate + { + public IssueTemplateHighResolution(ICheck check) + : base(check, IssueType.Problem, "\"{0}\" resolution exceeds 1280x720 ({1}x{2})") + { + } + + public Issue Create(string filename, int width, int height) => new Issue(this, filename, width, height); + } + + public class IssueTemplateFileError : IssueTemplate + { + public IssueTemplateFileError(ICheck check) + : base(check, IssueType.Error, "Could not check resolution for \"{0}\" ({1}).") + { + } + + public Issue Create(string filename, string errorReason) => new Issue(this, filename, errorReason); + } + + } +} From eb938272040be14caa292fe65800356a6ae03a1a Mon Sep 17 00:00:00 2001 From: Arthur Araujo Date: Sat, 23 Mar 2024 23:11:13 -0300 Subject: [PATCH 0409/2493] Add missing copyright header --- osu.Game.Tests/Editing/Checks/CheckVideoResolutionTest.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game.Tests/Editing/Checks/CheckVideoResolutionTest.cs b/osu.Game.Tests/Editing/Checks/CheckVideoResolutionTest.cs index ab677a15d4..1e16c67aab 100644 --- a/osu.Game.Tests/Editing/Checks/CheckVideoResolutionTest.cs +++ b/osu.Game.Tests/Editing/Checks/CheckVideoResolutionTest.cs @@ -1,3 +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.IO; using System.Linq; using Moq; From 8a05fecad5862d744d49b8febcb389e4e22b9850 Mon Sep 17 00:00:00 2001 From: Arthur Araujo Date: Sat, 23 Mar 2024 23:28:55 -0300 Subject: [PATCH 0410/2493] Fix formatting issue --- osu.Game/Rulesets/Edit/Checks/CheckVideoResolution.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Rulesets/Edit/Checks/CheckVideoResolution.cs b/osu.Game/Rulesets/Edit/Checks/CheckVideoResolution.cs index 831962e2e9..1b603b7e47 100644 --- a/osu.Game/Rulesets/Edit/Checks/CheckVideoResolution.cs +++ b/osu.Game/Rulesets/Edit/Checks/CheckVideoResolution.cs @@ -112,6 +112,5 @@ namespace osu.Game.Rulesets.Edit.Checks public Issue Create(string filename, string errorReason) => new Issue(this, filename, errorReason); } - } } From ef2a16dd8ff260efa5117a10352e6ec771dfa2c3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 23 Mar 2024 16:41:40 +0800 Subject: [PATCH 0411/2493] Various renaming and class updates to allow multiple menu banners --- .../Visual/Menus/TestSceneMainMenu.cs | 46 +++++++++----- ...tleRequest.cs => GetMenuContentRequest.cs} | 4 +- .../API/Requests/Responses/APIMenuContent.cs | 42 +++++++++++++ .../API/Requests/Responses/APIMenuImage.cs | 57 +++++++++++++++++ .../API/Requests/Responses/APISystemTitle.cs | 30 --------- osu.Game/Screens/Menu/MainMenu.cs | 8 +-- .../{SystemTitle.cs => OnlineMenuBanner.cs} | 62 ++++++++++--------- 7 files changed, 169 insertions(+), 80 deletions(-) rename osu.Game/Online/API/Requests/{GetSystemTitleRequest.cs => GetMenuContentRequest.cs} (74%) create mode 100644 osu.Game/Online/API/Requests/Responses/APIMenuContent.cs create mode 100644 osu.Game/Online/API/Requests/Responses/APIMenuImage.cs delete mode 100644 osu.Game/Online/API/Requests/Responses/APISystemTitle.cs rename osu.Game/Screens/Menu/{SystemTitle.cs => OnlineMenuBanner.cs} (79%) diff --git a/osu.Game.Tests/Visual/Menus/TestSceneMainMenu.cs b/osu.Game.Tests/Visual/Menus/TestSceneMainMenu.cs index 7053a9d544..3c78edb8a5 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneMainMenu.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneMainMenu.cs @@ -13,30 +13,48 @@ namespace osu.Game.Tests.Visual.Menus { public partial class TestSceneMainMenu : OsuGameTestScene { - private SystemTitle systemTitle => Game.ChildrenOfType().Single(); + private OnlineMenuBanner onlineMenuBanner => Game.ChildrenOfType().Single(); [Test] - public void TestSystemTitle() + public void TestOnlineMenuBanner() { - AddStep("set system title", () => systemTitle.Current.Value = new APISystemTitle + AddStep("set online content", () => onlineMenuBanner.Current.Value = new APIMenuContent { - Image = @"https://assets.ppy.sh/main-menu/project-loved-2@2x.png", - Url = @"https://osu.ppy.sh/home/news/2023-12-21-project-loved-december-2023", + Images = new[] + { + new APIMenuImage + { + Image = @"https://assets.ppy.sh/main-menu/project-loved-2@2x.png", + Url = @"https://osu.ppy.sh/home/news/2023-12-21-project-loved-december-2023", + } + } }); - AddAssert("system title not visible", () => systemTitle.State.Value, () => Is.EqualTo(Visibility.Hidden)); + AddAssert("system title not visible", () => onlineMenuBanner.State.Value, () => Is.EqualTo(Visibility.Hidden)); AddStep("enter menu", () => InputManager.Key(Key.Enter)); - AddUntilStep("system title visible", () => systemTitle.State.Value, () => Is.EqualTo(Visibility.Visible)); - AddStep("set another title", () => systemTitle.Current.Value = new APISystemTitle + AddUntilStep("system title visible", () => onlineMenuBanner.State.Value, () => Is.EqualTo(Visibility.Visible)); + AddStep("set another title", () => onlineMenuBanner.Current.Value = new APIMenuContent { - Image = @"https://assets.ppy.sh/main-menu/wf2023-vote@2x.png", - Url = @"https://osu.ppy.sh/community/contests/189", + Images = new[] + { + new APIMenuImage + { + Image = @"https://assets.ppy.sh/main-menu/wf2023-vote@2x.png", + Url = @"https://osu.ppy.sh/community/contests/189", + } + } }); - AddStep("set title with nonexistent image", () => systemTitle.Current.Value = new APISystemTitle + AddStep("set title with nonexistent image", () => onlineMenuBanner.Current.Value = new APIMenuContent { - Image = @"https://test.invalid/@2x", // .invalid TLD reserved by https://datatracker.ietf.org/doc/html/rfc2606#section-2 - Url = @"https://osu.ppy.sh/community/contests/189", + Images = new[] + { + new APIMenuImage + { + Image = @"https://test.invalid/@2x", // .invalid TLD reserved by https://datatracker.ietf.org/doc/html/rfc2606#section-2 + Url = @"https://osu.ppy.sh/community/contests/189", + } + } }); - AddStep("unset system title", () => systemTitle.Current.Value = null); + AddStep("unset system title", () => onlineMenuBanner.Current.Value = new APIMenuContent()); } } } diff --git a/osu.Game/Online/API/Requests/GetSystemTitleRequest.cs b/osu.Game/Online/API/Requests/GetMenuContentRequest.cs similarity index 74% rename from osu.Game/Online/API/Requests/GetSystemTitleRequest.cs rename to osu.Game/Online/API/Requests/GetMenuContentRequest.cs index 52ca0c11eb..ad2bac6696 100644 --- a/osu.Game/Online/API/Requests/GetSystemTitleRequest.cs +++ b/osu.Game/Online/API/Requests/GetMenuContentRequest.cs @@ -5,9 +5,9 @@ using osu.Game.Online.API.Requests.Responses; namespace osu.Game.Online.API.Requests { - public class GetSystemTitleRequest : OsuJsonWebRequest + public class GetMenuContentRequest : OsuJsonWebRequest { - public GetSystemTitleRequest() + public GetMenuContentRequest() : base(@"https://assets.ppy.sh/lazer-status.json") { } diff --git a/osu.Game/Online/API/Requests/Responses/APIMenuContent.cs b/osu.Game/Online/API/Requests/Responses/APIMenuContent.cs new file mode 100644 index 0000000000..acee6c99ba --- /dev/null +++ b/osu.Game/Online/API/Requests/Responses/APIMenuContent.cs @@ -0,0 +1,42 @@ +// 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 Newtonsoft.Json; + +namespace osu.Game.Online.API.Requests.Responses +{ + public class APIMenuContent : IEquatable + { + /// + /// Images which should be displayed in rotation. + /// + [JsonProperty(@"images")] + public APIMenuImage[] Images { get; init; } = Array.Empty(); + + public DateTimeOffset LastUpdated { get; init; } + + public bool Equals(APIMenuContent? other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + + return LastUpdated.Equals(other.LastUpdated); + } + + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + + if (obj.GetType() != GetType()) return false; + + return Equals((APIMenuContent)obj); + } + + public override int GetHashCode() + { + return LastUpdated.GetHashCode(); + } + } +} diff --git a/osu.Game/Online/API/Requests/Responses/APIMenuImage.cs b/osu.Game/Online/API/Requests/Responses/APIMenuImage.cs new file mode 100644 index 0000000000..4824e23d4b --- /dev/null +++ b/osu.Game/Online/API/Requests/Responses/APIMenuImage.cs @@ -0,0 +1,57 @@ +// 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 Newtonsoft.Json; + +namespace osu.Game.Online.API.Requests.Responses +{ + public class APIMenuImage : IEquatable + { + /// + /// A URL pointing to the image which should be displayed. Generally should be an @2x image filename. + /// + [JsonProperty(@"image")] + public string Image { get; init; } = string.Empty; + + /// + /// A URL that should be opened on clicking the image. + /// + [JsonProperty(@"url")] + public string Url { get; init; } = string.Empty; + + /// + /// The time at which this item should begin displaying. If null, will display immediately. + /// + [JsonProperty(@"begins")] + public DateTimeOffset? Begins { get; set; } + + /// + /// The time at which this item should stop displaying. If null, will display indefinitely. + /// + [JsonProperty(@"expires")] + public DateTimeOffset? Expires { get; set; } + + public bool Equals(APIMenuImage? other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + + return Image == other.Image && Url == other.Url; + } + + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != GetType()) return false; + + return Equals((APIMenuImage)obj); + } + + public override int GetHashCode() + { + return HashCode.Combine(Image, Url); + } + } +} diff --git a/osu.Game/Online/API/Requests/Responses/APISystemTitle.cs b/osu.Game/Online/API/Requests/Responses/APISystemTitle.cs deleted file mode 100644 index bfa5c1043b..0000000000 --- a/osu.Game/Online/API/Requests/Responses/APISystemTitle.cs +++ /dev/null @@ -1,30 +0,0 @@ -// 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 Newtonsoft.Json; - -namespace osu.Game.Online.API.Requests.Responses -{ - public class APISystemTitle : IEquatable - { - [JsonProperty(@"image")] - public string Image { get; set; } = string.Empty; - - [JsonProperty(@"url")] - public string Url { get; set; } = string.Empty; - - public bool Equals(APISystemTitle? other) - { - if (ReferenceEquals(null, other)) return false; - if (ReferenceEquals(this, other)) return true; - - return Image == other.Image && Url == other.Url; - } - - public override bool Equals(object? obj) => obj is APISystemTitle other && Equals(other); - - // ReSharper disable NonReadonlyMemberInGetHashCode - public override int GetHashCode() => HashCode.Combine(Image, Url); - } -} diff --git a/osu.Game/Screens/Menu/MainMenu.cs b/osu.Game/Screens/Menu/MainMenu.cs index decb901c32..235c5d5c56 100644 --- a/osu.Game/Screens/Menu/MainMenu.cs +++ b/osu.Game/Screens/Menu/MainMenu.cs @@ -98,7 +98,7 @@ namespace osu.Game.Screens.Menu private ParallaxContainer buttonsContainer; private SongTicker songTicker; private Container logoTarget; - private SystemTitle systemTitle; + private OnlineMenuBanner onlineMenuBanner; private MenuTip menuTip; private FillFlowContainer bottomElementsFlow; private SupporterDisplay supporterDisplay; @@ -178,7 +178,7 @@ namespace osu.Game.Screens.Menu Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, }, - systemTitle = new SystemTitle + onlineMenuBanner = new OnlineMenuBanner { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, @@ -201,12 +201,12 @@ namespace osu.Game.Screens.Menu case ButtonSystemState.Initial: case ButtonSystemState.Exit: ApplyToBackground(b => b.FadeColour(Color4.White, 500, Easing.OutSine)); - systemTitle.State.Value = Visibility.Hidden; + onlineMenuBanner.State.Value = Visibility.Hidden; break; default: ApplyToBackground(b => b.FadeColour(OsuColour.Gray(0.8f), 500, Easing.OutSine)); - systemTitle.State.Value = Visibility.Visible; + onlineMenuBanner.State.Value = Visibility.Visible; break; } }; diff --git a/osu.Game/Screens/Menu/SystemTitle.cs b/osu.Game/Screens/Menu/OnlineMenuBanner.cs similarity index 79% rename from osu.Game/Screens/Menu/SystemTitle.cs rename to osu.Game/Screens/Menu/OnlineMenuBanner.cs index 813a470ed6..cf20196f85 100644 --- a/osu.Game/Screens/Menu/SystemTitle.cs +++ b/osu.Game/Screens/Menu/OnlineMenuBanner.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Linq; using System.Threading; using System.Threading.Tasks; using osu.Framework.Allocation; @@ -18,42 +19,28 @@ using osu.Game.Online.API.Requests.Responses; namespace osu.Game.Screens.Menu { - public partial class SystemTitle : VisibilityContainer + public partial class OnlineMenuBanner : VisibilityContainer { - internal Bindable Current { get; } = new Bindable(); + internal Bindable Current { get; } = new Bindable(new APIMenuContent()); private const float transition_duration = 500; private Container content = null!; private CancellationTokenSource? cancellationTokenSource; - private SystemTitleImage? currentImage; - - private ScheduledDelegate? openUrlAction; + private MenuImage? currentImage; [BackgroundDependencyLoader] - private void load(OsuGame? game) + private void load() { AutoSizeAxes = Axes.Both; AutoSizeDuration = transition_duration; AutoSizeEasing = Easing.OutQuint; - InternalChild = content = new OsuClickableContainer + InternalChild = content = new Container { Anchor = Anchor.Centre, Origin = Anchor.Centre, AutoSizeAxes = Axes.Both, - Action = () => - { - currentImage?.Flash(); - - // Delay slightly to allow animation to play out. - openUrlAction?.Cancel(); - openUrlAction = Scheduler.AddDelayed(() => - { - if (!string.IsNullOrEmpty(Current.Value?.Url)) - game?.HandleLink(Current.Value.Url); - }, 250); - } }; } @@ -98,7 +85,7 @@ namespace osu.Game.Screens.Menu private void checkForUpdates() { - var request = new GetSystemTitleRequest(); + var request = new GetMenuContentRequest(); Task.Run(() => request.Perform()) .ContinueWith(r => { @@ -121,12 +108,12 @@ namespace osu.Game.Screens.Menu cancellationTokenSource = null; currentImage?.FadeOut(500, Easing.OutQuint).Expire(); - if (string.IsNullOrEmpty(Current.Value?.Image)) + if (Current.Value.Images.Length == 0) return; - LoadComponentAsync(new SystemTitleImage(Current.Value), loaded => + LoadComponentAsync(new MenuImage(Current.Value.Images.First()), loaded => { - if (!loaded.SystemTitle.Equals(Current.Value)) + if (!loaded.Image.Equals(Current.Value.Images.First())) loaded.Dispose(); content.Add(currentImage = loaded); @@ -134,22 +121,24 @@ namespace osu.Game.Screens.Menu } [LongRunningLoad] - private partial class SystemTitleImage : CompositeDrawable + private partial class MenuImage : OsuClickableContainer { - public readonly APISystemTitle SystemTitle; + public readonly APIMenuImage Image; private Sprite flash = null!; - public SystemTitleImage(APISystemTitle systemTitle) + private ScheduledDelegate? openUrlAction; + + public MenuImage(APIMenuImage image) { - SystemTitle = systemTitle; + Image = image; } [BackgroundDependencyLoader] - private void load(LargeTextureStore textureStore) + private void load(LargeTextureStore textureStore, OsuGame game) { - Texture? texture = textureStore.Get(SystemTitle.Image); - if (texture != null && SystemTitle.Image.Contains(@"@2x")) + Texture? texture = textureStore.Get(Image.Image); + if (texture != null && Image.Image.Contains(@"@2x")) texture.ScaleAdjust *= 2; AutoSizeAxes = Axes.Both; @@ -163,6 +152,19 @@ namespace osu.Game.Screens.Menu Blending = BlendingParameters.Additive, }, }; + + Action = () => + { + Flash(); + + // Delay slightly to allow animation to play out. + openUrlAction?.Cancel(); + openUrlAction = Scheduler.AddDelayed(() => + { + if (!string.IsNullOrEmpty(Image.Url)) + game?.HandleLink(Image.Url); + }, 250); + }; } protected override void LoadComplete() From 4c82e44291fdf23bf324d1d8f4a3092ede9437da Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 23 Mar 2024 20:07:17 +0800 Subject: [PATCH 0412/2493] Add isolated test coverage of online menu banner --- .../Visual/Menus/TestSceneMainMenu.cs | 23 ------ .../Visual/Menus/TestSceneOnlineMenuBanner.cs | 71 +++++++++++++++++++ 2 files changed, 71 insertions(+), 23 deletions(-) create mode 100644 osu.Game.Tests/Visual/Menus/TestSceneOnlineMenuBanner.cs diff --git a/osu.Game.Tests/Visual/Menus/TestSceneMainMenu.cs b/osu.Game.Tests/Visual/Menus/TestSceneMainMenu.cs index 3c78edb8a5..e2a841d79a 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneMainMenu.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneMainMenu.cs @@ -32,29 +32,6 @@ namespace osu.Game.Tests.Visual.Menus AddAssert("system title not visible", () => onlineMenuBanner.State.Value, () => Is.EqualTo(Visibility.Hidden)); AddStep("enter menu", () => InputManager.Key(Key.Enter)); AddUntilStep("system title visible", () => onlineMenuBanner.State.Value, () => Is.EqualTo(Visibility.Visible)); - AddStep("set another title", () => onlineMenuBanner.Current.Value = new APIMenuContent - { - Images = new[] - { - new APIMenuImage - { - Image = @"https://assets.ppy.sh/main-menu/wf2023-vote@2x.png", - Url = @"https://osu.ppy.sh/community/contests/189", - } - } - }); - AddStep("set title with nonexistent image", () => onlineMenuBanner.Current.Value = new APIMenuContent - { - Images = new[] - { - new APIMenuImage - { - Image = @"https://test.invalid/@2x", // .invalid TLD reserved by https://datatracker.ietf.org/doc/html/rfc2606#section-2 - Url = @"https://osu.ppy.sh/community/contests/189", - } - } - }); - AddStep("unset system title", () => onlineMenuBanner.Current.Value = new APIMenuContent()); } } } diff --git a/osu.Game.Tests/Visual/Menus/TestSceneOnlineMenuBanner.cs b/osu.Game.Tests/Visual/Menus/TestSceneOnlineMenuBanner.cs new file mode 100644 index 0000000000..a80212e0a1 --- /dev/null +++ b/osu.Game.Tests/Visual/Menus/TestSceneOnlineMenuBanner.cs @@ -0,0 +1,71 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Testing; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Screens.Menu; + +namespace osu.Game.Tests.Visual.Menus +{ + public partial class TestSceneOnlineMenuBanner : OsuTestScene + { + private OnlineMenuBanner onlineMenuBanner = null!; + + [SetUpSteps] + public void SetUpSteps() + { + AddStep("Create banner", () => + { + Child = onlineMenuBanner = new OnlineMenuBanner + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + State = { Value = Visibility.Visible } + }; + }); + } + + [Test] + public void TestBasic() + { + AddAssert("system title not visible", () => onlineMenuBanner.State.Value, () => Is.EqualTo(Visibility.Hidden)); + AddStep("set online content", () => onlineMenuBanner.Current.Value = new APIMenuContent + { + Images = new[] + { + new APIMenuImage + { + Image = @"https://assets.ppy.sh/main-menu/project-loved-2@2x.png", + Url = @"https://osu.ppy.sh/home/news/2023-12-21-project-loved-december-2023", + } + }, + }); + AddStep("set another title", () => onlineMenuBanner.Current.Value = new APIMenuContent + { + Images = new[] + { + new APIMenuImage + { + Image = @"https://assets.ppy.sh/main-menu/wf2023-vote@2x.png", + Url = @"https://osu.ppy.sh/community/contests/189", + } + } + }); + AddStep("set title with nonexistent image", () => onlineMenuBanner.Current.Value = new APIMenuContent + { + Images = new[] + { + new APIMenuImage + { + Image = @"https://test.invalid/@2x", // .invalid TLD reserved by https://datatracker.ietf.org/doc/html/rfc2606#section-2 + Url = @"https://osu.ppy.sh/community/contests/189", + } + } + }); + AddStep("unset system title", () => onlineMenuBanner.Current.Value = new APIMenuContent()); + } + } +} From ec4a9a5fdd4ffa1df41b2e26227d27d1277ae8d2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 23 Mar 2024 23:07:31 +0800 Subject: [PATCH 0413/2493] Make work again for simple case --- .../API/Requests/Responses/APIMenuContent.cs | 12 ++++++--- osu.Game/Screens/Menu/OnlineMenuBanner.cs | 25 +++++++------------ 2 files changed, 17 insertions(+), 20 deletions(-) diff --git a/osu.Game/Online/API/Requests/Responses/APIMenuContent.cs b/osu.Game/Online/API/Requests/Responses/APIMenuContent.cs index acee6c99ba..7b53488030 100644 --- a/osu.Game/Online/API/Requests/Responses/APIMenuContent.cs +++ b/osu.Game/Online/API/Requests/Responses/APIMenuContent.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Linq; using Newtonsoft.Json; namespace osu.Game.Online.API.Requests.Responses @@ -14,14 +15,12 @@ namespace osu.Game.Online.API.Requests.Responses [JsonProperty(@"images")] public APIMenuImage[] Images { get; init; } = Array.Empty(); - public DateTimeOffset LastUpdated { get; init; } - public bool Equals(APIMenuContent? other) { if (ReferenceEquals(null, other)) return false; if (ReferenceEquals(this, other)) return true; - return LastUpdated.Equals(other.LastUpdated); + return Images.SequenceEqual(other.Images); } public override bool Equals(object? obj) @@ -36,7 +35,12 @@ namespace osu.Game.Online.API.Requests.Responses public override int GetHashCode() { - return LastUpdated.GetHashCode(); + var hash = new HashCode(); + + foreach (var image in Images) + hash.Add(image.GetHashCode()); + + return hash.ToHashCode(); } } } diff --git a/osu.Game/Screens/Menu/OnlineMenuBanner.cs b/osu.Game/Screens/Menu/OnlineMenuBanner.cs index cf20196f85..613a6eed4c 100644 --- a/osu.Game/Screens/Menu/OnlineMenuBanner.cs +++ b/osu.Game/Screens/Menu/OnlineMenuBanner.cs @@ -78,7 +78,7 @@ namespace osu.Game.Screens.Menu { base.LoadComplete(); - Current.BindValueChanged(_ => loadNewImage(), true); + Current.BindValueChanged(_ => loadNewImages(), true); checkForUpdates(); } @@ -102,7 +102,7 @@ namespace osu.Game.Screens.Menu }); } - private void loadNewImage() + private void loadNewImages() { cancellationTokenSource?.Cancel(); cancellationTokenSource = null; @@ -131,19 +131,19 @@ namespace osu.Game.Screens.Menu public MenuImage(APIMenuImage image) { + AutoSizeAxes = Axes.Both; + Image = image; } [BackgroundDependencyLoader] - private void load(LargeTextureStore textureStore, OsuGame game) + private void load(LargeTextureStore textureStore, OsuGame? game) { Texture? texture = textureStore.Get(Image.Image); if (texture != null && Image.Image.Contains(@"@2x")) texture.ScaleAdjust *= 2; - AutoSizeAxes = Axes.Both; - - InternalChildren = new Drawable[] + Children = new Drawable[] { new Sprite { Texture = texture }, flash = new Sprite @@ -155,7 +155,9 @@ namespace osu.Game.Screens.Menu Action = () => { - Flash(); + flash.FadeInFromZero(50) + .Then() + .FadeOut(500, Easing.OutQuint); // Delay slightly to allow animation to play out. openUrlAction?.Cancel(); @@ -174,15 +176,6 @@ namespace osu.Game.Screens.Menu this.FadeInFromZero(500, Easing.OutQuint); flash.FadeOutFromOne(4000, Easing.OutQuint); } - - public Drawable Flash() - { - flash.FadeInFromZero(50) - .Then() - .FadeOut(500, Easing.OutQuint); - - return this; - } } } } From a4c619ea97c190a9a4929d89268677af7b72a985 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 24 Mar 2024 15:14:56 +0800 Subject: [PATCH 0414/2493] Add basic support for loading multiple images --- .../Visual/Menus/TestSceneOnlineMenuBanner.cs | 21 ++++- osu.Game/Screens/Menu/OnlineMenuBanner.cs | 80 ++++++++++--------- 2 files changed, 64 insertions(+), 37 deletions(-) diff --git a/osu.Game.Tests/Visual/Menus/TestSceneOnlineMenuBanner.cs b/osu.Game.Tests/Visual/Menus/TestSceneOnlineMenuBanner.cs index a80212e0a1..4cc379a18b 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneOnlineMenuBanner.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneOnlineMenuBanner.cs @@ -31,7 +31,6 @@ namespace osu.Game.Tests.Visual.Menus [Test] public void TestBasic() { - AddAssert("system title not visible", () => onlineMenuBanner.State.Value, () => Is.EqualTo(Visibility.Hidden)); AddStep("set online content", () => onlineMenuBanner.Current.Value = new APIMenuContent { Images = new[] @@ -43,6 +42,7 @@ namespace osu.Game.Tests.Visual.Menus } }, }); + AddStep("set another title", () => onlineMenuBanner.Current.Value = new APIMenuContent { Images = new[] @@ -54,6 +54,24 @@ namespace osu.Game.Tests.Visual.Menus } } }); + + AddStep("set multiple images", () => onlineMenuBanner.Current.Value = new APIMenuContent + { + Images = new[] + { + new APIMenuImage + { + Image = @"https://assets.ppy.sh/main-menu/project-loved-2@2x.png", + Url = @"https://osu.ppy.sh/home/news/2023-12-21-project-loved-december-2023", + }, + new APIMenuImage + { + Image = @"https://assets.ppy.sh/main-menu/wf2023-vote@2x.png", + Url = @"https://osu.ppy.sh/community/contests/189", + } + }, + }); + AddStep("set title with nonexistent image", () => onlineMenuBanner.Current.Value = new APIMenuContent { Images = new[] @@ -65,6 +83,7 @@ namespace osu.Game.Tests.Visual.Menus } } }); + AddStep("unset system title", () => onlineMenuBanner.Current.Value = new APIMenuContent()); } } diff --git a/osu.Game/Screens/Menu/OnlineMenuBanner.cs b/osu.Game/Screens/Menu/OnlineMenuBanner.cs index 613a6eed4c..587a93fb67 100644 --- a/osu.Game/Screens/Menu/OnlineMenuBanner.cs +++ b/osu.Game/Screens/Menu/OnlineMenuBanner.cs @@ -27,7 +27,6 @@ namespace osu.Game.Screens.Menu private Container content = null!; private CancellationTokenSource? cancellationTokenSource; - private MenuImage? currentImage; [BackgroundDependencyLoader] private void load() @@ -48,32 +47,6 @@ namespace osu.Game.Screens.Menu protected override void PopOut() => content.FadeOut(transition_duration, Easing.OutQuint); - protected override bool OnHover(HoverEvent e) - { - content.ScaleTo(1.05f, 2000, Easing.OutQuint); - return base.OnHover(e); - } - - protected override void OnHoverLost(HoverLostEvent e) - { - content.ScaleTo(1f, 500, Easing.OutQuint); - base.OnHoverLost(e); - } - - protected override bool OnMouseDown(MouseDownEvent e) - { - content.ScaleTo(0.95f, 500, Easing.OutQuint); - return base.OnMouseDown(e); - } - - protected override void OnMouseUp(MouseUpEvent e) - { - content - .ScaleTo(0.95f) - .ScaleTo(1, 500, Easing.OutElastic); - base.OnMouseUp(e); - } - protected override void LoadComplete() { base.LoadComplete(); @@ -106,17 +79,26 @@ namespace osu.Game.Screens.Menu { cancellationTokenSource?.Cancel(); cancellationTokenSource = null; - currentImage?.FadeOut(500, Easing.OutQuint).Expire(); - if (Current.Value.Images.Length == 0) + var newContent = Current.Value; + + foreach (var i in content) + { + i.FadeOutFromOne(100, Easing.OutQuint) + .Expire(); + } + + if (newContent.Images.Length == 0) return; - LoadComponentAsync(new MenuImage(Current.Value.Images.First()), loaded => + LoadComponentsAsync(newContent.Images.Select(i => new MenuImage(i)), loaded => { - if (!loaded.Image.Equals(Current.Value.Images.First())) - loaded.Dispose(); + if (!newContent.Equals(Current.Value)) + return; - content.Add(currentImage = loaded); + content.AddRange(loaded); + + loaded.First().Show(); }, (cancellationTokenSource ??= new CancellationTokenSource()).Token); } @@ -132,6 +114,8 @@ namespace osu.Game.Screens.Menu public MenuImage(APIMenuImage image) { AutoSizeAxes = Axes.Both; + Anchor = Anchor.BottomCentre; + Origin = Anchor.BottomCentre; Image = image; } @@ -169,13 +153,37 @@ namespace osu.Game.Screens.Menu }; } - protected override void LoadComplete() + public override void Show() { - base.LoadComplete(); - this.FadeInFromZero(500, Easing.OutQuint); flash.FadeOutFromOne(4000, Easing.OutQuint); } + + protected override bool OnHover(HoverEvent e) + { + this.ScaleTo(1.05f, 2000, Easing.OutQuint); + return true; + } + + protected override void OnHoverLost(HoverLostEvent e) + { + this.ScaleTo(1f, 500, Easing.OutQuint); + base.OnHoverLost(e); + } + + protected override bool OnMouseDown(MouseDownEvent e) + { + this.ScaleTo(0.95f, 500, Easing.OutQuint); + return true; + } + + protected override void OnMouseUp(MouseUpEvent e) + { + this + .ScaleTo(0.95f) + .ScaleTo(1, 500, Easing.OutElastic); + base.OnMouseUp(e); + } } } } From d0b164b44f6d986f965c469cd838bfacbe0a3de6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 24 Mar 2024 23:37:30 +0800 Subject: [PATCH 0415/2493] Add automatic rotation support --- osu.Game/Screens/Menu/OnlineMenuBanner.cs | 38 +++++++++++++++++++---- 1 file changed, 32 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/Menu/OnlineMenuBanner.cs b/osu.Game/Screens/Menu/OnlineMenuBanner.cs index 587a93fb67..a4648265ae 100644 --- a/osu.Game/Screens/Menu/OnlineMenuBanner.cs +++ b/osu.Game/Screens/Menu/OnlineMenuBanner.cs @@ -28,6 +28,10 @@ namespace osu.Game.Screens.Menu private Container content = null!; private CancellationTokenSource? cancellationTokenSource; + private int displayIndex = -1; + + private ScheduledDelegate? nextDisplay; + [BackgroundDependencyLoader] private void load() { @@ -77,16 +81,16 @@ namespace osu.Game.Screens.Menu private void loadNewImages() { + nextDisplay?.Cancel(); + cancellationTokenSource?.Cancel(); cancellationTokenSource = null; var newContent = Current.Value; - foreach (var i in content) - { - i.FadeOutFromOne(100, Easing.OutQuint) - .Expire(); - } + // A better fade out would be nice, but the menu content changes *very* rarely + // so let's keep things simple for now. + content.Clear(true); if (newContent.Images.Length == 0) return; @@ -96,12 +100,34 @@ namespace osu.Game.Screens.Menu if (!newContent.Equals(Current.Value)) return; + // start hidden + foreach (var image in loaded) + image.Hide(); + content.AddRange(loaded); - loaded.First().Show(); + displayIndex = -1; + showNext(); }, (cancellationTokenSource ??= new CancellationTokenSource()).Token); } + private void showNext() + { + nextDisplay?.Cancel(); + + bool previousShowing = displayIndex >= 0; + if (previousShowing) + content[displayIndex % content.Count].FadeOut(400, Easing.OutQuint); + + displayIndex++; + + using (BeginDelayedSequence(previousShowing ? 300 : 0)) + content[displayIndex % content.Count].Show(); + + if (content.Count > 1) + nextDisplay = Scheduler.AddDelayed(showNext, 12000); + } + [LongRunningLoad] private partial class MenuImage : OsuClickableContainer { From 3847aae57d88ea2156c7599c53a0e994fffd4760 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 25 Mar 2024 12:14:40 +0800 Subject: [PATCH 0416/2493] Don't rotate when hovering --- osu.Game/Screens/Menu/OnlineMenuBanner.cs | 26 ++++++++++++++++------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/osu.Game/Screens/Menu/OnlineMenuBanner.cs b/osu.Game/Screens/Menu/OnlineMenuBanner.cs index a4648265ae..74062d5b9f 100644 --- a/osu.Game/Screens/Menu/OnlineMenuBanner.cs +++ b/osu.Game/Screens/Menu/OnlineMenuBanner.cs @@ -21,6 +21,8 @@ namespace osu.Game.Screens.Menu { public partial class OnlineMenuBanner : VisibilityContainer { + public double DelayBetweenRotation = 7500; + internal Bindable Current { get; } = new Bindable(new APIMenuContent()); private const float transition_duration = 500; @@ -115,21 +117,29 @@ namespace osu.Game.Screens.Menu { nextDisplay?.Cancel(); - bool previousShowing = displayIndex >= 0; - if (previousShowing) - content[displayIndex % content.Count].FadeOut(400, Easing.OutQuint); + // If the user is hovering a banner, don't rotate yet. + bool anyHovered = content.Any(i => i.IsHovered); - displayIndex++; + if (!anyHovered) + { + bool previousShowing = displayIndex >= 0; + if (previousShowing) + content[displayIndex % content.Count].FadeOut(400, Easing.OutQuint); - using (BeginDelayedSequence(previousShowing ? 300 : 0)) - content[displayIndex % content.Count].Show(); + displayIndex++; + + using (BeginDelayedSequence(previousShowing ? 300 : 0)) + content[displayIndex % content.Count].Show(); + } if (content.Count > 1) - nextDisplay = Scheduler.AddDelayed(showNext, 12000); + { + nextDisplay = Scheduler.AddDelayed(showNext, DelayBetweenRotation); + } } [LongRunningLoad] - private partial class MenuImage : OsuClickableContainer + public partial class MenuImage : OsuClickableContainer { public readonly APIMenuImage Image; From e9f15534ed7ea64b7cfe77360f488b3a63e703db Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 25 Mar 2024 12:14:47 +0800 Subject: [PATCH 0417/2493] Improve test coverage --- .../Visual/Menus/TestSceneOnlineMenuBanner.cs | 75 ++++++++++++++++--- osu.Game/Screens/Menu/OnlineMenuBanner.cs | 7 +- 2 files changed, 71 insertions(+), 11 deletions(-) diff --git a/osu.Game.Tests/Visual/Menus/TestSceneOnlineMenuBanner.cs b/osu.Game.Tests/Visual/Menus/TestSceneOnlineMenuBanner.cs index 4cc379a18b..6be5b80983 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneOnlineMenuBanner.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneOnlineMenuBanner.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.Linq; using NUnit.Framework; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -21,6 +22,8 @@ namespace osu.Game.Tests.Visual.Menus { Child = onlineMenuBanner = new OnlineMenuBanner { + FetchOnlineContent = false, + DelayBetweenRotation = 500, Anchor = Anchor.Centre, Origin = Anchor.Centre, State = { Value = Visibility.Visible } @@ -43,6 +46,18 @@ namespace osu.Game.Tests.Visual.Menus }, }); + AddUntilStep("wait for one image shown", () => + { + var images = onlineMenuBanner.ChildrenOfType(); + + if (images.Count() != 1) + return false; + + var image = images.Single(); + + return image.IsPresent && image.Image.Url == "https://osu.ppy.sh/home/news/2023-12-21-project-loved-december-2023"; + }); + AddStep("set another title", () => onlineMenuBanner.Current.Value = new APIMenuContent { Images = new[] @@ -55,6 +70,40 @@ namespace osu.Game.Tests.Visual.Menus } }); + AddUntilStep("wait for new image shown", () => + { + var images = onlineMenuBanner.ChildrenOfType(); + + if (images.Count() != 1) + return false; + + var image = images.Single(); + + return image.IsPresent && image.Image.Url == "https://osu.ppy.sh/community/contests/189"; + }); + + AddStep("set title with nonexistent image", () => onlineMenuBanner.Current.Value = new APIMenuContent + { + Images = new[] + { + new APIMenuImage + { + Image = @"https://test.invalid/@2x", // .invalid TLD reserved by https://datatracker.ietf.org/doc/html/rfc2606#section-2 + Url = @"https://osu.ppy.sh/community/contests/189", + } + } + }); + + AddUntilStep("wait for no image shown", () => !onlineMenuBanner.ChildrenOfType().Any()); + + AddStep("unset system title", () => onlineMenuBanner.Current.Value = new APIMenuContent()); + + AddUntilStep("wait for no image shown", () => !onlineMenuBanner.ChildrenOfType().Any()); + } + + [Test] + public void TestMultipleImages() + { AddStep("set multiple images", () => onlineMenuBanner.Current.Value = new APIMenuContent { Images = new[] @@ -72,19 +121,25 @@ namespace osu.Game.Tests.Visual.Menus }, }); - AddStep("set title with nonexistent image", () => onlineMenuBanner.Current.Value = new APIMenuContent + AddUntilStep("wait for first image shown", () => { - Images = new[] - { - new APIMenuImage - { - Image = @"https://test.invalid/@2x", // .invalid TLD reserved by https://datatracker.ietf.org/doc/html/rfc2606#section-2 - Url = @"https://osu.ppy.sh/community/contests/189", - } - } + var images = onlineMenuBanner.ChildrenOfType(); + + if (images.Count() != 2) + return false; + + return images.First().IsPresent && !images.Last().IsPresent; }); - AddStep("unset system title", () => onlineMenuBanner.Current.Value = new APIMenuContent()); + AddUntilStep("wait for second image shown", () => + { + var images = onlineMenuBanner.ChildrenOfType(); + + if (images.Count() != 2) + return false; + + return !images.First().IsPresent && images.Last().IsPresent; + }); } } } diff --git a/osu.Game/Screens/Menu/OnlineMenuBanner.cs b/osu.Game/Screens/Menu/OnlineMenuBanner.cs index 74062d5b9f..2ab6417370 100644 --- a/osu.Game/Screens/Menu/OnlineMenuBanner.cs +++ b/osu.Game/Screens/Menu/OnlineMenuBanner.cs @@ -21,7 +21,9 @@ namespace osu.Game.Screens.Menu { public partial class OnlineMenuBanner : VisibilityContainer { - public double DelayBetweenRotation = 7500; + public double DelayBetweenRotation { get; set; } = 7500; + + public bool FetchOnlineContent { get; set; } = true; internal Bindable Current { get; } = new Bindable(new APIMenuContent()); @@ -64,6 +66,9 @@ namespace osu.Game.Screens.Menu private void checkForUpdates() { + if (!FetchOnlineContent) + return; + var request = new GetMenuContentRequest(); Task.Run(() => request.Perform()) .ContinueWith(r => From f0614928b182bf2d575c5e298e29d2254fd28ecb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 25 Mar 2024 13:19:12 +0800 Subject: [PATCH 0418/2493] Read from new location --- osu.Game/Online/API/Requests/GetMenuContentRequest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Online/API/Requests/GetMenuContentRequest.cs b/osu.Game/Online/API/Requests/GetMenuContentRequest.cs index ad2bac6696..26747489d6 100644 --- a/osu.Game/Online/API/Requests/GetMenuContentRequest.cs +++ b/osu.Game/Online/API/Requests/GetMenuContentRequest.cs @@ -8,7 +8,7 @@ namespace osu.Game.Online.API.Requests public class GetMenuContentRequest : OsuJsonWebRequest { public GetMenuContentRequest() - : base(@"https://assets.ppy.sh/lazer-status.json") + : base(@"https://assets.ppy.sh/menu-content.json") { } } From 057f86dd145dd5d0ba573737ca61aceb27dcc70a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 25 Mar 2024 14:28:23 +0800 Subject: [PATCH 0419/2493] Add handling of expiration --- .../Visual/Menus/TestSceneOnlineMenuBanner.cs | 60 +++++++++++++++++++ .../API/Requests/Responses/APIMenuImage.cs | 4 ++ osu.Game/Screens/Menu/OnlineMenuBanner.cs | 60 ++++++++++++------- 3 files changed, 102 insertions(+), 22 deletions(-) diff --git a/osu.Game.Tests/Visual/Menus/TestSceneOnlineMenuBanner.cs b/osu.Game.Tests/Visual/Menus/TestSceneOnlineMenuBanner.cs index 6be5b80983..2dd08ce306 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneOnlineMenuBanner.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneOnlineMenuBanner.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.Linq; using NUnit.Framework; using osu.Framework.Graphics; @@ -141,5 +142,64 @@ namespace osu.Game.Tests.Visual.Menus return !images.First().IsPresent && images.Last().IsPresent; }); } + + [Test] + public void TestExpiry() + { + AddStep("set multiple images, second expiring soon", () => onlineMenuBanner.Current.Value = new APIMenuContent + { + Images = new[] + { + new APIMenuImage + { + Image = @"https://assets.ppy.sh/main-menu/project-loved-2@2x.png", + Url = @"https://osu.ppy.sh/home/news/2023-12-21-project-loved-december-2023", + }, + new APIMenuImage + { + Image = @"https://assets.ppy.sh/main-menu/wf2023-vote@2x.png", + Url = @"https://osu.ppy.sh/community/contests/189", + Expires = DateTimeOffset.Now.AddSeconds(2), + } + }, + }); + + AddUntilStep("wait for first image shown", () => + { + var images = onlineMenuBanner.ChildrenOfType(); + + if (images.Count() != 2) + return false; + + return images.First().IsPresent && !images.Last().IsPresent; + }); + + AddUntilStep("wait for second image shown", () => + { + var images = onlineMenuBanner.ChildrenOfType(); + + if (images.Count() != 2) + return false; + + return !images.First().IsPresent && images.Last().IsPresent; + }); + + AddUntilStep("wait for expiry", () => + { + return onlineMenuBanner + .ChildrenOfType() + .Any(i => !i.Image.IsCurrent); + }); + + AddUntilStep("wait for first image shown", () => + { + var images = onlineMenuBanner.ChildrenOfType(); + + if (images.Count() != 2) + return false; + + return images.First().IsPresent && !images.Last().IsPresent; + }); + } } } diff --git a/osu.Game/Online/API/Requests/Responses/APIMenuImage.cs b/osu.Game/Online/API/Requests/Responses/APIMenuImage.cs index 4824e23d4b..42129ca96e 100644 --- a/osu.Game/Online/API/Requests/Responses/APIMenuImage.cs +++ b/osu.Game/Online/API/Requests/Responses/APIMenuImage.cs @@ -20,6 +20,10 @@ namespace osu.Game.Online.API.Requests.Responses [JsonProperty(@"url")] public string Url { get; init; } = string.Empty; + public bool IsCurrent => + (Begins == null || Begins < DateTimeOffset.UtcNow) && + (Expires == null || Expires > DateTimeOffset.UtcNow); + /// /// The time at which this item should begin displaying. If null, will display immediately. /// diff --git a/osu.Game/Screens/Menu/OnlineMenuBanner.cs b/osu.Game/Screens/Menu/OnlineMenuBanner.cs index 2ab6417370..55ceb84d7b 100644 --- a/osu.Game/Screens/Menu/OnlineMenuBanner.cs +++ b/osu.Game/Screens/Menu/OnlineMenuBanner.cs @@ -29,7 +29,7 @@ namespace osu.Game.Screens.Menu private const float transition_duration = 500; - private Container content = null!; + private Container content = null!; private CancellationTokenSource? cancellationTokenSource; private int displayIndex = -1; @@ -43,7 +43,7 @@ namespace osu.Game.Screens.Menu AutoSizeDuration = transition_duration; AutoSizeEasing = Easing.OutQuint; - InternalChild = content = new Container + InternalChild = content = new Container { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -59,8 +59,7 @@ namespace osu.Game.Screens.Menu { base.LoadComplete(); - Current.BindValueChanged(_ => loadNewImages(), true); - + Current.BindValueChanged(loadNewImages, true); checkForUpdates(); } @@ -86,25 +85,24 @@ namespace osu.Game.Screens.Menu }); } - private void loadNewImages() + /// + /// Takes and materialises and displays drawables for all valid images to be displayed. + /// + /// + private void loadNewImages(ValueChangedEvent images) { nextDisplay?.Cancel(); cancellationTokenSource?.Cancel(); cancellationTokenSource = null; - var newContent = Current.Value; - // A better fade out would be nice, but the menu content changes *very* rarely // so let's keep things simple for now. content.Clear(true); - if (newContent.Images.Length == 0) - return; - - LoadComponentsAsync(newContent.Images.Select(i => new MenuImage(i)), loaded => + LoadComponentsAsync(images.NewValue.Images.Select(i => new MenuImage(i)), loaded => { - if (!newContent.Equals(Current.Value)) + if (!images.NewValue.Equals(Current.Value)) return; // start hidden @@ -127,20 +125,38 @@ namespace osu.Game.Screens.Menu if (!anyHovered) { - bool previousShowing = displayIndex >= 0; - if (previousShowing) - content[displayIndex % content.Count].FadeOut(400, Easing.OutQuint); + int previousIndex = displayIndex; - displayIndex++; + if (displayIndex == -1) + displayIndex = 0; - using (BeginDelayedSequence(previousShowing ? 300 : 0)) - content[displayIndex % content.Count].Show(); + // To handle expiration simply, arrange all images in best-next order. + // Fade in the first valid one, then handle fading out the last if required. + var currentRotation = content + .Skip(displayIndex + 1) + .Concat(content.Take(displayIndex + 1)); + + foreach (var image in currentRotation) + { + if (!image.Image.IsCurrent) continue; + + using (BeginDelayedSequence(previousIndex >= 0 ? 300 : 0)) + { + displayIndex = content.IndexOf(image); + + if (displayIndex != previousIndex) + image.Show(); + + break; + } + } + + if (previousIndex >= 0 && previousIndex != displayIndex) + content[previousIndex].FadeOut(400, Easing.OutQuint); } - if (content.Count > 1) - { - nextDisplay = Scheduler.AddDelayed(showNext, DelayBetweenRotation); - } + // Re-scheduling this method will both handle rotation and re-checking for expiration dates. + nextDisplay = Scheduler.AddDelayed(showNext, DelayBetweenRotation); } [LongRunningLoad] From bb9fa52fda51f46e4e0fe39e2be8196ab0dc0fba Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 25 Mar 2024 14:53:05 +0800 Subject: [PATCH 0420/2493] Fix `displayIndex` not being correctly set to `-1` after last expiry date --- .../Visual/Menus/TestSceneOnlineMenuBanner.cs | 36 ++++++++++++++++++- osu.Game/Screens/Menu/OnlineMenuBanner.cs | 11 +++--- 2 files changed, 41 insertions(+), 6 deletions(-) diff --git a/osu.Game.Tests/Visual/Menus/TestSceneOnlineMenuBanner.cs b/osu.Game.Tests/Visual/Menus/TestSceneOnlineMenuBanner.cs index 2dd08ce306..380085ce04 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneOnlineMenuBanner.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneOnlineMenuBanner.cs @@ -144,7 +144,41 @@ namespace osu.Game.Tests.Visual.Menus } [Test] - public void TestExpiry() + public void TestFutureSingle() + { + AddStep("set image with time constraints", () => onlineMenuBanner.Current.Value = new APIMenuContent + { + Images = new[] + { + new APIMenuImage + { + Image = @"https://assets.ppy.sh/main-menu/project-loved-2@2x.png", + Url = @"https://osu.ppy.sh/home/news/2023-12-21-project-loved-december-2023", + Begins = DateTimeOffset.Now.AddSeconds(2), + Expires = DateTimeOffset.Now.AddSeconds(5), + }, + }, + }); + + AddUntilStep("wait for no image shown", () => !onlineMenuBanner.ChildrenOfType().Any(i => i.IsPresent)); + + AddUntilStep("wait for one image shown", () => + { + var images = onlineMenuBanner.ChildrenOfType(); + + if (images.Count() != 1) + return false; + + var image = images.Single(); + + return image.IsPresent && image.Image.Url == "https://osu.ppy.sh/home/news/2023-12-21-project-loved-december-2023"; + }); + + AddUntilStep("wait for no image shown", () => !onlineMenuBanner.ChildrenOfType().Any(i => i.IsPresent)); + } + + [Test] + public void TestExpiryMultiple() { AddStep("set multiple images, second expiring soon", () => onlineMenuBanner.Current.Value = new APIMenuContent { diff --git a/osu.Game/Screens/Menu/OnlineMenuBanner.cs b/osu.Game/Screens/Menu/OnlineMenuBanner.cs index 55ceb84d7b..37bec7aa63 100644 --- a/osu.Game/Screens/Menu/OnlineMenuBanner.cs +++ b/osu.Game/Screens/Menu/OnlineMenuBanner.cs @@ -127,14 +127,15 @@ namespace osu.Game.Screens.Menu { int previousIndex = displayIndex; - if (displayIndex == -1) - displayIndex = 0; - // To handle expiration simply, arrange all images in best-next order. // Fade in the first valid one, then handle fading out the last if required. var currentRotation = content - .Skip(displayIndex + 1) - .Concat(content.Take(displayIndex + 1)); + .Skip(Math.Max(0, previousIndex) + 1) + .Concat(content.Take(Math.Max(0, previousIndex) + 1)); + + // After the loop, displayIndex will be the new valid index or -1 if + // none valid. + displayIndex = -1; foreach (var image in currentRotation) { From 78037fa4773dfc8f8063d45e104bd84131515bcc Mon Sep 17 00:00:00 2001 From: Arthur Araujo Date: Mon, 25 Mar 2024 04:19:14 -0300 Subject: [PATCH 0421/2493] Handle new combo on `HandleReverse` --- .../Edit/CatchSelectionHandler.cs | 33 ++++++++++++++++--- .../Edit/OsuSelectionHandler.cs | 20 +++++++++++ 2 files changed, 48 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Edit/CatchSelectionHandler.cs b/osu.Game.Rulesets.Catch/Edit/CatchSelectionHandler.cs index 418351e2f3..e3d82cc517 100644 --- a/osu.Game.Rulesets.Catch/Edit/CatchSelectionHandler.cs +++ b/osu.Game.Rulesets.Catch/Edit/CatchSelectionHandler.cs @@ -76,21 +76,44 @@ namespace osu.Game.Rulesets.Catch.Edit public override bool HandleReverse() { + var hitObjects = EditorBeatmap.SelectedHitObjects; + double selectionStartTime = SelectedItems.Min(h => h.StartTime); double selectionEndTime = SelectedItems.Max(h => h.GetEndTime()); - EditorBeatmap.PerformOnSelection(hitObject => - { - hitObject.StartTime = selectionEndTime - (hitObject.GetEndTime() - selectionStartTime); + var newComboPlaces = hitObjects + .OfType() + .Where(h => h.NewCombo) + .Select(obj => obj.StartTime) + .ToList(); - if (hitObject is JuiceStream juiceStream) + foreach (var h in hitObjects) + { + h.StartTime = selectionEndTime - (h.GetEndTime() - selectionStartTime); + + if (h is JuiceStream juiceStream) { juiceStream.Path.Reverse(out Vector2 positionalOffset); juiceStream.OriginalX += positionalOffset.X; juiceStream.LegacyConvertedY += positionalOffset.Y; EditorBeatmap.Update(juiceStream); } - }); + } + + foreach (var h in hitObjects) + { + if (h is CatchHitObject obj) obj.NewCombo = false; + } + + foreach (double place in newComboPlaces) + { + hitObjects + .OfType() + .Where(obj => obj.StartTime == place) + .ToList() + .ForEach(obj => obj.NewCombo = true); + } + return true; } diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs index cea2adc6e2..b4980b55d4 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs @@ -85,6 +85,12 @@ namespace osu.Game.Rulesets.Osu.Edit bool moreThanOneObject = hitObjects.Count > 1; + var newComboPlaces = hitObjects + .OfType() + .Where(h => h.NewCombo) + .Select(obj => obj.StartTime) + .ToList(); + foreach (var h in hitObjects) { if (moreThanOneObject) @@ -97,6 +103,20 @@ namespace osu.Game.Rulesets.Osu.Edit } } + foreach (var h in hitObjects) + { + if (h is OsuHitObject obj) obj.NewCombo = false; + } + + foreach (double place in newComboPlaces) + { + hitObjects + .OfType() + .Where(obj => obj.StartTime == place) + .ToList() + .ForEach(obj => obj.NewCombo = true); + } + return true; } From fb08d6816ba21cc6cb3351694ff587a2100fc86a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 25 Mar 2024 11:33:15 +0100 Subject: [PATCH 0422/2493] Only attempt to disable rulesets when decidedly crashing out --- osu.Game/OsuGameBase.cs | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 8bda8fb6c2..fb7a238c46 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -678,18 +678,21 @@ namespace osu.Game /// /// Allows a maximum of one unhandled exception, per second of execution. /// + /// Whether to ignore the exception and continue running. private bool onExceptionThrown(Exception ex) { - bool continueExecution = Interlocked.Decrement(ref allowableExceptions) >= 0; - - Logger.Log($"Unhandled exception has been {(continueExecution ? $"allowed with {allowableExceptions} more allowable exceptions" : "denied")} ."); - - RulesetStore.TryDisableCustomRulesetsCausing(ex); + if (Interlocked.Decrement(ref allowableExceptions) < 0) + { + Logger.Log("Too many unhandled exceptions, crashing out."); + RulesetStore.TryDisableCustomRulesetsCausing(ex); + return false; + } + Logger.Log($"Unhandled exception has been allowed with {allowableExceptions} more allowable exceptions."); // restore the stock of allowable exceptions after a short delay. Task.Delay(1000).ContinueWith(_ => Interlocked.Increment(ref allowableExceptions)); - return continueExecution; + return true; } protected override void Dispose(bool isDisposing) From 4979305b2d59188e97efd03e4e49e90aacb32485 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 25 Mar 2024 11:34:29 +0100 Subject: [PATCH 0423/2493] Ensure `TryDisableCustomRulesetsCausing()` never actually crashes itself --- osu.Game/Rulesets/RealmRulesetStore.cs | 31 ++++++++++++++++---------- 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/osu.Game/Rulesets/RealmRulesetStore.cs b/osu.Game/Rulesets/RealmRulesetStore.cs index 36eae7af2c..2455a9a73f 100644 --- a/osu.Game/Rulesets/RealmRulesetStore.cs +++ b/osu.Game/Rulesets/RealmRulesetStore.cs @@ -164,26 +164,33 @@ namespace osu.Game.Rulesets internal void TryDisableCustomRulesetsCausing(Exception exception) { - var stackTrace = new StackTrace(exception); - - foreach (var frame in stackTrace.GetFrames()) + try { - var declaringAssembly = frame.GetMethod()?.DeclaringType?.Assembly; - if (declaringAssembly == null) - continue; + var stackTrace = new StackTrace(exception); - if (UserRulesetAssemblies.Contains(declaringAssembly)) + foreach (var frame in stackTrace.GetFrames()) { - string sourceLocation = declaringAssembly.Location; - string destinationLocation = Path.ChangeExtension(sourceLocation, @".dll.broken"); + var declaringAssembly = frame.GetMethod()?.DeclaringType?.Assembly; + if (declaringAssembly == null) + continue; - if (File.Exists(sourceLocation)) + if (UserRulesetAssemblies.Contains(declaringAssembly)) { - Logger.Log($"Unhandled exception traced back to custom ruleset {Path.GetFileNameWithoutExtension(sourceLocation)}. Marking as broken."); - File.Move(sourceLocation, destinationLocation); + string sourceLocation = declaringAssembly.Location; + string destinationLocation = Path.ChangeExtension(sourceLocation, @".dll.broken"); + + if (File.Exists(sourceLocation)) + { + Logger.Log($"Unhandled exception traced back to custom ruleset {Path.GetFileNameWithoutExtension(sourceLocation)}. Marking as broken."); + File.Move(sourceLocation, destinationLocation); + } } } } + catch (Exception ex) + { + Logger.Log($"Attempt to trace back crash to custom ruleset failed: {ex}"); + } } } } From 3db88fbcea288f5c4136739bd26e0bdab9e0b05a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 25 Mar 2024 17:54:20 +0100 Subject: [PATCH 0424/2493] Use less confusing message format when logging discord errors The "code" is a number, so it looked weird when put in the middle without any nearby punctuation. Example: An error occurred with Discord RPC Client: 5005 secrets cannot currently be sent with buttons --- osu.Desktop/DiscordRichPresence.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Desktop/DiscordRichPresence.cs b/osu.Desktop/DiscordRichPresence.cs index d78459ff28..eaa9a90f27 100644 --- a/osu.Desktop/DiscordRichPresence.cs +++ b/osu.Desktop/DiscordRichPresence.cs @@ -75,7 +75,7 @@ namespace osu.Desktop }; client.OnReady += onReady; - client.OnError += (_, e) => Logger.Log($"An error occurred with Discord RPC Client: {e.Code} {e.Message}", LoggingTarget.Network, LogLevel.Error); + client.OnError += (_, e) => Logger.Log($"An error occurred with Discord RPC Client: {e.Message} ({e.Code})", LoggingTarget.Network, LogLevel.Error); // A URI scheme is required to support game invitations, as well as informing Discord of the game executable path to support launching the game when a user clicks on join/spectate. client.RegisterUriScheme(); From e95f29cf4ba8840e0305bbae7637d01ddc03f285 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 25 Mar 2024 17:57:13 +0100 Subject: [PATCH 0425/2493] Rename `updatePresence() => schedulePresenceUpdate()` The method doesn't actually update anything by itself, and I want to free up the `updatePresence()` name for the actual update. --- osu.Desktop/DiscordRichPresence.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/osu.Desktop/DiscordRichPresence.cs b/osu.Desktop/DiscordRichPresence.cs index eaa9a90f27..811f2f3548 100644 --- a/osu.Desktop/DiscordRichPresence.cs +++ b/osu.Desktop/DiscordRichPresence.cs @@ -94,10 +94,10 @@ namespace osu.Desktop activity.BindTo(u.NewValue.Activity); }, true); - ruleset.BindValueChanged(_ => updatePresence()); - status.BindValueChanged(_ => updatePresence()); - activity.BindValueChanged(_ => updatePresence()); - privacyMode.BindValueChanged(_ => updatePresence()); + ruleset.BindValueChanged(_ => schedulePresenceUpdate()); + status.BindValueChanged(_ => schedulePresenceUpdate()); + activity.BindValueChanged(_ => schedulePresenceUpdate()); + privacyMode.BindValueChanged(_ => schedulePresenceUpdate()); multiplayerClient.RoomUpdated += onRoomUpdated; client.Initialize(); @@ -111,14 +111,14 @@ namespace osu.Desktop if (client.CurrentPresence != null) client.SetPresence(null); - updatePresence(); + schedulePresenceUpdate(); } - private void onRoomUpdated() => updatePresence(); + private void onRoomUpdated() => schedulePresenceUpdate(); private ScheduledDelegate? presenceUpdateDelegate; - private void updatePresence() + private void schedulePresenceUpdate() { presenceUpdateDelegate?.Cancel(); presenceUpdateDelegate = Scheduler.AddDelayed(() => From a398754a27ac26a1f6e57790b2c609226ad805fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 25 Mar 2024 18:00:42 +0100 Subject: [PATCH 0426/2493] Merge all presence methods into one I'm about to make them interdependent (and it's discord's fault), so it doesn't really make sense to make them separate at this point I don't think. And it felt weird anyway. --- osu.Desktop/DiscordRichPresence.cs | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/osu.Desktop/DiscordRichPresence.cs b/osu.Desktop/DiscordRichPresence.cs index 811f2f3548..d67437ba62 100644 --- a/osu.Desktop/DiscordRichPresence.cs +++ b/osu.Desktop/DiscordRichPresence.cs @@ -134,16 +134,14 @@ namespace osu.Desktop bool hideIdentifiableInformation = privacyMode.Value == DiscordRichPresenceMode.Limited || status.Value == UserStatus.DoNotDisturb; - updatePresenceStatus(hideIdentifiableInformation); - updatePresenceParty(hideIdentifiableInformation); - updatePresenceAssets(); - + updatePresence(hideIdentifiableInformation); client.SetPresence(presence); }, 200); } - private void updatePresenceStatus(bool hideIdentifiableInformation) + private void updatePresence(bool hideIdentifiableInformation) { + // user activity if (activity.Value != null) { presence.State = truncate(activity.Value.GetStatus(hideIdentifiableInformation)); @@ -170,10 +168,8 @@ namespace osu.Desktop presence.State = "Idle"; presence.Details = string.Empty; } - } - private void updatePresenceParty(bool hideIdentifiableInformation) - { + // user party if (!hideIdentifiableInformation && multiplayerClient.Room != null) { MultiplayerRoom room = multiplayerClient.Room; @@ -201,11 +197,9 @@ namespace osu.Desktop presence.Party = null; presence.Secrets.JoinSecret = null; } - } - private void updatePresenceAssets() - { - // update user information + // game images: + // large image tooltip if (privacyMode.Value == DiscordRichPresenceMode.Limited) presence.Assets.LargeImageText = string.Empty; else @@ -216,7 +210,7 @@ namespace osu.Desktop presence.Assets.LargeImageText = $"{user.Value.Username}" + (user.Value.Statistics?.GlobalRank > 0 ? $" (rank #{user.Value.Statistics.GlobalRank:N0})" : string.Empty); } - // update ruleset + // small image presence.Assets.SmallImageKey = ruleset.Value.IsLegacyRuleset() ? $"mode_{ruleset.Value.OnlineID}" : "mode_custom"; presence.Assets.SmallImageText = ruleset.Value.Name; } From 53c3aec3c364a2541180e1bc34b4c40ee688f2b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 25 Mar 2024 18:02:54 +0100 Subject: [PATCH 0427/2493] Fix discord RPC errors in multiplayer Reproduction steps: 1. Go to multiplayer 2. Create a room 3. Play a map to completion 4. Wait for "secrets cannot currently be sent with buttons" error messages The fix is to clear the buttons since they're the less important ones. --- osu.Desktop/DiscordRichPresence.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Desktop/DiscordRichPresence.cs b/osu.Desktop/DiscordRichPresence.cs index d67437ba62..6e8554d617 100644 --- a/osu.Desktop/DiscordRichPresence.cs +++ b/osu.Desktop/DiscordRichPresence.cs @@ -191,6 +191,9 @@ namespace osu.Desktop }; presence.Secrets.JoinSecret = JsonConvert.SerializeObject(roomSecret); + // discord cannot handle both secrets and buttons at the same time, so we need to choose something. + // the multiplayer room seems more important. + presence.Buttons = null; } else { From 6266af8a5696edb32da2f57b877f84dbcab66e3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 25 Mar 2024 18:57:58 +0100 Subject: [PATCH 0428/2493] Fix taiko legacy score simulator not including swell tick score gain into bonus portion Reported in https://discord.com/channels/188630481301012481/1097318920991559880/1221836384038551613. Example score: https://osu.ppy.sh/scores/1855965185 The cause of the overestimation was an error in taiko's score simulator. In lazer taiko, swell ticks don't give any score anymore, while they did in stable. For all intents and purposes, swell ticks can be considered "bonus" objects that "don't give any actual bonus score". Which is to say, during simulation of a legacy score swell ticks hit should be treated as bonus, because if they aren't, then otherwise they will be treated essentially as *normal hits*, meaning that they will be included in the *accuracy* portion of score, which breaks all sorts of follow-up assumptions: - The accuracy portion of the best possible total score becomes overinflated in comparison to reality, while the combo portion of that maximum score becomes underestimated. - Because the affected score has low accuracy, the estimated accuracy portion of the score (as given by maximmum accuracy portion of score times the actual numerical accuracy of the score) is also low. - However, the next step is estimating the combo portion, which is done by taking legacy total score, subtracting the aforementioned estimation for accuracy total score from that, and then dividing the result by the maximum achievable combo score on the map. Because most of actual "combo" score from swell ticks was "moved" into the accuracy portion due to the aforementioned error, the maximum achievable combo score becomes so small that the estimated combo portion exceeds 1. Instead, this change makes it so that gains from swell ticks are treated as "bonus", which means that they are excluded from the accuracy portion of score and instead count into the bonus portion of score, bringing the scores concerned more in line with expectations - although due to pessimistic assumptions in the simulation of the swell itself, the conversion will still overestimate total score for affected scores, just not by *that* much. --- osu.Game.Rulesets.Taiko/Difficulty/TaikoLegacyScoreSimulator.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoLegacyScoreSimulator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoLegacyScoreSimulator.cs index 66ff0fc3d9..9839d94277 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoLegacyScoreSimulator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoLegacyScoreSimulator.cs @@ -95,6 +95,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty case SwellTick: scoreIncrease = 300; increaseCombo = false; + isBonus = true; + bonusResult = HitResult.IgnoreHit; break; case DrumRollTick: From 73926592b91e1cf6348b3e7ae3c68142dbbd8d06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 25 Mar 2024 19:27:38 +0100 Subject: [PATCH 0429/2493] Bump legacy score version to recalculate all scores --- osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs index 93f51ee74d..0f00cce080 100644 --- a/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs +++ b/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs @@ -46,9 +46,10 @@ namespace osu.Game.Scoring.Legacy /// 30000013: All local scores will use lazer definitions of ranks for consistency. Recalculates the rank of all scores. /// 30000014: Fix edge cases in conversion for osu! scores on selected beatmaps. Reconvert all scores. /// 30000015: Fix osu! standardised score estimation algorithm violating basic invariants. Reconvert all scores. + /// 30000016: Fix taiko standardised score estimation algorithm not including swell tick score gain into bonus portion. Reconvert all scores. /// /// - public const int LATEST_VERSION = 30000015; + public const int LATEST_VERSION = 30000016; /// /// The first stable-compatible YYYYMMDD format version given to lazer usage of replays. From 10683de578590d9a17c690df4ef9fc89515599a5 Mon Sep 17 00:00:00 2001 From: Arthur Araujo Date: Tue, 26 Mar 2024 04:59:47 -0300 Subject: [PATCH 0430/2493] Use order of new combo flags instead of `StartTime` --- .../Edit/CatchSelectionHandler.cs | 22 +++++++------------ .../Edit/OsuSelectionHandler.cs | 22 +++++++------------ 2 files changed, 16 insertions(+), 28 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Edit/CatchSelectionHandler.cs b/osu.Game.Rulesets.Catch/Edit/CatchSelectionHandler.cs index e3d82cc517..e3d2347c4d 100644 --- a/osu.Game.Rulesets.Catch/Edit/CatchSelectionHandler.cs +++ b/osu.Game.Rulesets.Catch/Edit/CatchSelectionHandler.cs @@ -81,12 +81,13 @@ namespace osu.Game.Rulesets.Catch.Edit double selectionStartTime = SelectedItems.Min(h => h.StartTime); double selectionEndTime = SelectedItems.Max(h => h.GetEndTime()); - var newComboPlaces = hitObjects + var newComboOrder = hitObjects .OfType() - .Where(h => h.NewCombo) - .Select(obj => obj.StartTime) + .Select(obj => obj.NewCombo) .ToList(); + newComboOrder.Reverse(); + foreach (var h in hitObjects) { h.StartTime = selectionEndTime - (h.GetEndTime() - selectionStartTime); @@ -100,18 +101,11 @@ namespace osu.Game.Rulesets.Catch.Edit } } - foreach (var h in hitObjects) + int i = 0; + foreach (bool newCombo in newComboOrder) { - if (h is CatchHitObject obj) obj.NewCombo = false; - } - - foreach (double place in newComboPlaces) - { - hitObjects - .OfType() - .Where(obj => obj.StartTime == place) - .ToList() - .ForEach(obj => obj.NewCombo = true); + hitObjects.OfType().ToList()[i].NewCombo = newCombo; + i++; } return true; diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs index b4980b55d4..df39ab8fce 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs @@ -85,12 +85,13 @@ namespace osu.Game.Rulesets.Osu.Edit bool moreThanOneObject = hitObjects.Count > 1; - var newComboPlaces = hitObjects + var newComboOrder = hitObjects .OfType() - .Where(h => h.NewCombo) - .Select(obj => obj.StartTime) + .Select(obj => obj.NewCombo) .ToList(); + newComboOrder.Reverse(); + foreach (var h in hitObjects) { if (moreThanOneObject) @@ -103,18 +104,11 @@ namespace osu.Game.Rulesets.Osu.Edit } } - foreach (var h in hitObjects) + int i = 0; + foreach (bool newCombo in newComboOrder) { - if (h is OsuHitObject obj) obj.NewCombo = false; - } - - foreach (double place in newComboPlaces) - { - hitObjects - .OfType() - .Where(obj => obj.StartTime == place) - .ToList() - .ForEach(obj => obj.NewCombo = true); + hitObjects.OfType().ToList()[i].NewCombo = newCombo; + i++; } return true; From 9b923b19094bbcdf3f130d005085de70368cb8c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 26 Mar 2024 10:55:49 +0100 Subject: [PATCH 0431/2493] Fix code quality issues --- .../Editor/Checks/CheckCatchAbnormalDifficultySettingsTest.cs | 4 ++-- .../Editor/Checks/CheckKeyCountTest.cs | 2 +- .../Editor/Checks/CheckManiaAbnormalDifficultySettingsTest.cs | 4 ++-- .../Editor/Checks/CheckOsuAbnormalDifficultySettingsTest.cs | 4 ++-- .../Edit/Checks/CheckOsuAbnormalDifficultySettings.cs | 1 + .../Editor/Checks/CheckTaikoAbnormalDifficultySettingsTest.cs | 4 ++-- 6 files changed, 10 insertions(+), 9 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/Editor/Checks/CheckCatchAbnormalDifficultySettingsTest.cs b/osu.Game.Rulesets.Catch.Tests/Editor/Checks/CheckCatchAbnormalDifficultySettingsTest.cs index 2ae2e20215..33aa4cba5d 100644 --- a/osu.Game.Rulesets.Catch.Tests/Editor/Checks/CheckCatchAbnormalDifficultySettingsTest.cs +++ b/osu.Game.Rulesets.Catch.Tests/Editor/Checks/CheckCatchAbnormalDifficultySettingsTest.cs @@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor.Checks { private CheckCatchAbnormalDifficultySettings check = null!; - private IBeatmap beatmap = new Beatmap(); + private readonly IBeatmap beatmap = new Beatmap(); [SetUp] public void Setup() @@ -25,7 +25,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor.Checks check = new CheckCatchAbnormalDifficultySettings(); beatmap.BeatmapInfo.Ruleset = new CatchRuleset().RulesetInfo; - beatmap.Difficulty = new BeatmapDifficulty() + beatmap.Difficulty = new BeatmapDifficulty { ApproachRate = 5, CircleSize = 5, diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/Checks/CheckKeyCountTest.cs b/osu.Game.Rulesets.Mania.Tests/Editor/Checks/CheckKeyCountTest.cs index 564c611548..b40a62176c 100644 --- a/osu.Game.Rulesets.Mania.Tests/Editor/Checks/CheckKeyCountTest.cs +++ b/osu.Game.Rulesets.Mania.Tests/Editor/Checks/CheckKeyCountTest.cs @@ -23,7 +23,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor.Checks { check = new CheckKeyCount(); - beatmap = new Beatmap() + beatmap = new Beatmap { BeatmapInfo = new BeatmapInfo { diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/Checks/CheckManiaAbnormalDifficultySettingsTest.cs b/osu.Game.Rulesets.Mania.Tests/Editor/Checks/CheckManiaAbnormalDifficultySettingsTest.cs index 6c585aace3..da5ab037e5 100644 --- a/osu.Game.Rulesets.Mania.Tests/Editor/Checks/CheckManiaAbnormalDifficultySettingsTest.cs +++ b/osu.Game.Rulesets.Mania.Tests/Editor/Checks/CheckManiaAbnormalDifficultySettingsTest.cs @@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor.Checks { private CheckManiaAbnormalDifficultySettings check = null!; - private IBeatmap beatmap = new Beatmap(); + private readonly IBeatmap beatmap = new Beatmap(); [SetUp] public void Setup() @@ -25,7 +25,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor.Checks check = new CheckManiaAbnormalDifficultySettings(); beatmap.BeatmapInfo.Ruleset = new ManiaRuleset().RulesetInfo; - beatmap.Difficulty = new BeatmapDifficulty() + beatmap.Difficulty = new BeatmapDifficulty { OverallDifficulty = 5, DrainRate = 5, diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/Checks/CheckOsuAbnormalDifficultySettingsTest.cs b/osu.Game.Rulesets.Osu.Tests/Editor/Checks/CheckOsuAbnormalDifficultySettingsTest.cs index 53ccd3c7a7..5f49714d93 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/Checks/CheckOsuAbnormalDifficultySettingsTest.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/Checks/CheckOsuAbnormalDifficultySettingsTest.cs @@ -17,14 +17,14 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor.Checks { private CheckOsuAbnormalDifficultySettings check = null!; - private IBeatmap beatmap = new Beatmap(); + private readonly IBeatmap beatmap = new Beatmap(); [SetUp] public void Setup() { check = new CheckOsuAbnormalDifficultySettings(); - beatmap.Difficulty = new BeatmapDifficulty() + beatmap.Difficulty = new BeatmapDifficulty { ApproachRate = 5, CircleSize = 5, diff --git a/osu.Game.Rulesets.Osu/Edit/Checks/CheckOsuAbnormalDifficultySettings.cs b/osu.Game.Rulesets.Osu/Edit/Checks/CheckOsuAbnormalDifficultySettings.cs index c1eca7fff7..7ad861f317 100644 --- a/osu.Game.Rulesets.Osu/Edit/Checks/CheckOsuAbnormalDifficultySettings.cs +++ b/osu.Game.Rulesets.Osu/Edit/Checks/CheckOsuAbnormalDifficultySettings.cs @@ -11,6 +11,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Checks public class CheckOsuAbnormalDifficultySettings : CheckAbnormalDifficultySettings { public override CheckMetadata Metadata => new CheckMetadata(CheckCategory.Settings, "Checks osu relevant settings"); + public override IEnumerable Run(BeatmapVerifierContext context) { var diff = context.Beatmap.Difficulty; diff --git a/osu.Game.Rulesets.Taiko.Tests/Editor/Checks/CheckTaikoAbnormalDifficultySettingsTest.cs b/osu.Game.Rulesets.Taiko.Tests/Editor/Checks/CheckTaikoAbnormalDifficultySettingsTest.cs index f10e62f3bf..4a6cf0313a 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Editor/Checks/CheckTaikoAbnormalDifficultySettingsTest.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Editor/Checks/CheckTaikoAbnormalDifficultySettingsTest.cs @@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Editor.Checks { private CheckTaikoAbnormalDifficultySettings check = null!; - private IBeatmap beatmap = new Beatmap(); + private readonly IBeatmap beatmap = new Beatmap(); [SetUp] public void Setup() @@ -25,7 +25,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Editor.Checks check = new CheckTaikoAbnormalDifficultySettings(); beatmap.BeatmapInfo.Ruleset = new TaikoRuleset().RulesetInfo; - beatmap.Difficulty = new BeatmapDifficulty() + beatmap.Difficulty = new BeatmapDifficulty { OverallDifficulty = 5, }; From 8fb308c1925e1cdbda3eba6e58b7ef5c8fb1fe89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 26 Mar 2024 10:57:20 +0100 Subject: [PATCH 0432/2493] Add failing test coverage for checking taiko HP too I was wrong, taiko uses HP (to calculate miss penalty). --- ...heckTaikoAbnormalDifficultySettingsTest.cs | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/osu.Game.Rulesets.Taiko.Tests/Editor/Checks/CheckTaikoAbnormalDifficultySettingsTest.cs b/osu.Game.Rulesets.Taiko.Tests/Editor/Checks/CheckTaikoAbnormalDifficultySettingsTest.cs index 4a6cf0313a..6a50fd0956 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Editor/Checks/CheckTaikoAbnormalDifficultySettingsTest.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Editor/Checks/CheckTaikoAbnormalDifficultySettingsTest.cs @@ -52,6 +52,18 @@ namespace osu.Game.Rulesets.Taiko.Tests.Editor.Checks Assert.That(issues.Single().Template is CheckAbnormalDifficultySettings.IssueTemplateMoreThanOneDecimal); } + [Test] + public void TestDrainRateTwoDecimals() + { + beatmap.Difficulty.DrainRate = 5.55f; + + var context = getContext(); + var issues = check.Run(context).ToList(); + + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues.Single().Template is CheckAbnormalDifficultySettings.IssueTemplateMoreThanOneDecimal); + } + [Test] public void TestOverallDifficultyUnder() { @@ -76,6 +88,30 @@ namespace osu.Game.Rulesets.Taiko.Tests.Editor.Checks Assert.That(issues.Single().Template is CheckAbnormalDifficultySettings.IssueTemplateOutOfRange); } + [Test] + public void TestDrainRateUnder() + { + beatmap.Difficulty.DrainRate = -10; + + var context = getContext(); + var issues = check.Run(context).ToList(); + + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues.Single().Template is CheckAbnormalDifficultySettings.IssueTemplateOutOfRange); + } + + [Test] + public void TestDrainRateOver() + { + beatmap.Difficulty.DrainRate = 20; + + var context = getContext(); + var issues = check.Run(context).ToList(); + + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues.Single().Template is CheckAbnormalDifficultySettings.IssueTemplateOutOfRange); + } + private BeatmapVerifierContext getContext() { return new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap)); From e7cf1ab4df36879306ef6e4c38b535ba370a0e90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 26 Mar 2024 10:58:39 +0100 Subject: [PATCH 0433/2493] Add checks for taiko drain rate --- .../Edit/Checks/CheckTaikoAbnormalDifficultySettings.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/osu.Game.Rulesets.Taiko/Edit/Checks/CheckTaikoAbnormalDifficultySettings.cs b/osu.Game.Rulesets.Taiko/Edit/Checks/CheckTaikoAbnormalDifficultySettings.cs index ce35f21853..10e2867ca0 100644 --- a/osu.Game.Rulesets.Taiko/Edit/Checks/CheckTaikoAbnormalDifficultySettings.cs +++ b/osu.Game.Rulesets.Taiko/Edit/Checks/CheckTaikoAbnormalDifficultySettings.cs @@ -21,6 +21,12 @@ namespace osu.Game.Rulesets.Taiko.Edit.Checks if (OutOfRange(diff.OverallDifficulty)) yield return new IssueTemplateOutOfRange(this).Create("Overall difficulty", diff.OverallDifficulty); + + if (HasMoreThanOneDecimalPlace(diff.DrainRate)) + yield return new IssueTemplateMoreThanOneDecimal(this).Create("Drain rate", diff.DrainRate); + + if (OutOfRange(diff.DrainRate)) + yield return new IssueTemplateOutOfRange(this).Create("Drain rate", diff.DrainRate); } } } From 1866b4b6b10d1990db311eb96ae02c5c5609e918 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 26 Mar 2024 11:13:03 +0100 Subject: [PATCH 0434/2493] Refactor abstract check to reduce duplication --- .../CheckCatchAbnormalDifficultySettings.cs | 25 +++++++------- .../CheckManiaAbnormalDifficultySettings.cs | 17 +++++----- .../CheckOsuAbnormalDifficultySettings.cs | 33 ++++++++++--------- .../CheckTaikoAbnormalDifficultySettings.cs | 17 +++++----- .../Checks/CheckAbnormalDifficultySettings.cs | 13 +++++--- 5 files changed, 57 insertions(+), 48 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Edit/Checks/CheckCatchAbnormalDifficultySettings.cs b/osu.Game.Rulesets.Catch/Edit/Checks/CheckCatchAbnormalDifficultySettings.cs index 8295795f00..d2c3df0872 100644 --- a/osu.Game.Rulesets.Catch/Edit/Checks/CheckCatchAbnormalDifficultySettings.cs +++ b/osu.Game.Rulesets.Catch/Edit/Checks/CheckCatchAbnormalDifficultySettings.cs @@ -15,24 +15,25 @@ namespace osu.Game.Rulesets.Catch.Edit.Checks public override IEnumerable Run(BeatmapVerifierContext context) { var diff = context.Beatmap.Difficulty; + Issue? issue; - if (HasMoreThanOneDecimalPlace(diff.ApproachRate)) - yield return new IssueTemplateMoreThanOneDecimal(this).Create("Approach rate", diff.ApproachRate); + if (HasMoreThanOneDecimalPlace("Approach rate", diff.ApproachRate, out issue)) + yield return issue; - if (OutOfRange(diff.ApproachRate)) - yield return new IssueTemplateOutOfRange(this).Create("Approach rate", diff.ApproachRate); + if (OutOfRange("Approach rate", diff.ApproachRate, out issue)) + yield return issue; - if (HasMoreThanOneDecimalPlace(diff.CircleSize)) - yield return new IssueTemplateMoreThanOneDecimal(this).Create("Circle size", diff.CircleSize); + if (HasMoreThanOneDecimalPlace("Circle size", diff.CircleSize, out issue)) + yield return issue; - if (OutOfRange(diff.CircleSize)) - yield return new IssueTemplateOutOfRange(this).Create("Circle size", diff.CircleSize); + if (OutOfRange("Circle size", diff.CircleSize, out issue)) + yield return issue; - if (HasMoreThanOneDecimalPlace(diff.DrainRate)) - yield return new IssueTemplateMoreThanOneDecimal(this).Create("Drain rate", diff.DrainRate); + if (HasMoreThanOneDecimalPlace("Drain rate", diff.DrainRate, out issue)) + yield return issue; - if (OutOfRange(diff.DrainRate)) - yield return new IssueTemplateOutOfRange(this).Create("Drain rate", diff.DrainRate); + if (OutOfRange("Drain rate", diff.DrainRate, out issue)) + yield return issue; } } } diff --git a/osu.Game.Rulesets.Mania/Edit/Checks/CheckManiaAbnormalDifficultySettings.cs b/osu.Game.Rulesets.Mania/Edit/Checks/CheckManiaAbnormalDifficultySettings.cs index ae0cc3aa4c..233c602c21 100644 --- a/osu.Game.Rulesets.Mania/Edit/Checks/CheckManiaAbnormalDifficultySettings.cs +++ b/osu.Game.Rulesets.Mania/Edit/Checks/CheckManiaAbnormalDifficultySettings.cs @@ -15,18 +15,19 @@ namespace osu.Game.Rulesets.Mania.Edit.Checks public override IEnumerable Run(BeatmapVerifierContext context) { var diff = context.Beatmap.Difficulty; + Issue? issue; - if (HasMoreThanOneDecimalPlace(diff.OverallDifficulty)) - yield return new IssueTemplateMoreThanOneDecimal(this).Create("Overall difficulty", diff.OverallDifficulty); + if (HasMoreThanOneDecimalPlace("Overall difficulty", diff.OverallDifficulty, out issue)) + yield return issue; - if (OutOfRange(diff.OverallDifficulty)) - yield return new IssueTemplateOutOfRange(this).Create("Overall difficulty", diff.OverallDifficulty); + if (OutOfRange("Overall difficulty", diff.OverallDifficulty, out issue)) + yield return issue; - if (HasMoreThanOneDecimalPlace(diff.DrainRate)) - yield return new IssueTemplateMoreThanOneDecimal(this).Create("Drain rate", diff.DrainRate); + if (HasMoreThanOneDecimalPlace("Drain rate", diff.DrainRate, out issue)) + yield return issue; - if (OutOfRange(diff.DrainRate)) - yield return new IssueTemplateOutOfRange(this).Create("Drain rate", diff.DrainRate); + if (OutOfRange("Drain rate", diff.DrainRate, out issue)) + yield return issue; } } } diff --git a/osu.Game.Rulesets.Osu/Edit/Checks/CheckOsuAbnormalDifficultySettings.cs b/osu.Game.Rulesets.Osu/Edit/Checks/CheckOsuAbnormalDifficultySettings.cs index 7ad861f317..1c44d54633 100644 --- a/osu.Game.Rulesets.Osu/Edit/Checks/CheckOsuAbnormalDifficultySettings.cs +++ b/osu.Game.Rulesets.Osu/Edit/Checks/CheckOsuAbnormalDifficultySettings.cs @@ -15,30 +15,31 @@ namespace osu.Game.Rulesets.Osu.Edit.Checks public override IEnumerable Run(BeatmapVerifierContext context) { var diff = context.Beatmap.Difficulty; + Issue? issue; - if (HasMoreThanOneDecimalPlace(diff.ApproachRate)) - yield return new IssueTemplateMoreThanOneDecimal(this).Create("Approach rate", diff.ApproachRate); + if (HasMoreThanOneDecimalPlace("Approach rate", diff.ApproachRate, out issue)) + yield return issue; - if (OutOfRange(diff.ApproachRate)) - yield return new IssueTemplateOutOfRange(this).Create("Approach rate", diff.ApproachRate); + if (OutOfRange("Approach rate", diff.ApproachRate, out issue)) + yield return issue; - if (HasMoreThanOneDecimalPlace(diff.OverallDifficulty)) - yield return new IssueTemplateMoreThanOneDecimal(this).Create("Overall difficulty", diff.OverallDifficulty); + if (HasMoreThanOneDecimalPlace("Overall difficulty", diff.OverallDifficulty, out issue)) + yield return issue; - if (OutOfRange(diff.OverallDifficulty)) - yield return new IssueTemplateOutOfRange(this).Create("Overall difficulty", diff.OverallDifficulty); + if (OutOfRange("Overall difficulty", diff.OverallDifficulty, out issue)) + yield return issue; - if (HasMoreThanOneDecimalPlace(diff.CircleSize)) - yield return new IssueTemplateMoreThanOneDecimal(this).Create("Circle size", diff.CircleSize); + if (HasMoreThanOneDecimalPlace("Circle size", diff.CircleSize, out issue)) + yield return issue; - if (OutOfRange(diff.CircleSize)) - yield return new IssueTemplateOutOfRange(this).Create("Circle size", diff.CircleSize); + if (OutOfRange("Circle size", diff.CircleSize, out issue)) + yield return issue; - if (HasMoreThanOneDecimalPlace(diff.DrainRate)) - yield return new IssueTemplateMoreThanOneDecimal(this).Create("Drain rate", diff.DrainRate); + if (HasMoreThanOneDecimalPlace("Drain rate", diff.DrainRate, out issue)) + yield return issue; - if (OutOfRange(diff.DrainRate)) - yield return new IssueTemplateOutOfRange(this).Create("Drain rate", diff.DrainRate); + if (OutOfRange("Drain rate", diff.DrainRate, out issue)) + yield return issue; } } } diff --git a/osu.Game.Rulesets.Taiko/Edit/Checks/CheckTaikoAbnormalDifficultySettings.cs b/osu.Game.Rulesets.Taiko/Edit/Checks/CheckTaikoAbnormalDifficultySettings.cs index 10e2867ca0..38ba7b1b01 100644 --- a/osu.Game.Rulesets.Taiko/Edit/Checks/CheckTaikoAbnormalDifficultySettings.cs +++ b/osu.Game.Rulesets.Taiko/Edit/Checks/CheckTaikoAbnormalDifficultySettings.cs @@ -15,18 +15,19 @@ namespace osu.Game.Rulesets.Taiko.Edit.Checks public override IEnumerable Run(BeatmapVerifierContext context) { var diff = context.Beatmap.Difficulty; + Issue? issue; - if (HasMoreThanOneDecimalPlace(diff.OverallDifficulty)) - yield return new IssueTemplateMoreThanOneDecimal(this).Create("Overall difficulty", diff.OverallDifficulty); + if (HasMoreThanOneDecimalPlace("Overall difficulty", diff.OverallDifficulty, out issue)) + yield return issue; - if (OutOfRange(diff.OverallDifficulty)) - yield return new IssueTemplateOutOfRange(this).Create("Overall difficulty", diff.OverallDifficulty); + if (OutOfRange("Overall difficulty", diff.OverallDifficulty, out issue)) + yield return issue; - if (HasMoreThanOneDecimalPlace(diff.DrainRate)) - yield return new IssueTemplateMoreThanOneDecimal(this).Create("Drain rate", diff.DrainRate); + if (HasMoreThanOneDecimalPlace("Drain rate", diff.DrainRate, out issue)) + yield return issue; - if (OutOfRange(diff.DrainRate)) - yield return new IssueTemplateOutOfRange(this).Create("Drain rate", diff.DrainRate); + if (OutOfRange("Drain rate", diff.DrainRate, out issue)) + yield return issue; } } } diff --git a/osu.Game/Rulesets/Edit/Checks/CheckAbnormalDifficultySettings.cs b/osu.Game/Rulesets/Edit/Checks/CheckAbnormalDifficultySettings.cs index 93592a866b..638f0cfd53 100644 --- a/osu.Game/Rulesets/Edit/Checks/CheckAbnormalDifficultySettings.cs +++ b/osu.Game/Rulesets/Edit/Checks/CheckAbnormalDifficultySettings.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using osu.Game.Rulesets.Edit.Checks.Components; namespace osu.Game.Rulesets.Edit.Checks @@ -21,14 +22,18 @@ namespace osu.Game.Rulesets.Edit.Checks /// /// If the setting is out of the boundaries set by the editor (0 - 10) /// - protected bool OutOfRange(float setting) + protected bool OutOfRange(string setting, float value, [NotNullWhen(true)] out Issue? issue) { - return setting < 0f || setting > 10f; + bool hasIssue = value < 0f || value > 10f; + issue = hasIssue ? new IssueTemplateOutOfRange(this).Create(setting, value) : null; + return hasIssue; } - protected bool HasMoreThanOneDecimalPlace(float setting) + protected bool HasMoreThanOneDecimalPlace(string setting, float value, [NotNullWhen(true)] out Issue? issue) { - return float.Round(setting, 1) != setting; + bool hasIssue = float.Round(value, 1) != value; + issue = hasIssue ? new IssueTemplateMoreThanOneDecimal(this).Create(setting, value) : null; + return hasIssue; } public class IssueTemplateMoreThanOneDecimal : IssueTemplate From c24eb066dc4927580ffecfe79460819bc5486f84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 26 Mar 2024 12:03:24 +0100 Subject: [PATCH 0435/2493] Update tests to match new expected behaviour Co-authored-by: Vlad Frangu --- .../Filtering/FilterQueryParserTest.cs | 45 +++++++++++++++++-- 1 file changed, 42 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs b/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs index bd706b5b4c..ea14412f55 100644 --- a/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs +++ b/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs @@ -273,12 +273,21 @@ namespace osu.Game.Tests.NonVisual.Filtering } [Test] - public void TestApplyStatusMatches() + public void TestApplyMultipleEqualityStatusQueries() { const string query = "status=ranked status=loved"; var filterCriteria = new FilterCriteria(); FilterQueryParser.ApplyQueries(filterCriteria, query); - Assert.IsNotEmpty(filterCriteria.OnlineStatus.Values); + Assert.That(filterCriteria.OnlineStatus.Values, Is.Empty); + } + + [Test] + public void TestApplyEqualStatusQueryWithMultipleValues() + { + const string query = "status=ranked,loved"; + var filterCriteria = new FilterCriteria(); + FilterQueryParser.ApplyQueries(filterCriteria, query); + Assert.That(filterCriteria.OnlineStatus.Values, Is.Not.Empty); Assert.That(filterCriteria.OnlineStatus.Values, Contains.Item(BeatmapOnlineStatus.Ranked)); Assert.That(filterCriteria.OnlineStatus.Values, Contains.Item(BeatmapOnlineStatus.Loved)); } @@ -289,13 +298,43 @@ namespace osu.Game.Tests.NonVisual.Filtering const string query = "status>=r"; var filterCriteria = new FilterCriteria(); FilterQueryParser.ApplyQueries(filterCriteria, query); - Assert.IsNotEmpty(filterCriteria.OnlineStatus.Values); + Assert.That(filterCriteria.OnlineStatus.Values, Has.Count.EqualTo(4)); Assert.That(filterCriteria.OnlineStatus.Values, Contains.Item(BeatmapOnlineStatus.Ranked)); Assert.That(filterCriteria.OnlineStatus.Values, Contains.Item(BeatmapOnlineStatus.Approved)); Assert.That(filterCriteria.OnlineStatus.Values, Contains.Item(BeatmapOnlineStatus.Qualified)); Assert.That(filterCriteria.OnlineStatus.Values, Contains.Item(BeatmapOnlineStatus.Loved)); } + [Test] + public void TestApplyRangeStatusWithMultipleMatchesQuery() + { + const string query = "status>=r,l"; + var filterCriteria = new FilterCriteria(); + FilterQueryParser.ApplyQueries(filterCriteria, query); + Assert.That(filterCriteria.OnlineStatus.Values, Is.EquivalentTo(Enum.GetValues())); + } + + [Test] + public void TestApplyTwoRangeStatusQuery() + { + const string query = "status>r status Date: Tue, 26 Mar 2024 12:20:38 +0100 Subject: [PATCH 0436/2493] Update `OptionalSet` implementation to intersect across multiple filters rather than union --- osu.Game/Screens/Select/FilterCriteria.cs | 9 +- osu.Game/Screens/Select/FilterQueryParser.cs | 86 ++++++++++++-------- 2 files changed, 56 insertions(+), 39 deletions(-) diff --git a/osu.Game/Screens/Select/FilterCriteria.cs b/osu.Game/Screens/Select/FilterCriteria.cs index 6750b07aba..01b0e9b7d9 100644 --- a/osu.Game/Screens/Select/FilterCriteria.cs +++ b/osu.Game/Screens/Select/FilterCriteria.cs @@ -114,17 +114,18 @@ namespace osu.Game.Screens.Select public IRulesetFilterCriteria? RulesetCriteria { get; set; } - public struct OptionalSet : IEquatable> - where T : struct + public readonly struct OptionalSet : IEquatable> + where T : struct, Enum { - public bool HasFilter => Values.Count > 0; + public bool HasFilter => true; public bool IsInRange(T value) => Values.Contains(value); - public HashSet Values = new HashSet(); + public HashSet Values { get; } public OptionalSet() { + Values = Enum.GetValues().ToHashSet(); } public bool Equals(OptionalSet other) => Values.SetEquals(other.Values); diff --git a/osu.Game/Screens/Select/FilterQueryParser.cs b/osu.Game/Screens/Select/FilterQueryParser.cs index 32b533e18d..4e49495f47 100644 --- a/osu.Game/Screens/Select/FilterQueryParser.cs +++ b/osu.Game/Screens/Select/FilterQueryParser.cs @@ -69,7 +69,7 @@ namespace osu.Game.Screens.Select return TryUpdateCriteriaRange(ref criteria.BeatDivisor, op, value, tryParseInt); case "status": - return TryUpdateCriteriaSet(ref criteria.OnlineStatus, op, value, tryParseEnum); + return TryUpdateCriteriaSet(ref criteria.OnlineStatus, op, value); case "creator": case "author": @@ -302,54 +302,70 @@ namespace osu.Game.Screens.Select /// /// Attempts to parse a keyword filter of type , - /// from the specified and . - /// If can be parsed into using , the function returns true + /// from the specified and . + /// If can be parsed successfully, the function returns true /// and the resulting range constraint is stored into the 's expected values. /// /// The to store the parsed data into, if successful. /// The operator for the keyword filter. - /// The value of the keyword filter. - /// Function used to determine if can be converted to type . - public static bool TryUpdateCriteriaSet(ref FilterCriteria.OptionalSet range, Operator op, string val, TryParseFunction parseFunction) - where T : struct, Enum - => parseFunction.Invoke(val, out var converted) && tryUpdateCriteriaSet(ref range, op, converted); - - private static bool tryUpdateCriteriaSet(ref FilterCriteria.OptionalSet range, Operator op, T pivotValue) + /// The value of the keyword filter. + public static bool TryUpdateCriteriaSet(ref FilterCriteria.OptionalSet range, Operator op, string filterValue) where T : struct, Enum { - var allDefinedValues = Enum.GetValues(); + var matchingValues = new HashSet(); - foreach (var val in allDefinedValues) + if (op == Operator.Equal && filterValue.Contains(',')) { - int compareResult = Comparer.Default.Compare(val, pivotValue); + string[] splitValues = filterValue.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); - switch (op) + foreach (string splitValue in splitValues) { - case Operator.Less: - if (compareResult < 0) range.Values.Add(val); - break; - - case Operator.LessOrEqual: - if (compareResult <= 0) range.Values.Add(val); - break; - - case Operator.Equal: - if (compareResult == 0) range.Values.Add(val); - break; - - case Operator.GreaterOrEqual: - if (compareResult >= 0) range.Values.Add(val); - break; - - case Operator.Greater: - if (compareResult > 0) range.Values.Add(val); - break; - - default: + if (!tryParseEnum(splitValue, out var parsedValue)) return false; + + matchingValues.Add(parsedValue); + } + } + else + { + if (!tryParseEnum(filterValue, out var pivotValue)) + return false; + + var allDefinedValues = Enum.GetValues(); + + foreach (var val in allDefinedValues) + { + int compareResult = Comparer.Default.Compare(val, pivotValue); + + switch (op) + { + case Operator.Less: + if (compareResult < 0) matchingValues.Add(val); + break; + + case Operator.LessOrEqual: + if (compareResult <= 0) matchingValues.Add(val); + break; + + case Operator.Equal: + if (compareResult == 0) matchingValues.Add(val); + break; + + case Operator.GreaterOrEqual: + if (compareResult >= 0) matchingValues.Add(val); + break; + + case Operator.Greater: + if (compareResult > 0) matchingValues.Add(val); + break; + + default: + return false; + } } } + range.Values.IntersectWith(matchingValues); return true; } From 9474156df4bb33bda65c0bd2d68d565825f1b3d1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 26 Mar 2024 20:21:12 +0800 Subject: [PATCH 0437/2493] Improve equality implementations --- .../API/Requests/Responses/APIMenuContent.cs | 10 +--------- .../API/Requests/Responses/APIMenuImage.cs | 17 +++++------------ 2 files changed, 6 insertions(+), 21 deletions(-) diff --git a/osu.Game/Online/API/Requests/Responses/APIMenuContent.cs b/osu.Game/Online/API/Requests/Responses/APIMenuContent.cs index 7b53488030..6aad0f6c87 100644 --- a/osu.Game/Online/API/Requests/Responses/APIMenuContent.cs +++ b/osu.Game/Online/API/Requests/Responses/APIMenuContent.cs @@ -23,15 +23,7 @@ namespace osu.Game.Online.API.Requests.Responses return Images.SequenceEqual(other.Images); } - public override bool Equals(object? obj) - { - if (ReferenceEquals(null, obj)) return false; - if (ReferenceEquals(this, obj)) return true; - - if (obj.GetType() != GetType()) return false; - - return Equals((APIMenuContent)obj); - } + public override bool Equals(object? other) => other is APIMenuContent content && Equals(content); public override int GetHashCode() { diff --git a/osu.Game/Online/API/Requests/Responses/APIMenuImage.cs b/osu.Game/Online/API/Requests/Responses/APIMenuImage.cs index 42129ca96e..8aff08099a 100644 --- a/osu.Game/Online/API/Requests/Responses/APIMenuImage.cs +++ b/osu.Game/Online/API/Requests/Responses/APIMenuImage.cs @@ -28,34 +28,27 @@ namespace osu.Game.Online.API.Requests.Responses /// The time at which this item should begin displaying. If null, will display immediately. /// [JsonProperty(@"begins")] - public DateTimeOffset? Begins { get; set; } + public DateTimeOffset? Begins { get; init; } /// /// The time at which this item should stop displaying. If null, will display indefinitely. /// [JsonProperty(@"expires")] - public DateTimeOffset? Expires { get; set; } + public DateTimeOffset? Expires { get; init; } public bool Equals(APIMenuImage? other) { if (ReferenceEquals(null, other)) return false; if (ReferenceEquals(this, other)) return true; - return Image == other.Image && Url == other.Url; + return Image == other.Image && Url == other.Url && Begins == other.Begins && Expires == other.Expires; } - public override bool Equals(object? obj) - { - if (ReferenceEquals(null, obj)) return false; - if (ReferenceEquals(this, obj)) return true; - if (obj.GetType() != GetType()) return false; - - return Equals((APIMenuImage)obj); - } + public override bool Equals(object? other) => other is APIMenuImage content && Equals(content); public override int GetHashCode() { - return HashCode.Combine(Image, Url); + return HashCode.Combine(Image, Url, Begins, Expires); } } } From fd649edabae36603cccfc8a847a81de05b32f257 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 26 Mar 2024 20:21:48 +0800 Subject: [PATCH 0438/2493] Also don't rotate images during a drag operation --- osu.Game/Screens/Menu/OnlineMenuBanner.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Menu/OnlineMenuBanner.cs b/osu.Game/Screens/Menu/OnlineMenuBanner.cs index 37bec7aa63..260c021719 100644 --- a/osu.Game/Screens/Menu/OnlineMenuBanner.cs +++ b/osu.Game/Screens/Menu/OnlineMenuBanner.cs @@ -120,8 +120,8 @@ namespace osu.Game.Screens.Menu { nextDisplay?.Cancel(); - // If the user is hovering a banner, don't rotate yet. - bool anyHovered = content.Any(i => i.IsHovered); + // If the user is interacting with a banner, don't rotate yet. + bool anyHovered = content.Any(i => i.IsHovered || i.IsDragged); if (!anyHovered) { @@ -242,6 +242,8 @@ namespace osu.Game.Screens.Menu .ScaleTo(1, 500, Easing.OutElastic); base.OnMouseUp(e); } + + protected override bool OnDragStart(DragStartEvent e) => true; } } } From e77d4c8cfaf66e1e717b0094632a3704b77e73f0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 26 Mar 2024 20:28:03 +0800 Subject: [PATCH 0439/2493] Remove unnecessary `Math.Max` --- osu.Game/Screens/Menu/OnlineMenuBanner.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Menu/OnlineMenuBanner.cs b/osu.Game/Screens/Menu/OnlineMenuBanner.cs index 260c021719..6f98b73939 100644 --- a/osu.Game/Screens/Menu/OnlineMenuBanner.cs +++ b/osu.Game/Screens/Menu/OnlineMenuBanner.cs @@ -130,8 +130,8 @@ namespace osu.Game.Screens.Menu // To handle expiration simply, arrange all images in best-next order. // Fade in the first valid one, then handle fading out the last if required. var currentRotation = content - .Skip(Math.Max(0, previousIndex) + 1) - .Concat(content.Take(Math.Max(0, previousIndex) + 1)); + .Skip(previousIndex + 1) + .Concat(content.Take(previousIndex + 1)); // After the loop, displayIndex will be the new valid index or -1 if // none valid. From dee88573a756f9652628d13f2033cd4b78244870 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 26 Mar 2024 13:44:12 +0100 Subject: [PATCH 0440/2493] Fix test failure in visual browser I'm not sure why it's failing headless and I'm not particularly interested in finding that out right now. --- osu.Game.Tests/Visual/Menus/TestSceneOnlineMenuBanner.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Menus/TestSceneOnlineMenuBanner.cs b/osu.Game.Tests/Visual/Menus/TestSceneOnlineMenuBanner.cs index 380085ce04..60e42838d8 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneOnlineMenuBanner.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneOnlineMenuBanner.cs @@ -9,6 +9,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Testing; using osu.Game.Online.API.Requests.Responses; using osu.Game.Screens.Menu; +using osuTK; namespace osu.Game.Tests.Visual.Menus { @@ -95,7 +96,7 @@ namespace osu.Game.Tests.Visual.Menus } }); - AddUntilStep("wait for no image shown", () => !onlineMenuBanner.ChildrenOfType().Any()); + AddUntilStep("wait for no image shown", () => onlineMenuBanner.ChildrenOfType().Single().Size, () => Is.EqualTo(Vector2.Zero)); AddStep("unset system title", () => onlineMenuBanner.Current.Value = new APIMenuContent()); From b4ccbc68e447f0dba68470d70510195bfad009f5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 26 Mar 2024 21:20:22 +0800 Subject: [PATCH 0441/2493] Fix failing test --- osu.Game.Tests/Visual/Menus/TestSceneOnlineMenuBanner.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Menus/TestSceneOnlineMenuBanner.cs b/osu.Game.Tests/Visual/Menus/TestSceneOnlineMenuBanner.cs index 60e42838d8..0b90fd13c3 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneOnlineMenuBanner.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneOnlineMenuBanner.cs @@ -96,7 +96,7 @@ namespace osu.Game.Tests.Visual.Menus } }); - AddUntilStep("wait for no image shown", () => onlineMenuBanner.ChildrenOfType().Single().Size, () => Is.EqualTo(Vector2.Zero)); + AddUntilStep("wait for no image shown", () => onlineMenuBanner.ChildrenOfType().SingleOrDefault()?.Size, () => Is.EqualTo(Vector2.Zero)); AddStep("unset system title", () => onlineMenuBanner.Current.Value = new APIMenuContent()); From a5f15a119e325f954e8f5358227269a99dec4e5e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 26 Mar 2024 22:51:54 +0800 Subject: [PATCH 0442/2493] Apply rate adjust fix in all cases rather than specifically for `Clock.Rate == 1` --- osu.Game/Graphics/Containers/BeatSyncedContainer.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/osu.Game/Graphics/Containers/BeatSyncedContainer.cs b/osu.Game/Graphics/Containers/BeatSyncedContainer.cs index a14dfd4d64..de9a5aff3a 100644 --- a/osu.Game/Graphics/Containers/BeatSyncedContainer.cs +++ b/osu.Game/Graphics/Containers/BeatSyncedContainer.cs @@ -96,10 +96,8 @@ namespace osu.Game.Graphics.Containers // In the case of gameplay, we are usually within a hierarchy with the correct rate applied to our `Drawable.Clock`. // This means that the amount of early adjustment is adjusted in line with audio track rate changes. // But other cases like the osu! logo at the main menu won't correctly have this rate information. - // - // So for cases where the rate of the source isn't in sync with our hierarchy, let's assume we need to account for it locally. - if (Clock.Rate == 1 && BeatSyncSource.Clock.Rate != Clock.Rate) - early *= BeatSyncSource.Clock.Rate; + // We can adjust here to ensure the applied early activation always matches expectations. + early *= BeatSyncSource.Clock.Rate / Clock.Rate; currentTrackTime = BeatSyncSource.Clock.CurrentTime + early; From 4c1a1b54be7ffab08eb6e2713dbeca953f15044c Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 27 Mar 2024 00:07:51 +0900 Subject: [PATCH 0443/2493] Fix NVAPI startup exception on non-Windows platforms --- osu.Desktop/NVAPI.cs | 2 ++ osu.Desktop/Program.cs | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Desktop/NVAPI.cs b/osu.Desktop/NVAPI.cs index 78a814c585..554f89a847 100644 --- a/osu.Desktop/NVAPI.cs +++ b/osu.Desktop/NVAPI.cs @@ -8,11 +8,13 @@ using System; using System.Diagnostics.CodeAnalysis; using System.Runtime.InteropServices; +using System.Runtime.Versioning; using osu.Framework.Logging; namespace osu.Desktop { [SuppressMessage("ReSharper", "InconsistentNaming")] + [SupportedOSPlatform("windows")] internal static class NVAPI { private const string osu_filename = "osu!.exe"; diff --git a/osu.Desktop/Program.cs b/osu.Desktop/Program.cs index 73670adc49..2d7ec5aa5f 100644 --- a/osu.Desktop/Program.cs +++ b/osu.Desktop/Program.cs @@ -70,7 +70,8 @@ namespace osu.Desktop // NVIDIA profiles are based on the executable name of a process. // Lazer and stable share the same executable name. // Stable sets this setting to "Off", which may not be what we want, so let's force it back to the default "Auto" on startup. - NVAPI.ThreadedOptimisations = NvThreadControlSetting.OGL_THREAD_CONTROL_DEFAULT; + if (OperatingSystem.IsWindows()) + NVAPI.ThreadedOptimisations = NvThreadControlSetting.OGL_THREAD_CONTROL_DEFAULT; // Back up the cwd before DesktopGameHost changes it string cwd = Environment.CurrentDirectory; From 01a72d5afaa788e707308bea0764565ad1b044e6 Mon Sep 17 00:00:00 2001 From: Arthur Araujo Date: Tue, 26 Mar 2024 12:10:40 -0300 Subject: [PATCH 0444/2493] Fix combo not reversing properly depending on the order of selection --- .../Edit/CatchSelectionHandler.cs | 17 +++++++++-------- .../Edit/OsuSelectionHandler.cs | 17 +++++++++-------- 2 files changed, 18 insertions(+), 16 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Edit/CatchSelectionHandler.cs b/osu.Game.Rulesets.Catch/Edit/CatchSelectionHandler.cs index e3d2347c4d..f8fe9805e6 100644 --- a/osu.Game.Rulesets.Catch/Edit/CatchSelectionHandler.cs +++ b/osu.Game.Rulesets.Catch/Edit/CatchSelectionHandler.cs @@ -76,17 +76,15 @@ namespace osu.Game.Rulesets.Catch.Edit public override bool HandleReverse() { - var hitObjects = EditorBeatmap.SelectedHitObjects; + var hitObjects = EditorBeatmap.SelectedHitObjects + .OfType() + .OrderBy(obj => obj.StartTime) + .ToList(); double selectionStartTime = SelectedItems.Min(h => h.StartTime); double selectionEndTime = SelectedItems.Max(h => h.GetEndTime()); - var newComboOrder = hitObjects - .OfType() - .Select(obj => obj.NewCombo) - .ToList(); - - newComboOrder.Reverse(); + var newComboOrder = hitObjects.Select(obj => obj.NewCombo).ToList(); foreach (var h in hitObjects) { @@ -101,10 +99,13 @@ namespace osu.Game.Rulesets.Catch.Edit } } + // re-order objects again after flipping their times + hitObjects = [.. hitObjects.OrderBy(obj => obj.StartTime)]; + int i = 0; foreach (bool newCombo in newComboOrder) { - hitObjects.OfType().ToList()[i].NewCombo = newCombo; + hitObjects[i].NewCombo = newCombo; i++; } diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs index df39ab8fce..0e889cab81 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs @@ -78,19 +78,17 @@ namespace osu.Game.Rulesets.Osu.Edit public override bool HandleReverse() { - var hitObjects = EditorBeatmap.SelectedHitObjects; + var hitObjects = EditorBeatmap.SelectedHitObjects + .OfType() + .OrderBy(obj => obj.StartTime) + .ToList(); double endTime = hitObjects.Max(h => h.GetEndTime()); double startTime = hitObjects.Min(h => h.StartTime); bool moreThanOneObject = hitObjects.Count > 1; - var newComboOrder = hitObjects - .OfType() - .Select(obj => obj.NewCombo) - .ToList(); - - newComboOrder.Reverse(); + var newComboOrder = hitObjects.Select(obj => obj.NewCombo).ToList(); foreach (var h in hitObjects) { @@ -104,10 +102,13 @@ namespace osu.Game.Rulesets.Osu.Edit } } + // re-order objects again after flipping their times + hitObjects = [.. hitObjects.OrderBy(obj => obj.StartTime)]; + int i = 0; foreach (bool newCombo in newComboOrder) { - hitObjects.OfType().ToList()[i].NewCombo = newCombo; + hitObjects[i].NewCombo = newCombo; i++; } From e02ad6cf94d3a9bba0cbcac6ecee5a0125f9da89 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 26 Mar 2024 23:38:34 +0800 Subject: [PATCH 0445/2493] Fix test regression --- osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModTouchDevice.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModTouchDevice.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModTouchDevice.cs index fdb1cac3e5..8c81431770 100644 --- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModTouchDevice.cs +++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModTouchDevice.cs @@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods private SessionStatics statics { get; set; } = null!; private ScoreAccessibleSoloPlayer currentPlayer = null!; - private readonly ManualClock manualClock = new ManualClock { Rate = 0 }; + private readonly ManualClock manualClock = new ManualClock { Rate = 1 }; protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard? storyboard = null) => new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(manualClock), Audio); From 0b29a762b8aa712d1a0b697f367ae809a0896f15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 26 Mar 2024 17:36:00 +0100 Subject: [PATCH 0446/2493] Add precautionary guard to avoid potential div-by-zero Probably wouldn't happen outside of tests, but I'd rather not find out next release. --- osu.Game/Graphics/Containers/BeatSyncedContainer.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Graphics/Containers/BeatSyncedContainer.cs b/osu.Game/Graphics/Containers/BeatSyncedContainer.cs index de9a5aff3a..7210371ebf 100644 --- a/osu.Game/Graphics/Containers/BeatSyncedContainer.cs +++ b/osu.Game/Graphics/Containers/BeatSyncedContainer.cs @@ -97,7 +97,8 @@ namespace osu.Game.Graphics.Containers // This means that the amount of early adjustment is adjusted in line with audio track rate changes. // But other cases like the osu! logo at the main menu won't correctly have this rate information. // We can adjust here to ensure the applied early activation always matches expectations. - early *= BeatSyncSource.Clock.Rate / Clock.Rate; + if (Clock.Rate > 0) + early *= BeatSyncSource.Clock.Rate / Clock.Rate; currentTrackTime = BeatSyncSource.Clock.CurrentTime + early; From 600098d845611a45147154690cc09f992c961119 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 27 Mar 2024 04:05:04 +0900 Subject: [PATCH 0447/2493] Fix bulbs on Catmull sliders --- osu.Game/Rulesets/Objects/SliderPath.cs | 68 +++++++++++++++++++++++-- 1 file changed, 64 insertions(+), 4 deletions(-) diff --git a/osu.Game/Rulesets/Objects/SliderPath.cs b/osu.Game/Rulesets/Objects/SliderPath.cs index f33a07f082..5398d6c45f 100644 --- a/osu.Game/Rulesets/Objects/SliderPath.cs +++ b/osu.Game/Rulesets/Objects/SliderPath.cs @@ -42,6 +42,17 @@ namespace osu.Game.Rulesets.Objects private readonly List cumulativeLength = new List(); private readonly Cached pathCache = new Cached(); + /// + /// Any additional length of the path which was optimised out during piecewise approximation, but should still be considered as part of . + /// + /// + /// This is a hack for Catmull paths. + /// + private double optimisedLength; + + /// + /// The final calculated length of the path. + /// private double calculatedLength; private readonly List segmentEnds = new List(); @@ -244,6 +255,7 @@ namespace osu.Game.Rulesets.Objects { calculatedPath.Clear(); segmentEnds.Clear(); + optimisedLength = 0; if (ControlPoints.Count == 0) return; @@ -268,7 +280,8 @@ namespace osu.Game.Rulesets.Objects calculatedPath.Add(segmentVertices[0]); else if (segmentVertices.Length > 1) { - List subPath = calculateSubPath(segmentVertices, segmentType); + List subPath = calculateSubPath(segmentVertices, segmentType, ref optimisedLength); + // Skip the first vertex if it is the same as the last vertex from the previous segment bool skipFirst = calculatedPath.Count > 0 && subPath.Count > 0 && calculatedPath.Last() == subPath[0]; @@ -287,7 +300,7 @@ namespace osu.Game.Rulesets.Objects } } - private List calculateSubPath(ReadOnlySpan subControlPoints, PathType type) + private static List calculateSubPath(ReadOnlySpan subControlPoints, PathType type, ref double optimisedLength) { switch (type.Type) { @@ -295,6 +308,7 @@ namespace osu.Game.Rulesets.Objects return PathApproximator.LinearToPiecewiseLinear(subControlPoints); case SplineType.PerfectCurve: + { if (subControlPoints.Length != 3) break; @@ -305,9 +319,55 @@ namespace osu.Game.Rulesets.Objects break; return subPath; + } case SplineType.Catmull: - return PathApproximator.CatmullToPiecewiseLinear(subControlPoints); + { + List subPath = PathApproximator.CatmullToPiecewiseLinear(subControlPoints); + + // At draw time, osu!stable optimises paths by only keeping piecewise segments that are 6px apart. + // For the most part we don't care about this optimisation, and its additional heuristics are hard to reproduce in every implementation. + // + // However, it matters for Catmull paths which form "bulbs" around sequential knots with identical positions, + // so we'll apply a very basic form of the optimisation here and return a length representing the optimised portion. + // The returned length is important so that the optimisation doesn't cause the path to get extended to match the value of ExpectedDistance. + + List optimisedPath = new List(subPath.Count); + + Vector2? lastStart = null; + double lengthRemovedSinceStart = 0; + + for (int i = 0; i < subPath.Count; i++) + { + if (lastStart == null) + { + optimisedPath.Add(subPath[i]); + lastStart = subPath[i]; + continue; + } + + Debug.Assert(i > 0); + + double distFromStart = Vector2.Distance(lastStart.Value, subPath[i]); + lengthRemovedSinceStart += Vector2.Distance(subPath[i - 1], subPath[i]); + + // See PathApproximator.catmull_detail. + const int catmull_detail = 50; + const int catmull_segment_length = catmull_detail * 2; + + // Either 6px from the start, the last vertex at every knot, or the end of the path. + if (distFromStart > 6 || (i + 1) % catmull_segment_length == 0 || i == subPath.Count - 1) + { + optimisedPath.Add(subPath[i]); + optimisedLength += lengthRemovedSinceStart - distFromStart; + + lastStart = null; + lengthRemovedSinceStart = 0; + } + } + + return optimisedPath; + } } return PathApproximator.BSplineToPiecewiseLinear(subControlPoints, type.Degree ?? subControlPoints.Length); @@ -315,7 +375,7 @@ namespace osu.Game.Rulesets.Objects private void calculateLength() { - calculatedLength = 0; + calculatedLength = optimisedLength; cumulativeLength.Clear(); cumulativeLength.Add(0); From 4490dbf8960214dcc9d3f67dbfc88ab5255c6e94 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 27 Mar 2024 13:37:47 +0900 Subject: [PATCH 0448/2493] Update diffcalc workflow --- .github/workflows/diffcalc.yml | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/.github/workflows/diffcalc.yml b/.github/workflows/diffcalc.yml index 7a2dcecb9c..2ed176fe8d 100644 --- a/.github/workflows/diffcalc.yml +++ b/.github/workflows/diffcalc.yml @@ -110,10 +110,14 @@ jobs: if: ${{ github.event_name == 'workflow_dispatch' || contains(github.event.comment.body, '!diffcalc') }} steps: - name: Check permissions - if: ${{ github.event_name != 'workflow_dispatch' }} - uses: actions-cool/check-user-permission@a0668c9aec87f3875fc56170b6452a453e9dd819 # v2.2.0 - with: - require: 'write' + run: | + ALLOWED_USERS=(smoogipoo peppy bdach) + for i in "${ALLOWED_USERS[@]}"; do + if [[ "${{ github.actor }}" == "$i" ]]; then + exit 0 + fi + done + exit 1 create-comment: name: Create PR comment From 60c93d2c6de6690913ba6f9552615116c2470a29 Mon Sep 17 00:00:00 2001 From: Arthur Araujo Date: Wed, 27 Mar 2024 08:22:51 -0300 Subject: [PATCH 0449/2493] Add reverse pattern visual tests --- .../Editor/TestSceneCatchReverseSelection.cs | 313 ++++++++++++++++ .../Editor/TestSceneOsuReverseSelection.cs | 347 ++++++++++++++++++ 2 files changed, 660 insertions(+) create mode 100644 osu.Game.Rulesets.Catch.Tests/Editor/TestSceneCatchReverseSelection.cs create mode 100644 osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuReverseSelection.cs diff --git a/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneCatchReverseSelection.cs b/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneCatchReverseSelection.cs new file mode 100644 index 0000000000..c8a48f76eb --- /dev/null +++ b/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneCatchReverseSelection.cs @@ -0,0 +1,313 @@ +// 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.Catch.Objects; +using osu.Game.Rulesets.Objects; +using osu.Game.Tests.Beatmaps; +using osuTK; +using osuTK.Input; + +namespace osu.Game.Rulesets.Catch.Tests.Editor +{ + [TestFixture] + public partial class TestSceneCatchReverseSelection : TestSceneEditor + { + protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(ruleset, false); + + [Test] + public void TestReverseSelectionTwoFruits() + { + float fruit1OldX = default; + float fruit2OldX = default; + + addObjects([ + new Fruit + { + StartTime = 200, + X = fruit1OldX = 0, + }, + new Fruit + { + StartTime = 400, + X = fruit2OldX = 20, + } + ]); + + selectEverything(); + reverseSelection(); + + AddAssert("fruit1 is at fruit2's X", + () => EditorBeatmap.HitObjects.OfType().ElementAt(0).EffectiveX, + () => Is.EqualTo(fruit2OldX) + ); + + AddAssert("fruit2 is at fruit1's X", + () => EditorBeatmap.HitObjects.OfType().ElementAt(1).EffectiveX, + () => Is.EqualTo(fruit1OldX) + ); + + AddAssert("fruit2 is not a new combo", + () => EditorBeatmap.HitObjects.OfType().ElementAt(1).NewCombo, + () => Is.EqualTo(false) + ); + } + + [Test] + public void TestReverseSelectionThreeFruits() + { + float fruit1OldX = default; + float fruit2OldX = default; + float fruit3OldX = default; + + addObjects([ + new Fruit + { + StartTime = 200, + X = fruit1OldX = 0, + }, + new Fruit + { + StartTime = 400, + X = fruit2OldX = 20, + }, + new Fruit + { + StartTime = 600, + X = fruit3OldX = 40, + } + ]); + + selectEverything(); + reverseSelection(); + + AddAssert("fruit1 is at fruit3's X", + () => EditorBeatmap.HitObjects.OfType().ElementAt(0).EffectiveX, + () => Is.EqualTo(fruit3OldX) + ); + + AddAssert("fruit2's X is unchanged", + () => EditorBeatmap.HitObjects.OfType().ElementAt(1).EffectiveX, + () => Is.EqualTo(fruit2OldX) + ); + + AddAssert("fruit3's is at fruit1's X", + () => EditorBeatmap.HitObjects.OfType().ElementAt(2).EffectiveX, + () => Is.EqualTo(fruit1OldX) + ); + + AddAssert("fruit3 is not a new combo", + () => EditorBeatmap.HitObjects.OfType().ElementAt(2).NewCombo, + () => Is.EqualTo(false) + ); + } + + [Test] + public void TestReverseSelectionFruitAndJuiceStream() + { + addObjects([ + new Fruit + { + StartTime = 200, + X = 0, + }, + new JuiceStream + { + StartTime = 400, + X = 20, + Path = new SliderPath + { + ControlPoints = + { + new PathControlPoint(), + new PathControlPoint(new Vector2(50)) + } + } + } + ]); + + selectEverything(); + reverseSelection(); + + AddAssert("First element is juice stream", + () => EditorBeatmap.HitObjects.First().GetType(), + () => Is.EqualTo(typeof(JuiceStream)) + ); + + AddAssert("Last element is fruit", + () => EditorBeatmap.HitObjects.Last().GetType(), + () => Is.EqualTo(typeof(Fruit)) + ); + + AddAssert("Fruit is not new combo", + () => EditorBeatmap.HitObjects.OfType().ElementAt(0).NewCombo, + () => Is.EqualTo(false) + ); + } + + [Test] + public void TestReverseSelectionTwoFruitsAndJuiceStream() + { + addObjects([ + new Fruit + { + StartTime = 200, + X = 0, + }, + new Fruit + { + StartTime = 400, + X = 20, + }, + new JuiceStream + { + StartTime = 600, + X = 40, + Path = new SliderPath + { + ControlPoints = + { + new PathControlPoint(), + new PathControlPoint(new Vector2(50)) + } + } + } + ]); + + selectEverything(); + reverseSelection(); + + AddAssert("First element is juice stream", + () => EditorBeatmap.HitObjects.First().GetType(), + () => Is.EqualTo(typeof(JuiceStream)) + ); + + AddAssert("Middle element is Fruit", + () => EditorBeatmap.HitObjects.ElementAt(1).GetType(), + () => Is.EqualTo(typeof(Fruit)) + ); + + AddAssert("Last element is Fruit", + () => EditorBeatmap.HitObjects.Last().GetType(), + () => Is.EqualTo(typeof(Fruit)) + ); + + AddAssert("Last fruit is not new combo", + () => EditorBeatmap.HitObjects.OfType().Last().NewCombo, + () => Is.EqualTo(false) + ); + } + + [Test] + public void TestReverseSelectionTwoCombos() + { + float fruit1OldX = default; + float fruit2OldX = default; + float fruit3OldX = default; + + float fruit4OldX = default; + float fruit5OldX = default; + float fruit6OldX = default; + + addObjects([ + new Fruit + { + StartTime = 200, + X = fruit1OldX = 0, + }, + new Fruit + { + StartTime = 400, + X = fruit2OldX = 20, + }, + new Fruit + { + StartTime = 600, + X = fruit3OldX = 40, + }, + + new Fruit + { + StartTime = 800, + NewCombo = true, + X = fruit4OldX = 60, + }, + new Fruit + { + StartTime = 1000, + X = fruit5OldX = 80, + }, + new Fruit + { + StartTime = 1200, + X = fruit6OldX = 100, + } + ]); + + selectEverything(); + reverseSelection(); + + AddAssert("fruit1 is at fruit6 position", + () => EditorBeatmap.HitObjects.OfType().ElementAt(0).EffectiveX, + () => Is.EqualTo(fruit6OldX) + ); + + AddAssert("fruit2 is at fruit5 position", + () => EditorBeatmap.HitObjects.OfType().ElementAt(1).EffectiveX, + () => Is.EqualTo(fruit5OldX) + ); + + AddAssert("fruit3 is at fruit4 position", + () => EditorBeatmap.HitObjects.OfType().ElementAt(2).EffectiveX, + () => Is.EqualTo(fruit4OldX) + ); + + AddAssert("fruit4 is at fruit3 position", + () => EditorBeatmap.HitObjects.OfType().ElementAt(3).EffectiveX, + () => Is.EqualTo(fruit3OldX) + ); + + AddAssert("fruit5 is at fruit2 position", + () => EditorBeatmap.HitObjects.OfType().ElementAt(4).EffectiveX, + () => Is.EqualTo(fruit2OldX) + ); + + AddAssert("fruit6 is at fruit1 position", + () => EditorBeatmap.HitObjects.OfType().ElementAt(5).EffectiveX, + () => Is.EqualTo(fruit1OldX) + ); + + AddAssert("fruit1 is new combo", + () => EditorBeatmap.HitObjects.OfType().ElementAt(0).NewCombo, + () => Is.EqualTo(true) + ); + + AddAssert("fruit4 is new combo", + () => EditorBeatmap.HitObjects.OfType().ElementAt(3).NewCombo, + () => Is.EqualTo(true) + ); + } + + private void addObjects(CatchHitObject[] hitObjects) => AddStep("Add objects", () => EditorBeatmap.AddRange(hitObjects)); + + private void selectEverything() + { + AddStep("Select everything", () => + { + EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects); + }); + } + + private void reverseSelection() + { + AddStep("Reverse selection", () => + { + InputManager.PressKey(Key.LControl); + InputManager.Key(Key.G); + InputManager.ReleaseKey(Key.LControl); + }); + } + } +} diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuReverseSelection.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuReverseSelection.cs new file mode 100644 index 0000000000..33104288ab --- /dev/null +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuReverseSelection.cs @@ -0,0 +1,347 @@ +// 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.Objects; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Tests.Beatmaps; +using osuTK; +using osuTK.Input; + +namespace osu.Game.Rulesets.Osu.Tests.Editor +{ + [TestFixture] + public partial class TestSceneOsuReverseSelection : TestSceneOsuEditor + { + protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(ruleset, false); + + [Test] + public void TestReverseSelectionTwoCircles() + { + Vector2 circle1OldPosition = default; + Vector2 circle2OldPosition = default; + + AddStep("Add circles", () => + { + var circle1 = new HitCircle + { + StartTime = 0, + Position = circle1OldPosition = new Vector2(208, 240) + }; + var circle2 = new HitCircle + { + StartTime = 200, + Position = circle2OldPosition = new Vector2(256, 144) + }; + + EditorBeatmap.AddRange([circle1, circle2]); + }); + + AddStep("Select circles", () => + { + EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects); + }); + + AddStep("Reverse selection", () => + { + InputManager.PressKey(Key.LControl); + InputManager.Key(Key.G); + InputManager.ReleaseKey(Key.LControl); + }); + + AddAssert("circle1 is at circle2 position", + () => EditorBeatmap.HitObjects.OfType().ElementAt(0).Position, + () => Is.EqualTo(circle2OldPosition) + ); + + AddAssert("circle2 is at circle1 position", + () => EditorBeatmap.HitObjects.OfType().ElementAt(1).Position, + () => Is.EqualTo(circle1OldPosition) + ); + + AddAssert("circle2 is not a new combo", + () => EditorBeatmap.HitObjects.OfType().ElementAt(1).NewCombo, + () => Is.EqualTo(false) + ); + } + + [Test] + public void TestReverseSelectionThreeCircles() + { + Vector2 circle1OldPosition = default; + Vector2 circle2OldPosition = default; + Vector2 circle3OldPosition = default; + + AddStep("Add circles", () => + { + var circle1 = new HitCircle + { + StartTime = 0, + Position = circle1OldPosition = new Vector2(208, 240) + }; + var circle2 = new HitCircle + { + StartTime = 200, + Position = circle2OldPosition = new Vector2(256, 144) + }; + var circle3 = new HitCircle + { + StartTime = 400, + Position = circle3OldPosition = new Vector2(304, 240) + }; + + EditorBeatmap.AddRange([circle1, circle2, circle3]); + }); + + AddStep("Select circles", () => + { + EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects); + }); + + AddStep("Reverse selection", () => + { + InputManager.PressKey(Key.LControl); + InputManager.Key(Key.G); + InputManager.ReleaseKey(Key.LControl); + }); + + AddAssert("circle1 is at circle3 position", + () => EditorBeatmap.HitObjects.OfType().ElementAt(0).Position, + () => Is.EqualTo(circle3OldPosition) + ); + + AddAssert("circle3 is at circle1 position", + () => EditorBeatmap.HitObjects.OfType().ElementAt(2).Position, + () => Is.EqualTo(circle1OldPosition) + ); + + AddAssert("circle3 is not a new combo", + () => EditorBeatmap.HitObjects.OfType().ElementAt(2).NewCombo, + () => Is.EqualTo(false) + ); + } + + [Test] + public void TestReverseSelectionCircleAndSlider() + { + Vector2 circleOldPosition = default; + Vector2 sliderHeadOldPosition = default; + Vector2 sliderTailOldPosition = default; + + AddStep("Add objects", () => + { + var circle = new HitCircle + { + StartTime = 0, + Position = circleOldPosition = new Vector2(208, 240) + }; + var slider = new Slider + { + StartTime = 200, + Position = sliderHeadOldPosition = new Vector2(257, 144), + Path = new SliderPath + { + ControlPoints = + { + new PathControlPoint(), + new PathControlPoint(new Vector2(100)) + } + } + }; + + sliderTailOldPosition = slider.EndPosition; + + EditorBeatmap.AddRange([circle, slider]); + }); + + AddStep("Select objects", () => + { + var circle = (HitCircle)EditorBeatmap.HitObjects[0]; + var slider = (Slider)EditorBeatmap.HitObjects[1]; + + EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects); + }); + + AddStep("Reverse selection", () => + { + InputManager.PressKey(Key.LControl); + InputManager.Key(Key.G); + InputManager.ReleaseKey(Key.LControl); + }); + + AddAssert("circle is at the same position", + () => EditorBeatmap.HitObjects.OfType().ElementAt(0).Position, + () => Is.EqualTo(circleOldPosition) + ); + + AddAssert("Slider head is at slider tail", () => + Vector2.Distance(EditorBeatmap.HitObjects.OfType().ElementAt(0).Position, sliderTailOldPosition) < 1); + + AddAssert("Slider tail is at slider head", () => + Vector2.Distance(EditorBeatmap.HitObjects.OfType().ElementAt(0).EndPosition, sliderHeadOldPosition) < 1); + } + + [Test] + public void TestReverseSelectionTwoCirclesAndSlider() + { + Vector2 circle1OldPosition = default; + Vector2 circle2OldPosition = default; + + Vector2 sliderHeadOldPosition = default; + Vector2 sliderTailOldPosition = default; + + AddStep("Add objects", () => + { + var circle1 = new HitCircle + { + StartTime = 0, + Position = circle1OldPosition = new Vector2(208, 240) + }; + var circle2 = new HitCircle + { + StartTime = 200, + Position = circle2OldPosition = new Vector2(256, 144) + }; + var slider = new Slider + { + StartTime = 200, + Position = sliderHeadOldPosition = new Vector2(304, 240), + Path = new SliderPath + { + ControlPoints = + { + new PathControlPoint(), + new PathControlPoint(new Vector2(100)) + } + } + }; + + sliderTailOldPosition = slider.EndPosition; + + EditorBeatmap.AddRange([circle1, circle2, slider]); + }); + + AddStep("Select objects", () => + { + EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects); + }); + + AddStep("Reverse selection", () => + { + InputManager.PressKey(Key.LControl); + InputManager.Key(Key.G); + InputManager.ReleaseKey(Key.LControl); + }); + + AddAssert("circle1 is at circle2 position", + () => EditorBeatmap.HitObjects.OfType().ElementAt(0).Position, + () => Is.EqualTo(circle2OldPosition) + ); + + AddAssert("circle2 is at circle1 position", + () => EditorBeatmap.HitObjects.OfType().ElementAt(1).Position, + () => Is.EqualTo(circle1OldPosition) + ); + + AddAssert("Slider head is at slider tail", () => + Vector2.Distance(EditorBeatmap.HitObjects.OfType().ElementAt(0).Position, sliderTailOldPosition) < 1); + + AddAssert("Slider tail is at slider head", () => + Vector2.Distance(EditorBeatmap.HitObjects.OfType().ElementAt(0).EndPosition, sliderHeadOldPosition) < 1); + } + + [Test] + public void TestReverseSelectionTwoCombos() + { + Vector2 circle1OldPosition = default; + Vector2 circle2OldPosition = default; + Vector2 circle3OldPosition = default; + + Vector2 circle4OldPosition = default; + Vector2 circle5OldPosition = default; + Vector2 circle6OldPosition = default; + + AddStep("Add circles", () => + { + var circle1 = new HitCircle + { + StartTime = 0, + Position = circle1OldPosition = new Vector2(216, 240) + }; + var circle2 = new HitCircle + { + StartTime = 200, + Position = circle2OldPosition = new Vector2(120, 192) + }; + var circle3 = new HitCircle + { + StartTime = 400, + Position = circle3OldPosition = new Vector2(216, 144) + }; + + var circle4 = new HitCircle + { + StartTime = 646, + NewCombo = true, + Position = circle4OldPosition = new Vector2(296, 240) + }; + var circle5 = new HitCircle + { + StartTime = 846, + Position = circle5OldPosition = new Vector2(392, 162) + }; + var circle6 = new HitCircle + { + StartTime = 1046, + Position = circle6OldPosition = new Vector2(296, 144) + }; + + EditorBeatmap.AddRange([circle1, circle2, circle3, circle4, circle5, circle6]); + }); + + AddStep("Select circles", () => + { + EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects); + }); + + AddStep("Reverse selection", () => + { + InputManager.PressKey(Key.LControl); + InputManager.Key(Key.G); + InputManager.ReleaseKey(Key.LControl); + }); + + AddAssert("circle1 is at circle6 position", + () => EditorBeatmap.HitObjects.OfType().ElementAt(0).Position, + () => Is.EqualTo(circle6OldPosition) + ); + + AddAssert("circle2 is at circle5 position", + () => EditorBeatmap.HitObjects.OfType().ElementAt(1).Position, + () => Is.EqualTo(circle5OldPosition) + ); + + AddAssert("circle3 is at circle4 position", + () => EditorBeatmap.HitObjects.OfType().ElementAt(2).Position, + () => Is.EqualTo(circle4OldPosition) + ); + + AddAssert("circle4 is at circle3 position", + () => EditorBeatmap.HitObjects.OfType().ElementAt(3).Position, + () => Is.EqualTo(circle3OldPosition) + ); + + AddAssert("circle5 is at circle2 position", + () => EditorBeatmap.HitObjects.OfType().ElementAt(4).Position, + () => Is.EqualTo(circle2OldPosition) + ); + + AddAssert("circle6 is at circle1 position", + () => EditorBeatmap.HitObjects.OfType().ElementAt(5).Position, + () => Is.EqualTo(circle1OldPosition) + ); + } + } +} From 53900d5472ac14a41003f5353bd704e42e245a7b Mon Sep 17 00:00:00 2001 From: Susko3 Date: Wed, 27 Mar 2024 18:56:26 +0100 Subject: [PATCH 0450/2493] Fix tests failing locally due to not using invariant culture --- osu.Game/Skinning/SkinImporter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Skinning/SkinImporter.cs b/osu.Game/Skinning/SkinImporter.cs index 3e948a8afb..59c7f0ba26 100644 --- a/osu.Game/Skinning/SkinImporter.cs +++ b/osu.Game/Skinning/SkinImporter.cs @@ -132,7 +132,7 @@ namespace osu.Game.Skinning { // skins without a skin.ini are supposed to import using the "latest version" spec. // see https://github.com/peppy/osu-stable-reference/blob/1531237b63392e82c003c712faa028406073aa8f/osu!/Graphics/Skinning/SkinManager.cs#L297-L298 - newLines.Add($"Version: {SkinConfiguration.LATEST_VERSION}"); + newLines.Add(FormattableString.Invariant($"Version: {SkinConfiguration.LATEST_VERSION}")); // In the case a skin doesn't have a skin.ini yet, let's create one. writeNewSkinIni(); From a9cbabf71135ff97c587a1c01079b56850ef27f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 28 Mar 2024 10:05:26 +0100 Subject: [PATCH 0451/2493] Simplify tests --- .../Editor/TestSceneCatchReverseSelection.cs | 196 ++++++------------ .../Editor/TestSceneOsuReverseSelection.cs | 177 ++++++---------- 2 files changed, 133 insertions(+), 240 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneCatchReverseSelection.cs b/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneCatchReverseSelection.cs index c8a48f76eb..36a0e3388e 100644 --- a/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneCatchReverseSelection.cs +++ b/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneCatchReverseSelection.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; @@ -20,93 +21,78 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor [Test] public void TestReverseSelectionTwoFruits() { - float fruit1OldX = default; - float fruit2OldX = default; + CatchHitObject[] objects = null!; + bool[] newCombos = null!; addObjects([ new Fruit { StartTime = 200, - X = fruit1OldX = 0, + X = 0, }, new Fruit { StartTime = 400, - X = fruit2OldX = 20, + X = 20, } ]); + AddStep("store objects & new combo data", () => + { + objects = getObjects().ToArray(); + newCombos = getObjectNewCombos().ToArray(); + }); + selectEverything(); reverseSelection(); - AddAssert("fruit1 is at fruit2's X", - () => EditorBeatmap.HitObjects.OfType().ElementAt(0).EffectiveX, - () => Is.EqualTo(fruit2OldX) - ); - - AddAssert("fruit2 is at fruit1's X", - () => EditorBeatmap.HitObjects.OfType().ElementAt(1).EffectiveX, - () => Is.EqualTo(fruit1OldX) - ); - - AddAssert("fruit2 is not a new combo", - () => EditorBeatmap.HitObjects.OfType().ElementAt(1).NewCombo, - () => Is.EqualTo(false) - ); + AddAssert("objects reversed", getObjects, () => Is.EqualTo(objects.Reverse())); + AddAssert("new combo positions preserved", getObjectNewCombos, () => Is.EqualTo(newCombos)); } [Test] public void TestReverseSelectionThreeFruits() { - float fruit1OldX = default; - float fruit2OldX = default; - float fruit3OldX = default; + CatchHitObject[] objects = null!; + bool[] newCombos = null!; addObjects([ new Fruit { StartTime = 200, - X = fruit1OldX = 0, + X = 0, }, new Fruit { StartTime = 400, - X = fruit2OldX = 20, + X = 20, }, new Fruit { StartTime = 600, - X = fruit3OldX = 40, + X = 40, } ]); + AddStep("store objects & new combo data", () => + { + objects = getObjects().ToArray(); + newCombos = getObjectNewCombos().ToArray(); + }); + selectEverything(); reverseSelection(); - AddAssert("fruit1 is at fruit3's X", - () => EditorBeatmap.HitObjects.OfType().ElementAt(0).EffectiveX, - () => Is.EqualTo(fruit3OldX) - ); - - AddAssert("fruit2's X is unchanged", - () => EditorBeatmap.HitObjects.OfType().ElementAt(1).EffectiveX, - () => Is.EqualTo(fruit2OldX) - ); - - AddAssert("fruit3's is at fruit1's X", - () => EditorBeatmap.HitObjects.OfType().ElementAt(2).EffectiveX, - () => Is.EqualTo(fruit1OldX) - ); - - AddAssert("fruit3 is not a new combo", - () => EditorBeatmap.HitObjects.OfType().ElementAt(2).NewCombo, - () => Is.EqualTo(false) - ); + AddAssert("objects reversed", getObjects, () => Is.EqualTo(objects.Reverse())); + AddAssert("new combo positions preserved", getObjectNewCombos, () => Is.EqualTo(newCombos)); } [Test] public void TestReverseSelectionFruitAndJuiceStream() { + CatchHitObject[] objects = null!; + bool[] newCombos = null!; + addObjects([ new Fruit { @@ -128,28 +114,25 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor } ]); + AddStep("store objects & new combo data", () => + { + objects = getObjects().ToArray(); + newCombos = getObjectNewCombos().ToArray(); + }); + selectEverything(); reverseSelection(); - AddAssert("First element is juice stream", - () => EditorBeatmap.HitObjects.First().GetType(), - () => Is.EqualTo(typeof(JuiceStream)) - ); - - AddAssert("Last element is fruit", - () => EditorBeatmap.HitObjects.Last().GetType(), - () => Is.EqualTo(typeof(Fruit)) - ); - - AddAssert("Fruit is not new combo", - () => EditorBeatmap.HitObjects.OfType().ElementAt(0).NewCombo, - () => Is.EqualTo(false) - ); + AddAssert("objects reversed", getObjects, () => Is.EqualTo(objects.Reverse())); + AddAssert("new combo positions preserved", getObjectNewCombos, () => Is.EqualTo(newCombos)); } [Test] public void TestReverseSelectionTwoFruitsAndJuiceStream() { + CatchHitObject[] objects = null!; + bool[] newCombos = null!; + addObjects([ new Fruit { @@ -176,122 +159,79 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor } ]); + AddStep("store objects & new combo data", () => + { + objects = getObjects().ToArray(); + newCombos = getObjectNewCombos().ToArray(); + }); + selectEverything(); reverseSelection(); - AddAssert("First element is juice stream", - () => EditorBeatmap.HitObjects.First().GetType(), - () => Is.EqualTo(typeof(JuiceStream)) - ); - - AddAssert("Middle element is Fruit", - () => EditorBeatmap.HitObjects.ElementAt(1).GetType(), - () => Is.EqualTo(typeof(Fruit)) - ); - - AddAssert("Last element is Fruit", - () => EditorBeatmap.HitObjects.Last().GetType(), - () => Is.EqualTo(typeof(Fruit)) - ); - - AddAssert("Last fruit is not new combo", - () => EditorBeatmap.HitObjects.OfType().Last().NewCombo, - () => Is.EqualTo(false) - ); + AddAssert("objects reversed", getObjects, () => Is.EqualTo(objects.Reverse())); + AddAssert("new combo positions preserved", getObjectNewCombos, () => Is.EqualTo(newCombos)); } [Test] public void TestReverseSelectionTwoCombos() { - float fruit1OldX = default; - float fruit2OldX = default; - float fruit3OldX = default; - - float fruit4OldX = default; - float fruit5OldX = default; - float fruit6OldX = default; + CatchHitObject[] objects = null!; + bool[] newCombos = null!; addObjects([ new Fruit { StartTime = 200, - X = fruit1OldX = 0, + X = 0, }, new Fruit { StartTime = 400, - X = fruit2OldX = 20, + X = 20, }, new Fruit { StartTime = 600, - X = fruit3OldX = 40, + X = 40, }, new Fruit { StartTime = 800, NewCombo = true, - X = fruit4OldX = 60, + X = 60, }, new Fruit { StartTime = 1000, - X = fruit5OldX = 80, + X = 80, }, new Fruit { StartTime = 1200, - X = fruit6OldX = 100, + X = 100, } ]); + AddStep("store objects & new combo data", () => + { + objects = getObjects().ToArray(); + newCombos = getObjectNewCombos().ToArray(); + }); + selectEverything(); reverseSelection(); - AddAssert("fruit1 is at fruit6 position", - () => EditorBeatmap.HitObjects.OfType().ElementAt(0).EffectiveX, - () => Is.EqualTo(fruit6OldX) - ); - - AddAssert("fruit2 is at fruit5 position", - () => EditorBeatmap.HitObjects.OfType().ElementAt(1).EffectiveX, - () => Is.EqualTo(fruit5OldX) - ); - - AddAssert("fruit3 is at fruit4 position", - () => EditorBeatmap.HitObjects.OfType().ElementAt(2).EffectiveX, - () => Is.EqualTo(fruit4OldX) - ); - - AddAssert("fruit4 is at fruit3 position", - () => EditorBeatmap.HitObjects.OfType().ElementAt(3).EffectiveX, - () => Is.EqualTo(fruit3OldX) - ); - - AddAssert("fruit5 is at fruit2 position", - () => EditorBeatmap.HitObjects.OfType().ElementAt(4).EffectiveX, - () => Is.EqualTo(fruit2OldX) - ); - - AddAssert("fruit6 is at fruit1 position", - () => EditorBeatmap.HitObjects.OfType().ElementAt(5).EffectiveX, - () => Is.EqualTo(fruit1OldX) - ); - - AddAssert("fruit1 is new combo", - () => EditorBeatmap.HitObjects.OfType().ElementAt(0).NewCombo, - () => Is.EqualTo(true) - ); - - AddAssert("fruit4 is new combo", - () => EditorBeatmap.HitObjects.OfType().ElementAt(3).NewCombo, - () => Is.EqualTo(true) - ); + AddAssert("objects reversed", getObjects, () => Is.EqualTo(objects.Reverse())); + AddAssert("new combo positions preserved", getObjectNewCombos, () => Is.EqualTo(newCombos)); } private void addObjects(CatchHitObject[] hitObjects) => AddStep("Add objects", () => EditorBeatmap.AddRange(hitObjects)); + private IEnumerable getObjects() => EditorBeatmap.HitObjects.OfType(); + + private IEnumerable getObjectNewCombos() => getObjects().Select(ho => ho.NewCombo); + private void selectEverything() { AddStep("Select everything", () => diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuReverseSelection.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuReverseSelection.cs index 33104288ab..28c1577fcb 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuReverseSelection.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuReverseSelection.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; @@ -20,30 +21,33 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor [Test] public void TestReverseSelectionTwoCircles() { - Vector2 circle1OldPosition = default; - Vector2 circle2OldPosition = default; + OsuHitObject[] objects = null!; + bool[] newCombos = null!; AddStep("Add circles", () => { var circle1 = new HitCircle { StartTime = 0, - Position = circle1OldPosition = new Vector2(208, 240) + Position = new Vector2(208, 240) }; var circle2 = new HitCircle { StartTime = 200, - Position = circle2OldPosition = new Vector2(256, 144) + Position = new Vector2(256, 144) }; EditorBeatmap.AddRange([circle1, circle2]); }); - AddStep("Select circles", () => + AddStep("store objects & new combo data", () => { - EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects); + objects = getObjects().ToArray(); + newCombos = getObjectNewCombos().ToArray(); }); + AddStep("Select circles", () => EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects)); + AddStep("Reverse selection", () => { InputManager.PressKey(Key.LControl); @@ -51,55 +55,45 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor InputManager.ReleaseKey(Key.LControl); }); - AddAssert("circle1 is at circle2 position", - () => EditorBeatmap.HitObjects.OfType().ElementAt(0).Position, - () => Is.EqualTo(circle2OldPosition) - ); - - AddAssert("circle2 is at circle1 position", - () => EditorBeatmap.HitObjects.OfType().ElementAt(1).Position, - () => Is.EqualTo(circle1OldPosition) - ); - - AddAssert("circle2 is not a new combo", - () => EditorBeatmap.HitObjects.OfType().ElementAt(1).NewCombo, - () => Is.EqualTo(false) - ); + AddAssert("objects reversed", getObjects, () => Is.EqualTo(objects.Reverse())); + AddAssert("new combo positions preserved", getObjectNewCombos, () => Is.EqualTo(newCombos)); } [Test] public void TestReverseSelectionThreeCircles() { - Vector2 circle1OldPosition = default; - Vector2 circle2OldPosition = default; - Vector2 circle3OldPosition = default; + OsuHitObject[] objects = null!; + bool[] newCombos = null!; AddStep("Add circles", () => { var circle1 = new HitCircle { StartTime = 0, - Position = circle1OldPosition = new Vector2(208, 240) + Position = new Vector2(208, 240) }; var circle2 = new HitCircle { StartTime = 200, - Position = circle2OldPosition = new Vector2(256, 144) + Position = new Vector2(256, 144) }; var circle3 = new HitCircle { StartTime = 400, - Position = circle3OldPosition = new Vector2(304, 240) + Position = new Vector2(304, 240) }; EditorBeatmap.AddRange([circle1, circle2, circle3]); }); - AddStep("Select circles", () => + AddStep("store objects & new combo data", () => { - EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects); + objects = getObjects().ToArray(); + newCombos = getObjectNewCombos().ToArray(); }); + AddStep("Select circles", () => EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects)); + AddStep("Reverse selection", () => { InputManager.PressKey(Key.LControl); @@ -107,26 +101,16 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor InputManager.ReleaseKey(Key.LControl); }); - AddAssert("circle1 is at circle3 position", - () => EditorBeatmap.HitObjects.OfType().ElementAt(0).Position, - () => Is.EqualTo(circle3OldPosition) - ); - - AddAssert("circle3 is at circle1 position", - () => EditorBeatmap.HitObjects.OfType().ElementAt(2).Position, - () => Is.EqualTo(circle1OldPosition) - ); - - AddAssert("circle3 is not a new combo", - () => EditorBeatmap.HitObjects.OfType().ElementAt(2).NewCombo, - () => Is.EqualTo(false) - ); + AddAssert("objects reversed", getObjects, () => Is.EqualTo(objects.Reverse())); + AddAssert("new combo positions preserved", getObjectNewCombos, () => Is.EqualTo(newCombos)); } [Test] public void TestReverseSelectionCircleAndSlider() { - Vector2 circleOldPosition = default; + OsuHitObject[] objects = null!; + bool[] newCombos = null!; + Vector2 sliderHeadOldPosition = default; Vector2 sliderTailOldPosition = default; @@ -135,7 +119,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor var circle = new HitCircle { StartTime = 0, - Position = circleOldPosition = new Vector2(208, 240) + Position = new Vector2(208, 240) }; var slider = new Slider { @@ -156,14 +140,14 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor EditorBeatmap.AddRange([circle, slider]); }); - AddStep("Select objects", () => + AddStep("store objects & new combo data", () => { - var circle = (HitCircle)EditorBeatmap.HitObjects[0]; - var slider = (Slider)EditorBeatmap.HitObjects[1]; - - EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects); + objects = getObjects().ToArray(); + newCombos = getObjectNewCombos().ToArray(); }); + AddStep("Select objects", () => EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects)); + AddStep("Reverse selection", () => { InputManager.PressKey(Key.LControl); @@ -171,10 +155,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor InputManager.ReleaseKey(Key.LControl); }); - AddAssert("circle is at the same position", - () => EditorBeatmap.HitObjects.OfType().ElementAt(0).Position, - () => Is.EqualTo(circleOldPosition) - ); + AddAssert("objects reversed", getObjects, () => Is.EqualTo(objects.Reverse())); + AddAssert("new combo positions preserved", getObjectNewCombos, () => Is.EqualTo(newCombos)); AddAssert("Slider head is at slider tail", () => Vector2.Distance(EditorBeatmap.HitObjects.OfType().ElementAt(0).Position, sliderTailOldPosition) < 1); @@ -186,8 +168,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor [Test] public void TestReverseSelectionTwoCirclesAndSlider() { - Vector2 circle1OldPosition = default; - Vector2 circle2OldPosition = default; + OsuHitObject[] objects = null!; + bool[] newCombos = null!; Vector2 sliderHeadOldPosition = default; Vector2 sliderTailOldPosition = default; @@ -197,12 +179,12 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor var circle1 = new HitCircle { StartTime = 0, - Position = circle1OldPosition = new Vector2(208, 240) + Position = new Vector2(208, 240) }; var circle2 = new HitCircle { StartTime = 200, - Position = circle2OldPosition = new Vector2(256, 144) + Position = new Vector2(256, 144) }; var slider = new Slider { @@ -223,11 +205,14 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor EditorBeatmap.AddRange([circle1, circle2, slider]); }); - AddStep("Select objects", () => + AddStep("store objects & new combo data", () => { - EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects); + objects = getObjects().ToArray(); + newCombos = getObjectNewCombos().ToArray(); }); + AddStep("Select objects", () => EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects)); + AddStep("Reverse selection", () => { InputManager.PressKey(Key.LControl); @@ -235,15 +220,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor InputManager.ReleaseKey(Key.LControl); }); - AddAssert("circle1 is at circle2 position", - () => EditorBeatmap.HitObjects.OfType().ElementAt(0).Position, - () => Is.EqualTo(circle2OldPosition) - ); - - AddAssert("circle2 is at circle1 position", - () => EditorBeatmap.HitObjects.OfType().ElementAt(1).Position, - () => Is.EqualTo(circle1OldPosition) - ); + AddAssert("objects reversed", getObjects, () => Is.EqualTo(objects.Reverse())); + AddAssert("new combo positions preserved", getObjectNewCombos, () => Is.EqualTo(newCombos)); AddAssert("Slider head is at slider tail", () => Vector2.Distance(EditorBeatmap.HitObjects.OfType().ElementAt(0).Position, sliderTailOldPosition) < 1); @@ -255,57 +233,55 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor [Test] public void TestReverseSelectionTwoCombos() { - Vector2 circle1OldPosition = default; - Vector2 circle2OldPosition = default; - Vector2 circle3OldPosition = default; - - Vector2 circle4OldPosition = default; - Vector2 circle5OldPosition = default; - Vector2 circle6OldPosition = default; + OsuHitObject[] objects = null!; + bool[] newCombos = null!; AddStep("Add circles", () => { var circle1 = new HitCircle { StartTime = 0, - Position = circle1OldPosition = new Vector2(216, 240) + Position = new Vector2(216, 240) }; var circle2 = new HitCircle { StartTime = 200, - Position = circle2OldPosition = new Vector2(120, 192) + Position = new Vector2(120, 192) }; var circle3 = new HitCircle { StartTime = 400, - Position = circle3OldPosition = new Vector2(216, 144) + Position = new Vector2(216, 144) }; var circle4 = new HitCircle { StartTime = 646, NewCombo = true, - Position = circle4OldPosition = new Vector2(296, 240) + Position = new Vector2(296, 240) }; var circle5 = new HitCircle { StartTime = 846, - Position = circle5OldPosition = new Vector2(392, 162) + Position = new Vector2(392, 162) }; var circle6 = new HitCircle { StartTime = 1046, - Position = circle6OldPosition = new Vector2(296, 144) + Position = new Vector2(296, 144) }; EditorBeatmap.AddRange([circle1, circle2, circle3, circle4, circle5, circle6]); }); - AddStep("Select circles", () => + AddStep("store objects & new combo data", () => { - EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects); + objects = getObjects().ToArray(); + newCombos = getObjectNewCombos().ToArray(); }); + AddStep("Select circles", () => EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects)); + AddStep("Reverse selection", () => { InputManager.PressKey(Key.LControl); @@ -313,35 +289,12 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor InputManager.ReleaseKey(Key.LControl); }); - AddAssert("circle1 is at circle6 position", - () => EditorBeatmap.HitObjects.OfType().ElementAt(0).Position, - () => Is.EqualTo(circle6OldPosition) - ); - - AddAssert("circle2 is at circle5 position", - () => EditorBeatmap.HitObjects.OfType().ElementAt(1).Position, - () => Is.EqualTo(circle5OldPosition) - ); - - AddAssert("circle3 is at circle4 position", - () => EditorBeatmap.HitObjects.OfType().ElementAt(2).Position, - () => Is.EqualTo(circle4OldPosition) - ); - - AddAssert("circle4 is at circle3 position", - () => EditorBeatmap.HitObjects.OfType().ElementAt(3).Position, - () => Is.EqualTo(circle3OldPosition) - ); - - AddAssert("circle5 is at circle2 position", - () => EditorBeatmap.HitObjects.OfType().ElementAt(4).Position, - () => Is.EqualTo(circle2OldPosition) - ); - - AddAssert("circle6 is at circle1 position", - () => EditorBeatmap.HitObjects.OfType().ElementAt(5).Position, - () => Is.EqualTo(circle1OldPosition) - ); + AddAssert("objects reversed", getObjects, () => Is.EqualTo(objects.Reverse())); + AddAssert("new combo positions preserved", getObjectNewCombos, () => Is.EqualTo(newCombos)); } + + private IEnumerable getObjects() => EditorBeatmap.HitObjects.OfType(); + + private IEnumerable getObjectNewCombos() => getObjects().Select(ho => ho.NewCombo); } } From 2f786ffc32d925d15d3417f696ac3a046dc2af80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 28 Mar 2024 10:12:27 +0100 Subject: [PATCH 0452/2493] Simplify implementation --- .../Edit/CatchSelectionHandler.cs | 21 +++++++++---------- .../Edit/OsuSelectionHandler.cs | 21 +++++++++---------- 2 files changed, 20 insertions(+), 22 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Edit/CatchSelectionHandler.cs b/osu.Game.Rulesets.Catch/Edit/CatchSelectionHandler.cs index f8fe9805e6..a2784126eb 100644 --- a/osu.Game.Rulesets.Catch/Edit/CatchSelectionHandler.cs +++ b/osu.Game.Rulesets.Catch/Edit/CatchSelectionHandler.cs @@ -77,13 +77,16 @@ namespace osu.Game.Rulesets.Catch.Edit public override bool HandleReverse() { var hitObjects = EditorBeatmap.SelectedHitObjects - .OfType() - .OrderBy(obj => obj.StartTime) - .ToList(); + .OfType() + .OrderBy(obj => obj.StartTime) + .ToList(); double selectionStartTime = SelectedItems.Min(h => h.StartTime); double selectionEndTime = SelectedItems.Max(h => h.GetEndTime()); + // the expectation is that even if the objects themselves are reversed temporally, + // the position of new combos in the selection should remain the same. + // preserve it for later before doing the reversal. var newComboOrder = hitObjects.Select(obj => obj.NewCombo).ToList(); foreach (var h in hitObjects) @@ -99,15 +102,11 @@ namespace osu.Game.Rulesets.Catch.Edit } } - // re-order objects again after flipping their times - hitObjects = [.. hitObjects.OrderBy(obj => obj.StartTime)]; + // re-order objects by start time again after reversing, and restore new combo flag positioning + hitObjects = hitObjects.OrderBy(obj => obj.StartTime).ToList(); - int i = 0; - foreach (bool newCombo in newComboOrder) - { - hitObjects[i].NewCombo = newCombo; - i++; - } + for (int i = 0; i < hitObjects.Count; ++i) + hitObjects[i].NewCombo = newComboOrder[i]; return true; } diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs index 0e889cab81..b33272968b 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs @@ -79,15 +79,18 @@ namespace osu.Game.Rulesets.Osu.Edit public override bool HandleReverse() { var hitObjects = EditorBeatmap.SelectedHitObjects - .OfType() - .OrderBy(obj => obj.StartTime) - .ToList(); + .OfType() + .OrderBy(obj => obj.StartTime) + .ToList(); double endTime = hitObjects.Max(h => h.GetEndTime()); double startTime = hitObjects.Min(h => h.StartTime); bool moreThanOneObject = hitObjects.Count > 1; + // the expectation is that even if the objects themselves are reversed temporally, + // the position of new combos in the selection should remain the same. + // preserve it for later before doing the reversal. var newComboOrder = hitObjects.Select(obj => obj.NewCombo).ToList(); foreach (var h in hitObjects) @@ -102,15 +105,11 @@ namespace osu.Game.Rulesets.Osu.Edit } } - // re-order objects again after flipping their times - hitObjects = [.. hitObjects.OrderBy(obj => obj.StartTime)]; + // re-order objects by start time again after reversing, and restore new combo flag positioning + hitObjects = hitObjects.OrderBy(obj => obj.StartTime).ToList(); - int i = 0; - foreach (bool newCombo in newComboOrder) - { - hitObjects[i].NewCombo = newCombo; - i++; - } + for (int i = 0; i < hitObjects.Count; ++i) + hitObjects[i].NewCombo = newComboOrder[i]; return true; } From 5febd40bd9910f73e65d4bf37b08069833828e7d Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 28 Mar 2024 22:30:39 +0900 Subject: [PATCH 0453/2493] Add HP and AR to LegacyBeatmapConversionDifficultyInfo --- .../LegacyBeatmapConversionDifficultyInfo.cs | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/Scoring/Legacy/LegacyBeatmapConversionDifficultyInfo.cs b/osu.Game/Rulesets/Scoring/Legacy/LegacyBeatmapConversionDifficultyInfo.cs index 7d69069455..f8b8567305 100644 --- a/osu.Game/Rulesets/Scoring/Legacy/LegacyBeatmapConversionDifficultyInfo.cs +++ b/osu.Game/Rulesets/Scoring/Legacy/LegacyBeatmapConversionDifficultyInfo.cs @@ -18,6 +18,16 @@ namespace osu.Game.Rulesets.Scoring.Legacy /// public IRulesetInfo SourceRuleset { get; set; } = new RulesetInfo(); + /// + /// The beatmap drain rate. + /// + public float DrainRate { get; set; } + + /// + /// The beatmap approach rate. + /// + public float ApproachRate { get; set; } + /// /// The beatmap circle size. /// @@ -41,8 +51,6 @@ namespace osu.Game.Rulesets.Scoring.Legacy /// public int TotalObjectCount { get; set; } - float IBeatmapDifficultyInfo.DrainRate => 0; - float IBeatmapDifficultyInfo.ApproachRate => 0; double IBeatmapDifficultyInfo.SliderMultiplier => 0; double IBeatmapDifficultyInfo.SliderTickRate => 0; @@ -51,6 +59,8 @@ namespace osu.Game.Rulesets.Scoring.Legacy public static LegacyBeatmapConversionDifficultyInfo FromBeatmap(IBeatmap beatmap) => new LegacyBeatmapConversionDifficultyInfo { SourceRuleset = beatmap.BeatmapInfo.Ruleset, + DrainRate = beatmap.Difficulty.DrainRate, + ApproachRate = beatmap.Difficulty.ApproachRate, CircleSize = beatmap.Difficulty.CircleSize, OverallDifficulty = beatmap.Difficulty.OverallDifficulty, EndTimeObjectCount = beatmap.HitObjects.Count(h => h is IHasDuration), @@ -60,6 +70,8 @@ namespace osu.Game.Rulesets.Scoring.Legacy public static LegacyBeatmapConversionDifficultyInfo FromBeatmapInfo(IBeatmapInfo beatmapInfo) => new LegacyBeatmapConversionDifficultyInfo { SourceRuleset = beatmapInfo.Ruleset, + DrainRate = beatmapInfo.Difficulty.DrainRate, + ApproachRate = beatmapInfo.Difficulty.ApproachRate, CircleSize = beatmapInfo.Difficulty.CircleSize, OverallDifficulty = beatmapInfo.Difficulty.OverallDifficulty, EndTimeObjectCount = beatmapInfo.EndTimeObjectCount, From 64399e9dd9841b7062e5f218c688424440c00b65 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 28 Mar 2024 22:32:27 +0900 Subject: [PATCH 0454/2493] Refactor pattern generation to not require ManiaBeatmap --- .../Beatmaps/ManiaBeatmapConverter.cs | 30 ++++++++++++++----- .../Legacy/EndTimeObjectPatternGenerator.cs | 4 +-- .../Legacy/HitObjectPatternGenerator.cs | 6 ++-- .../Legacy/PathObjectPatternGenerator.cs | 4 +-- .../Patterns/Legacy/PatternGenerator.cs | 22 +++++--------- .../Beatmaps/Patterns/PatternGenerator.cs | 8 ++--- 6 files changed, 41 insertions(+), 33 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs index def22608d6..cc975c7def 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs @@ -27,8 +27,24 @@ namespace osu.Game.Rulesets.Mania.Beatmaps /// private const int max_notes_for_density = 7; + /// + /// The total number of columns. + /// + public int TotalColumns => TargetColumns * (Dual ? 2 : 1); + + /// + /// The number of columns per-stage. + /// public int TargetColumns; + + /// + /// Whether to double the number of stages. + /// public bool Dual; + + /// + /// Whether the beatmap instantiated with is for the mania ruleset. + /// public readonly bool IsForCurrentRuleset; private readonly int originalTargetColumns; @@ -152,7 +168,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps /// The hit objects generated. private IEnumerable generateSpecific(HitObject original, IBeatmap originalBeatmap) { - var generator = new SpecificBeatmapPatternGenerator(Random, original, beatmap, lastPattern, originalBeatmap); + var generator = new SpecificBeatmapPatternGenerator(Random, original, originalBeatmap, TotalColumns, lastPattern); foreach (var newPattern in generator.Generate()) { @@ -171,13 +187,13 @@ namespace osu.Game.Rulesets.Mania.Beatmaps /// The hit objects generated. private IEnumerable generateConverted(HitObject original, IBeatmap originalBeatmap) { - Patterns.PatternGenerator conversion = null; + Patterns.PatternGenerator? conversion = null; switch (original) { case IHasPath: { - var generator = new PathObjectPatternGenerator(Random, original, beatmap, lastPattern, originalBeatmap); + var generator = new PathObjectPatternGenerator(Random, original, originalBeatmap, TotalColumns, lastPattern); conversion = generator; var positionData = original as IHasPosition; @@ -195,7 +211,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps case IHasDuration endTimeData: { - conversion = new EndTimeObjectPatternGenerator(Random, original, beatmap, lastPattern, originalBeatmap); + conversion = new EndTimeObjectPatternGenerator(Random, original, originalBeatmap, TotalColumns, lastPattern); recordNote(endTimeData.EndTime, new Vector2(256, 192)); computeDensity(endTimeData.EndTime); @@ -206,7 +222,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps { computeDensity(original.StartTime); - conversion = new HitObjectPatternGenerator(Random, original, beatmap, lastPattern, lastTime, lastPosition, density, lastStair, originalBeatmap); + conversion = new HitObjectPatternGenerator(Random, original, originalBeatmap, TotalColumns, lastPattern, lastTime, lastPosition, density, lastStair); recordNote(original.StartTime, positionData.Position); break; @@ -231,8 +247,8 @@ namespace osu.Game.Rulesets.Mania.Beatmaps /// private class SpecificBeatmapPatternGenerator : Patterns.Legacy.PatternGenerator { - public SpecificBeatmapPatternGenerator(LegacyRandom random, HitObject hitObject, ManiaBeatmap beatmap, Pattern previousPattern, IBeatmap originalBeatmap) - : base(random, hitObject, beatmap, previousPattern, originalBeatmap) + public SpecificBeatmapPatternGenerator(LegacyRandom random, HitObject hitObject, IBeatmap beatmap, int totalColumns, Pattern previousPattern) + : base(random, hitObject, beatmap, previousPattern, totalColumns) { } diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/EndTimeObjectPatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/EndTimeObjectPatternGenerator.cs index 2265d3d347..52bb87ae19 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/EndTimeObjectPatternGenerator.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/EndTimeObjectPatternGenerator.cs @@ -17,8 +17,8 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy private readonly int endTime; private readonly PatternType convertType; - public EndTimeObjectPatternGenerator(LegacyRandom random, HitObject hitObject, ManiaBeatmap beatmap, Pattern previousPattern, IBeatmap originalBeatmap) - : base(random, hitObject, beatmap, previousPattern, originalBeatmap) + public EndTimeObjectPatternGenerator(LegacyRandom random, HitObject hitObject, IBeatmap beatmap, int totalColumns, Pattern previousPattern) + : base(random, hitObject, beatmap, previousPattern, totalColumns) { endTime = (int)((HitObject as IHasDuration)?.EndTime ?? 0); diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/HitObjectPatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/HitObjectPatternGenerator.cs index 27cb681300..ad45a3fb21 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/HitObjectPatternGenerator.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/HitObjectPatternGenerator.cs @@ -23,9 +23,9 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy private readonly PatternType convertType; - public HitObjectPatternGenerator(LegacyRandom random, HitObject hitObject, ManiaBeatmap beatmap, Pattern previousPattern, double previousTime, Vector2 previousPosition, double density, - PatternType lastStair, IBeatmap originalBeatmap) - : base(random, hitObject, beatmap, previousPattern, originalBeatmap) + public HitObjectPatternGenerator(LegacyRandom random, HitObject hitObject, IBeatmap beatmap, int totalColumns, Pattern previousPattern, double previousTime, Vector2 previousPosition, + double density, PatternType lastStair) + : base(random, hitObject, beatmap, previousPattern, totalColumns) { StairType = lastStair; diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PathObjectPatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PathObjectPatternGenerator.cs index 4922915c7d..6d593a75e7 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PathObjectPatternGenerator.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PathObjectPatternGenerator.cs @@ -31,8 +31,8 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy private PatternType convertType; - public PathObjectPatternGenerator(LegacyRandom random, HitObject hitObject, ManiaBeatmap beatmap, Pattern previousPattern, IBeatmap originalBeatmap) - : base(random, hitObject, beatmap, previousPattern, originalBeatmap) + public PathObjectPatternGenerator(LegacyRandom random, HitObject hitObject, IBeatmap beatmap, int totalColumns, Pattern previousPattern) + : base(random, hitObject, beatmap, previousPattern, totalColumns) { convertType = PatternType.None; if (!Beatmap.ControlPointInfo.EffectPointAt(hitObject.StartTime).KiaiMode) diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PatternGenerator.cs index 77f93b4ef9..48b8778501 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PatternGenerator.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PatternGenerator.cs @@ -27,20 +27,12 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy /// protected readonly LegacyRandom Random; - /// - /// The beatmap which is being converted from. - /// - protected readonly IBeatmap OriginalBeatmap; - - protected PatternGenerator(LegacyRandom random, HitObject hitObject, ManiaBeatmap beatmap, Pattern previousPattern, IBeatmap originalBeatmap) - : base(hitObject, beatmap, previousPattern) + protected PatternGenerator(LegacyRandom random, HitObject hitObject, IBeatmap beatmap, Pattern previousPattern, int totalColumns) + : base(hitObject, beatmap, totalColumns, previousPattern) { ArgumentNullException.ThrowIfNull(random); - ArgumentNullException.ThrowIfNull(originalBeatmap); Random = random; - OriginalBeatmap = originalBeatmap; - RandomStart = TotalColumns == 8 ? 1 : 0; } @@ -104,17 +96,17 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy if (conversionDifficulty != null) return conversionDifficulty.Value; - HitObject lastObject = OriginalBeatmap.HitObjects.LastOrDefault(); - HitObject firstObject = OriginalBeatmap.HitObjects.FirstOrDefault(); + HitObject lastObject = Beatmap.HitObjects.LastOrDefault(); + HitObject firstObject = Beatmap.HitObjects.FirstOrDefault(); // Drain time in seconds - int drainTime = (int)(((lastObject?.StartTime ?? 0) - (firstObject?.StartTime ?? 0) - OriginalBeatmap.TotalBreakTime) / 1000); + int drainTime = (int)(((lastObject?.StartTime ?? 0) - (firstObject?.StartTime ?? 0) - Beatmap.TotalBreakTime) / 1000); if (drainTime == 0) drainTime = 10000; - IBeatmapDifficultyInfo difficulty = OriginalBeatmap.Difficulty; - conversionDifficulty = ((difficulty.DrainRate + Math.Clamp(difficulty.ApproachRate, 4, 7)) / 1.5 + (double)OriginalBeatmap.HitObjects.Count / drainTime * 9f) / 38f * 5f / 1.15; + IBeatmapDifficultyInfo difficulty = Beatmap.Difficulty; + conversionDifficulty = ((difficulty.DrainRate + Math.Clamp(difficulty.ApproachRate, 4, 7)) / 1.5 + (double)Beatmap.HitObjects.Count / drainTime * 9f) / 38f * 5f / 1.15; conversionDifficulty = Math.Min(conversionDifficulty.Value, 12); return conversionDifficulty.Value; diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/PatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/PatternGenerator.cs index 3d3c35773b..8d98515fa4 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/PatternGenerator.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/PatternGenerator.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using osu.Game.Beatmaps; using osu.Game.Rulesets.Objects; namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns @@ -25,11 +26,11 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns /// /// The beatmap which is a part of. /// - protected readonly ManiaBeatmap Beatmap; + protected readonly IBeatmap Beatmap; protected readonly int TotalColumns; - protected PatternGenerator(HitObject hitObject, ManiaBeatmap beatmap, Pattern previousPattern) + protected PatternGenerator(HitObject hitObject, IBeatmap beatmap, int totalColumns, Pattern previousPattern) { ArgumentNullException.ThrowIfNull(hitObject); ArgumentNullException.ThrowIfNull(beatmap); @@ -38,8 +39,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns HitObject = hitObject; Beatmap = beatmap; PreviousPattern = previousPattern; - - TotalColumns = Beatmap.TotalColumns; + TotalColumns = totalColumns; } /// From 10edb5461490568a06f43c443dc6cc7f19b14c8a Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 28 Mar 2024 22:39:15 +0900 Subject: [PATCH 0455/2493] Add ability to query key count with mods --- .../Beatmaps/ManiaBeatmapConverter.cs | 93 ++++++++++--------- osu.Game.Rulesets.Mania/ManiaRuleset.cs | 4 +- osu.Game/Rulesets/ILegacyRuleset.cs | 5 +- .../Carousel/DrawableCarouselBeatmap.cs | 7 +- .../Screens/Select/Details/AdvancedStats.cs | 2 +- 5 files changed, 61 insertions(+), 50 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs index cc975c7def..8b339239a0 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.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.Game.Rulesets.Mania.Objects; using System; using System.Linq; @@ -14,6 +12,7 @@ using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Mania.Beatmaps.Patterns; using osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy; +using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Scoring.Legacy; using osu.Game.Utils; using osuTK; @@ -50,17 +49,21 @@ namespace osu.Game.Rulesets.Mania.Beatmaps private readonly int originalTargetColumns; // Internal for testing purposes - internal LegacyRandom Random { get; private set; } + internal readonly LegacyRandom Random; private Pattern lastPattern = new Pattern(); - private ManiaBeatmap beatmap; - public ManiaBeatmapConverter(IBeatmap beatmap, Ruleset ruleset) - : base(beatmap, ruleset) + : this(beatmap, LegacyBeatmapConversionDifficultyInfo.FromBeatmap(beatmap), ruleset) { - IsForCurrentRuleset = beatmap.BeatmapInfo.Ruleset.Equals(ruleset.RulesetInfo); - TargetColumns = GetColumnCount(LegacyBeatmapConversionDifficultyInfo.FromBeatmap(beatmap)); + } + + private ManiaBeatmapConverter(IBeatmap? beatmap, LegacyBeatmapConversionDifficultyInfo difficulty, Ruleset ruleset) + : base(beatmap!, ruleset) + { + IsForCurrentRuleset = difficulty.SourceRuleset.Equals(ruleset.RulesetInfo); + Random = new LegacyRandom((int)MathF.Round(difficulty.DrainRate + difficulty.CircleSize) * 20 + (int)(difficulty.OverallDifficulty * 41.2) + (int)MathF.Round(difficulty.ApproachRate)); + TargetColumns = getColumnCount(difficulty); if (IsForCurrentRuleset && TargetColumns > ManiaRuleset.MAX_STAGE_KEYS) { @@ -69,51 +72,57 @@ namespace osu.Game.Rulesets.Mania.Beatmaps } originalTargetColumns = TargetColumns; + + static int getColumnCount(LegacyBeatmapConversionDifficultyInfo difficulty) + { + double roundedCircleSize = Math.Round(difficulty.CircleSize); + + if (difficulty.SourceRuleset.ShortName == ManiaRuleset.SHORT_NAME) + return (int)Math.Max(1, roundedCircleSize); + + double roundedOverallDifficulty = Math.Round(difficulty.OverallDifficulty); + + if (difficulty.TotalObjectCount > 0 && difficulty.EndTimeObjectCount >= 0) + { + int countSliderOrSpinner = difficulty.EndTimeObjectCount; + + // In osu!stable, this division appears as if it happens on floats, but due to release-mode + // optimisations, it actually ends up happening on doubles. + double percentSpecialObjects = (double)countSliderOrSpinner / difficulty.TotalObjectCount; + + if (percentSpecialObjects < 0.2) + return 7; + if (percentSpecialObjects < 0.3 || roundedCircleSize >= 5) + return roundedOverallDifficulty > 5 ? 7 : 6; + if (percentSpecialObjects > 0.6) + return roundedOverallDifficulty > 4 ? 5 : 4; + } + + return Math.Max(4, Math.Min((int)roundedOverallDifficulty + 1, 7)); + } } - public static int GetColumnCount(LegacyBeatmapConversionDifficultyInfo difficulty) + public static int GetColumnCount(IBeatmapInfo beatmapInfo, IReadOnlyList? mods = null) + => GetColumnCount(LegacyBeatmapConversionDifficultyInfo.FromBeatmapInfo(beatmapInfo), mods); + + public static int GetColumnCount(LegacyBeatmapConversionDifficultyInfo difficulty, IReadOnlyList? mods = null) { - double roundedCircleSize = Math.Round(difficulty.CircleSize); + var converter = new ManiaBeatmapConverter(null, difficulty, new ManiaRuleset()); - if (difficulty.SourceRuleset.ShortName == ManiaRuleset.SHORT_NAME) - return (int)Math.Max(1, roundedCircleSize); - - double roundedOverallDifficulty = Math.Round(difficulty.OverallDifficulty); - - if (difficulty.TotalObjectCount > 0 && difficulty.EndTimeObjectCount >= 0) + if (mods != null) { - int countSliderOrSpinner = difficulty.EndTimeObjectCount; - - // In osu!stable, this division appears as if it happens on floats, but due to release-mode - // optimisations, it actually ends up happening on doubles. - double percentSpecialObjects = (double)countSliderOrSpinner / difficulty.TotalObjectCount; - - if (percentSpecialObjects < 0.2) - return 7; - if (percentSpecialObjects < 0.3 || roundedCircleSize >= 5) - return roundedOverallDifficulty > 5 ? 7 : 6; - if (percentSpecialObjects > 0.6) - return roundedOverallDifficulty > 4 ? 5 : 4; + foreach (var m in mods.OfType()) + m.ApplyToBeatmapConverter(converter); } - return Math.Max(4, Math.Min((int)roundedOverallDifficulty + 1, 7)); + return converter.TotalColumns; } public override bool CanConvert() => Beatmap.HitObjects.All(h => h is IHasXPosition); - protected override Beatmap ConvertBeatmap(IBeatmap original, CancellationToken cancellationToken) - { - IBeatmapDifficultyInfo difficulty = original.Difficulty; - - int seed = (int)MathF.Round(difficulty.DrainRate + difficulty.CircleSize) * 20 + (int)(difficulty.OverallDifficulty * 41.2) + (int)MathF.Round(difficulty.ApproachRate); - Random = new LegacyRandom(seed); - - return base.ConvertBeatmap(original, cancellationToken); - } - protected override Beatmap CreateBeatmap() { - beatmap = new ManiaBeatmap(new StageDefinition(TargetColumns), originalTargetColumns); + ManiaBeatmap beatmap = new ManiaBeatmap(new StageDefinition(TargetColumns), originalTargetColumns); if (Dual) beatmap.Stages.Add(new StageDefinition(TargetColumns)); @@ -131,10 +140,6 @@ namespace osu.Game.Rulesets.Mania.Beatmaps } var objects = IsForCurrentRuleset ? generateSpecific(original, beatmap) : generateConverted(original, beatmap); - - if (objects == null) - yield break; - foreach (ManiaHitObject obj in objects) yield return obj; } diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs index 3d4803f1e4..77168dca68 100644 --- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs @@ -423,8 +423,8 @@ namespace osu.Game.Rulesets.Mania public override DifficultySection CreateEditorDifficultySection() => new ManiaDifficultySection(); - public int GetKeyCount(IBeatmapInfo beatmapInfo) - => ManiaBeatmapConverter.GetColumnCount(LegacyBeatmapConversionDifficultyInfo.FromBeatmapInfo(beatmapInfo)); + public int GetKeyCount(IBeatmapInfo beatmapInfo, IReadOnlyList? mods = null) + => ManiaBeatmapConverter.GetColumnCount(beatmapInfo, mods); } public enum PlayfieldType diff --git a/osu.Game/Rulesets/ILegacyRuleset.cs b/osu.Game/Rulesets/ILegacyRuleset.cs index 18d86f477a..e116f7a1a3 100644 --- a/osu.Game/Rulesets/ILegacyRuleset.cs +++ b/osu.Game/Rulesets/ILegacyRuleset.cs @@ -1,7 +1,9 @@ // 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; using osu.Game.Rulesets.Scoring.Legacy; namespace osu.Game.Rulesets @@ -18,8 +20,7 @@ namespace osu.Game.Rulesets /// /// Retrieves the number of mania keys required to play the beatmap. /// - /// - int GetKeyCount(IBeatmapInfo beatmapInfo) => 0; + int GetKeyCount(IBeatmapInfo beatmapInfo, IReadOnlyList? mods = null) => 0; ILegacyScoreSimulator CreateLegacyScoreSimulator(); } diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs index 01e58d4ab2..2752beb645 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs @@ -28,6 +28,7 @@ using osu.Game.Graphics.UserInterface; using osu.Game.Overlays; using osu.Game.Resources.Localisation.Web; using osu.Game.Rulesets; +using osu.Game.Rulesets.Mods; using osuTK; using osuTK.Graphics; @@ -75,6 +76,9 @@ namespace osu.Game.Screens.Select.Carousel [Resolved] private IBindable ruleset { get; set; } = null!; + [Resolved] + private IBindable>? mods { get; set; } = null!; + private IBindable starDifficultyBindable = null!; private CancellationTokenSource? starDifficultyCancellationSource; @@ -185,6 +189,7 @@ namespace osu.Game.Screens.Select.Carousel base.LoadComplete(); ruleset.BindValueChanged(_ => updateKeyCount()); + mods?.BindValueChanged(_ => updateKeyCount()); } protected override void Selected() @@ -255,7 +260,7 @@ namespace osu.Game.Screens.Select.Carousel ILegacyRuleset legacyRuleset = (ILegacyRuleset)ruleset.Value.CreateInstance(); keyCountText.Alpha = 1; - keyCountText.Text = $"[{legacyRuleset.GetKeyCount(beatmapInfo)}K]"; + keyCountText.Text = $"[{legacyRuleset.GetKeyCount(beatmapInfo, mods?.Value)}K]"; } else keyCountText.Alpha = 0; diff --git a/osu.Game/Screens/Select/Details/AdvancedStats.cs b/osu.Game/Screens/Select/Details/AdvancedStats.cs index 1aba977f44..cb820f4da9 100644 --- a/osu.Game/Screens/Select/Details/AdvancedStats.cs +++ b/osu.Game/Screens/Select/Details/AdvancedStats.cs @@ -199,7 +199,7 @@ namespace osu.Game.Screens.Select.Details // For the time being, the key count is static no matter what, because: // a) The method doesn't have knowledge of the active keymods. Doing so may require considerations for filtering. // b) Using the difficulty adjustment mod to adjust OD doesn't have an effect on conversion. - int keyCount = baseDifficulty == null ? 0 : legacyRuleset.GetKeyCount(BeatmapInfo); + int keyCount = baseDifficulty == null ? 0 : legacyRuleset.GetKeyCount(BeatmapInfo, mods.Value); FirstValue.Title = BeatmapsetsStrings.ShowStatsCsMania; FirstValue.Value = (keyCount, keyCount); From ce21235db495bb10e0f49763b7351f4f70fa51b3 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 28 Mar 2024 22:47:43 +0900 Subject: [PATCH 0456/2493] Remove unused OriginalTargetColumns --- osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmap.cs | 6 ------ osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs | 6 +----- 2 files changed, 1 insertion(+), 11 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmap.cs b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmap.cs index 28cdf8907e..8222e5477d 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmap.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmap.cs @@ -22,11 +22,6 @@ namespace osu.Game.Rulesets.Mania.Beatmaps /// public int TotalColumns => Stages.Sum(g => g.Columns); - /// - /// The total number of columns that were present in this before any user adjustments. - /// - public readonly int OriginalTotalColumns; - /// /// Creates a new . /// @@ -35,7 +30,6 @@ namespace osu.Game.Rulesets.Mania.Beatmaps public ManiaBeatmap(StageDefinition defaultStage, int? originalTotalColumns = null) { Stages.Add(defaultStage); - OriginalTotalColumns = originalTotalColumns ?? defaultStage.Columns; } public override IEnumerable GetStatistics() diff --git a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs index 8b339239a0..bed04a882f 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs @@ -46,8 +46,6 @@ namespace osu.Game.Rulesets.Mania.Beatmaps /// public readonly bool IsForCurrentRuleset; - private readonly int originalTargetColumns; - // Internal for testing purposes internal readonly LegacyRandom Random; @@ -71,8 +69,6 @@ namespace osu.Game.Rulesets.Mania.Beatmaps Dual = true; } - originalTargetColumns = TargetColumns; - static int getColumnCount(LegacyBeatmapConversionDifficultyInfo difficulty) { double roundedCircleSize = Math.Round(difficulty.CircleSize); @@ -122,7 +118,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps protected override Beatmap CreateBeatmap() { - ManiaBeatmap beatmap = new ManiaBeatmap(new StageDefinition(TargetColumns), originalTargetColumns); + ManiaBeatmap beatmap = new ManiaBeatmap(new StageDefinition(TargetColumns)); if (Dual) beatmap.Stages.Add(new StageDefinition(TargetColumns)); From c08a4898b27347201d63200d162718ddaf874068 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 28 Mar 2024 22:58:39 +0900 Subject: [PATCH 0457/2493] Refactor score simulator to use GetColumnCount() --- .../Difficulty/ManiaLegacyScoreSimulator.cs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaLegacyScoreSimulator.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaLegacyScoreSimulator.cs index d9fd96ac6a..8a1b127265 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/ManiaLegacyScoreSimulator.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaLegacyScoreSimulator.cs @@ -51,13 +51,8 @@ namespace osu.Game.Rulesets.Mania.Difficulty return multiplier; // Apply key mod multipliers. - int originalColumns = ManiaBeatmapConverter.GetColumnCount(difficulty); - int actualColumns = originalColumns; - - actualColumns = mods.OfType().SingleOrDefault()?.KeyCount ?? actualColumns; - if (mods.Any(m => m is ManiaModDualStages)) - actualColumns *= 2; + int actualColumns = ManiaBeatmapConverter.GetColumnCount(difficulty, mods); if (actualColumns > originalColumns) multiplier *= 0.9; From 9fd6449fd8b7c8b7a9019d1d3a25cb46a5b5562c Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 28 Mar 2024 23:02:25 +0900 Subject: [PATCH 0458/2493] Add mods to FilterCriteria, pass to ruleset method --- osu.Game.Rulesets.Mania/ManiaFilterCriteria.cs | 5 ++--- .../NonVisual/Filtering/FilterMatchingTest.cs | 2 +- .../NonVisual/Filtering/FilterQueryParserTest.cs | 2 +- osu.Game/Rulesets/Filter/IRulesetFilterCriteria.cs | 3 ++- osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs | 2 +- .../Select/Carousel/DrawableCarouselBeatmap.cs | 6 +++--- osu.Game/Screens/Select/FilterControl.cs | 13 ++++++++++--- osu.Game/Screens/Select/FilterCriteria.cs | 2 ++ 8 files changed, 22 insertions(+), 13 deletions(-) diff --git a/osu.Game.Rulesets.Mania/ManiaFilterCriteria.cs b/osu.Game.Rulesets.Mania/ManiaFilterCriteria.cs index 930ca217cd..07ed3ebd63 100644 --- a/osu.Game.Rulesets.Mania/ManiaFilterCriteria.cs +++ b/osu.Game.Rulesets.Mania/ManiaFilterCriteria.cs @@ -4,7 +4,6 @@ using osu.Game.Beatmaps; using osu.Game.Rulesets.Filter; using osu.Game.Rulesets.Mania.Beatmaps; -using osu.Game.Rulesets.Scoring.Legacy; using osu.Game.Screens.Select; using osu.Game.Screens.Select.Filter; @@ -14,9 +13,9 @@ namespace osu.Game.Rulesets.Mania { private FilterCriteria.OptionalRange keys; - public bool Matches(BeatmapInfo beatmapInfo) + public bool Matches(BeatmapInfo beatmapInfo, FilterCriteria criteria) { - return !keys.HasFilter || keys.IsInRange(ManiaBeatmapConverter.GetColumnCount(LegacyBeatmapConversionDifficultyInfo.FromBeatmapInfo(beatmapInfo))); + return !keys.HasFilter || keys.IsInRange(ManiaBeatmapConverter.GetColumnCount(beatmapInfo, criteria.Mods)); } public bool TryParseCustomKeywordCriteria(string key, Operator op, string value) diff --git a/osu.Game.Tests/NonVisual/Filtering/FilterMatchingTest.cs b/osu.Game.Tests/NonVisual/Filtering/FilterMatchingTest.cs index c7a32ebbc4..78d8eabba7 100644 --- a/osu.Game.Tests/NonVisual/Filtering/FilterMatchingTest.cs +++ b/osu.Game.Tests/NonVisual/Filtering/FilterMatchingTest.cs @@ -309,7 +309,7 @@ namespace osu.Game.Tests.NonVisual.Filtering match = shouldMatch; } - public bool Matches(BeatmapInfo beatmapInfo) => match; + public bool Matches(BeatmapInfo beatmapInfo, FilterCriteria criteria) => match; public bool TryParseCustomKeywordCriteria(string key, Operator op, string value) => false; } } diff --git a/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs b/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs index ea14412f55..b0ceed45b9 100644 --- a/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs +++ b/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs @@ -502,7 +502,7 @@ namespace osu.Game.Tests.NonVisual.Filtering { public string? CustomValue { get; set; } - public bool Matches(BeatmapInfo beatmapInfo) => true; + public bool Matches(BeatmapInfo beatmapInfo, FilterCriteria criteria) => true; public bool TryParseCustomKeywordCriteria(string key, Operator op, string value) { diff --git a/osu.Game/Rulesets/Filter/IRulesetFilterCriteria.cs b/osu.Game/Rulesets/Filter/IRulesetFilterCriteria.cs index dd2ad2cbfa..f926b04db4 100644 --- a/osu.Game/Rulesets/Filter/IRulesetFilterCriteria.cs +++ b/osu.Game/Rulesets/Filter/IRulesetFilterCriteria.cs @@ -18,11 +18,12 @@ namespace osu.Game.Rulesets.Filter /// in addition to the ones mandated by song select. /// /// The beatmap to test the criteria against. + /// The filter criteria. /// /// true if the beatmap matches the ruleset-specific custom filtering criteria, /// false otherwise. /// - bool Matches(BeatmapInfo beatmapInfo); + bool Matches(BeatmapInfo beatmapInfo, FilterCriteria criteria); /// /// Attempts to parse a single custom keyword criterion, given by the user via the song select search box. diff --git a/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs index 43461a48bb..8f38ae710c 100644 --- a/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs @@ -85,7 +85,7 @@ namespace osu.Game.Screens.Select.Carousel match &= criteria.CollectionBeatmapMD5Hashes?.Contains(BeatmapInfo.MD5Hash) ?? true; if (match && criteria.RulesetCriteria != null) - match &= criteria.RulesetCriteria.Matches(BeatmapInfo); + match &= criteria.RulesetCriteria.Matches(BeatmapInfo, criteria); return match; } diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs index 2752beb645..f725d98342 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs @@ -77,7 +77,7 @@ namespace osu.Game.Screens.Select.Carousel private IBindable ruleset { get; set; } = null!; [Resolved] - private IBindable>? mods { get; set; } = null!; + private IBindable> mods { get; set; } = null!; private IBindable starDifficultyBindable = null!; private CancellationTokenSource? starDifficultyCancellationSource; @@ -189,7 +189,7 @@ namespace osu.Game.Screens.Select.Carousel base.LoadComplete(); ruleset.BindValueChanged(_ => updateKeyCount()); - mods?.BindValueChanged(_ => updateKeyCount()); + mods.BindValueChanged(_ => updateKeyCount()); } protected override void Selected() @@ -260,7 +260,7 @@ namespace osu.Game.Screens.Select.Carousel ILegacyRuleset legacyRuleset = (ILegacyRuleset)ruleset.Value.CreateInstance(); keyCountText.Alpha = 1; - keyCountText.Text = $"[{legacyRuleset.GetKeyCount(beatmapInfo, mods?.Value)}K]"; + keyCountText.Text = $"[{legacyRuleset.GetKeyCount(beatmapInfo, mods.Value)}K]"; } else keyCountText.Alpha = 0; diff --git a/osu.Game/Screens/Select/FilterControl.cs b/osu.Game/Screens/Select/FilterControl.cs index 17297c9ebf..b19a7699c5 100644 --- a/osu.Game/Screens/Select/FilterControl.cs +++ b/osu.Game/Screens/Select/FilterControl.cs @@ -4,6 +4,7 @@ #nullable disable using System; +using System.Collections.Generic; using System.Collections.Immutable; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -22,6 +23,7 @@ using osu.Game.Graphics.UserInterface; using osu.Game.Localisation; using osu.Game.Resources.Localisation.Web; using osu.Game.Rulesets; +using osu.Game.Rulesets.Mods; using osu.Game.Screens.Select.Filter; using osuTK; using osuTK.Graphics; @@ -65,6 +67,7 @@ namespace osu.Game.Screens.Select Sort = sortMode.Value, AllowConvertedBeatmaps = showConverted.Value, Ruleset = ruleset.Value, + Mods = mods.Value, CollectionBeatmapMD5Hashes = collectionDropdown.Current.Value?.Collection?.PerformRead(c => c.BeatmapMD5Hashes).ToImmutableHashSet() }; @@ -84,7 +87,7 @@ namespace osu.Game.Screens.Select base.ReceivePositionalInputAt(screenSpacePos) || sortTabs.ReceivePositionalInputAt(screenSpacePos); [BackgroundDependencyLoader(permitNulls: true)] - private void load(OsuColour colours, IBindable parentRuleset, OsuConfigManager config) + private void load(OsuColour colours, OsuConfigManager config) { sortMode = config.GetBindable(OsuSetting.SongSelectSortingMode); groupMode = config.GetBindable(OsuSetting.SongSelectGroupingMode); @@ -214,8 +217,8 @@ namespace osu.Game.Screens.Select config.BindWith(OsuSetting.DisplayStarsMaximum, maximumStars); maximumStars.ValueChanged += _ => updateCriteria(); - ruleset.BindTo(parentRuleset); ruleset.BindValueChanged(_ => updateCriteria()); + mods.BindValueChanged(_ => updateCriteria()); groupMode.BindValueChanged(_ => updateCriteria()); sortMode.BindValueChanged(_ => updateCriteria()); @@ -239,7 +242,11 @@ namespace osu.Game.Screens.Select searchTextBox.HoldFocus = true; } - private readonly IBindable ruleset = new Bindable(); + [Resolved] + private IBindable ruleset { get; set; } = null!; + + [Resolved] + private IBindable> mods { get; set; } = null!; private readonly Bindable showConverted = new Bindable(); private readonly Bindable minimumStars = new BindableDouble(); diff --git a/osu.Game/Screens/Select/FilterCriteria.cs b/osu.Game/Screens/Select/FilterCriteria.cs index 01b0e9b7d9..190efd0fb0 100644 --- a/osu.Game/Screens/Select/FilterCriteria.cs +++ b/osu.Game/Screens/Select/FilterCriteria.cs @@ -10,6 +10,7 @@ using osu.Game.Beatmaps; using osu.Game.Collections; using osu.Game.Rulesets; using osu.Game.Rulesets.Filter; +using osu.Game.Rulesets.Mods; using osu.Game.Screens.Select.Filter; namespace osu.Game.Screens.Select @@ -50,6 +51,7 @@ namespace osu.Game.Screens.Select public OptionalTextFilter[] SearchTerms = Array.Empty(); public RulesetInfo? Ruleset; + public IReadOnlyList? Mods; public bool AllowConvertedBeatmaps; private string searchText = string.Empty; From 6e746a0fa053eb5c5c0d1b8cde1d0a1e1ba2c737 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 28 Mar 2024 23:56:46 +0900 Subject: [PATCH 0459/2493] Fix carousel reoder on initial enter --- osu.Game/Screens/Select/FilterControl.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Select/FilterControl.cs b/osu.Game/Screens/Select/FilterControl.cs index b19a7699c5..7b8b5393bd 100644 --- a/osu.Game/Screens/Select/FilterControl.cs +++ b/osu.Game/Screens/Select/FilterControl.cs @@ -218,7 +218,13 @@ namespace osu.Game.Screens.Select maximumStars.ValueChanged += _ => updateCriteria(); ruleset.BindValueChanged(_ => updateCriteria()); - mods.BindValueChanged(_ => updateCriteria()); + mods.BindValueChanged(_ => + { + // Mods are updated once by the mod select overlay when song select is entered, regardless of if there are any mods. + // Updating the criteria here so early triggers a re-ordering of panels on song select, via... some mechanism. + // Todo: Investigate/fix the above and remove this schedule. + Scheduler.AddOnce(updateCriteria); + }); groupMode.BindValueChanged(_ => updateCriteria()); sortMode.BindValueChanged(_ => updateCriteria()); From cbbb46cad87d40b3416e23c60f8dc5bf391004c9 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 29 Mar 2024 00:25:03 +0900 Subject: [PATCH 0460/2493] Update action versions in diffcalc workflow --- .github/workflows/diffcalc.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/diffcalc.yml b/.github/workflows/diffcalc.yml index 2ed176fe8d..7fd0f798cd 100644 --- a/.github/workflows/diffcalc.yml +++ b/.github/workflows/diffcalc.yml @@ -126,7 +126,7 @@ jobs: if: ${{ github.event_name == 'issue_comment' && github.event.issue.pull_request }} steps: - name: Create comment - uses: thollander/actions-comment-pull-request@363c6f6eae92cc5c3a66e95ba016fc771bb38943 # v2.4.2 + uses: thollander/actions-comment-pull-request@fabd468d3a1a0b97feee5f6b9e499eab0dd903f6 # v2.5.0 with: comment_tag: ${{ env.EXECUTION_ID }} message: | @@ -253,7 +253,7 @@ jobs: - name: Restore cache id: restore-cache - uses: maxnowack/local-cache@038cc090b52e4f205fbc468bf5b0756df6f68775 # v1 + uses: maxnowack/local-cache@720e69c948191660a90aa1cf6a42fc4d2dacdf30 # v2 with: path: ${{ steps.query.outputs.DATA_NAME }}.tar.bz2 key: ${{ steps.query.outputs.DATA_NAME }} @@ -284,7 +284,7 @@ jobs: - name: Restore cache id: restore-cache - uses: maxnowack/local-cache@038cc090b52e4f205fbc468bf5b0756df6f68775 # v1 + uses: maxnowack/local-cache@720e69c948191660a90aa1cf6a42fc4d2dacdf30 # v2 with: path: ${{ steps.query.outputs.DATA_NAME }}.tar.bz2 key: ${{ steps.query.outputs.DATA_NAME }} @@ -358,7 +358,7 @@ jobs: steps: - name: Update comment on success if: ${{ needs.generator.result == 'success' }} - uses: thollander/actions-comment-pull-request@363c6f6eae92cc5c3a66e95ba016fc771bb38943 # v2.4.2 + uses: thollander/actions-comment-pull-request@fabd468d3a1a0b97feee5f6b9e499eab0dd903f6 # v2.5.0 with: comment_tag: ${{ env.EXECUTION_ID }} mode: upsert @@ -369,7 +369,7 @@ jobs: - name: Update comment on failure if: ${{ needs.generator.result == 'failure' }} - uses: thollander/actions-comment-pull-request@363c6f6eae92cc5c3a66e95ba016fc771bb38943 # v2.4.2 + uses: thollander/actions-comment-pull-request@fabd468d3a1a0b97feee5f6b9e499eab0dd903f6 # v2.5.0 with: comment_tag: ${{ env.EXECUTION_ID }} mode: upsert @@ -379,7 +379,7 @@ jobs: - name: Update comment on cancellation if: ${{ needs.generator.result == 'cancelled' }} - uses: thollander/actions-comment-pull-request@363c6f6eae92cc5c3a66e95ba016fc771bb38943 # v2.4.2 + uses: thollander/actions-comment-pull-request@fabd468d3a1a0b97feee5f6b9e499eab0dd903f6 # v2.5.0 with: comment_tag: ${{ env.EXECUTION_ID }} mode: delete From c51a2e169d374b1cb820e2975def1cb2bbd902f7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 29 Mar 2024 12:19:06 +0800 Subject: [PATCH 0461/2493] Add test coverage of crash scenario --- osu.Game.Tests/Visual/Menus/TestSceneLoginOverlay.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game.Tests/Visual/Menus/TestSceneLoginOverlay.cs b/osu.Game.Tests/Visual/Menus/TestSceneLoginOverlay.cs index e603f72bb8..24c9d1294f 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneLoginOverlay.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneLoginOverlay.cs @@ -74,6 +74,10 @@ namespace osu.Game.Tests.Visual.Menus }); AddStep("enter code", () => loginOverlay.ChildrenOfType().First().Text = "88800088"); assertAPIState(APIState.Online); + + AddStep("set failing", () => { dummyAPI.SetState(APIState.Failing); }); + AddStep("return to online", () => { dummyAPI.SetState(APIState.Online); }); + AddStep("clear handler", () => dummyAPI.HandleRequest = null); } From fef8afb833b7ddfd42b97260faff7593da514868 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 29 Mar 2024 12:03:05 +0800 Subject: [PATCH 0462/2493] Fix double binding causing game crash after API enters failing state See https://sentry.ppy.sh/organizations/ppy/issues/33406/?alert_rule_id=4&alert_timestamp=1711655107332&alert_type=email&environment=production&project=2&referrer=alert_email --- osu.Game/Overlays/Login/LoginPanel.cs | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/osu.Game/Overlays/Login/LoginPanel.cs b/osu.Game/Overlays/Login/LoginPanel.cs index 25bf612bc3..d5c7ed29b8 100644 --- a/osu.Game/Overlays/Login/LoginPanel.cs +++ b/osu.Game/Overlays/Login/LoginPanel.cs @@ -30,7 +30,7 @@ namespace osu.Game.Overlays.Login [Resolved] private OsuColour colours { get; set; } = null!; - private UserDropdown dropdown = null!; + private UserDropdown? dropdown; /// /// Called to request a hide of a parent displaying this container. @@ -68,6 +68,14 @@ namespace osu.Game.Overlays.Login apiState.BindValueChanged(onlineStateChanged, true); } + protected override void LoadComplete() + { + base.LoadComplete(); + + userStatus.BindTo(api.LocalUser.Value.Status); + userStatus.BindValueChanged(e => updateDropdownCurrent(e.NewValue), true); + } + private void onlineStateChanged(ValueChangedEvent state) => Schedule(() => { form = null; @@ -144,9 +152,6 @@ namespace osu.Game.Overlays.Login }, }; - userStatus.BindTo(api.LocalUser.Value.Status); - userStatus.BindValueChanged(e => updateDropdownCurrent(e.NewValue), true); - dropdown.Current.BindValueChanged(action => { switch (action.NewValue) @@ -171,6 +176,7 @@ namespace osu.Game.Overlays.Login break; } }, true); + break; } @@ -180,6 +186,9 @@ namespace osu.Game.Overlays.Login private void updateDropdownCurrent(UserStatus? status) { + if (dropdown == null) + return; + switch (status) { case UserStatus.Online: From d9cf5b5440ff3146e94d1c5da5c3c7c11bd53dd8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 29 Mar 2024 15:44:01 +0800 Subject: [PATCH 0463/2493] Fix bindable not being correctly re-bound across local user changes --- .../Visual/Menus/TestSceneLoginOverlay.cs | 12 +++++++++ osu.Game/Overlays/Login/LoginPanel.cs | 25 +++++++++++-------- 2 files changed, 27 insertions(+), 10 deletions(-) diff --git a/osu.Game.Tests/Visual/Menus/TestSceneLoginOverlay.cs b/osu.Game.Tests/Visual/Menus/TestSceneLoginOverlay.cs index 24c9d1294f..460d7814e0 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneLoginOverlay.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneLoginOverlay.cs @@ -15,6 +15,7 @@ using osu.Game.Online.API.Requests; using osu.Game.Overlays; using osu.Game.Overlays.Login; using osu.Game.Overlays.Settings; +using osu.Game.Users; using osu.Game.Users.Drawables; using osuTK.Input; @@ -72,13 +73,24 @@ namespace osu.Game.Tests.Visual.Menus return false; }); + AddStep("enter code", () => loginOverlay.ChildrenOfType().First().Text = "88800088"); assertAPIState(APIState.Online); + assertDropdownState(UserAction.Online); AddStep("set failing", () => { dummyAPI.SetState(APIState.Failing); }); AddStep("return to online", () => { dummyAPI.SetState(APIState.Online); }); AddStep("clear handler", () => dummyAPI.HandleRequest = null); + + assertDropdownState(UserAction.Online); + AddStep("change user state", () => dummyAPI.LocalUser.Value.Status.Value = UserStatus.DoNotDisturb); + assertDropdownState(UserAction.DoNotDisturb); + } + + private void assertDropdownState(UserAction state) + { + AddAssert($"dropdown state is {state}", () => loginOverlay.ChildrenOfType().First().Current.Value, () => Is.EqualTo(state)); } private void assertAPIState(APIState expected) => diff --git a/osu.Game/Overlays/Login/LoginPanel.cs b/osu.Game/Overlays/Login/LoginPanel.cs index d5c7ed29b8..a8adf4ce8c 100644 --- a/osu.Game/Overlays/Login/LoginPanel.cs +++ b/osu.Game/Overlays/Login/LoginPanel.cs @@ -15,6 +15,7 @@ using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Localisation; using osu.Game.Online.API; +using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays.Settings; using osu.Game.Users; using osuTK; @@ -37,8 +38,10 @@ namespace osu.Game.Overlays.Login /// public Action? RequestHide; + private IBindable user = null!; + private readonly Bindable status = new Bindable(); + private readonly IBindable apiState = new Bindable(); - private readonly Bindable userStatus = new Bindable(); [Resolved] private IAPIProvider api { get; set; } = null!; @@ -61,19 +64,21 @@ namespace osu.Game.Overlays.Login AutoSizeAxes = Axes.Y; } - [BackgroundDependencyLoader] - private void load() - { - apiState.BindTo(api.State); - apiState.BindValueChanged(onlineStateChanged, true); - } - protected override void LoadComplete() { base.LoadComplete(); - userStatus.BindTo(api.LocalUser.Value.Status); - userStatus.BindValueChanged(e => updateDropdownCurrent(e.NewValue), true); + apiState.BindTo(api.State); + apiState.BindValueChanged(onlineStateChanged, true); + + user = api.LocalUser.GetBoundCopy(); + user.BindValueChanged(u => + { + status.UnbindBindings(); + status.BindTo(u.NewValue.Status); + }, true); + + status.BindValueChanged(e => updateDropdownCurrent(e.NewValue), true); } private void onlineStateChanged(ValueChangedEvent state) => Schedule(() => From 233b9eb1723fb44a26c1a65c1d583e3e6f87f7b5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 29 Mar 2024 17:11:55 +0800 Subject: [PATCH 0464/2493] Avoid reporting an import as successful when all beatmaps failed to import --- osu.Game/Beatmaps/BeatmapImporter.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Beatmaps/BeatmapImporter.cs b/osu.Game/Beatmaps/BeatmapImporter.cs index 5ff3ab64b2..9ca0aafddd 100644 --- a/osu.Game/Beatmaps/BeatmapImporter.cs +++ b/osu.Game/Beatmaps/BeatmapImporter.cs @@ -434,6 +434,9 @@ namespace osu.Game.Beatmaps } } + if (!beatmaps.Any()) + throw new ArgumentException($"No valid beatmap files found in the beatmap archive."); + return beatmaps; } } From df4a28db915653f79603ef78ea44af48d781391e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 29 Mar 2024 17:32:20 +0800 Subject: [PATCH 0465/2493] Fix failing test due to missing ruleset store --- osu.Game.Tests/Database/LegacyBeatmapImporterTest.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game.Tests/Database/LegacyBeatmapImporterTest.cs b/osu.Game.Tests/Database/LegacyBeatmapImporterTest.cs index 5f722e381c..016928c6d6 100644 --- a/osu.Game.Tests/Database/LegacyBeatmapImporterTest.cs +++ b/osu.Game.Tests/Database/LegacyBeatmapImporterTest.cs @@ -12,6 +12,7 @@ using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Database; using osu.Game.IO; +using osu.Game.Rulesets; using osu.Game.Tests.Resources; namespace osu.Game.Tests.Database @@ -77,6 +78,7 @@ namespace osu.Game.Tests.Database { using (HeadlessGameHost host = new CleanRunHeadlessGameHost()) using (var tmpStorage = new TemporaryNativeStorage("stable-songs-folder")) + using (new RealmRulesetStore(realm, storage)) { var stableStorage = new StableStorage(tmpStorage.GetFullPath(""), host); var songsStorage = stableStorage.GetStorageForDirectory(StableStorage.STABLE_DEFAULT_SONGS_PATH); From 2d3b273974de4cb85dea652f3915a37f7ee0451e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 29 Mar 2024 10:36:17 +0100 Subject: [PATCH 0466/2493] Remove redundant string interpolation --- osu.Game/Beatmaps/BeatmapImporter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/BeatmapImporter.cs b/osu.Game/Beatmaps/BeatmapImporter.cs index 9ca0aafddd..2137f33e77 100644 --- a/osu.Game/Beatmaps/BeatmapImporter.cs +++ b/osu.Game/Beatmaps/BeatmapImporter.cs @@ -435,7 +435,7 @@ namespace osu.Game.Beatmaps } if (!beatmaps.Any()) - throw new ArgumentException($"No valid beatmap files found in the beatmap archive."); + throw new ArgumentException("No valid beatmap files found in the beatmap archive."); return beatmaps; } From e06df34a1c7f57ffec818be87264c4e82b61e508 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 29 Mar 2024 11:16:31 +0100 Subject: [PATCH 0467/2493] Apply partial fade on pp display on results screen when score will not give pp --- .../TestSceneExpandedPanelMiddleContent.cs | 37 +++++++++++++++++++ osu.Game/Localisation/ResultsScreenStrings.cs | 24 ++++++++++++ .../Statistics/PerformanceStatistic.cs | 32 ++++++++++++++-- 3 files changed, 89 insertions(+), 4 deletions(-) create mode 100644 osu.Game/Localisation/ResultsScreenStrings.cs diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs b/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs index d97946a1d5..9f7726313a 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs @@ -14,13 +14,16 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Graphics.Sprites; +using osu.Game.Localisation; using osu.Game.Models; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.Osu.Mods; using osu.Game.Scoring; using osu.Game.Screens.Ranking; using osu.Game.Screens.Ranking.Expanded; +using osu.Game.Screens.Ranking.Expanded.Statistics; using osu.Game.Tests.Beatmaps; using osu.Game.Tests.Resources; using osuTK; @@ -67,6 +70,40 @@ namespace osu.Game.Tests.Visual.Ranking AddAssert("play time displayed", () => this.ChildrenOfType().Any()); } + [Test] + public void TestPPShownAsProvisionalWhenBeatmapHasNoLeaderboard() + { + AddStep("show example score", () => + { + var beatmap = createTestBeatmap(new RealmUser()); + beatmap.Status = BeatmapOnlineStatus.Graveyard; + showPanel(TestResources.CreateTestScoreInfo(beatmap)); + }); + + AddAssert("pp display faded out", () => + { + var ppDisplay = this.ChildrenOfType().Single(); + return ppDisplay.Alpha == 0.5 && ppDisplay.TooltipText == ResultsScreenStrings.NoPPForUnrankedBeatmaps; + }); + } + + [Test] + public void TestPPShownAsProvisionalWhenUnrankedModsArePresent() + { + AddStep("show example score", () => + { + var score = TestResources.CreateTestScoreInfo(createTestBeatmap(new RealmUser())); + score.Mods = score.Mods.Append(new OsuModDifficultyAdjust()).ToArray(); + showPanel(score); + }); + + AddAssert("pp display faded out", () => + { + var ppDisplay = this.ChildrenOfType().Single(); + return ppDisplay.Alpha == 0.5 && ppDisplay.TooltipText == ResultsScreenStrings.NoPPForUnrankedMods; + }); + } + [Test] public void TestWithDefaultDate() { diff --git a/osu.Game/Localisation/ResultsScreenStrings.cs b/osu.Game/Localisation/ResultsScreenStrings.cs new file mode 100644 index 0000000000..54e7717af9 --- /dev/null +++ b/osu.Game/Localisation/ResultsScreenStrings.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 osu.Framework.Localisation; + +namespace osu.Game.Localisation +{ + public static class ResultsScreenStrings + { + private const string prefix = @"osu.Game.Resources.Localisation.ResultsScreen"; + + /// + /// "Performance points are not granted for this score because the beatmap is not ranked." + /// + public static LocalisableString NoPPForUnrankedBeatmaps => new TranslatableString(getKey(@"no_pp_for_unranked_beatmaps"), @"Performance points are not granted for this score because the beatmap is not ranked."); + + /// + /// "Performance points are not granted for this score because of unranked mods." + /// + public static LocalisableString NoPPForUnrankedMods => new TranslatableString(getKey(@"no_pp_for_unranked_mods"), @"Performance points are not granted for this score because of unranked mods."); + + private static string getKey(string key) => $@"{prefix}:{key}"; + } +} diff --git a/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs b/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs index 22c1e26d43..0a9c68eafc 100644 --- a/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs +++ b/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs @@ -4,20 +4,26 @@ #nullable disable using System; +using System.Linq; using System.Threading; using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Framework.Graphics.Cursor; +using osu.Framework.Localisation; using osu.Game.Beatmaps; using osu.Game.Graphics.UserInterface; using osu.Game.Resources.Localisation.Web; using osu.Game.Scoring; +using osu.Game.Localisation; namespace osu.Game.Screens.Ranking.Expanded.Statistics { - public partial class PerformanceStatistic : StatisticDisplay + public partial class PerformanceStatistic : StatisticDisplay, IHasTooltip { + public LocalisableString TooltipText { get; private set; } + private readonly ScoreInfo score; private readonly Bindable performance = new Bindable(); @@ -37,7 +43,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics { if (score.PP.HasValue) { - setPerformanceValue(score.PP.Value); + setPerformanceValue(score, score.PP.Value); } else { @@ -52,15 +58,33 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics var result = await performanceCalculator.CalculateAsync(score, attributes.Value.Attributes, cancellationToken ?? default).ConfigureAwait(false); - Schedule(() => setPerformanceValue(result.Total)); + Schedule(() => setPerformanceValue(score, result.Total)); }, cancellationToken ?? default); } } - private void setPerformanceValue(double? pp) + private void setPerformanceValue(ScoreInfo scoreInfo, double? pp) { if (pp.HasValue) + { performance.Value = (int)Math.Round(pp.Value, MidpointRounding.AwayFromZero); + + if (!scoreInfo.BeatmapInfo!.Status.GrantsPerformancePoints()) + { + Alpha = 0.5f; + TooltipText = ResultsScreenStrings.NoPPForUnrankedBeatmaps; + } + else if (scoreInfo.Mods.Any(m => !m.Ranked)) + { + Alpha = 0.5f; + TooltipText = ResultsScreenStrings.NoPPForUnrankedMods; + } + else + { + Alpha = 1f; + TooltipText = default; + } + } } public override void Appear() From c21805589eb2014bd07e6386b9398734e9a99dcb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 29 Mar 2024 22:40:04 +0800 Subject: [PATCH 0468/2493] Fix taiko mascot size not matching stable --- osu.Game.Rulesets.Taiko/UI/TaikoMascotAnimation.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoMascotAnimation.cs b/osu.Game.Rulesets.Taiko/UI/TaikoMascotAnimation.cs index 7b1e31112e..e863c4c2e4 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoMascotAnimation.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoMascotAnimation.cs @@ -27,7 +27,8 @@ namespace osu.Game.Rulesets.Taiko.UI InternalChild = textureAnimation = createTextureAnimation(state).With(animation => { animation.Origin = animation.Anchor = Anchor.BottomLeft; - animation.Scale = new Vector2(0.51f); // close enough to stable + // matches stable (https://github.com/peppy/osu-stable-reference/blob/054d0380c19aa5972be176d9d242ceb0e1630ae6/osu!/GameModes/Play/Rulesets/Taiko/TaikoMascot.cs#L34) + animation.Scale = new Vector2(0.6f); }); RelativeSizeAxes = Axes.Both; From 51f79c33e1c93ecb4cbf71e5de820941a0fa622e Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 29 Mar 2024 23:33:04 +0300 Subject: [PATCH 0469/2493] Fix URL pointing to non-existent commit --- osu.Game.Rulesets.Taiko/UI/TaikoMascotAnimation.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoMascotAnimation.cs b/osu.Game.Rulesets.Taiko/UI/TaikoMascotAnimation.cs index e863c4c2e4..90f7782aba 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoMascotAnimation.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoMascotAnimation.cs @@ -27,7 +27,7 @@ namespace osu.Game.Rulesets.Taiko.UI InternalChild = textureAnimation = createTextureAnimation(state).With(animation => { animation.Origin = animation.Anchor = Anchor.BottomLeft; - // matches stable (https://github.com/peppy/osu-stable-reference/blob/054d0380c19aa5972be176d9d242ceb0e1630ae6/osu!/GameModes/Play/Rulesets/Taiko/TaikoMascot.cs#L34) + // matches stable (https://github.com/peppy/osu-stable-reference/blob/e53980dd76857ee899f66ce519ba1597e7874f28/osu!/GameModes/Play/Rulesets/Taiko/TaikoMascot.cs#L34) animation.Scale = new Vector2(0.6f); }); From 8d6358a138605828c5260b4efca907049ed73e92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=C3=AAn=20Minh=20H=E1=BB=93?= Date: Sat, 30 Mar 2024 16:02:31 +0700 Subject: [PATCH 0470/2493] Fix editor rotation allowing spinner only bug --- osu.Game.Rulesets.Osu/Edit/OsuSelectionRotationHandler.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionRotationHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionRotationHandler.cs index 1998e02a5c..d48bc6a90b 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionRotationHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionRotationHandler.cs @@ -42,7 +42,7 @@ namespace osu.Game.Rulesets.Osu.Edit { var quad = GeometryUtils.GetSurroundingQuad(selectedMovableObjects); CanRotateSelectionOrigin.Value = quad.Width > 0 || quad.Height > 0; - CanRotatePlayfieldOrigin.Value = selectedItems.Any(); + CanRotatePlayfieldOrigin.Value = selectedMovableObjects.Any(); } private OsuHitObject[]? objectsInRotation; From 5d497ba4a8ada3ae5ad733ed6d856328b39a9576 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=C3=AAn=20Minh=20H=E1=BB=93?= Date: Sat, 30 Mar 2024 16:04:22 +0700 Subject: [PATCH 0471/2493] Simplify TooltipText for EditorRadioButton --- .../Edit/PreciseRotationPopover.cs | 8 +++++++- osu.Game/Rulesets/Edit/HitObjectComposer.cs | 8 +++++++- .../Components/RadioButtons/EditorRadioButton.cs | 2 +- .../Edit/Components/RadioButtons/RadioButton.cs | 13 +++---------- 4 files changed, 18 insertions(+), 13 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs b/osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs index 2cf6799279..6c29184be4 100644 --- a/osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs +++ b/osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs @@ -68,7 +68,13 @@ namespace osu.Game.Rulesets.Osu.Edit } } }; - selectionCentreButton.TooltipTextWhenDisabled = "We can't rotate a circle around itself! Can we?"; + selectionCentreButton.Selected.DisabledChanged += (isDisabled) => + { + if (isDisabled) + selectionCentreButton.TooltipText = "We can't rotate a circle around itself! Can we?"; + else + selectionCentreButton.TooltipText = string.Empty; + }; } protected override void LoadComplete() diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs index bc8de7f4b2..09bac7a791 100644 --- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs @@ -214,7 +214,13 @@ namespace osu.Game.Rulesets.Edit foreach (var item in toolboxCollection.Items) { - item.TooltipTextWhenDisabled = "Add at least one timing point first!"; + item.Selected.DisabledChanged += (isDisabled) => + { + if (isDisabled) + item.TooltipText = "Add at least one timing point first!"; + else + item.TooltipText = string.Empty; + }; } TernaryStates = CreateTernaryButtons().ToArray(); diff --git a/osu.Game/Screens/Edit/Components/RadioButtons/EditorRadioButton.cs b/osu.Game/Screens/Edit/Components/RadioButtons/EditorRadioButton.cs index 9d1f87e1e0..29bb24eb43 100644 --- a/osu.Game/Screens/Edit/Components/RadioButtons/EditorRadioButton.cs +++ b/osu.Game/Screens/Edit/Components/RadioButtons/EditorRadioButton.cs @@ -94,6 +94,6 @@ namespace osu.Game.Screens.Edit.Components.RadioButtons X = 40f }; - public LocalisableString TooltipText => Enabled.Value ? Button.TooltipTextWhenEnabled : Button.TooltipTextWhenDisabled; + public LocalisableString TooltipText => Button.TooltipText; } } diff --git a/osu.Game/Screens/Edit/Components/RadioButtons/RadioButton.cs b/osu.Game/Screens/Edit/Components/RadioButtons/RadioButton.cs index 2d1416c9c6..f49fc6f6ab 100644 --- a/osu.Game/Screens/Edit/Components/RadioButtons/RadioButton.cs +++ b/osu.Game/Screens/Edit/Components/RadioButtons/RadioButton.cs @@ -16,16 +16,6 @@ namespace osu.Game.Screens.Edit.Components.RadioButtons /// public readonly BindableBool Selected; - /// - /// Tooltip text that will be shown on hover if button is enabled. - /// - public LocalisableString TooltipTextWhenEnabled { get; set; } = string.Empty; - - /// - /// Tooltip text that will be shown on hover if button is disabled. - /// - public LocalisableString TooltipTextWhenDisabled { get; set; } = string.Empty; - /// /// The item related to this button. /// @@ -62,5 +52,8 @@ namespace osu.Game.Screens.Edit.Components.RadioButtons /// Deselects this . /// public void Deselect() => Selected.Value = false; + + // Tooltip text that will be shown when hovered over + public LocalisableString TooltipText { get; set; } = string.Empty; } } From 6f782266b51b717e996cc258b05da8fb737533d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=C3=AAn=20Minh=20H=E1=BB=93?= Date: Sat, 30 Mar 2024 17:03:40 +0700 Subject: [PATCH 0472/2493] Fix inspection --- osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs | 7 ++----- osu.Game/Rulesets/Edit/HitObjectComposer.cs | 7 ++----- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs b/osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs index 6c29184be4..70441b33dd 100644 --- a/osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs +++ b/osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs @@ -68,12 +68,9 @@ namespace osu.Game.Rulesets.Osu.Edit } } }; - selectionCentreButton.Selected.DisabledChanged += (isDisabled) => + selectionCentreButton.Selected.DisabledChanged += isDisabled => { - if (isDisabled) - selectionCentreButton.TooltipText = "We can't rotate a circle around itself! Can we?"; - else - selectionCentreButton.TooltipText = string.Empty; + selectionCentreButton.TooltipText = isDisabled ? "We can't rotate a circle around itself! Can we?" : string.Empty; }; } diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs index 09bac7a791..4d92a08bed 100644 --- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs @@ -214,12 +214,9 @@ namespace osu.Game.Rulesets.Edit foreach (var item in toolboxCollection.Items) { - item.Selected.DisabledChanged += (isDisabled) => + item.Selected.DisabledChanged += isDisabled => { - if (isDisabled) - item.TooltipText = "Add at least one timing point first!"; - else - item.TooltipText = string.Empty; + item.TooltipText = isDisabled ? "Add at least one timing point first!" : string.Empty; }; } From b445e27ad6bfae93244b89acc1544aef01978bdc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=C3=AAn=20Minh=20H=E1=BB=93?= Date: Sat, 30 Mar 2024 17:54:27 +0700 Subject: [PATCH 0473/2493] Aggregate two CanRotate bindable for enabling the Rotate button --- osu.Game.Rulesets.Osu/Edit/TransformToolboxGroup.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Edit/TransformToolboxGroup.cs b/osu.Game.Rulesets.Osu/Edit/TransformToolboxGroup.cs index 19590e9b6e..3e2cbe9d60 100644 --- a/osu.Game.Rulesets.Osu/Edit/TransformToolboxGroup.cs +++ b/osu.Game.Rulesets.Osu/Edit/TransformToolboxGroup.cs @@ -51,9 +51,17 @@ namespace osu.Game.Rulesets.Osu.Edit { base.LoadComplete(); + // aggregate two values into canRotate + RotationHandler.CanRotatePlayfieldOrigin.BindValueChanged(_ => updateCanRotateAggregate()); + RotationHandler.CanRotateSelectionOrigin.BindValueChanged(_ => updateCanRotateAggregate()); + + void updateCanRotateAggregate() + { + canRotate.Value = RotationHandler.CanRotatePlayfieldOrigin.Value || RotationHandler.CanRotateSelectionOrigin.Value; + } + // bindings to `Enabled` on the buttons are decoupled on purpose // due to the weird `OsuButton` behaviour of resetting `Enabled` to `false` when `Action` is set. - canRotate.BindTo(RotationHandler.CanRotatePlayfieldOrigin); canRotate.BindValueChanged(_ => rotateButton.Enabled.Value = canRotate.Value, true); } From 54472e6452c61671d487f59d6e61af89daaa6d59 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Sun, 31 Mar 2024 01:20:27 +0300 Subject: [PATCH 0474/2493] Decouple GlowingDrawable from GlowingSpriteText --- osu.Game/Graphics/Sprites/GlowingDrawable.cs | 41 +++++++++++++++++++ .../Graphics/Sprites/GlowingSpriteText.cs | 39 ++++-------------- 2 files changed, 50 insertions(+), 30 deletions(-) create mode 100644 osu.Game/Graphics/Sprites/GlowingDrawable.cs diff --git a/osu.Game/Graphics/Sprites/GlowingDrawable.cs b/osu.Game/Graphics/Sprites/GlowingDrawable.cs new file mode 100644 index 0000000000..10085ad38b --- /dev/null +++ b/osu.Game/Graphics/Sprites/GlowingDrawable.cs @@ -0,0 +1,41 @@ +// 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.Graphics; +using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Primitives; +using osu.Framework.Utils; +using osuTK; + +namespace osu.Game.Graphics.Sprites +{ + public abstract partial class GlowingDrawable : BufferedContainer + { + // Inflate draw quad to prevent glow from trimming at the edges. + // Padding won't suffice since it will affect drawable position in cases when it's not centered. + protected override Quad ComputeScreenSpaceDrawQuad() + => base.ComputeScreenSpaceDrawQuad().AABBFloat.Inflate(new Vector2(Blur.KernelSize(BlurSigma.X), Blur.KernelSize(BlurSigma.Y))); + + public ColourInfo GlowColour + { + get => EffectColour; + set + { + EffectColour = value; + BackgroundColour = value.MultiplyAlpha(0f); + } + } + + protected GlowingDrawable() + : base(cachedFrameBuffer: true) + { + AutoSizeAxes = Axes.Both; + RedrawOnScale = false; + DrawOriginal = true; + Child = CreateDrawable(); + } + + protected abstract Drawable CreateDrawable(); + } +} diff --git a/osu.Game/Graphics/Sprites/GlowingSpriteText.cs b/osu.Game/Graphics/Sprites/GlowingSpriteText.cs index 669c5da01e..3ac13bf862 100644 --- a/osu.Game/Graphics/Sprites/GlowingSpriteText.cs +++ b/osu.Game/Graphics/Sprites/GlowingSpriteText.cs @@ -4,24 +4,17 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; -using osu.Framework.Utils; using osuTK; namespace osu.Game.Graphics.Sprites { - public partial class GlowingSpriteText : BufferedContainer, IHasText + public partial class GlowingSpriteText : GlowingDrawable, IHasText { private const float blur_sigma = 3f; - // Inflate draw quad to prevent glow from trimming at the edges. - // Padding won't suffice since it will affect text position in cases when it's not centered. - protected override Quad ComputeScreenSpaceDrawQuad() => base.ComputeScreenSpaceDrawQuad().AABBFloat.Inflate(Blur.KernelSize(blur_sigma)); - - private readonly OsuSpriteText text; + private OsuSpriteText text = null!; public LocalisableString Text { @@ -47,16 +40,6 @@ namespace osu.Game.Graphics.Sprites set => text.Colour = value; } - public ColourInfo GlowColour - { - get => EffectColour; - set - { - EffectColour = value; - BackgroundColour = value.MultiplyAlpha(0f); - } - } - public Vector2 Spacing { get => text.Spacing; @@ -76,20 +59,16 @@ namespace osu.Game.Graphics.Sprites } public GlowingSpriteText() - : base(cachedFrameBuffer: true) { - AutoSizeAxes = Axes.Both; BlurSigma = new Vector2(blur_sigma); - RedrawOnScale = false; - DrawOriginal = true; EffectBlending = BlendingParameters.Additive; - EffectPlacement = EffectPlacement.InFront; - Child = text = new OsuSpriteText - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Shadow = false, - }; } + + protected override Drawable CreateDrawable() => text = new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Shadow = false, + }; } } From 58a68e94af79205aa13989fb8b896c9d27c0f9c0 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Sun, 31 Mar 2024 01:30:08 +0300 Subject: [PATCH 0475/2493] Simplify glowing icons in break overlay --- osu.Game/Screens/Play/Break/BlurredIcon.cs | 44 ++--------------- osu.Game/Screens/Play/Break/GlowIcon.cs | 57 ++++++++-------------- 2 files changed, 23 insertions(+), 78 deletions(-) diff --git a/osu.Game/Screens/Play/Break/BlurredIcon.cs b/osu.Game/Screens/Play/Break/BlurredIcon.cs index 2bf59ea63b..9cd617d3e3 100644 --- a/osu.Game/Screens/Play/Break/BlurredIcon.cs +++ b/osu.Game/Screens/Play/Break/BlurredIcon.cs @@ -1,52 +1,16 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using osu.Framework.Allocation; using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Sprites; -using osu.Game.Graphics; -using osuTK; namespace osu.Game.Screens.Play.Break { - public partial class BlurredIcon : BufferedContainer + public partial class BlurredIcon : GlowIcon { - private readonly SpriteIcon icon; - - public IconUsage Icon - { - set => icon.Icon = value; - get => icon.Icon; - } - - public override Vector2 Size - { - set - { - icon.Size = value; - base.Size = value + BlurSigma * 5; - ForceRedraw(); - } - get => base.Size; - } - public BlurredIcon() - : base(cachedFrameBuffer: true) { - RelativePositionAxes = Axes.X; - Child = icon = new SpriteIcon - { - Origin = Anchor.Centre, - Anchor = Anchor.Centre, - Shadow = false, - }; - } - - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { - Colour = colours.BlueLighter; + EffectBlending = BlendingParameters.Additive; + DrawOriginal = false; } } } diff --git a/osu.Game/Screens/Play/Break/GlowIcon.cs b/osu.Game/Screens/Play/Break/GlowIcon.cs index 8e2b9da0ad..a68cfdac42 100644 --- a/osu.Game/Screens/Play/Break/GlowIcon.cs +++ b/osu.Game/Screens/Play/Break/GlowIcon.cs @@ -3,64 +3,45 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; using osuTK; namespace osu.Game.Screens.Play.Break { - public partial class GlowIcon : Container + public partial class GlowIcon : GlowingDrawable { - private readonly SpriteIcon spriteIcon; - private readonly BlurredIcon blurredIcon; - - public override Vector2 Size - { - get => base.Size; - set - { - blurredIcon.Size = spriteIcon.Size = value; - blurredIcon.ForceRedraw(); - } - } - - public Vector2 BlurSigma - { - get => blurredIcon.BlurSigma; - set => blurredIcon.BlurSigma = value; - } + private SpriteIcon icon = null!; public IconUsage Icon { - get => spriteIcon.Icon; - set => spriteIcon.Icon = blurredIcon.Icon = value; + set => icon.Icon = value; + get => icon.Icon; + } + + public new Vector2 Size + { + set => icon.Size = value; + get => icon.Size; } public GlowIcon() { RelativePositionAxes = Axes.X; - AutoSizeAxes = Axes.Both; - Children = new Drawable[] - { - blurredIcon = new BlurredIcon - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - }, - spriteIcon = new SpriteIcon - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Shadow = false, - } - }; } [BackgroundDependencyLoader] private void load(OsuColour colours) { - blurredIcon.Colour = colours.Blue; + GlowColour = colours.BlueLighter; } + + protected override Drawable CreateDrawable() => icon = new SpriteIcon + { + Origin = Anchor.Centre, + Anchor = Anchor.Centre, + Shadow = false, + }; } } From 11b113580496fd979bfa083573e0e17ac6cb8f64 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Sun, 31 Mar 2024 01:55:34 +0300 Subject: [PATCH 0476/2493] Adjust blurred icons position due to size handling differences --- osu.Game/Screens/Play/Break/BreakArrows.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/Break/BreakArrows.cs b/osu.Game/Screens/Play/Break/BreakArrows.cs index 41277c7557..40474a7137 100644 --- a/osu.Game/Screens/Play/Break/BreakArrows.cs +++ b/osu.Game/Screens/Play/Break/BreakArrows.cs @@ -18,7 +18,7 @@ namespace osu.Game.Screens.Play.Break private const int blurred_icon_blur_sigma = 20; private const int blurred_icon_size = 130; - private const float blurred_icon_final_offset = 0.35f; + private const float blurred_icon_final_offset = 0.38f; private const float blurred_icon_offscreen_offset = 0.7f; private readonly GlowIcon leftGlowIcon; From 450e7016bcbf2e778910f6114da6688140d3cbd5 Mon Sep 17 00:00:00 2001 From: Arthur Araujo Date: Sat, 30 Mar 2024 21:23:05 -0300 Subject: [PATCH 0477/2493] Bind `StackHeight` changes to visual update methods --- .../TestScenePathControlPointVisualiser.cs | 48 +++++++++++++++++++ .../PathControlPointConnectionPiece.cs | 2 + .../Components/PathControlPointPiece.cs | 2 + 3 files changed, 52 insertions(+) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePathControlPointVisualiser.cs index 2b53554ed1..0c12e6fb21 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePathControlPointVisualiser.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePathControlPointVisualiser.cs @@ -172,6 +172,54 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor assertControlPointPathType(4, null); } + [Test] + public void TestStackingUpdatesPointsPosition() + { + createVisualiser(true); + + Vector2[] points = [ + new Vector2(200), + new Vector2(300), + new Vector2(500, 300), + new Vector2(700, 200), + new Vector2(500, 100) + ]; + + foreach (var point in points) addControlPointStep(point); + + AddStep("apply stacking", () => slider.StackHeightBindable.Value += 1); + + for (int i = 0; i < points.Length; i++) + addAssertPointPositionChanged(points, i); + } + + [Test] + public void TestStackingUpdatesConnectionPosition() + { + createVisualiser(true); + + Vector2 connectionPosition = default!; + + addControlPointStep(connectionPosition = new Vector2(300)); + addControlPointStep(new Vector2(600)); + + // Apply a big number in stacking so the person running the test can clearly see if it fails + AddStep("apply stacking", () => slider.StackHeightBindable.Value += 10); + + AddAssert($"Connection at {connectionPosition} changed", + () => visualiser.Connections[0].Position, + () => !Is.EqualTo(connectionPosition) + ); + } + + private void addAssertPointPositionChanged(Vector2[] points, int index) + { + AddAssert($"Point at {points.ElementAt(index)} changed", + () => visualiser.Pieces[index].Position, + () => !Is.EqualTo(points.ElementAt(index)) + ); + } + private void createVisualiser(bool allowSelection) => AddStep("create visualiser", () => Child = visualiser = new PathControlPointVisualiser(slider, allowSelection) { Anchor = Anchor.Centre, diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointConnectionPiece.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointConnectionPiece.cs index 7e7d653dbd..56dc16dd95 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointConnectionPiece.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointConnectionPiece.cs @@ -56,6 +56,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components pathVersion = hitObject.Path.Version.GetBoundCopy(); pathVersion.BindValueChanged(_ => Scheduler.AddOnce(updateConnectingPath)); + hitObject.StackHeightBindable.BindValueChanged(_ => updateConnectingPath()); + updateConnectingPath(); } diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs index e741d67e3b..ee306fb6d7 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs @@ -105,6 +105,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components hitObjectScale = hitObject.ScaleBindable.GetBoundCopy(); hitObjectScale.BindValueChanged(_ => updateMarkerDisplay()); + hitObject.StackHeightBindable.BindValueChanged(_ => updateMarkerDisplay()); + IsSelected.BindValueChanged(_ => updateMarkerDisplay()); updateMarkerDisplay(); From 86def7e263ff0bc811a9f7cd4b51c4d7ee1d7129 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=C3=AAn=20Minh=20H=E1=BB=93?= <32929093+honguyenminh@users.noreply.github.com> Date: Sun, 31 Mar 2024 16:00:47 +0700 Subject: [PATCH 0478/2493] Change editor rotate button disabled tooltip text Co-authored-by: Dean Herbert --- osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs b/osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs index 70441b33dd..da50233920 100644 --- a/osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs +++ b/osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs @@ -70,7 +70,7 @@ namespace osu.Game.Rulesets.Osu.Edit }; selectionCentreButton.Selected.DisabledChanged += isDisabled => { - selectionCentreButton.TooltipText = isDisabled ? "We can't rotate a circle around itself! Can we?" : string.Empty; + selectionCentreButton.TooltipText = isDisabled ? "Select more than one circles to perform rotation." : string.Empty; }; } From 19f0caa0f363427d0df39e5f9eb7e34c822f2699 Mon Sep 17 00:00:00 2001 From: Arthur Araujo Date: Sun, 31 Mar 2024 13:39:19 -0300 Subject: [PATCH 0479/2493] Fix wrong formatting in array creation --- .../Editor/TestScenePathControlPointVisualiser.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePathControlPointVisualiser.cs index 0c12e6fb21..335ccb5280 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePathControlPointVisualiser.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePathControlPointVisualiser.cs @@ -177,7 +177,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor { createVisualiser(true); - Vector2[] points = [ + Vector2[] points = + [ new Vector2(200), new Vector2(300), new Vector2(500, 300), From 7615c71efef1742e2c19620086964474d03a69f4 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 1 Apr 2024 15:28:20 +0900 Subject: [PATCH 0480/2493] Fix inspection --- .../Editor/TestScenePathControlPointVisualiser.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePathControlPointVisualiser.cs index 335ccb5280..0ca30e00bc 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePathControlPointVisualiser.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePathControlPointVisualiser.cs @@ -199,8 +199,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor { createVisualiser(true); - Vector2 connectionPosition = default!; - + Vector2 connectionPosition; addControlPointStep(connectionPosition = new Vector2(300)); addControlPointStep(new Vector2(600)); From 099ad22a92658f8bc016a1abe72fb2b13fb99da6 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 1 Apr 2024 15:31:34 +0900 Subject: [PATCH 0481/2493] Use local bindable instead Binding events directly to an external bindable will cause that bindable to hold a permanent reference to the current object. We use `GetBoundCopy()` or otherwise a local bindable + `.BindTo()` to create a weak-referenced copy of the target bindable. When the local bindable's lifetime expires, so does the external bindable's reference to it. --- .../Sliders/Components/PathControlPointConnectionPiece.cs | 4 +++- .../Blueprints/Sliders/Components/PathControlPointPiece.cs | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointConnectionPiece.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointConnectionPiece.cs index 56dc16dd95..9b3d8fc7a7 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointConnectionPiece.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointConnectionPiece.cs @@ -28,6 +28,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components private IBindable hitObjectPosition; private IBindable pathVersion; + private IBindable stackHeight; public PathControlPointConnectionPiece(T hitObject, int controlPointIndex) { @@ -56,7 +57,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components pathVersion = hitObject.Path.Version.GetBoundCopy(); pathVersion.BindValueChanged(_ => Scheduler.AddOnce(updateConnectingPath)); - hitObject.StackHeightBindable.BindValueChanged(_ => updateConnectingPath()); + stackHeight = hitObject.StackHeightBindable.GetBoundCopy(); + stackHeight.BindValueChanged(_ => updateConnectingPath()); updateConnectingPath(); } diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs index ee306fb6d7..c6e05d3ca3 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs @@ -48,6 +48,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components private IBindable hitObjectPosition; private IBindable hitObjectScale; + private IBindable stackHeight; public PathControlPointPiece(T hitObject, PathControlPoint controlPoint) { @@ -105,7 +106,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components hitObjectScale = hitObject.ScaleBindable.GetBoundCopy(); hitObjectScale.BindValueChanged(_ => updateMarkerDisplay()); - hitObject.StackHeightBindable.BindValueChanged(_ => updateMarkerDisplay()); + stackHeight = hitObject.StackHeightBindable.GetBoundCopy(); + stackHeight.BindValueChanged(_ => updateMarkerDisplay()); IsSelected.BindValueChanged(_ => updateMarkerDisplay()); From d12a2e7df7131fea37974bbb0707705cc2f0e977 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 1 Apr 2024 17:02:02 +0900 Subject: [PATCH 0482/2493] Replace schedule with SequenceEqual() --- osu.Game/Screens/Select/FilterControl.cs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Select/FilterControl.cs b/osu.Game/Screens/Select/FilterControl.cs index 7b8b5393bd..0bfd927234 100644 --- a/osu.Game/Screens/Select/FilterControl.cs +++ b/osu.Game/Screens/Select/FilterControl.cs @@ -6,6 +6,7 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; +using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -218,12 +219,16 @@ namespace osu.Game.Screens.Select maximumStars.ValueChanged += _ => updateCriteria(); ruleset.BindValueChanged(_ => updateCriteria()); - mods.BindValueChanged(_ => + mods.BindValueChanged(m => { - // Mods are updated once by the mod select overlay when song select is entered, regardless of if there are any mods. + // Mods are updated once by the mod select overlay when song select is entered, + // regardless of if there are any mods or any changes have taken place. // Updating the criteria here so early triggers a re-ordering of panels on song select, via... some mechanism. - // Todo: Investigate/fix the above and remove this schedule. - Scheduler.AddOnce(updateCriteria); + // Todo: Investigate/fix and potentially remove this. + if (m.NewValue.SequenceEqual(m.OldValue)) + return; + + updateCriteria(); }); groupMode.BindValueChanged(_ => updateCriteria()); From 8e0ca11d1cca1571b0d0efd61e0322b618333e5d Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 1 Apr 2024 17:02:32 +0900 Subject: [PATCH 0483/2493] Fully qualify LegacyBeatmapConversionDifficultyInfo --- osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs | 3 --- osu.Game.Rulesets.Mania/ManiaFilterCriteria.cs | 3 ++- osu.Game.Rulesets.Mania/ManiaRuleset.cs | 2 +- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs index bed04a882f..39ee3d209b 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs @@ -98,9 +98,6 @@ namespace osu.Game.Rulesets.Mania.Beatmaps } } - public static int GetColumnCount(IBeatmapInfo beatmapInfo, IReadOnlyList? mods = null) - => GetColumnCount(LegacyBeatmapConversionDifficultyInfo.FromBeatmapInfo(beatmapInfo), mods); - public static int GetColumnCount(LegacyBeatmapConversionDifficultyInfo difficulty, IReadOnlyList? mods = null) { var converter = new ManiaBeatmapConverter(null, difficulty, new ManiaRuleset()); diff --git a/osu.Game.Rulesets.Mania/ManiaFilterCriteria.cs b/osu.Game.Rulesets.Mania/ManiaFilterCriteria.cs index 07ed3ebd63..ea7eb5b8f0 100644 --- a/osu.Game.Rulesets.Mania/ManiaFilterCriteria.cs +++ b/osu.Game.Rulesets.Mania/ManiaFilterCriteria.cs @@ -4,6 +4,7 @@ using osu.Game.Beatmaps; using osu.Game.Rulesets.Filter; using osu.Game.Rulesets.Mania.Beatmaps; +using osu.Game.Rulesets.Scoring.Legacy; using osu.Game.Screens.Select; using osu.Game.Screens.Select.Filter; @@ -15,7 +16,7 @@ namespace osu.Game.Rulesets.Mania public bool Matches(BeatmapInfo beatmapInfo, FilterCriteria criteria) { - return !keys.HasFilter || keys.IsInRange(ManiaBeatmapConverter.GetColumnCount(beatmapInfo, criteria.Mods)); + return !keys.HasFilter || keys.IsInRange(ManiaBeatmapConverter.GetColumnCount(LegacyBeatmapConversionDifficultyInfo.FromBeatmapInfo(beatmapInfo), criteria.Mods)); } public bool TryParseCustomKeywordCriteria(string key, Operator op, string value) diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs index 77168dca68..b5614e2b56 100644 --- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs @@ -424,7 +424,7 @@ namespace osu.Game.Rulesets.Mania public override DifficultySection CreateEditorDifficultySection() => new ManiaDifficultySection(); public int GetKeyCount(IBeatmapInfo beatmapInfo, IReadOnlyList? mods = null) - => ManiaBeatmapConverter.GetColumnCount(beatmapInfo, mods); + => ManiaBeatmapConverter.GetColumnCount(LegacyBeatmapConversionDifficultyInfo.FromBeatmapInfo(beatmapInfo), mods); } public enum PlayfieldType From 4806ea54f1a55c1220c0772d2b385f674a06661d Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 1 Apr 2024 17:22:50 +0900 Subject: [PATCH 0484/2493] Only optimise Catmull segments in osu ruleset --- osu.Game.Rulesets.Osu/Objects/Slider.cs | 2 +- osu.Game/Rulesets/Objects/SliderPath.cs | 25 +++++++++++++++++++++++-- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs index 203e829180..cc3ffd376e 100644 --- a/osu.Game.Rulesets.Osu/Objects/Slider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs @@ -41,7 +41,7 @@ namespace osu.Game.Rulesets.Osu.Objects public Vector2 StackedPositionAt(double t) => StackedPosition + this.CurvePositionAt(t); - private readonly SliderPath path = new SliderPath(); + private readonly SliderPath path = new SliderPath { OptimiseCatmull = true }; public SliderPath Path { diff --git a/osu.Game/Rulesets/Objects/SliderPath.cs b/osu.Game/Rulesets/Objects/SliderPath.cs index 5398d6c45f..e8e769e3fa 100644 --- a/osu.Game/Rulesets/Objects/SliderPath.cs +++ b/osu.Game/Rulesets/Objects/SliderPath.cs @@ -134,6 +134,24 @@ namespace osu.Game.Rulesets.Objects } } + private bool optimiseCatmull; + + /// + /// Whether to optimise Catmull path segments, usually resulting in removing bulbs around stacked knots. + /// + /// + /// This changes the path shape and should therefore not be used. + /// + public bool OptimiseCatmull + { + get => optimiseCatmull; + set + { + optimiseCatmull = value; + invalidate(); + } + } + /// /// Computes the slider path until a given progress that ranges from 0 (beginning of the slider) /// to 1 (end of the slider) and stores the generated path in the given list. @@ -280,7 +298,7 @@ namespace osu.Game.Rulesets.Objects calculatedPath.Add(segmentVertices[0]); else if (segmentVertices.Length > 1) { - List subPath = calculateSubPath(segmentVertices, segmentType, ref optimisedLength); + List subPath = calculateSubPath(segmentVertices, segmentType); // Skip the first vertex if it is the same as the last vertex from the previous segment bool skipFirst = calculatedPath.Count > 0 && subPath.Count > 0 && calculatedPath.Last() == subPath[0]; @@ -300,7 +318,7 @@ namespace osu.Game.Rulesets.Objects } } - private static List calculateSubPath(ReadOnlySpan subControlPoints, PathType type, ref double optimisedLength) + private List calculateSubPath(ReadOnlySpan subControlPoints, PathType type) { switch (type.Type) { @@ -325,6 +343,9 @@ namespace osu.Game.Rulesets.Objects { List subPath = PathApproximator.CatmullToPiecewiseLinear(subControlPoints); + if (!OptimiseCatmull) + return subPath; + // At draw time, osu!stable optimises paths by only keeping piecewise segments that are 6px apart. // For the most part we don't care about this optimisation, and its additional heuristics are hard to reproduce in every implementation. // From ed5dd5c8cd100f99223a20360e10e52668c64edb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 2 Apr 2024 13:04:34 +0800 Subject: [PATCH 0485/2493] Bind using local bindables to avoid potentially event pollution --- osu.Game.Rulesets.Osu/Edit/TransformToolboxGroup.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/TransformToolboxGroup.cs b/osu.Game.Rulesets.Osu/Edit/TransformToolboxGroup.cs index 3e2cbe9d60..9499bacade 100644 --- a/osu.Game.Rulesets.Osu/Edit/TransformToolboxGroup.cs +++ b/osu.Game.Rulesets.Osu/Edit/TransformToolboxGroup.cs @@ -22,6 +22,9 @@ namespace osu.Game.Rulesets.Osu.Edit private EditorToolButton rotateButton = null!; + private Bindable canRotatePlayfieldOrigin = null!; + private Bindable canRotateSelectionOrigin = null!; + public SelectionRotationHandler RotationHandler { get; init; } = null!; public TransformToolboxGroup() @@ -52,8 +55,11 @@ namespace osu.Game.Rulesets.Osu.Edit base.LoadComplete(); // aggregate two values into canRotate - RotationHandler.CanRotatePlayfieldOrigin.BindValueChanged(_ => updateCanRotateAggregate()); - RotationHandler.CanRotateSelectionOrigin.BindValueChanged(_ => updateCanRotateAggregate()); + canRotatePlayfieldOrigin = RotationHandler.CanRotatePlayfieldOrigin.GetBoundCopy(); + canRotatePlayfieldOrigin.BindValueChanged(_ => updateCanRotateAggregate()); + + canRotateSelectionOrigin = RotationHandler.CanRotateSelectionOrigin.GetBoundCopy(); + canRotateSelectionOrigin.BindValueChanged(_ => updateCanRotateAggregate()); void updateCanRotateAggregate() { From eca242c1c73567433a25e34e075740b35ec7119d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 2 Apr 2024 13:17:16 +0800 Subject: [PATCH 0486/2493] Change tooltip text slightly --- osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs b/osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs index da50233920..caf02d1dda 100644 --- a/osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs +++ b/osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs @@ -70,7 +70,7 @@ namespace osu.Game.Rulesets.Osu.Edit }; selectionCentreButton.Selected.DisabledChanged += isDisabled => { - selectionCentreButton.TooltipText = isDisabled ? "Select more than one circles to perform rotation." : string.Empty; + selectionCentreButton.TooltipText = isDisabled ? "Select more than one objects to perform selection-based rotation." : string.Empty; }; } From 6642702fa94e1819ec13243d87a6626c88e88a1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=C3=AAn=20Minh=20H=E1=BB=93?= <32929093+honguyenminh@users.noreply.github.com> Date: Tue, 2 Apr 2024 12:36:44 +0700 Subject: [PATCH 0487/2493] Update plurals in editor rotate button tooltip Co-authored-by: Joseph Madamba --- osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs b/osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs index caf02d1dda..88c3d7414b 100644 --- a/osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs +++ b/osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs @@ -70,7 +70,7 @@ namespace osu.Game.Rulesets.Osu.Edit }; selectionCentreButton.Selected.DisabledChanged += isDisabled => { - selectionCentreButton.TooltipText = isDisabled ? "Select more than one objects to perform selection-based rotation." : string.Empty; + selectionCentreButton.TooltipText = isDisabled ? "Select more than one object to perform selection-based rotation." : string.Empty; }; } From 2a2a372595a7a64dc1b2fe660e6f053e4522cd1e Mon Sep 17 00:00:00 2001 From: Arthur Araujo Date: Tue, 2 Apr 2024 07:45:27 -0300 Subject: [PATCH 0488/2493] Check if `blueprint` is in `SelectionBlueprints` before changing its depth --- .../Editor/TestSceneObjectMerging.cs | 132 ++++++++++++++++++ .../Compose/Components/BlueprintContainer.cs | 4 +- 2 files changed, 135 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectMerging.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectMerging.cs index 3d35ab79f7..2da4c83a42 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectMerging.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectMerging.cs @@ -231,6 +231,138 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor (pos: circle2.Position, pathType: null))); } + [Test] + public void TestMergeSliderSliderSameStartTime() + { + Slider? slider1 = null; + SliderPath? slider1Path = null; + Slider? slider2 = null; + + AddStep("select two sliders", () => + { + slider1 = (Slider)EditorBeatmap.HitObjects.First(h => h is Slider); + slider1Path = new SliderPath(slider1.Path.ControlPoints.Select(p => new PathControlPoint(p.Position, p.Type)).ToArray(), slider1.Path.ExpectedDistance.Value); + slider2 = (Slider)EditorBeatmap.HitObjects.First(h => h is Slider && h.StartTime > slider1.StartTime); + EditorClock.Seek(slider1.StartTime); + EditorBeatmap.SelectedHitObjects.AddRange([slider1, slider2]); + }); + + AddStep("move sliders to the same start time", () => + { + slider2!.StartTime = slider1!.StartTime; + }); + + mergeSelection(); + + AddAssert("slider created", () => + { + if (slider1 is null || slider2 is null || slider1Path is null) + return false; + + var controlPoints1 = slider1Path.ControlPoints; + var controlPoints2 = slider2.Path.ControlPoints; + (Vector2, PathType?)[] args = new (Vector2, PathType?)[controlPoints1.Count + controlPoints2.Count - 1]; + + for (int i = 0; i < controlPoints1.Count - 1; i++) + { + args[i] = (controlPoints1[i].Position + slider1.Position, controlPoints1[i].Type); + } + + for (int i = 0; i < controlPoints2.Count; i++) + { + args[i + controlPoints1.Count - 1] = (controlPoints2[i].Position + controlPoints1[^1].Position + slider1.Position, controlPoints2[i].Type); + } + + return sliderCreatedFor(args); + }); + + AddAssert("samples exist", sliderSampleExist); + + AddAssert("merged slider matches first slider", () => + { + var mergedSlider = (Slider)EditorBeatmap.SelectedHitObjects.First(); + return slider1 is not null && mergedSlider.HeadCircle.Samples.SequenceEqual(slider1.HeadCircle.Samples) + && mergedSlider.TailCircle.Samples.SequenceEqual(slider1.TailCircle.Samples) + && mergedSlider.Samples.SequenceEqual(slider1.Samples); + }); + + AddAssert("slider end is at same completion for last slider", () => + { + if (slider1Path is null || slider2 is null) + return false; + + var mergedSlider = (Slider)EditorBeatmap.SelectedHitObjects.First(); + return Precision.AlmostEquals(mergedSlider.Path.Distance, slider1Path.CalculatedDistance + slider2.Path.Distance); + }); + } + + [Test] + public void TestMergeSliderSliderSameStartAndEndTime() + { + Slider? slider1 = null; + SliderPath? slider1Path = null; + Slider? slider2 = null; + + AddStep("select two sliders", () => + { + slider1 = (Slider)EditorBeatmap.HitObjects.First(h => h is Slider); + slider1Path = new SliderPath(slider1.Path.ControlPoints.Select(p => new PathControlPoint(p.Position, p.Type)).ToArray(), slider1.Path.ExpectedDistance.Value); + slider2 = (Slider)EditorBeatmap.HitObjects.First(h => h is Slider && h.StartTime > slider1.StartTime); + EditorClock.Seek(slider1.StartTime); + EditorBeatmap.SelectedHitObjects.AddRange([slider1, slider2]); + }); + + AddStep("move sliders to the same start & end time", () => + { + slider2!.StartTime = slider1!.StartTime; + slider2.Path = slider1.Path; + }); + + mergeSelection(); + + AddAssert("slider created", () => + { + if (slider1 is null || slider2 is null || slider1Path is null) + return false; + + var controlPoints1 = slider1Path.ControlPoints; + var controlPoints2 = slider2.Path.ControlPoints; + (Vector2, PathType?)[] args = new (Vector2, PathType?)[controlPoints1.Count + controlPoints2.Count - 1]; + + for (int i = 0; i < controlPoints1.Count - 1; i++) + { + args[i] = (controlPoints1[i].Position + slider1.Position, controlPoints1[i].Type); + } + + for (int i = 0; i < controlPoints2.Count; i++) + { + args[i + controlPoints1.Count - 1] = (controlPoints2[i].Position + controlPoints1[^1].Position + slider1.Position, controlPoints2[i].Type); + } + + return sliderCreatedFor(args); + }); + + AddAssert("samples exist", sliderSampleExist); + + AddAssert("merged slider matches first slider", () => + { + var mergedSlider = (Slider)EditorBeatmap.SelectedHitObjects.First(); + return slider1 is not null && mergedSlider.HeadCircle.Samples.SequenceEqual(slider1.HeadCircle.Samples) + && mergedSlider.TailCircle.Samples.SequenceEqual(slider1.TailCircle.Samples) + && mergedSlider.Samples.SequenceEqual(slider1.Samples); + }); + + AddAssert("slider end is at same completion for last slider", () => + { + if (slider1Path is null || slider2 is null) + return false; + + var mergedSlider = (Slider)EditorBeatmap.SelectedHitObjects.First(); + return Precision.AlmostEquals(mergedSlider.Path.Distance, slider1Path.CalculatedDistance + slider2.Path.Distance); + }); + } + + private void mergeSelection() { AddStep("merge selection", () => diff --git a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs index 2d6e234e57..c66be90605 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs @@ -512,7 +512,9 @@ namespace osu.Game.Screens.Edit.Compose.Components protected virtual void OnBlueprintDeselected(SelectionBlueprint blueprint) { - SelectionBlueprints.ChangeChildDepth(blueprint, 0); + if (SelectionBlueprints.Contains(blueprint)) + SelectionBlueprints.ChangeChildDepth(blueprint, 0); + SelectionHandler.HandleDeselected(blueprint); } From 9315aefe41a48501aa6ad222df849d0dd9b3dc89 Mon Sep 17 00:00:00 2001 From: Arthur Araujo Date: Tue, 2 Apr 2024 08:48:01 -0300 Subject: [PATCH 0489/2493] Fix remove additional blank line --- osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectMerging.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectMerging.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectMerging.cs index 2da4c83a42..76982b05a7 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectMerging.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectMerging.cs @@ -362,7 +362,6 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor }); } - private void mergeSelection() { AddStep("merge selection", () => From 94cbe1838fff6ce48d01fccea2ba14ac9c5b2067 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 3 Apr 2024 01:50:39 +0800 Subject: [PATCH 0490/2493] Replace usages of `is null` with `== null` --- .../Editor/TestSceneObjectMerging.cs | 14 +++++++------- .../Editor/TestSceneSliderSplitting.cs | 6 +++--- .../Blueprints/Sliders/SliderSelectionBlueprint.cs | 2 +- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 2 +- .../Objects/Legacy/ConvertHitObjectParser.cs | 2 +- osu.Game/Rulesets/Objects/PathControlPoint.cs | 2 +- osu.Game/Rulesets/Objects/SliderPathExtensions.cs | 4 ++-- osu.Game/Rulesets/UI/ModIcon.cs | 2 +- 8 files changed, 17 insertions(+), 17 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectMerging.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectMerging.cs index 76982b05a7..dfe950c01e 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectMerging.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectMerging.cs @@ -68,7 +68,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor AddAssert("slider created", () => { - if (circle1 is null || circle2 is null || slider is null) + if (circle1 == null || circle2 == null || slider == null) return false; var controlPoints = slider.Path.ControlPoints; @@ -111,7 +111,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor AddAssert("slider created", () => { - if (slider1 is null || slider2 is null || slider1Path is null) + if (slider1 == null || slider2 == null || slider1Path == null) return false; var controlPoints1 = slider1Path.ControlPoints; @@ -143,7 +143,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor AddAssert("slider end is at same completion for last slider", () => { - if (slider1Path is null || slider2 is null) + if (slider1Path == null || slider2 == null) return false; var mergedSlider = (Slider)EditorBeatmap.SelectedHitObjects.First(); @@ -256,7 +256,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor AddAssert("slider created", () => { - if (slider1 is null || slider2 is null || slider1Path is null) + if (slider1 == null || slider2 == null || slider1Path == null) return false; var controlPoints1 = slider1Path.ControlPoints; @@ -288,7 +288,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor AddAssert("slider end is at same completion for last slider", () => { - if (slider1Path is null || slider2 is null) + if (slider1Path == null || slider2 == null) return false; var mergedSlider = (Slider)EditorBeatmap.SelectedHitObjects.First(); @@ -322,7 +322,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor AddAssert("slider created", () => { - if (slider1 is null || slider2 is null || slider1Path is null) + if (slider1 == null || slider2 == null || slider1Path == null) return false; var controlPoints1 = slider1Path.ControlPoints; @@ -354,7 +354,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor AddAssert("slider end is at same completion for last slider", () => { - if (slider1Path is null || slider2 is null) + if (slider1Path == null || slider2 == null) return false; var mergedSlider = (Slider)EditorBeatmap.SelectedHitObjects.First(); diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSplitting.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSplitting.cs index 6c7733e68a..d68cbe6265 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSplitting.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSplitting.cs @@ -178,7 +178,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor AddStep("add hitsounds", () => { - if (slider is null) return; + if (slider == null) return; sample = new HitSampleInfo("hitwhistle", HitSampleInfo.BANK_SOFT, volume: 70); slider.Samples.Add(sample.With()); @@ -228,7 +228,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor { AddStep($"move mouse to control point {index}", () => { - if (slider is null || visualiser is null) return; + if (slider == null || visualiser == null) return; Vector2 position = slider.Path.ControlPoints[index].Position + slider.Position; InputManager.MoveMouseTo(visualiser.Pieces[0].Parent!.ToScreenSpace(position)); @@ -239,7 +239,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor { AddStep($"click context menu item \"{contextMenuText}\"", () => { - if (visualiser is null) return; + if (visualiser == null) return; MenuItem? item = visualiser.ContextMenuItems?.FirstOrDefault(menuItem => menuItem.Text.Value == contextMenuText); diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs index 4d2b980c23..2da462caf4 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs @@ -311,7 +311,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders foreach (var splitPoint in controlPointsToSplitAt) { - if (splitPoint == controlPoints[0] || splitPoint == controlPoints[^1] || splitPoint.Type is null) + if (splitPoint == controlPoints[0] || splitPoint == controlPoints[^1] || splitPoint.Type == null) continue; // Split off the section of slider before this control point so the remaining control points to split are in the latter part of the slider. diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index 53c24dc828..3325cfe407 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -781,7 +781,7 @@ namespace osu.Game.Overlays.Mods /// > public bool OnPressed(KeyBindingPressEvent e) { - if (e.Repeat || e.Action != PlatformAction.SelectAll || SelectAllModsButton is null) + if (e.Repeat || e.Action != PlatformAction.SelectAll || SelectAllModsButton == null) return false; SelectAllModsButton.TriggerClick(); diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs index 283a59b7ed..66b3033f90 100644 --- a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs @@ -347,7 +347,7 @@ namespace osu.Game.Rulesets.Objects.Legacy // Edge-case rules (to match stable). if (type == PathType.PERFECT_CURVE) { - int endPointLength = endPoint is null ? 0 : 1; + int endPointLength = endPoint == null ? 0 : 1; if (vertices.Length + endPointLength != 3) type = PathType.BEZIER; diff --git a/osu.Game/Rulesets/Objects/PathControlPoint.cs b/osu.Game/Rulesets/Objects/PathControlPoint.cs index 1f8e63b269..32245e9080 100644 --- a/osu.Game/Rulesets/Objects/PathControlPoint.cs +++ b/osu.Game/Rulesets/Objects/PathControlPoint.cs @@ -77,7 +77,7 @@ namespace osu.Game.Rulesets.Objects public bool Equals(PathControlPoint other) => Position == other?.Position && Type == other.Type; - public override string ToString() => type is null + public override string ToString() => type == null ? $"Position={Position}" : $"Position={Position}, Type={type}"; } diff --git a/osu.Game/Rulesets/Objects/SliderPathExtensions.cs b/osu.Game/Rulesets/Objects/SliderPathExtensions.cs index 29b34ae4f0..c03d3646da 100644 --- a/osu.Game/Rulesets/Objects/SliderPathExtensions.cs +++ b/osu.Game/Rulesets/Objects/SliderPathExtensions.cs @@ -29,7 +29,7 @@ namespace osu.Game.Rulesets.Objects { var controlPoints = sliderPath.ControlPoints; - var inheritedLinearPoints = controlPoints.Where(p => sliderPath.PointsInSegment(p)[0].Type == PathType.LINEAR && p.Type is null).ToList(); + var inheritedLinearPoints = controlPoints.Where(p => sliderPath.PointsInSegment(p)[0].Type == PathType.LINEAR && p.Type == null).ToList(); // Inherited points after a linear point, as well as the first control point if it inherited, // should be treated as linear points, so their types are temporarily changed to linear. @@ -53,7 +53,7 @@ namespace osu.Game.Rulesets.Objects inheritedLinearPoints.ForEach(p => p.Type = null); // Recalculate middle perfect curve control points at the end of the slider path. - if (controlPoints.Count >= 3 && controlPoints[^3].Type == PathType.PERFECT_CURVE && controlPoints[^2].Type is null && segmentEnds.Any()) + if (controlPoints.Count >= 3 && controlPoints[^3].Type == PathType.PERFECT_CURVE && controlPoints[^2].Type == null && segmentEnds.Any()) { double lastSegmentStart = segmentEnds.Length > 1 ? segmentEnds[^2] : 0; double lastSegmentEnd = segmentEnds[^1]; diff --git a/osu.Game/Rulesets/UI/ModIcon.cs b/osu.Game/Rulesets/UI/ModIcon.cs index d1776c5c0b..5d9fafd60c 100644 --- a/osu.Game/Rulesets/UI/ModIcon.cs +++ b/osu.Game/Rulesets/UI/ModIcon.cs @@ -177,7 +177,7 @@ namespace osu.Game.Rulesets.UI modAcronym.Text = value.Acronym; modIcon.Icon = value.Icon ?? FontAwesome.Solid.Question; - if (value.Icon is null) + if (value.Icon == null) { modIcon.FadeOut(); modAcronym.FadeIn(); From b5adcf2e0e8f94056af1f868d3fcc1baf37f08c0 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 3 Apr 2024 17:32:02 +0900 Subject: [PATCH 0491/2493] Fix SpectatorClient holding references to Player --- osu.Game/Online/Spectator/SpectatorClient.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Online/Spectator/SpectatorClient.cs b/osu.Game/Online/Spectator/SpectatorClient.cs index 07ee9115d6..fb7a3d13ca 100644 --- a/osu.Game/Online/Spectator/SpectatorClient.cs +++ b/osu.Game/Online/Spectator/SpectatorClient.cs @@ -248,6 +248,9 @@ namespace osu.Game.Online.Spectator isPlaying = false; currentBeatmap = null; + currentScore = null; + currentScoreProcessor = null; + currentScoreToken = null; if (state.HasPassed) currentState.State = SpectatedUserState.Passed; From ce68f6adb76c909af038284621d410e720beae61 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 3 Apr 2024 17:46:26 +0900 Subject: [PATCH 0492/2493] Fix SkinEditor binding event to external bindable --- osu.Game/Overlays/SkinEditor/SkinEditor.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/SkinEditor/SkinEditor.cs b/osu.Game/Overlays/SkinEditor/SkinEditor.cs index d3af928907..ac9649bcba 100644 --- a/osu.Game/Overlays/SkinEditor/SkinEditor.cs +++ b/osu.Game/Overlays/SkinEditor/SkinEditor.cs @@ -52,6 +52,7 @@ namespace osu.Game.Overlays.SkinEditor private OsuTextFlowContainer headerText = null!; private Bindable currentSkin = null!; + private Bindable clipboardContent = null!; [Resolved] private OsuGame? game { get; set; } @@ -243,7 +244,8 @@ namespace osu.Game.Overlays.SkinEditor canCopy.Value = canCut.Value = SelectedComponents.Any(); }, true); - clipboard.Content.BindValueChanged(content => canPaste.Value = !string.IsNullOrEmpty(content.NewValue), true); + clipboardContent = clipboard.Content.GetBoundCopy(); + clipboardContent.BindValueChanged(content => canPaste.Value = !string.IsNullOrEmpty(content.NewValue), true); Show(); From 05fe8968d88f763621afe3042d665afaa8934331 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 3 Apr 2024 11:39:12 +0200 Subject: [PATCH 0493/2493] Only interact with clipboard via bound copy --- osu.Game/Overlays/SkinEditor/SkinEditor.cs | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/osu.Game/Overlays/SkinEditor/SkinEditor.cs b/osu.Game/Overlays/SkinEditor/SkinEditor.cs index ac9649bcba..619eac8f4a 100644 --- a/osu.Game/Overlays/SkinEditor/SkinEditor.cs +++ b/osu.Game/Overlays/SkinEditor/SkinEditor.cs @@ -66,9 +66,6 @@ namespace osu.Game.Overlays.SkinEditor [Resolved] private RealmAccess realm { get; set; } = null!; - [Resolved] - private EditorClipboard clipboard { get; set; } = null!; - [Resolved] private SkinEditorOverlay? skinEditorOverlay { get; set; } @@ -114,7 +111,7 @@ namespace osu.Game.Overlays.SkinEditor } [BackgroundDependencyLoader] - private void load() + private void load(EditorClipboard clipboard) { RelativeSizeAxes = Axes.Both; @@ -225,6 +222,8 @@ namespace osu.Game.Overlays.SkinEditor } } }; + + clipboardContent = clipboard.Content.GetBoundCopy(); } protected override void LoadComplete() @@ -244,7 +243,6 @@ namespace osu.Game.Overlays.SkinEditor canCopy.Value = canCut.Value = SelectedComponents.Any(); }, true); - clipboardContent = clipboard.Content.GetBoundCopy(); clipboardContent.BindValueChanged(content => canPaste.Value = !string.IsNullOrEmpty(content.NewValue), true); Show(); @@ -497,7 +495,7 @@ namespace osu.Game.Overlays.SkinEditor protected void Copy() { - clipboard.Content.Value = JsonConvert.SerializeObject(SelectedComponents.Cast().Select(s => s.CreateSerialisedInfo()).ToArray()); + clipboardContent.Value = JsonConvert.SerializeObject(SelectedComponents.Cast().Select(s => s.CreateSerialisedInfo()).ToArray()); } protected void Clone() @@ -517,7 +515,7 @@ namespace osu.Game.Overlays.SkinEditor changeHandler?.BeginChange(); - var drawableInfo = JsonConvert.DeserializeObject(clipboard.Content.Value); + var drawableInfo = JsonConvert.DeserializeObject(clipboardContent.Value); if (drawableInfo == null) return; From 16276dfcd6ce5c35d50fa0c63877ff293c2bbdd0 Mon Sep 17 00:00:00 2001 From: Mafalda Fernandes Date: Mon, 1 Apr 2024 19:21:05 +0100 Subject: [PATCH 0494/2493] Fix #27105: Mod search box doesnt track external focus changes In the Mod selection area, the search bar's focus could be changed by pressing TAB. However, when clicking outside of the search bar, the focus would be killed but two TABs were required to get the focus back on the search bar. This happened because the action of clicking in an empty area would trigger the search bar to change its appearence, but not its internal state. In my solution, I made the OnClick function aware of the search bar's state, so it would not only change its appearance, but also its state. Now, after clicking in an empty area, there is only needed one TAB to select the search box again, as expected. --- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index 3325cfe407..5ca26c739e 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -420,7 +420,7 @@ namespace osu.Game.Overlays.Mods yield return new ColumnDimContainer(new ModPresetColumn { Margin = new MarginPadding { Right = 10 } - }); + }, this); } yield return createModColumnContent(ModType.DifficultyReduction); @@ -438,7 +438,7 @@ namespace osu.Game.Overlays.Mods column.Margin = new MarginPadding { Right = 10 }; }); - return new ColumnDimContainer(column); + return new ColumnDimContainer(column, this); } private void createLocalMods() @@ -899,13 +899,17 @@ namespace osu.Game.Overlays.Mods [Resolved] private OsuColour colours { get; set; } = null!; - public ColumnDimContainer(ModSelectColumn column) + private ModSelectOverlay modSelectOverlayInstance; + + public ColumnDimContainer(ModSelectColumn column, ModSelectOverlay modSelectOverlay) { AutoSizeAxes = Axes.X; RelativeSizeAxes = Axes.Y; Child = Column = column; column.Active.BindTo(Active); + + this.modSelectOverlayInstance = modSelectOverlay; } [BackgroundDependencyLoader] @@ -953,7 +957,7 @@ namespace osu.Game.Overlays.Mods RequestScroll?.Invoke(this); // Killing focus is done here because it's the only feasible place on ModSelectOverlay you can click on without triggering any action. - Scheduler.Add(() => GetContainingInputManager().ChangeFocus(null)); + modSelectOverlayInstance.setTextBoxFocus(false); return true; } From 524a5815bc3bf0842e6161efe7719abea550713f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 3 Apr 2024 16:11:23 +0200 Subject: [PATCH 0495/2493] Add test coverage --- .../Gameplay/TestSceneStoryboardWithIntro.cs | 89 +++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardWithIntro.cs diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardWithIntro.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardWithIntro.cs new file mode 100644 index 0000000000..502a0de616 --- /dev/null +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardWithIntro.cs @@ -0,0 +1,89 @@ +// 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.Game.Beatmaps; +using osu.Game.Configuration; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Screens.Play; +using osu.Game.Storyboards; +using osuTK; + +namespace osu.Game.Tests.Visual.Gameplay +{ + public partial class TestSceneStoryboardWithIntro : PlayerTestScene + { + protected override bool HasCustomSteps => true; + protected override bool AllowFail => true; + + protected override Ruleset CreatePlayerRuleset() => new OsuRuleset(); + + protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) + { + var beatmap = new Beatmap(); + beatmap.HitObjects.Add(new HitCircle { StartTime = firstObjectStartTime }); + return beatmap; + } + + protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard? storyboard = null) + { + return base.CreateWorkingBeatmap(beatmap, createStoryboard(storyboardStartTime)); + } + + private Storyboard createStoryboard(double startTime) + { + var storyboard = new Storyboard(); + var sprite = new StoryboardSprite("unknown", Anchor.TopLeft, Vector2.Zero); + sprite.TimelineGroup.Alpha.Add(Easing.None, startTime, 0, 0, 1); + storyboard.GetLayer("Background").Add(sprite); + return storyboard; + } + + private double firstObjectStartTime; + private double storyboardStartTime; + + [SetUpSteps] + public override void SetUpSteps() + { + base.SetUpSteps(); + AddStep("enable storyboard", () => LocalConfig.SetValue(OsuSetting.ShowStoryboard, true)); + AddStep("set dim level to 0", () => LocalConfig.SetValue(OsuSetting.DimLevel, 0)); + AddStep("reset first hitobject time", () => firstObjectStartTime = 0); + AddStep("reset storyboard start time", () => storyboardStartTime = 0); + } + + [TestCase(-5000, 0)] + [TestCase(-5000, 30000)] + public void TestStoryboardSingleSkip(double storyboardStart, double firstObject) + { + AddStep($"set storyboard start time to {storyboardStart}", () => storyboardStartTime = storyboardStart); + AddStep($"set first object start time to {firstObject}", () => firstObjectStartTime = firstObject); + CreateTest(); + + AddStep("skip", () => InputManager.Key(osuTK.Input.Key.Space)); + AddAssert("skip performed", () => Player.ChildrenOfType().Any(s => s.SkipCount == 1)); + AddUntilStep("gameplay clock advanced", () => Player.GameplayClockContainer.CurrentTime, () => Is.GreaterThanOrEqualTo(firstObject - 2000)); + } + + [Test] + public void TestStoryboardDoubleSkip() + { + AddStep("set storyboard start time to -11000", () => storyboardStartTime = -11000); + AddStep("set first object start time to 11000", () => firstObjectStartTime = 11000); + CreateTest(); + + AddStep("skip", () => InputManager.Key(osuTK.Input.Key.Space)); + AddAssert("skip performed", () => Player.ChildrenOfType().Any(s => s.SkipCount == 1)); + AddUntilStep("gameplay clock advanced", () => Player.GameplayClockContainer.CurrentTime, () => Is.GreaterThanOrEqualTo(0)); + + AddStep("skip", () => InputManager.Key(osuTK.Input.Key.Space)); + AddAssert("skip performed", () => Player.ChildrenOfType().Any(s => s.SkipCount == 2)); + AddUntilStep("gameplay clock advanced", () => Player.GameplayClockContainer.CurrentTime, () => Is.GreaterThanOrEqualTo(9000)); + } + } +} From 9d54f1a09270b9d9758a2df837c958476e6ec16b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 3 Apr 2024 16:12:20 +0200 Subject: [PATCH 0496/2493] Fix some maps requiring multiple intro skips that weren't there on stable Closes https://github.com/ppy/osu/issues/25633. The reason why that particular beatmap did not have a double skip on stable is here: https://github.com/peppy/osu-stable-reference/blob/e53980dd76857ee899f66ce519ba1597e7874f28/osu!/GameModes/Play/Player.cs#L1761-L1770 The particular place of interest is the `leadInTime < 10000` check. If `leadInTime < 10000`, then `leadIn == leadInTime`, and it turns out that `AudioEngine.Time` will always be more than or equal to `leadIn`, because it's also the gameplay start time: https://github.com/peppy/osu-stable-reference/blob/e53980dd76857ee899f66ce519ba1597e7874f28/osu!/GameModes/Play/Player.cs#L2765 This essentially means that if the `leadInTime` is less than 10000, that particular check is just dead. So a double skip can only occur if the gameplay starts at time -10000 or earlier due to the storyboard. --- osu.Game/Screens/Play/MasterGameplayClockContainer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs index 93bdcb1cab..b2f0ae5561 100644 --- a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs +++ b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs @@ -124,7 +124,7 @@ namespace osu.Game.Screens.Play double skipTarget = skipTargetTime - MINIMUM_SKIP_TIME; - if (GameplayClock.CurrentTime < 0 && skipTarget > 6000) + if (StartTime < -10000 && GameplayClock.CurrentTime < 0 && skipTarget > 6000) // double skip exception for storyboards with very long intros skipTarget = 0; From 8e00368f7cf7a4be473d18316ca4fe56080918eb Mon Sep 17 00:00:00 2001 From: Arthur Araujo Date: Wed, 3 Apr 2024 11:30:14 -0300 Subject: [PATCH 0497/2493] Add custom message in the case of a invalid beatmap_hash --- osu.Game/Screens/Play/SubmittingPlayer.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/Screens/Play/SubmittingPlayer.cs b/osu.Game/Screens/Play/SubmittingPlayer.cs index 62226c46dd..ce8260a52f 100644 --- a/osu.Game/Screens/Play/SubmittingPlayer.cs +++ b/osu.Game/Screens/Play/SubmittingPlayer.cs @@ -152,6 +152,10 @@ namespace osu.Game.Screens.Play Logger.Log($"Please ensure that you are using the latest version of the official game releases.\n\n{whatWillHappen}", level: LogLevel.Important); break; + case @"invalid beatmap hash": + Logger.Log($"A new version of this beatmapset is available please update. \n\n{whatWillHappen}", level: LogLevel.Important); + break; + case @"expired token": Logger.Log($"Your system clock is set incorrectly. Please check your system time, date and timezone.\n\n{whatWillHappen}", level: LogLevel.Important); break; From 9e92ebaa437c8adbbd2a406e8a493f8443d9d05d Mon Sep 17 00:00:00 2001 From: Arthur Araujo <90941580+64ArthurAraujo@users.noreply.github.com> Date: Wed, 3 Apr 2024 19:15:22 -0300 Subject: [PATCH 0498/2493] Make message more general Co-authored-by: Walavouchey <36758269+Walavouchey@users.noreply.github.com> --- osu.Game/Screens/Play/SubmittingPlayer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/SubmittingPlayer.cs b/osu.Game/Screens/Play/SubmittingPlayer.cs index ce8260a52f..8ccfd039ec 100644 --- a/osu.Game/Screens/Play/SubmittingPlayer.cs +++ b/osu.Game/Screens/Play/SubmittingPlayer.cs @@ -153,7 +153,7 @@ namespace osu.Game.Screens.Play break; case @"invalid beatmap hash": - Logger.Log($"A new version of this beatmapset is available please update. \n\n{whatWillHappen}", level: LogLevel.Important); + Logger.Log($"This beatmap does not match the online version. Please update or redownload it.\n\n{whatWillHappen}", level: LogLevel.Important); break; case @"expired token": From 9521c1e3e42e4c73fd94f840ce41585ff0552570 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 4 Apr 2024 14:31:40 +0800 Subject: [PATCH 0499/2493] Update hit error metre to use new icons - [ ] Depends on https://github.com/ppy/osu-resources/pull/317. --- osu.Game/Graphics/OsuIcon.cs | 8 ++++++++ .../Screens/Play/HUD/HitErrorMeters/BarHitErrorMeter.cs | 4 ++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/osu.Game/Graphics/OsuIcon.cs b/osu.Game/Graphics/OsuIcon.cs index 3cd10b1315..32e780f11c 100644 --- a/osu.Game/Graphics/OsuIcon.cs +++ b/osu.Game/Graphics/OsuIcon.cs @@ -175,6 +175,8 @@ namespace osu.Game.Graphics public static IconUsage EditorSelect => get(OsuIconMapping.EditorSelect); public static IconUsage EditorSound => get(OsuIconMapping.EditorSound); public static IconUsage EditorWhistle => get(OsuIconMapping.EditorWhistle); + public static IconUsage Tortoise => get(OsuIconMapping.Tortoise); + public static IconUsage Hare => get(OsuIconMapping.Hare); private static IconUsage get(OsuIconMapping glyph) => new IconUsage((char)glyph, FONT_NAME); @@ -380,6 +382,12 @@ namespace osu.Game.Graphics [Description(@"Editor/whistle")] EditorWhistle, + + [Description(@"tortoise")] + Tortoise, + + [Description(@"hare")] + Hare, } public class OsuIconStore : ITextureStore, ITexturedGlyphLookupStore diff --git a/osu.Game/Screens/Play/HUD/HitErrorMeters/BarHitErrorMeter.cs b/osu.Game/Screens/Play/HUD/HitErrorMeters/BarHitErrorMeter.cs index 443863fb2f..a71a46ec2a 100644 --- a/osu.Game/Screens/Play/HUD/HitErrorMeters/BarHitErrorMeter.cs +++ b/osu.Game/Screens/Play/HUD/HitErrorMeters/BarHitErrorMeter.cs @@ -303,13 +303,13 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters labelEarly.Child = new SpriteIcon { Size = new Vector2(icon_size), - Icon = FontAwesome.Solid.ShippingFast, + Icon = OsuIcon.Hare }; labelLate.Child = new SpriteIcon { Size = new Vector2(icon_size), - Icon = FontAwesome.Solid.Bicycle, + Icon = OsuIcon.Tortoise }; break; From cd474de61f963c924d42291a8872ae7c6ffa0988 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 4 Apr 2024 15:55:05 +0900 Subject: [PATCH 0500/2493] Update osu.Framework package --- 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 4901f30d8a..2d7a9d2652 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -10,7 +10,7 @@ true - + diff --git a/osu.iOS.props b/osu.iOS.props index 6b63bfa1e2..b2e3fc0779 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -23,6 +23,6 @@ iossimulator-x64 - + From 10d1308a0a1b69a463be814639425b4967191689 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 4 Apr 2024 15:05:59 +0800 Subject: [PATCH 0501/2493] 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 0e091dbd37..feaf47b809 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -36,7 +36,7 @@ - + From fb8fb4f34e2e7ef325248aaf264f8035a05795fc Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 4 Apr 2024 16:43:26 +0900 Subject: [PATCH 0502/2493] Disable Discord URI registration on macOS for now --- osu.Desktop/DiscordRichPresence.cs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/osu.Desktop/DiscordRichPresence.cs b/osu.Desktop/DiscordRichPresence.cs index 6e8554d617..7553924d1b 100644 --- a/osu.Desktop/DiscordRichPresence.cs +++ b/osu.Desktop/DiscordRichPresence.cs @@ -6,6 +6,7 @@ using System.Text; using DiscordRPC; using DiscordRPC.Message; using Newtonsoft.Json; +using osu.Framework; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.ObjectExtensions; @@ -78,9 +79,13 @@ namespace osu.Desktop client.OnError += (_, e) => Logger.Log($"An error occurred with Discord RPC Client: {e.Message} ({e.Code})", LoggingTarget.Network, LogLevel.Error); // A URI scheme is required to support game invitations, as well as informing Discord of the game executable path to support launching the game when a user clicks on join/spectate. - client.RegisterUriScheme(); - client.Subscribe(EventType.Join); - client.OnJoin += onJoin; + // The library doesn't properly support URI registration when ran from an app bundle on macOS. + if (!RuntimeInfo.IsApple) + { + client.RegisterUriScheme(); + client.Subscribe(EventType.Join); + client.OnJoin += onJoin; + } config.BindWith(OsuSetting.DiscordRichPresence, privacyMode); From 7b92c725b1e4004d1e082989522906335efe4859 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 5 Apr 2024 14:45:49 +0900 Subject: [PATCH 0503/2493] Revert "Merge pull request #27454 from EVAST9919/sb-lifetime-improvements" This reverts commit 0881e7c8c1f003416cc37507ce6448f74e9d1a7f, reversing changes made to 29a37e35852649c2f88b6c917b2921950c9e2dd9. --- osu.Game/Storyboards/CommandTimelineGroup.cs | 41 +++++++++++++- osu.Game/Storyboards/StoryboardSprite.cs | 58 +------------------- 2 files changed, 41 insertions(+), 58 deletions(-) diff --git a/osu.Game/Storyboards/CommandTimelineGroup.cs b/osu.Game/Storyboards/CommandTimelineGroup.cs index c899cf77d3..0b96db6861 100644 --- a/osu.Game/Storyboards/CommandTimelineGroup.cs +++ b/osu.Game/Storyboards/CommandTimelineGroup.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 osuTK; using osuTK.Graphics; using osu.Framework.Graphics; @@ -45,10 +46,32 @@ namespace osu.Game.Storyboards } [JsonIgnore] - public double CommandsStartTime => timelines.Min(static t => t.StartTime); + public double CommandsStartTime + { + get + { + double min = double.MaxValue; + + for (int i = 0; i < timelines.Length; i++) + min = Math.Min(min, timelines[i].StartTime); + + return min; + } + } [JsonIgnore] - public double CommandsEndTime => timelines.Max(static t => t.EndTime); + public double CommandsEndTime + { + get + { + double max = double.MinValue; + + for (int i = 0; i < timelines.Length; i++) + max = Math.Max(max, timelines[i].EndTime); + + return max; + } + } [JsonIgnore] public double CommandsDuration => CommandsEndTime - CommandsStartTime; @@ -60,7 +83,19 @@ namespace osu.Game.Storyboards public virtual double EndTime => CommandsEndTime; [JsonIgnore] - public bool HasCommands => timelines.Any(static t => t.HasCommands); + public bool HasCommands + { + get + { + for (int i = 0; i < timelines.Length; i++) + { + if (timelines[i].HasCommands) + return true; + } + + return false; + } + } public virtual IEnumerable.TypedCommand> GetCommands(CommandTimelineSelector timelineSelector, double offset = 0) { diff --git a/osu.Game/Storyboards/StoryboardSprite.cs b/osu.Game/Storyboards/StoryboardSprite.cs index 4992ae128d..982185d51b 100644 --- a/osu.Game/Storyboards/StoryboardSprite.cs +++ b/osu.Game/Storyboards/StoryboardSprite.cs @@ -85,23 +85,12 @@ namespace osu.Game.Storyboards { get { - double latestEndTime = double.MaxValue; - - // Ignore the whole setup if there are loops. In theory they can be handled here too, however the logic will be overly complex. - if (loops.Count == 0) - { - // Take the minimum time of all the potential "death" reasons. - latestEndTime = calculateOptimisedEndTime(TimelineGroup); - } - - // If the logic above fails to find anything or discarded by the fact that there are loops present, latestEndTime will be double.MaxValue - // and thus conservativeEndTime will be used. - double conservativeEndTime = TimelineGroup.EndTime; + double latestEndTime = TimelineGroup.EndTime; foreach (var l in loops) - conservativeEndTime = Math.Max(conservativeEndTime, l.StartTime + l.CommandsDuration * l.TotalIterations); + latestEndTime = Math.Max(latestEndTime, l.StartTime + l.CommandsDuration * l.TotalIterations); - return Math.Min(latestEndTime, conservativeEndTime); + return latestEndTime; } } @@ -205,47 +194,6 @@ namespace osu.Game.Storyboards return commands; } - private static double calculateOptimisedEndTime(CommandTimelineGroup timelineGroup) - { - // Here we are starting from maximum value and trying to minimise the end time on each step. - // There are few solid guesses we can make using which sprite's end time can be minimised: alpha = 0, scale = 0, colour.a = 0. - double[] deathTimes = - { - double.MaxValue, // alpha - double.MaxValue, // colour alpha - double.MaxValue, // scale - double.MaxValue, // scale x - double.MaxValue, // scale y - }; - - // The loops below are following the same pattern. - // We could be using TimelineGroup.EndValue here, however it's possible to have multiple commands with 0 value in a row - // so we are saving the earliest of them. - foreach (var alphaCommand in timelineGroup.Alpha.Commands) - { - if (alphaCommand.EndValue == 0) - // commands are ordered by the start time, however end time may vary. Save the earliest. - deathTimes[0] = Math.Min(alphaCommand.EndTime, deathTimes[0]); - else - // If value isn't 0 (sprite becomes visible again), revert the saved state. - deathTimes[0] = double.MaxValue; - } - - foreach (var colourCommand in timelineGroup.Colour.Commands) - deathTimes[1] = colourCommand.EndValue.A == 0 ? Math.Min(colourCommand.EndTime, deathTimes[1]) : double.MaxValue; - - foreach (var scaleCommand in timelineGroup.Scale.Commands) - deathTimes[2] = scaleCommand.EndValue == 0 ? Math.Min(scaleCommand.EndTime, deathTimes[2]) : double.MaxValue; - - foreach (var scaleCommand in timelineGroup.VectorScale.Commands) - { - deathTimes[3] = scaleCommand.EndValue.X == 0 ? Math.Min(scaleCommand.EndTime, deathTimes[3]) : double.MaxValue; - deathTimes[4] = scaleCommand.EndValue.Y == 0 ? Math.Min(scaleCommand.EndTime, deathTimes[4]) : double.MaxValue; - } - - return deathTimes.Min(); - } - public override string ToString() => $"{Path}, {Origin}, {InitialPosition}"; From 9f5f6b5d37405787a4308db875331060f54572b4 Mon Sep 17 00:00:00 2001 From: TextAdventurer12 Date: Sat, 6 Apr 2024 21:39:27 +1300 Subject: [PATCH 0504/2493] stop capping difficult strains per note --- osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs index 84bf8e3bf6..b585a30855 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs @@ -81,7 +81,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills { double consistentTopStrain = difficulty / 10; // What would the top strain be if all strain values were identical - return objectStrains.Sum(s => Math.Pow(Math.Min(1, s / consistentTopStrain), 5)); + // Apply a power to nerf diffspikes, but only apply that power if s / adjustedDifficulty is less than 1, to prevent buffing certain spiky maps + return objectStrains.Sum(s => s >= adjustedDifficulty ? s / adjustedDifficulty : Math.Pow(s / adjustedDifficulty, 8)); } } -} +} \ No newline at end of file From fefcd17db90150b8cd18079270a47cf173f96f47 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 8 Apr 2024 22:00:05 +0900 Subject: [PATCH 0505/2493] Fix gameplay PP counter not matching results screen --- .../Difficulty/DifficultyCalculator.cs | 28 ++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs index 00c90bd317..5973a83fd3 100644 --- a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs +++ b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs @@ -108,9 +108,18 @@ namespace osu.Game.Rulesets.Difficulty var skills = CreateSkills(Beatmap, playableMods, clockRate); var progressiveBeatmap = new ProgressiveCalculationBeatmap(Beatmap); + // There is a one-to-many relationship between the hitobjects in the beatmap and the "difficulty hitobjects". + // Each iteration of the loop bellow will add at most one hitobject to the progressive beatmap, + // representing the most-parenting hitobject - the hitobject from the original beatmap. + Dictionary hitObjectParentLinks = + createHitObjectParentLinks(Beatmap) + .ToDictionary(k => k.obj, k => k.mostParentingObject); + foreach (var hitObject in getDifficultyHitObjects()) { - progressiveBeatmap.HitObjects.Add(hitObject.BaseObject); + HitObject parent = hitObjectParentLinks[hitObject.BaseObject]; + if (progressiveBeatmap.HitObjects.Count == 0 || parent != progressiveBeatmap.HitObjects[^1]) + progressiveBeatmap.HitObjects.Add(parent); foreach (var skill in skills) { @@ -122,6 +131,23 @@ namespace osu.Game.Rulesets.Difficulty } return attribs; + + static IEnumerable<(HitObject obj, HitObject mostParentingObject)> createHitObjectParentLinks(IBeatmap beatmap) + { + foreach (var link in createNestedLinks(beatmap.HitObjects, null)) + yield return link; + + static IEnumerable<(HitObject obj, HitObject mostParentingObject)> createNestedLinks(IReadOnlyList objects, [CanBeNull] HitObject parent) + { + foreach (var o in objects) + { + yield return (o, parent ?? o); + + foreach (var n in createNestedLinks(o.NestedHitObjects, parent ?? o)) + yield return n; + } + } + } } /// From 7d8fe5117834aeb296cd6be28a89effa2bb9083f Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 8 Apr 2024 23:25:45 +0900 Subject: [PATCH 0506/2493] Fix possible crash due to race in DiscordRichPresence --- osu.Desktop/DiscordRichPresence.cs | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/osu.Desktop/DiscordRichPresence.cs b/osu.Desktop/DiscordRichPresence.cs index 7553924d1b..ed9d4ca2d2 100644 --- a/osu.Desktop/DiscordRichPresence.cs +++ b/osu.Desktop/DiscordRichPresence.cs @@ -36,8 +36,6 @@ namespace osu.Desktop [Resolved] private IBindable ruleset { get; set; } = null!; - private IBindable user = null!; - [Resolved] private IAPIProvider api { get; set; } = null!; @@ -50,9 +48,11 @@ namespace osu.Desktop [Resolved] private MultiplayerClient multiplayerClient { get; set; } = null!; + [Resolved] + private OsuConfigManager config { get; set; } = null!; + private readonly IBindable status = new Bindable(); private readonly IBindable activity = new Bindable(); - private readonly Bindable privacyMode = new Bindable(); private readonly RichPresence presence = new RichPresence @@ -65,8 +65,10 @@ namespace osu.Desktop }, }; + private IBindable? user; + [BackgroundDependencyLoader] - private void load(OsuConfigManager config) + private void load() { client = new DiscordRpcClient(client_id) { @@ -87,6 +89,13 @@ namespace osu.Desktop client.OnJoin += onJoin; } + client.Initialize(); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + config.BindWith(OsuSetting.DiscordRichPresence, privacyMode); user = api.LocalUser.GetBoundCopy(); @@ -104,8 +113,6 @@ namespace osu.Desktop activity.BindValueChanged(_ => schedulePresenceUpdate()); privacyMode.BindValueChanged(_ => schedulePresenceUpdate()); multiplayerClient.RoomUpdated += onRoomUpdated; - - client.Initialize(); } private void onReady(object _, ReadyMessage __) @@ -146,6 +153,9 @@ namespace osu.Desktop private void updatePresence(bool hideIdentifiableInformation) { + if (user == null) + return; + // user activity if (activity.Value != null) { From 14c26926f328c646ee3d7d4ec28f6199976238f8 Mon Sep 17 00:00:00 2001 From: Susko3 Date: Tue, 9 Apr 2024 09:55:50 +0200 Subject: [PATCH 0507/2493] Upgrade to SDL3 --- osu.Desktop/OsuGameDesktop.cs | 11 ++++++----- osu.Desktop/Program.cs | 25 ++++++++++++++----------- 2 files changed, 20 insertions(+), 16 deletions(-) diff --git a/osu.Desktop/OsuGameDesktop.cs b/osu.Desktop/OsuGameDesktop.cs index 2b232db274..e8783c997a 100644 --- a/osu.Desktop/OsuGameDesktop.cs +++ b/osu.Desktop/OsuGameDesktop.cs @@ -22,7 +22,7 @@ using osu.Game.IPC; using osu.Game.Online.Multiplayer; using osu.Game.Performance; using osu.Game.Utils; -using SDL2; +using SDL; namespace osu.Desktop { @@ -161,7 +161,7 @@ namespace osu.Desktop host.Window.Title = Name; } - protected override BatteryInfo CreateBatteryInfo() => new SDL2BatteryInfo(); + protected override BatteryInfo CreateBatteryInfo() => new SDL3BatteryInfo(); protected override void Dispose(bool isDisposing) { @@ -170,13 +170,14 @@ namespace osu.Desktop archiveImportIPCChannel?.Dispose(); } - private class SDL2BatteryInfo : BatteryInfo + private unsafe class SDL3BatteryInfo : BatteryInfo { public override double? ChargeLevel { get { - SDL.SDL_GetPowerInfo(out _, out int percentage); + int percentage; + SDL3.SDL_GetPowerInfo(null, &percentage); if (percentage == -1) return null; @@ -185,7 +186,7 @@ namespace osu.Desktop } } - public override bool OnBattery => SDL.SDL_GetPowerInfo(out _, out _) == SDL.SDL_PowerState.SDL_POWERSTATE_ON_BATTERY; + public override bool OnBattery => SDL3.SDL_GetPowerInfo(null, null) == SDL_PowerState.SDL_POWERSTATE_ON_BATTERY; } } } diff --git a/osu.Desktop/Program.cs b/osu.Desktop/Program.cs index 2d7ec5aa5f..29b05a402f 100644 --- a/osu.Desktop/Program.cs +++ b/osu.Desktop/Program.cs @@ -13,7 +13,7 @@ using osu.Framework.Platform; using osu.Game; using osu.Game.IPC; using osu.Game.Tournament; -using SDL2; +using SDL; using Squirrel; namespace osu.Desktop @@ -52,16 +52,19 @@ namespace osu.Desktop // See https://www.mongodb.com/docs/realm/sdk/dotnet/compatibility/ if (windowsVersion.Major < 6 || (windowsVersion.Major == 6 && windowsVersion.Minor <= 2)) { - // If users running in compatibility mode becomes more of a common thing, we may want to provide better guidance or even consider - // disabling it ourselves. - // We could also better detect compatibility mode if required: - // https://stackoverflow.com/questions/10744651/how-i-can-detect-if-my-application-is-running-under-compatibility-mode#comment58183249_10744730 - SDL.SDL_ShowSimpleMessageBox(SDL.SDL_MessageBoxFlags.SDL_MESSAGEBOX_ERROR, - "Your operating system is too old to run osu!", - "This version of osu! requires at least Windows 8.1 to run.\n" - + "Please upgrade your operating system or consider using an older version of osu!.\n\n" - + "If you are running a newer version of windows, please check you don't have \"Compatibility mode\" turned on for osu!", IntPtr.Zero); - return; + unsafe + { + // If users running in compatibility mode becomes more of a common thing, we may want to provide better guidance or even consider + // disabling it ourselves. + // We could also better detect compatibility mode if required: + // https://stackoverflow.com/questions/10744651/how-i-can-detect-if-my-application-is-running-under-compatibility-mode#comment58183249_10744730 + SDL3.SDL_ShowSimpleMessageBox(SDL_MessageBoxFlags.SDL_MESSAGEBOX_ERROR, + "Your operating system is too old to run osu!"u8, + "This version of osu! requires at least Windows 8.1 to run.\n"u8 + + "Please upgrade your operating system or consider using an older version of osu!.\n\n"u8 + + "If you are running a newer version of windows, please check you don't have \"Compatibility mode\" turned on for osu!"u8, null); + return; + } } setupSquirrel(); From 6cb5bffdfc07cbd17abe6bd4ddf01148d975d928 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 10 Apr 2024 13:03:37 +0800 Subject: [PATCH 0508/2493] 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 61ad2a4f5a..3ea756a8b8 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -36,7 +36,7 @@ - + From 3e8ddbd2a9d49173139dd06f709693d0999f456e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 11 Apr 2024 11:33:16 +0800 Subject: [PATCH 0509/2493] Add new entries to dotsettings (Rider 2024.1) --- osu.sln.DotSettings | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/osu.sln.DotSettings b/osu.sln.DotSettings index 452f90ecea..dd71744bf0 100644 --- a/osu.sln.DotSettings +++ b/osu.sln.DotSettings @@ -774,9 +774,19 @@ See the LICENCE file in the repository root for full licence text. <Policy Inspect="True" Prefix="" Suffix="" Style="aa_bb" /> <Policy Inspect="True" Prefix="" Suffix="" Style="AA_BB" /> <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + <Policy><Descriptor Staticness="Static" AccessRightKinds="Private" Description="Static readonly fields (private)"><ElementKinds><Kind Name="READONLY_FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="aa_bb" /></Policy> + <Policy><Descriptor Staticness="Any" AccessRightKinds="Private" Description="Constant fields (private)"><ElementKinds><Kind Name="CONSTANT_FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="aa_bb" /></Policy> + <Policy><Descriptor Staticness="Any" AccessRightKinds="Any" Description="Type parameters"><ElementKinds><Kind Name="TYPE_PARAMETER" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /></Policy> + <Policy><Descriptor Staticness="Instance" AccessRightKinds="Private" Description="Instance fields (private)"><ElementKinds><Kind Name="FIELD" /><Kind Name="READONLY_FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="aaBb"><ExtraRule Prefix="_" Suffix="" Style="aaBb" /></Policy></Policy> + <Policy><Descriptor Staticness="Any" AccessRightKinds="Protected, ProtectedInternal, Internal, Public, PrivateProtected" Description="Constant fields (not private)"><ElementKinds><Kind Name="CONSTANT_FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="AA_BB" /></Policy> + <Policy><Descriptor Staticness="Any" AccessRightKinds="Any" Description="Local functions"><ElementKinds><Kind Name="LOCAL_FUNCTION" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /></Policy> + <Policy><Descriptor Staticness="Any" AccessRightKinds="Any" Description="Enum members"><ElementKinds><Kind Name="ENUM_MEMBER" /></ElementKinds></Descriptor><Policy Inspect="False" Prefix="" Suffix="" Style="AaBb" /></Policy> <Policy><Descriptor Staticness="Static, Instance" AccessRightKinds="Private" Description="private methods"><ElementKinds><Kind Name="ASYNC_METHOD" /><Kind Name="METHOD" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /></Policy> <Policy><Descriptor Staticness="Static, Instance" AccessRightKinds="Protected, ProtectedInternal, Internal, Public" Description="internal/protected/public methods"><ElementKinds><Kind Name="ASYNC_METHOD" /><Kind Name="METHOD" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /></Policy> <Policy><Descriptor Staticness="Static, Instance" AccessRightKinds="Private" Description="private properties"><ElementKinds><Kind Name="PROPERTY" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /></Policy> + <Policy><Descriptor Staticness="Any" AccessRightKinds="Any" Description="Local constants"><ElementKinds><Kind Name="LOCAL_CONSTANT" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="aa_bb" /></Policy> + <Policy><Descriptor Staticness="Static" AccessRightKinds="Protected, ProtectedInternal, Internal, Public, PrivateProtected" Description="Static readonly fields (not private)"><ElementKinds><Kind Name="READONLY_FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="AA_BB" /></Policy> + <Policy><Descriptor Staticness="Static" AccessRightKinds="Private" Description="Static fields (private)"><ElementKinds><Kind Name="FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /></Policy> <Policy><Descriptor Staticness="Static, Instance" AccessRightKinds="Protected, ProtectedInternal, Internal, Public" Description="internal/protected/public properties"><ElementKinds><Kind Name="PROPERTY" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /></Policy> <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> @@ -841,6 +851,7 @@ See the LICENCE file in the repository root for full licence text. True True True + True TestFolder True True From f5555b9fa4f82564b2fa17c41aa0af91ab9e177b Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 11 Apr 2024 16:53:04 +0900 Subject: [PATCH 0510/2493] Also add potentially missed intermediate parents --- osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs index 5973a83fd3..084ead30c9 100644 --- a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs +++ b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs @@ -117,9 +117,11 @@ namespace osu.Game.Rulesets.Difficulty foreach (var hitObject in getDifficultyHitObjects()) { + // Add hitobjects between the original and progressive beatmap until the current hitobject's parent appears in the progressive beatmap. + // This covers cases where hitobjects aren't assigned "difficulty" representations because they don't meaningfully contribute to the calculations. HitObject parent = hitObjectParentLinks[hitObject.BaseObject]; - if (progressiveBeatmap.HitObjects.Count == 0 || parent != progressiveBeatmap.HitObjects[^1]) - progressiveBeatmap.HitObjects.Add(parent); + while (progressiveBeatmap.HitObjects.LastOrDefault() != parent) + progressiveBeatmap.HitObjects.Add(Beatmap.HitObjects[progressiveBeatmap.HitObjects.Count]); foreach (var skill in skills) { From bf63ba3f82a367033d21f001c45d0951a36c38c4 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 11 Apr 2024 17:56:34 +0900 Subject: [PATCH 0511/2493] Add test --- .../TestSceneTimedDifficultyCalculation.cs | 191 ++++++++++++++++++ 1 file changed, 191 insertions(+) create mode 100644 osu.Game.Tests/NonVisual/TestSceneTimedDifficultyCalculation.cs diff --git a/osu.Game.Tests/NonVisual/TestSceneTimedDifficultyCalculation.cs b/osu.Game.Tests/NonVisual/TestSceneTimedDifficultyCalculation.cs new file mode 100644 index 0000000000..b0b06ce292 --- /dev/null +++ b/osu.Game.Tests/NonVisual/TestSceneTimedDifficultyCalculation.cs @@ -0,0 +1,191 @@ +// 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; +using NUnit.Framework; +using osu.Game.Beatmaps; +using osu.Game.Rulesets; +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.UI; +using osu.Game.Tests.Beatmaps; + +namespace osu.Game.Tests.NonVisual +{ + [TestFixture] + public class TestSceneTimedDifficultyCalculation + { + [Test] + public void TestAttributesGeneratedForAllNonSkippedObjects() + { + var beatmap = new Beatmap + { + HitObjects = + { + new TestHitObject(), + new TestHitObject { Nested = 1 }, + new TestHitObject(), + } + }; + + List attribs = new TestDifficultyCalculator(new TestWorkingBeatmap(beatmap)).CalculateTimed(); + + Assert.That(attribs.Count, Is.EqualTo(4)); + assertEquals(attribs[0], beatmap.HitObjects[0]); + assertEquals(attribs[1], beatmap.HitObjects[0], beatmap.HitObjects[1]); + assertEquals(attribs[2], beatmap.HitObjects[0], beatmap.HitObjects[1]); // From the nested object. + assertEquals(attribs[3], beatmap.HitObjects[0], beatmap.HitObjects[1], beatmap.HitObjects[2]); + } + + [Test] + public void TestAttributesNotGeneratedForSkippedObjects() + { + var beatmap = new Beatmap + { + HitObjects = + { + // The first object is usually skipped in all implementations + new TestHitObject { Skip = true }, + // An intermediate skipped object. + new TestHitObject { Skip = true }, + new TestHitObject(), + } + }; + + List attribs = new TestDifficultyCalculator(new TestWorkingBeatmap(beatmap)).CalculateTimed(); + + Assert.That(attribs.Count, Is.EqualTo(1)); + assertEquals(attribs[0], beatmap.HitObjects[0], beatmap.HitObjects[1], beatmap.HitObjects[2]); + } + + [Test] + public void TestNestedObjectOnlyAddsParentOnce() + { + var beatmap = new Beatmap + { + HitObjects = + { + new TestHitObject { Skip = true, Nested = 2 }, + } + }; + + List attribs = new TestDifficultyCalculator(new TestWorkingBeatmap(beatmap)).CalculateTimed(); + + Assert.That(attribs.Count, Is.EqualTo(2)); + assertEquals(attribs[0], beatmap.HitObjects[0]); + assertEquals(attribs[1], beatmap.HitObjects[0]); + } + + private void assertEquals(TimedDifficultyAttributes attribs, params HitObject[] expected) + { + Assert.That(((TestDifficultyAttributes)attribs.Attributes).Objects, Is.EquivalentTo(expected)); + } + + private class TestHitObject : HitObject + { + /// + /// Whether to skip generating a difficulty representation for this object. + /// + public bool Skip { get; set; } + + /// + /// Whether to generate nested difficulty representations for this object, and if so, how many. + /// + public int Nested { get; set; } + + protected override void CreateNestedHitObjects(CancellationToken cancellationToken) + { + for (int i = 0; i < Nested; i++) + AddNested(new TestHitObject()); + } + } + + private class TestRuleset : Ruleset + { + public override IEnumerable GetModsFor(ModType type) => Enumerable.Empty(); + + public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList? mods = null) => throw new NotImplementedException(); + + public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new PassThroughBeatmapConverter(beatmap); + + public override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => new TestDifficultyCalculator(beatmap); + + public override string Description => string.Empty; + public override string ShortName => string.Empty; + + private class PassThroughBeatmapConverter : IBeatmapConverter + { + public event Action>? ObjectConverted + { + add { } + remove { } + } + + public IBeatmap Beatmap { get; } + + public PassThroughBeatmapConverter(IBeatmap beatmap) + { + Beatmap = beatmap; + } + + public bool CanConvert() => true; + + public IBeatmap Convert(CancellationToken cancellationToken = default) => Beatmap; + } + } + + private class TestDifficultyCalculator : DifficultyCalculator + { + public TestDifficultyCalculator(IWorkingBeatmap beatmap) + : base(new TestRuleset().RulesetInfo, beatmap) + { + } + + protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate) + => new TestDifficultyAttributes { Objects = beatmap.HitObjects.ToArray() }; + + protected override IEnumerable CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) + { + List objects = new List(); + + foreach (var obj in beatmap.HitObjects.OfType()) + { + if (!obj.Skip) + objects.Add(new DifficultyHitObject(obj, obj, clockRate, objects, objects.Count)); + + foreach (var nested in obj.NestedHitObjects) + objects.Add(new DifficultyHitObject(nested, nested, clockRate, objects, objects.Count)); + } + + return objects; + } + + protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) => new Skill[] { new PassThroughSkill(mods) }; + + private class PassThroughSkill : Skill + { + public PassThroughSkill(Mod[] mods) + : base(mods) + { + } + + public override void Process(DifficultyHitObject current) + { + } + + public override double DifficultyValue() => 1; + } + } + + private class TestDifficultyAttributes : DifficultyAttributes + { + public HitObject[] Objects = Array.Empty(); + } + } +} From 19cc847be6dd9674ac374d3bfef7e9a295f08e9f Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 11 Apr 2024 23:40:40 +0900 Subject: [PATCH 0512/2493] Implement a less failure-prone method --- .../TestSceneTimedDifficultyCalculation.cs | 33 ++++++++++++---- .../Difficulty/DifficultyCalculator.cs | 39 +++++-------------- 2 files changed, 35 insertions(+), 37 deletions(-) diff --git a/osu.Game.Tests/NonVisual/TestSceneTimedDifficultyCalculation.cs b/osu.Game.Tests/NonVisual/TestSceneTimedDifficultyCalculation.cs index b0b06ce292..4c32d3bf1c 100644 --- a/osu.Game.Tests/NonVisual/TestSceneTimedDifficultyCalculation.cs +++ b/osu.Game.Tests/NonVisual/TestSceneTimedDifficultyCalculation.cs @@ -28,9 +28,13 @@ namespace osu.Game.Tests.NonVisual { HitObjects = { - new TestHitObject(), - new TestHitObject { Nested = 1 }, - new TestHitObject(), + new TestHitObject { StartTime = 1 }, + new TestHitObject + { + StartTime = 2, + Nested = 1 + }, + new TestHitObject { StartTime = 3 }, } }; @@ -51,10 +55,18 @@ namespace osu.Game.Tests.NonVisual HitObjects = { // The first object is usually skipped in all implementations - new TestHitObject { Skip = true }, + new TestHitObject + { + StartTime = 1, + Skip = true + }, // An intermediate skipped object. - new TestHitObject { Skip = true }, - new TestHitObject(), + new TestHitObject + { + StartTime = 2, + Skip = true + }, + new TestHitObject { StartTime = 3 }, } }; @@ -71,7 +83,12 @@ namespace osu.Game.Tests.NonVisual { HitObjects = { - new TestHitObject { Skip = true, Nested = 2 }, + new TestHitObject + { + StartTime = 1, + Skip = true, + Nested = 2 + }, } }; @@ -102,7 +119,7 @@ namespace osu.Game.Tests.NonVisual protected override void CreateNestedHitObjects(CancellationToken cancellationToken) { for (int i = 0; i < Nested; i++) - AddNested(new TestHitObject()); + AddNested(new TestHitObject { StartTime = StartTime + 0.1 * i }); } } diff --git a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs index 084ead30c9..5d608deae2 100644 --- a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs +++ b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs @@ -108,20 +108,18 @@ namespace osu.Game.Rulesets.Difficulty var skills = CreateSkills(Beatmap, playableMods, clockRate); var progressiveBeatmap = new ProgressiveCalculationBeatmap(Beatmap); - // There is a one-to-many relationship between the hitobjects in the beatmap and the "difficulty hitobjects". - // Each iteration of the loop bellow will add at most one hitobject to the progressive beatmap, - // representing the most-parenting hitobject - the hitobject from the original beatmap. - Dictionary hitObjectParentLinks = - createHitObjectParentLinks(Beatmap) - .ToDictionary(k => k.obj, k => k.mostParentingObject); - foreach (var hitObject in getDifficultyHitObjects()) { - // Add hitobjects between the original and progressive beatmap until the current hitobject's parent appears in the progressive beatmap. - // This covers cases where hitobjects aren't assigned "difficulty" representations because they don't meaningfully contribute to the calculations. - HitObject parent = hitObjectParentLinks[hitObject.BaseObject]; - while (progressiveBeatmap.HitObjects.LastOrDefault() != parent) - progressiveBeatmap.HitObjects.Add(Beatmap.HitObjects[progressiveBeatmap.HitObjects.Count]); + // Implementations expect the progressive beatmap to only contain top-level objects from the original beatmap. + // At the same time, we also need to consider the possibility DHOs may not be generated for any given object, + // so we'll add all remaining objects up to the current point in time to the progressive beatmap. + for (int i = progressiveBeatmap.HitObjects.Count; i < Beatmap.HitObjects.Count; i++) + { + if (Beatmap.HitObjects[i].StartTime > hitObject.BaseObject.StartTime) + break; + + progressiveBeatmap.HitObjects.Add(Beatmap.HitObjects[i]); + } foreach (var skill in skills) { @@ -133,23 +131,6 @@ namespace osu.Game.Rulesets.Difficulty } return attribs; - - static IEnumerable<(HitObject obj, HitObject mostParentingObject)> createHitObjectParentLinks(IBeatmap beatmap) - { - foreach (var link in createNestedLinks(beatmap.HitObjects, null)) - yield return link; - - static IEnumerable<(HitObject obj, HitObject mostParentingObject)> createNestedLinks(IReadOnlyList objects, [CanBeNull] HitObject parent) - { - foreach (var o in objects) - { - yield return (o, parent ?? o); - - foreach (var n in createNestedLinks(o.NestedHitObjects, parent ?? o)) - yield return n; - } - } - } } /// From e9b319f4c6e43d2488cf4ebee2c9d56f7f828b70 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 12 Apr 2024 00:11:54 +0900 Subject: [PATCH 0513/2493] Ensure all remaining objects are added in the last iteration --- .../TestSceneTimedDifficultyCalculation.cs | 27 +++++++++++++++++++ .../Difficulty/DifficultyCalculator.cs | 9 ++++--- 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/NonVisual/TestSceneTimedDifficultyCalculation.cs b/osu.Game.Tests/NonVisual/TestSceneTimedDifficultyCalculation.cs index 4c32d3bf1c..1a75f735ef 100644 --- a/osu.Game.Tests/NonVisual/TestSceneTimedDifficultyCalculation.cs +++ b/osu.Game.Tests/NonVisual/TestSceneTimedDifficultyCalculation.cs @@ -99,6 +99,33 @@ namespace osu.Game.Tests.NonVisual assertEquals(attribs[1], beatmap.HitObjects[0]); } + [Test] + public void TestSkippedLastObjectAddedInLastIteration() + { + var beatmap = new Beatmap + { + HitObjects = + { + new TestHitObject { StartTime = 1 }, + new TestHitObject + { + StartTime = 2, + Skip = true + }, + new TestHitObject + { + StartTime = 3, + Skip = true + }, + } + }; + + List attribs = new TestDifficultyCalculator(new TestWorkingBeatmap(beatmap)).CalculateTimed(); + + Assert.That(attribs.Count, Is.EqualTo(1)); + assertEquals(attribs[0], beatmap.HitObjects[0], beatmap.HitObjects[1], beatmap.HitObjects[2]); + } + private void assertEquals(TimedDifficultyAttributes attribs, params HitObject[] expected) { Assert.That(((TestDifficultyAttributes)attribs.Attributes).Objects, Is.EquivalentTo(expected)); diff --git a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs index 5d608deae2..1599dff8d9 100644 --- a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs +++ b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs @@ -107,15 +107,16 @@ namespace osu.Game.Rulesets.Difficulty var skills = CreateSkills(Beatmap, playableMods, clockRate); var progressiveBeatmap = new ProgressiveCalculationBeatmap(Beatmap); + var difficultyObjects = getDifficultyHitObjects().ToArray(); - foreach (var hitObject in getDifficultyHitObjects()) + foreach (var obj in difficultyObjects) { // Implementations expect the progressive beatmap to only contain top-level objects from the original beatmap. // At the same time, we also need to consider the possibility DHOs may not be generated for any given object, // so we'll add all remaining objects up to the current point in time to the progressive beatmap. for (int i = progressiveBeatmap.HitObjects.Count; i < Beatmap.HitObjects.Count; i++) { - if (Beatmap.HitObjects[i].StartTime > hitObject.BaseObject.StartTime) + if (obj != difficultyObjects[^1] && Beatmap.HitObjects[i].StartTime > obj.BaseObject.StartTime) break; progressiveBeatmap.HitObjects.Add(Beatmap.HitObjects[i]); @@ -124,10 +125,10 @@ namespace osu.Game.Rulesets.Difficulty foreach (var skill in skills) { cancellationToken.ThrowIfCancellationRequested(); - skill.Process(hitObject); + skill.Process(obj); } - attribs.Add(new TimedDifficultyAttributes(hitObject.EndTime * clockRate, CreateDifficultyAttributes(progressiveBeatmap, playableMods, skills, clockRate))); + attribs.Add(new TimedDifficultyAttributes(obj.EndTime * clockRate, CreateDifficultyAttributes(progressiveBeatmap, playableMods, skills, clockRate))); } return attribs; From 8b2017be453fb03905bc857bd117c2555cb05e69 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 12 Apr 2024 01:02:40 +0900 Subject: [PATCH 0514/2493] Update Sentry to fix iOS build --- osu.Game/Utils/SentryLogger.cs | 2 +- osu.Game/osu.Game.csproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Utils/SentryLogger.cs b/osu.Game/Utils/SentryLogger.cs index 61622a7122..896f4daf33 100644 --- a/osu.Game/Utils/SentryLogger.cs +++ b/osu.Game/Utils/SentryLogger.cs @@ -64,7 +64,7 @@ namespace osu.Game.Utils localUser = user.GetBoundCopy(); localUser.BindValueChanged(u => { - SentrySdk.ConfigureScope(scope => scope.User = new User + SentrySdk.ConfigureScope(scope => scope.User = new SentryUser { Username = u.NewValue.Username, Id = u.NewValue.Id.ToString(), diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 3ea756a8b8..21b5bc60a5 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -37,7 +37,7 @@ - + From 3ec93745a45127a204b1b25df81784bc1392ac2f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 12 Apr 2024 01:07:52 +0800 Subject: [PATCH 0515/2493] Fix test failures due to sentry oversight --- osu.Game/Utils/SentryLogger.cs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/osu.Game/Utils/SentryLogger.cs b/osu.Game/Utils/SentryLogger.cs index 896f4daf33..8d3e5fb834 100644 --- a/osu.Game/Utils/SentryLogger.cs +++ b/osu.Game/Utils/SentryLogger.cs @@ -39,12 +39,13 @@ namespace osu.Game.Utils public SentryLogger(OsuGame game) { this.game = game; + + if (!game.IsDeployedBuild || !game.CreateEndpoints().WebsiteRootUrl.EndsWith(@".ppy.sh", StringComparison.Ordinal)) + return; + sentrySession = SentrySdk.Init(options => { - // Not setting the dsn will completely disable sentry. - if (game.IsDeployedBuild && game.CreateEndpoints().WebsiteRootUrl.EndsWith(@".ppy.sh", StringComparison.Ordinal)) - options.Dsn = "https://ad9f78529cef40ac874afb95a9aca04e@sentry.ppy.sh/2"; - + options.Dsn = "https://ad9f78529cef40ac874afb95a9aca04e@sentry.ppy.sh/2"; options.AutoSessionTracking = true; options.IsEnvironmentUser = false; options.IsGlobalModeEnabled = true; @@ -59,6 +60,9 @@ namespace osu.Game.Utils public void AttachUser(IBindable user) { + if (sentrySession == null) + return; + Debug.Assert(localUser == null); localUser = user.GetBoundCopy(); From c0dce94f1593ef13d3d9bb214d2ef32331da1f07 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 12 Apr 2024 16:24:46 +0800 Subject: [PATCH 0516/2493] Fix newly placed items in skin editor not getting correct anchor placement --- osu.Game/Overlays/SkinEditor/SkinEditor.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game/Overlays/SkinEditor/SkinEditor.cs b/osu.Game/Overlays/SkinEditor/SkinEditor.cs index 619eac8f4a..bc929177d1 100644 --- a/osu.Game/Overlays/SkinEditor/SkinEditor.cs +++ b/osu.Game/Overlays/SkinEditor/SkinEditor.cs @@ -454,6 +454,7 @@ namespace osu.Game.Overlays.SkinEditor } SelectedComponents.Add(component); + SkinSelectionHandler.ApplyClosestAnchor(drawableComponent); return true; } @@ -666,8 +667,6 @@ namespace osu.Game.Overlays.SkinEditor SelectedComponents.Clear(); placeComponent(sprite, false); - - SkinSelectionHandler.ApplyClosestAnchor(sprite); }); return Task.CompletedTask; From b32d73ec9b7be94b3f55a7007e236cffd9228ca1 Mon Sep 17 00:00:00 2001 From: TextAdventurer12 Date: Sat, 13 Apr 2024 02:43:33 +1200 Subject: [PATCH 0517/2493] adjust weighting function --- osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs index b585a30855..c20ea732ec 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs @@ -80,9 +80,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills public double CountDifficultStrains() { double consistentTopStrain = difficulty / 10; // What would the top strain be if all strain values were identical - - // Apply a power to nerf diffspikes, but only apply that power if s / adjustedDifficulty is less than 1, to prevent buffing certain spiky maps - return objectStrains.Sum(s => s >= adjustedDifficulty ? s / adjustedDifficulty : Math.Pow(s / adjustedDifficulty, 8)); + // Use a weighted sum of all strains. Constants are arbitrary and give nice values + return objectStrains.Sum(s => 1.3 / (1 + Math.Exp(-14.15 * (Math.Pow(s / consistentTopStrain, 2) - 0.945)))); } } } \ No newline at end of file From c7f3a599c98f49e821e5790a436ff508e90ce097 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Sat, 13 Apr 2024 13:17:06 +0900 Subject: [PATCH 0518/2493] Fix crash when entering multiplayer on macOS --- osu.Desktop/DiscordRichPresence.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Desktop/DiscordRichPresence.cs b/osu.Desktop/DiscordRichPresence.cs index ed9d4ca2d2..f1c796d0cd 100644 --- a/osu.Desktop/DiscordRichPresence.cs +++ b/osu.Desktop/DiscordRichPresence.cs @@ -205,7 +205,9 @@ namespace osu.Desktop Password = room.Settings.Password, }; - presence.Secrets.JoinSecret = JsonConvert.SerializeObject(roomSecret); + if (client.HasRegisteredUriScheme) + presence.Secrets.JoinSecret = JsonConvert.SerializeObject(roomSecret); + // discord cannot handle both secrets and buttons at the same time, so we need to choose something. // the multiplayer room seems more important. presence.Buttons = null; From feb9b5bdb8a74e566e35d00ce23e492f129b6f05 Mon Sep 17 00:00:00 2001 From: Givikap120 Date: Sat, 13 Apr 2024 13:42:57 +0300 Subject: [PATCH 0519/2493] Make traceable pp match HD --- .../Difficulty/OsuPerformanceCalculator.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index 4771bce280..e7e9308eb5 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -118,8 +118,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty } else if (score.Mods.Any(h => h is OsuModTraceable)) { - // Default 2% increase and another is scaled by AR - aimValue *= 1.02 + 0.02 * (12.0 - attributes.ApproachRate); + // The same as HD, placeholder bonus + aimValue *= 1.0 + 0.04 * (12.0 - attributes.ApproachRate); } // We assume 15% of sliders in a map are difficult since there's no way to tell from the performance calculator. @@ -174,8 +174,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty } else if (score.Mods.Any(h => h is OsuModTraceable)) { - // More reward for speed because speed on Traceable is annoying - speedValue *= 1.04 + 0.06 * (12.0 - attributes.ApproachRate); + // The same as HD, placeholder bonus + speedValue *= 1.0 + 0.04 * (12.0 - attributes.ApproachRate); } // Calculate accuracy assuming the worst case scenario @@ -225,7 +225,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty else if (score.Mods.Any(m => m is OsuModHidden)) accuracyValue *= 1.08; else if (score.Mods.Any(m => m is OsuModTraceable)) - accuracyValue *= 1.02 + 0.01 * (12.0 - attributes.ApproachRate); + accuracyValue *= 1.08; if (score.Mods.Any(m => m is OsuModFlashlight)) accuracyValue *= 1.02; From 4a21ff97263ac0219905e7a4fc51b8d5e1c55705 Mon Sep 17 00:00:00 2001 From: Givikap120 Date: Sat, 13 Apr 2024 13:59:09 +0300 Subject: [PATCH 0520/2493] removed duplication --- .../Difficulty/OsuPerformanceCalculator.cs | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index e7e9308eb5..18a4b8be0c 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -111,16 +111,11 @@ namespace osu.Game.Rulesets.Osu.Difficulty if (score.Mods.Any(m => m is OsuModBlinds)) aimValue *= 1.3 + (totalHits * (0.0016 / (1 + 2 * effectiveMissCount)) * Math.Pow(accuracy, 16)) * (1 - 0.003 * attributes.DrainRate * attributes.DrainRate); - else if (score.Mods.Any(h => h is OsuModHidden)) + else if (score.Mods.Any(m => m is OsuModHidden || m is OsuModTraceable)) { // We want to give more reward for lower AR when it comes to aim and HD. This nerfs high AR and buffs lower AR. aimValue *= 1.0 + 0.04 * (12.0 - attributes.ApproachRate); } - else if (score.Mods.Any(h => h is OsuModTraceable)) - { - // The same as HD, placeholder bonus - aimValue *= 1.0 + 0.04 * (12.0 - attributes.ApproachRate); - } // We assume 15% of sliders in a map are difficult since there's no way to tell from the performance calculator. double estimateDifficultSliders = attributes.SliderCount * 0.15; @@ -167,16 +162,11 @@ namespace osu.Game.Rulesets.Osu.Difficulty // Increasing the speed value by object count for Blinds isn't ideal, so the minimum buff is given. speedValue *= 1.12; } - else if (score.Mods.Any(m => m is OsuModHidden)) + else if (score.Mods.Any(m => m is OsuModHidden || m is OsuModTraceable)) { // We want to give more reward for lower AR when it comes to aim and HD. This nerfs high AR and buffs lower AR. speedValue *= 1.0 + 0.04 * (12.0 - attributes.ApproachRate); } - else if (score.Mods.Any(h => h is OsuModTraceable)) - { - // The same as HD, placeholder bonus - speedValue *= 1.0 + 0.04 * (12.0 - attributes.ApproachRate); - } // Calculate accuracy assuming the worst case scenario double relevantTotalDiff = totalHits - attributes.SpeedNoteCount; @@ -222,9 +212,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty // Increasing the accuracy value by object count for Blinds isn't ideal, so the minimum buff is given. if (score.Mods.Any(m => m is OsuModBlinds)) accuracyValue *= 1.14; - else if (score.Mods.Any(m => m is OsuModHidden)) - accuracyValue *= 1.08; - else if (score.Mods.Any(m => m is OsuModTraceable)) + else if (score.Mods.Any(m => m is OsuModHidden || m is OsuModTraceable)) accuracyValue *= 1.08; if (score.Mods.Any(m => m is OsuModFlashlight)) From 5a8b8908dd5ba8064f87d803fb1308340139582a Mon Sep 17 00:00:00 2001 From: Loreos7 Date: Sat, 13 Apr 2024 14:53:51 +0300 Subject: [PATCH 0521/2493] fix missing underscore --- osu.Game/Screens/Play/SubmittingPlayer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/SubmittingPlayer.cs b/osu.Game/Screens/Play/SubmittingPlayer.cs index 8ccfd039ec..6c5f7fab9e 100644 --- a/osu.Game/Screens/Play/SubmittingPlayer.cs +++ b/osu.Game/Screens/Play/SubmittingPlayer.cs @@ -152,7 +152,7 @@ namespace osu.Game.Screens.Play Logger.Log($"Please ensure that you are using the latest version of the official game releases.\n\n{whatWillHappen}", level: LogLevel.Important); break; - case @"invalid beatmap hash": + case @"invalid beatmap_hash": Logger.Log($"This beatmap does not match the online version. Please update or redownload it.\n\n{whatWillHappen}", level: LogLevel.Important); break; From 9833dd955f8c09ff8455ab499257ac044362414c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Vaj=C4=8F=C3=A1k?= Date: Sun, 14 Apr 2024 01:30:59 +0200 Subject: [PATCH 0522/2493] Fix toolbar volume bar masking --- osu.Game/Overlays/Toolbar/ToolbarMusicButton.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/osu.Game/Overlays/Toolbar/ToolbarMusicButton.cs b/osu.Game/Overlays/Toolbar/ToolbarMusicButton.cs index 5da0056787..718789e3c7 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarMusicButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarMusicButton.cs @@ -21,7 +21,7 @@ namespace osu.Game.Overlays.Toolbar { public partial class ToolbarMusicButton : ToolbarOverlayToggleButton { - private Circle volumeBar; + private Box volumeBar; protected override Anchor TooltipAnchor => Anchor.TopRight; @@ -45,14 +45,15 @@ namespace osu.Game.Overlays.Toolbar Height = IconContainer.Height, Margin = new MarginPadding { Horizontal = 2.5f }, Masking = true, - Children = new[] + CornerRadius = 3f, + Children = new Drawable[] { new Circle { RelativeSizeAxes = Axes.Both, Colour = Color4.White.Opacity(0.25f), }, - volumeBar = new Circle + volumeBar = new Box { RelativeSizeAxes = Axes.Both, Height = 0f, From ed6680a61d535849beb78bd26738e55172e2aabc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Vaj=C4=8F=C3=A1k?= Date: Sun, 14 Apr 2024 15:10:05 +0200 Subject: [PATCH 0523/2493] Fixed type inconsistency and rounding --- osu.Game/Overlays/Toolbar/ToolbarMusicButton.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/osu.Game/Overlays/Toolbar/ToolbarMusicButton.cs b/osu.Game/Overlays/Toolbar/ToolbarMusicButton.cs index 718789e3c7..51b95b7d32 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarMusicButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarMusicButton.cs @@ -37,7 +37,7 @@ namespace osu.Game.Overlays.Toolbar StateContainer = music; Flow.Padding = new MarginPadding { Horizontal = Toolbar.HEIGHT / 4 }; - Flow.Add(volumeDisplay = new Container + Flow.Add(volumeDisplay = new CircularContainer { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, @@ -45,10 +45,9 @@ namespace osu.Game.Overlays.Toolbar Height = IconContainer.Height, Margin = new MarginPadding { Horizontal = 2.5f }, Masking = true, - CornerRadius = 3f, - Children = new Drawable[] + Children = new[] { - new Circle + new Box { RelativeSizeAxes = Axes.Both, Colour = Color4.White.Opacity(0.25f), From f282152f996b3e15dffa9ae7564c1e09788430dd Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Sun, 14 Apr 2024 15:53:29 -0700 Subject: [PATCH 0524/2493] Enable NRT to `ScoreManager` --- .../Database/BackgroundDataStoreProcessor.cs | 17 ++++++++++------- osu.Game/Scoring/ScoreManager.cs | 18 ++++++++---------- osu.Game/Screens/Play/SaveFailedScoreButton.cs | 2 +- 3 files changed, 19 insertions(+), 18 deletions(-) diff --git a/osu.Game/Database/BackgroundDataStoreProcessor.cs b/osu.Game/Database/BackgroundDataStoreProcessor.cs index 872194aa1d..52336c0242 100644 --- a/osu.Game/Database/BackgroundDataStoreProcessor.cs +++ b/osu.Game/Database/BackgroundDataStoreProcessor.cs @@ -287,14 +287,17 @@ namespace osu.Game.Database { var score = scoreManager.Query(s => s.ID == id); - scoreManager.PopulateMaximumStatistics(score); - - // Can't use async overload because we're not on the update thread. - // ReSharper disable once MethodHasAsyncOverload - realmAccess.Write(r => + if (score != null) { - r.Find(id)!.MaximumStatisticsJson = JsonConvert.SerializeObject(score.MaximumStatistics); - }); + scoreManager.PopulateMaximumStatistics(score); + + // Can't use async overload because we're not on the update thread. + // ReSharper disable once MethodHasAsyncOverload + realmAccess.Write(r => + { + r.Find(id)!.MaximumStatisticsJson = JsonConvert.SerializeObject(score.MaximumStatistics); + }); + } ++processedCount; } diff --git a/osu.Game/Scoring/ScoreManager.cs b/osu.Game/Scoring/ScoreManager.cs index 1ee99e9e93..1cdf4b0c13 100644 --- a/osu.Game/Scoring/ScoreManager.cs +++ b/osu.Game/Scoring/ScoreManager.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.Collections.Generic; using System.Diagnostics; @@ -28,7 +26,7 @@ namespace osu.Game.Scoring public class ScoreManager : ModelManager, IModelImporter { private readonly Func beatmaps; - private readonly OsuConfigManager configManager; + private readonly OsuConfigManager? configManager; private readonly ScoreImporter scoreImporter; private readonly LegacyScoreExporter scoreExporter; @@ -43,7 +41,7 @@ namespace osu.Game.Scoring } public ScoreManager(RulesetStore rulesets, Func beatmaps, Storage storage, RealmAccess realm, IAPIProvider api, - OsuConfigManager configManager = null) + OsuConfigManager? configManager = null) : base(storage, realm) { this.beatmaps = beatmaps; @@ -67,7 +65,7 @@ namespace osu.Game.Scoring /// /// The query. /// The first result for the provided query, or null if no results were found. - public ScoreInfo Query(Expression> query) + public ScoreInfo? Query(Expression> query) { return Realm.Run(r => r.All().FirstOrDefault(query)?.Detach()); } @@ -104,7 +102,7 @@ namespace osu.Game.Scoring /// /// The to provide the total score of. /// The config. - public TotalScoreBindable(ScoreInfo score, OsuConfigManager configManager) + public TotalScoreBindable(ScoreInfo score, OsuConfigManager? configManager) { configManager?.BindWith(OsuSetting.ScoreDisplayMode, scoringMode); scoringMode.BindValueChanged(mode => Value = score.GetDisplayScore(mode.NewValue), true); @@ -126,7 +124,7 @@ namespace osu.Game.Scoring } } - public void Delete([CanBeNull] Expression> filter = null, bool silent = false) + public void Delete(Expression>? filter = null, bool silent = false) { Realm.Run(r => { @@ -165,9 +163,9 @@ namespace osu.Game.Scoring public Task Export(ScoreInfo score) => scoreExporter.ExportAsync(score.ToLive(Realm)); - public Task> ImportAsUpdate(ProgressNotification notification, ImportTask task, ScoreInfo original) => scoreImporter.ImportAsUpdate(notification, task, original); + public Task?> ImportAsUpdate(ProgressNotification notification, ImportTask task, ScoreInfo original) => scoreImporter.ImportAsUpdate(notification, task, original); - public Live Import(ScoreInfo item, ArchiveReader archive = null, ImportParameters parameters = default, CancellationToken cancellationToken = default) => + public Live? Import(ScoreInfo item, ArchiveReader? archive = null, ImportParameters parameters = default, CancellationToken cancellationToken = default) => scoreImporter.ImportModel(item, archive, parameters, cancellationToken); /// @@ -182,7 +180,7 @@ namespace osu.Game.Scoring #region Implementation of IPresentImports - public Action>> PresentImport + public Action>>? PresentImport { set => scoreImporter.PresentImport = value; } diff --git a/osu.Game/Screens/Play/SaveFailedScoreButton.cs b/osu.Game/Screens/Play/SaveFailedScoreButton.cs index ef27aac1b9..4f665b87e8 100644 --- a/osu.Game/Screens/Play/SaveFailedScoreButton.cs +++ b/osu.Game/Screens/Play/SaveFailedScoreButton.cs @@ -137,7 +137,7 @@ namespace osu.Game.Screens.Play { if (state.NewValue != DownloadState.LocallyAvailable) return; - scoreManager.Export(importedScore); + if (importedScore != null) scoreManager.Export(importedScore); this.state.ValueChanged -= exportWhenReady; } From ed8b59632561cfbbfa7e949daa4709660cc3356d Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Sun, 14 Apr 2024 16:22:58 -0700 Subject: [PATCH 0525/2493] Fix replay export not working correctly from online leaderboards --- osu.Game/OsuGame.cs | 19 ++--------- osu.Game/Scoring/ScoreManager.cs | 34 +++++++++++++++++-- .../Screens/Ranking/ReplayDownloadButton.cs | 4 ++- 3 files changed, 36 insertions(+), 21 deletions(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 732d5f867c..bcf12b308d 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -706,24 +706,9 @@ namespace osu.Game { Logger.Log($"Beginning {nameof(PresentScore)} with score {score}"); - // The given ScoreInfo may have missing properties if it was retrieved from online data. Re-retrieve it from the database - // to ensure all the required data for presenting a replay are present. - ScoreInfo databasedScoreInfo = null; + ScoreInfo databasedScoreInfo = ScoreManager.GetDatabasedScoreInfo(score); - if (score.OnlineID > 0) - databasedScoreInfo = ScoreManager.Query(s => s.OnlineID == score.OnlineID); - - if (score.LegacyOnlineID > 0) - databasedScoreInfo ??= ScoreManager.Query(s => s.LegacyOnlineID == score.LegacyOnlineID); - - if (score is ScoreInfo scoreInfo) - databasedScoreInfo ??= ScoreManager.Query(s => s.Hash == scoreInfo.Hash); - - if (databasedScoreInfo == null) - { - Logger.Log("The requested score could not be found locally.", LoggingTarget.Information); - return; - } + if (databasedScoreInfo == null) return; var databasedScore = ScoreManager.GetScore(databasedScoreInfo); diff --git a/osu.Game/Scoring/ScoreManager.cs b/osu.Game/Scoring/ScoreManager.cs index 1cdf4b0c13..f699e32ac7 100644 --- a/osu.Game/Scoring/ScoreManager.cs +++ b/osu.Game/Scoring/ScoreManager.cs @@ -8,8 +8,8 @@ using System.Linq; using System.Linq.Expressions; using System.Threading; using System.Threading.Tasks; -using JetBrains.Annotations; using osu.Framework.Bindables; +using osu.Framework.Logging; using osu.Framework.Platform; using osu.Game.Beatmaps; using osu.Game.Configuration; @@ -70,6 +70,34 @@ namespace osu.Game.Scoring return Realm.Run(r => r.All().FirstOrDefault(query)?.Detach()); } + /// + /// Re-retrieve a given from the database to ensure all the required data for presenting / exporting a replay are present. + /// + /// The to attempt querying on. + /// The databased score info. Null if the score on the database cannot be found. + /// Can be used when the was retrieved from online data, as it may have missing properties. + public ScoreInfo? GetDatabasedScoreInfo(IScoreInfo originalScoreInfo) + { + ScoreInfo? databasedScoreInfo = null; + + if (originalScoreInfo.OnlineID > 0) + databasedScoreInfo = Query(s => s.OnlineID == originalScoreInfo.OnlineID); + + if (originalScoreInfo.LegacyOnlineID > 0) + databasedScoreInfo ??= Query(s => s.LegacyOnlineID == originalScoreInfo.LegacyOnlineID); + + if (originalScoreInfo is ScoreInfo scoreInfo) + databasedScoreInfo ??= Query(s => s.Hash == scoreInfo.Hash); + + if (databasedScoreInfo == null) + { + Logger.Log("The requested score could not be found locally.", LoggingTarget.Information); + return null; + } + + return databasedScoreInfo; + } + /// /// Retrieves a bindable that represents the total score of a . /// @@ -78,7 +106,7 @@ namespace osu.Game.Scoring /// /// The to retrieve the bindable for. /// The bindable containing the total score. - public Bindable GetBindableTotalScore([NotNull] ScoreInfo score) => new TotalScoreBindable(score, configManager); + public Bindable GetBindableTotalScore(ScoreInfo score) => new TotalScoreBindable(score, configManager); /// /// Retrieves a bindable that represents the formatted total score string of a . @@ -88,7 +116,7 @@ namespace osu.Game.Scoring /// /// The to retrieve the bindable for. /// The bindable containing the formatted total score string. - public Bindable GetBindableTotalScoreString([NotNull] ScoreInfo score) => new TotalScoreStringBindable(GetBindableTotalScore(score)); + public Bindable GetBindableTotalScoreString(ScoreInfo score) => new TotalScoreStringBindable(GetBindableTotalScore(score)); /// /// Provides the total score of a . Responds to changes in the currently-selected . diff --git a/osu.Game/Screens/Ranking/ReplayDownloadButton.cs b/osu.Game/Screens/Ranking/ReplayDownloadButton.cs index df5f9c7a8a..9bacfc5ed3 100644 --- a/osu.Game/Screens/Ranking/ReplayDownloadButton.cs +++ b/osu.Game/Screens/Ranking/ReplayDownloadButton.cs @@ -147,7 +147,9 @@ namespace osu.Game.Screens.Ranking { if (state.NewValue != DownloadState.LocallyAvailable) return; - scoreManager.Export(Score.Value); + ScoreInfo? databasedScoreInfo = scoreManager.GetDatabasedScoreInfo(Score.Value); + + if (databasedScoreInfo != null) scoreManager.Export(databasedScoreInfo); State.ValueChanged -= exportWhenReady; } From 8506da725dcf0aac225bd6ab5c230fb0150827e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 15 Apr 2024 11:49:47 +0200 Subject: [PATCH 0526/2493] Add failing test --- .../TestSceneExpandedPanelMiddleContent.cs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs b/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs index 9f7726313a..02a321d22f 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs @@ -104,6 +104,21 @@ namespace osu.Game.Tests.Visual.Ranking }); } + [Test] + public void TestPPNotShownAsProvisionalIfClassicModIsPresentDueToLegacyScore() + { + AddStep("show example score", () => + { + var score = TestResources.CreateTestScoreInfo(createTestBeatmap(new RealmUser())); + score.PP = 400; + score.Mods = score.Mods.Append(new OsuModClassic()).ToArray(); + score.IsLegacyScore = true; + showPanel(score); + }); + + AddAssert("pp display faded out", () => this.ChildrenOfType().Single().Alpha == 1); + } + [Test] public void TestWithDefaultDate() { From 7c4c8ee75c746620f955f1fbb2b1e43f26badf4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 15 Apr 2024 11:53:03 +0200 Subject: [PATCH 0527/2493] Fix stable scores showing with faded out pp display due to classic mod presence --- .../Expanded/Statistics/PerformanceStatistic.cs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs b/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs index 0a9c68eafc..8366f8d7ef 100644 --- a/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs +++ b/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs @@ -4,6 +4,7 @@ #nullable disable using System; +using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -17,6 +18,7 @@ using osu.Game.Graphics.UserInterface; using osu.Game.Resources.Localisation.Web; using osu.Game.Scoring; using osu.Game.Localisation; +using osu.Game.Rulesets.Mods; namespace osu.Game.Screens.Ranking.Expanded.Statistics { @@ -74,7 +76,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics Alpha = 0.5f; TooltipText = ResultsScreenStrings.NoPPForUnrankedBeatmaps; } - else if (scoreInfo.Mods.Any(m => !m.Ranked)) + else if (hasUnrankedMods(scoreInfo)) { Alpha = 0.5f; TooltipText = ResultsScreenStrings.NoPPForUnrankedMods; @@ -87,6 +89,16 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics } } + private static bool hasUnrankedMods(ScoreInfo scoreInfo) + { + IEnumerable modsToCheck = scoreInfo.Mods; + + if (scoreInfo.IsLegacyScore) + modsToCheck = modsToCheck.Where(m => m is not ModClassic); + + return modsToCheck.Any(m => !m.Ranked); + } + public override void Appear() { base.Appear(); From fe7df808b6065dcbb405f7775fdaaa0d5a441f52 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 15 Apr 2024 21:07:08 +0900 Subject: [PATCH 0528/2493] Add tests --- .../SongSelect/TestScenePlaySongSelect.cs | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs index ce241f3676..e03ffd48f1 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs @@ -29,6 +29,7 @@ using osu.Game.Overlays.Dialog; using osu.Game.Overlays.Mods; using osu.Game.Overlays.Notifications; using osu.Game.Rulesets; +using osu.Game.Rulesets.Mania.Mods; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Mods; @@ -1147,6 +1148,62 @@ namespace osu.Game.Tests.Visual.SongSelect AddAssert("filter text cleared", () => songSelect!.FilterControl.ChildrenOfType().First().Text, () => Is.Empty); } + [Test] + public void TestNonFilterableModChange() + { + addRulesetImportStep(0); + + createSongSelect(); + + // Mod that is guaranteed to never re-filter. + AddStep("add non-filterable mod", () => SelectedMods.Value = new Mod[] { new OsuModCinema() }); + AddAssert("filter count is 1", () => songSelect!.FilterCount, () => Is.EqualTo(1)); + + // Removing the mod should still not re-filter. + AddStep("remove non-filterable mod", () => SelectedMods.Value = Array.Empty()); + AddAssert("filter count is 1", () => songSelect!.FilterCount, () => Is.EqualTo(1)); + } + + [Test] + public void TestFilterableModChange() + { + addRulesetImportStep(3); + + createSongSelect(); + + // Change to mania ruleset. + AddStep("filter to mania ruleset", () => Ruleset.Value = rulesets.AvailableRulesets.First(r => r.OnlineID == 3)); + AddAssert("filter count is 2", () => songSelect!.FilterCount, () => Is.EqualTo(2)); + + // Apply a mod, but this should NOT re-filter because there's no search text. + AddStep("add filterable mod", () => SelectedMods.Value = new Mod[] { new ManiaModKey3() }); + AddAssert("filter count is 2", () => songSelect!.FilterCount, () => Is.EqualTo(2)); + + // Set search text. Should re-filter. + AddStep("set search text to match mods", () => songSelect!.FilterControl.CurrentTextSearch.Value = "keys=3"); + AddAssert("filter count is 3", () => songSelect!.FilterCount, () => Is.EqualTo(3)); + + // Change filterable mod. Should re-filter. + AddStep("change new filterable mod", () => SelectedMods.Value = new Mod[] { new ManiaModKey5() }); + AddAssert("filter count is 4", () => songSelect!.FilterCount, () => Is.EqualTo(4)); + + // Add non-filterable mod. Should NOT re-filter. + AddStep("apply non-filterable mod", () => SelectedMods.Value = new Mod[] { new ManiaModNoFail(), new ManiaModKey5() }); + AddAssert("filter count is 4", () => songSelect!.FilterCount, () => Is.EqualTo(4)); + + // Remove filterable mod. Should re-filter. + AddStep("remove filterable mod", () => SelectedMods.Value = new Mod[] { new ManiaModNoFail() }); + AddAssert("filter count is 5", () => songSelect!.FilterCount, () => Is.EqualTo(5)); + + // Remove non-filterable mod. Should NOT re-filter. + AddStep("remove filterable mod", () => SelectedMods.Value = Array.Empty()); + AddAssert("filter count is 5", () => songSelect!.FilterCount, () => Is.EqualTo(5)); + + // Add filterable mod. Should re-filter. + AddStep("add filterable mod", () => SelectedMods.Value = new Mod[] { new ManiaModKey3() }); + AddAssert("filter count is 6", () => songSelect!.FilterCount, () => Is.EqualTo(6)); + } + private void waitForInitialSelection() { AddUntilStep("wait for initial selection", () => !Beatmap.IsDefault); From 343b3ba0e614dcfd68e9f3b6046b7d119776c95e Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 15 Apr 2024 21:07:36 +0900 Subject: [PATCH 0529/2493] Don't re-filter unless mods may change the filter --- .../ManiaFilterCriteria.cs | 20 +++++++++++++++++++ .../NonVisual/Filtering/FilterMatchingTest.cs | 5 +++++ .../Filtering/FilterQueryParserTest.cs | 5 +++++ .../Rulesets/Filter/IRulesetFilterCriteria.cs | 10 ++++++++++ osu.Game/Screens/Select/FilterControl.cs | 13 ++++++------ 5 files changed, 47 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Mania/ManiaFilterCriteria.cs b/osu.Game.Rulesets.Mania/ManiaFilterCriteria.cs index ea7eb5b8f0..8c6efbc72d 100644 --- a/osu.Game.Rulesets.Mania/ManiaFilterCriteria.cs +++ b/osu.Game.Rulesets.Mania/ManiaFilterCriteria.cs @@ -1,9 +1,14 @@ // 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.Bindables; using osu.Game.Beatmaps; using osu.Game.Rulesets.Filter; using osu.Game.Rulesets.Mania.Beatmaps; +using osu.Game.Rulesets.Mania.Mods; +using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Scoring.Legacy; using osu.Game.Screens.Select; using osu.Game.Screens.Select.Filter; @@ -30,5 +35,20 @@ namespace osu.Game.Rulesets.Mania return false; } + + public bool FilterMayChangeFromMods(ValueChangedEvent> mods) + { + if (keys.HasFilter) + { + // Interpreting as the Mod type is required for equality comparison. + HashSet oldSet = mods.OldValue.OfType().AsEnumerable().ToHashSet(); + HashSet newSet = mods.NewValue.OfType().AsEnumerable().ToHashSet(); + + if (!oldSet.SetEquals(newSet)) + return true; + } + + return false; + } } } diff --git a/osu.Game.Tests/NonVisual/Filtering/FilterMatchingTest.cs b/osu.Game.Tests/NonVisual/Filtering/FilterMatchingTest.cs index 78d8eabba7..10e0e46f4c 100644 --- a/osu.Game.Tests/NonVisual/Filtering/FilterMatchingTest.cs +++ b/osu.Game.Tests/NonVisual/Filtering/FilterMatchingTest.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 NUnit.Framework; +using osu.Framework.Bindables; using osu.Game.Beatmaps; using osu.Game.Rulesets; using osu.Game.Rulesets.Filter; +using osu.Game.Rulesets.Mods; using osu.Game.Screens.Select; using osu.Game.Screens.Select.Carousel; using osu.Game.Screens.Select.Filter; @@ -311,6 +314,8 @@ namespace osu.Game.Tests.NonVisual.Filtering public bool Matches(BeatmapInfo beatmapInfo, FilterCriteria criteria) => match; public bool TryParseCustomKeywordCriteria(string key, Operator op, string value) => false; + + public bool FilterMayChangeFromMods(ValueChangedEvent> mods) => false; } } } diff --git a/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs b/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs index b0ceed45b9..7897b3d8c0 100644 --- a/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs +++ b/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs @@ -2,10 +2,13 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; using System.Linq; using NUnit.Framework; +using osu.Framework.Bindables; using osu.Game.Beatmaps; using osu.Game.Rulesets.Filter; +using osu.Game.Rulesets.Mods; using osu.Game.Screens.Select; using osu.Game.Screens.Select.Carousel; using osu.Game.Screens.Select.Filter; @@ -514,6 +517,8 @@ namespace osu.Game.Tests.NonVisual.Filtering return false; } + + public bool FilterMayChangeFromMods(ValueChangedEvent> mods) => false; } private static readonly object[] correct_date_query_examples = diff --git a/osu.Game/Rulesets/Filter/IRulesetFilterCriteria.cs b/osu.Game/Rulesets/Filter/IRulesetFilterCriteria.cs index f926b04db4..c374fe315d 100644 --- a/osu.Game/Rulesets/Filter/IRulesetFilterCriteria.cs +++ b/osu.Game/Rulesets/Filter/IRulesetFilterCriteria.cs @@ -1,7 +1,10 @@ // 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.Bindables; using osu.Game.Beatmaps; +using osu.Game.Rulesets.Mods; using osu.Game.Screens.Select; using osu.Game.Screens.Select.Filter; @@ -52,5 +55,12 @@ namespace osu.Game.Rulesets.Filter /// while ignored criteria are included in . /// bool TryParseCustomKeywordCriteria(string key, Operator op, string value); + + /// + /// Whether to reapply the filter as a result of the given change in applied mods. + /// + /// The change in mods. + /// Whether the filter should be re-applied. + bool FilterMayChangeFromMods(ValueChangedEvent> mods); } } diff --git a/osu.Game/Screens/Select/FilterControl.cs b/osu.Game/Screens/Select/FilterControl.cs index 0bfd927234..73c122dda6 100644 --- a/osu.Game/Screens/Select/FilterControl.cs +++ b/osu.Game/Screens/Select/FilterControl.cs @@ -7,6 +7,7 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; +using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -49,15 +50,14 @@ namespace osu.Game.Screens.Select } private OsuTabControl sortTabs; - private Bindable sortMode; - private Bindable groupMode; - private FilterControlTextBox searchTextBox; - private CollectionDropdown collectionDropdown; + [CanBeNull] + private FilterCriteria currentCriteria; + public FilterCriteria CreateCriteria() { string query = searchTextBox.Text; @@ -228,7 +228,8 @@ namespace osu.Game.Screens.Select if (m.NewValue.SequenceEqual(m.OldValue)) return; - updateCriteria(); + if (currentCriteria?.RulesetCriteria?.FilterMayChangeFromMods(m) == true) + updateCriteria(); }); groupMode.BindValueChanged(_ => updateCriteria()); @@ -263,7 +264,7 @@ namespace osu.Game.Screens.Select private readonly Bindable minimumStars = new BindableDouble(); private readonly Bindable maximumStars = new BindableDouble(); - private void updateCriteria() => FilterChanged?.Invoke(CreateCriteria()); + private void updateCriteria() => FilterChanged?.Invoke(currentCriteria = CreateCriteria()); protected override bool OnClick(ClickEvent e) => true; From 7e4782d4b1aeec427ea41d3a5d3bfe5d25a2f1f4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 16 Apr 2024 09:50:51 +0800 Subject: [PATCH 0530/2493] Allow nested high performance sessions Mostly just for safety, since I noticed this would pretty much fall over in this scenario until now. --- .../HighPerformanceSessionManager.cs | 22 +++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/osu.Desktop/Performance/HighPerformanceSessionManager.cs b/osu.Desktop/Performance/HighPerformanceSessionManager.cs index eb2b3be5b9..058d247aee 100644 --- a/osu.Desktop/Performance/HighPerformanceSessionManager.cs +++ b/osu.Desktop/Performance/HighPerformanceSessionManager.cs @@ -11,16 +11,24 @@ namespace osu.Desktop.Performance { public class HighPerformanceSessionManager : IHighPerformanceSessionManager { + private int activeSessions; + private GCLatencyMode originalGCMode; public IDisposable BeginSession() { - enableHighPerformanceSession(); - return new InvokeOnDisposal(this, static m => m.disableHighPerformanceSession()); + enterSession(); + return new InvokeOnDisposal(this, static m => m.exitSession()); } - private void enableHighPerformanceSession() + private void enterSession() { + if (Interlocked.Increment(ref activeSessions) > 1) + { + Logger.Log($"High performance session requested ({activeSessions} others already running)"); + return; + } + Logger.Log("Starting high performance session"); originalGCMode = GCSettings.LatencyMode; @@ -30,8 +38,14 @@ namespace osu.Desktop.Performance GC.Collect(0); } - private void disableHighPerformanceSession() + private void exitSession() { + if (Interlocked.Decrement(ref activeSessions) > 0) + { + Logger.Log($"High performance session finished ({activeSessions} others remain)"); + return; + } + Logger.Log("Ending high performance session"); if (GCSettings.LatencyMode == GCLatencyMode.LowLatency) From d89edd2b4f52fe776c77a66a414c1dcd2472bede Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 16 Apr 2024 09:51:43 +0800 Subject: [PATCH 0531/2493] Expose high performance session state --- osu.Desktop/Performance/HighPerformanceSessionManager.cs | 3 +++ osu.Game/Performance/IHighPerformanceSessionManager.cs | 5 +++++ 2 files changed, 8 insertions(+) diff --git a/osu.Desktop/Performance/HighPerformanceSessionManager.cs b/osu.Desktop/Performance/HighPerformanceSessionManager.cs index 058d247aee..34762de04d 100644 --- a/osu.Desktop/Performance/HighPerformanceSessionManager.cs +++ b/osu.Desktop/Performance/HighPerformanceSessionManager.cs @@ -3,6 +3,7 @@ using System; using System.Runtime; +using System.Threading; using osu.Framework.Allocation; using osu.Framework.Logging; using osu.Game.Performance; @@ -11,6 +12,8 @@ namespace osu.Desktop.Performance { public class HighPerformanceSessionManager : IHighPerformanceSessionManager { + public bool IsSessionActive => activeSessions > 0; + private int activeSessions; private GCLatencyMode originalGCMode; diff --git a/osu.Game/Performance/IHighPerformanceSessionManager.cs b/osu.Game/Performance/IHighPerformanceSessionManager.cs index d3d1fda8fc..cc995e4942 100644 --- a/osu.Game/Performance/IHighPerformanceSessionManager.cs +++ b/osu.Game/Performance/IHighPerformanceSessionManager.cs @@ -14,6 +14,11 @@ namespace osu.Game.Performance /// public interface IHighPerformanceSessionManager { + /// + /// Whether a high performance session is currently active. + /// + bool IsSessionActive { get; } + /// /// Start a new high performance session. /// From a651cb8d507c8720a83db85acef9738107046461 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 16 Apr 2024 09:51:58 +0800 Subject: [PATCH 0532/2493] Stop background processing from running when inside a high performance session --- osu.Game/Database/BackgroundDataStoreProcessor.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/osu.Game/Database/BackgroundDataStoreProcessor.cs b/osu.Game/Database/BackgroundDataStoreProcessor.cs index 872194aa1d..f3b37f608c 100644 --- a/osu.Game/Database/BackgroundDataStoreProcessor.cs +++ b/osu.Game/Database/BackgroundDataStoreProcessor.cs @@ -16,6 +16,7 @@ using osu.Game.Extensions; using osu.Game.Online.API; using osu.Game.Overlays; using osu.Game.Overlays.Notifications; +using osu.Game.Performance; using osu.Game.Rulesets; using osu.Game.Scoring; using osu.Game.Scoring.Legacy; @@ -51,6 +52,9 @@ namespace osu.Game.Database [Resolved] private ILocalUserPlayInfo? localUserPlayInfo { get; set; } + [Resolved] + private IHighPerformanceSessionManager? highPerformanceSessionManager { get; set; } + [Resolved] private INotificationOverlay? notificationOverlay { get; set; } @@ -493,7 +497,9 @@ namespace osu.Game.Database private void sleepIfRequired() { - while (localUserPlayInfo?.IsPlaying.Value == true) + // Importantly, also sleep if high performance session is active. + // If we don't do this, memory usage can become runaway due to GC running in a more lenient mode. + while (localUserPlayInfo?.IsPlaying.Value == true || highPerformanceSessionManager?.IsSessionActive == true) { Logger.Log("Background processing sleeping due to active gameplay..."); Thread.Sleep(TimeToSleepDuringGameplay); From 67cfcddc7727fb5a735cff9ce3dab8d0bae0e004 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Mon, 15 Apr 2024 20:18:24 -0700 Subject: [PATCH 0533/2493] Use another beatmap query to not depend on databased score info --- osu.Game/OsuGame.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index bcf12b308d..6a29767a8e 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -718,7 +718,7 @@ namespace osu.Game return; } - var databasedBeatmap = BeatmapManager.QueryBeatmap(b => b.ID == databasedScoreInfo.BeatmapInfo.ID); + var databasedBeatmap = BeatmapManager.QueryBeatmap(b => b.OnlineID == score.Beatmap.OnlineID); if (databasedBeatmap == null) { From 514e316b49ad9721e0e7997d02fc14dae3c86504 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Mon, 15 Apr 2024 20:48:51 -0700 Subject: [PATCH 0534/2493] Make `getDatabasedScoreInfo()` private and move to `GetScore()` and `Export()` --- osu.Game/OsuGame.cs | 6 ++---- osu.Game/Scoring/ScoreManager.cs | 16 +++++++++++++--- osu.Game/Screens/Ranking/ReplayDownloadButton.cs | 4 +--- 3 files changed, 16 insertions(+), 10 deletions(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 6a29767a8e..ccdf9d151f 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -706,11 +706,9 @@ namespace osu.Game { Logger.Log($"Beginning {nameof(PresentScore)} with score {score}"); - ScoreInfo databasedScoreInfo = ScoreManager.GetDatabasedScoreInfo(score); + var databasedScore = ScoreManager.GetScore(score); - if (databasedScoreInfo == null) return; - - var databasedScore = ScoreManager.GetScore(databasedScoreInfo); + if (databasedScore == null) return; if (databasedScore.Replay == null) { diff --git a/osu.Game/Scoring/ScoreManager.cs b/osu.Game/Scoring/ScoreManager.cs index f699e32ac7..3f6c6ee49d 100644 --- a/osu.Game/Scoring/ScoreManager.cs +++ b/osu.Game/Scoring/ScoreManager.cs @@ -58,7 +58,12 @@ namespace osu.Game.Scoring }; } - public Score GetScore(ScoreInfo score) => scoreImporter.GetScore(score); + public Score? GetScore(IScoreInfo scoreInfo) + { + ScoreInfo? databasedScoreInfo = getDatabasedScoreInfo(scoreInfo); + + return databasedScoreInfo == null ? null : scoreImporter.GetScore(databasedScoreInfo); + } /// /// Perform a lookup query on available s. @@ -76,7 +81,7 @@ namespace osu.Game.Scoring /// The to attempt querying on. /// The databased score info. Null if the score on the database cannot be found. /// Can be used when the was retrieved from online data, as it may have missing properties. - public ScoreInfo? GetDatabasedScoreInfo(IScoreInfo originalScoreInfo) + private ScoreInfo? getDatabasedScoreInfo(IScoreInfo originalScoreInfo) { ScoreInfo? databasedScoreInfo = null; @@ -189,7 +194,12 @@ namespace osu.Game.Scoring public Task>> Import(ProgressNotification notification, ImportTask[] tasks, ImportParameters parameters = default) => scoreImporter.Import(notification, tasks); - public Task Export(ScoreInfo score) => scoreExporter.ExportAsync(score.ToLive(Realm)); + public Task? Export(ScoreInfo scoreInfo) + { + ScoreInfo? databasedScoreInfo = getDatabasedScoreInfo(scoreInfo); + + return databasedScoreInfo == null ? null : scoreExporter.ExportAsync(databasedScoreInfo.ToLive(Realm)); + } public Task?> ImportAsUpdate(ProgressNotification notification, ImportTask task, ScoreInfo original) => scoreImporter.ImportAsUpdate(notification, task, original); diff --git a/osu.Game/Screens/Ranking/ReplayDownloadButton.cs b/osu.Game/Screens/Ranking/ReplayDownloadButton.cs index 9bacfc5ed3..df5f9c7a8a 100644 --- a/osu.Game/Screens/Ranking/ReplayDownloadButton.cs +++ b/osu.Game/Screens/Ranking/ReplayDownloadButton.cs @@ -147,9 +147,7 @@ namespace osu.Game.Screens.Ranking { if (state.NewValue != DownloadState.LocallyAvailable) return; - ScoreInfo? databasedScoreInfo = scoreManager.GetDatabasedScoreInfo(Score.Value); - - if (databasedScoreInfo != null) scoreManager.Export(databasedScoreInfo); + scoreManager.Export(Score.Value); State.ValueChanged -= exportWhenReady; } From c7b1524b9ff6d4294b09eb2c3fc4fc4f04881a8f Mon Sep 17 00:00:00 2001 From: Arthur Araujo Date: Tue, 16 Apr 2024 05:25:52 -0300 Subject: [PATCH 0535/2493] Add new audio samples --- .../Resources/Samples/test-sample.ogg | Bin 0 -> 36347 bytes .../Resources/Samples/test-sample.webm | Bin 0 -> 34247 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 osu.Game.Tests/Resources/Samples/test-sample.ogg create mode 100644 osu.Game.Tests/Resources/Samples/test-sample.webm diff --git a/osu.Game.Tests/Resources/Samples/test-sample.ogg b/osu.Game.Tests/Resources/Samples/test-sample.ogg new file mode 100644 index 0000000000000000000000000000000000000000..b33119cfaf89615cece2655a30c95c82127093ee GIT binary patch literal 36347 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 literal 0 HcmV?d00001 diff --git a/osu.Game.Tests/Resources/Samples/test-sample.webm b/osu.Game.Tests/Resources/Samples/test-sample.webm new file mode 100644 index 0000000000000000000000000000000000000000..3964d248f4dc6fc0f84a6f3f4fcfe9db97461efa GIT binary patch literal 34247 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�tSAml_ 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 Date: Tue, 16 Apr 2024 16:37:11 +0800 Subject: [PATCH 0536/2493] Update HighPerformanceSessionManager.cs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bartłomiej Dach --- osu.Desktop/Performance/HighPerformanceSessionManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Desktop/Performance/HighPerformanceSessionManager.cs b/osu.Desktop/Performance/HighPerformanceSessionManager.cs index 34762de04d..0df87ab007 100644 --- a/osu.Desktop/Performance/HighPerformanceSessionManager.cs +++ b/osu.Desktop/Performance/HighPerformanceSessionManager.cs @@ -28,7 +28,7 @@ namespace osu.Desktop.Performance { if (Interlocked.Increment(ref activeSessions) > 1) { - Logger.Log($"High performance session requested ({activeSessions} others already running)"); + Logger.Log($"High performance session requested ({activeSessions} running in total)"); return; } From 9ef27104ce50c5844004659b3782d984f3c05c8f Mon Sep 17 00:00:00 2001 From: Arthur Araujo Date: Tue, 16 Apr 2024 06:15:21 -0300 Subject: [PATCH 0537/2493] Add checks for audio formats --- .../Checks/CheckHitsoundsFormatTest.cs | 96 +++++++++++++++++ .../Editing/Checks/CheckSongFormatTest.cs | 100 ++++++++++++++++++ osu.Game/Rulesets/Edit/BeatmapVerifier.cs | 2 + .../Edit/Checks/CheckHitsoundsFormat.cs | 93 ++++++++++++++++ .../Rulesets/Edit/Checks/CheckSongFormat.cs | 81 ++++++++++++++ 5 files changed, 372 insertions(+) create mode 100644 osu.Game.Tests/Editing/Checks/CheckHitsoundsFormatTest.cs create mode 100644 osu.Game.Tests/Editing/Checks/CheckSongFormatTest.cs create mode 100644 osu.Game/Rulesets/Edit/Checks/CheckHitsoundsFormat.cs create mode 100644 osu.Game/Rulesets/Edit/Checks/CheckSongFormat.cs diff --git a/osu.Game.Tests/Editing/Checks/CheckHitsoundsFormatTest.cs b/osu.Game.Tests/Editing/Checks/CheckHitsoundsFormatTest.cs new file mode 100644 index 0000000000..912a7468f5 --- /dev/null +++ b/osu.Game.Tests/Editing/Checks/CheckHitsoundsFormatTest.cs @@ -0,0 +1,96 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.IO; +using System.Linq; +using ManagedBass; +using Moq; +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.Tests.Beatmaps; +using osu.Game.Tests.Resources; +using osuTK.Audio; + +namespace osu.Game.Tests.Editing.Checks +{ + [TestFixture] + public class CheckHitsoundsFormatTest + { + private CheckHitsoundsFormat check = null!; + + private IBeatmap beatmap = null!; + + [SetUp] + public void Setup() + { + check = new CheckHitsoundsFormat(); + beatmap = new Beatmap + { + BeatmapInfo = new BeatmapInfo + { + BeatmapSet = new BeatmapSetInfo + { + Files = { CheckTestHelpers.CreateMockFile("wav") } + } + } + }; + + // 0 = No output device. This still allows decoding. + if (!Bass.Init(0) && Bass.LastError != Errors.Already) + throw new AudioException("Could not initialize Bass."); + } + + [Test] + public void TestMP3Audio() + { + using (var resourceStream = TestResources.OpenResource("Samples/test-sample-cut.mp3")) + { + var issues = check.Run(getContext(resourceStream)).ToList(); + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues.Single().Template is CheckHitsoundsFormat.IssueTemplateIncorrectFormat); + } + } + + [Test] + public void TestOGGAudio() + { + using (var resourceStream = TestResources.OpenResource("Samples/test-sample.ogg")) + { + var issues = check.Run(getContext(resourceStream)).ToList(); + Assert.That(issues, Has.Count.EqualTo(0)); + } + } + + [Test] + public void TestWAVAudio() + { + using (var resourceStream = TestResources.OpenResource("Samples/hitsound-delay.wav")) + { + var issues = check.Run(getContext(resourceStream)).ToList(); + Assert.That(issues, Has.Count.EqualTo(0)); + } + } + + [Test] + public void TestWEBMAudio() + { + using (var resourceStream = TestResources.OpenResource("Samples/test-sample.webm")) + { + var issues = check.Run(getContext(resourceStream)).ToList(); + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues.Single().Template is CheckHitsoundsFormat.IssueTemplateFormatUnsupported); + } + } + + private BeatmapVerifierContext getContext(Stream? resourceStream) + { + var mockWorkingBeatmap = new Mock(beatmap, null, null); + mockWorkingBeatmap.Setup(w => w.GetStream(It.IsAny())).Returns(resourceStream); + + return new BeatmapVerifierContext(beatmap, mockWorkingBeatmap.Object); + } + } +} diff --git a/osu.Game.Tests/Editing/Checks/CheckSongFormatTest.cs b/osu.Game.Tests/Editing/Checks/CheckSongFormatTest.cs new file mode 100644 index 0000000000..acbf25ebad --- /dev/null +++ b/osu.Game.Tests/Editing/Checks/CheckSongFormatTest.cs @@ -0,0 +1,100 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.IO; +using System.Linq; +using ManagedBass; +using Moq; +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.Tests.Beatmaps; +using osu.Game.Tests.Resources; +using osuTK.Audio; + +namespace osu.Game.Tests.Editing.Checks +{ + [TestFixture] + public partial class CheckSongFormatTest + { + private CheckSongFormat check = null!; + + private IBeatmap beatmap = null!; + + [SetUp] + public void Setup() + { + check = new CheckSongFormat(); + beatmap = new Beatmap + { + BeatmapInfo = new BeatmapInfo + { + BeatmapSet = new BeatmapSetInfo + { + Files = { CheckTestHelpers.CreateMockFile("mp3") } + } + } + }; + + // 0 = No output device. This still allows decoding. + if (!Bass.Init(0) && Bass.LastError != Errors.Already) + throw new AudioException("Could not initialize Bass."); + } + + [Test] + public void TestMP3Audio() + { + using (var resourceStream = TestResources.OpenResource("Samples/test-sample-cut.mp3")) + { + beatmap.Metadata.AudioFile = "abc123.mp3"; + var issues = check.Run(getContext(resourceStream)).ToList(); + Assert.That(issues, Has.Count.EqualTo(0)); + } + } + + [Test] + public void TestOGGAudio() + { + using (var resourceStream = TestResources.OpenResource("Samples/test-sample.ogg")) + { + beatmap.Metadata.AudioFile = "abc123.mp3"; + var issues = check.Run(getContext(resourceStream)).ToList(); + Assert.That(issues, Has.Count.EqualTo(0)); + } + } + + [Test] + public void TestWAVAudio() + { + using (var resourceStream = TestResources.OpenResource("Samples/hitsound-delay.wav")) + { + beatmap.Metadata.AudioFile = "abc123.mp3"; + var issues = check.Run(getContext(resourceStream)).ToList(); + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues.Single().Template is CheckSongFormat.IssueTemplateIncorrectFormat); + } + } + + [Test] + public void TestWEBMAudio() + { + using (var resourceStream = TestResources.OpenResource("Samples/test-sample.webm")) + { + beatmap.Metadata.AudioFile = "abc123.mp3"; + var issues = check.Run(getContext(resourceStream)).ToList(); + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues.Single().Template is CheckSongFormat.IssueTemplateFormatUnsupported); + } + } + + private BeatmapVerifierContext getContext(Stream? resourceStream) + { + var mockWorkingBeatmap = new Mock(beatmap, null, null); + mockWorkingBeatmap.Setup(w => w.GetStream(It.IsAny())).Returns(resourceStream); + + return new BeatmapVerifierContext(beatmap, mockWorkingBeatmap.Object); + } + } +} diff --git a/osu.Game/Rulesets/Edit/BeatmapVerifier.cs b/osu.Game/Rulesets/Edit/BeatmapVerifier.cs index 7d3c7d0b2f..a9681e13ba 100644 --- a/osu.Game/Rulesets/Edit/BeatmapVerifier.cs +++ b/osu.Game/Rulesets/Edit/BeatmapVerifier.cs @@ -28,6 +28,8 @@ namespace osu.Game.Rulesets.Edit new CheckTooShortAudioFiles(), new CheckAudioInVideo(), new CheckDelayedHitsounds(), + new CheckSongFormat(), + new CheckHitsoundsFormat(), // Files new CheckZeroByteFiles(), diff --git a/osu.Game/Rulesets/Edit/Checks/CheckHitsoundsFormat.cs b/osu.Game/Rulesets/Edit/Checks/CheckHitsoundsFormat.cs new file mode 100644 index 0000000000..e490a23963 --- /dev/null +++ b/osu.Game/Rulesets/Edit/Checks/CheckHitsoundsFormat.cs @@ -0,0 +1,93 @@ +// 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.IO; +using System.Linq; +using ManagedBass; +using osu.Framework.Audio.Callbacks; +using osu.Game.Beatmaps; +using osu.Game.Extensions; +using osu.Game.Rulesets.Edit.Checks.Components; + +namespace osu.Game.Rulesets.Edit.Checks +{ + public class CheckHitsoundsFormat : ICheck + { + public CheckMetadata Metadata => new CheckMetadata(CheckCategory.Audio, "Checks for hitsound formats."); + + public IEnumerable PossibleTemplates => new IssueTemplate[] + { + new IssueTemplateFormatUnsupported(this), + new IssueTemplateIncorrectFormat(this), + }; + + private IEnumerable allowedFormats => new ChannelType[] + { + ChannelType.WavePCM, + ChannelType.WaveFloat, + ChannelType.OGG, + ChannelType.Wave | ChannelType.OGG, + }; + + public IEnumerable Run(BeatmapVerifierContext context) + { + var beatmapSet = context.Beatmap.BeatmapInfo.BeatmapSet; + var audioFile = beatmapSet?.GetFile(context.Beatmap.Metadata.AudioFile); + + if (beatmapSet == null) yield break; + + foreach (var file in beatmapSet.Files) + { + if (audioFile != null && file.File == audioFile.File) continue; + + using (Stream data = context.WorkingBeatmap.GetStream(file.File.GetStoragePath())) + { + if (data == null) + continue; + + var fileCallbacks = new FileCallbacks(new DataStreamFileProcedures(data)); + int decodeStream = Bass.CreateStream(StreamSystem.NoBuffer, BassFlags.Decode | BassFlags.Prescan, fileCallbacks.Callbacks, fileCallbacks.Handle); + + // If the format is not supported by BASS + if (decodeStream == 0) + { + if (AudioCheckUtils.HasAudioExtension(file.Filename) && probablyHasAudioData(data)) + yield return new IssueTemplateFormatUnsupported(this).Create(file.Filename); + + continue; + } + + var audioInfo = Bass.ChannelGetInfo(decodeStream); + + if (!allowedFormats.Contains(audioInfo.ChannelType)) + { + yield return new IssueTemplateIncorrectFormat(this).Create(file.Filename, audioInfo.ChannelType.ToString()); + } + } + } + } + + private bool probablyHasAudioData(Stream data) => data.Length > 100; + + public class IssueTemplateFormatUnsupported : IssueTemplate + { + public IssueTemplateFormatUnsupported(ICheck check) + : base(check, IssueType.Problem, "\"{0}\" is using a unsupported format; Use wav or ogg for hitsounds.") + { + } + + public Issue Create(string file) => new Issue(this, file); + } + + public class IssueTemplateIncorrectFormat : IssueTemplate + { + public IssueTemplateIncorrectFormat(ICheck check) + : base(check, IssueType.Problem, "\"{0}\" is using a incorrect format ({1}); Use wav or ogg for hitsounds.") + { + } + + public Issue Create(string file, string format) => new Issue(this, file, format); + } + } +} diff --git a/osu.Game/Rulesets/Edit/Checks/CheckSongFormat.cs b/osu.Game/Rulesets/Edit/Checks/CheckSongFormat.cs new file mode 100644 index 0000000000..4162bf20a3 --- /dev/null +++ b/osu.Game/Rulesets/Edit/Checks/CheckSongFormat.cs @@ -0,0 +1,81 @@ +// 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.IO; +using System.Linq; +using ManagedBass; +using osu.Framework.Audio.Callbacks; +using osu.Game.Beatmaps; +using osu.Game.Extensions; +using osu.Game.Rulesets.Edit.Checks.Components; + +namespace osu.Game.Rulesets.Edit.Checks +{ + public class CheckSongFormat : ICheck + { + public CheckMetadata Metadata => new CheckMetadata(CheckCategory.Audio, "Checks for song formats."); + + public IEnumerable PossibleTemplates => new IssueTemplate[] + { + new IssueTemplateFormatUnsupported(this), + new IssueTemplateIncorrectFormat(this), + }; + + private IEnumerable allowedFormats => new ChannelType[] + { + ChannelType.MP3, + ChannelType.OGG, + }; + + public IEnumerable Run(BeatmapVerifierContext context) + { + var beatmapSet = context.Beatmap.BeatmapInfo.BeatmapSet; + var audioFile = beatmapSet?.GetFile(context.Beatmap.Metadata.AudioFile); + + if (beatmapSet == null) yield break; + if (audioFile == null) yield break; + + using (Stream data = context.WorkingBeatmap.GetStream(audioFile.File.GetStoragePath())) + { + if (data == null || data.Length <= 0) yield break; + + var fileCallbacks = new FileCallbacks(new DataStreamFileProcedures(data)); + int decodeStream = Bass.CreateStream(StreamSystem.NoBuffer, BassFlags.Decode | BassFlags.Prescan, fileCallbacks.Callbacks, fileCallbacks.Handle); + + // If the format is not supported by BASS + if (decodeStream == 0) + { + yield return new IssueTemplateFormatUnsupported(this).Create(audioFile.Filename); + yield break; + } + + var audioInfo = Bass.ChannelGetInfo(decodeStream); + + if (!allowedFormats.Contains(audioInfo.ChannelType)) + yield return new IssueTemplateIncorrectFormat(this).Create(audioFile.Filename, audioInfo.ChannelType.ToString()); + } + } + + public class IssueTemplateFormatUnsupported : IssueTemplate + { + public IssueTemplateFormatUnsupported(ICheck check) + : base(check, IssueType.Problem, "\"{0}\" is using a unsupported format; Use mp3 or ogg for the song's audio.") + { + } + + public Issue Create(string file) => new Issue(this, file); + } + + public class IssueTemplateIncorrectFormat : IssueTemplate + { + public IssueTemplateIncorrectFormat(ICheck check) + : base(check, IssueType.Problem, "\"{0}\" is using a incorrect format ({1}); Use mp3 or ogg for the song's audio.") + { + } + + public Issue Create(string file, string format) => new Issue(this, file, format); + } + } +} From c32d99250f1dce9d3bed129fd19d1ea7d9a02d22 Mon Sep 17 00:00:00 2001 From: Arthur Araujo Date: Tue, 16 Apr 2024 06:53:55 -0300 Subject: [PATCH 0538/2493] Deal with corrupt audio files This removes the corrupt file check from CheckTooShortAudioFiles and makes the audio formats checks deal with it instead to avoid redundant messages. --- .../Checks/CheckHitsoundsFormatTest.cs | 11 +++++++++ .../Editing/Checks/CheckSongFormatTest.cs | 12 ++++++++++ .../Checks/CheckTooShortAudioFilesTest.cs | 12 ---------- .../Edit/Checks/CheckHitsoundsFormat.cs | 2 +- .../Rulesets/Edit/Checks/CheckSongFormat.cs | 2 +- .../Edit/Checks/CheckTooShortAudioFiles.cs | 24 +------------------ 6 files changed, 26 insertions(+), 37 deletions(-) diff --git a/osu.Game.Tests/Editing/Checks/CheckHitsoundsFormatTest.cs b/osu.Game.Tests/Editing/Checks/CheckHitsoundsFormatTest.cs index 912a7468f5..f85a296c74 100644 --- a/osu.Game.Tests/Editing/Checks/CheckHitsoundsFormatTest.cs +++ b/osu.Game.Tests/Editing/Checks/CheckHitsoundsFormatTest.cs @@ -85,6 +85,17 @@ namespace osu.Game.Tests.Editing.Checks } } + [Test] + public void TestCorruptAudio() + { + using (var resourceStream = TestResources.OpenResource("Samples/corrupt.wav")) + { + var issues = check.Run(getContext(resourceStream)).ToList(); + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues.Single().Template is CheckHitsoundsFormat.IssueTemplateFormatUnsupported); + } + } + private BeatmapVerifierContext getContext(Stream? resourceStream) { var mockWorkingBeatmap = new Mock(beatmap, null, null); diff --git a/osu.Game.Tests/Editing/Checks/CheckSongFormatTest.cs b/osu.Game.Tests/Editing/Checks/CheckSongFormatTest.cs index acbf25ebad..0755fdd8ac 100644 --- a/osu.Game.Tests/Editing/Checks/CheckSongFormatTest.cs +++ b/osu.Game.Tests/Editing/Checks/CheckSongFormatTest.cs @@ -89,6 +89,18 @@ namespace osu.Game.Tests.Editing.Checks } } + [Test] + public void TestCorruptAudio() + { + using (var resourceStream = TestResources.OpenResource("Samples/corrupt.wav")) + { + beatmap.Metadata.AudioFile = "abc123.mp3"; + var issues = check.Run(getContext(resourceStream)).ToList(); + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues.Single().Template is CheckSongFormat.IssueTemplateFormatUnsupported); + } + } + private BeatmapVerifierContext getContext(Stream? resourceStream) { var mockWorkingBeatmap = new Mock(beatmap, null, null); diff --git a/osu.Game.Tests/Editing/Checks/CheckTooShortAudioFilesTest.cs b/osu.Game.Tests/Editing/Checks/CheckTooShortAudioFilesTest.cs index 4918369460..b646e63955 100644 --- a/osu.Game.Tests/Editing/Checks/CheckTooShortAudioFilesTest.cs +++ b/osu.Game.Tests/Editing/Checks/CheckTooShortAudioFilesTest.cs @@ -95,18 +95,6 @@ namespace osu.Game.Tests.Editing.Checks } } - [Test] - public void TestCorruptAudioFile() - { - using (var resourceStream = TestResources.OpenResource("Samples/corrupt.wav")) - { - var issues = check.Run(getContext(resourceStream)).ToList(); - - Assert.That(issues, Has.Count.EqualTo(1)); - Assert.That(issues.Single().Template is CheckTooShortAudioFiles.IssueTemplateBadFormat); - } - } - private BeatmapVerifierContext getContext(Stream? resourceStream) { var mockWorkingBeatmap = new Mock(beatmap, null, null); diff --git a/osu.Game/Rulesets/Edit/Checks/CheckHitsoundsFormat.cs b/osu.Game/Rulesets/Edit/Checks/CheckHitsoundsFormat.cs index e490a23963..f65b89fc01 100644 --- a/osu.Game/Rulesets/Edit/Checks/CheckHitsoundsFormat.cs +++ b/osu.Game/Rulesets/Edit/Checks/CheckHitsoundsFormat.cs @@ -73,7 +73,7 @@ namespace osu.Game.Rulesets.Edit.Checks public class IssueTemplateFormatUnsupported : IssueTemplate { public IssueTemplateFormatUnsupported(ICheck check) - : base(check, IssueType.Problem, "\"{0}\" is using a unsupported format; Use wav or ogg for hitsounds.") + : base(check, IssueType.Problem, "\"{0}\" may be corrupt or using a unsupported audio format; Use wav or ogg for hitsounds.") { } diff --git a/osu.Game/Rulesets/Edit/Checks/CheckSongFormat.cs b/osu.Game/Rulesets/Edit/Checks/CheckSongFormat.cs index 4162bf20a3..ae90dd96d5 100644 --- a/osu.Game/Rulesets/Edit/Checks/CheckSongFormat.cs +++ b/osu.Game/Rulesets/Edit/Checks/CheckSongFormat.cs @@ -61,7 +61,7 @@ namespace osu.Game.Rulesets.Edit.Checks public class IssueTemplateFormatUnsupported : IssueTemplate { public IssueTemplateFormatUnsupported(ICheck check) - : base(check, IssueType.Problem, "\"{0}\" is using a unsupported format; Use mp3 or ogg for the song's audio.") + : base(check, IssueType.Problem, "\"{0}\" may be corrupt or using a unsupported audio format; Use mp3 or ogg for the song's audio.") { } diff --git a/osu.Game/Rulesets/Edit/Checks/CheckTooShortAudioFiles.cs b/osu.Game/Rulesets/Edit/Checks/CheckTooShortAudioFiles.cs index 32a3aa5ad9..3f85926e04 100644 --- a/osu.Game/Rulesets/Edit/Checks/CheckTooShortAudioFiles.cs +++ b/osu.Game/Rulesets/Edit/Checks/CheckTooShortAudioFiles.cs @@ -13,14 +13,12 @@ namespace osu.Game.Rulesets.Edit.Checks public class CheckTooShortAudioFiles : ICheck { private const int ms_threshold = 25; - private const int min_bytes_threshold = 100; public CheckMetadata Metadata => new CheckMetadata(CheckCategory.Audio, "Too short audio files"); public IEnumerable PossibleTemplates => new IssueTemplate[] { new IssueTemplateTooShort(this), - new IssueTemplateBadFormat(this) }; public IEnumerable Run(BeatmapVerifierContext context) @@ -39,15 +37,7 @@ namespace osu.Game.Rulesets.Edit.Checks var fileCallbacks = new FileCallbacks(new DataStreamFileProcedures(data)); int decodeStream = Bass.CreateStream(StreamSystem.NoBuffer, BassFlags.Decode | BassFlags.Prescan, fileCallbacks.Callbacks, fileCallbacks.Handle); - if (decodeStream == 0) - { - // If the file is not likely to be properly parsed by Bass, we don't produce Error issues about it. - // Image files and audio files devoid of audio data both fail, for example, but neither would be issues in this check. - if (AudioCheckUtils.HasAudioExtension(file.Filename) && probablyHasAudioData(data)) - yield return new IssueTemplateBadFormat(this).Create(file.Filename); - - continue; - } + if (decodeStream == 0) continue; long length = Bass.ChannelGetLength(decodeStream); double ms = Bass.ChannelBytes2Seconds(decodeStream, length) * 1000; @@ -60,8 +50,6 @@ namespace osu.Game.Rulesets.Edit.Checks } } - private bool probablyHasAudioData(Stream data) => data.Length > min_bytes_threshold; - public class IssueTemplateTooShort : IssueTemplate { public IssueTemplateTooShort(ICheck check) @@ -71,15 +59,5 @@ namespace osu.Game.Rulesets.Edit.Checks public Issue Create(string filename, double ms) => new Issue(this, filename, ms, ms_threshold); } - - public class IssueTemplateBadFormat : IssueTemplate - { - public IssueTemplateBadFormat(ICheck check) - : base(check, IssueType.Error, "Could not check whether \"{0}\" is too short (code \"{1}\").") - { - } - - public Issue Create(string filename) => new Issue(this, filename, Bass.LastError); - } } } From c4bf03e6400d5047d2ebf916a7bd532334fff19c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 16 Apr 2024 12:42:12 +0200 Subject: [PATCH 0539/2493] Add failing test --- .../TestSceneResume.cs | 69 +++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 osu.Game.Rulesets.Osu.Tests/TestSceneResume.cs diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneResume.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneResume.cs new file mode 100644 index 0000000000..023016c32d --- /dev/null +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneResume.cs @@ -0,0 +1,69 @@ +// 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.Containers; +using osu.Framework.Testing; +using osu.Game.Screens.Play; +using osu.Game.Tests.Visual; +using osuTK; +using osuTK.Input; + +namespace osu.Game.Rulesets.Osu.Tests +{ + public partial class TestSceneResume : PlayerTestScene + { + protected override Ruleset CreatePlayerRuleset() => new OsuRuleset(); + + protected override TestPlayer CreatePlayer(Ruleset ruleset) => new TestPlayer(true, false, AllowBackwardsSeeks); + + [Test] + public void TestPauseViaKeyboard() + { + AddStep("move mouse to center", () => InputManager.MoveMouseTo(Player.ScreenSpaceDrawQuad.Centre)); + AddUntilStep("wait for gameplay start", () => Player.LocalUserPlaying.Value); + AddStep("press escape", () => InputManager.PressKey(Key.Escape)); + AddUntilStep("wait for pause overlay", () => Player.ChildrenOfType().Single().State.Value, () => Is.EqualTo(Visibility.Visible)); + AddStep("release escape", () => InputManager.ReleaseKey(Key.Escape)); + AddStep("resume", () => + { + InputManager.Key(Key.Down); + InputManager.Key(Key.Space); + }); + AddUntilStep("pause overlay present", () => Player.DrawableRuleset.ResumeOverlay.State.Value, () => Is.EqualTo(Visibility.Visible)); + } + + [Test] + public void TestPauseViaKeyboardWhenMouseOutsidePlayfield() + { + AddStep("move mouse outside playfield", () => InputManager.MoveMouseTo(Player.DrawableRuleset.Playfield.ScreenSpaceDrawQuad.BottomRight + new Vector2(1))); + AddUntilStep("wait for gameplay start", () => Player.LocalUserPlaying.Value); + AddStep("press escape", () => InputManager.PressKey(Key.Escape)); + AddUntilStep("wait for pause overlay", () => Player.ChildrenOfType().Single().State.Value, () => Is.EqualTo(Visibility.Visible)); + AddStep("release escape", () => InputManager.ReleaseKey(Key.Escape)); + AddStep("resume", () => + { + InputManager.Key(Key.Down); + InputManager.Key(Key.Space); + }); + AddUntilStep("pause overlay present", () => Player.DrawableRuleset.ResumeOverlay.State.Value, () => Is.EqualTo(Visibility.Visible)); + } + + [Test] + public void TestPauseViaKeyboardWhenMouseOutsideScreen() + { + AddStep("move mouse outside playfield", () => InputManager.MoveMouseTo(new Vector2(-20))); + AddUntilStep("wait for gameplay start", () => Player.LocalUserPlaying.Value); + AddStep("press escape", () => InputManager.PressKey(Key.Escape)); + AddUntilStep("wait for pause overlay", () => Player.ChildrenOfType().Single().State.Value, () => Is.EqualTo(Visibility.Visible)); + AddStep("release escape", () => InputManager.ReleaseKey(Key.Escape)); + AddStep("resume", () => + { + InputManager.Key(Key.Down); + InputManager.Key(Key.Space); + }); + AddUntilStep("pause overlay not present", () => Player.DrawableRuleset.ResumeOverlay.State.Value, () => Is.EqualTo(Visibility.Hidden)); + } + } +} From f9873968a5f06fa355b074f834fad8e9579a3ab7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 16 Apr 2024 13:37:12 +0200 Subject: [PATCH 0540/2493] Apply NRT in `OsuResumeOverlay` --- osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs b/osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs index adc7bd97ff..19d8a94f0a 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using osu.Framework.Allocation; using osu.Framework.Graphics; @@ -19,12 +17,12 @@ namespace osu.Game.Rulesets.Osu.UI { public partial class OsuResumeOverlay : ResumeOverlay { - private Container cursorScaleContainer; - private OsuClickToResumeCursor clickToResumeCursor; + private Container cursorScaleContainer = null!; + private OsuClickToResumeCursor clickToResumeCursor = null!; - private OsuCursorContainer localCursorContainer; + private OsuCursorContainer? localCursorContainer; - public override CursorContainer LocalCursor => State.Value == Visibility.Visible ? localCursorContainer : null; + public override CursorContainer? LocalCursor => State.Value == Visibility.Visible ? localCursorContainer : null; protected override LocalisableString Message => "Click the orange cursor to resume"; @@ -71,8 +69,8 @@ namespace osu.Game.Rulesets.Osu.UI { public override bool HandlePositionalInput => true; - public Action ResumeRequested; - private Container scaleTransitionContainer; + public Action? ResumeRequested; + private Container scaleTransitionContainer = null!; public OsuClickToResumeCursor() { From e5e345712efea6467ae895219cad53a461403672 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 16 Apr 2024 13:24:30 +0200 Subject: [PATCH 0541/2493] Fix resume overlay not appearing after pausing inside window but outside of actual playfield area Related to https://github.com/ppy/osu/discussions/27871 (although does not actually fix the issue with the pause button, _if_ it is to be considered an issue - the problem there is that the gameplay cursor gets hidden, so the other condition in the modified check takes over). Regressed in https://github.com/ppy/osu/commit/bce3bd55e5a863e52f41598c306a248a79638843. Reasoning for breakage is silent change in `this` when moving the `Contains()` check (`DrawableRuleset` will encompass screen bounds, while `OsuResumeOverlay` is only as big as the actual playfield). --- osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs b/osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs index 19d8a94f0a..a04ea80640 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs @@ -10,6 +10,7 @@ using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Framework.Localisation; using osu.Game.Rulesets.Osu.UI.Cursor; +using osu.Game.Rulesets.UI; using osu.Game.Screens.Play; using osuTK.Graphics; @@ -26,6 +27,9 @@ namespace osu.Game.Rulesets.Osu.UI protected override LocalisableString Message => "Click the orange cursor to resume"; + [Resolved] + private DrawableRuleset? drawableRuleset { get; set; } + [BackgroundDependencyLoader] private void load() { @@ -38,7 +42,7 @@ namespace osu.Game.Rulesets.Osu.UI protected override void PopIn() { // Can't display if the cursor is outside the window. - if (GameplayCursor.LastFrameState == Visibility.Hidden || !Contains(GameplayCursor.ActiveCursor.ScreenSpaceDrawQuad.Centre)) + if (GameplayCursor.LastFrameState == Visibility.Hidden || drawableRuleset?.Contains(GameplayCursor.ActiveCursor.ScreenSpaceDrawQuad.Centre) == false) { Resume(); return; From e2a5d1904b000965f009ee746a3175396011e3fc Mon Sep 17 00:00:00 2001 From: TextAdventurer12 Date: Wed, 17 Apr 2024 01:21:06 +1200 Subject: [PATCH 0542/2493] adjust count difficult strains formula --- osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs index c20ea732ec..beaf1d1288 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs @@ -81,7 +81,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills { double consistentTopStrain = difficulty / 10; // What would the top strain be if all strain values were identical // Use a weighted sum of all strains. Constants are arbitrary and give nice values - return objectStrains.Sum(s => 1.3 / (1 + Math.Exp(-14.15 * (Math.Pow(s / consistentTopStrain, 2) - 0.945)))); + return objectStrains.Sum(s => 1.1 / (1 + Math.Exp(-10 * (s / consistentTopStrain - 0.88)))); } } } \ No newline at end of file From 6c943681b0518848b5ace9755835e16996c505aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 16 Apr 2024 16:07:56 +0200 Subject: [PATCH 0543/2493] Fix preview tracks playing after their owning overlay has hidden RFC. Closes https://github.com/ppy/osu/issues/27883. The idea here is that `PopOut()` is called _when the hide is requested_, so once an overlay trigger would hide, the overlay would `StopAnyPlaying()`, but because of async load things, the actual track would start playing after that but before the overlay has fully hidden. (That last part is significant because after the overlay has fully hidden, schedules save the day.) Due to the loose coupling between `PreviewTrackManager` and `IPreviewTrackOwner` there's really no easy way to handle this locally to the usages of the preview tracks. Heck, `PreviewTrackManager` doesn't really know which preview track owner is to be considered _present_ at any time, it just kinda works on vibes based on DI until the owner tells all of its preview tracks to stop. This solution causes the preview tracks to stop a little bit later but maybe that's fine? Just trying to not overthink the issue is all. No tests because this is going to suck to test automatically while it is pretty easy to test manually (got it in a few tries on master). The issue also mentions that the track can sometimes resume playing after the overlay is pulled up again, but I don't see that as a problem necessarily, and even if it was, it's not going to be that easy to address due to the aforementioned loose coupling - to fix that, play buttons would have to know who is the current preview track owner and cancel schedules upon determining that their preview track owner has gone away. --- osu.Game/Overlays/WaveOverlayContainer.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/osu.Game/Overlays/WaveOverlayContainer.cs b/osu.Game/Overlays/WaveOverlayContainer.cs index 0295ff467a..7744db5dd5 100644 --- a/osu.Game/Overlays/WaveOverlayContainer.cs +++ b/osu.Game/Overlays/WaveOverlayContainer.cs @@ -40,10 +40,12 @@ namespace osu.Game.Overlays protected override void PopOut() { - base.PopOut(); - Waves.Hide(); - this.FadeOut(WaveContainer.DISAPPEAR_DURATION, Easing.InQuint); + this.FadeOut(WaveContainer.DISAPPEAR_DURATION, Easing.InQuint) + // base call is responsible for stopping preview tracks. + // delay it until the fade has concluded to ensure that nothing inside the overlay has triggered + // another preview track playback in the meantime, leaving an "orphaned" preview playing. + .OnComplete(_ => base.PopOut()); } } } From a386068ed3cdad2b7f3009780c7e6f7709778119 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 17 Apr 2024 08:51:53 +0200 Subject: [PATCH 0544/2493] Add `ScoreInfo.TotalScoreWithoutMods` --- osu.Game/Database/RealmAccess.cs | 3 ++- osu.Game/Scoring/ScoreInfo.cs | 11 +++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/osu.Game/Database/RealmAccess.cs b/osu.Game/Database/RealmAccess.cs index 167d170c81..12a71f3510 100644 --- a/osu.Game/Database/RealmAccess.cs +++ b/osu.Game/Database/RealmAccess.cs @@ -91,8 +91,9 @@ namespace osu.Game.Database /// 38 2023-12-10 Add EndTimeObjectCount and TotalObjectCount to BeatmapInfo. /// 39 2023-12-19 Migrate any EndTimeObjectCount and TotalObjectCount values of 0 to -1 to better identify non-calculated values. /// 40 2023-12-21 Add ScoreInfo.Version to keep track of which build scores were set on. + /// 41 2024-04-17 Add ScoreInfo.TotalScoreWithoutMods for future mod multiplier rebalances. /// - private const int schema_version = 40; + private const int schema_version = 41; /// /// Lock object which is held during sections, blocking realm retrieval during blocking periods. diff --git a/osu.Game/Scoring/ScoreInfo.cs b/osu.Game/Scoring/ScoreInfo.cs index fd98107792..92c18c9c1e 100644 --- a/osu.Game/Scoring/ScoreInfo.cs +++ b/osu.Game/Scoring/ScoreInfo.cs @@ -65,8 +65,19 @@ namespace osu.Game.Scoring public bool DeletePending { get; set; } + /// + /// The total number of points awarded for the score. + /// public long TotalScore { get; set; } + /// + /// The total number of points awarded for the score without including mod multipliers. + /// + /// + /// The purpose of this property is to enable future lossless rebalances of mod multipliers. + /// + public long TotalScoreWithoutMods { get; set; } + /// /// The version of processing applied to calculate total score as stored in the database. /// If this does not match , From 6b0d577f0bb3943cc66cd5bff118b52315ea7418 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 17 Apr 2024 08:52:27 +0200 Subject: [PATCH 0545/2493] Add backmigration of `TotalScoreWithoutMods` for existing scores --- osu.Game/Database/RealmAccess.cs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/osu.Game/Database/RealmAccess.cs b/osu.Game/Database/RealmAccess.cs index 12a71f3510..ee8eb4491b 100644 --- a/osu.Game/Database/RealmAccess.cs +++ b/osu.Game/Database/RealmAccess.cs @@ -1109,6 +1109,19 @@ namespace osu.Game.Database } } + break; + + case 41: + foreach (var score in migration.NewRealm.All()) + { + double modMultiplier = 1; + + foreach (var mod in score.Mods) + modMultiplier *= mod.ScoreMultiplier; + + score.TotalScoreWithoutMods = (long)Math.Round(score.TotalScore / modMultiplier); + } + break; } From e0178802b81f5a62ad0ddb8ed02cab7d98d5ebd5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 17 Apr 2024 08:52:47 +0200 Subject: [PATCH 0546/2493] Populate `TotalScoreWithoutMods` on scores set locally --- osu.Game/Rulesets/Scoring/ScoreProcessor.cs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index 9d12daad04..70d7f0fe37 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -56,6 +56,14 @@ namespace osu.Game.Rulesets.Scoring /// public readonly BindableLong TotalScore = new BindableLong { MinValue = 0 }; + /// + /// The total number of points awarded for the score without including mod multipliers. + /// + /// + /// The purpose of this property is to enable future lossless rebalances of mod multipliers. + /// + public readonly BindableLong TotalScoreWithoutMods = new BindableLong { MinValue = 0 }; + /// /// The current accuracy. /// @@ -363,7 +371,8 @@ namespace osu.Game.Rulesets.Scoring double comboProgress = maximumComboPortion > 0 ? currentComboPortion / maximumComboPortion : 1; double accuracyProcess = maximumAccuracyJudgementCount > 0 ? (double)currentAccuracyJudgementCount / maximumAccuracyJudgementCount : 1; - TotalScore.Value = (long)Math.Round(ComputeTotalScore(comboProgress, accuracyProcess, currentBonusPortion) * scoreMultiplier); + TotalScoreWithoutMods.Value = (long)Math.Round(ComputeTotalScore(comboProgress, accuracyProcess, currentBonusPortion)); + TotalScore.Value = (long)Math.Round(TotalScoreWithoutMods.Value * scoreMultiplier); } private void updateRank() @@ -446,6 +455,7 @@ namespace osu.Game.Rulesets.Scoring score.MaximumStatistics[result] = MaximumResultCounts.GetValueOrDefault(result); // Populate total score after everything else. + score.TotalScoreWithoutMods = TotalScoreWithoutMods.Value; score.TotalScore = TotalScore.Value; } From 2f1a4cdaa4d46c0dfd230f618c57563cee6bb654 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 17 Apr 2024 09:08:15 +0200 Subject: [PATCH 0547/2493] Export and import `TotalScoreWithoutMods` to replays (or recalculate if it missing) --- .../Formats/LegacyScoreDecoderTest.cs | 74 +++++++++++++++++++ osu.Game/Database/RealmAccess.cs | 9 +-- .../Legacy/LegacyReplaySoloScoreInfo.cs | 4 + osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs | 15 ++++ 4 files changed, 94 insertions(+), 8 deletions(-) diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs index 43e471320e..2f8cb9a3b3 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs @@ -412,6 +412,80 @@ namespace osu.Game.Tests.Beatmaps.Formats }); } + [Test] + public void TestTotalScoreWithoutModsReadIfPresent() + { + var ruleset = new OsuRuleset().RulesetInfo; + + var scoreInfo = TestResources.CreateTestScoreInfo(ruleset); + scoreInfo.Mods = new Mod[] + { + new OsuModDoubleTime { SpeedChange = { Value = 1.1 } } + }; + scoreInfo.OnlineID = 123123; + scoreInfo.ClientVersion = "2023.1221.0"; + scoreInfo.TotalScoreWithoutMods = 1_000_000; + scoreInfo.TotalScore = 1_020_000; + + var beatmap = new TestBeatmap(ruleset); + var score = new Score + { + ScoreInfo = scoreInfo, + Replay = new Replay + { + Frames = new List + { + new OsuReplayFrame(2000, OsuPlayfield.BASE_SIZE / 2, OsuAction.LeftButton) + } + } + }; + + var decodedAfterEncode = encodeThenDecode(LegacyBeatmapDecoder.LATEST_VERSION, score, beatmap); + + Assert.Multiple(() => + { + Assert.That(decodedAfterEncode.ScoreInfo.TotalScoreWithoutMods, Is.EqualTo(1_000_000)); + Assert.That(decodedAfterEncode.ScoreInfo.TotalScore, Is.EqualTo(1_020_000)); + }); + } + + [Test] + public void TestTotalScoreWithoutModsBackwardsPopulatedIfMissing() + { + var ruleset = new OsuRuleset().RulesetInfo; + + var scoreInfo = TestResources.CreateTestScoreInfo(ruleset); + scoreInfo.Mods = new Mod[] + { + new OsuModDoubleTime { SpeedChange = { Value = 1.1 } } + }; + scoreInfo.OnlineID = 123123; + scoreInfo.ClientVersion = "2023.1221.0"; + scoreInfo.TotalScoreWithoutMods = 0; + scoreInfo.TotalScore = 1_020_000; + + var beatmap = new TestBeatmap(ruleset); + var score = new Score + { + ScoreInfo = scoreInfo, + Replay = new Replay + { + Frames = new List + { + new OsuReplayFrame(2000, OsuPlayfield.BASE_SIZE / 2, OsuAction.LeftButton) + } + } + }; + + var decodedAfterEncode = encodeThenDecode(LegacyBeatmapDecoder.LATEST_VERSION, score, beatmap); + + Assert.Multiple(() => + { + Assert.That(decodedAfterEncode.ScoreInfo.TotalScoreWithoutMods, Is.EqualTo(1_000_000)); + Assert.That(decodedAfterEncode.ScoreInfo.TotalScore, Is.EqualTo(1_020_000)); + }); + } + private static Score encodeThenDecode(int beatmapVersion, Score score, TestBeatmap beatmap) { var encodeStream = new MemoryStream(); diff --git a/osu.Game/Database/RealmAccess.cs b/osu.Game/Database/RealmAccess.cs index ee8eb4491b..c075c3d82b 100644 --- a/osu.Game/Database/RealmAccess.cs +++ b/osu.Game/Database/RealmAccess.cs @@ -1113,14 +1113,7 @@ namespace osu.Game.Database case 41: foreach (var score in migration.NewRealm.All()) - { - double modMultiplier = 1; - - foreach (var mod in score.Mods) - modMultiplier *= mod.ScoreMultiplier; - - score.TotalScoreWithoutMods = (long)Math.Round(score.TotalScore / modMultiplier); - } + LegacyScoreDecoder.PopulateTotalScoreWithoutMods(score); break; } diff --git a/osu.Game/Scoring/Legacy/LegacyReplaySoloScoreInfo.cs b/osu.Game/Scoring/Legacy/LegacyReplaySoloScoreInfo.cs index afdcef1d21..c11e18462a 100644 --- a/osu.Game/Scoring/Legacy/LegacyReplaySoloScoreInfo.cs +++ b/osu.Game/Scoring/Legacy/LegacyReplaySoloScoreInfo.cs @@ -38,6 +38,9 @@ namespace osu.Game.Scoring.Legacy [JsonProperty("client_version")] public string ClientVersion = string.Empty; + [JsonProperty("total_score_without_mods")] + public long? TotalScoreWithoutMods { get; set; } + public static LegacyReplaySoloScoreInfo FromScore(ScoreInfo score) => new LegacyReplaySoloScoreInfo { OnlineID = score.OnlineID, @@ -45,6 +48,7 @@ namespace osu.Game.Scoring.Legacy Statistics = score.Statistics.Where(kvp => kvp.Value != 0).ToDictionary(), MaximumStatistics = score.MaximumStatistics.Where(kvp => kvp.Value != 0).ToDictionary(), ClientVersion = score.ClientVersion, + TotalScoreWithoutMods = score.TotalScoreWithoutMods > 0 ? score.TotalScoreWithoutMods : null, }; } } diff --git a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs index 65e2c02655..2358c0dfec 100644 --- a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs +++ b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs @@ -129,6 +129,11 @@ namespace osu.Game.Scoring.Legacy score.ScoreInfo.MaximumStatistics = readScore.MaximumStatistics; score.ScoreInfo.Mods = readScore.Mods.Select(m => m.ToMod(currentRuleset)).ToArray(); score.ScoreInfo.ClientVersion = readScore.ClientVersion; + + if (readScore.TotalScoreWithoutMods is long totalScoreWithoutMods) + score.ScoreInfo.TotalScoreWithoutMods = totalScoreWithoutMods; + else + PopulateTotalScoreWithoutMods(score.ScoreInfo); }); } } @@ -237,6 +242,16 @@ namespace osu.Game.Scoring.Legacy #pragma warning restore CS0618 } + public static void PopulateTotalScoreWithoutMods(ScoreInfo score) + { + double modMultiplier = 1; + + foreach (var mod in score.Mods) + modMultiplier *= mod.ScoreMultiplier; + + score.TotalScoreWithoutMods = (long)Math.Round(score.TotalScore / modMultiplier); + } + private void readLegacyReplay(Replay replay, StreamReader reader) { float lastTime = beatmapOffset; From 18bb81e7a7e4de6cf15ce0674ae44ef9f041ff84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 17 Apr 2024 09:11:47 +0200 Subject: [PATCH 0548/2493] Populate total score without mods when performing standardised score migration --- osu.Game/Database/StandardisedScoreMigrationTools.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Database/StandardisedScoreMigrationTools.cs b/osu.Game/Database/StandardisedScoreMigrationTools.cs index 6f2f8d64fa..7d09ebdb40 100644 --- a/osu.Game/Database/StandardisedScoreMigrationTools.cs +++ b/osu.Game/Database/StandardisedScoreMigrationTools.cs @@ -16,6 +16,7 @@ using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring.Legacy; using osu.Game.Scoring; +using osu.Game.Scoring.Legacy; namespace osu.Game.Database { @@ -248,6 +249,7 @@ namespace osu.Game.Database score.Accuracy = computeAccuracy(score, scoreProcessor); score.Rank = computeRank(score, scoreProcessor); score.TotalScore = convertFromLegacyTotalScore(score, ruleset, beatmap); + LegacyScoreDecoder.PopulateTotalScoreWithoutMods(score); } /// From e11e9fe14ff41af53eb6ccbbf9eb79952d045989 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 17 Apr 2024 09:15:50 +0200 Subject: [PATCH 0549/2493] Add `TotalScoreWithoutMods` to `SoloScoreInfo` End goal being storing it server-side. --- osu.Game/Online/API/Requests/Responses/SoloScoreInfo.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/osu.Game/Online/API/Requests/Responses/SoloScoreInfo.cs b/osu.Game/Online/API/Requests/Responses/SoloScoreInfo.cs index 64caddb2fc..36f1311f9d 100644 --- a/osu.Game/Online/API/Requests/Responses/SoloScoreInfo.cs +++ b/osu.Game/Online/API/Requests/Responses/SoloScoreInfo.cs @@ -33,6 +33,9 @@ namespace osu.Game.Online.API.Requests.Responses [JsonProperty("total_score")] public long TotalScore { get; set; } + [JsonProperty("total_score_without_mods")] + public long TotalScoreWithoutMods { get; set; } + [JsonProperty("accuracy")] public double Accuracy { get; set; } @@ -206,6 +209,7 @@ namespace osu.Game.Online.API.Requests.Responses Ruleset = new RulesetInfo { OnlineID = RulesetID }, Passed = Passed, TotalScore = TotalScore, + TotalScoreWithoutMods = TotalScoreWithoutMods, LegacyTotalScore = LegacyTotalScore, Accuracy = Accuracy, MaxCombo = MaxCombo, @@ -239,6 +243,7 @@ namespace osu.Game.Online.API.Requests.Responses { Rank = score.Rank, TotalScore = score.TotalScore, + TotalScoreWithoutMods = score.TotalScoreWithoutMods, Accuracy = score.Accuracy, PP = score.PP, MaxCombo = score.MaxCombo, From 03e13ddc3095e8713a90dab9bb5f672b0ae75d4d Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 17 Apr 2024 17:10:19 +0900 Subject: [PATCH 0550/2493] Globally silence Discord RPC registration failures --- osu.Desktop/DiscordRichPresence.cs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/osu.Desktop/DiscordRichPresence.cs b/osu.Desktop/DiscordRichPresence.cs index f1c796d0cd..74ebd38f2c 100644 --- a/osu.Desktop/DiscordRichPresence.cs +++ b/osu.Desktop/DiscordRichPresence.cs @@ -6,7 +6,6 @@ using System.Text; using DiscordRPC; using DiscordRPC.Message; using Newtonsoft.Json; -using osu.Framework; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.ObjectExtensions; @@ -80,14 +79,20 @@ namespace osu.Desktop client.OnReady += onReady; client.OnError += (_, e) => Logger.Log($"An error occurred with Discord RPC Client: {e.Message} ({e.Code})", LoggingTarget.Network, LogLevel.Error); - // A URI scheme is required to support game invitations, as well as informing Discord of the game executable path to support launching the game when a user clicks on join/spectate. - // The library doesn't properly support URI registration when ran from an app bundle on macOS. - if (!RuntimeInfo.IsApple) + try { client.RegisterUriScheme(); client.Subscribe(EventType.Join); client.OnJoin += onJoin; } + catch (Exception ex) + { + // This is known to fail in at least the following sandboxed environments: + // - macOS (when packaged as an app bundle) + // - flatpak (see: https://github.com/flathub/sh.ppy.osu/issues/170) + // There is currently no better way to do this offered by Discord, so the best we can do is simply ignore it for now. + Logger.Log($"Failed to register Discord URI scheme: {ex}"); + } client.Initialize(); } From 2a3ae6bce178124e65ca14d8ea62e82a6aa05ded Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 17 Apr 2024 02:34:41 +0300 Subject: [PATCH 0551/2493] Update all `TabItem` implementations to play select sample on `OnActivatedByUser` --- .../Graphics/UserInterface/OsuTabControl.cs | 24 ++++++++++++------- .../Graphics/UserInterface/PageTabControl.cs | 14 ++++++++++- .../BeatmapListingCardSizeTabControl.cs | 12 ++++++++-- .../Overlays/BeatmapListing/FilterTabItem.cs | 12 ++++++++-- .../OverlayPanelDisplayStyleControl.cs | 14 ++++++++++- osu.Game/Overlays/OverlayRulesetTabItem.cs | 14 ++++++++++- osu.Game/Overlays/OverlayStreamItem.cs | 12 ++++++++-- osu.Game/Overlays/OverlayTabControl.cs | 14 ++++++++++- .../Toolbar/ToolbarRulesetSelector.cs | 4 ---- .../Toolbar/ToolbarRulesetTabButton.cs | 12 ++++++++++ .../Match/Components/MatchTypePicker.cs | 11 +++++++-- 11 files changed, 119 insertions(+), 24 deletions(-) diff --git a/osu.Game/Graphics/UserInterface/OsuTabControl.cs b/osu.Game/Graphics/UserInterface/OsuTabControl.cs index f24977927f..5ce384c53c 100644 --- a/osu.Game/Graphics/UserInterface/OsuTabControl.cs +++ b/osu.Game/Graphics/UserInterface/OsuTabControl.cs @@ -8,6 +8,8 @@ using System.Linq; using osuTK; using osuTK.Graphics; using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Audio.Sample; using osu.Framework.Bindables; using osu.Framework.Extensions; using osu.Framework.Extensions.Color4Extensions; @@ -143,13 +145,6 @@ namespace osu.Game.Graphics.UserInterface FadeUnhovered(); } - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { - if (accentColour == default) - AccentColour = colours.Blue; - } - public OsuTabItem(T value) : base(value) { @@ -196,10 +191,21 @@ namespace osu.Game.Graphics.UserInterface Origin = Anchor.BottomLeft, Anchor = Anchor.BottomLeft, }, - new HoverClickSounds(HoverSampleSet.TabSelect) + new HoverSounds(HoverSampleSet.TabSelect) }; } + private Sample selectSample; + + [BackgroundDependencyLoader] + private void load(OsuColour colours, AudioManager audio) + { + if (accentColour == default) + AccentColour = colours.Blue; + + selectSample = audio.Samples.Get(@"UI/tabselect-select"); + } + protected override void OnActivated() { Text.Font = Text.Font.With(weight: FontWeight.Bold); @@ -211,6 +217,8 @@ namespace osu.Game.Graphics.UserInterface Text.Font = Text.Font.With(weight: FontWeight.Medium); FadeUnhovered(); } + + protected override void OnActivatedByUser() => selectSample.Play(); } } } diff --git a/osu.Game/Graphics/UserInterface/PageTabControl.cs b/osu.Game/Graphics/UserInterface/PageTabControl.cs index 2fe8acfbd5..44c659f945 100644 --- a/osu.Game/Graphics/UserInterface/PageTabControl.cs +++ b/osu.Game/Graphics/UserInterface/PageTabControl.cs @@ -7,6 +7,8 @@ using System; using osuTK; using osuTK.Graphics; using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Audio.Sample; using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Shapes; @@ -53,6 +55,8 @@ namespace osu.Game.Graphics.UserInterface } } + private Sample selectSample = null!; + public PageTabItem(T value) : base(value) { @@ -78,12 +82,18 @@ namespace osu.Game.Graphics.UserInterface Origin = Anchor.BottomLeft, Anchor = Anchor.BottomLeft, }, - new HoverClickSounds(HoverSampleSet.TabSelect) + new HoverSounds(HoverSampleSet.TabSelect) }; Active.BindValueChanged(active => Text.Font = Text.Font.With(Typeface.Torus, weight: active.NewValue ? FontWeight.Bold : FontWeight.Medium), true); } + [BackgroundDependencyLoader] + private void load(AudioManager audio) + { + selectSample = audio.Samples.Get(@"UI/tabselect-select"); + } + protected virtual LocalisableString CreateText() => (Value as Enum)?.GetLocalisableDescription() ?? Value.ToString(); protected override bool OnHover(HoverEvent e) @@ -112,6 +122,8 @@ namespace osu.Game.Graphics.UserInterface protected override void OnActivated() => slideActive(); protected override void OnDeactivated() => slideInactive(); + + protected override void OnActivatedByUser() => selectSample.Play(); } } } diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapListingCardSizeTabControl.cs b/osu.Game/Overlays/BeatmapListing/BeatmapListingCardSizeTabControl.cs index 9cd0031e3d..63a533c92e 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapListingCardSizeTabControl.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapListingCardSizeTabControl.cs @@ -5,6 +5,8 @@ using System; using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Audio.Sample; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; @@ -47,13 +49,15 @@ namespace osu.Game.Overlays.BeatmapListing [Resolved] private OverlayColourProvider colourProvider { get; set; } + private Sample selectSample = null!; + public TabItem(BeatmapCardSize value) : base(value) { } [BackgroundDependencyLoader] - private void load() + private void load(AudioManager audio) { AutoSizeAxes = Axes.Both; Masking = true; @@ -79,8 +83,10 @@ namespace osu.Game.Overlays.BeatmapListing Icon = getIconForCardSize(Value) } }, - new HoverClickSounds(HoverSampleSet.TabSelect) + new HoverSounds(HoverSampleSet.TabSelect) }; + + selectSample = audio.Samples.Get(@"UI/tabselect-select"); } private static IconUsage getIconForCardSize(BeatmapCardSize cardSize) @@ -111,6 +117,8 @@ namespace osu.Game.Overlays.BeatmapListing updateState(); } + protected override void OnActivatedByUser() => selectSample.Play(); + protected override void OnDeactivated() { if (IsLoaded) diff --git a/osu.Game/Overlays/BeatmapListing/FilterTabItem.cs b/osu.Game/Overlays/BeatmapListing/FilterTabItem.cs index 831cf812ff..89bf61dd18 100644 --- a/osu.Game/Overlays/BeatmapListing/FilterTabItem.cs +++ b/osu.Game/Overlays/BeatmapListing/FilterTabItem.cs @@ -5,6 +5,8 @@ using System; using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Audio.Sample; using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.UserInterface; @@ -24,13 +26,15 @@ namespace osu.Game.Overlays.BeatmapListing private OsuSpriteText text; + private Sample selectSample = null!; + public FilterTabItem(T value) : base(value) { } [BackgroundDependencyLoader] - private void load() + private void load(AudioManager audio) { AutoSizeAxes = Axes.Both; AddRangeInternal(new Drawable[] @@ -40,10 +44,12 @@ namespace osu.Game.Overlays.BeatmapListing Font = OsuFont.GetFont(size: 13, weight: FontWeight.Regular), Text = LabelFor(Value) }, - new HoverClickSounds(HoverSampleSet.TabSelect) + new HoverSounds(HoverSampleSet.TabSelect) }); Enabled.Value = true; + + selectSample = audio.Samples.Get(@"UI/tabselect-select"); } protected override void LoadComplete() @@ -71,6 +77,8 @@ namespace osu.Game.Overlays.BeatmapListing protected override void OnDeactivated() => UpdateState(); + protected override void OnActivatedByUser() => selectSample.Play(); + /// /// Returns the label text to be used for the supplied . /// diff --git a/osu.Game/Overlays/OverlayPanelDisplayStyleControl.cs b/osu.Game/Overlays/OverlayPanelDisplayStyleControl.cs index d7d6bd4a2a..c2bea0ed91 100644 --- a/osu.Game/Overlays/OverlayPanelDisplayStyleControl.cs +++ b/osu.Game/Overlays/OverlayPanelDisplayStyleControl.cs @@ -11,6 +11,8 @@ using osuTK; using osu.Framework.Input.Events; using osu.Game.Graphics.UserInterface; using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Audio.Sample; using osuTK.Graphics; using osu.Framework.Graphics.Cursor; using osu.Framework.Localisation; @@ -65,6 +67,8 @@ namespace osu.Game.Overlays private readonly SpriteIcon icon; + private Sample selectSample = null!; + public PanelDisplayTabItem(OverlayPanelDisplayStyle value) : base(value) { @@ -78,14 +82,22 @@ namespace osu.Game.Overlays RelativeSizeAxes = Axes.Both, FillMode = FillMode.Fit }, - new HoverClickSounds() + new HoverSounds(HoverSampleSet.TabSelect) }); } + [BackgroundDependencyLoader] + private void load(AudioManager audio) + { + selectSample = audio.Samples.Get(@"UI/tabselect-select"); + } + protected override void OnActivated() => updateState(); protected override void OnDeactivated() => updateState(); + protected override void OnActivatedByUser() => selectSample.Play(); + protected override bool OnHover(HoverEvent e) { updateState(); diff --git a/osu.Game/Overlays/OverlayRulesetTabItem.cs b/osu.Game/Overlays/OverlayRulesetTabItem.cs index 6d318820b3..b245486adf 100644 --- a/osu.Game/Overlays/OverlayRulesetTabItem.cs +++ b/osu.Game/Overlays/OverlayRulesetTabItem.cs @@ -10,6 +10,8 @@ using osu.Game.Rulesets; using osuTK.Graphics; using osuTK; using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Audio.Sample; using osu.Framework.Graphics.Cursor; using osu.Framework.Localisation; using osu.Game.Graphics.Containers; @@ -39,6 +41,8 @@ namespace osu.Game.Overlays public LocalisableString TooltipText => Value.Name; + private Sample selectSample = null!; + public OverlayRulesetTabItem(RulesetInfo value) : base(value) { @@ -59,12 +63,18 @@ namespace osu.Game.Overlays Icon = value.CreateInstance().CreateIcon(), }, }, - new HoverClickSounds() + new HoverSounds(HoverSampleSet.TabSelect) }); Enabled.Value = true; } + [BackgroundDependencyLoader] + private void load(AudioManager audio) + { + selectSample = audio.Samples.Get(@"UI/tabselect-select"); + } + protected override void LoadComplete() { base.LoadComplete(); @@ -90,6 +100,8 @@ namespace osu.Game.Overlays protected override void OnDeactivated() => updateState(); + protected override void OnActivatedByUser() => selectSample.Play(); + private void updateState() { AccentColour = Enabled.Value ? getActiveColour() : colourProvider.Foreground1; diff --git a/osu.Game/Overlays/OverlayStreamItem.cs b/osu.Game/Overlays/OverlayStreamItem.cs index 45181c13e4..f0ae0b41fc 100644 --- a/osu.Game/Overlays/OverlayStreamItem.cs +++ b/osu.Game/Overlays/OverlayStreamItem.cs @@ -11,6 +11,8 @@ using osu.Framework.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osu.Framework.Graphics.Sprites; using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Audio.Sample; using osu.Game.Graphics.Sprites; using osu.Game.Graphics; using osuTK.Graphics; @@ -49,8 +51,10 @@ namespace osu.Game.Overlays Margin = new MarginPadding(PADDING); } + private Sample selectSample; + [BackgroundDependencyLoader] - private void load(OverlayColourProvider colourProvider, OsuColour colours) + private void load(OverlayColourProvider colourProvider, OsuColour colours, AudioManager audio) { AddRange(new Drawable[] { @@ -87,9 +91,11 @@ namespace osu.Game.Overlays CollapsedSize = 2, Expanded = true }, - new HoverClickSounds() + new HoverSounds(HoverSampleSet.TabSelect) }); + selectSample = audio.Samples.Get(@"UI/tabselect-select"); + SelectedItem.BindValueChanged(_ => updateState(), true); } @@ -105,6 +111,8 @@ namespace osu.Game.Overlays protected override void OnDeactivated() => updateState(); + protected override void OnActivatedByUser() => selectSample.Play(); + protected override bool OnHover(HoverEvent e) { updateState(); diff --git a/osu.Game/Overlays/OverlayTabControl.cs b/osu.Game/Overlays/OverlayTabControl.cs index 884e31868f..a27caa13f1 100644 --- a/osu.Game/Overlays/OverlayTabControl.cs +++ b/osu.Game/Overlays/OverlayTabControl.cs @@ -4,6 +4,8 @@ #nullable disable using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Audio.Sample; using osu.Framework.Graphics; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.UserInterface; @@ -80,6 +82,8 @@ namespace osu.Game.Overlays } } + private Sample selectSample = null!; + public OverlayTabItem(T value) : base(value) { @@ -101,10 +105,16 @@ namespace osu.Game.Overlays ExpandedSize = 5f, CollapsedSize = 0 }, - new HoverClickSounds(HoverSampleSet.TabSelect) + new HoverSounds(HoverSampleSet.TabSelect) }; } + [BackgroundDependencyLoader] + private void load(AudioManager audio) + { + selectSample = audio.Samples.Get(@"UI/tabselect-select"); + } + protected override bool OnHover(HoverEvent e) { base.OnHover(e); @@ -136,6 +146,8 @@ namespace osu.Game.Overlays Text.Font = Text.Font.With(weight: FontWeight.Medium); } + protected override void OnActivatedByUser() => selectSample.Play(); + private void updateState() { if (Active.Value) diff --git a/osu.Game/Overlays/Toolbar/ToolbarRulesetSelector.cs b/osu.Game/Overlays/Toolbar/ToolbarRulesetSelector.cs index 723c24597a..518152e455 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarRulesetSelector.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarRulesetSelector.cs @@ -88,10 +88,6 @@ namespace osu.Game.Overlays.Toolbar if (SelectedTab != null) { ModeButtonLine.MoveToX(SelectedTab.DrawPosition.X, !hasInitialPosition ? 0 : 500, Easing.OutElasticQuarter); - - if (hasInitialPosition) - selectionSamples[SelectedTab.Value.ShortName]?.Play(); - hasInitialPosition = true; } } diff --git a/osu.Game/Overlays/Toolbar/ToolbarRulesetTabButton.cs b/osu.Game/Overlays/Toolbar/ToolbarRulesetTabButton.cs index 3287ac6eaa..0315bede64 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarRulesetTabButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarRulesetTabButton.cs @@ -2,6 +2,8 @@ // See the LICENCE file in the repository root for full licence text. 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.UserInterface; @@ -17,6 +19,8 @@ namespace osu.Game.Overlays.Toolbar { private readonly RulesetButton ruleset; + private Sample? selectSample; + public ToolbarRulesetTabButton(RulesetInfo value) : base(value) { @@ -34,10 +38,18 @@ namespace osu.Game.Overlays.Toolbar ruleset.SetIcon(rInstance.CreateIcon()); } + [BackgroundDependencyLoader] + private void load(AudioManager audio) + { + selectSample = audio.Samples.Get($@"UI/ruleset-select-{Value.ShortName}"); + } + protected override void OnActivated() => ruleset.Active = true; protected override void OnDeactivated() => ruleset.Active = false; + protected override void OnActivatedByUser() => selectSample?.Play(); + private partial class RulesetButton : ToolbarButton { protected override HoverSounds CreateHoverSounds(HoverSampleSet sampleSet) => new HoverSounds(); diff --git a/osu.Game/Screens/OnlinePlay/Match/Components/MatchTypePicker.cs b/osu.Game/Screens/OnlinePlay/Match/Components/MatchTypePicker.cs index 995fce085e..477336e8ea 100644 --- a/osu.Game/Screens/OnlinePlay/Match/Components/MatchTypePicker.cs +++ b/osu.Game/Screens/OnlinePlay/Match/Components/MatchTypePicker.cs @@ -4,6 +4,8 @@ #nullable disable using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Audio.Sample; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; @@ -78,14 +80,17 @@ namespace osu.Game.Screens.OnlinePlay.Match.Components }, }, }, - new HoverClickSounds(), + new HoverSounds(HoverSampleSet.TabSelect), }; } + private Sample selectSample; + [BackgroundDependencyLoader] - private void load(OsuColour colours) + private void load(OsuColour colours, AudioManager audio) { selection.Colour = colours.Yellow; + selectSample = audio.Samples.Get(@"UI/tabselect-select"); } protected override bool OnHover(HoverEvent e) @@ -109,6 +114,8 @@ namespace osu.Game.Screens.OnlinePlay.Match.Components { selection.FadeOut(transition_duration, Easing.OutQuint); } + + protected override void OnActivatedByUser() => selectSample.Play(); } } } From 50a21fb7273489f682b680877ad69200a2d4c227 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 17 Apr 2024 02:38:39 +0300 Subject: [PATCH 0552/2493] Change `ToolbarRulesetSelector` to use `SelectItem` to trigger new sound feedback path --- osu.Game/Overlays/Toolbar/ToolbarRulesetSelector.cs | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/osu.Game/Overlays/Toolbar/ToolbarRulesetSelector.cs b/osu.Game/Overlays/Toolbar/ToolbarRulesetSelector.cs index 518152e455..d49c340ed4 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarRulesetSelector.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarRulesetSelector.cs @@ -3,11 +3,8 @@ #nullable disable -using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; -using osu.Framework.Audio; -using osu.Framework.Audio.Sample; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; @@ -24,8 +21,6 @@ namespace osu.Game.Overlays.Toolbar { protected Drawable ModeButtonLine { get; private set; } - private readonly Dictionary selectionSamples = new Dictionary(); - public ToolbarRulesetSelector() { RelativeSizeAxes = Axes.Y; @@ -33,7 +28,7 @@ namespace osu.Game.Overlays.Toolbar } [BackgroundDependencyLoader] - private void load(AudioManager audio) + private void load() { AddRangeInternal(new[] { @@ -59,9 +54,6 @@ namespace osu.Game.Overlays.Toolbar } }, }); - - foreach (var ruleset in Rulesets.AvailableRulesets) - selectionSamples[ruleset.ShortName] = audio.Samples.Get($"UI/ruleset-select-{ruleset.ShortName}"); } protected override void LoadComplete() @@ -117,7 +109,7 @@ namespace osu.Game.Overlays.Toolbar RulesetInfo found = Rulesets.AvailableRulesets.ElementAtOrDefault(requested); if (found != null) - Current.Value = found; + SelectItem(found); return true; } From 42892acc1adaf8be01897de0c4e5fef7b6319a4d Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 17 Apr 2024 02:42:24 +0300 Subject: [PATCH 0553/2493] Fix multiple selection filter tab items no longer playing sounds These tab items are not managed by a `TabControl`, activation events are manually served by the class itself toggling `Active` when clicked. --- .../BeatmapSearchMultipleSelectionFilterRow.cs | 1 + osu.Game/Overlays/BeatmapListing/FilterTabItem.cs | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapSearchMultipleSelectionFilterRow.cs b/osu.Game/Overlays/BeatmapListing/BeatmapSearchMultipleSelectionFilterRow.cs index e59beb43ff..0a4c2b1e21 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapSearchMultipleSelectionFilterRow.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapSearchMultipleSelectionFilterRow.cs @@ -129,6 +129,7 @@ namespace osu.Game.Overlays.BeatmapListing { base.OnClick(e); Active.Toggle(); + SelectSample.Play(); return true; } } diff --git a/osu.Game/Overlays/BeatmapListing/FilterTabItem.cs b/osu.Game/Overlays/BeatmapListing/FilterTabItem.cs index 89bf61dd18..ee188d34ce 100644 --- a/osu.Game/Overlays/BeatmapListing/FilterTabItem.cs +++ b/osu.Game/Overlays/BeatmapListing/FilterTabItem.cs @@ -26,7 +26,7 @@ namespace osu.Game.Overlays.BeatmapListing private OsuSpriteText text; - private Sample selectSample = null!; + protected Sample SelectSample { get; private set; } = null!; public FilterTabItem(T value) : base(value) @@ -49,7 +49,7 @@ namespace osu.Game.Overlays.BeatmapListing Enabled.Value = true; - selectSample = audio.Samples.Get(@"UI/tabselect-select"); + SelectSample = audio.Samples.Get(@"UI/tabselect-select"); } protected override void LoadComplete() @@ -77,7 +77,7 @@ namespace osu.Game.Overlays.BeatmapListing protected override void OnDeactivated() => UpdateState(); - protected override void OnActivatedByUser() => selectSample.Play(); + protected override void OnActivatedByUser() => SelectSample.Play(); /// /// Returns the label text to be used for the supplied . From 9e692686760ea1f00cf661bc011da18e8b4b43a0 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 17 Apr 2024 02:48:32 +0300 Subject: [PATCH 0554/2493] Change editor screen selection logic to use `SelectItem` for sound feedback --- osu.Game/Screens/Edit/Editor.cs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index c1f6c02301..37f4b4f5be 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -372,7 +372,7 @@ namespace osu.Game.Screens.Edit } } }, - new EditorScreenSwitcherControl + screenSwitcher = new EditorScreenSwitcherControl { Anchor = Anchor.BottomRight, Origin = Anchor.BottomRight, @@ -662,23 +662,23 @@ namespace osu.Game.Screens.Edit return true; case GlobalAction.EditorComposeMode: - Mode.Value = EditorScreenMode.Compose; + screenSwitcher.SelectItem(EditorScreenMode.Compose); return true; case GlobalAction.EditorDesignMode: - Mode.Value = EditorScreenMode.Design; + screenSwitcher.SelectItem(EditorScreenMode.Design); return true; case GlobalAction.EditorTimingMode: - Mode.Value = EditorScreenMode.Timing; + screenSwitcher.SelectItem(EditorScreenMode.Timing); return true; case GlobalAction.EditorSetupMode: - Mode.Value = EditorScreenMode.SongSetup; + screenSwitcher.SelectItem(EditorScreenMode.SongSetup); return true; case GlobalAction.EditorVerifyMode: - Mode.Value = EditorScreenMode.Verify; + screenSwitcher.SelectItem(EditorScreenMode.Verify); return true; case GlobalAction.EditorTestGameplay: @@ -959,6 +959,8 @@ namespace osu.Game.Screens.Edit [CanBeNull] private ScheduledDelegate playbackDisabledDebounce; + private EditorScreenSwitcherControl screenSwitcher; + private void updateSampleDisabledState() { bool shouldDisableSamples = clock.SeekingOrStopped.Value From 24e8b88320af93d257674452178e878253ccb4fc Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 17 Apr 2024 03:00:27 +0300 Subject: [PATCH 0555/2493] Add inline comment to the custom implementation of multiple-selection tab items --- .../BeatmapListing/BeatmapSearchMultipleSelectionFilterRow.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapSearchMultipleSelectionFilterRow.cs b/osu.Game/Overlays/BeatmapListing/BeatmapSearchMultipleSelectionFilterRow.cs index 0a4c2b1e21..4bd25f6561 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapSearchMultipleSelectionFilterRow.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapSearchMultipleSelectionFilterRow.cs @@ -128,6 +128,9 @@ namespace osu.Game.Overlays.BeatmapListing protected override bool OnClick(ClickEvent e) { base.OnClick(e); + + // this tab item implementation is not managed by a TabControl, + // therefore we have to manually update Active state and play select sound when this tab item is clicked. Active.Toggle(); SelectSample.Play(); return true; From a7e043bdbe3b145b5e33c392e06061e9e71fc7b2 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 17 Apr 2024 03:08:29 +0300 Subject: [PATCH 0556/2493] Fix beatmap rulest selector playing sound for initial ruleset selection --- osu.Game/Overlays/BeatmapSet/BeatmapRulesetSelector.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/BeatmapSet/BeatmapRulesetSelector.cs b/osu.Game/Overlays/BeatmapSet/BeatmapRulesetSelector.cs index 426fbcdb8d..29744f27fc 100644 --- a/osu.Game/Overlays/BeatmapSet/BeatmapRulesetSelector.cs +++ b/osu.Game/Overlays/BeatmapSet/BeatmapRulesetSelector.cs @@ -21,7 +21,7 @@ namespace osu.Game.Overlays.BeatmapSet // propagate value to tab items first to enable only available rulesets. beatmapSet.Value = value; - SelectTab(TabContainer.TabItems.FirstOrDefault(t => t.Enabled.Value)); + Current.Value = TabContainer.TabItems.FirstOrDefault(t => t.Enabled.Value)?.Value; } } From 8d94f8d995cd34c2f9d7d6ab68a169b7e554c0f4 Mon Sep 17 00:00:00 2001 From: Arthur Araujo Date: Wed, 17 Apr 2024 08:14:19 -0300 Subject: [PATCH 0557/2493] Remove `BassFlags.Prescan` and free the decodeStream --- osu.Game/Rulesets/Edit/Checks/CheckHitsoundsFormat.cs | 7 ++++--- osu.Game/Rulesets/Edit/Checks/CheckSongFormat.cs | 4 +++- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/osu.Game/Rulesets/Edit/Checks/CheckHitsoundsFormat.cs b/osu.Game/Rulesets/Edit/Checks/CheckHitsoundsFormat.cs index f65b89fc01..c71c7b0ef3 100644 --- a/osu.Game/Rulesets/Edit/Checks/CheckHitsoundsFormat.cs +++ b/osu.Game/Rulesets/Edit/Checks/CheckHitsoundsFormat.cs @@ -47,7 +47,7 @@ namespace osu.Game.Rulesets.Edit.Checks continue; var fileCallbacks = new FileCallbacks(new DataStreamFileProcedures(data)); - int decodeStream = Bass.CreateStream(StreamSystem.NoBuffer, BassFlags.Decode | BassFlags.Prescan, fileCallbacks.Callbacks, fileCallbacks.Handle); + int decodeStream = Bass.CreateStream(StreamSystem.NoBuffer, BassFlags.Decode, fileCallbacks.Callbacks, fileCallbacks.Handle); // If the format is not supported by BASS if (decodeStream == 0) @@ -61,9 +61,10 @@ namespace osu.Game.Rulesets.Edit.Checks var audioInfo = Bass.ChannelGetInfo(decodeStream); if (!allowedFormats.Contains(audioInfo.ChannelType)) - { yield return new IssueTemplateIncorrectFormat(this).Create(file.Filename, audioInfo.ChannelType.ToString()); - } + + + Bass.StreamFree(decodeStream); } } } diff --git a/osu.Game/Rulesets/Edit/Checks/CheckSongFormat.cs b/osu.Game/Rulesets/Edit/Checks/CheckSongFormat.cs index ae90dd96d5..9ed1350f56 100644 --- a/osu.Game/Rulesets/Edit/Checks/CheckSongFormat.cs +++ b/osu.Game/Rulesets/Edit/Checks/CheckSongFormat.cs @@ -42,7 +42,7 @@ namespace osu.Game.Rulesets.Edit.Checks if (data == null || data.Length <= 0) yield break; var fileCallbacks = new FileCallbacks(new DataStreamFileProcedures(data)); - int decodeStream = Bass.CreateStream(StreamSystem.NoBuffer, BassFlags.Decode | BassFlags.Prescan, fileCallbacks.Callbacks, fileCallbacks.Handle); + int decodeStream = Bass.CreateStream(StreamSystem.NoBuffer, BassFlags.Decode, fileCallbacks.Callbacks, fileCallbacks.Handle); // If the format is not supported by BASS if (decodeStream == 0) @@ -55,6 +55,8 @@ namespace osu.Game.Rulesets.Edit.Checks if (!allowedFormats.Contains(audioInfo.ChannelType)) yield return new IssueTemplateIncorrectFormat(this).Create(audioFile.Filename, audioInfo.ChannelType.ToString()); + + Bass.StreamFree(decodeStream); } } From ac03856ebdb8f8ffff860879263f16ab2564756b Mon Sep 17 00:00:00 2001 From: Arthur Araujo Date: Wed, 17 Apr 2024 08:22:05 -0300 Subject: [PATCH 0558/2493] Fix test methods names --- osu.Game.Tests/Editing/Checks/CheckHitsoundsFormatTest.cs | 8 ++++---- osu.Game.Tests/Editing/Checks/CheckSongFormatTest.cs | 8 ++++---- osu.Game/Rulesets/Edit/Checks/CheckHitsoundsFormat.cs | 1 - 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/osu.Game.Tests/Editing/Checks/CheckHitsoundsFormatTest.cs b/osu.Game.Tests/Editing/Checks/CheckHitsoundsFormatTest.cs index f85a296c74..9a806f6cb7 100644 --- a/osu.Game.Tests/Editing/Checks/CheckHitsoundsFormatTest.cs +++ b/osu.Game.Tests/Editing/Checks/CheckHitsoundsFormatTest.cs @@ -44,7 +44,7 @@ namespace osu.Game.Tests.Editing.Checks } [Test] - public void TestMP3Audio() + public void TestMp3Audio() { using (var resourceStream = TestResources.OpenResource("Samples/test-sample-cut.mp3")) { @@ -55,7 +55,7 @@ namespace osu.Game.Tests.Editing.Checks } [Test] - public void TestOGGAudio() + public void TestOggAudio() { using (var resourceStream = TestResources.OpenResource("Samples/test-sample.ogg")) { @@ -65,7 +65,7 @@ namespace osu.Game.Tests.Editing.Checks } [Test] - public void TestWAVAudio() + public void TestWavAudio() { using (var resourceStream = TestResources.OpenResource("Samples/hitsound-delay.wav")) { @@ -75,7 +75,7 @@ namespace osu.Game.Tests.Editing.Checks } [Test] - public void TestWEBMAudio() + public void TestWebmAudio() { using (var resourceStream = TestResources.OpenResource("Samples/test-sample.webm")) { diff --git a/osu.Game.Tests/Editing/Checks/CheckSongFormatTest.cs b/osu.Game.Tests/Editing/Checks/CheckSongFormatTest.cs index 0755fdd8ac..98a4e1f9e9 100644 --- a/osu.Game.Tests/Editing/Checks/CheckSongFormatTest.cs +++ b/osu.Game.Tests/Editing/Checks/CheckSongFormatTest.cs @@ -44,7 +44,7 @@ namespace osu.Game.Tests.Editing.Checks } [Test] - public void TestMP3Audio() + public void TestMp3Audio() { using (var resourceStream = TestResources.OpenResource("Samples/test-sample-cut.mp3")) { @@ -55,7 +55,7 @@ namespace osu.Game.Tests.Editing.Checks } [Test] - public void TestOGGAudio() + public void TestOggAudio() { using (var resourceStream = TestResources.OpenResource("Samples/test-sample.ogg")) { @@ -66,7 +66,7 @@ namespace osu.Game.Tests.Editing.Checks } [Test] - public void TestWAVAudio() + public void TestWavAudio() { using (var resourceStream = TestResources.OpenResource("Samples/hitsound-delay.wav")) { @@ -78,7 +78,7 @@ namespace osu.Game.Tests.Editing.Checks } [Test] - public void TestWEBMAudio() + public void TestWebmAudio() { using (var resourceStream = TestResources.OpenResource("Samples/test-sample.webm")) { diff --git a/osu.Game/Rulesets/Edit/Checks/CheckHitsoundsFormat.cs b/osu.Game/Rulesets/Edit/Checks/CheckHitsoundsFormat.cs index c71c7b0ef3..3343d07f25 100644 --- a/osu.Game/Rulesets/Edit/Checks/CheckHitsoundsFormat.cs +++ b/osu.Game/Rulesets/Edit/Checks/CheckHitsoundsFormat.cs @@ -63,7 +63,6 @@ namespace osu.Game.Rulesets.Edit.Checks if (!allowedFormats.Contains(audioInfo.ChannelType)) yield return new IssueTemplateIncorrectFormat(this).Create(file.Filename, audioInfo.ChannelType.ToString()); - Bass.StreamFree(decodeStream); } } From f92833b588577f6e55f53948f97a8ed284867fb0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 18 Apr 2024 12:02:49 +0800 Subject: [PATCH 0559/2493] Add ability to quick exit from results screen --- osu.Game/Screens/Ranking/ResultsScreen.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs index e579c3fe51..f28b9b2554 100644 --- a/osu.Game/Screens/Ranking/ResultsScreen.cs +++ b/osu.Game/Screens/Ranking/ResultsScreen.cs @@ -191,6 +191,16 @@ namespace osu.Game.Screens.Ranking }); } + AddInternal(new HotkeyExitOverlay + { + Action = () => + { + if (!this.IsCurrentScreen()) return; + + this.Exit(); + }, + }); + if (player != null && AllowRetry) { buttons.Add(new RetryButton { Width = 300 }); From d08b1e5ae7a413c3ec6a958b1bc592e20baf6c8e Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Wed, 17 Apr 2024 21:18:54 -0700 Subject: [PATCH 0560/2493] Add xmldoc to `GetScore()` and `Export()` and remove xmldoc from private `getDatabasedScoreInfo()` --- osu.Game/Scoring/ScoreManager.cs | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/osu.Game/Scoring/ScoreManager.cs b/osu.Game/Scoring/ScoreManager.cs index 3f6c6ee49d..b6bb637537 100644 --- a/osu.Game/Scoring/ScoreManager.cs +++ b/osu.Game/Scoring/ScoreManager.cs @@ -58,6 +58,15 @@ namespace osu.Game.Scoring }; } + /// + /// Retrieve a from a given . + /// + /// The to convert. + /// The . Null if the score on the database cannot be found. + /// + /// The is re-retrieved from the database to ensure all the required data + /// for retrieving a replay are present (may have missing properties if it was retrieved from online data). + /// public Score? GetScore(IScoreInfo scoreInfo) { ScoreInfo? databasedScoreInfo = getDatabasedScoreInfo(scoreInfo); @@ -75,12 +84,6 @@ namespace osu.Game.Scoring return Realm.Run(r => r.All().FirstOrDefault(query)?.Detach()); } - /// - /// Re-retrieve a given from the database to ensure all the required data for presenting / exporting a replay are present. - /// - /// The to attempt querying on. - /// The databased score info. Null if the score on the database cannot be found. - /// Can be used when the was retrieved from online data, as it may have missing properties. private ScoreInfo? getDatabasedScoreInfo(IScoreInfo originalScoreInfo) { ScoreInfo? databasedScoreInfo = null; @@ -194,6 +197,15 @@ namespace osu.Game.Scoring public Task>> Import(ProgressNotification notification, ImportTask[] tasks, ImportParameters parameters = default) => scoreImporter.Import(notification, tasks); + /// + /// Export a replay from a given . + /// + /// The to export. + /// The . Null if the score on the database cannot be found. + /// + /// The is re-retrieved from the database to ensure all the required data + /// for exporting a replay are present (may have missing properties if it was retrieved from online data). + /// public Task? Export(ScoreInfo scoreInfo) { ScoreInfo? databasedScoreInfo = getDatabasedScoreInfo(scoreInfo); From 46e2cfdba526d77106d1eff41ec5e542ac7e8bca Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 18 Apr 2024 18:33:30 +0800 Subject: [PATCH 0561/2493] 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 2d7a9d2652..bf02a5d8e2 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -10,7 +10,7 @@ true - + diff --git a/osu.iOS.props b/osu.iOS.props index b2e3fc0779..4f973dbeb9 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -23,6 +23,6 @@ iossimulator-x64 - + From a41ae99dd7e8f39c909a3519cce1817469b139a9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 19 Apr 2024 15:14:52 +0800 Subject: [PATCH 0562/2493] 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 bf02a5d8e2..c61977cfa3 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -10,7 +10,7 @@ true - + diff --git a/osu.iOS.props b/osu.iOS.props index 4f973dbeb9..6389172fe7 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -23,6 +23,6 @@ iossimulator-x64 - + From 362a7b2c7735fa753e81c7075dba38b4d634e1be Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 19 Apr 2024 18:03:13 +0900 Subject: [PATCH 0563/2493] Remove unused members from GameplaySkinComponentLookup --- osu.Game.Rulesets.Catch/CatchSkinComponentLookup.cs | 4 ---- osu.Game.Rulesets.Mania/ManiaSkinComponentLookup.cs | 4 ---- osu.Game.Rulesets.Osu/OsuSkinComponentLookup.cs | 4 ---- osu.Game.Rulesets.Taiko/TaikoSkinComponentLookup.cs | 4 ---- osu.Game/Skinning/GameplaySkinComponentLookup.cs | 3 --- 5 files changed, 19 deletions(-) diff --git a/osu.Game.Rulesets.Catch/CatchSkinComponentLookup.cs b/osu.Game.Rulesets.Catch/CatchSkinComponentLookup.cs index 149aae1cb4..596b102ac5 100644 --- a/osu.Game.Rulesets.Catch/CatchSkinComponentLookup.cs +++ b/osu.Game.Rulesets.Catch/CatchSkinComponentLookup.cs @@ -11,9 +11,5 @@ namespace osu.Game.Rulesets.Catch : base(component) { } - - protected override string RulesetPrefix => "catch"; // todo: use CatchRuleset.SHORT_NAME; - - protected override string ComponentName => Component.ToString().ToLowerInvariant(); } } diff --git a/osu.Game.Rulesets.Mania/ManiaSkinComponentLookup.cs b/osu.Game.Rulesets.Mania/ManiaSkinComponentLookup.cs index 44120e16e6..046d1c5b34 100644 --- a/osu.Game.Rulesets.Mania/ManiaSkinComponentLookup.cs +++ b/osu.Game.Rulesets.Mania/ManiaSkinComponentLookup.cs @@ -15,10 +15,6 @@ namespace osu.Game.Rulesets.Mania : base(component) { } - - protected override string RulesetPrefix => ManiaRuleset.SHORT_NAME; - - protected override string ComponentName => Component.ToString().ToLowerInvariant(); } public enum ManiaSkinComponents diff --git a/osu.Game.Rulesets.Osu/OsuSkinComponentLookup.cs b/osu.Game.Rulesets.Osu/OsuSkinComponentLookup.cs index 81d5811f85..3b3653e1ba 100644 --- a/osu.Game.Rulesets.Osu/OsuSkinComponentLookup.cs +++ b/osu.Game.Rulesets.Osu/OsuSkinComponentLookup.cs @@ -11,9 +11,5 @@ namespace osu.Game.Rulesets.Osu : base(component) { } - - protected override string RulesetPrefix => OsuRuleset.SHORT_NAME; - - protected override string ComponentName => Component.ToString().ToLowerInvariant(); } } diff --git a/osu.Game.Rulesets.Taiko/TaikoSkinComponentLookup.cs b/osu.Game.Rulesets.Taiko/TaikoSkinComponentLookup.cs index c35971e9fd..8841c3d3ca 100644 --- a/osu.Game.Rulesets.Taiko/TaikoSkinComponentLookup.cs +++ b/osu.Game.Rulesets.Taiko/TaikoSkinComponentLookup.cs @@ -11,9 +11,5 @@ namespace osu.Game.Rulesets.Taiko : base(component) { } - - protected override string RulesetPrefix => TaikoRuleset.SHORT_NAME; - - protected override string ComponentName => Component.ToString().ToLowerInvariant(); } } diff --git a/osu.Game/Skinning/GameplaySkinComponentLookup.cs b/osu.Game/Skinning/GameplaySkinComponentLookup.cs index ec159873f8..c317a17e21 100644 --- a/osu.Game/Skinning/GameplaySkinComponentLookup.cs +++ b/osu.Game/Skinning/GameplaySkinComponentLookup.cs @@ -24,8 +24,5 @@ namespace osu.Game.Skinning { Component = component; } - - protected virtual string RulesetPrefix => string.Empty; - protected virtual string ComponentName => Component.ToString(); } } From 9d04b44a8872663476798264d8ab91943768f0da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 19 Apr 2024 11:04:05 +0200 Subject: [PATCH 0564/2493] Add failing test case --- .../UserInterface/TestSceneModSelectOverlay.cs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs index 6c75530a6e..8ddbd84890 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs @@ -612,6 +612,23 @@ namespace osu.Game.Tests.Visual.UserInterface AddAssert("search text box unfocused", () => !modSelectOverlay.SearchTextBox.HasFocus); } + [Test] + public void TestSearchBoxFocusToggleRespondsToExternalChanges() + { + AddStep("text search does not start active", () => configManager.SetValue(OsuSetting.ModSelectTextSearchStartsActive, false)); + createScreen(); + + AddUntilStep("search text box not focused", () => !modSelectOverlay.SearchTextBox.HasFocus); + + AddStep("press tab", () => InputManager.Key(Key.Tab)); + AddAssert("search text box focused", () => modSelectOverlay.SearchTextBox.HasFocus); + + AddStep("unfocus search text box externally", () => InputManager.ChangeFocus(null)); + + AddStep("press tab", () => InputManager.Key(Key.Tab)); + AddAssert("search text box focused", () => modSelectOverlay.SearchTextBox.HasFocus); + } + [Test] public void TestTextSearchDoesNotBlockCustomisationPanelKeyboardInteractions() { From 2dcbb823ef327f947547ed8f74990e74d021712d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 19 Apr 2024 11:08:34 +0200 Subject: [PATCH 0565/2493] Use textbox focus state directly rather than trying to track it independently --- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index 5ca26c739e..47362c0003 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -143,8 +143,6 @@ namespace osu.Game.Overlays.Mods protected ShearedToggleButton? CustomisationButton { get; private set; } protected SelectAllModsButton? SelectAllModsButton { get; set; } - private bool textBoxShouldFocus; - private Sample? columnAppearSample; private WorkingBeatmap? beatmap; @@ -542,7 +540,7 @@ namespace osu.Game.Overlays.Mods if (customisationVisible.Value) SearchTextBox.KillFocus(); else - setTextBoxFocus(textBoxShouldFocus); + setTextBoxFocus(textSearchStartsActive.Value); } /// @@ -798,15 +796,13 @@ namespace osu.Game.Overlays.Mods return false; // TODO: should probably eventually support typical platform search shortcuts (`Ctrl-F`, `/`) - setTextBoxFocus(!textBoxShouldFocus); + setTextBoxFocus(!SearchTextBox.HasFocus); return true; } - private void setTextBoxFocus(bool keepFocus) + private void setTextBoxFocus(bool focus) { - textBoxShouldFocus = keepFocus; - - if (textBoxShouldFocus) + if (focus) SearchTextBox.TakeFocus(); else SearchTextBox.KillFocus(); From 509862490e88f5ebea63005e23463336cb43f9d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 19 Apr 2024 11:11:18 +0200 Subject: [PATCH 0566/2493] Revert unnecessary changes --- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index 47362c0003..25293e8e20 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -418,7 +418,7 @@ namespace osu.Game.Overlays.Mods yield return new ColumnDimContainer(new ModPresetColumn { Margin = new MarginPadding { Right = 10 } - }, this); + }); } yield return createModColumnContent(ModType.DifficultyReduction); @@ -436,7 +436,7 @@ namespace osu.Game.Overlays.Mods column.Margin = new MarginPadding { Right = 10 }; }); - return new ColumnDimContainer(column, this); + return new ColumnDimContainer(column); } private void createLocalMods() @@ -895,17 +895,13 @@ namespace osu.Game.Overlays.Mods [Resolved] private OsuColour colours { get; set; } = null!; - private ModSelectOverlay modSelectOverlayInstance; - - public ColumnDimContainer(ModSelectColumn column, ModSelectOverlay modSelectOverlay) + public ColumnDimContainer(ModSelectColumn column) { AutoSizeAxes = Axes.X; RelativeSizeAxes = Axes.Y; Child = Column = column; column.Active.BindTo(Active); - - this.modSelectOverlayInstance = modSelectOverlay; } [BackgroundDependencyLoader] @@ -953,7 +949,7 @@ namespace osu.Game.Overlays.Mods RequestScroll?.Invoke(this); // Killing focus is done here because it's the only feasible place on ModSelectOverlay you can click on without triggering any action. - modSelectOverlayInstance.setTextBoxFocus(false); + Scheduler.Add(() => GetContainingInputManager().ChangeFocus(null)); return true; } From eac9dededfb59ab668cedc0e788aef11532cf152 Mon Sep 17 00:00:00 2001 From: Arthur Araujo <90941580+64ArthurAraujo@users.noreply.github.com> Date: Fri, 19 Apr 2024 07:20:59 -0300 Subject: [PATCH 0567/2493] Use periods instead of semicolons MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bartłomiej Dach --- osu.Game/Rulesets/Edit/Checks/CheckHitsoundsFormat.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/Edit/Checks/CheckHitsoundsFormat.cs b/osu.Game/Rulesets/Edit/Checks/CheckHitsoundsFormat.cs index 3343d07f25..d716f5ba3c 100644 --- a/osu.Game/Rulesets/Edit/Checks/CheckHitsoundsFormat.cs +++ b/osu.Game/Rulesets/Edit/Checks/CheckHitsoundsFormat.cs @@ -73,7 +73,7 @@ namespace osu.Game.Rulesets.Edit.Checks public class IssueTemplateFormatUnsupported : IssueTemplate { public IssueTemplateFormatUnsupported(ICheck check) - : base(check, IssueType.Problem, "\"{0}\" may be corrupt or using a unsupported audio format; Use wav or ogg for hitsounds.") + : base(check, IssueType.Problem, "\"{0}\" may be corrupt or using a unsupported audio format. Use wav or ogg for hitsounds.") { } @@ -83,7 +83,7 @@ namespace osu.Game.Rulesets.Edit.Checks public class IssueTemplateIncorrectFormat : IssueTemplate { public IssueTemplateIncorrectFormat(ICheck check) - : base(check, IssueType.Problem, "\"{0}\" is using a incorrect format ({1}); Use wav or ogg for hitsounds.") + : base(check, IssueType.Problem, "\"{0}\" is using a incorrect format ({1}). Use wav or ogg for hitsounds.") { } From 68e86b5105fae6a4dd72bada15774b4ad2a052e0 Mon Sep 17 00:00:00 2001 From: Arthur Araujo Date: Fri, 19 Apr 2024 07:38:55 -0300 Subject: [PATCH 0568/2493] Remove unsued import and add a blankline before `yield break;` --- osu.Game/Rulesets/Edit/Checks/CheckSongFormat.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Edit/Checks/CheckSongFormat.cs b/osu.Game/Rulesets/Edit/Checks/CheckSongFormat.cs index 9ed1350f56..7dd469833e 100644 --- a/osu.Game/Rulesets/Edit/Checks/CheckSongFormat.cs +++ b/osu.Game/Rulesets/Edit/Checks/CheckSongFormat.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.IO; using System.Linq; @@ -48,6 +47,7 @@ namespace osu.Game.Rulesets.Edit.Checks if (decodeStream == 0) { yield return new IssueTemplateFormatUnsupported(this).Create(audioFile.Filename); + yield break; } From 92b85beff6735e83f3324427478833d0b62d504d Mon Sep 17 00:00:00 2001 From: Arthur Araujo Date: Fri, 19 Apr 2024 08:01:23 -0300 Subject: [PATCH 0569/2493] Remove `ChannelType` from messages --- osu.Game/Rulesets/Edit/Checks/CheckHitsoundsFormat.cs | 6 +++--- osu.Game/Rulesets/Edit/Checks/CheckSongFormat.cs | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/osu.Game/Rulesets/Edit/Checks/CheckHitsoundsFormat.cs b/osu.Game/Rulesets/Edit/Checks/CheckHitsoundsFormat.cs index d716f5ba3c..184311b1c3 100644 --- a/osu.Game/Rulesets/Edit/Checks/CheckHitsoundsFormat.cs +++ b/osu.Game/Rulesets/Edit/Checks/CheckHitsoundsFormat.cs @@ -61,7 +61,7 @@ namespace osu.Game.Rulesets.Edit.Checks var audioInfo = Bass.ChannelGetInfo(decodeStream); if (!allowedFormats.Contains(audioInfo.ChannelType)) - yield return new IssueTemplateIncorrectFormat(this).Create(file.Filename, audioInfo.ChannelType.ToString()); + yield return new IssueTemplateIncorrectFormat(this).Create(file.Filename); Bass.StreamFree(decodeStream); } @@ -83,11 +83,11 @@ namespace osu.Game.Rulesets.Edit.Checks public class IssueTemplateIncorrectFormat : IssueTemplate { public IssueTemplateIncorrectFormat(ICheck check) - : base(check, IssueType.Problem, "\"{0}\" is using a incorrect format ({1}). Use wav or ogg for hitsounds.") + : base(check, IssueType.Problem, "\"{0}\" is using a incorrect format. Use wav or ogg for hitsounds.") { } - public Issue Create(string file, string format) => new Issue(this, file, format); + public Issue Create(string file) => new Issue(this, file); } } } diff --git a/osu.Game/Rulesets/Edit/Checks/CheckSongFormat.cs b/osu.Game/Rulesets/Edit/Checks/CheckSongFormat.cs index 7dd469833e..0cb54709af 100644 --- a/osu.Game/Rulesets/Edit/Checks/CheckSongFormat.cs +++ b/osu.Game/Rulesets/Edit/Checks/CheckSongFormat.cs @@ -54,7 +54,7 @@ namespace osu.Game.Rulesets.Edit.Checks var audioInfo = Bass.ChannelGetInfo(decodeStream); if (!allowedFormats.Contains(audioInfo.ChannelType)) - yield return new IssueTemplateIncorrectFormat(this).Create(audioFile.Filename, audioInfo.ChannelType.ToString()); + yield return new IssueTemplateIncorrectFormat(this).Create(audioFile.Filename); Bass.StreamFree(decodeStream); } @@ -63,7 +63,7 @@ namespace osu.Game.Rulesets.Edit.Checks public class IssueTemplateFormatUnsupported : IssueTemplate { public IssueTemplateFormatUnsupported(ICheck check) - : base(check, IssueType.Problem, "\"{0}\" may be corrupt or using a unsupported audio format; Use mp3 or ogg for the song's audio.") + : base(check, IssueType.Problem, "\"{0}\" may be corrupt or using a unsupported audio format. Use mp3 or ogg for the song's audio.") { } @@ -73,11 +73,11 @@ namespace osu.Game.Rulesets.Edit.Checks public class IssueTemplateIncorrectFormat : IssueTemplate { public IssueTemplateIncorrectFormat(ICheck check) - : base(check, IssueType.Problem, "\"{0}\" is using a incorrect format ({1}); Use mp3 or ogg for the song's audio.") + : base(check, IssueType.Problem, "\"{0}\" is using a incorrect format. Use mp3 or ogg for the song's audio.") { } - public Issue Create(string file, string format) => new Issue(this, file, format); + public Issue Create(string file) => new Issue(this, file); } } } From ddc1b90ee1836662e788e02f266992cecad91e91 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 19 Apr 2024 20:36:24 +0900 Subject: [PATCH 0570/2493] Remove unused method --- .../Skinning/ISerialisableDrawableContainer.cs | 5 ----- osu.Game/Skinning/SkinComponentsContainer.cs | 15 --------------- 2 files changed, 20 deletions(-) diff --git a/osu.Game/Skinning/ISerialisableDrawableContainer.cs b/osu.Game/Skinning/ISerialisableDrawableContainer.cs index a19c8c5162..57ea75bc7e 100644 --- a/osu.Game/Skinning/ISerialisableDrawableContainer.cs +++ b/osu.Game/Skinning/ISerialisableDrawableContainer.cs @@ -30,11 +30,6 @@ namespace osu.Game.Skinning /// void Reload(); - /// - /// Reload this target from the provided skinnable information. - /// - void Reload(SerialisedDrawableInfo[] skinnableInfo); - /// /// Add a new skinnable component to this target. /// diff --git a/osu.Game/Skinning/SkinComponentsContainer.cs b/osu.Game/Skinning/SkinComponentsContainer.cs index adf0a288b4..02ba43fd39 100644 --- a/osu.Game/Skinning/SkinComponentsContainer.cs +++ b/osu.Game/Skinning/SkinComponentsContainer.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Collections.Generic; using System.Linq; using System.Threading; using osu.Framework.Bindables; @@ -44,20 +43,6 @@ namespace osu.Game.Skinning Lookup = lookup; } - public void Reload(SerialisedDrawableInfo[] skinnableInfo) - { - var drawables = new List(); - - foreach (var i in skinnableInfo) - drawables.Add(i.CreateInstance()); - - Reload(new Container - { - RelativeSizeAxes = Axes.Both, - Children = drawables, - }); - } - public void Reload() => Reload(CurrentSkin.GetDrawableComponent(Lookup) as Container); public void Reload(Container? componentsContainer) From bac70da1a1a12ddae4769da8ead2374ed6932311 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 19 Apr 2024 21:40:02 +0900 Subject: [PATCH 0571/2493] Give SerialisedDrawableInfo sane defaults --- osu.Game/Skinning/SerialisedDrawableInfo.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Skinning/SerialisedDrawableInfo.cs b/osu.Game/Skinning/SerialisedDrawableInfo.cs index 2d6113ff70..ac1aa80d29 100644 --- a/osu.Game/Skinning/SerialisedDrawableInfo.cs +++ b/osu.Game/Skinning/SerialisedDrawableInfo.cs @@ -34,15 +34,15 @@ namespace osu.Game.Skinning public float Rotation { get; set; } - public Vector2 Scale { get; set; } + public Vector2 Scale { get; set; } = Vector2.One; public float? Width { get; set; } public float? Height { get; set; } - public Anchor Anchor { get; set; } + public Anchor Anchor { get; set; } = Anchor.TopLeft; - public Anchor Origin { get; set; } + public Anchor Origin { get; set; } = Anchor.TopLeft; /// public bool UsesFixedAnchor { get; set; } From 4cffc39dffde5e14172df2693e71eebb4f3964ad Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 20 Apr 2024 04:53:31 +0800 Subject: [PATCH 0572/2493] Don't require hold for quick exit at results screen --- osu.Game/Screens/Ranking/ResultsScreen.cs | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs index f28b9b2554..ebb0530046 100644 --- a/osu.Game/Screens/Ranking/ResultsScreen.cs +++ b/osu.Game/Screens/Ranking/ResultsScreen.cs @@ -191,16 +191,6 @@ namespace osu.Game.Screens.Ranking }); } - AddInternal(new HotkeyExitOverlay - { - Action = () => - { - if (!this.IsCurrentScreen()) return; - - this.Exit(); - }, - }); - if (player != null && AllowRetry) { buttons.Add(new RetryButton { Width = 300 }); @@ -400,6 +390,15 @@ namespace osu.Game.Screens.Ranking switch (e.Action) { + case GlobalAction.QuickExit: + if (this.IsCurrentScreen()) + { + this.Exit(); + return true; + } + + break; + case GlobalAction.Select: StatisticsPanel.ToggleVisibility(); return true; From 722fa228cedfb81726dcc08e7b2baa4c524fb958 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Sun, 21 Apr 2024 17:40:35 +0300 Subject: [PATCH 0573/2493] Don't consider editor table content for input --- osu.Game/Screens/Edit/EditorTable.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/Screens/Edit/EditorTable.cs b/osu.Game/Screens/Edit/EditorTable.cs index e5dc540b06..49b41fac21 100644 --- a/osu.Game/Screens/Edit/EditorTable.cs +++ b/osu.Game/Screens/Edit/EditorTable.cs @@ -30,6 +30,10 @@ namespace osu.Game.Screens.Edit protected readonly FillFlowContainer BackgroundFlow; + // We can avoid potentially thousands of objects being added to the input sub-tree since item selection is being handled by the BackgroundFlow + // and no items in the underlying table are clickable. + protected override bool ShouldBeConsideredForInput(Drawable child) => base.ShouldBeConsideredForInput(child) && child == BackgroundFlow; + protected EditorTable() { RelativeSizeAxes = Axes.X; From 1d3fd65d86f517805f573517ad7650d1a89277af Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Mon, 22 Apr 2024 07:02:49 +0300 Subject: [PATCH 0574/2493] Adjust execution order --- osu.Game/Screens/Edit/EditorTable.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/EditorTable.cs b/osu.Game/Screens/Edit/EditorTable.cs index 49b41fac21..4d8393e829 100644 --- a/osu.Game/Screens/Edit/EditorTable.cs +++ b/osu.Game/Screens/Edit/EditorTable.cs @@ -32,7 +32,7 @@ namespace osu.Game.Screens.Edit // We can avoid potentially thousands of objects being added to the input sub-tree since item selection is being handled by the BackgroundFlow // and no items in the underlying table are clickable. - protected override bool ShouldBeConsideredForInput(Drawable child) => base.ShouldBeConsideredForInput(child) && child == BackgroundFlow; + protected override bool ShouldBeConsideredForInput(Drawable child) => child == BackgroundFlow && base.ShouldBeConsideredForInput(child); protected EditorTable() { From 40c48f903bdfdb0f11809c5aeea268aec198fbb6 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 22 Apr 2024 14:58:45 +0900 Subject: [PATCH 0575/2493] Add failing test --- .../TestSceneCursorTrail.cs | 31 +++++++++++++++---- .../Skinning/Legacy/LegacyCursorTrail.cs | 16 +++++----- 2 files changed, 33 insertions(+), 14 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneCursorTrail.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneCursorTrail.cs index 421a32b9eb..afb0246b21 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneCursorTrail.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneCursorTrail.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; +using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Audio.Sample; @@ -13,6 +14,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Rendering; using osu.Framework.Graphics.Textures; +using osu.Framework.Testing; using osu.Framework.Testing.Input; using osu.Game.Audio; using osu.Game.Rulesets.Osu.Skinning.Legacy; @@ -70,6 +72,22 @@ namespace osu.Game.Rulesets.Osu.Tests }); } + [Test] + public void TestLegacyDisjointCursorTrailViaNoCursor() + { + createTest(() => + { + var skinContainer = new LegacySkinContainer(renderer, false, false); + var legacyCursorTrail = new LegacyCursorTrail(skinContainer); + + skinContainer.Child = legacyCursorTrail; + + return skinContainer; + }); + + AddAssert("trail is disjoint", () => this.ChildrenOfType().Single().DisjointTrail, () => Is.True); + } + private void createTest(Func createContent) => AddStep("create trail", () => { Clear(); @@ -87,11 +105,13 @@ namespace osu.Game.Rulesets.Osu.Tests { private readonly IRenderer renderer; private readonly bool disjoint; + private readonly bool provideCursor; - public LegacySkinContainer(IRenderer renderer, bool disjoint) + public LegacySkinContainer(IRenderer renderer, bool disjoint, bool provideCursor = true) { this.renderer = renderer; this.disjoint = disjoint; + this.provideCursor = provideCursor; RelativeSizeAxes = Axes.Both; } @@ -102,12 +122,11 @@ namespace osu.Game.Rulesets.Osu.Tests { switch (componentName) { - case "cursortrail": - var tex = new Texture(renderer.WhitePixel); + case "cursor": + return provideCursor ? new Texture(renderer.WhitePixel) : null; - if (disjoint) - tex.ScaleAdjust = 1 / 25f; - return tex; + case "cursortrail": + return new Texture(renderer.WhitePixel); case "cursormiddle": return disjoint ? null : renderer.WhitePixel; diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyCursorTrail.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyCursorTrail.cs index af71e2a5d9..4ebafd0c4b 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyCursorTrail.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyCursorTrail.cs @@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy private readonly ISkin skin; private const double disjoint_trail_time_separation = 1000 / 60.0; - private bool disjointTrail; + public bool DisjointTrail { get; private set; } private double lastTrailTime; private IBindable cursorSize = null!; @@ -36,9 +36,9 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy cursorSize = config.GetBindable(OsuSetting.GameplayCursorSize).GetBoundCopy(); Texture = skin.GetTexture("cursortrail"); - disjointTrail = skin.GetTexture("cursormiddle") == null; + DisjointTrail = skin.GetTexture("cursormiddle") == null; - if (disjointTrail) + if (DisjointTrail) { bool centre = skin.GetConfig(OsuSkinConfiguration.CursorCentre)?.Value ?? true; @@ -57,19 +57,19 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy } } - protected override double FadeDuration => disjointTrail ? 150 : 500; + protected override double FadeDuration => DisjointTrail ? 150 : 500; protected override float FadeExponent => 1; - protected override bool InterpolateMovements => !disjointTrail; + protected override bool InterpolateMovements => !DisjointTrail; protected override float IntervalMultiplier => 1 / Math.Max(cursorSize.Value, 1); - protected override bool AvoidDrawingNearCursor => !disjointTrail; + protected override bool AvoidDrawingNearCursor => !DisjointTrail; protected override void Update() { base.Update(); - if (!disjointTrail || !currentPosition.HasValue) + if (!DisjointTrail || !currentPosition.HasValue) return; if (Time.Current - lastTrailTime >= disjoint_trail_time_separation) @@ -81,7 +81,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy protected override bool OnMouseMove(MouseMoveEvent e) { - if (!disjointTrail) + if (!DisjointTrail) return base.OnMouseMove(e); currentPosition = e.ScreenSpaceMousePosition; From 0170c04baf447abd9a4f986bcc169f940e92f9d4 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 22 Apr 2024 15:00:09 +0900 Subject: [PATCH 0576/2493] Fix `cursormiddle` not using the same source as `cursor` --- osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyCursorTrail.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyCursorTrail.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyCursorTrail.cs index 4ebafd0c4b..da52956719 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyCursorTrail.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyCursorTrail.cs @@ -31,12 +31,14 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy } [BackgroundDependencyLoader] - private void load(OsuConfigManager config) + private void load(OsuConfigManager config, ISkinSource skinSource) { cursorSize = config.GetBindable(OsuSetting.GameplayCursorSize).GetBoundCopy(); Texture = skin.GetTexture("cursortrail"); - DisjointTrail = skin.GetTexture("cursormiddle") == null; + + var cursorProvider = skinSource.FindProvider(s => s.GetTexture("cursor") != null); + DisjointTrail = cursorProvider?.GetTexture("cursormiddle") == null; if (DisjointTrail) { From fb1d20bd39605eca96837d2d8e9b2cc394800cba Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 22 Apr 2024 15:07:11 +0800 Subject: [PATCH 0577/2493] Comment in some clarification --- .../TestSceneCursorTrail.cs | 14 +++++++------- .../Skinning/Legacy/LegacyCursorTrail.cs | 3 +++ 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneCursorTrail.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneCursorTrail.cs index afb0246b21..4db66fde4b 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneCursorTrail.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneCursorTrail.cs @@ -49,7 +49,7 @@ namespace osu.Game.Rulesets.Osu.Tests { createTest(() => { - var skinContainer = new LegacySkinContainer(renderer, false); + var skinContainer = new LegacySkinContainer(renderer, provideMiddle: false); var legacyCursorTrail = new LegacyCursorTrail(skinContainer); skinContainer.Child = legacyCursorTrail; @@ -63,7 +63,7 @@ namespace osu.Game.Rulesets.Osu.Tests { createTest(() => { - var skinContainer = new LegacySkinContainer(renderer, true); + var skinContainer = new LegacySkinContainer(renderer, provideMiddle: true); var legacyCursorTrail = new LegacyCursorTrail(skinContainer); skinContainer.Child = legacyCursorTrail; @@ -77,7 +77,7 @@ namespace osu.Game.Rulesets.Osu.Tests { createTest(() => { - var skinContainer = new LegacySkinContainer(renderer, false, false); + var skinContainer = new LegacySkinContainer(renderer, provideMiddle: false, provideCursor: false); var legacyCursorTrail = new LegacyCursorTrail(skinContainer); skinContainer.Child = legacyCursorTrail; @@ -104,13 +104,13 @@ namespace osu.Game.Rulesets.Osu.Tests private partial class LegacySkinContainer : Container, ISkinSource { private readonly IRenderer renderer; - private readonly bool disjoint; + private readonly bool provideMiddle; private readonly bool provideCursor; - public LegacySkinContainer(IRenderer renderer, bool disjoint, bool provideCursor = true) + public LegacySkinContainer(IRenderer renderer, bool provideMiddle, bool provideCursor = true) { this.renderer = renderer; - this.disjoint = disjoint; + this.provideMiddle = provideMiddle; this.provideCursor = provideCursor; RelativeSizeAxes = Axes.Both; @@ -129,7 +129,7 @@ namespace osu.Game.Rulesets.Osu.Tests return new Texture(renderer.WhitePixel); case "cursormiddle": - return disjoint ? null : renderer.WhitePixel; + return provideMiddle ? null : renderer.WhitePixel; } return null; diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyCursorTrail.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyCursorTrail.cs index da52956719..ca0002d8c0 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyCursorTrail.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyCursorTrail.cs @@ -37,6 +37,9 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy Texture = skin.GetTexture("cursortrail"); + // Cursor and cursor trail components are sourced from potentially different skin sources. + // Stable always chooses cursor trail disjoint behaviour based on the cursor texture lookup source, so we need to fetch where that occurred. + // See https://github.com/peppy/osu-stable-reference/blob/3ea48705eb67172c430371dcfc8a16a002ed0d3d/osu!/Graphics/Skinning/SkinManager.cs#L269 var cursorProvider = skinSource.FindProvider(s => s.GetTexture("cursor") != null); DisjointTrail = cursorProvider?.GetTexture("cursormiddle") == null; From 49563f4e5bc9ce3ea72b5645456a6d0e8af6a89c Mon Sep 17 00:00:00 2001 From: Arthur Araujo Date: Mon, 22 Apr 2024 04:44:38 -0300 Subject: [PATCH 0578/2493] Use bitwise to check hitsound formats --- osu.Game/Rulesets/Edit/Checks/CheckHitsoundsFormat.cs | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/osu.Game/Rulesets/Edit/Checks/CheckHitsoundsFormat.cs b/osu.Game/Rulesets/Edit/Checks/CheckHitsoundsFormat.cs index 184311b1c3..9779696e4b 100644 --- a/osu.Game/Rulesets/Edit/Checks/CheckHitsoundsFormat.cs +++ b/osu.Game/Rulesets/Edit/Checks/CheckHitsoundsFormat.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.IO; -using System.Linq; using ManagedBass; using osu.Framework.Audio.Callbacks; using osu.Game.Beatmaps; @@ -22,14 +21,6 @@ namespace osu.Game.Rulesets.Edit.Checks new IssueTemplateIncorrectFormat(this), }; - private IEnumerable allowedFormats => new ChannelType[] - { - ChannelType.WavePCM, - ChannelType.WaveFloat, - ChannelType.OGG, - ChannelType.Wave | ChannelType.OGG, - }; - public IEnumerable Run(BeatmapVerifierContext context) { var beatmapSet = context.Beatmap.BeatmapInfo.BeatmapSet; @@ -60,7 +51,7 @@ namespace osu.Game.Rulesets.Edit.Checks var audioInfo = Bass.ChannelGetInfo(decodeStream); - if (!allowedFormats.Contains(audioInfo.ChannelType)) + if ((audioInfo.ChannelType & ChannelType.Wave) == 0 && audioInfo.ChannelType != ChannelType.OGG) yield return new IssueTemplateIncorrectFormat(this).Create(file.Filename); Bass.StreamFree(decodeStream); From 09b0f3005e28cb2248ddb206b80d3eb3a94c6e2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 22 Apr 2024 10:15:56 +0200 Subject: [PATCH 0579/2493] Apply generic math-related changes --- osu.Game/Graphics/UserInterface/ExpandableSlider.cs | 8 ++++---- osu.Game/Graphics/UserInterface/OsuSliderBar.cs | 9 +++++---- osu.Game/Graphics/UserInterface/RoundedSliderBar.cs | 5 +++-- osu.Game/Graphics/UserInterface/ShearedSliderBar.cs | 5 +++-- osu.Game/Graphics/UserInterfaceV2/LabelledSliderBar.cs | 4 ++-- .../Graphics/UserInterfaceV2/SliderWithTextBoxInput.cs | 8 ++++---- osu.Game/Overlays/Settings/Sections/SizeSlider.cs | 3 ++- osu.Game/Overlays/Settings/SettingsPercentageSlider.cs | 4 ++-- osu.Game/Overlays/Settings/SettingsSlider.cs | 6 +++--- .../Edit/Timing/IndeterminateSliderWithTextBoxInput.cs | 8 ++++---- osu.Game/Screens/Play/PlayerSettings/PlayerSliderBar.cs | 4 ++-- 11 files changed, 34 insertions(+), 30 deletions(-) diff --git a/osu.Game/Graphics/UserInterface/ExpandableSlider.cs b/osu.Game/Graphics/UserInterface/ExpandableSlider.cs index 121a1eef49..a7a8561b94 100644 --- a/osu.Game/Graphics/UserInterface/ExpandableSlider.cs +++ b/osu.Game/Graphics/UserInterface/ExpandableSlider.cs @@ -1,7 +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.Numerics; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -10,7 +10,7 @@ using osu.Framework.Graphics.UserInterface; using osu.Framework.Localisation; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; -using osuTK; +using Vector2 = osuTK.Vector2; namespace osu.Game.Graphics.UserInterface { @@ -18,7 +18,7 @@ namespace osu.Game.Graphics.UserInterface /// An implementation for the UI slider bar control. /// public partial class ExpandableSlider : CompositeDrawable, IExpandable, IHasCurrentValue - where T : struct, IEquatable, IComparable, IConvertible + where T : struct, INumber, IMinMaxValue where TSlider : RoundedSliderBar, new() { private readonly OsuSpriteText label; @@ -129,7 +129,7 @@ namespace osu.Game.Graphics.UserInterface /// An implementation for the UI slider bar control. /// public partial class ExpandableSlider : ExpandableSlider> - where T : struct, IEquatable, IComparable, IConvertible + where T : struct, INumber, IMinMaxValue { } } diff --git a/osu.Game/Graphics/UserInterface/OsuSliderBar.cs b/osu.Game/Graphics/UserInterface/OsuSliderBar.cs index 191a7ca246..9cb6356cab 100644 --- a/osu.Game/Graphics/UserInterface/OsuSliderBar.cs +++ b/osu.Game/Graphics/UserInterface/OsuSliderBar.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Numerics; using System.Globalization; using osu.Framework.Allocation; using osu.Framework.Audio; @@ -15,7 +16,7 @@ using osu.Game.Utils; namespace osu.Game.Graphics.UserInterface { public abstract partial class OsuSliderBar : SliderBar, IHasTooltip - where T : struct, IEquatable, IComparable, IConvertible + where T : struct, INumber, IMinMaxValue { public bool PlaySamplesOnAdjust { get; set; } = true; @@ -85,11 +86,11 @@ namespace osu.Game.Graphics.UserInterface private LocalisableString getTooltipText(T value) { if (CurrentNumber.IsInteger) - return value.ToInt32(NumberFormatInfo.InvariantInfo).ToString("N0"); + return int.CreateTruncating(value).ToString("N0"); - double floatValue = value.ToDouble(NumberFormatInfo.InvariantInfo); + double floatValue = double.CreateTruncating(value); - decimal decimalPrecision = normalise(CurrentNumber.Precision.ToDecimal(NumberFormatInfo.InvariantInfo), max_decimal_digits); + decimal decimalPrecision = normalise(decimal.CreateTruncating(CurrentNumber.Precision), max_decimal_digits); // Find the number of significant digits (we could have less than 5 after normalize()) int significantDigits = FormatUtils.FindPrecision(decimalPrecision); diff --git a/osu.Game/Graphics/UserInterface/RoundedSliderBar.cs b/osu.Game/Graphics/UserInterface/RoundedSliderBar.cs index 0981881ead..56047173bb 100644 --- a/osu.Game/Graphics/UserInterface/RoundedSliderBar.cs +++ b/osu.Game/Graphics/UserInterface/RoundedSliderBar.cs @@ -2,7 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; -using osuTK; +using System.Numerics; using osuTK.Graphics; using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; @@ -11,11 +11,12 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; using osu.Game.Overlays; +using Vector2 = osuTK.Vector2; namespace osu.Game.Graphics.UserInterface { public partial class RoundedSliderBar : OsuSliderBar - where T : struct, IEquatable, IComparable, IConvertible + where T : struct, INumber, IMinMaxValue { protected readonly Nub Nub; protected readonly Box LeftBox; diff --git a/osu.Game/Graphics/UserInterface/ShearedSliderBar.cs b/osu.Game/Graphics/UserInterface/ShearedSliderBar.cs index 60a6670492..0df1c1d204 100644 --- a/osu.Game/Graphics/UserInterface/ShearedSliderBar.cs +++ b/osu.Game/Graphics/UserInterface/ShearedSliderBar.cs @@ -2,7 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; -using osuTK; +using System.Numerics; using osuTK.Graphics; using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; @@ -12,11 +12,12 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; using osu.Game.Overlays; using static osu.Game.Graphics.UserInterface.ShearedNub; +using Vector2 = osuTK.Vector2; namespace osu.Game.Graphics.UserInterface { public partial class ShearedSliderBar : OsuSliderBar - where T : struct, IEquatable, IComparable, IConvertible + where T : struct, INumber, IMinMaxValue { protected readonly ShearedNub Nub; protected readonly Box LeftBox; diff --git a/osu.Game/Graphics/UserInterfaceV2/LabelledSliderBar.cs b/osu.Game/Graphics/UserInterfaceV2/LabelledSliderBar.cs index 4585d3a4c9..4912a21fab 100644 --- a/osu.Game/Graphics/UserInterfaceV2/LabelledSliderBar.cs +++ b/osu.Game/Graphics/UserInterfaceV2/LabelledSliderBar.cs @@ -1,14 +1,14 @@ // 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.Numerics; using osu.Framework.Graphics; using osu.Game.Overlays.Settings; namespace osu.Game.Graphics.UserInterfaceV2 { public partial class LabelledSliderBar : LabelledComponent, TNumber> - where TNumber : struct, IEquatable, IComparable, IConvertible + where TNumber : struct, INumber, IMinMaxValue { public LabelledSliderBar() : base(true) diff --git a/osu.Game/Graphics/UserInterfaceV2/SliderWithTextBoxInput.cs b/osu.Game/Graphics/UserInterfaceV2/SliderWithTextBoxInput.cs index e5ba7f61bf..abd828e98f 100644 --- a/osu.Game/Graphics/UserInterfaceV2/SliderWithTextBoxInput.cs +++ b/osu.Game/Graphics/UserInterfaceV2/SliderWithTextBoxInput.cs @@ -1,7 +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.Numerics; using System.Globalization; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -10,12 +10,12 @@ using osu.Framework.Graphics.UserInterface; using osu.Framework.Localisation; using osu.Game.Overlays.Settings; using osu.Game.Utils; -using osuTK; +using Vector2 = osuTK.Vector2; namespace osu.Game.Graphics.UserInterfaceV2 { public partial class SliderWithTextBoxInput : CompositeDrawable, IHasCurrentValue - where T : struct, IEquatable, IComparable, IConvertible + where T : struct, INumber, IMinMaxValue { /// /// A custom step value for each key press which actuates a change on this control. @@ -138,7 +138,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 { if (updatingFromTextBox) return; - decimal decimalValue = slider.Current.Value.ToDecimal(NumberFormatInfo.InvariantInfo); + decimal decimalValue = decimal.CreateTruncating(slider.Current.Value); textBox.Text = decimalValue.ToString($@"N{FormatUtils.FindPrecision(decimalValue)}"); } } diff --git a/osu.Game/Overlays/Settings/Sections/SizeSlider.cs b/osu.Game/Overlays/Settings/Sections/SizeSlider.cs index c73831d8d1..14ef58ff88 100644 --- a/osu.Game/Overlays/Settings/Sections/SizeSlider.cs +++ b/osu.Game/Overlays/Settings/Sections/SizeSlider.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Numerics; using System.Globalization; using osu.Framework.Localisation; using osu.Game.Graphics.UserInterface; @@ -12,7 +13,7 @@ namespace osu.Game.Overlays.Settings.Sections /// A slider intended to show a "size" multiplier number, where 1x is 1.0. /// public partial class SizeSlider : RoundedSliderBar - where T : struct, IEquatable, IComparable, IConvertible, IFormattable + where T : struct, INumber, IMinMaxValue, IFormattable { public override LocalisableString TooltipText => Current.Value.ToString(@"0.##x", NumberFormatInfo.CurrentInfo); } diff --git a/osu.Game/Overlays/Settings/SettingsPercentageSlider.cs b/osu.Game/Overlays/Settings/SettingsPercentageSlider.cs index fa59d18de1..d7a09d3392 100644 --- a/osu.Game/Overlays/Settings/SettingsPercentageSlider.cs +++ b/osu.Game/Overlays/Settings/SettingsPercentageSlider.cs @@ -1,7 +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.Numerics; using osu.Framework.Graphics; using osu.Game.Configuration; using osu.Game.Graphics.UserInterface; @@ -13,7 +13,7 @@ namespace osu.Game.Overlays.Settings /// Mostly provided for convenience of use with . /// public partial class SettingsPercentageSlider : SettingsSlider - where TValue : struct, IEquatable, IComparable, IConvertible + where TValue : struct, INumber, IMinMaxValue { protected override Drawable CreateControl() => ((RoundedSliderBar)base.CreateControl()).With(sliderBar => sliderBar.DisplayAsPercentage = true); } diff --git a/osu.Game/Overlays/Settings/SettingsSlider.cs b/osu.Game/Overlays/Settings/SettingsSlider.cs index 6c81fece13..2460d78099 100644 --- a/osu.Game/Overlays/Settings/SettingsSlider.cs +++ b/osu.Game/Overlays/Settings/SettingsSlider.cs @@ -1,7 +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.Numerics; using osu.Framework.Graphics; using osu.Framework.Graphics.UserInterface; using osu.Game.Graphics.UserInterface; @@ -9,12 +9,12 @@ using osu.Game.Graphics.UserInterface; namespace osu.Game.Overlays.Settings { public partial class SettingsSlider : SettingsSlider> - where T : struct, IEquatable, IComparable, IConvertible + where T : struct, INumber, IMinMaxValue { } public partial class SettingsSlider : SettingsItem - where TValue : struct, IEquatable, IComparable, IConvertible + where TValue : struct, INumber, IMinMaxValue where TSlider : RoundedSliderBar, new() { protected override Drawable CreateControl() => new TSlider diff --git a/osu.Game/Screens/Edit/Timing/IndeterminateSliderWithTextBoxInput.cs b/osu.Game/Screens/Edit/Timing/IndeterminateSliderWithTextBoxInput.cs index 151d469415..26f374ba85 100644 --- a/osu.Game/Screens/Edit/Timing/IndeterminateSliderWithTextBoxInput.cs +++ b/osu.Game/Screens/Edit/Timing/IndeterminateSliderWithTextBoxInput.cs @@ -1,7 +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.Numerics; using System.Globalization; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -12,7 +12,7 @@ using osu.Framework.Localisation; using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Overlays.Settings; using osu.Game.Utils; -using osuTK; +using Vector2 = osuTK.Vector2; namespace osu.Game.Screens.Edit.Timing { @@ -22,7 +22,7 @@ namespace osu.Game.Screens.Edit.Timing /// by providing an "indeterminate state". /// public partial class IndeterminateSliderWithTextBoxInput : CompositeDrawable, IHasCurrentValue - where T : struct, IEquatable, IComparable, IConvertible + where T : struct, INumber, IMinMaxValue { /// /// A custom step value for each key press which actuates a change on this control. @@ -136,7 +136,7 @@ namespace osu.Game.Screens.Edit.Timing slider.Current.Value = nonNullValue; // use the value from the slider to ensure that any precision/min/max set on it via the initial indeterminate value have been applied correctly. - decimal decimalValue = slider.Current.Value.ToDecimal(NumberFormatInfo.InvariantInfo); + decimal decimalValue = decimal.CreateTruncating(slider.Current.Value); textBox.Text = decimalValue.ToString($@"N{FormatUtils.FindPrecision(decimalValue)}"); textBox.PlaceholderText = string.Empty; } diff --git a/osu.Game/Screens/Play/PlayerSettings/PlayerSliderBar.cs b/osu.Game/Screens/Play/PlayerSettings/PlayerSliderBar.cs index 88b778fafb..1fc1155c0b 100644 --- a/osu.Game/Screens/Play/PlayerSettings/PlayerSliderBar.cs +++ b/osu.Game/Screens/Play/PlayerSettings/PlayerSliderBar.cs @@ -1,7 +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.Numerics; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Game.Graphics; @@ -11,7 +11,7 @@ using osu.Game.Overlays.Settings; namespace osu.Game.Screens.Play.PlayerSettings { public partial class PlayerSliderBar : SettingsSlider - where T : struct, IEquatable, IComparable, IConvertible + where T : struct, INumber, IMinMaxValue { public RoundedSliderBar Bar => (RoundedSliderBar)Control; From 1bcf835d227ce3899d1e16ff16cfba4a9c08dcd6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 22 Apr 2024 10:25:46 +0200 Subject: [PATCH 0580/2493] Remove redundant array type specification --- osu.Game/Rulesets/Edit/Checks/CheckSongFormat.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Edit/Checks/CheckSongFormat.cs b/osu.Game/Rulesets/Edit/Checks/CheckSongFormat.cs index 0cb54709af..dd01fe110a 100644 --- a/osu.Game/Rulesets/Edit/Checks/CheckSongFormat.cs +++ b/osu.Game/Rulesets/Edit/Checks/CheckSongFormat.cs @@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Edit.Checks new IssueTemplateIncorrectFormat(this), }; - private IEnumerable allowedFormats => new ChannelType[] + private IEnumerable allowedFormats => new[] { ChannelType.MP3, ChannelType.OGG, From ceeb9b081903dab467a5659595f6746403ccc845 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 22 Apr 2024 10:27:30 +0200 Subject: [PATCH 0581/2493] Use explicit reference equality check --- osu.Game/Rulesets/Edit/Checks/CheckHitsoundsFormat.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Edit/Checks/CheckHitsoundsFormat.cs b/osu.Game/Rulesets/Edit/Checks/CheckHitsoundsFormat.cs index 9779696e4b..728567b490 100644 --- a/osu.Game/Rulesets/Edit/Checks/CheckHitsoundsFormat.cs +++ b/osu.Game/Rulesets/Edit/Checks/CheckHitsoundsFormat.cs @@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Edit.Checks foreach (var file in beatmapSet.Files) { - if (audioFile != null && file.File == audioFile.File) continue; + if (audioFile != null && ReferenceEquals(file.File, audioFile.File)) continue; using (Stream data = context.WorkingBeatmap.GetStream(file.File.GetStoragePath())) { From b28bf4d2ecf2dbc6b11b01ffdca5ae6adbcaecaa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 22 Apr 2024 10:43:20 +0200 Subject: [PATCH 0582/2493] Add test covering non-audio file formats not being checked --- .../Checks/CheckHitsoundsFormatTest.cs | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/osu.Game.Tests/Editing/Checks/CheckHitsoundsFormatTest.cs b/osu.Game.Tests/Editing/Checks/CheckHitsoundsFormatTest.cs index 9a806f6cb7..cb1cf21734 100644 --- a/osu.Game.Tests/Editing/Checks/CheckHitsoundsFormatTest.cs +++ b/osu.Game.Tests/Editing/Checks/CheckHitsoundsFormatTest.cs @@ -85,6 +85,27 @@ namespace osu.Game.Tests.Editing.Checks } } + [Test] + public void TestNotAnAudioFile() + { + beatmap = new Beatmap + { + BeatmapInfo = new BeatmapInfo + { + BeatmapSet = new BeatmapSetInfo + { + Files = { CheckTestHelpers.CreateMockFile("png") } + } + } + }; + + using (var resourceStream = TestResources.OpenResource("Textures/test-image.png")) + { + var issues = check.Run(getContext(resourceStream)).ToList(); + Assert.That(issues, Has.Count.EqualTo(0)); + } + } + [Test] public void TestCorruptAudio() { From 70a5288a6859dae28f7870f338e82cbbdcf4c1fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 22 Apr 2024 10:45:09 +0200 Subject: [PATCH 0583/2493] Do not attempt to pass files that don't look like audio to BASS --- osu.Game/Rulesets/Edit/Checks/CheckHitsoundsFormat.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/Edit/Checks/CheckHitsoundsFormat.cs b/osu.Game/Rulesets/Edit/Checks/CheckHitsoundsFormat.cs index 728567b490..9b6a861358 100644 --- a/osu.Game/Rulesets/Edit/Checks/CheckHitsoundsFormat.cs +++ b/osu.Game/Rulesets/Edit/Checks/CheckHitsoundsFormat.cs @@ -37,14 +37,16 @@ namespace osu.Game.Rulesets.Edit.Checks if (data == null) continue; + if (!AudioCheckUtils.HasAudioExtension(file.Filename) || !probablyHasAudioData(data)) + continue; + var fileCallbacks = new FileCallbacks(new DataStreamFileProcedures(data)); int decodeStream = Bass.CreateStream(StreamSystem.NoBuffer, BassFlags.Decode, fileCallbacks.Callbacks, fileCallbacks.Handle); // If the format is not supported by BASS if (decodeStream == 0) { - if (AudioCheckUtils.HasAudioExtension(file.Filename) && probablyHasAudioData(data)) - yield return new IssueTemplateFormatUnsupported(this).Create(file.Filename); + yield return new IssueTemplateFormatUnsupported(this).Create(file.Filename); continue; } From 4ae9f81c73f39828e4e812cc362bacbda2656c9f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 22 Apr 2024 18:43:15 +0800 Subject: [PATCH 0584/2493] Apply transforms to storyboard videos This requires that they are a `StoryboardSprite` for simplicity. Luckily this works just fine. --- .../Beatmaps/Formats/LegacyStoryboardDecoder.cs | 2 +- osu.Game/Storyboards/StoryboardVideo.cs | 15 +++++---------- 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs index 6689f087cb..b5d9ad1194 100644 --- a/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs @@ -114,7 +114,7 @@ namespace osu.Game.Beatmaps.Formats if (!OsuGameBase.VIDEO_EXTENSIONS.Contains(Path.GetExtension(path).ToLowerInvariant())) break; - storyboard.GetLayer("Video").Add(new StoryboardVideo(path, offset)); + storyboard.GetLayer("Video").Add(storyboardSprite = new StoryboardVideo(path, offset)); break; } diff --git a/osu.Game/Storyboards/StoryboardVideo.cs b/osu.Game/Storyboards/StoryboardVideo.cs index 8c11e19a06..bd1b933b6f 100644 --- a/osu.Game/Storyboards/StoryboardVideo.cs +++ b/osu.Game/Storyboards/StoryboardVideo.cs @@ -3,23 +3,18 @@ using osu.Framework.Graphics; using osu.Game.Storyboards.Drawables; +using osuTK; namespace osu.Game.Storyboards { - public class StoryboardVideo : IStoryboardElement + public class StoryboardVideo : StoryboardSprite { - public string Path { get; } - - public bool IsDrawable => true; - - public double StartTime { get; } - public StoryboardVideo(string path, double offset) + : base(path, Anchor.Centre, Vector2.Zero) { - Path = path; - StartTime = offset; + TimelineGroup.Alpha.Add(Easing.None, offset, offset, 0, 1); } - public Drawable CreateDrawable() => new DrawableStoryboardVideo(this); + public override Drawable CreateDrawable() => new DrawableStoryboardVideo(this); } } From c43c383abf5d05caa5827b23b1fd53f61cff5b55 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 22 Apr 2024 18:43:34 +0800 Subject: [PATCH 0585/2493] Allow storboard videos to take on no-relative size when specified by a mapper --- .../Drawables/DrawableStoryboardVideo.cs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardVideo.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardVideo.cs index 9a5db4bb39..98cb01d5f3 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardVideo.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardVideo.cs @@ -23,7 +23,17 @@ namespace osu.Game.Storyboards.Drawables { Video = video; - RelativeSizeAxes = Axes.Both; + // In osu-stable, a mapper can add a scale command for a storyboard. + // This allows scaling based on the video's absolute size. + // + // If not specified we take up the full available space. + bool useRelative = !video.TimelineGroup.Scale.HasCommands; + + RelativeSizeAxes = useRelative ? Axes.Both : Axes.None; + AutoSizeAxes = useRelative ? Axes.None : Axes.Both; + + Anchor = Anchor.Centre; + Origin = Anchor.Centre; } [BackgroundDependencyLoader(true)] @@ -36,7 +46,7 @@ namespace osu.Game.Storyboards.Drawables InternalChild = drawableVideo = new Video(stream, false) { - RelativeSizeAxes = Axes.Both, + RelativeSizeAxes = RelativeSizeAxes, FillMode = FillMode.Fill, Anchor = Anchor.Centre, Origin = Anchor.Centre, From 9e7182acf0cfa3fc658758b78af4dc7378cef536 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 22 Apr 2024 18:45:02 +0800 Subject: [PATCH 0586/2493] Remove unused DI beatmap paramater --- osu.Game/Storyboards/Drawables/DrawableStoryboardVideo.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardVideo.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardVideo.cs index 98cb01d5f3..ca2c7b774c 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardVideo.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardVideo.cs @@ -2,12 +2,10 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; -using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.Video; -using osu.Game.Beatmaps; namespace osu.Game.Storyboards.Drawables { @@ -37,7 +35,7 @@ namespace osu.Game.Storyboards.Drawables } [BackgroundDependencyLoader(true)] - private void load(IBindable beatmap, TextureStore textureStore) + private void load(TextureStore textureStore) { var stream = textureStore.GetStream(Video.Path); From 2eda56ff0859a0e0fe44e59d2b7f936139dcc94b Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Mon, 22 Apr 2024 11:15:50 -0700 Subject: [PATCH 0587/2493] Revert beatmap query change --- osu.Game/OsuGame.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index ccdf9d151f..98533a5c82 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -716,7 +716,7 @@ namespace osu.Game return; } - var databasedBeatmap = BeatmapManager.QueryBeatmap(b => b.OnlineID == score.Beatmap.OnlineID); + var databasedBeatmap = BeatmapManager.QueryBeatmap(b => b.ID == databasedScore.ScoreInfo.BeatmapInfo.ID); if (databasedBeatmap == null) { From a59bf6d373cfdc234261fc1ce2d489803bd35f14 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Mon, 22 Apr 2024 11:17:27 -0700 Subject: [PATCH 0588/2493] Improve xmldoc wording --- osu.Game/Scoring/ScoreManager.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Scoring/ScoreManager.cs b/osu.Game/Scoring/ScoreManager.cs index b6bb637537..f37ee2b101 100644 --- a/osu.Game/Scoring/ScoreManager.cs +++ b/osu.Game/Scoring/ScoreManager.cs @@ -62,7 +62,7 @@ namespace osu.Game.Scoring /// Retrieve a from a given . /// /// The to convert. - /// The . Null if the score on the database cannot be found. + /// The . Null if the score cannot be found in the database. /// /// The is re-retrieved from the database to ensure all the required data /// for retrieving a replay are present (may have missing properties if it was retrieved from online data). @@ -201,7 +201,7 @@ namespace osu.Game.Scoring /// Export a replay from a given . /// /// The to export. - /// The . Null if the score on the database cannot be found. + /// The . Null if the score cannot be found in the database. /// /// The is re-retrieved from the database to ensure all the required data /// for exporting a replay are present (may have missing properties if it was retrieved from online data). From 35eddf35c5651b883a65e955a750ddd2c05b47be Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Mon, 22 Apr 2024 11:22:39 -0700 Subject: [PATCH 0589/2493] Return `Task.CompletedTask` instead of `null` --- osu.Game/Scoring/ScoreManager.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Scoring/ScoreManager.cs b/osu.Game/Scoring/ScoreManager.cs index f37ee2b101..1ba5c7d4cf 100644 --- a/osu.Game/Scoring/ScoreManager.cs +++ b/osu.Game/Scoring/ScoreManager.cs @@ -201,16 +201,16 @@ namespace osu.Game.Scoring /// Export a replay from a given . /// /// The to export. - /// The . Null if the score cannot be found in the database. + /// The . Return if the score cannot be found in the database. /// /// The is re-retrieved from the database to ensure all the required data /// for exporting a replay are present (may have missing properties if it was retrieved from online data). /// - public Task? Export(ScoreInfo scoreInfo) + public Task Export(ScoreInfo scoreInfo) { ScoreInfo? databasedScoreInfo = getDatabasedScoreInfo(scoreInfo); - return databasedScoreInfo == null ? null : scoreExporter.ExportAsync(databasedScoreInfo.ToLive(Realm)); + return databasedScoreInfo == null ? Task.CompletedTask : scoreExporter.ExportAsync(databasedScoreInfo.ToLive(Realm)); } public Task?> ImportAsUpdate(ProgressNotification notification, ImportTask task, ScoreInfo original) => scoreImporter.ImportAsUpdate(notification, task, original); From 49154c0e231c25f78c849ecf7180b4240b9e0145 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Mon, 22 Apr 2024 11:23:38 -0700 Subject: [PATCH 0590/2493] Fix code quality --- osu.Game.Tests/Visual/Navigation/TestScenePresentScore.cs | 2 +- .../Visual/UserInterface/TestSceneDeleteLocalScore.cs | 2 +- osu.Game/Screens/Play/Player.cs | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Navigation/TestScenePresentScore.cs b/osu.Game.Tests/Visual/Navigation/TestScenePresentScore.cs index 6590339311..004d1de116 100644 --- a/osu.Game.Tests/Visual/Navigation/TestScenePresentScore.cs +++ b/osu.Game.Tests/Visual/Navigation/TestScenePresentScore.cs @@ -170,7 +170,7 @@ namespace osu.Game.Tests.Visual.Navigation BeatmapInfo = beatmap.Beatmaps.First(), Ruleset = ruleset ?? new OsuRuleset().RulesetInfo, User = new GuestUser(), - }).Value; + })!.Value; }); AddAssert($"import {i} succeeded", () => imported != null); diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs index 529874b71e..e2fe10fa74 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs @@ -104,7 +104,7 @@ namespace osu.Game.Tests.Visual.UserInterface Files = { new RealmNamedFileUsage(new RealmFile { Hash = $"{i}" }, string.Empty) } }; - importedScores.Add(scoreManager.Import(score).Value); + importedScores.Add(scoreManager.Import(score)!.Value); } }); }); diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 4fcc52bc5d..3a80caf259 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -1200,6 +1200,7 @@ namespace osu.Game.Screens.Play var importableScore = score.ScoreInfo.DeepClone(); var imported = scoreManager.Import(importableScore, replayReader); + Debug.Assert(imported != null); imported.PerformRead(s => { From beee76d64ab5b7604787603a2c9c8f559910d604 Mon Sep 17 00:00:00 2001 From: DavidBeh <67109172+DavidBeh@users.noreply.github.com> Date: Mon, 22 Apr 2024 20:25:43 +0200 Subject: [PATCH 0591/2493] enabled and fixed judgements for the magnetised mod --- osu.Game.Rulesets.Osu/Mods/OsuModMagnetised.cs | 13 ++++++++++--- osu.Game.Rulesets.Osu/Objects/Slider.cs | 9 +++++++++ 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModMagnetised.cs b/osu.Game.Rulesets.Osu/Mods/OsuModMagnetised.cs index b49fb931d1..860f96965a 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModMagnetised.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModMagnetised.cs @@ -13,7 +13,6 @@ using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects.Drawables; -using osu.Game.Rulesets.Osu.UI; using osu.Game.Rulesets.UI; using osuTK; @@ -37,12 +36,16 @@ namespace osu.Game.Rulesets.Osu.Mods MaxValue = 1.0f, }; + // Bindable Setting for Show Judgements + [SettingSource("Show Judgements", "Whether to show judgements or not.")] + public BindableBool ShowJudgements { get; } = new BindableBool(true); + public void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset) { // Hide judgment displays and follow points as they won't make any sense. // Judgements can potentially be turned on in a future where they display at a position relative to their drawable counterpart. - drawableRuleset.Playfield.DisplayJudgements.Value = false; - (drawableRuleset.Playfield as OsuPlayfield)?.FollowPoints.Hide(); + drawableRuleset.Playfield.DisplayJudgements.Value = ShowJudgements.Value; + //(drawableRuleset.Playfield as OsuPlayfield)?.FollowPoints.Hide(); } public void Update(Playfield playfield) @@ -78,6 +81,10 @@ namespace osu.Game.Rulesets.Osu.Mods float x = (float)Interpolation.DampContinuously(hitObject.X, destination.X, dampLength, clock.ElapsedFrameTime); float y = (float)Interpolation.DampContinuously(hitObject.Y, destination.Y, dampLength, clock.ElapsedFrameTime); + // I added these two lines + if (hitObject is DrawableOsuHitObject h) + h.HitObject.Position = new Vector2(x, y); + hitObject.Position = new Vector2(x, y); } } diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs index cc3ffd376e..2660933a70 100644 --- a/osu.Game.Rulesets.Osu/Objects/Slider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs @@ -8,6 +8,7 @@ using osu.Game.Rulesets.Objects.Types; using System.Collections.Generic; using osu.Game.Rulesets.Objects; using System.Linq; +using System.Runtime.CompilerServices; using System.Threading; using Newtonsoft.Json; using osu.Framework.Bindables; @@ -24,6 +25,8 @@ namespace osu.Game.Rulesets.Osu.Objects { public class Slider : OsuHitObject, IHasPathWithRepeats, IHasSliderVelocity, IHasGenerateTicks { + private static readonly ConditionalWeakTable> SliderProgress = new ConditionalWeakTable>(); + public double EndTime => StartTime + this.SpanCount() * Path.Distance / Velocity; [JsonIgnore] @@ -201,6 +204,7 @@ namespace osu.Game.Rulesets.Osu.Objects Position = Position + Path.PositionAt(e.PathProgress), StackHeight = StackHeight, }); + SliderProgress.AddOrUpdate(NestedHitObjects.Last(), new StrongBox(e.PathProgress)); break; case SliderEventType.Head: @@ -232,6 +236,7 @@ namespace osu.Game.Rulesets.Osu.Objects Position = Position + Path.PositionAt(e.PathProgress), StackHeight = StackHeight, }); + SliderProgress.Add(NestedHitObjects.Last(), new StrongBox(e.PathProgress)); break; } } @@ -248,6 +253,10 @@ namespace osu.Game.Rulesets.Osu.Objects if (TailCircle != null) TailCircle.Position = EndPosition; + + foreach (var hitObject in NestedHitObjects) + if (hitObject is SliderTick or SliderRepeat) + ((OsuHitObject)hitObject).Position = Position + Path.PositionAt(SliderProgress.TryGetValue(hitObject, out var progress) ? progress?.Value ?? 0 : 0); } protected void UpdateNestedSamples() From 50afd48812aaf7e4deb437855202836280c93339 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 22 Apr 2024 23:04:56 +0800 Subject: [PATCH 0592/2493] Add manual test coverage of storyboard videos --- .../TestSceneDrawableStoryboardSprite.cs | 98 ++++++++++++++----- 1 file changed, 75 insertions(+), 23 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableStoryboardSprite.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableStoryboardSprite.cs index 32693c2bb2..6ac112cc5f 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableStoryboardSprite.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableStoryboardSprite.cs @@ -10,6 +10,7 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; using osu.Framework.IO.Stores; using osu.Framework.Testing; @@ -40,7 +41,7 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("disallow all lookups", () => { storyboard.UseSkinSprites = false; - storyboard.AlwaysProvideTexture = false; + storyboard.ProvideResources = false; }); AddStep("create sprites", () => SetContents(_ => createSprite(lookup_name, Anchor.TopLeft, Vector2.Zero))); @@ -55,7 +56,7 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("allow storyboard lookup", () => { storyboard.UseSkinSprites = false; - storyboard.AlwaysProvideTexture = true; + storyboard.ProvideResources = true; }); AddStep("create sprites", () => SetContents(_ => createSprite(lookup_name, Anchor.TopLeft, Vector2.Zero))); @@ -67,13 +68,47 @@ namespace osu.Game.Tests.Visual.Gameplay assertStoryboardSourced(); } + [TestCase(false)] + [TestCase(true)] + public void TestVideo(bool scaleTransformProvided) + { + AddStep("allow storyboard lookup", () => + { + storyboard.ProvideResources = true; + }); + + AddStep("create video", () => SetContents(_ => + { + var layer = storyboard.GetLayer("Video"); + + var sprite = new StoryboardVideo("Videos/test-video.mp4", Time.Current); + + sprite.AddLoop(Time.Current, 100).Alpha.Add(Easing.None, 0, 10000, 1, 1); + + if (scaleTransformProvided) + sprite.TimelineGroup.Scale.Add(Easing.None, Time.Current, Time.Current, 1, 1); + + layer.Elements.Clear(); + layer.Add(sprite); + + return new Container + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + storyboard.CreateDrawable() + } + }; + })); + } + [Test] public void TestSkinLookupPreferredOverStoryboard() { AddStep("allow all lookups", () => { storyboard.UseSkinSprites = true; - storyboard.AlwaysProvideTexture = true; + storyboard.ProvideResources = true; }); AddStep("create sprites", () => SetContents(_ => createSprite(lookup_name, Anchor.TopLeft, Vector2.Zero))); @@ -91,7 +126,7 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("allow skin lookup", () => { storyboard.UseSkinSprites = true; - storyboard.AlwaysProvideTexture = false; + storyboard.ProvideResources = false; }); AddStep("create sprites", () => SetContents(_ => createSprite(lookup_name, Anchor.TopLeft, Vector2.Zero))); @@ -109,7 +144,7 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("allow all lookups", () => { storyboard.UseSkinSprites = true; - storyboard.AlwaysProvideTexture = true; + storyboard.ProvideResources = true; }); AddStep("create sprites", () => SetContents(_ => createSprite(lookup_name, Anchor.TopLeft, Vector2.Zero))); @@ -127,7 +162,7 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("allow all lookups", () => { storyboard.UseSkinSprites = true; - storyboard.AlwaysProvideTexture = true; + storyboard.ProvideResources = true; }); AddStep("create sprites", () => SetContents(_ => createSprite(lookup_name, Anchor.TopLeft, Vector2.Zero))); @@ -142,7 +177,7 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("allow all lookups", () => { storyboard.UseSkinSprites = true; - storyboard.AlwaysProvideTexture = true; + storyboard.ProvideResources = true; }); AddStep("create sprites", () => SetContents(_ => createSprite(lookup_name, Anchor.TopLeft, Vector2.Zero))); @@ -156,7 +191,7 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("allow all lookups", () => { storyboard.UseSkinSprites = true; - storyboard.AlwaysProvideTexture = true; + storyboard.ProvideResources = true; }); AddStep("create sprites", () => SetContents(_ => createSprite(lookup_name, Anchor.TopLeft, Vector2.Zero))); @@ -170,7 +205,7 @@ namespace osu.Game.Tests.Visual.Gameplay AddAssert("origin back", () => sprites.All(s => s.Origin == Anchor.TopLeft)); } - private DrawableStoryboard createSprite(string lookupName, Anchor origin, Vector2 initialPosition) + private Drawable createSprite(string lookupName, Anchor origin, Vector2 initialPosition) { var layer = storyboard.GetLayer("Background"); @@ -180,7 +215,14 @@ namespace osu.Game.Tests.Visual.Gameplay layer.Elements.Clear(); layer.Add(sprite); - return storyboard.CreateDrawable().With(s => s.RelativeSizeAxes = Axes.Both); + return new Container + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + storyboard.CreateDrawable() + } + }; } private void assertStoryboardSourced() @@ -202,42 +244,52 @@ namespace osu.Game.Tests.Visual.Gameplay return new TestDrawableStoryboard(this, mods); } - public bool AlwaysProvideTexture { get; set; } + public bool ProvideResources { get; set; } - public override string GetStoragePathFromStoryboardPath(string path) => AlwaysProvideTexture ? path : string.Empty; + public override string GetStoragePathFromStoryboardPath(string path) => ProvideResources ? path : string.Empty; private partial class TestDrawableStoryboard : DrawableStoryboard { - private readonly bool alwaysProvideTexture; + private readonly bool provideResources; public TestDrawableStoryboard(TestStoryboard storyboard, IReadOnlyList? mods) : base(storyboard, mods) { - alwaysProvideTexture = storyboard.AlwaysProvideTexture; + provideResources = storyboard.ProvideResources; } - protected override IResourceStore CreateResourceLookupStore() => alwaysProvideTexture - ? new AlwaysReturnsTextureStore() + protected override IResourceStore CreateResourceLookupStore() => provideResources + ? new ResourcesTextureStore() : new ResourceStore(); - internal class AlwaysReturnsTextureStore : IResourceStore + internal class ResourcesTextureStore : IResourceStore { - private const string test_image = "Resources/Textures/test-image.png"; - private readonly DllResourceStore store; - public AlwaysReturnsTextureStore() + public ResourcesTextureStore() { store = TestResources.GetStore(); } public void Dispose() => store.Dispose(); - public byte[] Get(string name) => store.Get(test_image); + public byte[] Get(string name) => store.Get(map(name)); - public Task GetAsync(string name, CancellationToken cancellationToken = new CancellationToken()) => store.GetAsync(test_image, cancellationToken); + public Task GetAsync(string name, CancellationToken cancellationToken = new CancellationToken()) => store.GetAsync(map(name), cancellationToken); - public Stream GetStream(string name) => store.GetStream(test_image); + public Stream GetStream(string name) => store.GetStream(map(name)); + + private string map(string name) + { + switch (name) + { + case lookup_name: + return "Resources/Textures/test-image.png"; + + default: + return $"Resources/{name}"; + } + } public IEnumerable GetAvailableResources() => store.GetAvailableResources(); } From 7eeac0f3b90d34d71a8e2d7390b4a0310b89288b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 23 Apr 2024 17:34:52 +0800 Subject: [PATCH 0593/2493] Fix incorrect xmldoc MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bartłomiej Dach --- osu.Game/Storyboards/Drawables/DrawableStoryboardVideo.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardVideo.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardVideo.cs index ca2c7b774c..848699a4d2 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardVideo.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardVideo.cs @@ -21,7 +21,7 @@ namespace osu.Game.Storyboards.Drawables { Video = video; - // In osu-stable, a mapper can add a scale command for a storyboard. + // In osu-stable, a mapper can add a scale command for a storyboard video. // This allows scaling based on the video's absolute size. // // If not specified we take up the full available space. From 17ca29c2c6e0e5f141f5393923a511cfa2d61437 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 23 Apr 2024 17:37:37 +0800 Subject: [PATCH 0594/2493] Actually apply transforms to the video --- osu.Game/Storyboards/Drawables/DrawableStoryboardVideo.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardVideo.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardVideo.cs index 848699a4d2..f2454be190 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardVideo.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardVideo.cs @@ -50,6 +50,8 @@ namespace osu.Game.Storyboards.Drawables Origin = Anchor.Centre, Alpha = 0, }; + + Video.ApplyTransforms(drawableVideo); } protected override void LoadComplete() From a978518a74c362016d2aeb39d4a8f1fdcb78a28d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 23 Apr 2024 12:32:52 +0200 Subject: [PATCH 0595/2493] Add failing tests --- .../Settings/TestSceneKeyBindingPanel.cs | 44 ++++++++++++++++++- 1 file changed, 42 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingPanel.cs b/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingPanel.cs index 57c9770c9a..86008a56a4 100644 --- a/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingPanel.cs +++ b/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingPanel.cs @@ -296,7 +296,7 @@ namespace osu.Game.Tests.Visual.Settings } [Test] - public void TestBindingConflictResolvedByRollback() + public void TestBindingConflictResolvedByRollbackViaMouse() { AddStep("reset taiko section to default", () => { @@ -315,7 +315,7 @@ namespace osu.Game.Tests.Visual.Settings } [Test] - public void TestBindingConflictResolvedByOverwrite() + public void TestBindingConflictResolvedByOverwriteViaMouse() { AddStep("reset taiko section to default", () => { @@ -333,6 +333,46 @@ namespace osu.Game.Tests.Visual.Settings checkBinding("Left (rim)", "M1"); } + [Test] + public void TestBindingConflictResolvedByRollbackViaKeyboard() + { + AddStep("reset taiko & global sections to default", () => + { + panel.ChildrenOfType().First(section => new TaikoRuleset().RulesetInfo.Equals(section.Ruleset)) + .ChildrenOfType().Single().TriggerClick(); + + panel.ChildrenOfType().First().TriggerClick(); + }); + AddStep("move mouse to centre", () => InputManager.MoveMouseTo(panel.ScreenSpaceDrawQuad.Centre)); + scrollToAndStartBinding("Left (rim)"); + AddStep("attempt to bind M1 to two keys", () => InputManager.Click(MouseButton.Left)); + + AddUntilStep("wait for popover", () => panel.ChildrenOfType().SingleOrDefault(), () => Is.Not.Null); + AddStep("press Esc", () => InputManager.Key(Key.Escape)); + checkBinding("Left (centre)", "M1"); + checkBinding("Left (rim)", "M2"); + } + + [Test] + public void TestBindingConflictResolvedByOverwriteViaKeyboard() + { + AddStep("reset taiko & global sections to default", () => + { + panel.ChildrenOfType().First(section => new TaikoRuleset().RulesetInfo.Equals(section.Ruleset)) + .ChildrenOfType().Single().TriggerClick(); + + panel.ChildrenOfType().First().TriggerClick(); + }); + AddStep("move mouse to centre", () => InputManager.MoveMouseTo(panel.ScreenSpaceDrawQuad.Centre)); + scrollToAndStartBinding("Left (rim)"); + AddStep("attempt to bind M1 to two keys", () => InputManager.Click(MouseButton.Left)); + + AddUntilStep("wait for popover", () => panel.ChildrenOfType().SingleOrDefault(), () => Is.Not.Null); + AddStep("press Enter", () => InputManager.Key(Key.Enter)); + checkBinding("Left (centre)", InputSettingsStrings.ActionHasNoKeyBinding.ToString()); + checkBinding("Left (rim)", "M1"); + } + [Test] public void TestBindingConflictCausedByResetToDefaultOfSingleRow() { From 1e0db1ab9f676ffb781afe7d151c2d6d95126329 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 23 Apr 2024 12:44:16 +0200 Subject: [PATCH 0596/2493] Allow confirming keybinding overwrite on conflict via "select" binding --- .../Sections/Input/KeyBindingConflictPopover.cs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingConflictPopover.cs b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingConflictPopover.cs index 60d1bd31be..05aeb0b810 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingConflictPopover.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingConflictPopover.cs @@ -152,6 +152,17 @@ namespace osu.Game.Overlays.Settings.Sections.Input newPreview.IsChosen.Value = applyNewButton.IsHovered; } + public override bool OnPressed(KeyBindingPressEvent e) + { + if (e.Action == GlobalAction.Select && !e.Repeat) + { + applyNew(); + return true; + } + + return base.OnPressed(e); + } + private partial class ConflictingKeyBindingPreview : CompositeDrawable { private readonly object action; From 564dec7a142b8fe3bf6729b9223bc490df18b5c7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 23 Apr 2024 19:21:55 +0800 Subject: [PATCH 0597/2493] Add test coverage of transforms actually being applied to video --- .../Visual/Gameplay/TestSceneDrawableStoryboardSprite.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableStoryboardSprite.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableStoryboardSprite.cs index 6ac112cc5f..fc52d749b3 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableStoryboardSprite.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableStoryboardSprite.cs @@ -86,7 +86,10 @@ namespace osu.Game.Tests.Visual.Gameplay sprite.AddLoop(Time.Current, 100).Alpha.Add(Easing.None, 0, 10000, 1, 1); if (scaleTransformProvided) - sprite.TimelineGroup.Scale.Add(Easing.None, Time.Current, Time.Current, 1, 1); + { + sprite.TimelineGroup.Scale.Add(Easing.None, Time.Current, Time.Current + 1000, 1, 2); + sprite.TimelineGroup.Scale.Add(Easing.None, Time.Current + 1000, Time.Current + 2000, 2, 1); + } layer.Elements.Clear(); layer.Add(sprite); From f7626aba1821f5f346dfa36a06438836bbc819ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 23 Apr 2024 13:36:12 +0200 Subject: [PATCH 0598/2493] Fix mod select overlay columns not displaying properly sometimes Closes https://github.com/ppy/osu/issues/26504. As far as I can tell the issue is basically another manifestation of https://github.com/ppy/osu-framework/issues/5129, i.e. presence overrides causing dropped invalidations and thus completely bogus hierarchy state. The fix is to raise the appropriate invalidation manually. --- osu.Game/Overlays/Mods/ModColumn.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/osu.Game/Overlays/Mods/ModColumn.cs b/osu.Game/Overlays/Mods/ModColumn.cs index df33c78ea4..e9f21338bd 100644 --- a/osu.Game/Overlays/Mods/ModColumn.cs +++ b/osu.Game/Overlays/Mods/ModColumn.cs @@ -69,6 +69,7 @@ namespace osu.Game.Overlays.Mods private Task? latestLoadTask; private ModPanel[]? latestLoadedPanels; internal bool ItemsLoaded => latestLoadTask?.IsCompleted == true && allPanelsLoaded; + private bool? wasPresent; private bool allPanelsLoaded { @@ -192,6 +193,15 @@ namespace osu.Game.Overlays.Mods { base.Update(); + // we override `IsPresent` to include the scheduler's pending task state to make async loads work correctly when columns are masked away + // (see description of https://github.com/ppy/osu/pull/19783). + // however, because of that we must also ensure that we signal correct invalidations (https://github.com/ppy/osu-framework/issues/5129). + // failing to do so causes columns to be stuck in "present" mode despite actually not being present themselves. + // this works because `Update()` will always run after a scheduler update, which is what causes the presence state change responsible for the failure. + if (wasPresent != null && wasPresent != IsPresent) + Invalidate(Invalidation.Presence); + wasPresent = IsPresent; + if (selectionDelay == initial_multiple_selection_delay || Time.Current - lastSelection >= selectionDelay) { if (pendingSelectionOperations.TryDequeue(out var dequeuedAction)) From 804b1b0d884da13b1a79a31ffd09e23a16bffbe8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 23 Apr 2024 20:54:44 +0800 Subject: [PATCH 0599/2493] Fix settings colour scheme wrong when viewing gameplay from skin editor button Closes https://github.com/ppy/osu/issues/27949. --- osu.Game/Screens/Play/Player.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 3a80caf259..42ff1d74f3 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -101,6 +101,11 @@ namespace osu.Game.Screens.Play /// public IBindable ShowingOverlayComponents = new Bindable(); + // Should match PlayerLoader for consistency. Cached here for the rare case we push a Player + // without the loading screen (one such usage is the skin editor's scene library). + [Cached] + private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple); + [Resolved] private ScoreManager scoreManager { get; set; } From 436203a8c12093c076a9d4e5d01264ad9ce26f55 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 23 Apr 2024 21:32:32 +0800 Subject: [PATCH 0600/2493] Add a chevron to distinguish editor menus with submenus --- .../Edit/Components/Menus/EditorMenuBar.cs | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/osu.Game/Screens/Edit/Components/Menus/EditorMenuBar.cs b/osu.Game/Screens/Edit/Components/Menus/EditorMenuBar.cs index 0e125d0ec0..152bcee214 100644 --- a/osu.Game/Screens/Edit/Components/Menus/EditorMenuBar.cs +++ b/osu.Game/Screens/Edit/Components/Menus/EditorMenuBar.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.Linq; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -184,6 +185,17 @@ namespace osu.Game.Screens.Edit.Components.Menus { } + private bool hasSubmenu => Item.Items.Any(); + + protected override TextContainer CreateTextContainer() => base.CreateTextContainer().With(c => + { + c.Padding = new MarginPadding + { + // Add some padding for the chevron below. + Right = hasSubmenu ? 5 : 0, + }; + }); + [BackgroundDependencyLoader] private void load(OverlayColourProvider colourProvider) { @@ -191,6 +203,18 @@ namespace osu.Game.Screens.Edit.Components.Menus BackgroundColourHover = colourProvider.Background1; Foreground.Padding = new MarginPadding { Vertical = 2 }; + + if (hasSubmenu) + { + AddInternal(new SpriteIcon + { + Margin = new MarginPadding(6), + Size = new Vector2(8), + Icon = FontAwesome.Solid.ChevronRight, + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + }); + } } } } From 602b16f533dd5e578025144d025dc3a9c7336fa7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 23 Apr 2024 22:03:32 +0800 Subject: [PATCH 0601/2493] Fix fade-in no longer working on videos --- .../Visual/Gameplay/TestSceneDrawableStoryboardSprite.cs | 2 -- osu.Game/Storyboards/StoryboardVideo.cs | 4 +++- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableStoryboardSprite.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableStoryboardSprite.cs index fc52d749b3..8fa2c9922e 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableStoryboardSprite.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableStoryboardSprite.cs @@ -83,8 +83,6 @@ namespace osu.Game.Tests.Visual.Gameplay var sprite = new StoryboardVideo("Videos/test-video.mp4", Time.Current); - sprite.AddLoop(Time.Current, 100).Alpha.Add(Easing.None, 0, 10000, 1, 1); - if (scaleTransformProvided) { sprite.TimelineGroup.Scale.Add(Easing.None, Time.Current, Time.Current + 1000, 1, 2); diff --git a/osu.Game/Storyboards/StoryboardVideo.cs b/osu.Game/Storyboards/StoryboardVideo.cs index bd1b933b6f..5573162d26 100644 --- a/osu.Game/Storyboards/StoryboardVideo.cs +++ b/osu.Game/Storyboards/StoryboardVideo.cs @@ -12,7 +12,9 @@ namespace osu.Game.Storyboards public StoryboardVideo(string path, double offset) : base(path, Anchor.Centre, Vector2.Zero) { - TimelineGroup.Alpha.Add(Easing.None, offset, offset, 0, 1); + // This is just required to get a valid StartTime based on the incoming offset. + // Actual fades are handled inside DrawableStoryboardVideo for now. + TimelineGroup.Alpha.Add(Easing.None, offset, offset, 0, 0); } public override Drawable CreateDrawable() => new DrawableStoryboardVideo(this); From d0edf72a0c384004f7e6fbb252d368af3fa14cdf Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 23 Apr 2024 22:04:28 +0800 Subject: [PATCH 0602/2493] 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 c61977cfa3..97dfe5d9f7 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -10,7 +10,7 @@ true - + diff --git a/osu.iOS.props b/osu.iOS.props index 6389172fe7..66347acdf0 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -23,6 +23,6 @@ iossimulator-x64 - + From 787e60f70649d5346c1916332ff74cab0518f959 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 23 Apr 2024 18:37:15 +0200 Subject: [PATCH 0603/2493] Fix `LegacyDrainingHealthProcessor` drain rate computation diverging to infinity --- osu.Game/Rulesets/Scoring/LegacyDrainingHealthProcessor.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/osu.Game/Rulesets/Scoring/LegacyDrainingHealthProcessor.cs b/osu.Game/Rulesets/Scoring/LegacyDrainingHealthProcessor.cs index 7cee5ebecf..25c5b3643a 100644 --- a/osu.Game/Rulesets/Scoring/LegacyDrainingHealthProcessor.cs +++ b/osu.Game/Rulesets/Scoring/LegacyDrainingHealthProcessor.cs @@ -129,6 +129,13 @@ namespace osu.Game.Rulesets.Scoring OnIterationFail?.Invoke($"FAILED drop {testDrop}: recovery too low ({recovery} < {hpRecoveryAvailable})"); } + if (!fail && double.IsInfinity(HpMultiplierNormal)) + { + OnIterationSuccess?.Invoke("Drain computation algorithm diverged to infinity. PASSING with zero drop, resetting HP multiplier to 1."); + HpMultiplierNormal = 1; + return 0; + } + if (!fail) { OnIterationSuccess?.Invoke($"PASSED drop {testDrop}"); From cbbf2dd1584da084293db3afe4bd4e6108e5f7e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 23 Apr 2024 18:58:40 +0200 Subject: [PATCH 0604/2493] Fix `DifficultyBindable` bound desync between `maxValue` and `CurrentNumber.MaxValue` --- osu.Game/Rulesets/Mods/DifficultyBindable.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Mods/DifficultyBindable.cs b/osu.Game/Rulesets/Mods/DifficultyBindable.cs index a207048882..5f6fd21860 100644 --- a/osu.Game/Rulesets/Mods/DifficultyBindable.cs +++ b/osu.Game/Rulesets/Mods/DifficultyBindable.cs @@ -48,7 +48,7 @@ namespace osu.Game.Rulesets.Mods } } - private float maxValue; + private float maxValue = 10; // matches default max value of `CurrentNumber` public float MaxValue { From 331f1f31b08c508d4d51008a402df2b9d6b46238 Mon Sep 17 00:00:00 2001 From: DavidBeh <67109172+DavidBeh@users.noreply.github.com> Date: Tue, 23 Apr 2024 19:19:11 +0200 Subject: [PATCH 0605/2493] Attempt to position DrawableOsuJudgement based on its DrawableOsuHitObject instead of DrawableOsuHitObject.HitObject --- osu.Game.Rulesets.Osu/Mods/OsuModMagnetised.cs | 4 ++-- .../Objects/Drawables/DrawableOsuJudgement.cs | 9 ++++++--- .../Objects/Drawables/DrawableSliderTick.cs | 2 +- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModMagnetised.cs b/osu.Game.Rulesets.Osu/Mods/OsuModMagnetised.cs index 860f96965a..f0ad284019 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModMagnetised.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModMagnetised.cs @@ -82,9 +82,9 @@ namespace osu.Game.Rulesets.Osu.Mods float y = (float)Interpolation.DampContinuously(hitObject.Y, destination.Y, dampLength, clock.ElapsedFrameTime); // I added these two lines - if (hitObject is DrawableOsuHitObject h) + /*if (hitObject is DrawableOsuHitObject h) h.HitObject.Position = new Vector2(x, y); - +*/ hitObject.Position = new Vector2(x, y); } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs index 76ae7340ff..0960748320 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs @@ -39,10 +39,13 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables Lighting.ResetAnimation(); Lighting.SetColourFrom(JudgedObject, Result); - if (JudgedObject?.HitObject is OsuHitObject osuObject) + if (JudgedObject is DrawableOsuHitObject osuObject) { - Position = osuObject.StackedEndPosition; - Scale = new Vector2(osuObject.Scale); + Position = osuObject.ToSpaceOfOtherDrawable(Vector2.Zero, Parent); + // Works only for normal hit circles, also with magnetised: + // Position = osuObject.Position; + + Scale = new Vector2(osuObject.HitObject.Scale); } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs index 73c061afbd..0c7ba180f2 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs @@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables public const float DEFAULT_TICK_SIZE = 16; - protected DrawableSlider DrawableSlider => (DrawableSlider)ParentHitObject; + public DrawableSlider DrawableSlider => (DrawableSlider)ParentHitObject; private SkinnableDrawable scaleContainer; From 3a914b9337ce15cff315162431fef5769903ba8e Mon Sep 17 00:00:00 2001 From: DavidBeh <67109172+DavidBeh@users.noreply.github.com> Date: Tue, 23 Apr 2024 23:24:51 +0200 Subject: [PATCH 0606/2493] Fixed judgements with MG mod without causing side effects --- osu.Game.Rulesets.Osu/Mods/OsuModMagnetised.cs | 4 ---- .../Objects/Drawables/DrawableOsuJudgement.cs | 5 +---- osu.Game.Rulesets.Osu/Objects/Slider.cs | 8 +------- 3 files changed, 2 insertions(+), 15 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModMagnetised.cs b/osu.Game.Rulesets.Osu/Mods/OsuModMagnetised.cs index f0ad284019..c64b5a18bc 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModMagnetised.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModMagnetised.cs @@ -81,10 +81,6 @@ namespace osu.Game.Rulesets.Osu.Mods float x = (float)Interpolation.DampContinuously(hitObject.X, destination.X, dampLength, clock.ElapsedFrameTime); float y = (float)Interpolation.DampContinuously(hitObject.Y, destination.Y, dampLength, clock.ElapsedFrameTime); - // I added these two lines - /*if (hitObject is DrawableOsuHitObject h) - h.HitObject.Position = new Vector2(x, y); -*/ hitObject.Position = new Vector2(x, y); } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs index 0960748320..d0270c68f6 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs @@ -41,10 +41,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables if (JudgedObject is DrawableOsuHitObject osuObject) { - Position = osuObject.ToSpaceOfOtherDrawable(Vector2.Zero, Parent); - // Works only for normal hit circles, also with magnetised: - // Position = osuObject.Position; - + Position = osuObject.ToSpaceOfOtherDrawable(osuObject.OriginPosition, Parent!); Scale = new Vector2(osuObject.HitObject.Scale); } } diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs index 2660933a70..5b52996e22 100644 --- a/osu.Game.Rulesets.Osu/Objects/Slider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs @@ -8,7 +8,6 @@ using osu.Game.Rulesets.Objects.Types; using System.Collections.Generic; using osu.Game.Rulesets.Objects; using System.Linq; -using System.Runtime.CompilerServices; using System.Threading; using Newtonsoft.Json; using osu.Framework.Bindables; @@ -25,7 +24,6 @@ namespace osu.Game.Rulesets.Osu.Objects { public class Slider : OsuHitObject, IHasPathWithRepeats, IHasSliderVelocity, IHasGenerateTicks { - private static readonly ConditionalWeakTable> SliderProgress = new ConditionalWeakTable>(); public double EndTime => StartTime + this.SpanCount() * Path.Distance / Velocity; @@ -204,7 +202,6 @@ namespace osu.Game.Rulesets.Osu.Objects Position = Position + Path.PositionAt(e.PathProgress), StackHeight = StackHeight, }); - SliderProgress.AddOrUpdate(NestedHitObjects.Last(), new StrongBox(e.PathProgress)); break; case SliderEventType.Head: @@ -236,7 +233,6 @@ namespace osu.Game.Rulesets.Osu.Objects Position = Position + Path.PositionAt(e.PathProgress), StackHeight = StackHeight, }); - SliderProgress.Add(NestedHitObjects.Last(), new StrongBox(e.PathProgress)); break; } } @@ -254,9 +250,7 @@ namespace osu.Game.Rulesets.Osu.Objects if (TailCircle != null) TailCircle.Position = EndPosition; - foreach (var hitObject in NestedHitObjects) - if (hitObject is SliderTick or SliderRepeat) - ((OsuHitObject)hitObject).Position = Position + Path.PositionAt(SliderProgress.TryGetValue(hitObject, out var progress) ? progress?.Value ?? 0 : 0); + // Positions of other nested hitobjects are not updated } protected void UpdateNestedSamples() From f863ea30e10fce9dce2d388a0e8b0f7f7532c9f1 Mon Sep 17 00:00:00 2001 From: DavidBeh <67109172+DavidBeh@users.noreply.github.com> Date: Tue, 23 Apr 2024 23:31:23 +0200 Subject: [PATCH 0607/2493] Made judgements always on and disabled follow paths again --- osu.Game.Rulesets.Osu/Mods/OsuModMagnetised.cs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModMagnetised.cs b/osu.Game.Rulesets.Osu/Mods/OsuModMagnetised.cs index c64b5a18bc..97ec669703 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModMagnetised.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModMagnetised.cs @@ -13,6 +13,7 @@ using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects.Drawables; +using osu.Game.Rulesets.Osu.UI; using osu.Game.Rulesets.UI; using osuTK; @@ -36,16 +37,11 @@ namespace osu.Game.Rulesets.Osu.Mods MaxValue = 1.0f, }; - // Bindable Setting for Show Judgements - [SettingSource("Show Judgements", "Whether to show judgements or not.")] - public BindableBool ShowJudgements { get; } = new BindableBool(true); - public void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset) { // Hide judgment displays and follow points as they won't make any sense. // Judgements can potentially be turned on in a future where they display at a position relative to their drawable counterpart. - drawableRuleset.Playfield.DisplayJudgements.Value = ShowJudgements.Value; - //(drawableRuleset.Playfield as OsuPlayfield)?.FollowPoints.Hide(); + (drawableRuleset.Playfield as OsuPlayfield)?.FollowPoints.Hide(); } public void Update(Playfield playfield) From 7dac5afd90bf3fa7194e46c20c9b10e3fe7450e9 Mon Sep 17 00:00:00 2001 From: DavidBeh <67109172+DavidBeh@users.noreply.github.com> Date: Tue, 23 Apr 2024 23:57:27 +0200 Subject: [PATCH 0608/2493] Enabled judgements for repel (but not in depth). Updated comments in repel, mag, depth --- osu.Game.Rulesets.Osu/Mods/OsuModDepth.cs | 6 ++++-- osu.Game.Rulesets.Osu/Mods/OsuModMagnetised.cs | 2 +- osu.Game.Rulesets.Osu/Mods/OsuModRepel.cs | 3 +-- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModDepth.cs b/osu.Game.Rulesets.Osu/Mods/OsuModDepth.cs index a9111eec1f..6d4a621f4d 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModDepth.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModDepth.cs @@ -47,10 +47,12 @@ namespace osu.Game.Rulesets.Osu.Mods public void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset) { - // Hide judgment displays and follow points as they won't make any sense. + // Hide follow points as they won't make any sense. // Judgements can potentially be turned on in a future where they display at a position relative to their drawable counterpart. - drawableRuleset.Playfield.DisplayJudgements.Value = false; (drawableRuleset.Playfield as OsuPlayfield)?.FollowPoints.Hide(); + // Hide judgements as they don't move with the drawables after appearing, which does look bad. + // They would need to either move with them or disappear sooner. + drawableRuleset.Playfield.DisplayJudgements.Value = false; } private void applyTransform(DrawableHitObject drawable, ArmedState state) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModMagnetised.cs b/osu.Game.Rulesets.Osu/Mods/OsuModMagnetised.cs index 97ec669703..b2553e295c 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModMagnetised.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModMagnetised.cs @@ -39,7 +39,7 @@ namespace osu.Game.Rulesets.Osu.Mods public void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset) { - // Hide judgment displays and follow points as they won't make any sense. + // Hide follow points as they won't make any sense. // Judgements can potentially be turned on in a future where they display at a position relative to their drawable counterpart. (drawableRuleset.Playfield as OsuPlayfield)?.FollowPoints.Hide(); } diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModRepel.cs b/osu.Game.Rulesets.Osu/Mods/OsuModRepel.cs index ced98f0cd5..302e17432e 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModRepel.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModRepel.cs @@ -38,9 +38,8 @@ namespace osu.Game.Rulesets.Osu.Mods public void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset) { - // Hide judgment displays and follow points as they won't make any sense. + // Hide follow points as they won't make any sense. // Judgements can potentially be turned on in a future where they display at a position relative to their drawable counterpart. - drawableRuleset.Playfield.DisplayJudgements.Value = false; (drawableRuleset.Playfield as OsuPlayfield)?.FollowPoints.Hide(); } From 16190a4ed7efe012de82aa9cf2b25925556e4a3c Mon Sep 17 00:00:00 2001 From: DavidBeh <67109172+DavidBeh@users.noreply.github.com> Date: Wed, 24 Apr 2024 00:23:45 +0200 Subject: [PATCH 0609/2493] Made judgements follow DrawableOsuHitObjects. Enabled judgements for depth mod --- osu.Game.Rulesets.Osu/Mods/OsuModDepth.cs | 3 --- .../Objects/Drawables/DrawableOsuJudgement.cs | 11 +++++++++++ 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModDepth.cs b/osu.Game.Rulesets.Osu/Mods/OsuModDepth.cs index 6d4a621f4d..306dcee839 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModDepth.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModDepth.cs @@ -50,9 +50,6 @@ namespace osu.Game.Rulesets.Osu.Mods // Hide follow points as they won't make any sense. // Judgements can potentially be turned on in a future where they display at a position relative to their drawable counterpart. (drawableRuleset.Playfield as OsuPlayfield)?.FollowPoints.Hide(); - // Hide judgements as they don't move with the drawables after appearing, which does look bad. - // They would need to either move with them or disappear sooner. - drawableRuleset.Playfield.DisplayJudgements.Value = false; } private void applyTransform(DrawableHitObject drawable, ArmedState state) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs index d0270c68f6..64bf25cceb 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs @@ -46,6 +46,17 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables } } + + protected override void Update() + { + base.Update(); + if (JudgedObject is DrawableOsuHitObject osuObject && Parent != null && osuObject.HitObject != null) + { + Position = osuObject.ToSpaceOfOtherDrawable(osuObject.OriginPosition, Parent!); + Scale = new Vector2(osuObject.HitObject.Scale); + } + } + protected override void ApplyHitAnimations() { bool hitLightingEnabled = config.Get(OsuSetting.HitLighting); From 16fdd4e08d81cfc00ddc1a6d370fdece195c3461 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 24 Apr 2024 09:01:31 +0800 Subject: [PATCH 0610/2493] Add ability to show beatmap source using skin editor's beatmap attribute text As per https://github.com/ppy/osu/discussions/27955. --- osu.Game/Localisation/EditorSetupStrings.cs | 32 +++++++++++++------ .../Components/BeatmapAttributeText.cs | 3 ++ 2 files changed, 26 insertions(+), 9 deletions(-) diff --git a/osu.Game/Localisation/EditorSetupStrings.cs b/osu.Game/Localisation/EditorSetupStrings.cs index eff6f9e6b8..350517734f 100644 --- a/osu.Game/Localisation/EditorSetupStrings.cs +++ b/osu.Game/Localisation/EditorSetupStrings.cs @@ -42,7 +42,8 @@ namespace osu.Game.Localisation /// /// "If enabled, an "Are you ready? 3, 2, 1, GO!" countdown will be inserted at the beginning of the beatmap, assuming there is enough time to do so." /// - public static LocalisableString CountdownDescription => new TranslatableString(getKey(@"countdown_description"), @"If enabled, an ""Are you ready? 3, 2, 1, GO!"" countdown will be inserted at the beginning of the beatmap, assuming there is enough time to do so."); + public static LocalisableString CountdownDescription => new TranslatableString(getKey(@"countdown_description"), + @"If enabled, an ""Are you ready? 3, 2, 1, GO!"" countdown will be inserted at the beginning of the beatmap, assuming there is enough time to do so."); /// /// "Countdown speed" @@ -52,7 +53,8 @@ namespace osu.Game.Localisation /// /// "If the countdown sounds off-time, use this to make it appear one or more beats early." /// - public static LocalisableString CountdownOffsetDescription => new TranslatableString(getKey(@"countdown_offset_description"), @"If the countdown sounds off-time, use this to make it appear one or more beats early."); + public static LocalisableString CountdownOffsetDescription => + new TranslatableString(getKey(@"countdown_offset_description"), @"If the countdown sounds off-time, use this to make it appear one or more beats early."); /// /// "Countdown offset" @@ -67,7 +69,8 @@ namespace osu.Game.Localisation /// /// "Allows storyboards to use the full screen space, rather than be confined to a 4:3 area." /// - public static LocalisableString WidescreenSupportDescription => new TranslatableString(getKey(@"widescreen_support_description"), @"Allows storyboards to use the full screen space, rather than be confined to a 4:3 area."); + public static LocalisableString WidescreenSupportDescription => + new TranslatableString(getKey(@"widescreen_support_description"), @"Allows storyboards to use the full screen space, rather than be confined to a 4:3 area."); /// /// "Epilepsy warning" @@ -77,7 +80,8 @@ namespace osu.Game.Localisation /// /// "Recommended if the storyboard or video contain scenes with rapidly flashing colours." /// - public static LocalisableString EpilepsyWarningDescription => new TranslatableString(getKey(@"epilepsy_warning_description"), @"Recommended if the storyboard or video contain scenes with rapidly flashing colours."); + public static LocalisableString EpilepsyWarningDescription => + new TranslatableString(getKey(@"epilepsy_warning_description"), @"Recommended if the storyboard or video contain scenes with rapidly flashing colours."); /// /// "Letterbox during breaks" @@ -87,7 +91,8 @@ namespace osu.Game.Localisation /// /// "Adds horizontal letterboxing to give a cinematic look during breaks." /// - public static LocalisableString LetterboxDuringBreaksDescription => new TranslatableString(getKey(@"letterbox_during_breaks_description"), @"Adds horizontal letterboxing to give a cinematic look during breaks."); + public static LocalisableString LetterboxDuringBreaksDescription => + new TranslatableString(getKey(@"letterbox_during_breaks_description"), @"Adds horizontal letterboxing to give a cinematic look during breaks."); /// /// "Samples match playback rate" @@ -97,7 +102,8 @@ namespace osu.Game.Localisation /// /// "When enabled, all samples will speed up or slow down when rate-changing mods are enabled." /// - public static LocalisableString SamplesMatchPlaybackRateDescription => new TranslatableString(getKey(@"samples_match_playback_rate_description"), @"When enabled, all samples will speed up or slow down when rate-changing mods are enabled."); + public static LocalisableString SamplesMatchPlaybackRateDescription => new TranslatableString(getKey(@"samples_match_playback_rate_description"), + @"When enabled, all samples will speed up or slow down when rate-changing mods are enabled."); /// /// "The size of all hit objects" @@ -117,7 +123,8 @@ namespace osu.Game.Localisation /// /// "The harshness of hit windows and difficulty of special objects (ie. spinners)" /// - public static LocalisableString OverallDifficultyDescription => new TranslatableString(getKey(@"overall_difficulty_description"), @"The harshness of hit windows and difficulty of special objects (ie. spinners)"); + public static LocalisableString OverallDifficultyDescription => + new TranslatableString(getKey(@"overall_difficulty_description"), @"The harshness of hit windows and difficulty of special objects (ie. spinners)"); /// /// "Tick Rate" @@ -127,7 +134,8 @@ namespace osu.Game.Localisation /// /// "Determines how many "ticks" are generated within long hit objects. A tick rate of 1 will generate ticks on each beat, 2 would be twice per beat, etc." /// - public static LocalisableString TickRateDescription => new TranslatableString(getKey(@"tick_rate_description"), @"Determines how many ""ticks"" are generated within long hit objects. A tick rate of 1 will generate ticks on each beat, 2 would be twice per beat, etc."); + public static LocalisableString TickRateDescription => new TranslatableString(getKey(@"tick_rate_description"), + @"Determines how many ""ticks"" are generated within long hit objects. A tick rate of 1 will generate ticks on each beat, 2 would be twice per beat, etc."); /// /// "Base Velocity" @@ -137,7 +145,8 @@ namespace osu.Game.Localisation /// /// "The base velocity of the beatmap, affecting things like slider velocity and scroll speed in some rulesets." /// - public static LocalisableString BaseVelocityDescription => new TranslatableString(getKey(@"base_velocity_description"), @"The base velocity of the beatmap, affecting things like slider velocity and scroll speed in some rulesets."); + public static LocalisableString BaseVelocityDescription => new TranslatableString(getKey(@"base_velocity_description"), + @"The base velocity of the beatmap, affecting things like slider velocity and scroll speed in some rulesets."); /// /// "Metadata" @@ -159,6 +168,11 @@ namespace osu.Game.Localisation /// public static LocalisableString Creator => new TranslatableString(getKey(@"creator"), @"Creator"); + /// + /// "Source" + /// + public static LocalisableString Source => new TranslatableString(getKey(@"source"), @"Source"); + /// /// "Difficulty Name" /// diff --git a/osu.Game/Skinning/Components/BeatmapAttributeText.cs b/osu.Game/Skinning/Components/BeatmapAttributeText.cs index 52c439a624..5c5e509fb2 100644 --- a/osu.Game/Skinning/Components/BeatmapAttributeText.cs +++ b/osu.Game/Skinning/Components/BeatmapAttributeText.cs @@ -48,6 +48,7 @@ namespace osu.Game.Skinning.Components [BeatmapAttribute.Artist] = EditorSetupStrings.Artist, [BeatmapAttribute.DifficultyName] = EditorSetupStrings.DifficultyHeader, [BeatmapAttribute.Creator] = EditorSetupStrings.Creator, + [BeatmapAttribute.Source] = EditorSetupStrings.Source, [BeatmapAttribute.Length] = ArtistStrings.TracklistLength.ToTitle(), [BeatmapAttribute.RankedStatus] = BeatmapDiscussionsStrings.IndexFormBeatmapsetStatusDefault, [BeatmapAttribute.BPM] = BeatmapsetsStrings.ShowStatsBpm, @@ -88,6 +89,7 @@ namespace osu.Game.Skinning.Components valueDictionary[BeatmapAttribute.Artist] = new RomanisableString(workingBeatmap.BeatmapInfo.Metadata.ArtistUnicode, workingBeatmap.BeatmapInfo.Metadata.Artist); valueDictionary[BeatmapAttribute.DifficultyName] = workingBeatmap.BeatmapInfo.DifficultyName; valueDictionary[BeatmapAttribute.Creator] = workingBeatmap.BeatmapInfo.Metadata.Author.Username; + valueDictionary[BeatmapAttribute.Source] = workingBeatmap.BeatmapInfo.Metadata.Source; valueDictionary[BeatmapAttribute.Length] = TimeSpan.FromMilliseconds(workingBeatmap.BeatmapInfo.Length).ToFormattedDuration(); valueDictionary[BeatmapAttribute.RankedStatus] = workingBeatmap.BeatmapInfo.Status.GetLocalisableDescription(); valueDictionary[BeatmapAttribute.BPM] = workingBeatmap.BeatmapInfo.BPM.ToLocalisableString(@"F2"); @@ -132,6 +134,7 @@ namespace osu.Game.Skinning.Components StarRating, Title, Artist, + Source, DifficultyName, Creator, Length, From f97c519451e0b17c95309f2d78bbddd30614e5c9 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Wed, 24 Apr 2024 00:19:10 -0700 Subject: [PATCH 0611/2493] Add chevron to distinguish all menus with submenus --- .../UserInterface/DrawableOsuMenuItem.cs | 23 +++++++++++++++++++ .../Edit/Components/Menus/EditorMenuBar.cs | 23 ------------------- 2 files changed, 23 insertions(+), 23 deletions(-) diff --git a/osu.Game/Graphics/UserInterface/DrawableOsuMenuItem.cs b/osu.Game/Graphics/UserInterface/DrawableOsuMenuItem.cs index 2f2cb7e5f8..06ef75cf58 100644 --- a/osu.Game/Graphics/UserInterface/DrawableOsuMenuItem.cs +++ b/osu.Game/Graphics/UserInterface/DrawableOsuMenuItem.cs @@ -3,6 +3,7 @@ #nullable disable +using System.Linq; using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; @@ -12,6 +13,7 @@ using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; using osu.Framework.Localisation; using osu.Game.Graphics.Sprites; +using osuTK; using osuTK.Graphics; namespace osu.Game.Graphics.UserInterface @@ -40,6 +42,27 @@ namespace osu.Game.Graphics.UserInterface AddInternal(hoverClickSounds = new HoverClickSounds()); updateTextColour(); + + bool hasSubmenu = Item.Items.Any(); + + // Only add right chevron if direction of menu items is vertical (i.e. width is relative size, see `DrawableMenuItem.SetFlowDirection()`). + if (hasSubmenu && RelativeSizeAxes == Axes.X) + { + AddInternal(new SpriteIcon + { + Margin = new MarginPadding(6), + Size = new Vector2(8), + Icon = FontAwesome.Solid.ChevronRight, + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + }); + + text.Padding = new MarginPadding + { + // Add some padding for the chevron above. + Right = 5, + }; + } } protected override void LoadComplete() diff --git a/osu.Game/Screens/Edit/Components/Menus/EditorMenuBar.cs b/osu.Game/Screens/Edit/Components/Menus/EditorMenuBar.cs index 152bcee214..c410c2519b 100644 --- a/osu.Game/Screens/Edit/Components/Menus/EditorMenuBar.cs +++ b/osu.Game/Screens/Edit/Components/Menus/EditorMenuBar.cs @@ -185,17 +185,6 @@ namespace osu.Game.Screens.Edit.Components.Menus { } - private bool hasSubmenu => Item.Items.Any(); - - protected override TextContainer CreateTextContainer() => base.CreateTextContainer().With(c => - { - c.Padding = new MarginPadding - { - // Add some padding for the chevron below. - Right = hasSubmenu ? 5 : 0, - }; - }); - [BackgroundDependencyLoader] private void load(OverlayColourProvider colourProvider) { @@ -203,18 +192,6 @@ namespace osu.Game.Screens.Edit.Components.Menus BackgroundColourHover = colourProvider.Background1; Foreground.Padding = new MarginPadding { Vertical = 2 }; - - if (hasSubmenu) - { - AddInternal(new SpriteIcon - { - Margin = new MarginPadding(6), - Size = new Vector2(8), - Icon = FontAwesome.Solid.ChevronRight, - Anchor = Anchor.CentreRight, - Origin = Anchor.CentreRight, - }); - } } } } From 5f463b81a8ecc883ed8040cacf70139e11b042dd Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Wed, 24 Apr 2024 00:22:20 -0700 Subject: [PATCH 0612/2493] Remove hardcoded chevrons in test --- osu.Game.Tests/Visual/UserInterface/TestSceneContextMenu.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneContextMenu.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneContextMenu.cs index 7b80549854..2a2f267fc8 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneContextMenu.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneContextMenu.cs @@ -77,21 +77,21 @@ namespace osu.Game.Tests.Visual.UserInterface new OsuMenuItem(@"Some option"), new OsuMenuItem(@"Highlighted option", MenuItemType.Highlighted), new OsuMenuItem(@"Another option"), - new OsuMenuItem(@"Nested option >") + new OsuMenuItem(@"Nested option") { Items = new MenuItem[] { new OsuMenuItem(@"Sub-One"), new OsuMenuItem(@"Sub-Two"), new OsuMenuItem(@"Sub-Three"), - new OsuMenuItem(@"Sub-Nested option >") + new OsuMenuItem(@"Sub-Nested option") { Items = new MenuItem[] { new OsuMenuItem(@"Double Sub-One"), new OsuMenuItem(@"Double Sub-Two"), new OsuMenuItem(@"Double Sub-Three"), - new OsuMenuItem(@"Sub-Sub-Nested option >") + new OsuMenuItem(@"Sub-Sub-Nested option") { Items = new MenuItem[] { From 4f7c9f297068b5ec27aaa3d34e8176d6e0e7fe1a Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Wed, 24 Apr 2024 01:00:03 -0700 Subject: [PATCH 0613/2493] Remove unused using --- osu.Game/Screens/Edit/Components/Menus/EditorMenuBar.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Components/Menus/EditorMenuBar.cs b/osu.Game/Screens/Edit/Components/Menus/EditorMenuBar.cs index c410c2519b..0e125d0ec0 100644 --- a/osu.Game/Screens/Edit/Components/Menus/EditorMenuBar.cs +++ b/osu.Game/Screens/Edit/Components/Menus/EditorMenuBar.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.Linq; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; From 8c2a4eb78ab0b900f552f91641ba7b526d33443f Mon Sep 17 00:00:00 2001 From: DavidBeh <67109172+DavidBeh@users.noreply.github.com> Date: Wed, 24 Apr 2024 12:40:23 +0200 Subject: [PATCH 0614/2493] Fix formatting --- osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs | 2 +- osu.Game.Rulesets.Osu/Objects/Slider.cs | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs index 64bf25cceb..ffbf45291f 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs @@ -46,10 +46,10 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables } } - protected override void Update() { base.Update(); + if (JudgedObject is DrawableOsuHitObject osuObject && Parent != null && osuObject.HitObject != null) { Position = osuObject.ToSpaceOfOtherDrawable(osuObject.OriginPosition, Parent!); diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs index 5b52996e22..248f40208a 100644 --- a/osu.Game.Rulesets.Osu/Objects/Slider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs @@ -24,7 +24,6 @@ namespace osu.Game.Rulesets.Osu.Objects { public class Slider : OsuHitObject, IHasPathWithRepeats, IHasSliderVelocity, IHasGenerateTicks { - public double EndTime => StartTime + this.SpanCount() * Path.Distance / Velocity; [JsonIgnore] From 72726809cb6cdd53fb3f01d13b2438d323c47e72 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Wed, 24 Apr 2024 09:47:41 -0700 Subject: [PATCH 0615/2493] Ignore autogenerated .idea android file --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 525b3418cd..11fee27f28 100644 --- a/.gitignore +++ b/.gitignore @@ -340,4 +340,5 @@ inspectcode # Fody (pulled in by Realm) - schema file FodyWeavers.xsd -.idea/.idea.osu.Desktop/.idea/misc.xml \ No newline at end of file +.idea/.idea.osu.Desktop/.idea/misc.xml +.idea/.idea.osu.Android/.idea/deploymentTargetDropDown.xml From 94275f148e4eb523fbf4247503c7e072f66ef780 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 25 Apr 2024 09:01:47 +0200 Subject: [PATCH 0616/2493] Fix adding slider control points via context menu not undoing correctly Closes https://github.com/ppy/osu/issues/27985. --- .../Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs index 2da462caf4..49fdf12d60 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs @@ -403,7 +403,12 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders public override MenuItem[] ContextMenuItems => new MenuItem[] { - new OsuMenuItem("Add control point", MenuItemType.Standard, () => addControlPoint(rightClickPosition)), + new OsuMenuItem("Add control point", MenuItemType.Standard, () => + { + changeHandler?.BeginChange(); + addControlPoint(rightClickPosition); + changeHandler?.EndChange(); + }), new OsuMenuItem("Convert to stream", MenuItemType.Destructive, convertToStream), }; From da953b34a721540e266b33d495ce11525bf1c8fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 25 Apr 2024 09:52:26 +0200 Subject: [PATCH 0617/2493] Apply nullability annotations to `ResultsScreen` & inheritors --- .../Visual/Ranking/TestSceneResultsScreen.cs | 2 +- .../MultiplayerTeamResultsScreen.cs | 8 ++-- .../Spectate/MultiSpectatorResultsScreen.cs | 6 +-- .../Playlists/PlaylistsResultsScreen.cs | 35 +++++++--------- osu.Game/Screens/Ranking/ResultsScreen.cs | 42 +++++++++---------- osu.Game/Screens/Ranking/SoloResultsScreen.cs | 4 +- 6 files changed, 43 insertions(+), 54 deletions(-) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs index ffc5dbc8fb..fca1d0f82a 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs @@ -424,7 +424,7 @@ namespace osu.Game.Tests.Visual.Ranking scores.Add(score); } - scoresCallback?.Invoke(scores); + scoresCallback.Invoke(scores); return null; } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerTeamResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerTeamResultsScreen.cs index a8c513603c..ab83860ba7 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerTeamResultsScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerTeamResultsScreen.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.Collections.Generic; using System.Linq; @@ -28,8 +26,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer { private readonly SortedDictionary teamScores; - private Container winnerBackground; - private Drawable winnerText; + private Container winnerBackground = null!; + private Drawable winnerText = null!; public MultiplayerTeamResultsScreen(ScoreInfo score, long roomId, PlaylistItem playlistItem, SortedDictionary teamScores) : base(score, roomId, playlistItem) @@ -41,7 +39,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer } [Resolved] - private OsuColour colours { get; set; } + private OsuColour colours { get; set; } = null!; [BackgroundDependencyLoader] private void load() diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorResultsScreen.cs index 2afc187e40..c240bbea0c 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorResultsScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorResultsScreen.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.Collections.Generic; using osu.Game.Online.API; @@ -25,8 +23,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate Scheduler.AddDelayed(() => StatisticsPanel.ToggleVisibility(), 1000); } - protected override APIRequest FetchScores(Action> scoresCallback) => null; + protected override APIRequest? FetchScores(Action> scoresCallback) => null; - protected override APIRequest FetchNextPage(int direction, Action> scoresCallback) => null; + protected override APIRequest? FetchNextPage(int direction, Action> scoresCallback) => null; } } diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsResultsScreen.cs index add7aee8cd..fdb83b5ae8 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsResultsScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsResultsScreen.cs @@ -1,13 +1,10 @@ // 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.Collections.Generic; using System.Diagnostics; using System.Linq; -using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -25,23 +22,23 @@ namespace osu.Game.Screens.OnlinePlay.Playlists private readonly long roomId; private readonly PlaylistItem playlistItem; - protected LoadingSpinner LeftSpinner { get; private set; } - protected LoadingSpinner CentreSpinner { get; private set; } - protected LoadingSpinner RightSpinner { get; private set; } + protected LoadingSpinner LeftSpinner { get; private set; } = null!; + protected LoadingSpinner CentreSpinner { get; private set; } = null!; + protected LoadingSpinner RightSpinner { get; private set; } = null!; - private MultiplayerScores higherScores; - private MultiplayerScores lowerScores; + private MultiplayerScores? higherScores; + private MultiplayerScores? lowerScores; [Resolved] - private IAPIProvider api { get; set; } + private IAPIProvider api { get; set; } = null!; [Resolved] - private ScoreManager scoreManager { get; set; } + private ScoreManager scoreManager { get; set; } = null!; [Resolved] - private RulesetStore rulesets { get; set; } + private RulesetStore rulesets { get; set; } = null!; - public PlaylistsResultsScreen([CanBeNull] ScoreInfo score, long roomId, PlaylistItem playlistItem) + public PlaylistsResultsScreen(ScoreInfo? score, long roomId, PlaylistItem playlistItem) : base(score) { this.roomId = roomId; @@ -123,11 +120,11 @@ namespace osu.Game.Screens.OnlinePlay.Playlists return userScoreReq; } - protected override APIRequest FetchNextPage(int direction, Action> scoresCallback) + protected override APIRequest? FetchNextPage(int direction, Action> scoresCallback) { Debug.Assert(direction == 1 || direction == -1); - MultiplayerScores pivot = direction == -1 ? higherScores : lowerScores; + MultiplayerScores? pivot = direction == -1 ? higherScores : lowerScores; if (pivot?.Cursor == null) return null; @@ -147,7 +144,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists /// The callback to perform with the resulting scores. /// An optional score pivot to retrieve scores around. Can be null to retrieve scores from the highest score. /// The indexing . - private APIRequest createIndexRequest(Action> scoresCallback, [CanBeNull] MultiplayerScores pivot = null) + private APIRequest createIndexRequest(Action> scoresCallback, MultiplayerScores? pivot = null) { var indexReq = pivot != null ? new IndexPlaylistScoresRequest(roomId, playlistItem.ID, pivot.Cursor, pivot.Params) @@ -180,7 +177,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists /// The callback to invoke with the final s. /// The s that were retrieved from s. /// An optional pivot around which the scores were retrieved. - private void performSuccessCallback([NotNull] Action> callback, [NotNull] List scores, [CanBeNull] MultiplayerScores pivot = null) => Schedule(() => + private void performSuccessCallback(Action> callback, List scores, MultiplayerScores? pivot = null) => Schedule(() => { var scoreInfos = scores.Select(s => s.CreateScoreInfo(scoreManager, rulesets, playlistItem, Beatmap.Value.BeatmapInfo)).OrderByTotalScore().ToArray(); @@ -201,7 +198,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists hideLoadingSpinners(pivot); }); - private void hideLoadingSpinners([CanBeNull] MultiplayerScores pivot = null) + private void hideLoadingSpinners(MultiplayerScores? pivot = null) { CentreSpinner.Hide(); @@ -217,7 +214,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists /// The to set positions on. /// The pivot. /// The amount to increment the pivot position by for each in . - private void setPositions([NotNull] MultiplayerScores scores, [CanBeNull] MultiplayerScores pivot, int increment) + private void setPositions(MultiplayerScores scores, MultiplayerScores? pivot, int increment) => setPositions(scores, pivot?.Scores[^1].Position ?? 0, increment); /// @@ -226,7 +223,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists /// The to set positions on. /// The pivot position. /// The amount to increment the pivot position by for each in . - private void setPositions([NotNull] MultiplayerScores scores, int pivotPosition, int increment) + private void setPositions(MultiplayerScores scores, int pivotPosition, int increment) { foreach (var s in scores.Scores) { diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs index ebb0530046..1c3518909d 100644 --- a/osu.Game/Screens/Ranking/ResultsScreen.cs +++ b/osu.Game/Screens/Ranking/ResultsScreen.cs @@ -1,12 +1,9 @@ // 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.Collections.Generic; using System.Linq; -using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; @@ -45,25 +42,24 @@ namespace osu.Game.Screens.Ranking protected override OverlayActivation InitialOverlayActivationMode => OverlayActivation.UserTriggered; - public readonly Bindable SelectedScore = new Bindable(); + public readonly Bindable SelectedScore = new Bindable(); - [CanBeNull] - public readonly ScoreInfo Score; + public readonly ScoreInfo? Score; - protected ScorePanelList ScorePanelList { get; private set; } + protected ScorePanelList ScorePanelList { get; private set; } = null!; - protected VerticalScrollContainer VerticalScrollContent { get; private set; } - - [Resolved(CanBeNull = true)] - private Player player { get; set; } + protected VerticalScrollContainer VerticalScrollContent { get; private set; } = null!; [Resolved] - private IAPIProvider api { get; set; } + private Player? player { get; set; } - protected StatisticsPanel StatisticsPanel { get; private set; } + [Resolved] + private IAPIProvider api { get; set; } = null!; - private Drawable bottomPanel; - private Container detachedPanelContainer; + protected StatisticsPanel StatisticsPanel { get; private set; } = null!; + + private Drawable bottomPanel = null!; + private Container detachedPanelContainer = null!; private bool lastFetchCompleted; @@ -84,9 +80,9 @@ namespace osu.Game.Screens.Ranking /// public bool ShowUserStatistics { get; init; } - private Sample popInSample; + private Sample? popInSample; - protected ResultsScreen([CanBeNull] ScoreInfo score) + protected ResultsScreen(ScoreInfo? score) { Score = score; @@ -182,11 +178,11 @@ namespace osu.Game.Screens.Ranking Scheduler.AddDelayed(() => OverlayActivationMode.Value = OverlayActivation.All, shouldFlair ? AccuracyCircle.TOTAL_DURATION + 1000 : 0); } - if (AllowWatchingReplay) + if (SelectedScore.Value != null && AllowWatchingReplay) { buttons.Add(new ReplayDownloadButton(SelectedScore.Value) { - Score = { BindTarget = SelectedScore }, + Score = { BindTarget = SelectedScore! }, Width = 300 }); } @@ -225,7 +221,7 @@ namespace osu.Game.Screens.Ranking if (lastFetchCompleted) { - APIRequest nextPageRequest = null; + APIRequest? nextPageRequest = null; if (ScorePanelList.IsScrolledToStart) nextPageRequest = FetchNextPage(-1, fetchScoresCallback); @@ -245,7 +241,7 @@ namespace osu.Game.Screens.Ranking /// /// A callback which should be called when fetching is completed. Scheduling is not required. /// An responsible for the fetch operation. This will be queued and performed automatically. - protected virtual APIRequest FetchScores(Action> scoresCallback) => null; + protected virtual APIRequest? FetchScores(Action> scoresCallback) => null; /// /// Performs a fetch of the next page of scores. This is invoked every frame until a non-null is returned. @@ -253,7 +249,7 @@ namespace osu.Game.Screens.Ranking /// The fetch direction. -1 to fetch scores greater than the current start of the list, and 1 to fetch scores lower than the current end of the list. /// A callback which should be called when fetching is completed. Scheduling is not required. /// An responsible for the fetch operation. This will be queued and performed automatically. - protected virtual APIRequest FetchNextPage(int direction, Action> scoresCallback) => null; + protected virtual APIRequest? FetchNextPage(int direction, Action> scoresCallback) => null; /// /// Creates the to be used to display extended information about scores. @@ -327,7 +323,7 @@ namespace osu.Game.Screens.Ranking panel.Alpha = 0; } - private ScorePanel detachedPanel; + private ScorePanel? detachedPanel; private void onStatisticsStateChanged(ValueChangedEvent state) { diff --git a/osu.Game/Screens/Ranking/SoloResultsScreen.cs b/osu.Game/Screens/Ranking/SoloResultsScreen.cs index ee0251b5ac..33b4bf976b 100644 --- a/osu.Game/Screens/Ranking/SoloResultsScreen.cs +++ b/osu.Game/Screens/Ranking/SoloResultsScreen.cs @@ -27,7 +27,7 @@ namespace osu.Game.Screens.Ranking { } - protected override APIRequest? FetchScores(Action>? scoresCallback) + protected override APIRequest? FetchScores(Action> scoresCallback) { Debug.Assert(Score != null); @@ -35,7 +35,7 @@ namespace osu.Game.Screens.Ranking return null; getScoreRequest = new GetScoresRequest(Score.BeatmapInfo, Score.Ruleset); - getScoreRequest.Success += r => scoresCallback?.Invoke(r.Scores.Where(s => !s.MatchesOnlineID(Score)).Select(s => s.ToScoreInfo(rulesets, Beatmap.Value.BeatmapInfo))); + getScoreRequest.Success += r => scoresCallback.Invoke(r.Scores.Where(s => !s.MatchesOnlineID(Score)).Select(s => s.ToScoreInfo(rulesets, Beatmap.Value.BeatmapInfo))); return getScoreRequest; } From 9e919b784d99e80371bcdab948eeb80346b4892a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 25 Apr 2024 11:19:29 +0200 Subject: [PATCH 0618/2493] Add test case covering ignoring non-basic results --- .../Ranking/TestSceneHitEventTimingDistributionGraph.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneHitEventTimingDistributionGraph.cs b/osu.Game.Tests/Visual/Ranking/TestSceneHitEventTimingDistributionGraph.cs index 325a535731..3e38b66029 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneHitEventTimingDistributionGraph.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneHitEventTimingDistributionGraph.cs @@ -82,6 +82,14 @@ namespace osu.Game.Tests.Visual.Ranking }).ToList()); } + [Test] + public void TestNonBasicHitResultsAreIgnored() + { + createTest(CreateDistributedHitEvents(0, 50) + .Select(h => new HitEvent(h.TimeOffset, 1.0, h.TimeOffset > 0 ? HitResult.Ok : HitResult.LargeTickHit, placeholder_object, placeholder_object, null)) + .ToList()); + } + [Test] public void TestMultipleWindowsOfHitResult() { From b250a924b19687d626737c78dffbd6e692859d92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 25 Apr 2024 11:20:07 +0200 Subject: [PATCH 0619/2493] Do not show non-basic results in timing distribution graph Closes https://github.com/ppy/osu/issues/24274. Bit of an ad-hoc resolution but maybe fine? This basically proposes to bypass the problem described in the issue by just not showing tick hits at all on the distribution graph. --- .../Ranking/Statistics/HitEventTimingDistributionGraph.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs b/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs index 1260ec2339..47807a8346 100644 --- a/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs +++ b/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs @@ -59,7 +59,7 @@ namespace osu.Game.Screens.Ranking.Statistics /// The s to display the timing distribution of. public HitEventTimingDistributionGraph(IReadOnlyList hitEvents) { - this.hitEvents = hitEvents.Where(e => !(e.HitObject.HitWindows is HitWindows.EmptyHitWindows) && e.Result.IsHit()).ToList(); + this.hitEvents = hitEvents.Where(e => !(e.HitObject.HitWindows is HitWindows.EmptyHitWindows) && e.Result.IsBasic() && e.Result.IsHit()).ToList(); bins = Enumerable.Range(0, total_timing_distribution_bins).Select(_ => new Dictionary()).ToArray>(); } From d2e9c33b6a34442a0b3e1bead88eac96d20c53e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 25 Apr 2024 12:49:25 +0200 Subject: [PATCH 0620/2493] Add failing test case --- .../Editing/TestSceneDifficultyDelete.cs | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneDifficultyDelete.cs b/osu.Game.Tests/Visual/Editing/TestSceneDifficultyDelete.cs index 12e00c4485..0f99270a9b 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneDifficultyDelete.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneDifficultyDelete.cs @@ -12,6 +12,7 @@ using osu.Game.Beatmaps; using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets; using osu.Game.Rulesets.Osu; +using osu.Game.Screens.Edit; using osu.Game.Storyboards; using osu.Game.Tests.Beatmaps.IO; using osuTK.Input; @@ -83,6 +84,49 @@ namespace osu.Game.Tests.Visual.Editing } } + [Test] + public void TestDeleteDifficultyWithPendingChanges() + { + Guid deletedDifficultyID = Guid.Empty; + int countBeforeDeletion = 0; + string beatmapSetHashBefore = string.Empty; + + AddUntilStep("wait for editor to load", () => Editor?.ReadyForUse == true); + + AddStep("store selected difficulty", () => + { + deletedDifficultyID = EditorBeatmap.BeatmapInfo.ID; + countBeforeDeletion = Beatmap.Value.BeatmapSetInfo.Beatmaps.Count; + beatmapSetHashBefore = Beatmap.Value.BeatmapSetInfo.Hash; + }); + + AddStep("make change to difficulty", () => + { + EditorBeatmap.BeginChange(); + EditorBeatmap.BeatmapInfo.DifficultyName = "changin' things"; + EditorBeatmap.EndChange(); + }); + + AddStep("click File", () => this.ChildrenOfType().First().TriggerClick()); + + AddStep("click delete", () => getDeleteMenuItem().TriggerClick()); + AddUntilStep("wait for dialog", () => DialogOverlay.CurrentDialog != null); + AddAssert("dialog is deletion confirmation dialog", () => DialogOverlay.CurrentDialog, Is.InstanceOf); + AddStep("confirm", () => InputManager.Key(Key.Number1)); + + AddUntilStep("no next dialog", () => DialogOverlay.CurrentDialog == null); + AddUntilStep("switched to different difficulty", + () => this.ChildrenOfType().SingleOrDefault() != null && EditorBeatmap.BeatmapInfo.ID != deletedDifficultyID); + + AddAssert($"difficulty is unattached from set", + () => Beatmap.Value.BeatmapSetInfo.Beatmaps.Select(b => b.ID), () => Does.Not.Contain(deletedDifficultyID)); + AddAssert("beatmap set difficulty count decreased by one", + () => Beatmap.Value.BeatmapSetInfo.Beatmaps.Count, () => Is.EqualTo(countBeforeDeletion - 1)); + AddAssert("set hash changed", () => Beatmap.Value.BeatmapSetInfo.Hash, () => Is.Not.EqualTo(beatmapSetHashBefore)); + AddAssert($"difficulty is deleted from realm", + () => Realm.Run(r => r.Find(deletedDifficultyID)), () => Is.Null); + } + private DrawableOsuMenuItem getDeleteMenuItem() => this.ChildrenOfType() .Single(item => item.ChildrenOfType().Any(text => text.Text.ToString().StartsWith("Delete", StringComparison.Ordinal))); } From 19d006d8182812710bffd94d26a4fb2512c101cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 25 Apr 2024 12:51:30 +0200 Subject: [PATCH 0621/2493] Fix deleting modified difficulty via editor leaving user in broken state Closes https://github.com/ppy/osu/issues/22783. If the difficulty being edited has unsaved changes, the editor exit flow would prompt for save *after* the deletion method has run. This is undesirable from a UX standpoint, and also leaves the user in a broken state. Thus, just fake an update of the last saved hash of the beatmap to fool the editor into thinking that it's not dirty, so that the exit flow will not show a save dialog. --- osu.Game/Screens/Edit/Editor.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 37f4b4f5be..980c613311 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -1088,6 +1088,13 @@ namespace osu.Game.Screens.Edit var difficultiesBeforeDeletion = groupedOrderedBeatmaps.SelectMany(g => g).ToList(); + // if the difficulty being currently deleted has unsaved changes, + // the editor exit flow would prompt for save *after* this method has done its thing. + // this is generally undesirable and also ends up leaving the user in a broken state. + // therefore, just update the last saved hash to make the exit flow think the deleted beatmap is not dirty, + // so that it will not show the save dialog on exit. + updateLastSavedHash(); + beatmapManager.DeleteDifficultyImmediately(difficultyToDelete); int deletedIndex = difficultiesBeforeDeletion.IndexOf(difficultyToDelete); From c1107d2797ae807d33cdefff3edd54459781919d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 25 Apr 2024 14:31:13 +0200 Subject: [PATCH 0622/2493] Fully refetch working beatmap when entering editor Closes https://github.com/ppy/osu/issues/21794. I'm not actually super sure as to what the exact mode of failure is here, but it's 99% to do with working beatmap cache invalidation. Likely this can be even considered as another case of https://github.com/ppy/osu/issues/21357, but because this is a one-liner "fix," I'm PRing it anyways. The issue is confusing to understand when working with the swap scenario given in the issue, but it's a little easier to understand when performing the following: 1. Have a beatmap set with 2 difficulties. Let's call them "A" and "B". 2. From song select, without ever exiting to main menu, edit "A". Change the difficulty name to "AA". Save and exit back to song select; do not exit out to main menu. 3. From song select, edit "B". Change the difficulty name to "BB". Save and exit back to song select. 4. The difficulty names will be "A" and "BB". Basically what I *think* is causing this, is the fact that even though editor invalidates the working beatmap by refetching it afresh on exit, song select is blissfully unaware of this, and continues working with its own `BeatmapInfo` instances which have backlinks to `BeatmapSetInfo`. When editing the first of the two difficulties and then the second, the editing of the first one only invalidates the first one rather than the entire set, and the second difficulty continues to have a stale reference to the first one via the beatmap set, and as such ends up overwriting the changes from the first save when passed into the editor and modified again. --- osu.Game/Screens/Select/SongSelect.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index 15469fad5b..16879d0cf0 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -425,7 +425,7 @@ namespace osu.Game.Screens.Select if (!AllowEditing) throw new InvalidOperationException($"Attempted to edit when {nameof(AllowEditing)} is disabled"); - Beatmap.Value = beatmaps.GetWorkingBeatmap(beatmapInfo ?? beatmapInfoNoDebounce); + Beatmap.Value = beatmaps.GetWorkingBeatmap(beatmapInfo ?? beatmapInfoNoDebounce, true); this.Push(new EditorLoader()); } From 1756da0dda45363e89c9e3aaf3a70fb32356977d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 25 Apr 2024 21:14:09 +0800 Subject: [PATCH 0623/2493] Fix redundant string interpolations --- osu.Game.Tests/Visual/Editing/TestSceneDifficultyDelete.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneDifficultyDelete.cs b/osu.Game.Tests/Visual/Editing/TestSceneDifficultyDelete.cs index 0f99270a9b..d4bd77642c 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneDifficultyDelete.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneDifficultyDelete.cs @@ -118,12 +118,12 @@ namespace osu.Game.Tests.Visual.Editing AddUntilStep("switched to different difficulty", () => this.ChildrenOfType().SingleOrDefault() != null && EditorBeatmap.BeatmapInfo.ID != deletedDifficultyID); - AddAssert($"difficulty is unattached from set", + AddAssert("difficulty is unattached from set", () => Beatmap.Value.BeatmapSetInfo.Beatmaps.Select(b => b.ID), () => Does.Not.Contain(deletedDifficultyID)); AddAssert("beatmap set difficulty count decreased by one", () => Beatmap.Value.BeatmapSetInfo.Beatmaps.Count, () => Is.EqualTo(countBeforeDeletion - 1)); AddAssert("set hash changed", () => Beatmap.Value.BeatmapSetInfo.Hash, () => Is.Not.EqualTo(beatmapSetHashBefore)); - AddAssert($"difficulty is deleted from realm", + AddAssert("difficulty is deleted from realm", () => Realm.Run(r => r.Find(deletedDifficultyID)), () => Is.Null); } From 387fcb87819f15dbaa4c8d10423ea0d6bd01b6db Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 25 Apr 2024 21:31:36 +0800 Subject: [PATCH 0624/2493] Add a brief inline comment to make sure we don't undo the fix --- osu.Game/Screens/Select/SongSelect.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index 16879d0cf0..6225534e95 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -425,6 +425,7 @@ namespace osu.Game.Screens.Select if (!AllowEditing) throw new InvalidOperationException($"Attempted to edit when {nameof(AllowEditing)} is disabled"); + // Forced refetch is important here to guarantee correct invalidation across all difficulties. Beatmap.Value = beatmaps.GetWorkingBeatmap(beatmapInfo ?? beatmapInfoNoDebounce, true); this.Push(new EditorLoader()); } From e0e790fa9412368ff7b414791348476f05e28f2f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 26 Apr 2024 14:44:44 +0800 Subject: [PATCH 0625/2493] Fix a couple of xmldoc typos --- osu.Game/OsuGameBase.cs | 2 +- .../OnlinePlay/Multiplayer/Spectate/SpectatorSyncManager.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index fb7a238c46..0122afb239 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -94,7 +94,7 @@ namespace osu.Game public const int SAMPLE_DEBOUNCE_TIME = 20; /// - /// The maximum volume at which audio tracks should playback. This can be set lower than 1 to create some head-room for sound effects. + /// The maximum volume at which audio tracks should play back at. This can be set lower than 1 to create some head-room for sound effects. /// private const double global_track_volume_adjust = 0.8; diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/SpectatorSyncManager.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/SpectatorSyncManager.cs index fd61b60fe4..5ff52be8bc 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/SpectatorSyncManager.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/SpectatorSyncManager.cs @@ -130,7 +130,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate } /// - /// Updates the catchup states of all player clocks clocks. + /// Updates the catchup states of all player clocks. /// private void updatePlayerCatchup() { From 21d65568651a2760891dd3b2c5c6d55c1c99a688 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 26 Apr 2024 15:29:59 +0800 Subject: [PATCH 0626/2493] Remove managed clocks from `SpectatorSyncManager` on gameplay completion / abort --- .../Multiplayer/Spectate/MultiSpectatorScreen.cs | 13 +++++++++++-- .../Multiplayer/Spectate/SpectatorSyncManager.cs | 1 + osu.Game/Screens/Spectate/SpectatorScreen.cs | 7 +++++++ 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs index e2159f0e3b..cb00763e6b 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs @@ -244,10 +244,19 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate playerArea.LoadScore(spectatorGameplayState.Score); }); - protected override void FailGameplay(int userId) + protected override void FailGameplay(int userId) => Schedule(() => { // We probably want to visualise this in the future. - } + + var instance = instances.Single(i => i.UserId == userId); + syncManager.RemoveManagedClock(instance.SpectatorPlayerClock); + }); + + protected override void PassGameplay(int userId) => Schedule(() => + { + var instance = instances.Single(i => i.UserId == userId); + syncManager.RemoveManagedClock(instance.SpectatorPlayerClock); + }); protected override void QuitGameplay(int userId) => Schedule(() => { diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/SpectatorSyncManager.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/SpectatorSyncManager.cs index 5ff52be8bc..9eb448d9d0 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/SpectatorSyncManager.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/SpectatorSyncManager.cs @@ -76,6 +76,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate public void RemoveManagedClock(SpectatorPlayerClock clock) { playerClocks.Remove(clock); + Logger.Log($"Removing managed clock from {nameof(SpectatorSyncManager)} ({playerClocks.Count} remain)"); clock.IsRunning = false; } diff --git a/osu.Game/Screens/Spectate/SpectatorScreen.cs b/osu.Game/Screens/Spectate/SpectatorScreen.cs index c4aef3c878..ddc638b7c5 100644 --- a/osu.Game/Screens/Spectate/SpectatorScreen.cs +++ b/osu.Game/Screens/Spectate/SpectatorScreen.cs @@ -135,6 +135,7 @@ namespace osu.Game.Screens.Spectate case SpectatedUserState.Passed: markReceivedAllFrames(userId); + PassGameplay(userId); break; case SpectatedUserState.Failed: @@ -233,6 +234,12 @@ namespace osu.Game.Screens.Spectate /// The gameplay state. protected abstract void StartGameplay(int userId, SpectatorGameplayState spectatorGameplayState); + /// + /// Fired when a user passes gameplay. + /// + /// The user which passed. + protected virtual void PassGameplay(int userId) { } + /// /// Quits gameplay for a user. /// Thread safety is not guaranteed – should be scheduled as required. From fb2d28f7e03aba53ae905b906fcea30da9a9c4ad Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 26 Apr 2024 15:30:26 +0800 Subject: [PATCH 0627/2493] Fix audio being paused in a spectator session when all players finish playing --- .../OnlinePlay/Multiplayer/Spectate/SpectatorSyncManager.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/SpectatorSyncManager.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/SpectatorSyncManager.cs index 9eb448d9d0..1638102089 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/SpectatorSyncManager.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/SpectatorSyncManager.cs @@ -177,7 +177,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate /// private void updateMasterState() { - MasterClockState newState = playerClocks.Any(s => !s.IsCatchingUp) ? MasterClockState.Synchronised : MasterClockState.TooFarAhead; + // Clocks are removed as players complete the beatmap. + // Once there are no clocks we want to make sure the track plays out to the end. + MasterClockState newState = playerClocks.Count == 0 || playerClocks.Any(s => !s.IsCatchingUp) ? MasterClockState.Synchronised : MasterClockState.TooFarAhead; if (masterState == newState) return; From 694e3900dbd849bf5638efc8e8dbeec9cf57faf0 Mon Sep 17 00:00:00 2001 From: Taevas <67872932+TTTaevas@users.noreply.github.com> Date: Sat, 27 Apr 2024 23:43:27 +0200 Subject: [PATCH 0628/2493] Add missing space in setup wizard --- osu.Game/Overlays/FirstRunSetup/ScreenImportFromStable.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Overlays/FirstRunSetup/ScreenImportFromStable.cs b/osu.Game/Overlays/FirstRunSetup/ScreenImportFromStable.cs index b19a9c6c99..983cb0bbb4 100644 --- a/osu.Game/Overlays/FirstRunSetup/ScreenImportFromStable.cs +++ b/osu.Game/Overlays/FirstRunSetup/ScreenImportFromStable.cs @@ -128,6 +128,7 @@ namespace osu.Game.Overlays.FirstRunSetup if (available) { copyInformation.Text = FirstRunOverlayImportFromStableScreenStrings.DataMigrationNoExtraSpace; + copyInformation.AddText(@" "); // just to ensure correct spacing copyInformation.AddLink(FirstRunOverlayImportFromStableScreenStrings.LearnAboutHardLinks, LinkAction.OpenWiki, @"Client/Release_stream/Lazer/File_storage#via-hard-links"); } else if (!RuntimeInfo.IsDesktop) From 48c608e0169a1f9280f75fb3bdc0eadaa3466976 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Mon, 18 Mar 2024 12:22:23 -0700 Subject: [PATCH 0629/2493] Make player width a const --- osu.Game/Overlays/NowPlayingOverlay.cs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/osu.Game/Overlays/NowPlayingOverlay.cs b/osu.Game/Overlays/NowPlayingOverlay.cs index ab99370603..1145ebaa2f 100644 --- a/osu.Game/Overlays/NowPlayingOverlay.cs +++ b/osu.Game/Overlays/NowPlayingOverlay.cs @@ -33,6 +33,7 @@ namespace osu.Game.Overlays public LocalisableString Title => NowPlayingStrings.HeaderTitle; public LocalisableString Description => NowPlayingStrings.HeaderDescription; + private const float player_width = 400; private const float player_height = 130; private const float transition_length = 800; private const float progress_height = 10; @@ -70,7 +71,7 @@ namespace osu.Game.Overlays public NowPlayingOverlay() { - Width = 400; + Width = player_width; Margin = new MarginPadding(margin); } @@ -319,15 +320,15 @@ namespace osu.Game.Overlays switch (direction) { case TrackChangeDirection.Next: - newBackground.Position = new Vector2(400, 0); + newBackground.Position = new Vector2(player_width, 0); newBackground.MoveToX(0, 500, Easing.OutCubic); - background.MoveToX(-400, 500, Easing.OutCubic); + background.MoveToX(-player_width, 500, Easing.OutCubic); break; case TrackChangeDirection.Prev: - newBackground.Position = new Vector2(-400, 0); + newBackground.Position = new Vector2(-player_width, 0); newBackground.MoveToX(0, 500, Easing.OutCubic); - background.MoveToX(400, 500, Easing.OutCubic); + background.MoveToX(player_width, 500, Easing.OutCubic); break; } From d4951a093fd9746c1ac885a8af13874dfc5390a8 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Mon, 18 Mar 2024 12:24:02 -0700 Subject: [PATCH 0630/2493] Scroll now playing overlay text when overflowing --- .../TestSceneNowPlayingOverlay.cs | 20 +++- osu.Game/Overlays/NowPlayingOverlay.cs | 100 +++++++++++++++++- 2 files changed, 115 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneNowPlayingOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneNowPlayingOverlay.cs index d07b90025f..40e0d9250d 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneNowPlayingOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneNowPlayingOverlay.cs @@ -6,6 +6,7 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; +using osu.Game.Beatmaps; using osu.Game.Overlays; using osu.Game.Rulesets.Osu; @@ -22,8 +23,6 @@ namespace osu.Game.Tests.Visual.UserInterface [BackgroundDependencyLoader] private void load() { - Beatmap.Value = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo); - nowPlayingOverlay = new NowPlayingOverlay { Origin = Anchor.Centre, @@ -37,9 +36,26 @@ namespace osu.Game.Tests.Visual.UserInterface [Test] public void TestShowHideDisable() { + AddStep(@"set beatmap", () => Beatmap.Value = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo)); AddStep(@"show", () => nowPlayingOverlay.Show()); AddToggleStep(@"toggle beatmap lock", state => Beatmap.Disabled = state); AddStep(@"hide", () => nowPlayingOverlay.Hide()); } + + [Test] + public void TestLongMetadata() + { + AddStep(@"set beatmap", () => Beatmap.Value = CreateWorkingBeatmap(new Beatmap + { + Metadata = + { + Artist = "very very very very very very very very very very very long artist", + ArtistUnicode = "very very very very very very very very very very very long artist", + Title = "very very very very very very very very very very very long title", + TitleUnicode = "very very very very very very very very very very very long title", + } + })); + AddStep(@"show", () => nowPlayingOverlay.Show()); + } } } diff --git a/osu.Game/Overlays/NowPlayingOverlay.cs b/osu.Game/Overlays/NowPlayingOverlay.cs index 1145ebaa2f..be405257ca 100644 --- a/osu.Game/Overlays/NowPlayingOverlay.cs +++ b/osu.Game/Overlays/NowPlayingOverlay.cs @@ -6,6 +6,7 @@ using System.Threading; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -48,7 +49,7 @@ namespace osu.Game.Overlays private IconButton nextButton = null!; private IconButton playlistButton = null!; - private SpriteText title = null!, artist = null!; + private ScrollingTextContainer title = null!, artist = null!; private PlaylistOverlay? playlist; @@ -102,7 +103,7 @@ namespace osu.Game.Overlays Children = new[] { background = Empty(), - title = new OsuSpriteText + title = new ScrollingTextContainer { Origin = Anchor.BottomCentre, Anchor = Anchor.TopCentre, @@ -111,7 +112,7 @@ namespace osu.Game.Overlays Colour = Color4.White, Text = @"Nothing to play", }, - artist = new OsuSpriteText + artist = new ScrollingTextContainer { Origin = Anchor.TopCentre, Anchor = Anchor.TopCentre, @@ -470,5 +471,98 @@ namespace osu.Game.Overlays base.OnHoverLost(e); } } + + private partial class ScrollingTextContainer : CompositeDrawable + { + private const float initial_move_delay = 1000; + private const float pixels_per_second = 50; + + private LocalisableString text; + private OsuSpriteText mainSpriteText = null!; + private OsuSpriteText fillerSpriteText = null!; + + public LocalisableString Text + { + get => text; + set + { + text = value; + Schedule(updateText); + } + } + + public FontUsage Font + { + set => + Schedule(() => + { + mainSpriteText.Font = value; + fillerSpriteText.Font = value; + + updateText(); + }); + } + + public ScrollingTextContainer() + { + AutoSizeAxes = Axes.Both; + } + + [BackgroundDependencyLoader] + private void load() + { + InternalChild = new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Children = new[] + { + mainSpriteText = new OsuSpriteText { Padding = new MarginPadding { Horizontal = margin } }, + fillerSpriteText = new OsuSpriteText { Padding = new MarginPadding { Horizontal = margin }, Alpha = 0 }, + } + }; + } + + private void updateText() + { + mainSpriteText.Text = text; + fillerSpriteText.Alpha = 0; + + ClearTransforms(); + X = 0; + + float textOverflowWidth = mainSpriteText.Width - player_width; + + if (textOverflowWidth > 0) + { + fillerSpriteText.Alpha = 1; + fillerSpriteText.Text = text; + + float initialX; + float targetX; + + if (Anchor.HasFlagFast(Anchor.x0)) + { + initialX = 0; + targetX = -mainSpriteText.Width; + } + else if (Anchor.HasFlagFast(Anchor.x1)) + { + initialX = (textOverflowWidth + mainSpriteText.Width) / 2; + targetX = (textOverflowWidth - mainSpriteText.Width) / 2; + } + else // Anchor.x2 + { + initialX = textOverflowWidth + mainSpriteText.Width; + targetX = textOverflowWidth; + } + + this.MoveToX(initialX) + .Delay(initial_move_delay) + .MoveToX(targetX, mainSpriteText.Width * 1000 / pixels_per_second) + .Loop(); + } + } + } } } From b262497083ccef7d72b9c838d37bf13706ee4faf Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 28 Apr 2024 19:07:39 +0800 Subject: [PATCH 0631/2493] Check realm file can be written to before attempting further initialisation Rather than creating a "corrupt" realm file in such cases, the game will now refuse to start. This behaviour is usually what we want. In most cases a second click on the game will start it successfully (the previous instance's file handles are still doing stuff, or windows defender is being silly). Closes https://github.com/ppy/osu/issues/28018. --- osu.Game/Database/RealmAccess.cs | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/osu.Game/Database/RealmAccess.cs b/osu.Game/Database/RealmAccess.cs index 167d170c81..4bc7ec4979 100644 --- a/osu.Game/Database/RealmAccess.cs +++ b/osu.Game/Database/RealmAccess.cs @@ -300,6 +300,21 @@ namespace osu.Game.Database private Realm prepareFirstRealmAccess() { + // Before attempting to initialise realm, make sure the realm file isn't locked and has correct permissions. + // + // This is to avoid failures like: + // Realms.Exceptions.RealmException: SetEndOfFile() failed: unknown error (1224) + // + // which can occur due to file handles still being open by a previous instance. + if (storage.Exists(Filename)) + { + // If this fails we allow it to block game startup. + // It's better than any alternative we can offer. + using (var _ = storage.GetStream(Filename, FileAccess.ReadWrite)) + { + } + } + string newerVersionFilename = $"{Filename.Replace(realm_extension, string.Empty)}_newer_version{realm_extension}"; // Attempt to recover a newer database version if available. @@ -321,7 +336,7 @@ namespace osu.Game.Database { Logger.Error(e, "Your local database is too new to work with this version of osu!. Please close osu! and install the latest release to recover your data."); - // If a newer version database already exists, don't backup again. We can presume that the first backup is the one we care about. + // If a newer version database already exists, don't create another backup. We can presume that the first backup is the one we care about. if (!storage.Exists(newerVersionFilename)) createBackup(newerVersionFilename); } From a4bc5a8fc9059e2f13d736965e8cc52eff95f7ea Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 29 Apr 2024 10:35:37 +0800 Subject: [PATCH 0632/2493] Use helper method for backup retry attempts --- osu.Game/Database/RealmAccess.cs | 32 +++++++++----------------------- 1 file changed, 9 insertions(+), 23 deletions(-) diff --git a/osu.Game/Database/RealmAccess.cs b/osu.Game/Database/RealmAccess.cs index 4bc7ec4979..b5faa898e7 100644 --- a/osu.Game/Database/RealmAccess.cs +++ b/osu.Game/Database/RealmAccess.cs @@ -35,6 +35,7 @@ using osu.Game.Rulesets.Mods; using osu.Game.Scoring; using osu.Game.Scoring.Legacy; using osu.Game.Skinning; +using osu.Game.Utils; using osuTK.Input; using Realms; using Realms.Exceptions; @@ -1157,33 +1158,18 @@ namespace osu.Game.Database { Logger.Log($"Creating full realm database backup at {backupFilename}", LoggingTarget.Database); - int attempts = 10; - - while (true) + FileUtils.AttemptOperation(() => { - try + using (var source = storage.GetStream(Filename, mode: FileMode.Open)) { - using (var source = storage.GetStream(Filename, mode: FileMode.Open)) - { - // source may not exist. - if (source == null) - return; + // source may not exist. + if (source == null) + return; - using (var destination = storage.GetStream(backupFilename, FileAccess.Write, FileMode.CreateNew)) - source.CopyTo(destination); - } - - return; + using (var destination = storage.GetStream(backupFilename, FileAccess.Write, FileMode.CreateNew)) + source.CopyTo(destination); } - catch (IOException) - { - if (attempts-- <= 0) - throw; - - // file may be locked during use. - Thread.Sleep(500); - } - } + }, 20); } /// From 4c4621eb58c8bd23744bb03970c07cfdf841ee0a Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Sun, 28 Apr 2024 23:12:48 -0700 Subject: [PATCH 0633/2493] Fix compile errors --- osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs | 7 ------- 1 file changed, 7 deletions(-) diff --git a/osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs b/osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs index 4e20b7f8f5..d71ce2bdf9 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs @@ -346,7 +346,6 @@ namespace osu.Game.Online.Leaderboards RelativeSizeAxes = Axes.Both, Anchor = Anchor.TopRight, Origin = Anchor.TopRight, - Masking = true, SpawnRatio = 2, Velocity = 0.7f, Colour = ColourInfo.GradientHorizontal(backgroundColour.Opacity(0), OsuColour.ForRank(score.Rank).Darken(0.2f)), @@ -521,15 +520,9 @@ namespace osu.Game.Online.Leaderboards avatar.FadeOut(transition_duration, Easing.OutQuint).MoveToX(-avatar.DrawWidth, transition_duration, Easing.OutQuint); if (centreContent.DrawWidth >= username_min_width) - { usernameAndFlagContainer.FadeIn(transition_duration, Easing.OutQuint).MoveToX(0, transition_duration, Easing.OutQuint); - innerAvatar.ShowUsernameTooltip = false; - } else - { usernameAndFlagContainer.FadeOut(transition_duration, Easing.OutQuint).MoveToX(usernameAndFlagContainer.DrawWidth, transition_duration, Easing.OutQuint); - innerAvatar.ShowUsernameTooltip = true; - } if (centreContent.DrawWidth >= height + statisticsContainer.DrawWidth + username_min_width) statisticsContainer.FadeIn(transition_duration, Easing.OutQuint).MoveToX(0, transition_duration, Easing.OutQuint); From 32df6991ad238df5a82aaa6a0e4d27dcd6b028bd Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Sun, 28 Apr 2024 23:12:55 -0700 Subject: [PATCH 0634/2493] Remove dark border and update cover gradient --- .../Online/Leaderboards/LeaderboardScoreV2.cs | 195 ++++++++---------- 1 file changed, 87 insertions(+), 108 deletions(-) diff --git a/osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs b/osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs index d71ce2bdf9..80bf251631 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs @@ -66,7 +66,6 @@ namespace osu.Game.Online.Leaderboards private Colour4 foregroundColour; private Colour4 backgroundColour; - private Colour4 shadowColour; private ColourInfo totalScoreBackgroundGradient; private static readonly Vector2 shear = new Vector2(0.15f, 0); @@ -127,7 +126,6 @@ namespace osu.Game.Online.Leaderboards foregroundColour = isPersonalBest ? colourProvider.Background1 : colourProvider.Background5; backgroundColour = isPersonalBest ? colourProvider.Background2 : colourProvider.Background4; - shadowColour = isPersonalBest ? colourProvider.Background3 : colourProvider.Background6; totalScoreBackgroundGradient = ColourInfo.GradientHorizontal(backgroundColour.Opacity(0), backgroundColour); statisticsLabels = GetStatistics(score).Select(s => new ScoreComponentLabel(s, score) @@ -184,129 +182,110 @@ namespace osu.Game.Online.Leaderboards RelativeSizeAxes = Axes.Both, Children = new Drawable[] { - new Box + foreground = new Box { RelativeSizeAxes = Axes.Both, - Colour = shadowColour, + Colour = foregroundColour }, - new Container + new UserCoverBackground { RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding { Right = 5 }, - Child = new Container + User = score.User, + Shear = -shear, + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Colour = ColourInfo.GradientHorizontal(Colour4.White.Opacity(0.5f), Colour4.FromHex(@"222A27").Opacity(1)), + }, + new GridContainer + { + RelativeSizeAxes = Axes.Both, + ColumnDimensions = new[] { - Masking = true, - CornerRadius = corner_radius, - RelativeSizeAxes = Axes.Both, - Children = new Drawable[] + new Dimension(GridSizeMode.AutoSize), + new Dimension(), + new Dimension(GridSizeMode.AutoSize), + }, + Content = new[] + { + new Drawable[] { - foreground = new Box + new Container { - RelativeSizeAxes = Axes.Both, - Colour = foregroundColour - }, - new UserCoverBackground - { - RelativeSizeAxes = Axes.Both, - User = score.User, - Shear = -shear, - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - Colour = ColourInfo.GradientHorizontal(Colour4.White.Opacity(0.5f), Colour4.White.Opacity(0)), - }, - new GridContainer - { - RelativeSizeAxes = Axes.Both, - ColumnDimensions = new[] - { - new Dimension(GridSizeMode.AutoSize), - new Dimension(), - new Dimension(GridSizeMode.AutoSize), - }, - Content = new[] - { - new Drawable[] + AutoSizeAxes = Axes.Both, + Child = avatar = new MaskedWrapper( + innerAvatar = new ClickableAvatar(user) { - new Container + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Scale = new Vector2(1.1f), + Shear = -shear, + RelativeSizeAxes = Axes.Both, + }) + { + RelativeSizeAxes = Axes.None, + Size = new Vector2(height) + }, + }, + usernameAndFlagContainer = new FillFlowContainer + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Padding = new MarginPadding { Horizontal = corner_radius }, + Children = new Drawable[] + { + flagBadgeAndDateContainer = new FillFlowContainer + { + Shear = -shear, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(5), + AutoSizeAxes = Axes.Both, + Masking = true, + Children = new Drawable[] { - AutoSizeAxes = Axes.Both, - Child = avatar = new MaskedWrapper( - innerAvatar = new ClickableAvatar(user) - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Scale = new Vector2(1.1f), - Shear = -shear, - RelativeSizeAxes = Axes.Both, - }) + new UpdateableFlag(user.CountryCode) { - RelativeSizeAxes = Axes.None, - Size = new Vector2(height) + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Size = new Vector2(24, 16), }, - }, - usernameAndFlagContainer = new FillFlowContainer - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Direction = FillDirection.Vertical, - Padding = new MarginPadding { Horizontal = corner_radius }, - Children = new Drawable[] + new DateLabel(score.Date) { - flagBadgeAndDateContainer = new FillFlowContainer - { - Shear = -shear, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(5), - AutoSizeAxes = Axes.Both, - Masking = true, - Children = new Drawable[] - { - new UpdateableFlag(user.CountryCode) - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Size = new Vector2(24, 16), - }, - new DateLabel(score.Date) - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - UseFullGlyphHeight = false, - } - } - }, - nameLabel = new TruncatingSpriteText - { - RelativeSizeAxes = Axes.X, - Shear = -shear, - Text = user.Username, - Font = OsuFont.GetFont(size: 24, weight: FontWeight.SemiBold) - } - } - }, - new Container - { - AutoSizeAxes = Axes.Both, - Anchor = Anchor.CentreRight, - Origin = Anchor.CentreRight, - Child = statisticsContainer = new FillFlowContainer - { - Name = @"Statistics container", - Padding = new MarginPadding { Right = 40 }, - Spacing = new Vector2(25), - Shear = -shear, - Anchor = Anchor.CentreRight, - Origin = Anchor.CentreRight, - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Children = statisticsLabels, - Alpha = 0, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + UseFullGlyphHeight = false, } } + }, + nameLabel = new TruncatingSpriteText + { + RelativeSizeAxes = Axes.X, + Shear = -shear, + Text = user.Username, + Font = OsuFont.GetFont(size: 24, weight: FontWeight.SemiBold) } } + }, + new Container + { + AutoSizeAxes = Axes.Both, + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + Child = statisticsContainer = new FillFlowContainer + { + Name = @"Statistics container", + Padding = new MarginPadding { Right = 40 }, + Spacing = new Vector2(25), + Shear = -shear, + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Children = statisticsLabels, + Alpha = 0, + } } } }, From 1c1ee22aa70cfa3e92786fba7a208f2af08a2e69 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 29 Apr 2024 10:36:49 +0800 Subject: [PATCH 0635/2493] Add retry attempts to hopefully fix windows tests runs --- osu.Game/Database/RealmAccess.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/osu.Game/Database/RealmAccess.cs b/osu.Game/Database/RealmAccess.cs index b5faa898e7..31ae22178f 100644 --- a/osu.Game/Database/RealmAccess.cs +++ b/osu.Game/Database/RealmAccess.cs @@ -311,9 +311,12 @@ namespace osu.Game.Database { // If this fails we allow it to block game startup. // It's better than any alternative we can offer. - using (var _ = storage.GetStream(Filename, FileAccess.ReadWrite)) + FileUtils.AttemptOperation(() => { - } + using (var _ = storage.GetStream(Filename, FileAccess.ReadWrite)) + { + } + }); } string newerVersionFilename = $"{Filename.Replace(realm_extension, string.Empty)}_newer_version{realm_extension}"; From a3d239c11aa85215b3565171de2b580b8b1c8411 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 29 Apr 2024 18:48:07 +0800 Subject: [PATCH 0636/2493] Remove unused method --- osu.Game/Database/RealmArchiveModelImporter.cs | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/osu.Game/Database/RealmArchiveModelImporter.cs b/osu.Game/Database/RealmArchiveModelImporter.cs index bc4954c6ea..0014e246dc 100644 --- a/osu.Game/Database/RealmArchiveModelImporter.cs +++ b/osu.Game/Database/RealmArchiveModelImporter.cs @@ -449,16 +449,6 @@ namespace osu.Game.Database return reader.Name.ComputeSHA2Hash(); } - /// - /// Create all required s for the provided archive, adding them to the global file store. - /// - private List createFileInfos(ArchiveReader reader, RealmFileStore files, Realm realm) - { - var fileInfos = new List(); - - return fileInfos; - } - private IEnumerable<(string original, string shortened)> getShortenedFilenames(ArchiveReader reader) { string prefix = reader.Filenames.GetCommonPrefix(); From 45c2327509911032984c863acb912406c44075fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 29 Apr 2024 13:00:22 +0200 Subject: [PATCH 0637/2493] Apply adjustments after framework-side `FriendlyGameName` changes --- osu.Desktop/Program.cs | 6 +++++- osu.Game/OsuGameBase.cs | 12 +++++++----- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/osu.Desktop/Program.cs b/osu.Desktop/Program.cs index 29b05a402f..d8364fc6e6 100644 --- a/osu.Desktop/Program.cs +++ b/osu.Desktop/Program.cs @@ -107,7 +107,11 @@ namespace osu.Desktop } } - using (DesktopGameHost host = Host.GetSuitableDesktopHost(gameName, new HostOptions { IPCPort = !tournamentClient ? OsuGame.IPC_PORT : null })) + using (DesktopGameHost host = Host.GetSuitableDesktopHost(gameName, new HostOptions + { + IPCPort = !tournamentClient ? OsuGame.IPC_PORT : null, + FriendlyGameName = OsuGameBase.GAME_NAME, + })) { if (!host.IsPrimaryInstance) { diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 0122afb239..5533ee8337 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -75,6 +75,12 @@ namespace osu.Game { public static readonly string[] VIDEO_EXTENSIONS = { ".mp4", ".mov", ".avi", ".flv", ".mpg", ".wmv", ".m4v" }; +#if DEBUG + public const string GAME_NAME = "osu! (development)"; +#else + public const string GAME_NAME = "osu!"; +#endif + public const string OSU_PROTOCOL = "osu://"; public const string CLIENT_STREAM_NAME = @"lazer"; @@ -241,11 +247,7 @@ namespace osu.Game public OsuGameBase() { - Name = @"osu!"; - -#if DEBUG - Name += " (development)"; -#endif + Name = GAME_NAME; allowableExceptions = UnhandledExceptionsBeforeCrash; } From 9fc56f1cc7e916a7c41f7073be25b47642860df3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 29 Apr 2024 13:07:36 +0200 Subject: [PATCH 0638/2493] Apply adjustments after migration of android to SDL3 --- osu.Android/AndroidJoystickSettings.cs | 76 -------------------- osu.Android/AndroidMouseSettings.cs | 97 -------------------------- osu.Android/OsuGameAndroid.cs | 22 ------ 3 files changed, 195 deletions(-) delete mode 100644 osu.Android/AndroidJoystickSettings.cs delete mode 100644 osu.Android/AndroidMouseSettings.cs diff --git a/osu.Android/AndroidJoystickSettings.cs b/osu.Android/AndroidJoystickSettings.cs deleted file mode 100644 index bf69461f0d..0000000000 --- a/osu.Android/AndroidJoystickSettings.cs +++ /dev/null @@ -1,76 +0,0 @@ -// 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.Android.Input; -using osu.Framework.Bindables; -using osu.Framework.Graphics; -using osu.Framework.Localisation; -using osu.Game.Localisation; -using osu.Game.Overlays.Settings; - -namespace osu.Android -{ - public partial class AndroidJoystickSettings : SettingsSubsection - { - protected override LocalisableString Header => JoystickSettingsStrings.JoystickGamepad; - - private readonly AndroidJoystickHandler joystickHandler; - - private readonly Bindable enabled = new BindableBool(true); - - private SettingsSlider deadzoneSlider = null!; - - private Bindable handlerDeadzone = null!; - - private Bindable localDeadzone = null!; - - public AndroidJoystickSettings(AndroidJoystickHandler joystickHandler) - { - this.joystickHandler = joystickHandler; - } - - [BackgroundDependencyLoader] - private void load() - { - // use local bindable to avoid changing enabled state of game host's bindable. - handlerDeadzone = joystickHandler.DeadzoneThreshold.GetBoundCopy(); - localDeadzone = handlerDeadzone.GetUnboundCopy(); - - Children = new Drawable[] - { - new SettingsCheckbox - { - LabelText = CommonStrings.Enabled, - Current = enabled - }, - deadzoneSlider = new SettingsSlider - { - LabelText = JoystickSettingsStrings.DeadzoneThreshold, - KeyboardStep = 0.01f, - DisplayAsPercentage = true, - Current = localDeadzone, - }, - }; - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - enabled.BindTo(joystickHandler.Enabled); - enabled.BindValueChanged(e => deadzoneSlider.Current.Disabled = !e.NewValue, true); - - handlerDeadzone.BindValueChanged(val => - { - bool disabled = localDeadzone.Disabled; - - localDeadzone.Disabled = false; - localDeadzone.Value = val.NewValue; - localDeadzone.Disabled = disabled; - }, true); - - localDeadzone.BindValueChanged(val => handlerDeadzone.Value = val.NewValue); - } - } -} diff --git a/osu.Android/AndroidMouseSettings.cs b/osu.Android/AndroidMouseSettings.cs deleted file mode 100644 index fd01b11164..0000000000 --- a/osu.Android/AndroidMouseSettings.cs +++ /dev/null @@ -1,97 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using Android.OS; -using osu.Framework.Allocation; -using osu.Framework.Android.Input; -using osu.Framework.Bindables; -using osu.Framework.Graphics; -using osu.Framework.Localisation; -using osu.Game.Configuration; -using osu.Game.Localisation; -using osu.Game.Overlays.Settings; -using osu.Game.Overlays.Settings.Sections.Input; - -namespace osu.Android -{ - public partial class AndroidMouseSettings : SettingsSubsection - { - private readonly AndroidMouseHandler mouseHandler; - - protected override LocalisableString Header => MouseSettingsStrings.Mouse; - - private Bindable handlerSensitivity = null!; - - private Bindable localSensitivity = null!; - - private Bindable relativeMode = null!; - - public AndroidMouseSettings(AndroidMouseHandler mouseHandler) - { - this.mouseHandler = mouseHandler; - } - - [BackgroundDependencyLoader] - private void load(OsuConfigManager osuConfig) - { - // use local bindable to avoid changing enabled state of game host's bindable. - handlerSensitivity = mouseHandler.Sensitivity.GetBoundCopy(); - localSensitivity = handlerSensitivity.GetUnboundCopy(); - - relativeMode = mouseHandler.UseRelativeMode.GetBoundCopy(); - - // High precision/pointer capture is only available on Android 8.0 and up - if (Build.VERSION.SdkInt >= BuildVersionCodes.O) - { - AddRange(new Drawable[] - { - new SettingsCheckbox - { - LabelText = MouseSettingsStrings.HighPrecisionMouse, - TooltipText = MouseSettingsStrings.HighPrecisionMouseTooltip, - Current = relativeMode, - Keywords = new[] { @"raw", @"input", @"relative", @"cursor", @"captured", @"pointer" }, - }, - new MouseSettings.SensitivitySetting - { - LabelText = MouseSettingsStrings.CursorSensitivity, - Current = localSensitivity, - }, - }); - } - - AddRange(new Drawable[] - { - new SettingsCheckbox - { - LabelText = MouseSettingsStrings.DisableMouseWheelVolumeAdjust, - TooltipText = MouseSettingsStrings.DisableMouseWheelVolumeAdjustTooltip, - Current = osuConfig.GetBindable(OsuSetting.MouseDisableWheel), - }, - new SettingsCheckbox - { - LabelText = MouseSettingsStrings.DisableClicksDuringGameplay, - Current = osuConfig.GetBindable(OsuSetting.MouseDisableButtons), - }, - }); - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - relativeMode.BindValueChanged(relative => localSensitivity.Disabled = !relative.NewValue, true); - - handlerSensitivity.BindValueChanged(val => - { - bool disabled = localSensitivity.Disabled; - - localSensitivity.Disabled = false; - localSensitivity.Value = val.NewValue; - localSensitivity.Disabled = disabled; - }, true); - - localSensitivity.BindValueChanged(val => handlerSensitivity.Value = val.NewValue); - } - } -} diff --git a/osu.Android/OsuGameAndroid.cs b/osu.Android/OsuGameAndroid.cs index 52cfb67f42..a235913ef3 100644 --- a/osu.Android/OsuGameAndroid.cs +++ b/osu.Android/OsuGameAndroid.cs @@ -5,13 +5,9 @@ using System; using Android.App; using Microsoft.Maui.Devices; using osu.Framework.Allocation; -using osu.Framework.Android.Input; using osu.Framework.Extensions.ObjectExtensions; -using osu.Framework.Input.Handlers; using osu.Framework.Platform; using osu.Game; -using osu.Game.Overlays.Settings; -using osu.Game.Overlays.Settings.Sections.Input; using osu.Game.Updater; using osu.Game.Utils; @@ -88,24 +84,6 @@ namespace osu.Android protected override BatteryInfo CreateBatteryInfo() => new AndroidBatteryInfo(); - public override SettingsSubsection CreateSettingsSubsectionFor(InputHandler handler) - { - switch (handler) - { - case AndroidMouseHandler mh: - return new AndroidMouseSettings(mh); - - case AndroidJoystickHandler jh: - return new AndroidJoystickSettings(jh); - - case AndroidTouchHandler th: - return new TouchSettings(th); - - default: - return base.CreateSettingsSubsectionFor(handler); - } - } - private class AndroidBatteryInfo : BatteryInfo { public override double? ChargeLevel => Battery.ChargeLevel; From fa3aeca09d8656ec76ae9e0401047b4f5f15646a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 29 Apr 2024 14:06:02 +0200 Subject: [PATCH 0639/2493] Add failing test for skins not saving on change --- .../TestSceneSkinEditorNavigation.cs | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneSkinEditorNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneSkinEditorNavigation.cs index 9c180d43da..38fb2846aa 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneSkinEditorNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneSkinEditorNavigation.cs @@ -3,6 +3,7 @@ #nullable disable +using System; using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; @@ -321,6 +322,30 @@ namespace osu.Game.Tests.Visual.Navigation AddUntilStep("nested input disabled", () => ((Player)Game.ScreenStack.CurrentScreen).ChildrenOfType().All(manager => !manager.UseParentInput)); } + [Test] + public void TestSkinSavesOnChange() + { + advanceToSongSelect(); + openSkinEditor(); + + Guid editedSkinId = Guid.Empty; + AddStep("save skin id", () => editedSkinId = Game.Dependencies.Get().CurrentSkinInfo.Value.ID); + AddStep("add skinnable component", () => + { + skinEditor.ChildrenOfType().First().TriggerClick(); + }); + + AddStep("change to triangles skin", () => Game.Dependencies.Get().SetSkinFromConfiguration(SkinInfo.TRIANGLES_SKIN.ToString())); + AddUntilStep("components loaded", () => Game.ChildrenOfType().All(c => c.ComponentsLoaded)); + // sort of implicitly relies on song select not being skinnable. + // TODO: revisit if the above ever changes + AddUntilStep("skin changed", () => !skinEditor.ChildrenOfType().Any()); + + AddStep("change back to modified skin", () => Game.Dependencies.Get().SetSkinFromConfiguration(editedSkinId.ToString())); + AddUntilStep("components loaded", () => Game.ChildrenOfType().All(c => c.ComponentsLoaded)); + AddUntilStep("changes saved", () => skinEditor.ChildrenOfType().Any()); + } + private void advanceToSongSelect() { PushAndConfirm(() => songSelect = new TestPlaySongSelect()); From f78abf801c2f2d9af66bc6baa71c0a0c6ec52406 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 29 Apr 2024 14:06:23 +0200 Subject: [PATCH 0640/2493] Autosave edited skin on change --- osu.Game/Overlays/SkinEditor/SkinEditor.cs | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/osu.Game/Overlays/SkinEditor/SkinEditor.cs b/osu.Game/Overlays/SkinEditor/SkinEditor.cs index bc929177d1..690c6b35e3 100644 --- a/osu.Game/Overlays/SkinEditor/SkinEditor.cs +++ b/osu.Game/Overlays/SkinEditor/SkinEditor.cs @@ -255,8 +255,11 @@ namespace osu.Game.Overlays.SkinEditor // schedule ensures this only happens when the skin editor is visible. // also avoid some weird endless recursion / bindable feedback loop (something to do with tracking skins across three different bindable types). // probably something which will be factored out in a future database refactor so not too concerning for now. - currentSkin.BindValueChanged(_ => + currentSkin.BindValueChanged(val => { + if (val.OldValue != null && hasBegunMutating) + save(val.OldValue); + hasBegunMutating = false; Scheduler.AddOnce(skinChanged); }, true); @@ -537,7 +540,9 @@ namespace osu.Game.Overlays.SkinEditor protected void Redo() => changeHandler?.RestoreState(1); - public void Save(bool userTriggered = true) + public void Save(bool userTriggered = true) => save(currentSkin.Value); + + private void save(Skin skin, bool userTriggered = true) { if (!hasBegunMutating) return; @@ -551,11 +556,11 @@ namespace osu.Game.Overlays.SkinEditor return; foreach (var t in targetContainers) - currentSkin.Value.UpdateDrawableTarget(t); + skin.UpdateDrawableTarget(t); // In the case the save was user triggered, always show the save message to make them feel confident. - if (skins.Save(skins.CurrentSkin.Value) || userTriggered) - onScreenDisplay?.Display(new SkinEditorToast(ToastStrings.SkinSaved, currentSkin.Value.SkinInfo.ToString() ?? "Unknown")); + if (skins.Save(skin) || userTriggered) + onScreenDisplay?.Display(new SkinEditorToast(ToastStrings.SkinSaved, skin.SkinInfo.ToString() ?? "Unknown")); } protected override bool OnHover(HoverEvent e) => true; From 85c085e5879d59fea0533c78261bf8b111d878c3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 29 Apr 2024 23:22:25 +0800 Subject: [PATCH 0641/2493] Reduce startup volume Constant complaints about startup volume mean we should reduce it further and let users adjust as they see fit. --- osu.Game/OsuGame.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 98533a5c82..7c89314014 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -841,7 +841,10 @@ namespace osu.Game { // General expectation that osu! starts in fullscreen by default (also gives the most predictable performance). // However, macOS is bound to have issues when using exclusive fullscreen as it takes full control away from OS, therefore borderless is default there. - { FrameworkSetting.WindowMode, RuntimeInfo.OS == RuntimeInfo.Platform.macOS ? WindowMode.Borderless : WindowMode.Fullscreen } + { FrameworkSetting.WindowMode, RuntimeInfo.OS == RuntimeInfo.Platform.macOS ? WindowMode.Borderless : WindowMode.Fullscreen }, + { FrameworkSetting.VolumeUniversal, 0.6 }, + { FrameworkSetting.VolumeMusic, 0.6 }, + { FrameworkSetting.VolumeEffect, 0.6 }, }; } From fd3f4a9e7b30560c6270845af4a77ae1ed8073ec Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 29 Apr 2024 22:26:44 +0800 Subject: [PATCH 0642/2493] Preserve storyboard events when saving a beatmap in the editor Until we have full encoding support for storyboards, this stop-gap measure ensures that storyboards don't just disappear from existence. --- .../Beatmaps/Formats/LegacyBeatmapEncoderTest.cs | 15 +++++++++++++++ osu.Game/Beatmaps/Beatmap.cs | 2 ++ osu.Game/Beatmaps/BeatmapConverter.cs | 1 + osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs | 14 ++++++++++++++ osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs | 3 +++ osu.Game/Beatmaps/IBeatmap.cs | 6 ++++++ .../Rulesets/Difficulty/DifficultyCalculator.cs | 2 ++ osu.Game/Screens/Edit/EditorBeatmap.cs | 2 ++ osu.Game/Tests/Beatmaps/TestBeatmap.cs | 1 + 9 files changed, 46 insertions(+) diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs index e847b61fbe..ef30f020ce 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs @@ -37,6 +37,21 @@ namespace osu.Game.Tests.Beatmaps.Formats private static IEnumerable allBeatmaps = beatmaps_resource_store.GetAvailableResources().Where(res => res.EndsWith(".osu", StringComparison.Ordinal)); + [Test] + public void TestUnsupportedStoryboardEvents() + { + const string name = "Resources/storyboard_only_video.osu"; + + var decoded = decodeFromLegacy(beatmaps_resource_store.GetStream(name), name); + var decodedAfterEncode = decodeFromLegacy(encodeToLegacy(decoded), name); + + Assert.That(decoded.beatmap.UnhandledEventLines.Count, Is.EqualTo(1)); + Assert.That(decoded.beatmap.UnhandledEventLines.Single(), Is.EqualTo("Video,0,\"video.avi\"")); + + Assert.That(decodedAfterEncode.beatmap.UnhandledEventLines.Count, Is.EqualTo(1)); + Assert.That(decodedAfterEncode.beatmap.UnhandledEventLines.Single(), Is.EqualTo("Video,0,\"video.avi\"")); + } + [TestCaseSource(nameof(allBeatmaps))] public void TestEncodeDecodeStability(string name) { diff --git a/osu.Game/Beatmaps/Beatmap.cs b/osu.Game/Beatmaps/Beatmap.cs index 6db9febf36..ae77e4adcf 100644 --- a/osu.Game/Beatmaps/Beatmap.cs +++ b/osu.Game/Beatmaps/Beatmap.cs @@ -63,6 +63,8 @@ namespace osu.Game.Beatmaps public List Breaks { get; set; } = new List(); + public List UnhandledEventLines { get; set; } = new List(); + [JsonIgnore] public double TotalBreakTime => Breaks.Sum(b => b.Duration); diff --git a/osu.Game/Beatmaps/BeatmapConverter.cs b/osu.Game/Beatmaps/BeatmapConverter.cs index c7c244bf0e..b68c80d4b3 100644 --- a/osu.Game/Beatmaps/BeatmapConverter.cs +++ b/osu.Game/Beatmaps/BeatmapConverter.cs @@ -66,6 +66,7 @@ namespace osu.Game.Beatmaps beatmap.ControlPointInfo = original.ControlPointInfo; beatmap.HitObjects = convertHitObjects(original.HitObjects, original, cancellationToken).OrderBy(s => s.StartTime).ToList(); beatmap.Breaks = original.Breaks; + beatmap.UnhandledEventLines = original.UnhandledEventLines; return beatmap; } diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs index 386dada328..7407c3590f 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs @@ -420,6 +420,10 @@ namespace osu.Game.Beatmaps.Formats if (!Enum.TryParse(split[0], out LegacyEventType type)) throw new InvalidDataException($@"Unknown event type: {split[0]}"); + // Until we have full storyboard encoder coverage, let's track any lines which aren't handled + // and store them to a temporary location such that they aren't lost on editor save / export. + bool lineSupportedByEncoder = false; + switch (type) { case LegacyEventType.Sprite: @@ -427,7 +431,11 @@ namespace osu.Game.Beatmaps.Formats // In some older beatmaps, it is not present and replaced by a storyboard-level background instead. // Allow the first sprite (by file order) to act as the background in such cases. if (string.IsNullOrEmpty(beatmap.BeatmapInfo.Metadata.BackgroundFile)) + { beatmap.BeatmapInfo.Metadata.BackgroundFile = CleanFilename(split[3]); + lineSupportedByEncoder = true; + } + break; case LegacyEventType.Video: @@ -439,12 +447,14 @@ namespace osu.Game.Beatmaps.Formats if (!OsuGameBase.VIDEO_EXTENSIONS.Contains(Path.GetExtension(filename).ToLowerInvariant())) { beatmap.BeatmapInfo.Metadata.BackgroundFile = filename; + lineSupportedByEncoder = true; } break; case LegacyEventType.Background: beatmap.BeatmapInfo.Metadata.BackgroundFile = CleanFilename(split[2]); + lineSupportedByEncoder = true; break; case LegacyEventType.Break: @@ -452,8 +462,12 @@ namespace osu.Game.Beatmaps.Formats double end = Math.Max(start, getOffsetTime(Parsing.ParseDouble(split[2]))); beatmap.Breaks.Add(new BreakPeriod(start, end)); + lineSupportedByEncoder = true; break; } + + if (!lineSupportedByEncoder) + beatmap.UnhandledEventLines.Add(line); } private void handleTimingPoint(string line) diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs index 290d29090a..186b565c39 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs @@ -156,6 +156,9 @@ namespace osu.Game.Beatmaps.Formats foreach (var b in beatmap.Breaks) writer.WriteLine(FormattableString.Invariant($"{(int)LegacyEventType.Break},{b.StartTime},{b.EndTime}")); + + foreach (string l in beatmap.UnhandledEventLines) + writer.WriteLine(l); } private void handleControlPoints(TextWriter writer) diff --git a/osu.Game/Beatmaps/IBeatmap.cs b/osu.Game/Beatmaps/IBeatmap.cs index 6fe494ca0f..5cc38e5b84 100644 --- a/osu.Game/Beatmaps/IBeatmap.cs +++ b/osu.Game/Beatmaps/IBeatmap.cs @@ -42,6 +42,12 @@ namespace osu.Game.Beatmaps /// List Breaks { get; } + /// + /// All lines from the [Events] section which aren't handled in the encoding process yet. + /// These lines shoule be written out to the beatmap file on save or export. + /// + List UnhandledEventLines { get; } + /// /// Total amount of break time in the beatmap. /// diff --git a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs index 1599dff8d9..d37cfc28b9 100644 --- a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs +++ b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs @@ -330,6 +330,8 @@ namespace osu.Game.Rulesets.Difficulty } public List Breaks => baseBeatmap.Breaks; + public List UnhandledEventLines => baseBeatmap.UnhandledEventLines; + public double TotalBreakTime => baseBeatmap.TotalBreakTime; public IEnumerable GetStatistics() => baseBeatmap.GetStatistics(); public double GetMostCommonBeatLength() => baseBeatmap.GetMostCommonBeatLength(); diff --git a/osu.Game/Screens/Edit/EditorBeatmap.cs b/osu.Game/Screens/Edit/EditorBeatmap.cs index dc1fda13f4..7a3ea474fb 100644 --- a/osu.Game/Screens/Edit/EditorBeatmap.cs +++ b/osu.Game/Screens/Edit/EditorBeatmap.cs @@ -174,6 +174,8 @@ namespace osu.Game.Screens.Edit public List Breaks => PlayableBeatmap.Breaks; + public List UnhandledEventLines => PlayableBeatmap.UnhandledEventLines; + public double TotalBreakTime => PlayableBeatmap.TotalBreakTime; public IReadOnlyList HitObjects => PlayableBeatmap.HitObjects; diff --git a/osu.Game/Tests/Beatmaps/TestBeatmap.cs b/osu.Game/Tests/Beatmaps/TestBeatmap.cs index ff670e1232..de7bcfcfaa 100644 --- a/osu.Game/Tests/Beatmaps/TestBeatmap.cs +++ b/osu.Game/Tests/Beatmaps/TestBeatmap.cs @@ -27,6 +27,7 @@ namespace osu.Game.Tests.Beatmaps BeatmapInfo = baseBeatmap.BeatmapInfo; ControlPointInfo = baseBeatmap.ControlPointInfo; Breaks = baseBeatmap.Breaks; + UnhandledEventLines = baseBeatmap.UnhandledEventLines; if (withHitObjects) HitObjects = baseBeatmap.HitObjects; From 19897c4c074fbca635b9bdbee66f4fb6e368a126 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 30 Apr 2024 00:50:40 +0800 Subject: [PATCH 0643/2493] Add testing for actual presence of video after encode-decode --- .../Beatmaps/Formats/LegacyBeatmapEncoderTest.cs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs index ef30f020ce..b931896898 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs @@ -25,6 +25,7 @@ using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Taiko; using osu.Game.Skinning; +using osu.Game.Storyboards; using osu.Game.Tests.Resources; using osuTK; @@ -43,13 +44,14 @@ namespace osu.Game.Tests.Beatmaps.Formats const string name = "Resources/storyboard_only_video.osu"; var decoded = decodeFromLegacy(beatmaps_resource_store.GetStream(name), name); - var decodedAfterEncode = decodeFromLegacy(encodeToLegacy(decoded), name); - Assert.That(decoded.beatmap.UnhandledEventLines.Count, Is.EqualTo(1)); Assert.That(decoded.beatmap.UnhandledEventLines.Single(), Is.EqualTo("Video,0,\"video.avi\"")); - Assert.That(decodedAfterEncode.beatmap.UnhandledEventLines.Count, Is.EqualTo(1)); - Assert.That(decodedAfterEncode.beatmap.UnhandledEventLines.Single(), Is.EqualTo("Video,0,\"video.avi\"")); + var memoryStream = encodeToLegacy(decoded); + + var storyboard = new LegacyStoryboardDecoder().Decode(new LineBufferedReader(memoryStream)); + StoryboardLayer video = storyboard.Layers.Single(l => l.Name == "Video"); + Assert.That(video.Elements.Count, Is.EqualTo(1)); } [TestCaseSource(nameof(allBeatmaps))] From b455e793dd7f3e3d8a75c57d3304e326ad75ab32 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 30 Apr 2024 01:14:58 +0800 Subject: [PATCH 0644/2493] Add test coverage of reading japanese file from stable export --- .../Resources/Archives/japanese-filename.osz | Bin 0 -> 816 bytes .../Skins/TestSceneBeatmapSkinResources.cs | 9 +++++++++ 2 files changed, 9 insertions(+) create mode 100644 osu.Game.Tests/Resources/Archives/japanese-filename.osz diff --git a/osu.Game.Tests/Resources/Archives/japanese-filename.osz b/osu.Game.Tests/Resources/Archives/japanese-filename.osz new file mode 100644 index 0000000000000000000000000000000000000000..4825c88179bb6d351f8766c4b4a17d89a50a0af5 GIT binary patch literal 816 zcmWIWW@Zs#U|`^2h!mU`q3R&=>KG#fg9S4KgE~+&v8W`oxI{r$p(L{;CsjeCAhn>N zQd1#1B{MB8Gr2UUq%u}7zqqtE$Twfgk!SDvE&mhyz6c0KtqLn{p1`k?5n%4SG|X~_ zq;qWB%&F-L3-|Av$Ji9IP_8Vz?)6l9$IN{%u3jy@@gwDM_XQ!-4SBL=Gq(H^vf-L= zV2#=f&b>E1+utqUHH)o#=}nekr+MwyH_96w(h~b(9of?+SUZ{N{WHcJVor1JMF{!y z^l7MlV4E$*^K)rOxocg=1h*|qB_gzEzU@0{_Qm~*-SyCKZ%#b!7r1tG&6n9pOx&FF zjOWU{nsUGW!EE)(YvndDstaBXusi-?MzPEKRKCI$(K)wcV{H}Px#FUubWY^N2(96i zHe2D!GA;1E@A`$Z{a>y|D@afIk(6KbB}~WceY!H+r+CGM|1_$OxQWe6pYkGc_WKup ztdcTYblDB|_o?b4qr)&hc$rPbw$9 zS+L$&g2#wlFe7?$5&$@yl&)v2%MRPC{zeJOZuv_kp3n)O!39UpCeI%+XL{y*z` zsQUWj2j9i-(+qxFe|N_7X(!)n+McOjta beatmap = importBeatmapFromArchives(@"japanese-filename.osz")); + AddAssert("sample is non-null", () => beatmap.Skin.GetSample(new SampleInfo(@"見本")) != null); + } + [Test] public void TestRetrieveOggAudio() { From 97da47f69ca90fe35dad7914b04e97c7363e7df4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 30 Apr 2024 01:27:45 +0800 Subject: [PATCH 0645/2493] Add test coverage of reading japanese filename after exporting --- .../Skins/TestSceneBeatmapSkinResources.cs | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/osu.Game.Tests/Skins/TestSceneBeatmapSkinResources.cs b/osu.Game.Tests/Skins/TestSceneBeatmapSkinResources.cs index b566e892cd..e0922c52f7 100644 --- a/osu.Game.Tests/Skins/TestSceneBeatmapSkinResources.cs +++ b/osu.Game.Tests/Skins/TestSceneBeatmapSkinResources.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.IO; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Audio.Track; @@ -12,6 +13,7 @@ using osu.Game.Beatmaps; using osu.Game.Database; using osu.Game.Tests.Resources; using osu.Game.Tests.Visual; +using MemoryStream = System.IO.MemoryStream; namespace osu.Game.Tests.Skins { @@ -25,9 +27,23 @@ namespace osu.Game.Tests.Skins public void TestRetrieveJapaneseFilename() { IWorkingBeatmap beatmap = null!; + MemoryStream outStream = null!; + // Ensure importer encoding is correct AddStep("import beatmap", () => beatmap = importBeatmapFromArchives(@"japanese-filename.osz")); AddAssert("sample is non-null", () => beatmap.Skin.GetSample(new SampleInfo(@"見本")) != null); + + // Ensure exporter encoding is correct (round trip) + AddStep("export", () => + { + outStream = new MemoryStream(); + + new LegacyBeatmapExporter(LocalStorage) + .ExportToStream((BeatmapSetInfo)beatmap.BeatmapInfo.BeatmapSet!, outStream, null); + }); + + AddStep("import beatmap again", () => beatmap = importBeatmapFromStream(outStream)); + AddAssert("sample is non-null", () => beatmap.Skin.GetSample(new SampleInfo(@"見本")) != null); } [Test] @@ -54,6 +70,12 @@ namespace osu.Game.Tests.Skins AddAssert("sample is non-null", () => beatmap.Skin.GetSample(new SampleInfo(@"spinner-osu")) != null); } + private IWorkingBeatmap importBeatmapFromStream(Stream stream) + { + var imported = beatmaps.Import(new ImportTask(stream, "filename.osz")).GetResultSafely(); + return imported.AsNonNull().PerformRead(s => beatmaps.GetWorkingBeatmap(s.Beatmaps[0])); + } + private IWorkingBeatmap importBeatmapFromArchives(string filename) { var imported = beatmaps.Import(new ImportTask(TestResources.OpenResource($@"Archives/{filename}"), filename)).GetResultSafely(); From c8f7f2215b4c9b3bcb5df8fdc46b7f906890dd2c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 29 Apr 2024 18:49:17 +0800 Subject: [PATCH 0646/2493] Force encoding to Shift-JIS for archive filenames MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit After way too much time investigating this, the encoding situation is not great right now. - Stable sets the "default code page" to be used for encoding filenames to Shift-JIS (932): https://github.com/peppy/osu-stable-reference/blob/c29ebd7fc52113013fb4ac2db230699d81e1fe2c/osu!/GameBase.cs#L3099 - Lazer does nothing (therefore using UTF-8). When importing to lazer, stable files are assumed to be UTF-8. This means that the linked beatmaps don't work correctly. Forcing lazer to decompress *and* compress using Shift-JIS will fix this. Here's a rough idea of how things look for japanese character filenames in current `master`: | | stable | lazer | |--------|--------|--------| | export encoding | shift-jis | utf8 | | utf8 [bit flag](https://superuser.com/a/1507988) set | ❌ | ❌ | | import stable export osz | ✅ | ❌ | | import lazer export osz | ❌ | ✅ | | windows unzip | ❌ | ❌ | | macos unzip | ✅ | ✅ | and after this change | | stable | lazer | |--------|--------|--------| | export encoding | shift-jis | shift-jis | | utf8 [bit flag](https://superuser.com/a/1507988) set | ❌ | ❌ | | import stable export osz | ✅ | ✅ | | import lazer export osz | ✅ | ✅ | | windows unzip | ❌ | ❌ | | macos unzip | ✅ | ✅ | A future endeavour to improve compatibility would be to look at setting the utf8 flag in lazer, switching the default to utf8, and ensuring the stable supports this flag (I don't believe it does right now). --- osu.Game/Database/LegacyArchiveExporter.cs | 6 +++++- osu.Game/IO/Archives/ZipArchiveReader.cs | 24 +++++++++++++++++++++- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/osu.Game/Database/LegacyArchiveExporter.cs b/osu.Game/Database/LegacyArchiveExporter.cs index 9805207591..1d9d252220 100644 --- a/osu.Game/Database/LegacyArchiveExporter.cs +++ b/osu.Game/Database/LegacyArchiveExporter.cs @@ -7,6 +7,7 @@ using System.Threading; using osu.Framework.Logging; using osu.Framework.Platform; using osu.Game.Extensions; +using osu.Game.IO.Archives; using osu.Game.Overlays.Notifications; using Realms; using SharpCompress.Common; @@ -29,7 +30,10 @@ namespace osu.Game.Database public override void ExportToStream(TModel model, Stream outputStream, ProgressNotification? notification, CancellationToken cancellationToken = default) { - using (var writer = new ZipWriter(outputStream, new ZipWriterOptions(CompressionType.Deflate))) + using (var writer = new ZipWriter(outputStream, new ZipWriterOptions(CompressionType.Deflate) + { + ArchiveEncoding = ZipArchiveReader.DEFAULT_ENCODING + })) { int i = 0; int fileCount = model.Files.Count(); diff --git a/osu.Game/IO/Archives/ZipArchiveReader.cs b/osu.Game/IO/Archives/ZipArchiveReader.cs index cc5c65d184..6bb2a314e7 100644 --- a/osu.Game/IO/Archives/ZipArchiveReader.cs +++ b/osu.Game/IO/Archives/ZipArchiveReader.cs @@ -7,23 +7,45 @@ using System.Buffers; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Text; using Microsoft.Toolkit.HighPerformance; using osu.Framework.IO.Stores; using SharpCompress.Archives.Zip; +using SharpCompress.Common; +using SharpCompress.Readers; using SixLabors.ImageSharp.Memory; namespace osu.Game.IO.Archives { public sealed class ZipArchiveReader : ArchiveReader { + /// + /// Archives created by osu!stable still write out as Shift-JIS. + /// We want to force this fallback rather than leave it up to the library/system. + /// In the future we may want to change exports to set the zip UTF-8 flag and use that instead. + /// + public static readonly ArchiveEncoding DEFAULT_ENCODING; + private readonly Stream archiveStream; private readonly ZipArchive archive; + static ZipArchiveReader() + { + // Required to support rare code pages. + Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); + + DEFAULT_ENCODING = new ArchiveEncoding(Encoding.GetEncoding(932), Encoding.GetEncoding(932)); + } + public ZipArchiveReader(Stream archiveStream, string name = null) : base(name) { this.archiveStream = archiveStream; - archive = ZipArchive.Open(archiveStream); + + archive = ZipArchive.Open(archiveStream, new ReaderOptions + { + ArchiveEncoding = DEFAULT_ENCODING + }); } public override Stream GetStream(string name) From a8416f3572bfa143c6019322365211fb0df1837b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 30 Apr 2024 12:39:18 +0800 Subject: [PATCH 0647/2493] Move `exists` check inside retry operation Might help? --- osu.Game/Database/RealmAccess.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Game/Database/RealmAccess.cs b/osu.Game/Database/RealmAccess.cs index 31ae22178f..465d7b15a7 100644 --- a/osu.Game/Database/RealmAccess.cs +++ b/osu.Game/Database/RealmAccess.cs @@ -307,17 +307,17 @@ namespace osu.Game.Database // Realms.Exceptions.RealmException: SetEndOfFile() failed: unknown error (1224) // // which can occur due to file handles still being open by a previous instance. - if (storage.Exists(Filename)) + // + // If this fails we allow it to block game startup. It's better than any alternative we can offer. + FileUtils.AttemptOperation(() => { - // If this fails we allow it to block game startup. - // It's better than any alternative we can offer. - FileUtils.AttemptOperation(() => + if (storage.Exists(Filename)) { using (var _ = storage.GetStream(Filename, FileAccess.ReadWrite)) { } - }); - } + } + }); string newerVersionFilename = $"{Filename.Replace(realm_extension, string.Empty)}_newer_version{realm_extension}"; From 0bfad74907be51a9755e43cad6f44db372b57f21 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 30 Apr 2024 14:09:29 +0800 Subject: [PATCH 0648/2493] Move realm error handling to avoid triggering in test scenarios --- osu.Game/Database/RealmAccess.cs | 38 +++++++++++++++++--------------- 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/osu.Game/Database/RealmAccess.cs b/osu.Game/Database/RealmAccess.cs index 465d7b15a7..057bbe02e6 100644 --- a/osu.Game/Database/RealmAccess.cs +++ b/osu.Game/Database/RealmAccess.cs @@ -301,24 +301,6 @@ namespace osu.Game.Database private Realm prepareFirstRealmAccess() { - // Before attempting to initialise realm, make sure the realm file isn't locked and has correct permissions. - // - // This is to avoid failures like: - // Realms.Exceptions.RealmException: SetEndOfFile() failed: unknown error (1224) - // - // which can occur due to file handles still being open by a previous instance. - // - // If this fails we allow it to block game startup. It's better than any alternative we can offer. - FileUtils.AttemptOperation(() => - { - if (storage.Exists(Filename)) - { - using (var _ = storage.GetStream(Filename, FileAccess.ReadWrite)) - { - } - } - }); - string newerVersionFilename = $"{Filename.Replace(realm_extension, string.Empty)}_newer_version{realm_extension}"; // Attempt to recover a newer database version if available. @@ -346,6 +328,26 @@ namespace osu.Game.Database } else { + // This error can occur due to file handles still being open by a previous instance. + // If this is the case, rather than assuming the realm file is corrupt, block game startup. + if (e.Message.StartsWith("SetEndOfFile() failed", StringComparison.Ordinal)) + { + // This will throw if the realm file is not available for write access after 5 seconds. + FileUtils.AttemptOperation(() => + { + if (storage.Exists(Filename)) + { + using (var _ = storage.GetStream(Filename, FileAccess.ReadWrite)) + { + } + } + }, 20); + + // If the above eventually succeeds, try and continue startup as per normal. + // This may throw again but let's allow it to, and block startup. + return getRealmInstance(); + } + Logger.Error(e, "Realm startup failed with unrecoverable error; starting with a fresh database. A backup of your database has been made."); createBackup($"{Filename.Replace(realm_extension, string.Empty)}_{DateTimeOffset.UtcNow.ToUnixTimeSeconds()}_corrupt{realm_extension}"); } From ba9f4e4baff591cc4b35787216edfa4d88ccabc1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 30 Apr 2024 16:08:30 +0800 Subject: [PATCH 0649/2493] Don't skip lines in beatmap decoding Was added in cc76c58f5f250151bb85ad5efa3f6ce008f0cbb0 without any specific reasoning. Likely not required (and will fix some storyboard elements inside `.osu` files from not being correctly saved). --- osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs index 7407c3590f..84f3c0d82a 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs @@ -167,8 +167,6 @@ namespace osu.Game.Beatmaps.Formats beatmapInfo.SamplesMatchPlaybackRate = false; } - protected override bool ShouldSkipLine(string line) => base.ShouldSkipLine(line) || line.StartsWith(' ') || line.StartsWith('_'); - protected override void ParseLine(Beatmap beatmap, Section section, string line) { switch (section) From a3213fc36dd23f835db6cac300b1963948757e75 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 30 Apr 2024 21:40:04 +0800 Subject: [PATCH 0650/2493] Change `.olz` to use UTF-8 encoding --- osu.Game/Database/BeatmapExporter.cs | 2 ++ osu.Game/Database/LegacyArchiveExporter.cs | 8 +++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/osu.Game/Database/BeatmapExporter.cs b/osu.Game/Database/BeatmapExporter.cs index f37c57dea5..01ef09d3d7 100644 --- a/osu.Game/Database/BeatmapExporter.cs +++ b/osu.Game/Database/BeatmapExporter.cs @@ -17,6 +17,8 @@ namespace osu.Game.Database { } + protected override bool UseFixedEncoding => false; + protected override string FileExtension => @".olz"; } } diff --git a/osu.Game/Database/LegacyArchiveExporter.cs b/osu.Game/Database/LegacyArchiveExporter.cs index 1d9d252220..b0e5304ffd 100644 --- a/osu.Game/Database/LegacyArchiveExporter.cs +++ b/osu.Game/Database/LegacyArchiveExporter.cs @@ -3,6 +3,7 @@ using System.IO; using System.Linq; +using System.Text; using System.Threading; using osu.Framework.Logging; using osu.Framework.Platform; @@ -23,6 +24,11 @@ namespace osu.Game.Database public abstract class LegacyArchiveExporter : LegacyExporter where TModel : RealmObject, IHasNamedFiles, IHasGuidPrimaryKey { + /// + /// Whether to always use Shift-JIS encoding for archive filenames (like osu!stable did). + /// + protected virtual bool UseFixedEncoding => true; + protected LegacyArchiveExporter(Storage storage) : base(storage) { @@ -32,7 +38,7 @@ namespace osu.Game.Database { using (var writer = new ZipWriter(outputStream, new ZipWriterOptions(CompressionType.Deflate) { - ArchiveEncoding = ZipArchiveReader.DEFAULT_ENCODING + ArchiveEncoding = UseFixedEncoding ? ZipArchiveReader.DEFAULT_ENCODING : new ArchiveEncoding(Encoding.UTF8, Encoding.UTF8) })) { int i = 0; From 6a7e2dc258a5cb4d2ea2a72b022cf75eec32186e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 30 Apr 2024 21:47:03 +0800 Subject: [PATCH 0651/2493] Fix formatting --- osu.Game/Database/LegacyArchiveExporter.cs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/osu.Game/Database/LegacyArchiveExporter.cs b/osu.Game/Database/LegacyArchiveExporter.cs index b0e5304ffd..e4d3ed4681 100644 --- a/osu.Game/Database/LegacyArchiveExporter.cs +++ b/osu.Game/Database/LegacyArchiveExporter.cs @@ -36,10 +36,12 @@ namespace osu.Game.Database public override void ExportToStream(TModel model, Stream outputStream, ProgressNotification? notification, CancellationToken cancellationToken = default) { - using (var writer = new ZipWriter(outputStream, new ZipWriterOptions(CompressionType.Deflate) - { - ArchiveEncoding = UseFixedEncoding ? ZipArchiveReader.DEFAULT_ENCODING : new ArchiveEncoding(Encoding.UTF8, Encoding.UTF8) - })) + var zipWriterOptions = new ZipWriterOptions(CompressionType.Deflate) + { + ArchiveEncoding = UseFixedEncoding ? ZipArchiveReader.DEFAULT_ENCODING : new ArchiveEncoding(Encoding.UTF8, Encoding.UTF8) + }; + + using (var writer = new ZipWriter(outputStream, zipWriterOptions)) { int i = 0; int fileCount = model.Files.Count(); From 38e239a435409e66cb1ccde05696b17afe2eae71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 30 Apr 2024 16:07:37 +0200 Subject: [PATCH 0652/2493] Expand test to cover both legacy and non-legacy export paths --- .../Skins/TestSceneBeatmapSkinResources.cs | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Skins/TestSceneBeatmapSkinResources.cs b/osu.Game.Tests/Skins/TestSceneBeatmapSkinResources.cs index e0922c52f7..5086b64433 100644 --- a/osu.Game.Tests/Skins/TestSceneBeatmapSkinResources.cs +++ b/osu.Game.Tests/Skins/TestSceneBeatmapSkinResources.cs @@ -24,7 +24,7 @@ namespace osu.Game.Tests.Skins private BeatmapManager beatmaps { get; set; } = null!; [Test] - public void TestRetrieveJapaneseFilename() + public void TestRetrieveAndLegacyExportJapaneseFilename() { IWorkingBeatmap beatmap = null!; MemoryStream outStream = null!; @@ -46,6 +46,29 @@ namespace osu.Game.Tests.Skins AddAssert("sample is non-null", () => beatmap.Skin.GetSample(new SampleInfo(@"見本")) != null); } + [Test] + public void TestRetrieveAndNonLegacyExportJapaneseFilename() + { + IWorkingBeatmap beatmap = null!; + MemoryStream outStream = null!; + + // Ensure importer encoding is correct + AddStep("import beatmap", () => beatmap = importBeatmapFromArchives(@"japanese-filename.osz")); + AddAssert("sample is non-null", () => beatmap.Skin.GetSample(new SampleInfo(@"見本")) != null); + + // Ensure exporter encoding is correct (round trip) + AddStep("export", () => + { + outStream = new MemoryStream(); + + new BeatmapExporter(LocalStorage) + .ExportToStream((BeatmapSetInfo)beatmap.BeatmapInfo.BeatmapSet!, outStream, null); + }); + + AddStep("import beatmap again", () => beatmap = importBeatmapFromStream(outStream)); + AddAssert("sample is non-null", () => beatmap.Skin.GetSample(new SampleInfo(@"見本")) != null); + } + [Test] public void TestRetrieveOggAudio() { From ff108416d86a920dbe9f652898c59b263f2379a1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 1 May 2024 00:05:14 +0800 Subject: [PATCH 0653/2493] Fix incorrect background being loaded due to async race If the API login (and thus user set) completed between `load` and `LoadComplete`, the re-fetch on user change would not yet be hooked up, causing an incorrect default background to be used instead. Of note, moving this out of async load doesn't really affect load performance as the bulk of the load operation is already scheduled and `LoadComponentAsync`ed anyway --- osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs b/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs index a552b22c11..090e006671 100644 --- a/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs +++ b/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs @@ -56,10 +56,6 @@ namespace osu.Game.Screens.Backgrounds introSequence = config.GetBindable(OsuSetting.IntroSequence); AddInternal(seasonalBackgroundLoader); - - // Load first background asynchronously as part of BDL load. - currentDisplay = RNG.Next(0, background_count); - Next(); } protected override void LoadComplete() @@ -73,6 +69,9 @@ namespace osu.Game.Screens.Backgrounds introSequence.ValueChanged += _ => Scheduler.AddOnce(next); seasonalBackgroundLoader.SeasonalBackgroundChanged += () => Scheduler.AddOnce(next); + currentDisplay = RNG.Next(0, background_count); + Next(); + // helper function required for AddOnce usage. void next() => Next(); } From 44091b1f352273c36f7545bfe44eaaeb0ef19003 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 1 May 2024 17:33:03 +0800 Subject: [PATCH 0654/2493] Fix some lines still getting forgotten about --- .../Beatmaps/Formats/LegacyBeatmapDecoder.cs | 75 ++++++++++--------- 1 file changed, 38 insertions(+), 37 deletions(-) diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs index 84f3c0d82a..3ecc29bd02 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs @@ -415,53 +415,54 @@ namespace osu.Game.Beatmaps.Formats { string[] split = line.Split(','); - if (!Enum.TryParse(split[0], out LegacyEventType type)) - throw new InvalidDataException($@"Unknown event type: {split[0]}"); // Until we have full storyboard encoder coverage, let's track any lines which aren't handled // and store them to a temporary location such that they aren't lost on editor save / export. bool lineSupportedByEncoder = false; - switch (type) + if (Enum.TryParse(split[0], out LegacyEventType type)) { - case LegacyEventType.Sprite: - // Generally, the background is the first thing defined in a beatmap file. - // In some older beatmaps, it is not present and replaced by a storyboard-level background instead. - // Allow the first sprite (by file order) to act as the background in such cases. - if (string.IsNullOrEmpty(beatmap.BeatmapInfo.Metadata.BackgroundFile)) - { - beatmap.BeatmapInfo.Metadata.BackgroundFile = CleanFilename(split[3]); + switch (type) + { + case LegacyEventType.Sprite: + // Generally, the background is the first thing defined in a beatmap file. + // In some older beatmaps, it is not present and replaced by a storyboard-level background instead. + // Allow the first sprite (by file order) to act as the background in such cases. + if (string.IsNullOrEmpty(beatmap.BeatmapInfo.Metadata.BackgroundFile)) + { + beatmap.BeatmapInfo.Metadata.BackgroundFile = CleanFilename(split[3]); + lineSupportedByEncoder = true; + } + + break; + + case LegacyEventType.Video: + string filename = CleanFilename(split[2]); + + // Some very old beatmaps had incorrect type specifications for their backgrounds (ie. using 1 for VIDEO + // instead of 0 for BACKGROUND). To handle this gracefully, check the file extension against known supported + // video extensions and handle similar to a background if it doesn't match. + if (!OsuGameBase.VIDEO_EXTENSIONS.Contains(Path.GetExtension(filename).ToLowerInvariant())) + { + beatmap.BeatmapInfo.Metadata.BackgroundFile = filename; + lineSupportedByEncoder = true; + } + + break; + + case LegacyEventType.Background: + beatmap.BeatmapInfo.Metadata.BackgroundFile = CleanFilename(split[2]); lineSupportedByEncoder = true; - } + break; - break; + case LegacyEventType.Break: + double start = getOffsetTime(Parsing.ParseDouble(split[1])); + double end = Math.Max(start, getOffsetTime(Parsing.ParseDouble(split[2]))); - case LegacyEventType.Video: - string filename = CleanFilename(split[2]); - - // Some very old beatmaps had incorrect type specifications for their backgrounds (ie. using 1 for VIDEO - // instead of 0 for BACKGROUND). To handle this gracefully, check the file extension against known supported - // video extensions and handle similar to a background if it doesn't match. - if (!OsuGameBase.VIDEO_EXTENSIONS.Contains(Path.GetExtension(filename).ToLowerInvariant())) - { - beatmap.BeatmapInfo.Metadata.BackgroundFile = filename; + beatmap.Breaks.Add(new BreakPeriod(start, end)); lineSupportedByEncoder = true; - } - - break; - - case LegacyEventType.Background: - beatmap.BeatmapInfo.Metadata.BackgroundFile = CleanFilename(split[2]); - lineSupportedByEncoder = true; - break; - - case LegacyEventType.Break: - double start = getOffsetTime(Parsing.ParseDouble(split[1])); - double end = Math.Max(start, getOffsetTime(Parsing.ParseDouble(split[2]))); - - beatmap.Breaks.Add(new BreakPeriod(start, end)); - lineSupportedByEncoder = true; - break; + break; + } } if (!lineSupportedByEncoder) From 67c0d7590a93a07cc642ecd1b03f70d6768f4638 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 1 May 2024 19:31:39 +0800 Subject: [PATCH 0655/2493] Decrease alpha of delayed resume overlay as count approaches zero Allows better visibility of playfield underneath it. --- osu.Game/Screens/Play/DelayedResumeOverlay.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Screens/Play/DelayedResumeOverlay.cs b/osu.Game/Screens/Play/DelayedResumeOverlay.cs index 147d48ae02..32cdabcf98 100644 --- a/osu.Game/Screens/Play/DelayedResumeOverlay.cs +++ b/osu.Game/Screens/Play/DelayedResumeOverlay.cs @@ -170,6 +170,8 @@ namespace osu.Game.Screens.Play countdownProgress.Progress = amountTimePassed; countdownProgress.InnerRadius = progress_stroke_width / progress_size / countdownProgress.Scale.X; + Alpha = 0.2f + 0.8f * newCount / 3f; + if (countdownCount != newCount) { if (newCount > 0) From 87e814e2019d0480d638f2287f2392ec14e7a1ea Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 1 May 2024 19:34:42 +0800 Subject: [PATCH 0656/2493] Fix incorrect xmldoc and adjust colour provider to match `Player` --- osu.Game/Rulesets/UI/DrawableRuleset.cs | 2 +- osu.Game/Screens/Play/DelayedResumeOverlay.cs | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/osu.Game/Rulesets/UI/DrawableRuleset.cs b/osu.Game/Rulesets/UI/DrawableRuleset.cs index a422761800..a28b2716cb 100644 --- a/osu.Game/Rulesets/UI/DrawableRuleset.cs +++ b/osu.Game/Rulesets/UI/DrawableRuleset.cs @@ -529,7 +529,7 @@ namespace osu.Game.Rulesets.UI public ResumeOverlay ResumeOverlay { get; protected set; } /// - /// Whether the should be used to return the user's cursor position to its previous location after a pause. + /// Whether a should be displayed on resuming after a pause. /// /// /// Defaults to true. diff --git a/osu.Game/Screens/Play/DelayedResumeOverlay.cs b/osu.Game/Screens/Play/DelayedResumeOverlay.cs index 32cdabcf98..3202a524a8 100644 --- a/osu.Game/Screens/Play/DelayedResumeOverlay.cs +++ b/osu.Game/Screens/Play/DelayedResumeOverlay.cs @@ -24,8 +24,9 @@ namespace osu.Game.Screens.Play /// public partial class DelayedResumeOverlay : ResumeOverlay { - // todo: this shouldn't define its own colour provider, but nothing in Player screen does, so let's do that for now. - private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Blue); + // todo: this shouldn't define its own colour provider, but nothing in DrawableRuleset guarantees this, so let's do it locally for now. + // (of note, Player does cache one but any test which uses a DrawableRuleset without Player will fail without this). + private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple); private const float outer_size = 200; private const float inner_size = 150; From b8209b92f61d1a4098ccf0a42c36c7b7aceeb473 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 1 May 2024 19:49:45 +0800 Subject: [PATCH 0657/2493] Add delay before delayed resume starts It was previously a bit sudden after dismissing the pause screen. Now there's a short delay before the actual countdown begins. --- osu.Game/Screens/Play/DelayedResumeOverlay.cs | 33 ++++++++++--------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/osu.Game/Screens/Play/DelayedResumeOverlay.cs b/osu.Game/Screens/Play/DelayedResumeOverlay.cs index 3202a524a8..8acb94a5af 100644 --- a/osu.Game/Screens/Play/DelayedResumeOverlay.cs +++ b/osu.Game/Screens/Play/DelayedResumeOverlay.cs @@ -11,7 +11,6 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.UserInterface; using osu.Framework.Localisation; -using osu.Framework.Threading; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Overlays; @@ -35,9 +34,10 @@ namespace osu.Game.Screens.Play private const double countdown_time = 2000; + private const int total_count = 3; + protected override LocalisableString Message => string.Empty; - private ScheduledDelegate? scheduledResume; private int? countdownCount; private double countdownStartTime; private bool countdownComplete; @@ -121,21 +121,17 @@ namespace osu.Game.Screens.Play innerContent.FadeIn().ScaleTo(Vector2.Zero).Then().ScaleTo(Vector2.One, 400, Easing.OutElasticHalf); countdownComponents.FadeOut().Delay(50).FadeTo(1, 100); + countdownProgress.Progress = 0; + // Reset states for various components. countdownBackground.FadeIn(); countdownText.FadeIn(); + countdownText.Text = string.Empty; countdownProgress.FadeIn().ScaleTo(1); countdownComplete = false; countdownCount = null; - countdownStartTime = Time.Current; - - scheduledResume?.Cancel(); - scheduledResume = Scheduler.AddDelayed(() => - { - countdownComplete = true; - Resume(); - }, countdown_time); + countdownStartTime = Time.Current + 200; } protected override void PopOut() @@ -153,8 +149,6 @@ namespace osu.Game.Screens.Play } else countdownProgress.FadeOut(); - - scheduledResume?.Cancel(); } protected override void Update() @@ -165,13 +159,16 @@ namespace osu.Game.Screens.Play private void updateCountdown() { - double amountTimePassed = Math.Min(countdown_time, Time.Current - countdownStartTime) / countdown_time; - int newCount = 3 - (int)Math.Floor(amountTimePassed * 3); + if (State.Value == Visibility.Hidden || countdownComplete || Time.Current < countdownStartTime) + return; + + double amountTimePassed = Math.Clamp((Time.Current - countdownStartTime) / countdown_time, 0, countdown_time); + int newCount = Math.Clamp(total_count - (int)Math.Floor(amountTimePassed * total_count), 0, total_count); countdownProgress.Progress = amountTimePassed; countdownProgress.InnerRadius = progress_stroke_width / progress_size / countdownProgress.Scale.X; - Alpha = 0.2f + 0.8f * newCount / 3f; + Alpha = 0.2f + 0.8f * newCount / total_count; if (countdownCount != newCount) { @@ -194,6 +191,12 @@ namespace osu.Game.Screens.Play } countdownCount = newCount; + + if (countdownCount == 0) + { + countdownComplete = true; + Resume(); + } } } } From f0eef329139f0dbe25fb0aa5ee534b33386f466f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 1 May 2024 15:21:39 +0200 Subject: [PATCH 0658/2493] Fix code quality inspection --- osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs index 3ecc29bd02..6fa78fa8e6 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs @@ -415,7 +415,6 @@ namespace osu.Game.Beatmaps.Formats { string[] split = line.Split(','); - // Until we have full storyboard encoder coverage, let's track any lines which aren't handled // and store them to a temporary location such that they aren't lost on editor save / export. bool lineSupportedByEncoder = false; From 981a19f6a5c6a9b2d9ef904e9644ac82af5e7007 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 1 May 2024 17:17:50 +0300 Subject: [PATCH 0659/2493] Disable naming inspections in p/invoke code --- osu.Desktop/NVAPI.cs | 3 +++ osu.Desktop/Windows/WindowsAssociationManager.cs | 5 +++++ 2 files changed, 8 insertions(+) diff --git a/osu.Desktop/NVAPI.cs b/osu.Desktop/NVAPI.cs index 554f89a847..c1622e7cc1 100644 --- a/osu.Desktop/NVAPI.cs +++ b/osu.Desktop/NVAPI.cs @@ -489,6 +489,7 @@ namespace osu.Desktop public static uint Stride => (uint)Marshal.SizeOf(typeof(NvApplication)) | (2 << 16); } + // ReSharper disable InconsistentNaming internal enum NvStatus { OK = 0, // Success. Request is completed. @@ -738,4 +739,6 @@ namespace osu.Desktop OGL_THREAD_CONTROL_NUM_VALUES = 2, OGL_THREAD_CONTROL_DEFAULT = 0 } + + // ReSharper restore InconsistentNaming } diff --git a/osu.Desktop/Windows/WindowsAssociationManager.cs b/osu.Desktop/Windows/WindowsAssociationManager.cs index 11b5c19ca1..e4c070934e 100644 --- a/osu.Desktop/Windows/WindowsAssociationManager.cs +++ b/osu.Desktop/Windows/WindowsAssociationManager.cs @@ -163,6 +163,8 @@ namespace osu.Desktop.Windows [DllImport("Shell32.dll")] private static extern void SHChangeNotify(EventId wEventId, Flags uFlags, IntPtr dwItem1, IntPtr dwItem2); + // ReSharper disable InconsistentNaming + private enum EventId { /// @@ -174,9 +176,12 @@ namespace osu.Desktop.Windows private enum Flags : uint { + // ReSharper disable once InconsistentNaming SHCNF_IDLIST = 0x0000 } + // ReSharper restore InconsistentNaming + #endregion private record FileAssociation(string Extension, LocalisableString Description, string IconPath) From 02be275554beed4e450c8eca29d1cf979890d489 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 1 May 2024 17:20:20 +0300 Subject: [PATCH 0660/2493] Disable naming inspections in country/language enums --- osu.Game/Localisation/Language.cs | 2 ++ osu.Game/Users/CountryCode.cs | 2 ++ 2 files changed, 4 insertions(+) diff --git a/osu.Game/Localisation/Language.cs b/osu.Game/Localisation/Language.cs index 711e95486f..e946779cba 100644 --- a/osu.Game/Localisation/Language.cs +++ b/osu.Game/Localisation/Language.cs @@ -4,6 +4,8 @@ using System.ComponentModel; using JetBrains.Annotations; +// ReSharper disable InconsistentNaming + namespace osu.Game.Localisation { [UsedImplicitly(ImplicitUseTargetFlags.WithMembers)] diff --git a/osu.Game/Users/CountryCode.cs b/osu.Game/Users/CountryCode.cs index edaa1562c7..6a0ed63648 100644 --- a/osu.Game/Users/CountryCode.cs +++ b/osu.Game/Users/CountryCode.cs @@ -6,6 +6,8 @@ using JetBrains.Annotations; using Newtonsoft.Json; using Newtonsoft.Json.Converters; +// ReSharper disable InconsistentNaming + namespace osu.Game.Users { /// From 2fd8950b2135edde3d63431ea68e25f951a93b5b Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 1 May 2024 17:22:08 +0300 Subject: [PATCH 0661/2493] Disable inconsistent naming in some fields of `LegacyManiaSkinConfigurationLookup` --- osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs b/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs index cacca0de23..1550fc8a47 100644 --- a/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs +++ b/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs @@ -67,7 +67,10 @@ namespace osu.Game.Skinning LeftStageImage, RightStageImage, BottomStageImage, + + // ReSharper disable once InconsistentNaming Hit300g, + Hit300, Hit200, Hit100, From 97fc2a5473854b403525bcc028aaf24711af58ec Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 1 May 2024 17:20:57 +0300 Subject: [PATCH 0662/2493] Disable inconsistent naming in some fields of `ScoreRank` --- osu.Game/Scoring/ScoreRank.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Scoring/ScoreRank.cs b/osu.Game/Scoring/ScoreRank.cs index 327e4191d7..957cfc9b95 100644 --- a/osu.Game/Scoring/ScoreRank.cs +++ b/osu.Game/Scoring/ScoreRank.cs @@ -35,6 +35,7 @@ namespace osu.Game.Scoring [LocalisableDescription(typeof(BeatmapsStrings), nameof(BeatmapsStrings.RankSH))] [Description(@"S+")] + // ReSharper disable once InconsistentNaming SH, [LocalisableDescription(typeof(BeatmapsStrings), nameof(BeatmapsStrings.RankX))] @@ -43,6 +44,7 @@ namespace osu.Game.Scoring [LocalisableDescription(typeof(BeatmapsStrings), nameof(BeatmapsStrings.RankXH))] [Description(@"SS+")] + // ReSharper disable once InconsistentNaming XH, } } From 16bae4f0046f0c9bde8cdadb2a96f535c0904744 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 1 May 2024 17:22:42 +0300 Subject: [PATCH 0663/2493] Update naming of enum fields in `StartMode` --- .../Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs index 66acd6d1b0..5446211ced 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs @@ -486,16 +486,16 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match Off = 0, [Description("30 seconds")] - Seconds_30 = 30, + Seconds30 = 30, [Description("1 minute")] - Seconds_60 = 60, + Seconds60 = 60, [Description("3 minutes")] - Seconds_180 = 180, + Seconds180 = 180, [Description("5 minutes")] - Seconds_300 = 300 + Seconds300 = 300 } } } From e8a63813953ac17f3f646fe02dcfdf6f404cb158 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 1 May 2024 17:22:52 +0300 Subject: [PATCH 0664/2493] Update naming of enum fields in `ObjType` --- osu.Game/IO/Legacy/SerializationReader.cs | 76 +++++++++++------------ osu.Game/IO/Legacy/SerializationWriter.cs | 40 ++++++------ 2 files changed, 58 insertions(+), 58 deletions(-) diff --git a/osu.Game/IO/Legacy/SerializationReader.cs b/osu.Game/IO/Legacy/SerializationReader.cs index 2d3d5bffd5..fc61b028a4 100644 --- a/osu.Game/IO/Legacy/SerializationReader.cs +++ b/osu.Game/IO/Legacy/SerializationReader.cs @@ -123,58 +123,58 @@ namespace osu.Game.IO.Legacy switch (t) { - case ObjType.boolType: + case ObjType.BoolType: return ReadBoolean(); - case ObjType.byteType: + case ObjType.ByteType: return ReadByte(); - case ObjType.uint16Type: + case ObjType.UInt16Type: return ReadUInt16(); - case ObjType.uint32Type: + case ObjType.UInt32Type: return ReadUInt32(); - case ObjType.uint64Type: + case ObjType.UInt64Type: return ReadUInt64(); - case ObjType.sbyteType: + case ObjType.SByteType: return ReadSByte(); - case ObjType.int16Type: + case ObjType.Int16Type: return ReadInt16(); - case ObjType.int32Type: + case ObjType.Int32Type: return ReadInt32(); - case ObjType.int64Type: + case ObjType.Int64Type: return ReadInt64(); - case ObjType.charType: + case ObjType.CharType: return ReadChar(); - case ObjType.stringType: + case ObjType.StringType: return base.ReadString(); - case ObjType.singleType: + case ObjType.SingleType: return ReadSingle(); - case ObjType.doubleType: + case ObjType.DoubleType: return ReadDouble(); - case ObjType.decimalType: + case ObjType.DecimalType: return ReadDecimal(); - case ObjType.dateTimeType: + case ObjType.DateTimeType: return ReadDateTime(); - case ObjType.byteArrayType: + case ObjType.ByteArrayType: return ReadByteArray(); - case ObjType.charArrayType: + case ObjType.CharArrayType: return ReadCharArray(); - case ObjType.otherType: + case ObjType.OtherType: throw new IOException("Deserialization of arbitrary type is not supported."); default: @@ -185,25 +185,25 @@ namespace osu.Game.IO.Legacy public enum ObjType : byte { - nullType, - boolType, - byteType, - uint16Type, - uint32Type, - uint64Type, - sbyteType, - int16Type, - int32Type, - int64Type, - charType, - stringType, - singleType, - doubleType, - decimalType, - dateTimeType, - byteArrayType, - charArrayType, - otherType, - ILegacySerializableType + NullType, + BoolType, + ByteType, + UInt16Type, + UInt32Type, + UInt64Type, + SByteType, + Int16Type, + Int32Type, + Int64Type, + CharType, + StringType, + SingleType, + DoubleType, + DecimalType, + DateTimeType, + ByteArrayType, + CharArrayType, + OtherType, + LegacySerializableType } } diff --git a/osu.Game/IO/Legacy/SerializationWriter.cs b/osu.Game/IO/Legacy/SerializationWriter.cs index 10572a6478..afe86cd096 100644 --- a/osu.Game/IO/Legacy/SerializationWriter.cs +++ b/osu.Game/IO/Legacy/SerializationWriter.cs @@ -34,11 +34,11 @@ namespace osu.Game.IO.Legacy { if (str == null) { - Write((byte)ObjType.nullType); + Write((byte)ObjType.NullType); } else { - Write((byte)ObjType.stringType); + Write((byte)ObjType.StringType); base.Write(str); } } @@ -125,94 +125,94 @@ namespace osu.Game.IO.Legacy { if (obj == null) { - Write((byte)ObjType.nullType); + Write((byte)ObjType.NullType); } else { switch (obj) { case bool boolObj: - Write((byte)ObjType.boolType); + Write((byte)ObjType.BoolType); Write(boolObj); break; case byte byteObj: - Write((byte)ObjType.byteType); + Write((byte)ObjType.ByteType); Write(byteObj); break; case ushort ushortObj: - Write((byte)ObjType.uint16Type); + Write((byte)ObjType.UInt16Type); Write(ushortObj); break; case uint uintObj: - Write((byte)ObjType.uint32Type); + Write((byte)ObjType.UInt32Type); Write(uintObj); break; case ulong ulongObj: - Write((byte)ObjType.uint64Type); + Write((byte)ObjType.UInt64Type); Write(ulongObj); break; case sbyte sbyteObj: - Write((byte)ObjType.sbyteType); + Write((byte)ObjType.SByteType); Write(sbyteObj); break; case short shortObj: - Write((byte)ObjType.int16Type); + Write((byte)ObjType.Int16Type); Write(shortObj); break; case int intObj: - Write((byte)ObjType.int32Type); + Write((byte)ObjType.Int32Type); Write(intObj); break; case long longObj: - Write((byte)ObjType.int64Type); + Write((byte)ObjType.Int64Type); Write(longObj); break; case char charObj: - Write((byte)ObjType.charType); + Write((byte)ObjType.CharType); base.Write(charObj); break; case string stringObj: - Write((byte)ObjType.stringType); + Write((byte)ObjType.StringType); base.Write(stringObj); break; case float floatObj: - Write((byte)ObjType.singleType); + Write((byte)ObjType.SingleType); Write(floatObj); break; case double doubleObj: - Write((byte)ObjType.doubleType); + Write((byte)ObjType.DoubleType); Write(doubleObj); break; case decimal decimalObj: - Write((byte)ObjType.decimalType); + Write((byte)ObjType.DecimalType); Write(decimalObj); break; case DateTime dateTimeObj: - Write((byte)ObjType.dateTimeType); + Write((byte)ObjType.DateTimeType); Write(dateTimeObj); break; case byte[] byteArray: - Write((byte)ObjType.byteArrayType); + Write((byte)ObjType.ByteArrayType); base.Write(byteArray); break; case char[] charArray: - Write((byte)ObjType.charArrayType); + Write((byte)ObjType.CharArrayType); base.Write(charArray); break; From 9dc1a58ce7a4fb00a973a3ce4498b5bff8164e16 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 1 May 2024 17:23:27 +0300 Subject: [PATCH 0665/2493] Add few abbreviations from existing enums to appease naming inspection --- osu.sln.DotSettings | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.sln.DotSettings b/osu.sln.DotSettings index dd71744bf0..51af281ac6 100644 --- a/osu.sln.DotSettings +++ b/osu.sln.DotSettings @@ -340,6 +340,7 @@ API ARGB BPM + DDKK EF FPS GC @@ -357,6 +358,8 @@ IP IPC JIT + KDDK + KKDD LTRB MD5 NS @@ -375,6 +378,7 @@ QAT BNG UI + WIP False HINT <?xml version="1.0" encoding="utf-16"?> From 30fd40efd16a651a6c00b5c89289a85ffcbe546b Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 1 May 2024 21:18:56 +0300 Subject: [PATCH 0666/2493] Avoid disable/restore pairs --- osu.Desktop/NVAPI.cs | 9 ++++++--- osu.Desktop/Windows/WindowsAssociationManager.cs | 8 +++----- osu.Game/Localisation/Language.cs | 4 ++-- osu.Game/Users/CountryCode.cs | 4 ++-- 4 files changed, 13 insertions(+), 12 deletions(-) diff --git a/osu.Desktop/NVAPI.cs b/osu.Desktop/NVAPI.cs index c1622e7cc1..0b09613ba0 100644 --- a/osu.Desktop/NVAPI.cs +++ b/osu.Desktop/NVAPI.cs @@ -489,7 +489,7 @@ namespace osu.Desktop public static uint Stride => (uint)Marshal.SizeOf(typeof(NvApplication)) | (2 << 16); } - // ReSharper disable InconsistentNaming + [SuppressMessage("ReSharper", "InconsistentNaming")] internal enum NvStatus { OK = 0, // Success. Request is completed. @@ -612,6 +612,7 @@ namespace osu.Desktop FIRMWARE_REVISION_NOT_SUPPORTED = -200, // The device's firmware is not supported. } + [SuppressMessage("ReSharper", "InconsistentNaming")] internal enum NvSystemType { UNKNOWN = 0, @@ -619,6 +620,7 @@ namespace osu.Desktop DESKTOP = 2 } + [SuppressMessage("ReSharper", "InconsistentNaming")] internal enum NvGpuType { UNKNOWN = 0, @@ -626,6 +628,7 @@ namespace osu.Desktop DGPU = 2, // Discrete } + [SuppressMessage("ReSharper", "InconsistentNaming")] internal enum NvSettingID : uint { OGL_AA_LINE_GAMMA_ID = 0x2089BF6C, @@ -718,6 +721,7 @@ namespace osu.Desktop INVALID_SETTING_ID = 0xFFFFFFFF } + [SuppressMessage("ReSharper", "InconsistentNaming")] internal enum NvShimSetting : uint { SHIM_RENDERING_MODE_INTEGRATED = 0x00000000, @@ -732,6 +736,7 @@ namespace osu.Desktop SHIM_RENDERING_MODE_DEFAULT = SHIM_RENDERING_MODE_AUTO_SELECT } + [SuppressMessage("ReSharper", "InconsistentNaming")] internal enum NvThreadControlSetting : uint { OGL_THREAD_CONTROL_ENABLE = 0x00000001, @@ -739,6 +744,4 @@ namespace osu.Desktop OGL_THREAD_CONTROL_NUM_VALUES = 2, OGL_THREAD_CONTROL_DEFAULT = 0 } - - // ReSharper restore InconsistentNaming } diff --git a/osu.Desktop/Windows/WindowsAssociationManager.cs b/osu.Desktop/Windows/WindowsAssociationManager.cs index e4c070934e..b32c01433d 100644 --- a/osu.Desktop/Windows/WindowsAssociationManager.cs +++ b/osu.Desktop/Windows/WindowsAssociationManager.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Runtime.InteropServices; using System.Runtime.Versioning; @@ -163,8 +164,7 @@ namespace osu.Desktop.Windows [DllImport("Shell32.dll")] private static extern void SHChangeNotify(EventId wEventId, Flags uFlags, IntPtr dwItem1, IntPtr dwItem2); - // ReSharper disable InconsistentNaming - + [SuppressMessage("ReSharper", "InconsistentNaming")] private enum EventId { /// @@ -174,14 +174,12 @@ namespace osu.Desktop.Windows SHCNE_ASSOCCHANGED = 0x08000000 } + [SuppressMessage("ReSharper", "InconsistentNaming")] private enum Flags : uint { - // ReSharper disable once InconsistentNaming SHCNF_IDLIST = 0x0000 } - // ReSharper restore InconsistentNaming - #endregion private record FileAssociation(string Extension, LocalisableString Description, string IconPath) diff --git a/osu.Game/Localisation/Language.cs b/osu.Game/Localisation/Language.cs index e946779cba..4e1fc3a474 100644 --- a/osu.Game/Localisation/Language.cs +++ b/osu.Game/Localisation/Language.cs @@ -2,12 +2,12 @@ // See the LICENCE file in the repository root for full licence text. using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; using JetBrains.Annotations; -// ReSharper disable InconsistentNaming - namespace osu.Game.Localisation { + [SuppressMessage("ReSharper", "InconsistentNaming")] [UsedImplicitly(ImplicitUseTargetFlags.WithMembers)] public enum Language { diff --git a/osu.Game/Users/CountryCode.cs b/osu.Game/Users/CountryCode.cs index 6a0ed63648..59fcd5d625 100644 --- a/osu.Game/Users/CountryCode.cs +++ b/osu.Game/Users/CountryCode.cs @@ -2,18 +2,18 @@ // See the LICENCE file in the repository root for full licence text. using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; using JetBrains.Annotations; using Newtonsoft.Json; using Newtonsoft.Json.Converters; -// ReSharper disable InconsistentNaming - namespace osu.Game.Users { /// /// Matches `osu_countries` database table. /// [JsonConverter(typeof(StringEnumConverter))] + [SuppressMessage("ReSharper", "InconsistentNaming")] [UsedImplicitly(ImplicitUseTargetFlags.WithMembers)] public enum CountryCode { From eb45a406e153e9f533f51efaa95d30bb5bc0abe8 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 1 May 2024 23:00:24 +0300 Subject: [PATCH 0667/2493] Add failing test case --- .../Formats/LegacyScoreEncoderTest.cs | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyScoreEncoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyScoreEncoderTest.cs index 806f538249..1e57bd76cf 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyScoreEncoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyScoreEncoderTest.cs @@ -20,7 +20,7 @@ namespace osu.Game.Tests.Beatmaps.Formats [TestCase(1, 3)] [TestCase(1, 0)] [TestCase(0, 3)] - public void CatchMergesFruitAndDropletMisses(int missCount, int largeTickMissCount) + public void TestCatchMergesFruitAndDropletMisses(int missCount, int largeTickMissCount) { var ruleset = new CatchRuleset().RulesetInfo; var scoreInfo = TestResources.CreateTestScoreInfo(ruleset); @@ -41,7 +41,22 @@ namespace osu.Game.Tests.Beatmaps.Formats } [Test] - public void ScoreWithMissIsNotPerfect() + public void TestFailPreserved() + { + var ruleset = new OsuRuleset().RulesetInfo; + var scoreInfo = TestResources.CreateTestScoreInfo(); + var beatmap = new TestBeatmap(ruleset); + + scoreInfo.Rank = ScoreRank.F; + + var score = new Score { ScoreInfo = scoreInfo }; + var decodedAfterEncode = encodeThenDecode(LegacyBeatmapDecoder.LATEST_VERSION, score, beatmap); + + Assert.That(decodedAfterEncode.ScoreInfo.Rank, Is.EqualTo(ScoreRank.F)); + } + + [Test] + public void TestScoreWithMissIsNotPerfect() { var ruleset = new OsuRuleset().RulesetInfo; var scoreInfo = TestResources.CreateTestScoreInfo(ruleset); From 0106f1fe3ead4d5bdbef25c1e0262b3988131894 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 1 May 2024 23:31:23 +0300 Subject: [PATCH 0668/2493] Preserve score rank on encode/decode --- osu.Game/Scoring/Legacy/LegacyReplaySoloScoreInfo.cs | 6 ++++++ osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs | 5 +++++ 2 files changed, 11 insertions(+) diff --git a/osu.Game/Scoring/Legacy/LegacyReplaySoloScoreInfo.cs b/osu.Game/Scoring/Legacy/LegacyReplaySoloScoreInfo.cs index afdcef1d21..32c18b3af2 100644 --- a/osu.Game/Scoring/Legacy/LegacyReplaySoloScoreInfo.cs +++ b/osu.Game/Scoring/Legacy/LegacyReplaySoloScoreInfo.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Linq; using Newtonsoft.Json; +using Newtonsoft.Json.Converters; using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; using osu.Game.Rulesets.Scoring; @@ -38,6 +39,10 @@ namespace osu.Game.Scoring.Legacy [JsonProperty("client_version")] public string ClientVersion = string.Empty; + [JsonProperty("rank")] + [JsonConverter(typeof(StringEnumConverter))] + public ScoreRank? Rank; + public static LegacyReplaySoloScoreInfo FromScore(ScoreInfo score) => new LegacyReplaySoloScoreInfo { OnlineID = score.OnlineID, @@ -45,6 +50,7 @@ namespace osu.Game.Scoring.Legacy Statistics = score.Statistics.Where(kvp => kvp.Value != 0).ToDictionary(), MaximumStatistics = score.MaximumStatistics.Where(kvp => kvp.Value != 0).ToDictionary(), ClientVersion = score.ClientVersion, + Rank = score.Rank, }; } } diff --git a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs index 65e2c02655..b0d7087ed1 100644 --- a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs +++ b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs @@ -40,6 +40,7 @@ namespace osu.Game.Scoring.Legacy }; WorkingBeatmap workingBeatmap; + ScoreRank? decodedRank = null; using (SerializationReader sr = new SerializationReader(stream)) { @@ -129,6 +130,7 @@ namespace osu.Game.Scoring.Legacy score.ScoreInfo.MaximumStatistics = readScore.MaximumStatistics; score.ScoreInfo.Mods = readScore.Mods.Select(m => m.ToMod(currentRuleset)).ToArray(); score.ScoreInfo.ClientVersion = readScore.ClientVersion; + decodedRank = readScore.Rank; }); } } @@ -140,6 +142,9 @@ namespace osu.Game.Scoring.Legacy StandardisedScoreMigrationTools.UpdateFromLegacy(score.ScoreInfo, workingBeatmap); + if (decodedRank != null) + score.ScoreInfo.Rank = decodedRank.Value; + // 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 4ffeb5b469b86bdfbf80dde018fc3c9d32ec4e4b Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 1 May 2024 23:57:21 +0300 Subject: [PATCH 0669/2493] Resolve post-merge-conflict issues --- .../Visual/Gameplay/TestSceneDrawableStoryboardSprite.cs | 4 ++-- .../Visual/Gameplay/TestSceneStoryboardWithIntro.cs | 2 +- osu.Game/Storyboards/Drawables/DrawableStoryboardVideo.cs | 2 +- osu.Game/Storyboards/StoryboardVideo.cs | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableStoryboardSprite.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableStoryboardSprite.cs index dd6f833ec5..800857c973 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableStoryboardSprite.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableStoryboardSprite.cs @@ -85,8 +85,8 @@ namespace osu.Game.Tests.Visual.Gameplay if (scaleTransformProvided) { - sprite.TimelineGroup.Scale.Add(Easing.None, Time.Current, Time.Current + 1000, 1, 2); - sprite.TimelineGroup.Scale.Add(Easing.None, Time.Current + 1000, Time.Current + 2000, 2, 1); + sprite.Commands.AddScale(Easing.None, Time.Current, Time.Current + 1000, 1, 2); + sprite.Commands.AddScale(Easing.None, Time.Current + 1000, Time.Current + 2000, 2, 1); } layer.Elements.Clear(); diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardWithIntro.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardWithIntro.cs index 502a0de616..ee6a6938a9 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardWithIntro.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardWithIntro.cs @@ -39,7 +39,7 @@ namespace osu.Game.Tests.Visual.Gameplay { var storyboard = new Storyboard(); var sprite = new StoryboardSprite("unknown", Anchor.TopLeft, Vector2.Zero); - sprite.TimelineGroup.Alpha.Add(Easing.None, startTime, 0, 0, 1); + sprite.Commands.AddAlpha(Easing.None, startTime, 0, 0, 1); storyboard.GetLayer("Background").Add(sprite); return storyboard; } diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardVideo.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardVideo.cs index f2454be190..08c9459042 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardVideo.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardVideo.cs @@ -25,7 +25,7 @@ namespace osu.Game.Storyboards.Drawables // This allows scaling based on the video's absolute size. // // If not specified we take up the full available space. - bool useRelative = !video.TimelineGroup.Scale.HasCommands; + bool useRelative = !video.Commands.Scale.Any(); RelativeSizeAxes = useRelative ? Axes.Both : Axes.None; AutoSizeAxes = useRelative ? Axes.None : Axes.Both; diff --git a/osu.Game/Storyboards/StoryboardVideo.cs b/osu.Game/Storyboards/StoryboardVideo.cs index 5573162d26..14189a1a6c 100644 --- a/osu.Game/Storyboards/StoryboardVideo.cs +++ b/osu.Game/Storyboards/StoryboardVideo.cs @@ -14,7 +14,7 @@ namespace osu.Game.Storyboards { // This is just required to get a valid StartTime based on the incoming offset. // Actual fades are handled inside DrawableStoryboardVideo for now. - TimelineGroup.Alpha.Add(Easing.None, offset, offset, 0, 0); + Commands.AddAlpha(Easing.None, offset, offset, 0, 0); } public override Drawable CreateDrawable() => new DrawableStoryboardVideo(this); From 6d6f165884ab31f4aec606bae0ce6ee3eba4f9fd Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 2 May 2024 00:00:56 +0300 Subject: [PATCH 0670/2493] Make video sprites flippable and vector-scalable to fix type constraint errors Stable supports these on videos already from a quick read on code (since videos are generally a subclass of sprites). --- .../Drawables/DrawableStoryboardVideo.cs | 69 ++++++++++++++++++- 1 file changed, 67 insertions(+), 2 deletions(-) diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardVideo.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardVideo.cs index 08c9459042..329564a345 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardVideo.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardVideo.cs @@ -1,11 +1,16 @@ // 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 osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.Video; +using osu.Framework.Utils; +using osuTK; namespace osu.Game.Storyboards.Drawables { @@ -13,7 +18,7 @@ namespace osu.Game.Storyboards.Drawables { public readonly StoryboardVideo Video; - private Video? drawableVideo; + private DrawableVideo? drawableVideo; public override bool RemoveWhenNotAlive => false; @@ -42,7 +47,7 @@ namespace osu.Game.Storyboards.Drawables if (stream == null) return; - InternalChild = drawableVideo = new Video(stream, false) + InternalChild = drawableVideo = new DrawableVideo(stream, false) { RelativeSizeAxes = RelativeSizeAxes, FillMode = FillMode.Fill, @@ -70,5 +75,65 @@ namespace osu.Game.Storyboards.Drawables drawableVideo.FadeOut(500); } } + + private partial class DrawableVideo : Video, IFlippable, IVectorScalable + { + private bool flipH; + + public bool FlipH + { + get => flipH; + set + { + if (flipH == value) + return; + + flipH = value; + Invalidate(Invalidation.MiscGeometry); + } + } + + private bool flipV; + + public bool FlipV + { + get => flipV; + set + { + if (flipV == value) + return; + + flipV = value; + Invalidate(Invalidation.MiscGeometry); + } + } + + private Vector2 vectorScale = Vector2.One; + + public Vector2 VectorScale + { + get => vectorScale; + set + { + if (vectorScale == value) + return; + + if (!Validation.IsFinite(value)) throw new ArgumentException($@"{nameof(VectorScale)} must be finite, but is {value}."); + + vectorScale = value; + Invalidate(Invalidation.MiscGeometry); + } + } + + protected override Vector2 DrawScale + => new Vector2(FlipH ? -base.DrawScale.X : base.DrawScale.X, FlipV ? -base.DrawScale.Y : base.DrawScale.Y) * VectorScale; + + public override Anchor Origin => StoryboardExtensions.AdjustOrigin(base.Origin, VectorScale, FlipH, FlipV); + + public DrawableVideo(Stream stream, bool startAtCurrentTime = true) + : base(stream, startAtCurrentTime) + { + } + } } } From b1aff91bba0d9aefdfb09d0e40e0b79673a691f3 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 2 May 2024 00:31:48 +0300 Subject: [PATCH 0671/2493] Use throw helper methods MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bartłomiej Dach --- osu.Game/Storyboards/Commands/StoryboardLoopingGroup.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Storyboards/Commands/StoryboardLoopingGroup.cs b/osu.Game/Storyboards/Commands/StoryboardLoopingGroup.cs index a886998679..fe334ad608 100644 --- a/osu.Game/Storyboards/Commands/StoryboardLoopingGroup.cs +++ b/osu.Game/Storyboards/Commands/StoryboardLoopingGroup.cs @@ -23,7 +23,7 @@ namespace osu.Game.Storyboards.Commands /// The number of times the loop should repeat. Should be greater than zero. Zero means a single playback. public StoryboardLoopingGroup(double startTime, int repeatCount) { - if (repeatCount < 0) throw new ArgumentException("Repeat count must be zero or above.", nameof(repeatCount)); + ArgumentOutOfRangeException.ThrowIfNegative(repeatCount); loopStartTime = startTime; TotalIterations = repeatCount + 1; From 1eac34667d8c91679889475d17c8030e3bb85171 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 2 May 2024 08:02:55 +0200 Subject: [PATCH 0672/2493] Fix test failure --- osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs index 43e471320e..383c08c10f 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs @@ -352,6 +352,7 @@ namespace osu.Game.Tests.Beatmaps.Formats [HitResult.Great] = 200, [HitResult.LargeTickHit] = 1, }; + scoreInfo.Rank = ScoreRank.A; var beatmap = new TestBeatmap(ruleset); var score = new Score From f9ef689492775d7c419fc1640621fe6cdf9f79e1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 2 May 2024 15:36:40 +0800 Subject: [PATCH 0673/2493] Don't hide playfield layer with HUD --- osu.Game/Screens/Play/HUDOverlay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index 2ec2a011a6..9d7a05bc90 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -171,7 +171,7 @@ namespace osu.Game.Screens.Play }, }; - hideTargets = new List { mainComponents, playfieldComponents, topRightElements }; + hideTargets = new List { mainComponents, topRightElements }; if (rulesetComponents != null) hideTargets.Add(rulesetComponents); From 093c25539cd85911f4a06ba780e647737c7ac676 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 2 May 2024 13:59:40 +0200 Subject: [PATCH 0674/2493] Add failing test case --- .../Editor/TestSceneOsuEditorGrids.cs | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorGrids.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorGrids.cs index d14e593587..b720eb148b 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorGrids.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorGrids.cs @@ -5,6 +5,7 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Testing; using osu.Framework.Utils; +using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Osu.Edit; using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles; using osu.Game.Tests.Visual; @@ -54,6 +55,30 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor AddUntilStep("distance snap grid hidden", () => !this.ChildrenOfType().Any()); } + [Test] + public void TestDistanceSnapAdjustDoesNotHideTheGrid() + { + double distanceSnap = double.PositiveInfinity; + + AddStep("enable distance snap grid", () => InputManager.Key(Key.T)); + + AddStep("select second object", () => EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects.ElementAt(1))); + AddUntilStep("distance snap grid visible", () => this.ChildrenOfType().Any()); + AddStep("store distance snap", () => distanceSnap = this.ChildrenOfType().First().DistanceSpacingMultiplier.Value); + + AddStep("increase distance", () => + { + InputManager.PressKey(Key.AltLeft); + InputManager.PressKey(Key.ControlLeft); + InputManager.ScrollVerticalBy(1); + InputManager.ReleaseKey(Key.ControlLeft); + InputManager.ReleaseKey(Key.AltLeft); + }); + + AddUntilStep("distance snap increased", () => this.ChildrenOfType().First().DistanceSpacingMultiplier.Value, () => Is.GreaterThan(distanceSnap)); + AddUntilStep("distance snap grid still visible", () => this.ChildrenOfType().Any()); + } + [Test] public void TestGridSnapMomentaryToggle() { From 14658824e712a9aea3d1e6697d169a6e0b873201 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 2 May 2024 14:11:44 +0200 Subject: [PATCH 0675/2493] Adjust distance snap grid momentary toggle logic to not hide it on spacing adjust --- .../Rulesets/Edit/ComposerDistanceSnapProvider.cs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/Edit/ComposerDistanceSnapProvider.cs b/osu.Game/Rulesets/Edit/ComposerDistanceSnapProvider.cs index 62ad2ce7e9..b9850a94a3 100644 --- a/osu.Game/Rulesets/Edit/ComposerDistanceSnapProvider.cs +++ b/osu.Game/Rulesets/Edit/ComposerDistanceSnapProvider.cs @@ -63,6 +63,7 @@ namespace osu.Game.Rulesets.Edit public readonly Bindable DistanceSnapToggle = new Bindable(); private bool distanceSnapMomentary; + private TernaryState? distanceSnapStateBeforeMomentaryToggle; private EditorToolboxGroup? toolboxGroup; @@ -213,10 +214,19 @@ namespace osu.Game.Rulesets.Edit { bool altPressed = key.AltPressed; - if (altPressed != distanceSnapMomentary) + if (altPressed && !distanceSnapMomentary) { - distanceSnapMomentary = altPressed; + distanceSnapStateBeforeMomentaryToggle = DistanceSnapToggle.Value; DistanceSnapToggle.Value = DistanceSnapToggle.Value == TernaryState.False ? TernaryState.True : TernaryState.False; + distanceSnapMomentary = true; + } + + if (!altPressed && distanceSnapMomentary) + { + Debug.Assert(distanceSnapStateBeforeMomentaryToggle != null); + DistanceSnapToggle.Value = distanceSnapStateBeforeMomentaryToggle.Value; + distanceSnapStateBeforeMomentaryToggle = null; + distanceSnapMomentary = false; } } From 6000ffed2a7eb000efb4ea045055d620be2dcbbd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 2 May 2024 14:15:46 +0200 Subject: [PATCH 0676/2493] Add extended test coverage for intended behaviour --- .../Editor/TestSceneOsuEditorGrids.cs | 37 ++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorGrids.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorGrids.cs index b720eb148b..5798869210 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorGrids.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorGrids.cs @@ -53,10 +53,17 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor AddUntilStep("distance snap grid visible", () => this.ChildrenOfType().Any()); AddStep("release alt", () => InputManager.ReleaseKey(Key.AltLeft)); AddUntilStep("distance snap grid hidden", () => !this.ChildrenOfType().Any()); + + AddStep("enable distance snap grid", () => InputManager.Key(Key.T)); + AddUntilStep("distance snap grid visible", () => this.ChildrenOfType().Any()); + AddStep("hold alt", () => InputManager.PressKey(Key.AltLeft)); + AddUntilStep("distance snap grid hidden", () => !this.ChildrenOfType().Any()); + AddStep("release alt", () => InputManager.ReleaseKey(Key.AltLeft)); + AddUntilStep("distance snap grid visible", () => this.ChildrenOfType().Any()); } [Test] - public void TestDistanceSnapAdjustDoesNotHideTheGrid() + public void TestDistanceSnapAdjustDoesNotHideTheGridIfStartingEnabled() { double distanceSnap = double.PositiveInfinity; @@ -79,6 +86,34 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor AddUntilStep("distance snap grid still visible", () => this.ChildrenOfType().Any()); } + [Test] + public void TestDistanceSnapAdjustShowsGridMomentarilyIfStartingDisabled() + { + double distanceSnap = double.PositiveInfinity; + + AddStep("select second object", () => EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects.ElementAt(1))); + AddUntilStep("distance snap grid hidden", () => !this.ChildrenOfType().Any()); + AddStep("store distance snap", () => distanceSnap = this.ChildrenOfType().First().DistanceSpacingMultiplier.Value); + + AddStep("start increasing distance", () => + { + InputManager.PressKey(Key.AltLeft); + InputManager.PressKey(Key.ControlLeft); + }); + + AddUntilStep("distance snap grid visible", () => this.ChildrenOfType().Any()); + + AddStep("finish increasing distance", () => + { + InputManager.ScrollVerticalBy(1); + InputManager.ReleaseKey(Key.ControlLeft); + InputManager.ReleaseKey(Key.AltLeft); + }); + + AddUntilStep("distance snap increased", () => this.ChildrenOfType().First().DistanceSpacingMultiplier.Value, () => Is.GreaterThan(distanceSnap)); + AddUntilStep("distance snap hidden in the end", () => !this.ChildrenOfType().Any()); + } + [Test] public void TestGridSnapMomentaryToggle() { From 802666e621254fc4574f6c6db7fec2a1dd9f9548 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 2 May 2024 16:08:09 +0200 Subject: [PATCH 0677/2493] Update local metadata lookup cache more often As proposed in https://github.com/ppy/osu/issues/27332#issuecomment-1962308306. --- .../LocalCachedBeatmapMetadataSource.cs | 55 ++++++++++++++++--- 1 file changed, 48 insertions(+), 7 deletions(-) diff --git a/osu.Game/Beatmaps/LocalCachedBeatmapMetadataSource.cs b/osu.Game/Beatmaps/LocalCachedBeatmapMetadataSource.cs index 3f93c32283..27bc803449 100644 --- a/osu.Game/Beatmaps/LocalCachedBeatmapMetadataSource.cs +++ b/osu.Game/Beatmaps/LocalCachedBeatmapMetadataSource.cs @@ -44,11 +44,34 @@ namespace osu.Game.Beatmaps this.storage = storage; - // avoid downloading / using cache for unit tests. - if (!DebugUtils.IsNUnitRunning && !storage.Exists(cache_database_name)) + if (shouldFetchCache()) prepareLocalCache(); } + private bool shouldFetchCache() + { + // avoid downloading / using cache for unit tests. + if (DebugUtils.IsNUnitRunning) + return false; + + if (!storage.Exists(cache_database_name)) + { + log(@"Fetching local cache because it does not exist."); + return true; + } + + // periodically update the cache to include newer beatmaps. + var fileInfo = new FileInfo(storage.GetFullPath(cache_database_name)); + + if (fileInfo.LastWriteTime < DateTime.Now.AddMonths(-1)) + { + log($@"Refetching local cache because it was last written to on {fileInfo.LastWriteTime}."); + return true; + } + + return false; + } + public bool Available => // no download in progress. cacheDownloadRequest == null @@ -124,6 +147,8 @@ namespace osu.Game.Beatmaps private void prepareLocalCache() { + bool isRefetch = storage.Exists(cache_database_name); + string cacheFilePath = storage.GetFullPath(cache_database_name); string compressedCacheFilePath = $@"{cacheFilePath}.bz2"; @@ -132,9 +157,15 @@ namespace osu.Game.Beatmaps cacheDownloadRequest.Failed += ex => { File.Delete(compressedCacheFilePath); - File.Delete(cacheFilePath); - Logger.Log($@"{nameof(BeatmapUpdaterMetadataLookup)}'s online cache download failed: {ex}", LoggingTarget.Database); + // don't clobber the cache when refetching if the download didn't succeed. seems excessive. + // consequently, also null the download request to allow the existing cache to be used (see `Available`). + if (isRefetch) + cacheDownloadRequest = null; + else + File.Delete(cacheFilePath); + + log($@"Online cache download failed: {ex}"); }; cacheDownloadRequest.Finished += () => @@ -143,15 +174,22 @@ namespace osu.Game.Beatmaps { using (var stream = File.OpenRead(cacheDownloadRequest.Filename)) using (var outStream = File.OpenWrite(cacheFilePath)) - using (var bz2 = new BZip2Stream(stream, CompressionMode.Decompress, false)) - bz2.CopyTo(outStream); + { + // ensure to clobber any and all existing data to avoid accidental corruption. + outStream.SetLength(0); + + using (var bz2 = new BZip2Stream(stream, CompressionMode.Decompress, false)) + bz2.CopyTo(outStream); + } // set to null on completion to allow lookups to begin using the new source cacheDownloadRequest = null; + log(@"Local cache fetch completed successfully."); } catch (Exception ex) { - Logger.Log($@"{nameof(LocalCachedBeatmapMetadataSource)}'s online cache extraction failed: {ex}", LoggingTarget.Database); + log($@"Online cache extraction failed: {ex}"); + // at this point clobber the cache regardless of whether we're refetching, because by this point who knows what state the cache file is in. File.Delete(cacheFilePath); } finally @@ -173,6 +211,9 @@ namespace osu.Game.Beatmaps }); } + private static void log(string message) + => Logger.Log($@"[{nameof(LocalCachedBeatmapMetadataSource)}] {message}", LoggingTarget.Database); + private void logForModel(BeatmapSetInfo set, string message) => RealmArchiveModelImporter.LogForModel(set, $@"[{nameof(LocalCachedBeatmapMetadataSource)}] {message}"); From f534c4aadab0a274674bd5b077e4acea797f177e Mon Sep 17 00:00:00 2001 From: Fabian van Oeffelt Date: Thu, 2 May 2024 18:42:35 +0200 Subject: [PATCH 0678/2493] Initial implementation --- .../Input/Bindings/GlobalActionContainer.cs | 8 ++ .../GlobalActionKeyBindingStrings.cs | 10 ++ osu.Game/Screens/Select/SongSelect.cs | 97 ++++++++++++++++++- 3 files changed, 113 insertions(+), 2 deletions(-) diff --git a/osu.Game/Input/Bindings/GlobalActionContainer.cs b/osu.Game/Input/Bindings/GlobalActionContainer.cs index 296232d9ea..ff7d9f0a6d 100644 --- a/osu.Game/Input/Bindings/GlobalActionContainer.cs +++ b/osu.Game/Input/Bindings/GlobalActionContainer.cs @@ -134,6 +134,8 @@ namespace osu.Game.Input.Bindings new KeyBinding(new[] { InputKey.Shift, InputKey.F2 }, GlobalAction.SelectPreviousRandom), new KeyBinding(InputKey.F3, GlobalAction.ToggleBeatmapOptions), new KeyBinding(InputKey.BackSpace, GlobalAction.DeselectAllMods), + new KeyBinding(InputKey.PageUp, GlobalAction.IncreaseSpeed), // Not working with minus and other keys.... + new KeyBinding(InputKey.PageDown, GlobalAction.DecreaseSpeed), }; public IEnumerable AudioControlKeyBindings => new[] @@ -364,5 +366,11 @@ namespace osu.Game.Input.Bindings [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorToggleRotateControl))] EditorToggleRotateControl, + + [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.IncreaseSpeed))] + IncreaseSpeed, + + [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.DecreaseSpeed))] + DecreaseSpeed, } } diff --git a/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs b/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs index 8356c480dd..40fe4064ed 100644 --- a/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs +++ b/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs @@ -349,6 +349,16 @@ namespace osu.Game.Localisation /// public static LocalisableString EditorToggleRotateControl => new TranslatableString(getKey(@"editor_toggle_rotate_control"), @"Toggle rotate control"); + /// + /// "Increase Speed" + /// + public static LocalisableString IncreaseSpeed => new TranslatableString(getKey(@"increase_speed"), @"Increase Speed"); + + /// + /// "Decrease Speed" + /// + public static LocalisableString DecreaseSpeed => new TranslatableString(getKey(@"decrease_speed"), @"Decrease Speed"); + private static string getKey(string key) => $@"{prefix}:{key}"; } } diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index 34ee0ae4e8..f2036f017d 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -755,7 +755,95 @@ namespace osu.Game.Screens.Select return false; } - + public void IncreaseSpeed() + { + // find way of grabbing all types of ModSpeedAdjust + var rateAdjustStates = ModSelect.AllAvailableMods.Where(pair => pair.Mod.Acronym == "DT" || pair.Mod.Acronym == "NC" || pair.Mod.Acronym == "HT" || pair.Mod.Acronym == "DC"); + var stateDoubleTime = ModSelect.AllAvailableMods.First(pair => pair.Mod.Acronym == "DT"); + bool oneActive = false; + double newRate = 1.05d; + foreach (var state in rateAdjustStates) + { + ModRateAdjust mod = (ModRateAdjust)state.Mod; + if (state.Active.Value) + { + oneActive = true; + newRate = mod.SpeedChange.Value + 0.05d; + if (mod.Acronym == "DT" || mod.Acronym == "NC") + { + mod.SpeedChange.Value = newRate; + return; + } + else + { + if (newRate == 1.0d) + { + state.Active.Value = false; + } + if (newRate > 1d) + { + state.Active.Value = false; + stateDoubleTime.Active.Value = true; + ((ModDoubleTime)stateDoubleTime.Mod).SpeedChange.Value = newRate; + return; + } + if (newRate < 1d) + { + mod.SpeedChange.Value = newRate; + } + } + } + } + if (!oneActive) + { + stateDoubleTime.Active.Value = true; + ((ModDoubleTime)stateDoubleTime.Mod).SpeedChange.Value = newRate; + } + } + public void DecreaseSpeed() + { + var rateAdjustStates = ModSelect.AllAvailableMods.Where(pair => pair.Mod.Acronym == "DT" || pair.Mod.Acronym == "NC" || pair.Mod.Acronym == "HT" || pair.Mod.Acronym == "DC"); + var stateHalfTime = ModSelect.AllAvailableMods.First(pair => pair.Mod.Acronym == "HT"); + bool oneActive = false; + double newRate = 0.95d; + foreach (var state in rateAdjustStates) + { + ModRateAdjust mod = (ModRateAdjust)state.Mod; + if (state.Active.Value) + { + oneActive = true; + newRate = mod.SpeedChange.Value - 0.05d; + if (mod.Acronym == "HT" || mod.Acronym == "DC") + { + mod.SpeedChange.Value = newRate; + return; + } + else + { + if (newRate == 1.0d) + { + state.Active.Value = false; + } + if (newRate < 1d) + { + state.Active.Value = false; + stateHalfTime.Active.Value = true; + ((ModHalfTime)stateHalfTime.Mod).SpeedChange.Value = newRate; + return; + } + if (newRate > 1d) + { + mod.SpeedChange.Value = newRate; + } + } + } + } + if (!oneActive) + { + stateHalfTime.Active.Value = true; + ((ModHalfTime)stateHalfTime.Mod).SpeedChange.Value = newRate; + } + } protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); @@ -955,12 +1043,17 @@ namespace osu.Game.Screens.Select return false; if (!this.IsCurrentScreen()) return false; - switch (e.Action) { case GlobalAction.Select: FinaliseSelection(); return true; + case GlobalAction.IncreaseSpeed: + IncreaseSpeed(); + return true; + case GlobalAction.DecreaseSpeed: + DecreaseSpeed(); + return true; } return false; From 5c21a0330addcf81db97e7ec157510d894cbd6a0 Mon Sep 17 00:00:00 2001 From: Fabian van Oeffelt Date: Thu, 2 May 2024 19:05:07 +0200 Subject: [PATCH 0679/2493] F1 also does not work with minus in song select, same behaviour --- 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 ff7d9f0a6d..204ecb9061 100644 --- a/osu.Game/Input/Bindings/GlobalActionContainer.cs +++ b/osu.Game/Input/Bindings/GlobalActionContainer.cs @@ -134,7 +134,7 @@ namespace osu.Game.Input.Bindings new KeyBinding(new[] { InputKey.Shift, InputKey.F2 }, GlobalAction.SelectPreviousRandom), new KeyBinding(InputKey.F3, GlobalAction.ToggleBeatmapOptions), new KeyBinding(InputKey.BackSpace, GlobalAction.DeselectAllMods), - new KeyBinding(InputKey.PageUp, GlobalAction.IncreaseSpeed), // Not working with minus and other keys.... + new KeyBinding(InputKey.PageUp, GlobalAction.IncreaseSpeed), new KeyBinding(InputKey.PageDown, GlobalAction.DecreaseSpeed), }; From 7527ddbc6865c35e26a7c4deef850e0d4e93d786 Mon Sep 17 00:00:00 2001 From: Fabian van Oeffelt Date: Thu, 2 May 2024 19:05:43 +0200 Subject: [PATCH 0680/2493] Comment, make code more readable, functions are now private --- osu.Game/Screens/Select/SongSelect.cs | 133 ++++++++++++-------------- 1 file changed, 60 insertions(+), 73 deletions(-) diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index f2036f017d..5de4a7a9a3 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -755,93 +755,80 @@ namespace osu.Game.Screens.Select return false; } - public void IncreaseSpeed() + private void increaseSpeed() { - // find way of grabbing all types of ModSpeedAdjust - var rateAdjustStates = ModSelect.AllAvailableMods.Where(pair => pair.Mod.Acronym == "DT" || pair.Mod.Acronym == "NC" || pair.Mod.Acronym == "HT" || pair.Mod.Acronym == "DC"); - var stateDoubleTime = ModSelect.AllAvailableMods.First(pair => pair.Mod.Acronym == "DT"); - bool oneActive = false; - double newRate = 1.05d; - foreach (var state in rateAdjustStates) - { - ModRateAdjust mod = (ModRateAdjust)state.Mod; - if (state.Active.Value) - { - oneActive = true; - newRate = mod.SpeedChange.Value + 0.05d; - if (mod.Acronym == "DT" || mod.Acronym == "NC") - { - mod.SpeedChange.Value = newRate; - return; - } - else - { - if (newRate == 1.0d) - { - state.Active.Value = false; - } - if (newRate > 1d) - { - state.Active.Value = false; - stateDoubleTime.Active.Value = true; - ((ModDoubleTime)stateDoubleTime.Mod).SpeedChange.Value = newRate; - return; - } - if (newRate < 1d) - { - mod.SpeedChange.Value = newRate; - } - } - } - } - if (!oneActive) + var rateAdjustStates = ModSelect.AllAvailableMods.Where(pair => pair.Mod is ModRateAdjust); + var stateDoubleTime = ModSelect.AllAvailableMods.First(pair => pair.Mod is ModDoubleTime); + bool rateModActive = ModSelect.AllAvailableMods.Where(pair => pair.Mod is ModRateAdjust && pair.Active.Value).Count() > 0; + double stepSize = 0.05d; + double newRate = 1d + stepSize; + // If no mod rateAdjust mod is currently active activate DoubleTime with speed newRate + if (!rateModActive) { stateDoubleTime.Active.Value = true; ((ModDoubleTime)stateDoubleTime.Mod).SpeedChange.Value = newRate; + return; } - } - public void DecreaseSpeed() - { - var rateAdjustStates = ModSelect.AllAvailableMods.Where(pair => pair.Mod.Acronym == "DT" || pair.Mod.Acronym == "NC" || pair.Mod.Acronym == "HT" || pair.Mod.Acronym == "DC"); - var stateHalfTime = ModSelect.AllAvailableMods.First(pair => pair.Mod.Acronym == "HT"); - bool oneActive = false; - double newRate = 0.95d; + // Find current active rateAdjust mod and modify speed, enable DoubleTime if necessary foreach (var state in rateAdjustStates) { ModRateAdjust mod = (ModRateAdjust)state.Mod; - if (state.Active.Value) + if (!state.Active.Value) continue; + newRate = mod.SpeedChange.Value + stepSize; + if (mod.Acronym == "DT" || mod.Acronym == "NC") + mod.SpeedChange.Value = newRate; + else { - oneActive = true; - newRate = mod.SpeedChange.Value - 0.05d; - if (mod.Acronym == "HT" || mod.Acronym == "DC") + if (newRate == 1.0d) + state.Active.Value = false; + if (newRate > 1d) { + state.Active.Value = false; + stateDoubleTime.Active.Value = true; + ((ModDoubleTime)stateDoubleTime.Mod).SpeedChange.Value = newRate; + break; + } + if (newRate < 1d) mod.SpeedChange.Value = newRate; - return; - } - else - { - if (newRate == 1.0d) - { - state.Active.Value = false; - } - if (newRate < 1d) - { - state.Active.Value = false; - stateHalfTime.Active.Value = true; - ((ModHalfTime)stateHalfTime.Mod).SpeedChange.Value = newRate; - return; - } - if (newRate > 1d) - { - mod.SpeedChange.Value = newRate; - } - } } } - if (!oneActive) + } + private void decreaseSpeed() + { + var rateAdjustStates = ModSelect.AllAvailableMods.Where(pair => pair.Mod is ModRateAdjust); + var stateHalfTime = ModSelect.AllAvailableMods.First(pair => pair.Mod is ModHalfTime); + bool rateModActive = ModSelect.AllAvailableMods.Where(pair => pair.Mod is ModRateAdjust && pair.Active.Value).Count() > 0; + double stepSize = 0.05d; + double newRate = 1d - stepSize; + // If no mod rateAdjust mod is currently active activate HalfTime with speed newRate + if (!rateModActive) { stateHalfTime.Active.Value = true; ((ModHalfTime)stateHalfTime.Mod).SpeedChange.Value = newRate; + return; + } + // Find current active rateAdjust mod and modify speed, enable HalfTime if necessary + foreach (var state in rateAdjustStates) + { + ModRateAdjust mod = (ModRateAdjust)state.Mod; + if (!state.Active.Value) continue; + newRate = mod.SpeedChange.Value - stepSize; + if (mod.Acronym == "HT" || mod.Acronym == "DC") + mod.SpeedChange.Value = newRate; + else + { + if (newRate == 1.0d) + state.Active.Value = false; + if (newRate < 1d) + { + state.Active.Value = false; + stateHalfTime.Active.Value = true; + ((ModHalfTime)stateHalfTime.Mod).SpeedChange.Value = newRate; + break; + } + if (newRate > 1d) + mod.SpeedChange.Value = newRate; + } } } protected override void Dispose(bool isDisposing) @@ -1049,10 +1036,10 @@ namespace osu.Game.Screens.Select FinaliseSelection(); return true; case GlobalAction.IncreaseSpeed: - IncreaseSpeed(); + increaseSpeed(); return true; case GlobalAction.DecreaseSpeed: - DecreaseSpeed(); + decreaseSpeed(); return true; } From 588badf29216782699ae359700d6449e82638187 Mon Sep 17 00:00:00 2001 From: Fabian van Oeffelt Date: Thu, 2 May 2024 19:22:39 +0200 Subject: [PATCH 0681/2493] Fix Formatting --- 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 f6bc002e32..5dacb6db4d 100644 --- a/osu.Game/Input/Bindings/GlobalActionContainer.cs +++ b/osu.Game/Input/Bindings/GlobalActionContainer.cs @@ -416,7 +416,7 @@ namespace osu.Game.Input.Bindings [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.DecreaseSpeed))] DecreaseSpeed, - + [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.IncreaseOffset))] IncreaseOffset, From 4b5ea6bd0bc46cf37612efa0de3c81f3d4fe52bc Mon Sep 17 00:00:00 2001 From: Fabian van Oeffelt Date: Thu, 2 May 2024 19:41:00 +0200 Subject: [PATCH 0682/2493] Fix Code Inspection --- osu.Game/Screens/Select/SongSelect.cs | 36 +++++++++++++++++++++------ 1 file changed, 28 insertions(+), 8 deletions(-) diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index 73ce029d3f..de0f24aa90 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -808,13 +808,15 @@ namespace osu.Game.Screens.Select return false; } + private void increaseSpeed() { var rateAdjustStates = ModSelect.AllAvailableMods.Where(pair => pair.Mod is ModRateAdjust); var stateDoubleTime = ModSelect.AllAvailableMods.First(pair => pair.Mod is ModDoubleTime); - bool rateModActive = ModSelect.AllAvailableMods.Where(pair => pair.Mod is ModRateAdjust && pair.Active.Value).Count() > 0; - double stepSize = 0.05d; - double newRate = 1d + stepSize; + bool rateModActive = ModSelect.AllAvailableMods.Count(pair => pair.Mod is ModRateAdjust && pair.Active.Value) > 0; + const double stepsize = 0.05d; + double newRate = 1d + stepsize; + // If no mod rateAdjust mod is currently active activate DoubleTime with speed newRate if (!rateModActive) { @@ -822,18 +824,23 @@ namespace osu.Game.Screens.Select ((ModDoubleTime)stateDoubleTime.Mod).SpeedChange.Value = newRate; return; } + // Find current active rateAdjust mod and modify speed, enable DoubleTime if necessary foreach (var state in rateAdjustStates) { ModRateAdjust mod = (ModRateAdjust)state.Mod; + if (!state.Active.Value) continue; - newRate = mod.SpeedChange.Value + stepSize; + + newRate = mod.SpeedChange.Value + stepsize; + if (mod.Acronym == "DT" || mod.Acronym == "NC") mod.SpeedChange.Value = newRate; else { if (newRate == 1.0d) state.Active.Value = false; + if (newRate > 1d) { state.Active.Value = false; @@ -841,18 +848,21 @@ namespace osu.Game.Screens.Select ((ModDoubleTime)stateDoubleTime.Mod).SpeedChange.Value = newRate; break; } + if (newRate < 1d) mod.SpeedChange.Value = newRate; } } } + private void decreaseSpeed() { var rateAdjustStates = ModSelect.AllAvailableMods.Where(pair => pair.Mod is ModRateAdjust); var stateHalfTime = ModSelect.AllAvailableMods.First(pair => pair.Mod is ModHalfTime); - bool rateModActive = ModSelect.AllAvailableMods.Where(pair => pair.Mod is ModRateAdjust && pair.Active.Value).Count() > 0; - double stepSize = 0.05d; - double newRate = 1d - stepSize; + bool rateModActive = ModSelect.AllAvailableMods.Count(pair => pair.Mod is ModRateAdjust && pair.Active.Value) > 0; + const double stepsize = 0.05d; + double newRate = 1d - stepsize; + // If no mod rateAdjust mod is currently active activate HalfTime with speed newRate if (!rateModActive) { @@ -860,18 +870,23 @@ namespace osu.Game.Screens.Select ((ModHalfTime)stateHalfTime.Mod).SpeedChange.Value = newRate; return; } + // Find current active rateAdjust mod and modify speed, enable HalfTime if necessary foreach (var state in rateAdjustStates) { ModRateAdjust mod = (ModRateAdjust)state.Mod; + if (!state.Active.Value) continue; - newRate = mod.SpeedChange.Value - stepSize; + + newRate = mod.SpeedChange.Value - stepsize; + if (mod.Acronym == "HT" || mod.Acronym == "DC") mod.SpeedChange.Value = newRate; else { if (newRate == 1.0d) state.Active.Value = false; + if (newRate < 1d) { state.Active.Value = false; @@ -879,11 +894,13 @@ namespace osu.Game.Screens.Select ((ModHalfTime)stateHalfTime.Mod).SpeedChange.Value = newRate; break; } + if (newRate > 1d) mod.SpeedChange.Value = newRate; } } } + protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); @@ -1086,14 +1103,17 @@ namespace osu.Game.Screens.Select return false; if (!this.IsCurrentScreen()) return false; + switch (e.Action) { case GlobalAction.Select: FinaliseSelection(); return true; + case GlobalAction.IncreaseSpeed: increaseSpeed(); return true; + case GlobalAction.DecreaseSpeed: decreaseSpeed(); return true; From 2f075e32479542e8b3dc0abd0ae3c93c4f0dae9f Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Thu, 2 May 2024 17:01:40 -0700 Subject: [PATCH 0683/2493] Apply half margin of tolerance on both sides before text scrolls --- .../TestSceneNowPlayingOverlay.cs | 22 ++++++++++++++----- osu.Game/Overlays/NowPlayingOverlay.cs | 4 +++- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneNowPlayingOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneNowPlayingOverlay.cs index 40e0d9250d..1670741cbd 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneNowPlayingOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneNowPlayingOverlay.cs @@ -45,16 +45,28 @@ namespace osu.Game.Tests.Visual.UserInterface [Test] public void TestLongMetadata() { - AddStep(@"set beatmap", () => Beatmap.Value = CreateWorkingBeatmap(new Beatmap + AddStep(@"set metadata within tolerance", () => Beatmap.Value = CreateWorkingBeatmap(new Beatmap { Metadata = { - Artist = "very very very very very very very very very very very long artist", - ArtistUnicode = "very very very very very very very very very very very long artist", - Title = "very very very very very very very very very very very long title", - TitleUnicode = "very very very very very very very very very very very long title", + Artist = "very very very very very very very very very very verry long artist", + ArtistUnicode = "very very very very very very very very very very verry long artist", + Title = "very very very very very verry long title", + TitleUnicode = "very very very very very verry long title", } })); + + AddStep(@"set metadata outside bounds", () => Beatmap.Value = CreateWorkingBeatmap(new Beatmap + { + Metadata = + { + Artist = "very very very very very very very very very very verrry long artist", + ArtistUnicode = "very very very very very very very very very very verrry long artist", + Title = "very very very very very verrry long title", + TitleUnicode = "very very very very very verrry long title", + } + })); + AddStep(@"show", () => nowPlayingOverlay.Show()); } } diff --git a/osu.Game/Overlays/NowPlayingOverlay.cs b/osu.Game/Overlays/NowPlayingOverlay.cs index be405257ca..b1f72fd792 100644 --- a/osu.Game/Overlays/NowPlayingOverlay.cs +++ b/osu.Game/Overlays/NowPlayingOverlay.cs @@ -9,6 +9,7 @@ using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Shapes; @@ -533,7 +534,8 @@ namespace osu.Game.Overlays float textOverflowWidth = mainSpriteText.Width - player_width; - if (textOverflowWidth > 0) + // apply half margin of tolerance on both sides before the text scrolls + if (textOverflowWidth > margin) { fillerSpriteText.Alpha = 1; fillerSpriteText.Text = text; From 269077f85496142f470b3fce4306ac33ac8216ea Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Thu, 2 May 2024 17:04:22 -0700 Subject: [PATCH 0684/2493] Only support centre anchors --- osu.Game/Overlays/NowPlayingOverlay.cs | 20 ++------------------ 1 file changed, 2 insertions(+), 18 deletions(-) diff --git a/osu.Game/Overlays/NowPlayingOverlay.cs b/osu.Game/Overlays/NowPlayingOverlay.cs index b1f72fd792..7329a3b404 100644 --- a/osu.Game/Overlays/NowPlayingOverlay.cs +++ b/osu.Game/Overlays/NowPlayingOverlay.cs @@ -540,24 +540,8 @@ namespace osu.Game.Overlays fillerSpriteText.Alpha = 1; fillerSpriteText.Text = text; - float initialX; - float targetX; - - if (Anchor.HasFlagFast(Anchor.x0)) - { - initialX = 0; - targetX = -mainSpriteText.Width; - } - else if (Anchor.HasFlagFast(Anchor.x1)) - { - initialX = (textOverflowWidth + mainSpriteText.Width) / 2; - targetX = (textOverflowWidth - mainSpriteText.Width) / 2; - } - else // Anchor.x2 - { - initialX = textOverflowWidth + mainSpriteText.Width; - targetX = textOverflowWidth; - } + float initialX = (textOverflowWidth + mainSpriteText.Width) / 2; + float targetX = (textOverflowWidth - mainSpriteText.Width) / 2; this.MoveToX(initialX) .Delay(initial_move_delay) From 381ddb067618abb7b7cfceed944669e346b1bdf3 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Thu, 2 May 2024 17:05:12 -0700 Subject: [PATCH 0685/2493] Fix weird formatting --- osu.Game/Overlays/NowPlayingOverlay.cs | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/osu.Game/Overlays/NowPlayingOverlay.cs b/osu.Game/Overlays/NowPlayingOverlay.cs index 7329a3b404..1488f59246 100644 --- a/osu.Game/Overlays/NowPlayingOverlay.cs +++ b/osu.Game/Overlays/NowPlayingOverlay.cs @@ -494,14 +494,13 @@ namespace osu.Game.Overlays public FontUsage Font { - set => - Schedule(() => - { - mainSpriteText.Font = value; - fillerSpriteText.Font = value; + set => Schedule(() => + { + mainSpriteText.Font = value; + fillerSpriteText.Font = value; - updateText(); - }); + updateText(); + }); } public ScrollingTextContainer() From c15a68507152b0710b90a4fa947cb5b994258bf9 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Thu, 2 May 2024 17:07:49 -0700 Subject: [PATCH 0686/2493] Remove unused usings --- osu.Game/Overlays/NowPlayingOverlay.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game/Overlays/NowPlayingOverlay.cs b/osu.Game/Overlays/NowPlayingOverlay.cs index 1488f59246..8f97ce50cc 100644 --- a/osu.Game/Overlays/NowPlayingOverlay.cs +++ b/osu.Game/Overlays/NowPlayingOverlay.cs @@ -6,10 +6,8 @@ using System.Threading; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; -using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; -using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Shapes; From aa4d16bdb873d7296b899cee7b7491ffdf5cd6ab Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 3 May 2024 12:13:52 +0800 Subject: [PATCH 0687/2493] Fix beatmap listing cards being far too large --- osu.Game/Overlays/BeatmapListingOverlay.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Overlays/BeatmapListingOverlay.cs b/osu.Game/Overlays/BeatmapListingOverlay.cs index a645683c5f..9b2f26e8ae 100644 --- a/osu.Game/Overlays/BeatmapListingOverlay.cs +++ b/osu.Game/Overlays/BeatmapListingOverlay.cs @@ -198,6 +198,7 @@ namespace osu.Game.Overlays { c.Anchor = Anchor.TopCentre; c.Origin = Anchor.TopCentre; + c.Scale = new Vector2(0.8f); })).ToArray(); private static ReverseChildIDFillFlowContainer createCardContainerFor(IEnumerable newCards) From 3249ecee2731f49f95d8095e4bb96371d301e4a3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 3 May 2024 12:31:19 +0800 Subject: [PATCH 0688/2493] Fix chat overlay being far too large --- osu.Game/Overlays/Chat/ChannelList/ChannelListItem.cs | 4 ++-- osu.Game/Overlays/Chat/ChatLine.cs | 2 +- osu.Game/Overlays/Chat/ChatTextBar.cs | 9 ++++++--- osu.Game/Overlays/ChatOverlay.cs | 3 +-- 4 files changed, 10 insertions(+), 8 deletions(-) diff --git a/osu.Game/Overlays/Chat/ChannelList/ChannelListItem.cs b/osu.Game/Overlays/Chat/ChannelList/ChannelListItem.cs index 87b1f4ef01..e8c251e7fd 100644 --- a/osu.Game/Overlays/Chat/ChannelList/ChannelListItem.cs +++ b/osu.Game/Overlays/Chat/ChannelList/ChannelListItem.cs @@ -49,7 +49,7 @@ namespace osu.Game.Overlays.Chat.ChannelList [BackgroundDependencyLoader] private void load() { - Height = 30; + Height = 25; RelativeSizeAxes = Axes.X; Children = new Drawable[] @@ -87,7 +87,7 @@ namespace osu.Game.Overlays.Chat.ChannelList Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, Text = Channel.Name, - Font = OsuFont.Torus.With(size: 17, weight: FontWeight.SemiBold), + Font = OsuFont.Torus.With(size: 14, weight: FontWeight.SemiBold), Colour = colourProvider.Light3, Margin = new MarginPadding { Bottom = 2 }, RelativeSizeAxes = Axes.X, diff --git a/osu.Game/Overlays/Chat/ChatLine.cs b/osu.Game/Overlays/Chat/ChatLine.cs index bbc3ee5bf4..9bcca3ac9d 100644 --- a/osu.Game/Overlays/Chat/ChatLine.cs +++ b/osu.Game/Overlays/Chat/ChatLine.cs @@ -47,7 +47,7 @@ namespace osu.Game.Overlays.Chat public IReadOnlyCollection DrawableContentFlow => drawableContentFlow; - protected virtual float FontSize => 20; + protected virtual float FontSize => 14; protected virtual float Spacing => 15; diff --git a/osu.Game/Overlays/Chat/ChatTextBar.cs b/osu.Game/Overlays/Chat/ChatTextBar.cs index 16a8d14b10..0a42363279 100644 --- a/osu.Game/Overlays/Chat/ChatTextBar.cs +++ b/osu.Game/Overlays/Chat/ChatTextBar.cs @@ -19,6 +19,8 @@ namespace osu.Game.Overlays.Chat { public partial class ChatTextBar : Container { + public const float HEIGHT = 40; + public readonly BindableBool ShowSearch = new BindableBool(); public event Action? OnChatMessageCommitted; @@ -45,7 +47,7 @@ namespace osu.Game.Overlays.Chat private void load(OverlayColourProvider colourProvider) { RelativeSizeAxes = Axes.X; - Height = 60; + Height = HEIGHT; Children = new Drawable[] { @@ -76,7 +78,7 @@ namespace osu.Game.Overlays.Chat Child = chattingText = new TruncatingSpriteText { MaxWidth = chatting_text_width - padding * 2, - Font = OsuFont.Torus.With(size: 20), + Font = OsuFont.Torus, Colour = colourProvider.Background1, Anchor = Anchor.CentreRight, Origin = Anchor.CentreRight, @@ -91,7 +93,7 @@ namespace osu.Game.Overlays.Chat Icon = FontAwesome.Solid.Search, Origin = Anchor.CentreRight, Anchor = Anchor.CentreRight, - Size = new Vector2(20), + Size = new Vector2(OsuFont.DEFAULT_FONT_SIZE), Margin = new MarginPadding { Right = 2 }, }, }, @@ -101,6 +103,7 @@ namespace osu.Game.Overlays.Chat Padding = new MarginPadding { Right = padding }, Child = chatTextBox = new ChatTextBox { + FontSize = OsuFont.DEFAULT_FONT_SIZE, Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, RelativeSizeAxes = Axes.X, diff --git a/osu.Game/Overlays/ChatOverlay.cs b/osu.Game/Overlays/ChatOverlay.cs index 8f3b7031c2..b11483e678 100644 --- a/osu.Game/Overlays/ChatOverlay.cs +++ b/osu.Game/Overlays/ChatOverlay.cs @@ -55,7 +55,6 @@ namespace osu.Game.Overlays private const int transition_length = 500; private const float top_bar_height = 40; private const float side_bar_width = 190; - private const float chat_bar_height = 60; protected override string PopInSampleName => @"UI/overlay-big-pop-in"; protected override string PopOutSampleName => @"UI/overlay-big-pop-out"; @@ -136,7 +135,7 @@ namespace osu.Game.Overlays Padding = new MarginPadding { Left = side_bar_width, - Bottom = chat_bar_height, + Bottom = ChatTextBar.HEIGHT, }, Children = new Drawable[] { From 058bd5ced66d8d6937f77d41f3e4704089177530 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 3 May 2024 13:38:27 +0800 Subject: [PATCH 0689/2493] Stop using visually noisy `bg4` for default backgrounds This has always really annoyed me. --- osu.Game/Beatmaps/DummyWorkingBeatmap.cs | 2 +- osu.Game/Overlays/NowPlayingOverlay.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Beatmaps/DummyWorkingBeatmap.cs b/osu.Game/Beatmaps/DummyWorkingBeatmap.cs index d254945a51..35067f4055 100644 --- a/osu.Game/Beatmaps/DummyWorkingBeatmap.cs +++ b/osu.Game/Beatmaps/DummyWorkingBeatmap.cs @@ -53,7 +53,7 @@ namespace osu.Game.Beatmaps protected override IBeatmap GetBeatmap() => new Beatmap(); - public override Texture GetBackground() => textures?.Get(@"Backgrounds/bg4"); + public override Texture GetBackground() => textures?.Get(@"Backgrounds/bg2"); protected override Track GetBeatmapTrack() => GetVirtualTrack(); diff --git a/osu.Game/Overlays/NowPlayingOverlay.cs b/osu.Game/Overlays/NowPlayingOverlay.cs index ab99370603..7e4f4f4e9b 100644 --- a/osu.Game/Overlays/NowPlayingOverlay.cs +++ b/osu.Game/Overlays/NowPlayingOverlay.cs @@ -422,7 +422,7 @@ namespace osu.Game.Overlays [BackgroundDependencyLoader] private void load(LargeTextureStore textures) { - sprite.Texture = beatmap.GetBackground() ?? textures.Get(@"Backgrounds/bg4"); + sprite.Texture = beatmap.GetBackground() ?? textures.Get(@"Backgrounds/bg2"); } } From c1e9b6d4cae0793de9bc15cc055808db4995b1a3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 3 May 2024 13:42:56 +0800 Subject: [PATCH 0690/2493] Fix beatmap backgrounds loading default briefly before final display Due to the way `ModelBackedDrawable` works, the default starts to get loaded even though a final `Beatmap` has been set. This avoids loading the default fallback unless a beatmap has been set (and has no background itself). --- .../Beatmaps/Drawables/UpdateableBeatmapBackgroundSprite.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Beatmaps/Drawables/UpdateableBeatmapBackgroundSprite.cs b/osu.Game/Beatmaps/Drawables/UpdateableBeatmapBackgroundSprite.cs index 0bb60847e5..f067af5360 100644 --- a/osu.Game/Beatmaps/Drawables/UpdateableBeatmapBackgroundSprite.cs +++ b/osu.Game/Beatmaps/Drawables/UpdateableBeatmapBackgroundSprite.cs @@ -52,6 +52,9 @@ namespace osu.Game.Beatmaps.Drawables private Drawable getDrawableForModel(IBeatmapInfo? model) { + if (model == null) + return Empty(); + // prefer online cover where available. if (model?.BeatmapSet is IBeatmapSetOnlineInfo online) return new OnlineBeatmapSetCover(online, beatmapSetCoverType); From c21b7c7df994fa6a00a3edf7bc79653035bbaa8c Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Thu, 2 May 2024 22:46:42 -0700 Subject: [PATCH 0691/2493] Use `IsLoaded` instead of `Schedule` --- osu.Game/Overlays/NowPlayingOverlay.cs | 35 ++++++++++++++++++++------ 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/osu.Game/Overlays/NowPlayingOverlay.cs b/osu.Game/Overlays/NowPlayingOverlay.cs index 8f97ce50cc..744d32ff7e 100644 --- a/osu.Game/Overlays/NowPlayingOverlay.cs +++ b/osu.Game/Overlays/NowPlayingOverlay.cs @@ -476,29 +476,35 @@ namespace osu.Game.Overlays private const float initial_move_delay = 1000; private const float pixels_per_second = 50; - private LocalisableString text; private OsuSpriteText mainSpriteText = null!; private OsuSpriteText fillerSpriteText = null!; + private LocalisableString text; + public LocalisableString Text { get => text; set { text = value; - Schedule(updateText); + + if (IsLoaded) + updateText(); } } + private FontUsage font = OsuFont.Default; + public FontUsage Font { - set => Schedule(() => + get => font; + set { - mainSpriteText.Font = value; - fillerSpriteText.Font = value; + font = value; - updateText(); - }); + if (IsLoaded) + updateFontAndText(); + } } public ScrollingTextContainer() @@ -521,6 +527,21 @@ namespace osu.Game.Overlays }; } + protected override void LoadComplete() + { + base.LoadComplete(); + + updateFontAndText(); + } + + private void updateFontAndText() + { + mainSpriteText.Font = font; + fillerSpriteText.Font = font; + + updateText(); + } + private void updateText() { mainSpriteText.Text = text; From 2d4f2245ee1dc33bb6f0410f7ce230de5f4b5a06 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Thu, 2 May 2024 23:00:04 -0700 Subject: [PATCH 0692/2493] Remove total score border gradient --- osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs b/osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs index 80bf251631..b4f6379f06 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs @@ -353,8 +353,7 @@ namespace osu.Game.Online.Leaderboards new Container { AutoSizeAxes = Axes.X, - // makeshift inner border - Height = height - 4, + RelativeSizeAxes = Axes.Y, Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, Padding = new MarginPadding { Right = grade_width }, From c935d3bf6c12e0b6cd5e9213dcd9f3d68c27b22b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 3 May 2024 14:00:28 +0800 Subject: [PATCH 0693/2493] Reduce font size in channel listing too --- osu.Game/Overlays/Chat/Listing/ChannelListingItem.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Chat/Listing/ChannelListingItem.cs b/osu.Game/Overlays/Chat/Listing/ChannelListingItem.cs index 9c85c73ee4..466f8b2f5d 100644 --- a/osu.Game/Overlays/Chat/Listing/ChannelListingItem.cs +++ b/osu.Game/Overlays/Chat/Listing/ChannelListingItem.cs @@ -44,7 +44,7 @@ namespace osu.Game.Overlays.Chat.Listing [Resolved] private OverlayColourProvider colourProvider { get; set; } = null!; - private const float text_size = 18; + private const float text_size = 14; private const float icon_size = 14; private const float vertical_margin = 1.5f; From d1a50ff85bbfb4a502bd4df25a53d93386d7f920 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 3 May 2024 08:11:00 +0200 Subject: [PATCH 0694/2493] Remove redundant conditional access --- .../Beatmaps/Drawables/UpdateableBeatmapBackgroundSprite.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/Drawables/UpdateableBeatmapBackgroundSprite.cs b/osu.Game/Beatmaps/Drawables/UpdateableBeatmapBackgroundSprite.cs index f067af5360..6f71fa90b8 100644 --- a/osu.Game/Beatmaps/Drawables/UpdateableBeatmapBackgroundSprite.cs +++ b/osu.Game/Beatmaps/Drawables/UpdateableBeatmapBackgroundSprite.cs @@ -56,7 +56,7 @@ namespace osu.Game.Beatmaps.Drawables return Empty(); // prefer online cover where available. - if (model?.BeatmapSet is IBeatmapSetOnlineInfo online) + if (model.BeatmapSet is IBeatmapSetOnlineInfo online) return new OnlineBeatmapSetCover(online, beatmapSetCoverType); if (model is BeatmapInfo localModel) From 42e49067e55ddb232d6fe1af0af44234a772f5fc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 3 May 2024 17:10:59 +0800 Subject: [PATCH 0695/2493] Move `Room.Status` updates to a common location --- osu.Game/Online/Rooms/GetRoomsRequest.cs | 19 +++++++++++++++++++ osu.Game/Online/Rooms/Room.cs | 6 ++---- .../Lounge/Components/RoomStatusPill.cs | 12 +----------- .../Screens/OnlinePlay/OnlinePlayComposite.cs | 3 +++ 4 files changed, 25 insertions(+), 15 deletions(-) diff --git a/osu.Game/Online/Rooms/GetRoomsRequest.cs b/osu.Game/Online/Rooms/GetRoomsRequest.cs index 7feb709acb..bfb2629c64 100644 --- a/osu.Game/Online/Rooms/GetRoomsRequest.cs +++ b/osu.Game/Online/Rooms/GetRoomsRequest.cs @@ -1,10 +1,12 @@ // 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 osu.Framework.IO.Network; using osu.Game.Extensions; using osu.Game.Online.API; +using osu.Game.Online.Rooms.RoomStatuses; using osu.Game.Screens.OnlinePlay.Lounge.Components; namespace osu.Game.Online.Rooms @@ -33,6 +35,23 @@ namespace osu.Game.Online.Rooms return req; } + protected override void PostProcess() + { + base.PostProcess(); + + if (Response != null) + { + // API doesn't populate status so let's do it here. + foreach (var room in Response) + { + if (room.EndDate.Value != null && DateTimeOffset.Now >= room.EndDate.Value) + room.Status.Value = new RoomStatusEnded(); + else + room.Status.Value = new RoomStatusOpen(); + } + } + } + protected override string Target => "rooms"; } } diff --git a/osu.Game/Online/Rooms/Room.cs b/osu.Game/Online/Rooms/Room.cs index 8f346c4057..23c77f8773 100644 --- a/osu.Game/Online/Rooms/Room.cs +++ b/osu.Game/Online/Rooms/Room.cs @@ -111,8 +111,9 @@ namespace osu.Game.Online.Rooms [JsonProperty("current_user_score")] public readonly Bindable UserScore = new Bindable(); + [Cached] [JsonProperty("has_password")] - public readonly BindableBool HasPassword = new BindableBool(); + public readonly Bindable HasPassword = new Bindable(); [Cached] [JsonProperty("recent_participants")] @@ -201,9 +202,6 @@ namespace osu.Game.Online.Rooms CurrentPlaylistItem.Value = other.CurrentPlaylistItem.Value; AutoSkip.Value = other.AutoSkip.Value; - if (EndDate.Value != null && DateTimeOffset.Now >= EndDate.Value) - Status.Value = new RoomStatusEnded(); - other.RemoveExpiredPlaylistItems(); if (!Playlist.SequenceEqual(other.Playlist)) diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomStatusPill.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomStatusPill.cs index aae82b6721..96d698a184 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomStatusPill.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomStatusPill.cs @@ -1,13 +1,11 @@ // 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.Graphics; using osu.Framework.Graphics.Sprites; using osu.Game.Graphics; using osu.Game.Online.Rooms; -using osu.Game.Online.Rooms.RoomStatuses; namespace osu.Game.Screens.OnlinePlay.Lounge.Components { @@ -36,18 +34,10 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components private void updateDisplay() { - RoomStatus status = getDisplayStatus(); + RoomStatus status = Status.Value; Pill.Background.FadeColour(status.GetAppropriateColour(colours), 100); TextFlow.Text = status.Message; } - - private RoomStatus getDisplayStatus() - { - if (EndDate.Value < DateTimeOffset.Now) - return new RoomStatusEnded(); - - return Status.Value; - } } } diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlayComposite.cs b/osu.Game/Screens/OnlinePlay/OnlinePlayComposite.cs index ff536a65c4..83df1c6161 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlayComposite.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlayComposite.cs @@ -77,6 +77,9 @@ namespace osu.Game.Screens.OnlinePlay [Resolved(typeof(Room))] public Bindable Password { get; private set; } + [Resolved(typeof(Room))] + public Bindable HasPassword { get; private set; } + [Resolved(typeof(Room))] protected Bindable Duration { get; private set; } From 7141177966c82ffa6f9e947687422f45645c4966 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 3 May 2024 17:11:29 +0800 Subject: [PATCH 0696/2493] Better signify private rooms by showing a different status pill design --- osu.Game/Online/Multiplayer/MultiplayerClient.cs | 2 +- osu.Game/Online/Rooms/GetRoomsRequest.cs | 2 ++ .../Rooms/RoomStatuses/RoomStatusOpenPrivate.cs | 14 ++++++++++++++ 3 files changed, 17 insertions(+), 1 deletion(-) create mode 100644 osu.Game/Online/Rooms/RoomStatuses/RoomStatusOpenPrivate.cs diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index bbf0e3697a..871fbc15b3 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -396,7 +396,7 @@ namespace osu.Game.Online.Multiplayer switch (state) { case MultiplayerRoomState.Open: - APIRoom.Status.Value = new RoomStatusOpen(); + APIRoom.Status.Value = APIRoom.HasPassword.Value ? new RoomStatusOpenPrivate() : new RoomStatusOpen(); break; case MultiplayerRoomState.Playing: diff --git a/osu.Game/Online/Rooms/GetRoomsRequest.cs b/osu.Game/Online/Rooms/GetRoomsRequest.cs index bfb2629c64..1b5e08c729 100644 --- a/osu.Game/Online/Rooms/GetRoomsRequest.cs +++ b/osu.Game/Online/Rooms/GetRoomsRequest.cs @@ -46,6 +46,8 @@ namespace osu.Game.Online.Rooms { if (room.EndDate.Value != null && DateTimeOffset.Now >= room.EndDate.Value) room.Status.Value = new RoomStatusEnded(); + else if (room.HasPassword.Value) + room.Status.Value = new RoomStatusOpenPrivate(); else room.Status.Value = new RoomStatusOpen(); } diff --git a/osu.Game/Online/Rooms/RoomStatuses/RoomStatusOpenPrivate.cs b/osu.Game/Online/Rooms/RoomStatuses/RoomStatusOpenPrivate.cs new file mode 100644 index 0000000000..d71e706c76 --- /dev/null +++ b/osu.Game/Online/Rooms/RoomStatuses/RoomStatusOpenPrivate.cs @@ -0,0 +1,14 @@ +// 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.Graphics; +using osuTK.Graphics; + +namespace osu.Game.Online.Rooms.RoomStatuses +{ + public class RoomStatusOpenPrivate : RoomStatus + { + public override string Message => "Open (Private)"; + public override Color4 GetAppropriateColour(OsuColour colours) => colours.GreenDark; + } +} From f57818f5a2245d2b2b27a19555fc65f7e5575ac1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 3 May 2024 11:25:50 +0200 Subject: [PATCH 0697/2493] Add visual test coverage of private room status --- osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoom.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoom.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoom.cs index 4ffccdbf0e..98242e2d92 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoom.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoom.cs @@ -69,8 +69,9 @@ namespace osu.Game.Tests.Visual.Multiplayer }), createLoungeRoom(new Room { - Name = { Value = "Multiplayer room" }, - Status = { Value = new RoomStatusOpen() }, + Name = { Value = "Private room" }, + Status = { Value = new RoomStatusOpenPrivate() }, + HasPassword = { Value = true }, EndDate = { Value = DateTimeOffset.Now.AddDays(1) }, Type = { Value = MatchType.HeadToHead }, Playlist = From 221b4cd599df85d23212519a8a68bec8bb1e7797 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 3 May 2024 11:36:18 +0200 Subject: [PATCH 0698/2493] Remove unused cache --- osu.Game/Online/Rooms/Room.cs | 1 - osu.Game/Screens/OnlinePlay/OnlinePlayComposite.cs | 3 --- 2 files changed, 4 deletions(-) diff --git a/osu.Game/Online/Rooms/Room.cs b/osu.Game/Online/Rooms/Room.cs index 23c77f8773..5abf5034d9 100644 --- a/osu.Game/Online/Rooms/Room.cs +++ b/osu.Game/Online/Rooms/Room.cs @@ -111,7 +111,6 @@ namespace osu.Game.Online.Rooms [JsonProperty("current_user_score")] public readonly Bindable UserScore = new Bindable(); - [Cached] [JsonProperty("has_password")] public readonly Bindable HasPassword = new Bindable(); diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlayComposite.cs b/osu.Game/Screens/OnlinePlay/OnlinePlayComposite.cs index 83df1c6161..ff536a65c4 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlayComposite.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlayComposite.cs @@ -77,9 +77,6 @@ namespace osu.Game.Screens.OnlinePlay [Resolved(typeof(Room))] public Bindable Password { get; private set; } - [Resolved(typeof(Room))] - public Bindable HasPassword { get; private set; } - [Resolved(typeof(Room))] protected Bindable Duration { get; private set; } From 7d31af6f16f3ecbc6d04051bad0a2b5284cb9326 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 3 May 2024 11:34:42 +0200 Subject: [PATCH 0699/2493] Fix room status not updating when password is changed while inside the room --- osu.Game/Online/Multiplayer/MultiplayerClient.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index 871fbc15b3..77ede1fd35 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -816,6 +816,7 @@ namespace osu.Game.Online.Multiplayer Room.Settings = settings; APIRoom.Name.Value = Room.Settings.Name; APIRoom.Password.Value = Room.Settings.Password; + APIRoom.Status.Value = string.IsNullOrEmpty(Room.Settings.Password) ? new RoomStatusOpen() : new RoomStatusOpenPrivate(); APIRoom.Type.Value = Room.Settings.MatchType; APIRoom.QueueMode.Value = Room.Settings.QueueMode; APIRoom.AutoStartDuration.Value = Room.Settings.AutoStartDuration; From 1b7652e60d37d496c9c1f01a984e22b743904d35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 3 May 2024 13:47:10 +0200 Subject: [PATCH 0700/2493] Add failing tests --- osu.Game.Tests/Scores/IO/ImportScoreTest.cs | 192 ++++++++++++++++++++ 1 file changed, 192 insertions(+) diff --git a/osu.Game.Tests/Scores/IO/ImportScoreTest.cs b/osu.Game.Tests/Scores/IO/ImportScoreTest.cs index ebbc329b9d..eb2c098ab8 100644 --- a/osu.Game.Tests/Scores/IO/ImportScoreTest.cs +++ b/osu.Game.Tests/Scores/IO/ImportScoreTest.cs @@ -15,6 +15,7 @@ using osu.Game.Beatmaps; using osu.Game.Database; using osu.Game.IO.Archives; using osu.Game.Online.API; +using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests.Responses; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu; @@ -23,6 +24,7 @@ using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; using osu.Game.Tests.Beatmaps.IO; using osu.Game.Tests.Resources; +using osu.Game.Users; namespace osu.Game.Tests.Scores.IO { @@ -284,6 +286,196 @@ namespace osu.Game.Tests.Scores.IO } } + [Test] + public void TestUserLookedUpForOnlineScore() + { + using (HeadlessGameHost host = new CleanRunHeadlessGameHost()) + { + try + { + var osu = LoadOsuIntoHost(host, true); + + var api = (DummyAPIAccess)osu.API; + api.HandleRequest = req => + { + switch (req) + { + case GetUserRequest userRequest: + userRequest.TriggerSuccess(new APIUser + { + Username = "Test user", + CountryCode = CountryCode.JP, + Id = 1234 + }); + return true; + + default: + return false; + } + }; + + var beatmap = BeatmapImportHelper.LoadOszIntoOsu(osu, TestResources.GetQuickTestBeatmapForImport()).GetResultSafely(); + + var toImport = new ScoreInfo + { + Rank = ScoreRank.B, + TotalScore = 987654, + Accuracy = 0.8, + MaxCombo = 500, + Combo = 250, + User = new APIUser { Username = "Test user" }, + Date = DateTimeOffset.Now, + OnlineID = 12345, + Ruleset = new OsuRuleset().RulesetInfo, + BeatmapInfo = beatmap.Beatmaps.First() + }; + + var imported = LoadScoreIntoOsu(osu, toImport); + + Assert.AreEqual(toImport.Rank, imported.Rank); + Assert.AreEqual(toImport.TotalScore, imported.TotalScore); + Assert.AreEqual(toImport.Accuracy, imported.Accuracy); + Assert.AreEqual(toImport.MaxCombo, imported.MaxCombo); + Assert.AreEqual(toImport.User.Username, imported.User.Username); + Assert.AreEqual(toImport.Date, imported.Date); + Assert.AreEqual(toImport.OnlineID, imported.OnlineID); + Assert.AreEqual(toImport.User.Username, imported.RealmUser.Username); + Assert.AreEqual(1234, imported.RealmUser.OnlineID); + } + finally + { + host.Exit(); + } + } + } + + [Test] + public void TestUserLookedUpForLegacyOnlineScore() + { + using (HeadlessGameHost host = new CleanRunHeadlessGameHost()) + { + try + { + var osu = LoadOsuIntoHost(host, true); + + var api = (DummyAPIAccess)osu.API; + api.HandleRequest = req => + { + switch (req) + { + case GetUserRequest userRequest: + userRequest.TriggerSuccess(new APIUser + { + Username = "Test user", + CountryCode = CountryCode.JP, + Id = 1234 + }); + return true; + + default: + return false; + } + }; + + var beatmap = BeatmapImportHelper.LoadOszIntoOsu(osu, TestResources.GetQuickTestBeatmapForImport()).GetResultSafely(); + + var toImport = new ScoreInfo + { + Rank = ScoreRank.B, + TotalScore = 987654, + Accuracy = 0.8, + MaxCombo = 500, + Combo = 250, + User = new APIUser { Username = "Test user" }, + Date = DateTimeOffset.Now, + LegacyOnlineID = 12345, + Ruleset = new OsuRuleset().RulesetInfo, + BeatmapInfo = beatmap.Beatmaps.First() + }; + + var imported = LoadScoreIntoOsu(osu, toImport); + + Assert.AreEqual(toImport.Rank, imported.Rank); + Assert.AreEqual(toImport.TotalScore, imported.TotalScore); + Assert.AreEqual(toImport.Accuracy, imported.Accuracy); + Assert.AreEqual(toImport.MaxCombo, imported.MaxCombo); + Assert.AreEqual(toImport.User.Username, imported.User.Username); + Assert.AreEqual(toImport.Date, imported.Date); + Assert.AreEqual(toImport.OnlineID, imported.OnlineID); + Assert.AreEqual(toImport.User.Username, imported.RealmUser.Username); + Assert.AreEqual(1234, imported.RealmUser.OnlineID); + } + finally + { + host.Exit(); + } + } + } + + [Test] + public void TestUserNotLookedUpForOfflineScore() + { + using (HeadlessGameHost host = new CleanRunHeadlessGameHost()) + { + try + { + var osu = LoadOsuIntoHost(host, true); + + var api = (DummyAPIAccess)osu.API; + api.HandleRequest = req => + { + switch (req) + { + case GetUserRequest userRequest: + userRequest.TriggerSuccess(new APIUser + { + Username = "Test user", + CountryCode = CountryCode.JP, + Id = 1234 + }); + return true; + + default: + return false; + } + }; + + var beatmap = BeatmapImportHelper.LoadOszIntoOsu(osu, TestResources.GetQuickTestBeatmapForImport()).GetResultSafely(); + + var toImport = new ScoreInfo + { + Rank = ScoreRank.B, + TotalScore = 987654, + Accuracy = 0.8, + MaxCombo = 500, + Combo = 250, + User = new APIUser { Username = "Test user" }, + Date = DateTimeOffset.Now, + OnlineID = -1, + LegacyOnlineID = -1, + Ruleset = new OsuRuleset().RulesetInfo, + BeatmapInfo = beatmap.Beatmaps.First() + }; + + var imported = LoadScoreIntoOsu(osu, toImport); + + Assert.AreEqual(toImport.Rank, imported.Rank); + Assert.AreEqual(toImport.TotalScore, imported.TotalScore); + Assert.AreEqual(toImport.Accuracy, imported.Accuracy); + Assert.AreEqual(toImport.MaxCombo, imported.MaxCombo); + Assert.AreEqual(toImport.User.Username, imported.User.Username); + Assert.AreEqual(toImport.Date, imported.Date); + Assert.AreEqual(toImport.OnlineID, imported.OnlineID); + Assert.AreEqual(toImport.User.Username, imported.RealmUser.Username); + Assert.That(imported.RealmUser.OnlineID, Is.LessThanOrEqualTo(1)); + } + finally + { + host.Exit(); + } + } + } + public static ScoreInfo LoadScoreIntoOsu(OsuGameBase osu, ScoreInfo score, ArchiveReader archive = null) { // clone to avoid attaching the input score to realm. From afb491dff064967f4ccc6fe18f26e94e9322c570 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 3 May 2024 13:48:06 +0200 Subject: [PATCH 0701/2493] Do not perform username lookups for scores without an online ID --- osu.Game/Scoring/ScoreImporter.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Scoring/ScoreImporter.cs b/osu.Game/Scoring/ScoreImporter.cs index 768c28cc38..4ae8e51f6d 100644 --- a/osu.Game/Scoring/ScoreImporter.cs +++ b/osu.Game/Scoring/ScoreImporter.cs @@ -127,6 +127,9 @@ namespace osu.Game.Scoring if (model.RealmUser.OnlineID == APIUser.SYSTEM_USER_ID) return; + if (model.OnlineID < 0 && model.LegacyOnlineID <= 0) + return; + string username = model.RealmUser.Username; if (usernameLookupCache.TryGetValue(username, out var existing)) From a23d25e0a15c5ac6f3ab8565d3e4b2733501ff81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 3 May 2024 14:27:34 +0200 Subject: [PATCH 0702/2493] Fix `BeatmapAttributeText` breaking due to enum serialisation woes --- osu.Game/Skinning/Components/BeatmapAttributeText.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Skinning/Components/BeatmapAttributeText.cs b/osu.Game/Skinning/Components/BeatmapAttributeText.cs index 5c5e509fb2..c467b2e946 100644 --- a/osu.Game/Skinning/Components/BeatmapAttributeText.cs +++ b/osu.Game/Skinning/Components/BeatmapAttributeText.cs @@ -125,6 +125,8 @@ namespace osu.Game.Skinning.Components protected override void SetFont(FontUsage font) => text.Font = font.With(size: 40); } + // WARNING: DO NOT ADD ANY VALUES TO THIS ENUM ANYWHERE ELSE THAN AT THE END. + // Doing so will break existing user skins. public enum BeatmapAttribute { CircleSize, @@ -134,11 +136,11 @@ namespace osu.Game.Skinning.Components StarRating, Title, Artist, - Source, DifficultyName, Creator, Length, RankedStatus, BPM, + Source, } } From e0e7e123bf7d052b074d1cbfbda82e6678a5d3cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Brandst=C3=B6tter?= Date: Fri, 3 May 2024 16:57:31 +0200 Subject: [PATCH 0703/2493] Keep menus open when clicking a stateful item with CTRL held --- .../UserInterface/DrawableStatefulMenuItem.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/osu.Game/Graphics/UserInterface/DrawableStatefulMenuItem.cs b/osu.Game/Graphics/UserInterface/DrawableStatefulMenuItem.cs index 5af275c9e7..d63aaf2053 100644 --- a/osu.Game/Graphics/UserInterface/DrawableStatefulMenuItem.cs +++ b/osu.Game/Graphics/UserInterface/DrawableStatefulMenuItem.cs @@ -4,6 +4,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; +using osu.Framework.Input; using osuTK; namespace osu.Game.Graphics.UserInterface @@ -19,6 +20,17 @@ namespace osu.Game.Graphics.UserInterface protected override TextContainer CreateTextContainer() => new ToggleTextContainer(Item); + private InputManager inputManager = null!; + + public override bool CloseMenuOnClick => !inputManager.CurrentState.Keyboard.ControlPressed; + + protected override void LoadComplete() + { + base.LoadComplete(); + + inputManager = GetContainingInputManager(); + } + private partial class ToggleTextContainer : TextContainer { private readonly StatefulMenuItem menuItem; From 0b61e2cd421d2bd8468fc836c23d93dfd07d429c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 12 Apr 2024 15:52:36 +0800 Subject: [PATCH 0704/2493] Use closest origin along with closest anchor --- osu.Game/Overlays/SkinEditor/SkinEditor.cs | 2 +- .../SkinEditor/SkinSelectionHandler.cs | 35 ++++++++++++------- 2 files changed, 23 insertions(+), 14 deletions(-) diff --git a/osu.Game/Overlays/SkinEditor/SkinEditor.cs b/osu.Game/Overlays/SkinEditor/SkinEditor.cs index 690c6b35e3..5bf28ae79b 100644 --- a/osu.Game/Overlays/SkinEditor/SkinEditor.cs +++ b/osu.Game/Overlays/SkinEditor/SkinEditor.cs @@ -457,7 +457,7 @@ namespace osu.Game.Overlays.SkinEditor } SelectedComponents.Add(component); - SkinSelectionHandler.ApplyClosestAnchor(drawableComponent); + SkinSelectionHandler.ApplyClosestAnchorOrigin(drawableComponent); return true; } diff --git a/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs b/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs index cf6fb60636..f41bad4716 100644 --- a/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs +++ b/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs @@ -204,15 +204,20 @@ namespace osu.Game.Overlays.SkinEditor drawable.Position += drawable.ScreenSpaceDeltaToParentSpace(moveEvent.ScreenSpaceDelta); - if (item.UsesFixedAnchor) continue; - - ApplyClosestAnchor(drawable); + if (!item.UsesFixedAnchor) + ApplyClosestAnchorOrigin(drawable); } return true; } - public static void ApplyClosestAnchor(Drawable drawable) => applyAnchor(drawable, getClosestAnchor(drawable)); + public static void ApplyClosestAnchorOrigin(Drawable drawable) + { + var closest = getClosestAnchor(drawable); + + applyAnchor(drawable, closest); + applyOrigin(drawable, closest); + } protected override void OnSelectionChanged() { @@ -325,15 +330,10 @@ namespace osu.Game.Overlays.SkinEditor { var drawable = (Drawable)item; - if (origin == drawable.Origin) continue; + applyOrigin(drawable, origin); - var previousOrigin = drawable.OriginPosition; - drawable.Origin = origin; - drawable.Position += drawable.OriginPosition - previousOrigin; - - if (item.UsesFixedAnchor) continue; - - ApplyClosestAnchor(drawable); + if (item.UsesFixedAnchor) + ApplyClosestAnchorOrigin(drawable); } OnOperationEnded(); @@ -368,7 +368,7 @@ namespace osu.Game.Overlays.SkinEditor foreach (var item in SelectedItems) { item.UsesFixedAnchor = false; - ApplyClosestAnchor((Drawable)item); + ApplyClosestAnchorOrigin((Drawable)item); } OnOperationEnded(); @@ -414,6 +414,15 @@ namespace osu.Game.Overlays.SkinEditor drawable.Position -= drawable.AnchorPosition - previousAnchor; } + private static void applyOrigin(Drawable drawable, Anchor origin) + { + if (origin == drawable.Origin) return; + + var previousOrigin = drawable.OriginPosition; + drawable.Origin = origin; + drawable.Position += drawable.OriginPosition - previousOrigin; + } + private static void adjustScaleFromAnchor(ref Vector2 scale, Anchor reference) { // cancel out scale in axes we don't care about (based on which drag handle was used). From e7ca02ffde25da718116566947f8d7b09e7ba961 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 4 May 2024 13:28:33 +0800 Subject: [PATCH 0705/2493] Fix position changing when origin updates during a drag --- osu.Game/Overlays/SkinEditor/SkinBlueprint.cs | 4 +++- osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs | 6 +++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/osu.Game/Overlays/SkinEditor/SkinBlueprint.cs b/osu.Game/Overlays/SkinEditor/SkinBlueprint.cs index 8f8d899fad..6b59d940cc 100644 --- a/osu.Game/Overlays/SkinEditor/SkinBlueprint.cs +++ b/osu.Game/Overlays/SkinEditor/SkinBlueprint.cs @@ -40,7 +40,9 @@ namespace osu.Game.Overlays.SkinEditor public override bool Contains(Vector2 screenSpacePos) => drawableQuad.Contains(screenSpacePos); - public override Vector2 ScreenSpaceSelectionPoint => drawable.ToScreenSpace(drawable.OriginPosition); + public override Vector2 ScreenSpaceSelectionPoint => + // Important to use a stable position (not based on origin) as origin may be automatically updated during drag operations. + drawable.ScreenSpaceDrawQuad.Centre; protected override bool ReceivePositionalInputAtSubTree(Vector2 screenSpacePos) => drawableQuad.Contains(screenSpacePos); diff --git a/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs b/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs index f41bad4716..680cc02311 100644 --- a/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs +++ b/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs @@ -202,10 +202,10 @@ namespace osu.Game.Overlays.SkinEditor var item = c.Item; Drawable drawable = (Drawable)item; - drawable.Position += drawable.ScreenSpaceDeltaToParentSpace(moveEvent.ScreenSpaceDelta); - if (!item.UsesFixedAnchor) ApplyClosestAnchorOrigin(drawable); + + drawable.Position += drawable.ScreenSpaceDeltaToParentSpace(moveEvent.ScreenSpaceDelta); } return true; @@ -332,7 +332,7 @@ namespace osu.Game.Overlays.SkinEditor applyOrigin(drawable, origin); - if (item.UsesFixedAnchor) + if (!item.UsesFixedAnchor) ApplyClosestAnchorOrigin(drawable); } From 2cb367fdce2a04181bb71e0c2545e07ae975f6e0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 4 May 2024 13:29:05 +0800 Subject: [PATCH 0706/2493] Disable "origin" menu when in "Closest" placement mode --- .../Overlays/SkinEditor/SkinSelectionHandler.cs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs b/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs index 680cc02311..8b9e6436b1 100644 --- a/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs +++ b/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs @@ -23,6 +23,8 @@ namespace osu.Game.Overlays.SkinEditor { public partial class SkinSelectionHandler : SelectionHandler { + private OsuMenuItem originMenu = null!; + [Resolved] private SkinEditor skinEditor { get; set; } = null!; @@ -248,10 +250,15 @@ namespace osu.Game.Overlays.SkinEditor .ToArray() }; - yield return new OsuMenuItem("Origin") + yield return originMenu = new OsuMenuItem("Origin"); + + closestItem.State.BindValueChanged(s => { - Items = createAnchorItems((d, o) => ((Drawable)d).Origin == o, applyOrigins).ToArray() - }; + // For UX simplicity, origin should only be user-editable when "closest" anchor mode is disabled. + originMenu.Items = s.NewValue == TernaryState.True + ? Array.Empty() + : createAnchorItems((d, o) => ((Drawable)d).Origin == o, applyOrigins).ToArray(); + }, true); yield return new OsuMenuItemSpacer(); From b35f2c99e64e67b21fea9c272de843c9aa41c51d Mon Sep 17 00:00:00 2001 From: cdwcgt Date: Sat, 4 May 2024 18:43:04 +0800 Subject: [PATCH 0707/2493] add failed test --- .../UserInterface/TestSceneModPresetPanel.cs | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetPanel.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetPanel.cs index c79cbd3691..d0303b3849 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetPanel.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetPanel.cs @@ -123,6 +123,34 @@ namespace osu.Game.Tests.Visual.UserInterface assertSelectedModsEquivalentTo(new Mod[] { new OsuModTouchDevice(), new OsuModHardRock(), new OsuModDoubleTime { SpeedChange = { Value = 1.5 } } }); } + [Test] + public void TestActivatingPresetWithAutoplayWhenSystemModEnabled() + { + ModPresetPanel? panel = null; + + AddStep("create panel", () => Child = panel = new ModPresetPanel(new ModPreset + { + Name = "Autoplay include", + Description = "no way", + Mods = new Mod[] + { + new OsuModAutoplay() + }, + Ruleset = new OsuRuleset().RulesetInfo + }.ToLiveUnmanaged()) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Width = 0.5f + }); + + AddStep("Add touch device to selected mod", () => SelectedMods.Value = new Mod[] { new OsuModTouchDevice() }); + AddStep("activate panel", () => panel.AsNonNull().TriggerClick()); + + // touch device should be removed due to incompatible with autoplay. + assertSelectedModsEquivalentTo(new Mod[] { new OsuModAutoplay() }); + } + private void assertSelectedModsEquivalentTo(IEnumerable mods) => AddAssert("selected mods changed correctly", () => new HashSet(SelectedMods.Value).SetEquals(mods)); From f9be9ed479555a355be8bc3673540ce312fbf39e Mon Sep 17 00:00:00 2001 From: cdwcgt Date: Sat, 4 May 2024 18:44:47 +0800 Subject: [PATCH 0708/2493] remove incompatible system mods before enable preset --- osu.Game/Overlays/Mods/ModPresetPanel.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Mods/ModPresetPanel.cs b/osu.Game/Overlays/Mods/ModPresetPanel.cs index 3982abeba7..ca7e64957f 100644 --- a/osu.Game/Overlays/Mods/ModPresetPanel.cs +++ b/osu.Game/Overlays/Mods/ModPresetPanel.cs @@ -55,7 +55,8 @@ namespace osu.Game.Overlays.Mods protected override void Select() { - var selectedSystemMods = selectedMods.Value.Where(mod => mod.Type == ModType.System); + var selectedSystemMods = selectedMods.Value.Where(mod => mod.Type == ModType.System + && !mod.IncompatibleMods.Any(t => Preset.Value.Mods.Any(m => m.GetType() == t))); // will also have the side effect of activating the preset (see `updateActiveState()`). selectedMods.Value = Preset.Value.Mods.Concat(selectedSystemMods).ToArray(); } From 6af30a3d45ef7d4a971082be060407778c148a4b Mon Sep 17 00:00:00 2001 From: cdwcgt Date: Sat, 4 May 2024 20:02:35 +0800 Subject: [PATCH 0709/2493] add test for non-td system mod --- .../Visual/UserInterface/TestSceneModPresetPanel.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetPanel.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetPanel.cs index d0303b3849..9a141e0df0 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetPanel.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetPanel.cs @@ -149,6 +149,15 @@ namespace osu.Game.Tests.Visual.UserInterface // touch device should be removed due to incompatible with autoplay. assertSelectedModsEquivalentTo(new Mod[] { new OsuModAutoplay() }); + + AddStep("deactivate panel", () => panel.AsNonNull().TriggerClick()); + assertSelectedModsEquivalentTo(Array.Empty()); + + // just for test purpose + AddStep("Add score v2 to selected mod", () => SelectedMods.Value = new Mod[] { new ModScoreV2() }); + AddStep("activate panel", () => panel.AsNonNull().TriggerClick()); + + assertSelectedModsEquivalentTo(new Mod[] { new OsuModAutoplay(), new ModScoreV2() }); } private void assertSelectedModsEquivalentTo(IEnumerable mods) From fe30ca3d397b774eb8ab8eafe576a5d42b383b83 Mon Sep 17 00:00:00 2001 From: cdwcgt Date: Sat, 4 May 2024 20:11:59 +0800 Subject: [PATCH 0710/2493] fix linq logic --- osu.Game/Overlays/Mods/ModPresetPanel.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModPresetPanel.cs b/osu.Game/Overlays/Mods/ModPresetPanel.cs index ca7e64957f..9f5dda4a75 100644 --- a/osu.Game/Overlays/Mods/ModPresetPanel.cs +++ b/osu.Game/Overlays/Mods/ModPresetPanel.cs @@ -55,8 +55,10 @@ namespace osu.Game.Overlays.Mods protected override void Select() { - var selectedSystemMods = selectedMods.Value.Where(mod => mod.Type == ModType.System - && !mod.IncompatibleMods.Any(t => Preset.Value.Mods.Any(m => m.GetType() == t))); + var selectedSystemMods = selectedMods.Value.Where(mod => mod.Type == ModType.System && + !mod.IncompatibleMods.Any(t => Preset.Value.Mods.Any(t.IsInstanceOfType))); + + // will also have the side effect of activating the preset (see `updateActiveState()`). selectedMods.Value = Preset.Value.Mods.Concat(selectedSystemMods).ToArray(); } From 1f92f1d19b15a652e580f0531b0a70f7a5788c94 Mon Sep 17 00:00:00 2001 From: cdwcgt Date: Sat, 4 May 2024 20:41:36 +0800 Subject: [PATCH 0711/2493] remove blank line nt --- osu.Game/Overlays/Mods/ModPresetPanel.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Overlays/Mods/ModPresetPanel.cs b/osu.Game/Overlays/Mods/ModPresetPanel.cs index 9f5dda4a75..450c684e54 100644 --- a/osu.Game/Overlays/Mods/ModPresetPanel.cs +++ b/osu.Game/Overlays/Mods/ModPresetPanel.cs @@ -58,7 +58,6 @@ namespace osu.Game.Overlays.Mods var selectedSystemMods = selectedMods.Value.Where(mod => mod.Type == ModType.System && !mod.IncompatibleMods.Any(t => Preset.Value.Mods.Any(t.IsInstanceOfType))); - // will also have the side effect of activating the preset (see `updateActiveState()`). selectedMods.Value = Preset.Value.Mods.Concat(selectedSystemMods).ToArray(); } From 21917218ce17a2f3422b1e34e4d2ea42de0cb52d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Brandst=C3=B6tter?= Date: Sat, 4 May 2024 16:00:22 +0200 Subject: [PATCH 0712/2493] No longer keep menu open when CTRL is held --- .../Graphics/UserInterface/DrawableStatefulMenuItem.cs | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/osu.Game/Graphics/UserInterface/DrawableStatefulMenuItem.cs b/osu.Game/Graphics/UserInterface/DrawableStatefulMenuItem.cs index d63aaf2053..000a2f9f91 100644 --- a/osu.Game/Graphics/UserInterface/DrawableStatefulMenuItem.cs +++ b/osu.Game/Graphics/UserInterface/DrawableStatefulMenuItem.cs @@ -2,9 +2,8 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Bindables; -using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; -using osu.Framework.Input; +using osu.Framework.Graphics; using osuTK; namespace osu.Game.Graphics.UserInterface @@ -20,15 +19,8 @@ namespace osu.Game.Graphics.UserInterface protected override TextContainer CreateTextContainer() => new ToggleTextContainer(Item); - private InputManager inputManager = null!; - - public override bool CloseMenuOnClick => !inputManager.CurrentState.Keyboard.ControlPressed; - - protected override void LoadComplete() { - base.LoadComplete(); - inputManager = GetContainingInputManager(); } private partial class ToggleTextContainer : TextContainer From c62952ea3afd2d81ba3f703491525c5249b79cac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Brandst=C3=B6tter?= Date: Sat, 4 May 2024 16:01:31 +0200 Subject: [PATCH 0713/2493] Invoke the registered Action when a stateful item is right clicked --- .../Graphics/UserInterface/DrawableStatefulMenuItem.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/osu.Game/Graphics/UserInterface/DrawableStatefulMenuItem.cs b/osu.Game/Graphics/UserInterface/DrawableStatefulMenuItem.cs index 000a2f9f91..0c4e575621 100644 --- a/osu.Game/Graphics/UserInterface/DrawableStatefulMenuItem.cs +++ b/osu.Game/Graphics/UserInterface/DrawableStatefulMenuItem.cs @@ -4,6 +4,8 @@ using osu.Framework.Bindables; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics; +using osu.Framework.Input.Events; +using osuTK.Input; using osuTK; namespace osu.Game.Graphics.UserInterface @@ -19,8 +21,16 @@ namespace osu.Game.Graphics.UserInterface protected override TextContainer CreateTextContainer() => new ToggleTextContainer(Item); + protected override bool OnMouseDown(MouseDownEvent e) { + if (!IsActionable) + return true; + if (e.Button != MouseButton.Right) + return true; + + Item.Action.Value?.Invoke(); + return true; } private partial class ToggleTextContainer : TextContainer From 78d6f24fcae2e11c1c5fe7ba148fe4dd15f4a3b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Brandst=C3=B6tter?= Date: Sat, 4 May 2024 16:25:19 +0200 Subject: [PATCH 0714/2493] Add Test --- .../TestSceneStatefulMenuItem.cs | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneStatefulMenuItem.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneStatefulMenuItem.cs index 88187f1808..63497040db 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneStatefulMenuItem.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneStatefulMenuItem.cs @@ -114,6 +114,51 @@ namespace osu.Game.Tests.Visual.UserInterface => AddAssert($"state is {expected}", () => state.Value == expected); } + [Test] + public void TestItemRespondsToRightClick() + { + OsuMenu menu = null; + + Bindable state = new Bindable(TernaryState.Indeterminate); + + AddStep("create menu", () => + { + state.Value = TernaryState.Indeterminate; + + Child = menu = new OsuMenu(Direction.Vertical, true) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Items = new[] + { + new TernaryStateToggleMenuItem("First"), + new TernaryStateToggleMenuItem("Second") { State = { BindTarget = state } }, + new TernaryStateToggleMenuItem("Third") { State = { Value = TernaryState.True } }, + } + }; + }); + + checkState(TernaryState.Indeterminate); + + click(); + checkState(TernaryState.True); + + click(); + checkState(TernaryState.False); + + AddStep("change state via bindable", () => state.Value = TernaryState.True); + + void click() => + AddStep("click", () => + { + InputManager.MoveMouseTo(menu.ScreenSpaceDrawQuad.Centre); + InputManager.Click(MouseButton.Right); + }); + + void checkState(TernaryState expected) + => AddAssert($"state is {expected}", () => state.Value == expected); + } + [Test] public void TestCustomState() { From b1696db9c87d59fd047aa3b94c2b34577e68c593 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Brandst=C3=B6tter?= Date: Sat, 4 May 2024 16:32:18 +0200 Subject: [PATCH 0715/2493] Reorder imports with `dotnet format` --- osu.Game/Graphics/UserInterface/DrawableStatefulMenuItem.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Graphics/UserInterface/DrawableStatefulMenuItem.cs b/osu.Game/Graphics/UserInterface/DrawableStatefulMenuItem.cs index 0c4e575621..8ed52593a7 100644 --- a/osu.Game/Graphics/UserInterface/DrawableStatefulMenuItem.cs +++ b/osu.Game/Graphics/UserInterface/DrawableStatefulMenuItem.cs @@ -2,11 +2,11 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Bindables; -using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics; +using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Events; -using osuTK.Input; using osuTK; +using osuTK.Input; namespace osu.Game.Graphics.UserInterface { From cf313cd67f3c1a89b77edd90c94a73a5794e31d7 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Sat, 4 May 2024 21:53:48 +0300 Subject: [PATCH 0716/2493] Use single path to display slider control point connections --- .../TestScenePathControlPointVisualiser.cs | 35 ---------------- ...Piece.cs => PathControlPointConnection.cs} | 42 +++++-------------- .../Components/PathControlPointVisualiser.cs | 29 +------------ 3 files changed, 11 insertions(+), 95 deletions(-) rename osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/{PathControlPointConnectionPiece.cs => PathControlPointConnection.cs} (51%) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePathControlPointVisualiser.cs index 0ca30e00bc..9af028fd8c 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePathControlPointVisualiser.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePathControlPointVisualiser.cs @@ -30,23 +30,6 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor slider.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); }); - [Test] - public void TestAddOverlappingControlPoints() - { - createVisualiser(true); - - addControlPointStep(new Vector2(200)); - addControlPointStep(new Vector2(300)); - addControlPointStep(new Vector2(300)); - addControlPointStep(new Vector2(500, 300)); - - AddAssert("last connection displayed", () => - { - var lastConnection = visualiser.Connections.Last(c => c.ControlPoint.Position == new Vector2(300)); - return lastConnection.DrawWidth > 50; - }); - } - [Test] public void TestPerfectCurveTooManyPoints() { @@ -194,24 +177,6 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor addAssertPointPositionChanged(points, i); } - [Test] - public void TestStackingUpdatesConnectionPosition() - { - createVisualiser(true); - - Vector2 connectionPosition; - addControlPointStep(connectionPosition = new Vector2(300)); - addControlPointStep(new Vector2(600)); - - // Apply a big number in stacking so the person running the test can clearly see if it fails - AddStep("apply stacking", () => slider.StackHeightBindable.Value += 10); - - AddAssert($"Connection at {connectionPosition} changed", - () => visualiser.Connections[0].Position, - () => !Is.EqualTo(connectionPosition) - ); - } - private void addAssertPointPositionChanged(Vector2[] points, int index) { AddAssert($"Point at {points.ElementAt(index)} changed", diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointConnectionPiece.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointConnection.cs similarity index 51% rename from osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointConnectionPiece.cs rename to osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointConnection.cs index 9b3d8fc7a7..5706ed4baf 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointConnectionPiece.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointConnection.cs @@ -4,10 +4,7 @@ #nullable disable using osu.Framework.Bindables; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Lines; -using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu.Objects; using osuTK; @@ -15,36 +12,21 @@ using osuTK; namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components { /// - /// A visualisation of the line between two s. + /// A visualisation of the lines between s. /// - /// The type of which this visualises. - public partial class PathControlPointConnectionPiece : CompositeDrawable where T : OsuHitObject, IHasPath + /// The type of which this visualises. + public partial class PathControlPointConnection : SmoothPath where T : OsuHitObject, IHasPath { - public readonly PathControlPoint ControlPoint; - - private readonly Path path; private readonly T hitObject; - public int ControlPointIndex { get; set; } private IBindable hitObjectPosition; private IBindable pathVersion; private IBindable stackHeight; - public PathControlPointConnectionPiece(T hitObject, int controlPointIndex) + public PathControlPointConnection(T hitObject) { this.hitObject = hitObject; - ControlPointIndex = controlPointIndex; - - Origin = Anchor.Centre; - AutoSizeAxes = Axes.Both; - - ControlPoint = hitObject.Path.ControlPoints[controlPointIndex]; - - InternalChild = path = new SmoothPath - { - Anchor = Anchor.Centre, - PathRadius = 1 - }; + PathRadius = 1; } protected override void LoadComplete() @@ -68,18 +50,14 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components /// private void updateConnectingPath() { - Position = hitObject.StackedPosition + ControlPoint.Position; + Position = hitObject.StackedPosition; - path.ClearVertices(); + ClearVertices(); - int nextIndex = ControlPointIndex + 1; - if (nextIndex == 0 || nextIndex >= hitObject.Path.ControlPoints.Count) - return; + foreach (var controlPoint in hitObject.Path.ControlPoints) + AddVertex(controlPoint.Position); - path.AddVertex(Vector2.Zero); - path.AddVertex(hitObject.Path.ControlPoints[nextIndex].Position - ControlPoint.Position); - - path.OriginPosition = path.PositionInBoundingBox(Vector2.Zero); + OriginPosition = PositionInBoundingBox(Vector2.Zero); } } } diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs index b2d1709531..7212de322d 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs @@ -37,7 +37,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; // allow context menu to appear outside of the playfield. internal readonly Container> Pieces; - internal readonly Container> Connections; private readonly IBindableList controlPoints = new BindableList(); private readonly T hitObject; @@ -63,7 +62,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components InternalChildren = new Drawable[] { - Connections = new Container> { RelativeSizeAxes = Axes.Both }, + new PathControlPointConnection(hitObject), Pieces = new Container> { RelativeSizeAxes = Axes.Both } }; } @@ -185,17 +184,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components case NotifyCollectionChangedAction.Add: Debug.Assert(e.NewItems != null); - // If inserting in the path (not appending), - // update indices of existing connections after insert location - if (e.NewStartingIndex < Pieces.Count) - { - foreach (var connection in Connections) - { - if (connection.ControlPointIndex >= e.NewStartingIndex) - connection.ControlPointIndex += e.NewItems.Count; - } - } - for (int i = 0; i < e.NewItems.Count; i++) { var point = (PathControlPoint)e.NewItems[i]; @@ -209,8 +197,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components d.DragInProgress = DragInProgress; d.DragEnded = DragEnded; })); - - Connections.Add(new PathControlPointConnectionPiece(hitObject, e.NewStartingIndex + i)); } break; @@ -222,19 +208,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components { foreach (var piece in Pieces.Where(p => p.ControlPoint == point).ToArray()) piece.RemoveAndDisposeImmediately(); - foreach (var connection in Connections.Where(c => c.ControlPoint == point).ToArray()) - connection.RemoveAndDisposeImmediately(); - } - - // If removing before the end of the path, - // update indices of connections after remove location - if (e.OldStartingIndex < Pieces.Count) - { - foreach (var connection in Connections) - { - if (connection.ControlPointIndex >= e.OldStartingIndex) - connection.ControlPointIndex -= e.OldItems.Count; - } } break; From e319a3e885e9ae7d545c70a274ecad87220f62c7 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Sat, 4 May 2024 22:07:08 +0300 Subject: [PATCH 0717/2493] Don't perform masking updates in PathControlPointVisualiser --- .../Sliders/Components/PathControlPointVisualiser.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs index 7212de322d..836d348ff4 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs @@ -77,6 +77,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components controlPoints.BindTo(hitObject.Path.ControlPoints); } + // Generally all the control points are within the visible area all the time. + public override bool UpdateSubTreeMasking(Drawable source, RectangleF maskingBounds) => true; + /// /// Handles correction of invalid path types. /// From 9e7712740b2d881c14bb8f0872e124d95436fe0a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 5 May 2024 23:32:24 +0800 Subject: [PATCH 0718/2493] Refactor for legibility --- .../Graphics/UserInterface/DrawableStatefulMenuItem.cs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/osu.Game/Graphics/UserInterface/DrawableStatefulMenuItem.cs b/osu.Game/Graphics/UserInterface/DrawableStatefulMenuItem.cs index 8ed52593a7..686c490930 100644 --- a/osu.Game/Graphics/UserInterface/DrawableStatefulMenuItem.cs +++ b/osu.Game/Graphics/UserInterface/DrawableStatefulMenuItem.cs @@ -23,13 +23,11 @@ namespace osu.Game.Graphics.UserInterface protected override bool OnMouseDown(MouseDownEvent e) { - if (!IsActionable) - return true; + // Right mouse button is a special case where we allow actioning without dismissing the menu. + // This is achieved by not calling `Clicked` (as done by the base implementation in OnClick). + if (IsActionable && e.Button == MouseButton.Right) + Item.Action.Value?.Invoke(); - if (e.Button != MouseButton.Right) - return true; - - Item.Action.Value?.Invoke(); return true; } From 848e497c94d1d0a440cc74b05dfa441f19dfb40b Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sun, 5 May 2024 21:32:25 +0300 Subject: [PATCH 0719/2493] Add "HP" to the abbreviations list --- osu.sln.DotSettings | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.sln.DotSettings b/osu.sln.DotSettings index 51af281ac6..08eb264aab 100644 --- a/osu.sln.DotSettings +++ b/osu.sln.DotSettings @@ -347,6 +347,7 @@ GL GLSL HID + HP HSL HSPA HSV From 1665c5e0e1e3e3434cd98c0f0075362b0e7ea5a5 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Sun, 5 May 2024 14:41:48 -0700 Subject: [PATCH 0720/2493] Use existing `AutomaticallyDownloadMissingBeatmaps` localisation on solo spectator screen --- osu.Game/Screens/Play/SoloSpectatorScreen.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/SoloSpectatorScreen.cs b/osu.Game/Screens/Play/SoloSpectatorScreen.cs index 2db751402c..95eb2d4376 100644 --- a/osu.Game/Screens/Play/SoloSpectatorScreen.cs +++ b/osu.Game/Screens/Play/SoloSpectatorScreen.cs @@ -16,6 +16,7 @@ using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterfaceV2; +using osu.Game.Localisation; using osu.Game.Online.API; using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests.Responses; @@ -138,7 +139,7 @@ namespace osu.Game.Screens.Play }, automaticDownload = new SettingsCheckbox { - LabelText = "Automatically download beatmaps", + LabelText = OnlineSettingsStrings.AutomaticallyDownloadMissingBeatmaps, Current = config.GetBindable(OsuSetting.AutomaticallyDownloadMissingBeatmaps), Anchor = Anchor.Centre, Origin = Anchor.Centre, From eb92e8de370d5fff1407c64527b3760d6b63ac82 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Sun, 5 May 2024 15:15:14 -0700 Subject: [PATCH 0721/2493] Edit title/artist unicode values and add unicode toggle in test --- .../UserInterface/TestSceneNowPlayingOverlay.cs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneNowPlayingOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneNowPlayingOverlay.cs index 1670741cbd..d84089fb6f 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneNowPlayingOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneNowPlayingOverlay.cs @@ -5,6 +5,7 @@ using NUnit.Framework; using osu.Framework.Allocation; +using osu.Framework.Configuration; using osu.Framework.Graphics; using osu.Game.Beatmaps; using osu.Game.Overlays; @@ -21,8 +22,10 @@ namespace osu.Game.Tests.Visual.UserInterface private NowPlayingOverlay nowPlayingOverlay; [BackgroundDependencyLoader] - private void load() + private void load(FrameworkConfigManager frameworkConfig) { + AddToggleStep("toggle unicode", v => frameworkConfig.SetValue(FrameworkSetting.ShowUnicode, v)); + nowPlayingOverlay = new NowPlayingOverlay { Origin = Anchor.Centre, @@ -50,9 +53,9 @@ namespace osu.Game.Tests.Visual.UserInterface Metadata = { Artist = "very very very very very very very very very very verry long artist", - ArtistUnicode = "very very very very very very very very very very verry long artist", + ArtistUnicode = "very very very very very very very very very very verry long artist unicode", Title = "very very very very very verry long title", - TitleUnicode = "very very very very very verry long title", + TitleUnicode = "very very very very very verry long title unicode", } })); @@ -61,9 +64,9 @@ namespace osu.Game.Tests.Visual.UserInterface Metadata = { Artist = "very very very very very very very very very very verrry long artist", - ArtistUnicode = "very very very very very very very very very very verrry long artist", + ArtistUnicode = "not very long artist unicode", Title = "very very very very very verrry long title", - TitleUnicode = "very very very very very verrry long title", + TitleUnicode = "not very long title unicode", } })); From 359238395f273686b611ac87f5ae1ceef1072a04 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Sun, 5 May 2024 15:17:03 -0700 Subject: [PATCH 0722/2493] Fix now playing overlay text scroll breaking when toggling metadata language setting --- osu.Game/Overlays/NowPlayingOverlay.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/osu.Game/Overlays/NowPlayingOverlay.cs b/osu.Game/Overlays/NowPlayingOverlay.cs index 29052ace8e..76c8c237d5 100644 --- a/osu.Game/Overlays/NowPlayingOverlay.cs +++ b/osu.Game/Overlays/NowPlayingOverlay.cs @@ -5,6 +5,7 @@ using System; using System.Threading; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Configuration; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; @@ -479,6 +480,11 @@ namespace osu.Game.Overlays private OsuSpriteText mainSpriteText = null!; private OsuSpriteText fillerSpriteText = null!; + private Bindable showUnicode = null!; + + [Resolved] + private FrameworkConfigManager frameworkConfig { get; set; } = null!; + private LocalisableString text; public LocalisableString Text @@ -531,6 +537,9 @@ namespace osu.Game.Overlays { base.LoadComplete(); + showUnicode = frameworkConfig.GetBindable(FrameworkSetting.ShowUnicode); + showUnicode.BindValueChanged(_ => updateText()); + updateFontAndText(); } From 12cd3bbe1c79d7f2134ed79f03cb1e8005086d99 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 6 May 2024 11:34:37 +0800 Subject: [PATCH 0723/2493] Fix incorrect scale handling due to selection point changes --- osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs b/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs index 8b9e6436b1..75bb77fa73 100644 --- a/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs +++ b/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs @@ -139,7 +139,7 @@ namespace osu.Game.Overlays.SkinEditor var drawableItem = (Drawable)b.Item; // each drawable's relative position should be maintained in the scaled quad. - var screenPosition = b.ScreenSpaceSelectionPoint; + var screenPosition = drawableItem.ToScreenSpace(drawableItem.OriginPosition); var relativePositionInOriginal = new Vector2( From 4c7e6b125cdc889b12039b8b4048ab09afc963b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 6 May 2024 08:49:30 +0200 Subject: [PATCH 0724/2493] Add clarification comment --- osu.Game/Overlays/Mods/ModPresetPanel.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Overlays/Mods/ModPresetPanel.cs b/osu.Game/Overlays/Mods/ModPresetPanel.cs index 450c684e54..568ca5ecc9 100644 --- a/osu.Game/Overlays/Mods/ModPresetPanel.cs +++ b/osu.Game/Overlays/Mods/ModPresetPanel.cs @@ -55,6 +55,9 @@ namespace osu.Game.Overlays.Mods protected override void Select() { + // this implicitly presumes that if a system mod declares incompatibility with a non-system mod, + // the non-system mod should take precedence. + // if this assumption is ever broken, this should be reconsidered. var selectedSystemMods = selectedMods.Value.Where(mod => mod.Type == ModType.System && !mod.IncompatibleMods.Any(t => Preset.Value.Mods.Any(t.IsInstanceOfType))); From cb4af794161d325143e4ba4421774f746137b51f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 6 May 2024 08:53:41 +0200 Subject: [PATCH 0725/2493] Touch up test case --- .../UserInterface/TestSceneModPresetPanel.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetPanel.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetPanel.cs index 9a141e0df0..f87d8e0d2b 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetPanel.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetPanel.cs @@ -124,17 +124,17 @@ namespace osu.Game.Tests.Visual.UserInterface } [Test] - public void TestActivatingPresetWithAutoplayWhenSystemModEnabled() + public void TestSystemModsNotPreservedIfIncompatibleWithPresetMods() { ModPresetPanel? panel = null; AddStep("create panel", () => Child = panel = new ModPresetPanel(new ModPreset { - Name = "Autoplay include", + Name = "Autopilot included", Description = "no way", Mods = new Mod[] { - new OsuModAutoplay() + new OsuModAutopilot() }, Ruleset = new OsuRuleset().RulesetInfo }.ToLiveUnmanaged()) @@ -144,20 +144,20 @@ namespace osu.Game.Tests.Visual.UserInterface Width = 0.5f }); - AddStep("Add touch device to selected mod", () => SelectedMods.Value = new Mod[] { new OsuModTouchDevice() }); + AddStep("Add touch device to selected mods", () => SelectedMods.Value = new Mod[] { new OsuModTouchDevice() }); AddStep("activate panel", () => panel.AsNonNull().TriggerClick()); - // touch device should be removed due to incompatible with autoplay. - assertSelectedModsEquivalentTo(new Mod[] { new OsuModAutoplay() }); + // touch device should be removed due to incompatibility with autopilot. + assertSelectedModsEquivalentTo(new Mod[] { new OsuModAutopilot() }); AddStep("deactivate panel", () => panel.AsNonNull().TriggerClick()); assertSelectedModsEquivalentTo(Array.Empty()); - // just for test purpose + // just for test purposes, can't/shouldn't happen in reality AddStep("Add score v2 to selected mod", () => SelectedMods.Value = new Mod[] { new ModScoreV2() }); AddStep("activate panel", () => panel.AsNonNull().TriggerClick()); - assertSelectedModsEquivalentTo(new Mod[] { new OsuModAutoplay(), new ModScoreV2() }); + assertSelectedModsEquivalentTo(new Mod[] { new OsuModAutopilot(), new ModScoreV2() }); } private void assertSelectedModsEquivalentTo(IEnumerable mods) From cf87e9cd40008cedba6f0e183f5c07bf0978643f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 6 May 2024 11:22:56 +0200 Subject: [PATCH 0726/2493] Do not show integration settings on mobile Closes https://github.com/ppy/osu/issues/28097. The settings weren't actually doing anything at all there anyway. --- osu.Game/Overlays/Settings/Sections/OnlineSection.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Settings/Sections/OnlineSection.cs b/osu.Game/Overlays/Settings/Sections/OnlineSection.cs index 1484f2c756..6593eb69fa 100644 --- a/osu.Game/Overlays/Settings/Sections/OnlineSection.cs +++ b/osu.Game/Overlays/Settings/Sections/OnlineSection.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; using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; @@ -25,8 +26,10 @@ namespace osu.Game.Overlays.Settings.Sections { new WebSettings(), new AlertsAndPrivacySettings(), - new IntegrationSettings() }; + + if (RuntimeInfo.IsDesktop) + Add(new IntegrationSettings()); } } } From f066026503b4f50377fe3eca5ee55f1261ff00f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 6 May 2024 11:55:49 +0200 Subject: [PATCH 0727/2493] Fix sizing of gameplay preview in skin editor not updating on scaling mode change Closes https://github.com/ppy/osu/issues/28115. --- osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs b/osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs index 2f4820e207..748e9c6c0b 100644 --- a/osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs +++ b/osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs @@ -13,6 +13,7 @@ using osu.Framework.Graphics.Primitives; using osu.Framework.Input; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; +using osu.Framework.Layout; using osu.Framework.Screens; using osu.Framework.Testing; using osu.Game.Beatmaps; @@ -30,7 +31,6 @@ using osu.Game.Screens.Play; using osu.Game.Screens.Select; using osu.Game.Users; using osu.Game.Utils; -using osuTK; namespace osu.Game.Overlays.SkinEditor { @@ -70,12 +70,14 @@ namespace osu.Game.Overlays.SkinEditor private OsuScreen? lastTargetScreen; private InvokeOnDisposal? nestedInputManagerDisable; - private Vector2 lastDrawSize; + private LayoutValue drawSizeLayout; public SkinEditorOverlay(ScalingContainer scalingContainer) { this.scalingContainer = scalingContainer; RelativeSizeAxes = Axes.Both; + + AddLayout(drawSizeLayout = new LayoutValue(Invalidation.DrawSize)); } [BackgroundDependencyLoader] @@ -199,10 +201,10 @@ namespace osu.Game.Overlays.SkinEditor { base.Update(); - if (game.DrawSize != lastDrawSize) + if (!drawSizeLayout.IsValid) { - lastDrawSize = game.DrawSize; updateScreenSizing(); + drawSizeLayout.Validate(); } } From 353a07f7a5fa4a24c3d91b545defda6f8128efe0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 6 May 2024 12:23:11 +0200 Subject: [PATCH 0728/2493] Fix code quality inspection --- osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs b/osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs index 748e9c6c0b..571f99bd08 100644 --- a/osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs +++ b/osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs @@ -70,7 +70,7 @@ namespace osu.Game.Overlays.SkinEditor private OsuScreen? lastTargetScreen; private InvokeOnDisposal? nestedInputManagerDisable; - private LayoutValue drawSizeLayout; + private readonly LayoutValue drawSizeLayout; public SkinEditorOverlay(ScalingContainer scalingContainer) { From 4d9ccdc3b2391463355d297387c12181f72a6fc2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 3 May 2024 13:23:41 +0200 Subject: [PATCH 0729/2493] Encode user ID to replays --- osu.Game/Scoring/Legacy/LegacyReplaySoloScoreInfo.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/Scoring/Legacy/LegacyReplaySoloScoreInfo.cs b/osu.Game/Scoring/Legacy/LegacyReplaySoloScoreInfo.cs index 32c18b3af2..b71d9d916e 100644 --- a/osu.Game/Scoring/Legacy/LegacyReplaySoloScoreInfo.cs +++ b/osu.Game/Scoring/Legacy/LegacyReplaySoloScoreInfo.cs @@ -43,6 +43,9 @@ namespace osu.Game.Scoring.Legacy [JsonConverter(typeof(StringEnumConverter))] public ScoreRank? Rank; + [JsonProperty("user_id")] + public int UserID = -1; + public static LegacyReplaySoloScoreInfo FromScore(ScoreInfo score) => new LegacyReplaySoloScoreInfo { OnlineID = score.OnlineID, @@ -51,6 +54,7 @@ namespace osu.Game.Scoring.Legacy MaximumStatistics = score.MaximumStatistics.Where(kvp => kvp.Value != 0).ToDictionary(), ClientVersion = score.ClientVersion, Rank = score.Rank, + UserID = score.UserID, }; } } From 554ead0d9dd596a01bf55ab36c72f8eb346f81bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 3 May 2024 13:34:02 +0200 Subject: [PATCH 0730/2493] Decode user ID from score if available --- .../Beatmaps/Formats/LegacyScoreDecoderTest.cs | 9 +++++++++ osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs | 2 ++ 2 files changed, 11 insertions(+) diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs index 383c08c10f..cc7b37e6a8 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs @@ -14,6 +14,7 @@ using osu.Game.Beatmaps; using osu.Game.Beatmaps.Formats; using osu.Game.Beatmaps.Legacy; using osu.Game.IO.Legacy; +using osu.Game.Models; using osu.Game.Replays; using osu.Game.Rulesets; using osu.Game.Rulesets.Catch; @@ -31,6 +32,7 @@ using osu.Game.Rulesets.Taiko; using osu.Game.Scoring; using osu.Game.Scoring.Legacy; using osu.Game.Tests.Resources; +using osu.Game.Users; namespace osu.Game.Tests.Beatmaps.Formats { @@ -224,6 +226,12 @@ namespace osu.Game.Tests.Beatmaps.Formats new OsuModDoubleTime { SpeedChange = { Value = 1.1 } } }; scoreInfo.OnlineID = 123123; + scoreInfo.RealmUser = new RealmUser + { + Username = "spaceman_atlas", + OnlineID = 3035836, + CountryCode = CountryCode.PL + }; scoreInfo.ClientVersion = "2023.1221.0"; var beatmap = new TestBeatmap(ruleset); @@ -248,6 +256,7 @@ namespace osu.Game.Tests.Beatmaps.Formats Assert.That(decodedAfterEncode.ScoreInfo.MaximumStatistics, Is.EqualTo(scoreInfo.MaximumStatistics)); Assert.That(decodedAfterEncode.ScoreInfo.Mods, Is.EqualTo(scoreInfo.Mods)); Assert.That(decodedAfterEncode.ScoreInfo.ClientVersion, Is.EqualTo("2023.1221.0")); + Assert.That(decodedAfterEncode.ScoreInfo.RealmUser.OnlineID, Is.EqualTo(3035836)); }); } diff --git a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs index b0d7087ed1..00e294fdcd 100644 --- a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs +++ b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs @@ -131,6 +131,8 @@ namespace osu.Game.Scoring.Legacy score.ScoreInfo.Mods = readScore.Mods.Select(m => m.ToMod(currentRuleset)).ToArray(); score.ScoreInfo.ClientVersion = readScore.ClientVersion; decodedRank = readScore.Rank; + if (readScore.UserID > 1) + score.ScoreInfo.RealmUser.OnlineID = readScore.UserID; }); } } From bd869b6cdcd41c3406022fb784f54fe2fdebf849 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 6 May 2024 13:24:24 +0200 Subject: [PATCH 0731/2493] Add failing tests for looking up users by online ID if present when importing scores --- osu.Game.Tests/Scores/IO/ImportScoreTest.cs | 82 ++++++++++++++++++++- 1 file changed, 79 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Scores/IO/ImportScoreTest.cs b/osu.Game.Tests/Scores/IO/ImportScoreTest.cs index eb2c098ab8..9c72804a6b 100644 --- a/osu.Game.Tests/Scores/IO/ImportScoreTest.cs +++ b/osu.Game.Tests/Scores/IO/ImportScoreTest.cs @@ -287,7 +287,7 @@ namespace osu.Game.Tests.Scores.IO } [Test] - public void TestUserLookedUpForOnlineScore() + public void TestUserLookedUpByUsernameForOnlineScoreIfUserIDMissing() { using (HeadlessGameHost host = new CleanRunHeadlessGameHost()) { @@ -301,6 +301,9 @@ namespace osu.Game.Tests.Scores.IO switch (req) { case GetUserRequest userRequest: + if (userRequest.Lookup != "Test user") + return false; + userRequest.TriggerSuccess(new APIUser { Username = "Test user", @@ -350,7 +353,7 @@ namespace osu.Game.Tests.Scores.IO } [Test] - public void TestUserLookedUpForLegacyOnlineScore() + public void TestUserLookedUpByUsernameForLegacyOnlineScore() { using (HeadlessGameHost host = new CleanRunHeadlessGameHost()) { @@ -364,6 +367,9 @@ namespace osu.Game.Tests.Scores.IO switch (req) { case GetUserRequest userRequest: + if (userRequest.Lookup != "Test user") + return false; + userRequest.TriggerSuccess(new APIUser { Username = "Test user", @@ -413,7 +419,7 @@ namespace osu.Game.Tests.Scores.IO } [Test] - public void TestUserNotLookedUpForOfflineScore() + public void TestUserNotLookedUpForOfflineScoreIfUserIDMissing() { using (HeadlessGameHost host = new CleanRunHeadlessGameHost()) { @@ -427,6 +433,9 @@ namespace osu.Game.Tests.Scores.IO switch (req) { case GetUserRequest userRequest: + if (userRequest.Lookup != "Test user") + return false; + userRequest.TriggerSuccess(new APIUser { Username = "Test user", @@ -476,6 +485,73 @@ namespace osu.Game.Tests.Scores.IO } } + [Test] + public void TestUserLookedUpByOnlineIDIfPresent([Values] bool isOnlineScore) + { + using (HeadlessGameHost host = new CleanRunHeadlessGameHost()) + { + try + { + var osu = LoadOsuIntoHost(host, true); + + var api = (DummyAPIAccess)osu.API; + api.HandleRequest = req => + { + switch (req) + { + case GetUserRequest userRequest: + if (userRequest.Lookup != "5555") + return false; + + userRequest.TriggerSuccess(new APIUser + { + Username = "Some other guy", + CountryCode = CountryCode.DE, + Id = 5555 + }); + return true; + + default: + return false; + } + }; + + var beatmap = BeatmapImportHelper.LoadOszIntoOsu(osu, TestResources.GetQuickTestBeatmapForImport()).GetResultSafely(); + + var toImport = new ScoreInfo + { + Rank = ScoreRank.B, + TotalScore = 987654, + Accuracy = 0.8, + MaxCombo = 500, + Combo = 250, + User = new APIUser { Id = 5555 }, + Date = DateTimeOffset.Now, + Ruleset = new OsuRuleset().RulesetInfo, + BeatmapInfo = beatmap.Beatmaps.First() + }; + if (isOnlineScore) + toImport.OnlineID = 12345; + + var imported = LoadScoreIntoOsu(osu, toImport); + + Assert.AreEqual(toImport.Rank, imported.Rank); + Assert.AreEqual(toImport.TotalScore, imported.TotalScore); + Assert.AreEqual(toImport.Accuracy, imported.Accuracy); + Assert.AreEqual(toImport.MaxCombo, imported.MaxCombo); + Assert.AreEqual(toImport.Date, imported.Date); + Assert.AreEqual(toImport.OnlineID, imported.OnlineID); + Assert.AreEqual("Some other guy", imported.RealmUser.Username); + Assert.AreEqual(5555, imported.RealmUser.OnlineID); + Assert.AreEqual(CountryCode.DE, imported.RealmUser.CountryCode); + } + finally + { + host.Exit(); + } + } + } + public static ScoreInfo LoadScoreIntoOsu(OsuGameBase osu, ScoreInfo score, ArchiveReader archive = null) { // clone to avoid attaching the input score to realm. From abfb2c00bcaaf071b264f82e5205350ec7a5edf4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 6 May 2024 13:24:34 +0200 Subject: [PATCH 0732/2493] Look up users by ID if available when importing scores --- osu.Game/Scoring/ScoreImporter.cs | 71 ++++++++++++++++++++++++++----- 1 file changed, 61 insertions(+), 10 deletions(-) diff --git a/osu.Game/Scoring/ScoreImporter.cs b/osu.Game/Scoring/ScoreImporter.cs index 4ae8e51f6d..69c53af16f 100644 --- a/osu.Game/Scoring/ScoreImporter.cs +++ b/osu.Game/Scoring/ScoreImporter.cs @@ -103,6 +103,14 @@ namespace osu.Game.Scoring } // Very naive local caching to improve performance of large score imports (where the username is usually the same for most or all scores). + + // TODO: `UserLookupCache` cannot currently be used here because of async foibles. + // It only supports lookups by user ID (username would require web changes), and even then the ID lookups cannot be used. + // That is because that component provides an async interface, and async functions cannot be consumed safely here due to the rigid structure of `RealmArchiveModelImporter`. + // The importer has two paths, one async and one sync; the async path runs the sync path in a task. + // This means that sometimes `PostImport()` is called from a sync context, and sometimes from an async one, whilst itself being a sync method. + // That in turn makes `.GetResultSafely()` not callable inside `PostImport()`, as it will throw when called from an async context, + private readonly Dictionary idLookupCache = new Dictionary(); private readonly Dictionary usernameLookupCache = new Dictionary(); protected override void PostImport(ScoreInfo model, Realm realm, ImportParameters parameters) @@ -127,24 +135,34 @@ namespace osu.Game.Scoring if (model.RealmUser.OnlineID == APIUser.SYSTEM_USER_ID) return; - if (model.OnlineID < 0 && model.LegacyOnlineID <= 0) - return; - - string username = model.RealmUser.Username; - - if (usernameLookupCache.TryGetValue(username, out var existing)) + if (model.RealmUser.OnlineID > 1) { - model.User = existing; + model.User = lookupUserById(model.RealmUser.OnlineID) ?? model.User; return; } - var userRequest = new GetUserRequest(username); + if (model.OnlineID < 0 && model.LegacyOnlineID <= 0) + return; + + model.User = lookupUserByName(model.RealmUser.Username) ?? model.User; + } + + private APIUser? lookupUserById(int id) + { + if (idLookupCache.TryGetValue(id, out var existing)) + { + return existing; + } + + var userRequest = new GetUserRequest(id); api.Perform(userRequest); if (userRequest.Response is APIUser user) { - usernameLookupCache.TryAdd(username, new APIUser + APIUser cachedUser; + + idLookupCache.TryAdd(id, cachedUser = new APIUser { // Because this is a permanent cache, let's only store the pieces we're interested in, // rather than the full API response. If we start to store more than these three fields @@ -154,8 +172,41 @@ namespace osu.Game.Scoring CountryCode = user.CountryCode, }); - model.User = user; + return cachedUser; } + + return null; + } + + private APIUser? lookupUserByName(string username) + { + if (usernameLookupCache.TryGetValue(username, out var existing)) + { + return existing; + } + + var userRequest = new GetUserRequest(username); + + api.Perform(userRequest); + + if (userRequest.Response is APIUser user) + { + APIUser cachedUser; + + usernameLookupCache.TryAdd(username, cachedUser = new APIUser + { + // Because this is a permanent cache, let's only store the pieces we're interested in, + // rather than the full API response. If we start to store more than these three fields + // in realm, this should be undone. + Id = user.Id, + Username = user.Username, + CountryCode = user.CountryCode, + }); + + return cachedUser; + } + + return null; } } } From 9c6968c13a2c6a1a0392649dddd1c0cfb7828f1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 6 May 2024 16:29:03 +0200 Subject: [PATCH 0733/2493] Use `score.User.OnlineID` instead of `score.UserID` You'd hope that they'd be the same thing, but post-https://github.com/ppy/osu-server-spectator/pull/230 it turns out that cannot be guaranteed, so just attempt to use `User` in the encoder consistently everywhere... --- osu.Game/Scoring/Legacy/LegacyReplaySoloScoreInfo.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Scoring/Legacy/LegacyReplaySoloScoreInfo.cs b/osu.Game/Scoring/Legacy/LegacyReplaySoloScoreInfo.cs index b71d9d916e..60bec687f4 100644 --- a/osu.Game/Scoring/Legacy/LegacyReplaySoloScoreInfo.cs +++ b/osu.Game/Scoring/Legacy/LegacyReplaySoloScoreInfo.cs @@ -54,7 +54,7 @@ namespace osu.Game.Scoring.Legacy MaximumStatistics = score.MaximumStatistics.Where(kvp => kvp.Value != 0).ToDictionary(), ClientVersion = score.ClientVersion, Rank = score.Rank, - UserID = score.UserID, + UserID = score.User.OnlineID, }; } } From a694f4625309667534c4a76e794ce0728c4b5b7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Erik=20Wolfschl=C3=A4ger?= Date: Mon, 6 May 2024 18:10:58 +0200 Subject: [PATCH 0734/2493] Add new localisable strings --- .../DeleteConfirmationContentStrings.cs | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 osu.Game/Localisation/DeleteConfirmationContentStrings.cs diff --git a/osu.Game/Localisation/DeleteConfirmationContentStrings.cs b/osu.Game/Localisation/DeleteConfirmationContentStrings.cs new file mode 100644 index 0000000000..26b7133456 --- /dev/null +++ b/osu.Game/Localisation/DeleteConfirmationContentStrings.cs @@ -0,0 +1,47 @@ +// 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.Localisation +{ + public static class DeleteConfirmationContentStrings + { + private const string prefix = @"osu.Game.Resources.Localisation.DeleteConfirmationContent"; + + /// + /// "All beatmaps?" + /// + public static LocalisableString Beatmaps => new TranslatableString(getKey(@"beatmaps"), @"All beatmaps?"); + + /// + /// "All beatmaps videos? This cannot be undone!" + /// + public static LocalisableString BeatmapVideos => new TranslatableString(getKey(@"beatmap_videos"), @"All beatmaps videos? This cannot be undone!"); + + /// + /// "All skins? This cannot be undone!" + /// + public static LocalisableString Skins => new TranslatableString(getKey(@"skins"), @"All skins? This cannot be undone!"); + + /// + /// "All collections? This cannot be undone!" + /// + public static LocalisableString Collections => new TranslatableString(getKey(@"collections"), @"All collections? This cannot be undone!"); + + /// + /// "All scores? This cannot be undone!" + /// + public static LocalisableString Scores => new TranslatableString(getKey(@"collections"), @"All scores? This cannot be undone!"); + + /// + /// "All mod presets?" + /// + public static LocalisableString ModPresets => new TranslatableString(getKey(@"mod_presets"), @"All mod presets?"); + + + private static string getKey(string key) => $@"{prefix}:{key}"; + + } +} From 32444e0e30f10e4baadc6aac971c2f66f0bfbce1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Erik=20Wolfschl=C3=A4ger?= Date: Mon, 6 May 2024 18:11:40 +0200 Subject: [PATCH 0735/2493] Make deletion confirmation content less confusing --- .../Sections/Maintenance/BeatmapSettings.cs | 6 +++--- .../Sections/Maintenance/CollectionsSettings.cs | 2 +- .../Maintenance/MassDeleteConfirmationDialog.cs | 5 +++-- .../MassVideoDeleteConfirmationDialog.cs | 16 ---------------- .../Sections/Maintenance/ModPresetSettings.cs | 2 +- .../Sections/Maintenance/ScoreSettings.cs | 2 +- .../Sections/Maintenance/SkinSettings.cs | 2 +- 7 files changed, 10 insertions(+), 25 deletions(-) delete mode 100644 osu.Game/Overlays/Settings/Sections/Maintenance/MassVideoDeleteConfirmationDialog.cs diff --git a/osu.Game/Overlays/Settings/Sections/Maintenance/BeatmapSettings.cs b/osu.Game/Overlays/Settings/Sections/Maintenance/BeatmapSettings.cs index 4b1836ed86..d0a8fc7d2c 100644 --- a/osu.Game/Overlays/Settings/Sections/Maintenance/BeatmapSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Maintenance/BeatmapSettings.cs @@ -31,7 +31,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance { deleteBeatmapsButton.Enabled.Value = false; Task.Run(() => beatmaps.Delete()).ContinueWith(_ => Schedule(() => deleteBeatmapsButton.Enabled.Value = true)); - })); + }, DeleteConfirmationContentStrings.Beatmaps)); } }); @@ -40,11 +40,11 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance Text = MaintenanceSettingsStrings.DeleteAllBeatmapVideos, Action = () => { - dialogOverlay?.Push(new MassVideoDeleteConfirmationDialog(() => + dialogOverlay?.Push(new MassDeleteConfirmationDialog(() => { deleteBeatmapVideosButton.Enabled.Value = false; Task.Run(beatmaps.DeleteAllVideos).ContinueWith(_ => Schedule(() => deleteBeatmapVideosButton.Enabled.Value = true)); - })); + }, DeleteConfirmationContentStrings.BeatmapVideos)); } }); AddRange(new Drawable[] diff --git a/osu.Game/Overlays/Settings/Sections/Maintenance/CollectionsSettings.cs b/osu.Game/Overlays/Settings/Sections/Maintenance/CollectionsSettings.cs index b373535a8b..b1c44aa93c 100644 --- a/osu.Game/Overlays/Settings/Sections/Maintenance/CollectionsSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Maintenance/CollectionsSettings.cs @@ -29,7 +29,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance Text = MaintenanceSettingsStrings.DeleteAllCollections, Action = () => { - dialogOverlay?.Push(new MassDeleteConfirmationDialog(deleteAllCollections)); + dialogOverlay?.Push(new MassDeleteConfirmationDialog(deleteAllCollections, DeleteConfirmationContentStrings.Collections)); } }); } diff --git a/osu.Game/Overlays/Settings/Sections/Maintenance/MassDeleteConfirmationDialog.cs b/osu.Game/Overlays/Settings/Sections/Maintenance/MassDeleteConfirmationDialog.cs index 99ef62d94b..7ead815fe9 100644 --- a/osu.Game/Overlays/Settings/Sections/Maintenance/MassDeleteConfirmationDialog.cs +++ b/osu.Game/Overlays/Settings/Sections/Maintenance/MassDeleteConfirmationDialog.cs @@ -2,15 +2,16 @@ // See the LICENCE file in the repository root for full licence text. using System; +using osu.Framework.Localisation; using osu.Game.Overlays.Dialog; namespace osu.Game.Overlays.Settings.Sections.Maintenance { public partial class MassDeleteConfirmationDialog : DangerousActionDialog { - public MassDeleteConfirmationDialog(Action deleteAction) + public MassDeleteConfirmationDialog(Action deleteAction, LocalisableString deleteContent) { - BodyText = "Everything?"; + BodyText = deleteContent; DangerousAction = deleteAction; } } diff --git a/osu.Game/Overlays/Settings/Sections/Maintenance/MassVideoDeleteConfirmationDialog.cs b/osu.Game/Overlays/Settings/Sections/Maintenance/MassVideoDeleteConfirmationDialog.cs deleted file mode 100644 index 6312e09b3e..0000000000 --- a/osu.Game/Overlays/Settings/Sections/Maintenance/MassVideoDeleteConfirmationDialog.cs +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System; - -namespace osu.Game.Overlays.Settings.Sections.Maintenance -{ - public partial class MassVideoDeleteConfirmationDialog : MassDeleteConfirmationDialog - { - public MassVideoDeleteConfirmationDialog(Action deleteAction) - : base(deleteAction) - { - BodyText = "All beatmap videos? This cannot be undone!"; - } - } -} diff --git a/osu.Game/Overlays/Settings/Sections/Maintenance/ModPresetSettings.cs b/osu.Game/Overlays/Settings/Sections/Maintenance/ModPresetSettings.cs index f0d6d10e51..9c55308abe 100644 --- a/osu.Game/Overlays/Settings/Sections/Maintenance/ModPresetSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Maintenance/ModPresetSettings.cs @@ -42,7 +42,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance { deleteAllButton.Enabled.Value = false; Task.Run(deleteAllModPresets).ContinueWith(t => Schedule(onAllModPresetsDeleted, t)); - })); + }, DeleteConfirmationContentStrings.ModPresets)); } }, undeleteButton = new SettingsButton diff --git a/osu.Game/Overlays/Settings/Sections/Maintenance/ScoreSettings.cs b/osu.Game/Overlays/Settings/Sections/Maintenance/ScoreSettings.cs index c6f4f1e1a5..235f239c7c 100644 --- a/osu.Game/Overlays/Settings/Sections/Maintenance/ScoreSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Maintenance/ScoreSettings.cs @@ -27,7 +27,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance { deleteScoresButton.Enabled.Value = false; Task.Run(() => scores.Delete()).ContinueWith(_ => Schedule(() => deleteScoresButton.Enabled.Value = true)); - })); + }, DeleteConfirmationContentStrings.Scores)); } }); } diff --git a/osu.Game/Overlays/Settings/Sections/Maintenance/SkinSettings.cs b/osu.Game/Overlays/Settings/Sections/Maintenance/SkinSettings.cs index c3ac49af6d..e962118a36 100644 --- a/osu.Game/Overlays/Settings/Sections/Maintenance/SkinSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Maintenance/SkinSettings.cs @@ -27,7 +27,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance { deleteSkinsButton.Enabled.Value = false; Task.Run(() => skins.Delete()).ContinueWith(_ => Schedule(() => deleteSkinsButton.Enabled.Value = true)); - })); + }, DeleteConfirmationContentStrings.Skins)); } }); } From 12522d9ae5dc1ef36055a7698518231f8a74ebbd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Erik=20Wolfschl=C3=A4ger?= Date: Mon, 6 May 2024 18:29:46 +0200 Subject: [PATCH 0736/2493] Fix formatting issues --- osu.Game/Localisation/DeleteConfirmationContentStrings.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/osu.Game/Localisation/DeleteConfirmationContentStrings.cs b/osu.Game/Localisation/DeleteConfirmationContentStrings.cs index 26b7133456..d9e90675f7 100644 --- a/osu.Game/Localisation/DeleteConfirmationContentStrings.cs +++ b/osu.Game/Localisation/DeleteConfirmationContentStrings.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 osu.Framework.Localisation; namespace osu.Game.Localisation @@ -40,8 +39,6 @@ namespace osu.Game.Localisation /// public static LocalisableString ModPresets => new TranslatableString(getKey(@"mod_presets"), @"All mod presets?"); - private static string getKey(string key) => $@"{prefix}:{key}"; - } } From f824bd14414e9e37af05a9dba130c02c7aa5a62d Mon Sep 17 00:00:00 2001 From: Susko3 Date: Mon, 6 May 2024 21:52:03 +0200 Subject: [PATCH 0737/2493] Fix `userTriggered` not being passed to private helper --- osu.Game/Overlays/SkinEditor/SkinEditor.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/SkinEditor/SkinEditor.cs b/osu.Game/Overlays/SkinEditor/SkinEditor.cs index 5bf28ae79b..67fd6a9550 100644 --- a/osu.Game/Overlays/SkinEditor/SkinEditor.cs +++ b/osu.Game/Overlays/SkinEditor/SkinEditor.cs @@ -540,7 +540,7 @@ namespace osu.Game.Overlays.SkinEditor protected void Redo() => changeHandler?.RestoreState(1); - public void Save(bool userTriggered = true) => save(currentSkin.Value); + public void Save(bool userTriggered = true) => save(currentSkin.Value, userTriggered); private void save(Skin skin, bool userTriggered = true) { From 91c684151a740005e24c3c52ff188772489b5171 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 7 May 2024 14:16:25 +0800 Subject: [PATCH 0738/2493] Update bundled beatmaps --- .../Drawables/BundledBeatmapDownloader.cs | 504 +++++++++++------- 1 file changed, 312 insertions(+), 192 deletions(-) diff --git a/osu.Game/Beatmaps/Drawables/BundledBeatmapDownloader.cs b/osu.Game/Beatmaps/Drawables/BundledBeatmapDownloader.cs index 21ab1b78ea..3aa34a5580 100644 --- a/osu.Game/Beatmaps/Drawables/BundledBeatmapDownloader.cs +++ b/osu.Game/Beatmaps/Drawables/BundledBeatmapDownloader.cs @@ -6,6 +6,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Text.RegularExpressions; using osu.Framework.Allocation; @@ -21,6 +22,8 @@ using osu.Game.Utils; namespace osu.Game.Beatmaps.Drawables { + [SuppressMessage("ReSharper", "StringLiteralTypo")] + [SuppressMessage("ReSharper", "CommentTypo")] public partial class BundledBeatmapDownloader : CompositeDrawable { private readonly bool shouldPostNotifications; @@ -50,7 +53,7 @@ namespace osu.Game.Beatmaps.Drawables { queueDownloads(always_bundled_beatmaps); - queueDownloads(bundled_osu, 8); + queueDownloads(bundled_osu, 6); queueDownloads(bundled_taiko, 3); queueDownloads(bundled_catch, 3); queueDownloads(bundled_mania, 3); @@ -128,6 +131,26 @@ namespace osu.Game.Beatmaps.Drawables } } + /* + * criteria for bundled maps (managed by pishifat) + * + * auto: + * - licensed song + * - includes ENHI diffs + * - between 60s and 240s + * + * manual: + * - bg is explicitly permitted as okay to use. lots of artists say some variation of "it's ok for personal use/non-commercial use/with credit" + * (which is prob fine when maps are presented as user-generated content), but for a new osu! player, it's easy to assume bundled maps are + * commercial content like other rhythm games, so it's best to be cautious about using not-explicitly-permitted artwork. + * + * - no ai/thirst bgs + * - no controversial/explicit song content or titles + * - no repeating bundled songs (within each mode) + * - no songs that are relatively low production value + * - no songs with limited accessibility (annoying high pitch vocals, noise rock, etc) + */ + private const string tutorial_filename = "1011011 nekodex - new beginnings.osz"; /// @@ -135,215 +158,312 @@ namespace osu.Game.Beatmaps.Drawables /// private static readonly string[] always_bundled_beatmaps = { - // This thing is 40mb, I'm not sure we want it here... + // winner of https://osu.ppy.sh/home/news/2013-09-06-osu-monthly-beatmapping-contest-1 + @"123593 Rostik - Liquid (Paul Rosenthal Remix).osz", + // winner of https://osu.ppy.sh/home/news/2013-10-28-monthly-beatmapping-contest-2-submissions-open + @"140662 cYsmix feat. Emmy - Tear Rain.osz", + // winner of https://osu.ppy.sh/home/news/2013-12-15-monthly-beatmapping-contest-3-submissions-open + @"151878 Chasers - Lost.osz", + // winner of https://osu.ppy.sh/home/news/2014-02-14-monthly-beatmapping-contest-4-submissions-now + @"163112 Kuba Oms - My Love.osz", + // winner of https://osu.ppy.sh/home/news/2014-05-07-monthly-beatmapping-contest-5-submissions-now + @"190390 Rameses B - Flaklypa.osz", + // winner of https://osu.ppy.sh/home/news/2014-09-24-monthly-beatmapping-contest-7 + @"241526 Soleily - Renatus.osz", + // winner of https://osu.ppy.sh/home/news/2015-02-11-monthly-beatmapping-contest-8 + @"299224 raja - the light.osz", + // winner of https://osu.ppy.sh/home/news/2015-04-13-monthly-beatmapping-contest-9-taiko-only + @"319473 Furries in a Blender - Storm World.osz", + // winner of https://osu.ppy.sh/home/news/2015-06-15-monthly-beatmapping-contest-10-ctb-only + @"342751 Hylian Lemon - Foresight Is for Losers.osz", + // winner of https://osu.ppy.sh/home/news/2015-08-22-monthly-beatmapping-contest-11-mania-only + @"385056 Toni Leys - Dragon Valley (Toni Leys Remix feat. Esteban Bellucci).osz", + // winner of https://osu.ppy.sh/home/news/2016-03-04-beatmapping-contest-12-osu + @"456054 IAHN - Candy Luv (Short Ver.).osz", + // winner of https://osu.ppy.sh/home/news/2020-11-30-a-labour-of-love + // (this thing is 40mb, I'm not sure if we want it here...) @"1388906 Raphlesia & BilliumMoto - My Love.osz", - // Winner of Triangles mapping competition: https://osu.ppy.sh/home/news/2022-10-06-results-triangles + // winner of https://osu.ppy.sh/home/news/2022-05-31-triangles @"1841885 cYsmix - triangles.osz", + // winner of https://osu.ppy.sh/home/news/2023-02-01-twin-trials-contest-beatmapping-phase + @"1971987 James Landino - Aresene's Bazaar.osz", }; private static readonly string[] bundled_osu = { - "682286 Yuyoyuppe - Emerald Galaxy.osz", - "682287 baker - For a Dead Girl+.osz", - "682289 Hige Driver - I Wanna Feel Your Love (feat. shully).osz", - "682290 Hige Driver - Miracle Sugite Yabai (feat. shully).osz", - "682416 Hige Driver - Palette.osz", - "682595 baker - Kimi ga Kimi ga -vocanico remix-.osz", - "716211 yuki. - Spring Signal.osz", - "716213 dark cat - BUBBLE TEA (feat. juu & cinders).osz", - "716215 LukHash - CLONED.osz", - "716219 IAHN - Snowdrop.osz", - "716249 *namirin - Senaka Awase no Kuukyo (with Kakichoco).osz", - "716390 sakuraburst - SHA.osz", - "716441 Fractal Dreamers - Paradigm Shift.osz", - "729808 Thaehan - Leprechaun.osz", - "751771 Cranky - Hanaarashi.osz", - "751772 Cranky - Ran.osz", - "751773 Cranky - Feline, the White....osz", - "751774 Function Phantom - Variable.osz", - "751779 Rin - Daishibyo set 14 ~ Sado no Futatsuiwa.osz", - "751782 Fractal Dreamers - Fata Morgana.osz", - "751785 Cranky - Chandelier - King.osz", - "751846 Fractal Dreamers - Celestial Horizon.osz", - "751866 Rin - Moriya set 08 ReEdit ~ Youkai no Yama.osz", - "751894 Fractal Dreamers - Blue Haven.osz", - "751896 Cranky - Rave 2 Rave.osz", - "751932 Cranky - La fuite des jours.osz", - "751972 Cranky - CHASER.osz", - "779173 Thaehan - Superpower.osz", - "780932 VINXIS - A Centralized View.osz", - "785572 S3RL - I'll See You Again (feat. Chi Chi).osz", - "785650 yuki. feat. setsunan - Hello! World.osz", - "785677 Dictate - Militant.osz", - "785731 S3RL - Catchit (Radio Edit).osz", - "785774 LukHash - GLITCH.osz", - "786498 Trial & Error - Tokoyami no keiyaku KEGARETA-SHOUJO feat. GUMI.osz", - "789374 Pulse - LP.osz", - "789528 James Portland - Sky.osz", - "789529 Lexurus - Gravity.osz", - "789544 Andromedik - Invasion.osz", - "789905 Gourski x Himmes - Silence.osz", - "791667 cYsmix - Babaroque (Short Ver.).osz", - "791798 cYsmix - Behind the Walls.osz", - "791845 cYsmix - Little Knight.osz", - "792241 cYsmix - Eden.osz", - "792396 cYsmix - The Ballad of a Mindless Girl.osz", - "795432 Phonetic - Journey.osz", - "831322 DJ'TEKINA//SOMETHING - Hidamari no Uta.osz", - "847764 Cranky - Crocus.osz", - "847776 Culprate & Joe Ford - Gaucho.osz", - "847812 J. Pachelbel - Canon (Cranky Remix).osz", - "847900 Cranky - Time Alter.osz", - "847930 LukHash - 8BIT FAIRY TALE.osz", - "848003 Culprate - Aurora.osz", - "848068 nanobii - popsicle beach.osz", - "848090 Trial & Error - DAI*TAN SENSATION feat. Nanahira, Mii, Aitsuki Nakuru (Short Ver.).osz", - "848259 Culprate & Skorpion - Jester.osz", - "848976 Dictate - Treason.osz", - "851543 Culprate - Florn.osz", - "864748 Thaehan - Angry Birds Epic (Remix).osz", - "873667 OISHII - ONIGIRI FREEWAY.osz", - "876227 Culprate, Keota & Sophie Meiers - Mechanic Heartbeat.osz", - "880487 cYsmix - Peer Gynt.osz", - "883088 Wisp X - Somewhere I'd Rather Be.osz", - "891333 HyuN - White Aura.osz", - "891334 HyuN - Wild Card.osz", - "891337 HyuN feat. LyuU - Cross Over.osz", - "891338 HyuN & Ritoru - Apocalypse in Love.osz", - "891339 HyuN feat. Ato - Asu wa Ame ga Yamukara.osz", - "891345 HyuN - Infinity Heaven.osz", - "891348 HyuN - Guitian.osz", - "891356 HyuN - Legend of Genesis.osz", - "891366 HyuN - Illusion of Inflict.osz", - "891417 HyuN feat. Yu-A - My life is for you.osz", - "891441 HyuN - You'Re aRleAdY dEAd.osz", - "891632 HyuN feat. YURI - Disorder.osz", - "891712 HyuN - Tokyo's Starlight.osz", - "901091 *namirin - Ciel etoile.osz", - "916990 *namirin - Koishiteiku Planet.osz", - "929284 tieff - Sense of Nostalgia.osz", - "933940 Ben Briggs - Yes (Maybe).osz", - "934415 Ben Briggs - Fearless Living.osz", - "934627 Ben Briggs - New Game Plus.osz", - "934666 Ben Briggs - Wave Island.osz", - "936126 siromaru + cranky - conflict.osz", - "940377 onumi - ARROGANCE.osz", - "940597 tieff - Take Your Swimsuit.osz", - "941085 tieff - Our Story.osz", - "949297 tieff - Sunflower.osz", - "952380 Ben Briggs - Why Are We Yelling.osz", - "954272 *namirin - Kanzen Shouri*Esper Girl.osz", - "955866 KIRA & Heartbreaker - B.B.F (feat. Hatsune Miku & Kagamine Rin).osz", - "961320 Kuba Oms - All In All.osz", - "964553 The Flashbulb - You Take the World's Weight Away.osz", - "965651 Fractal Dreamers - Ad Astra.osz", - "966225 The Flashbulb - Passage D.osz", - "966324 DJ'TEKINA//SOMETHING - Hidamari no Uta.osz", - "972810 James Landino & Kabuki - Birdsong.osz", - "972932 James Landino - Hide And Seek.osz", - "977276 The Flashbulb - Mellann.osz", - "981616 *namirin - Mizutamari Tobikoete (with Nanahira).osz", - "985788 Loki - Wizard's Tower.osz", - "996628 OISHII - ONIGIRI FREEWAY.osz", - "996898 HyuN - White Aura.osz", - "1003554 yuki. - Nadeshiko Sensation.osz", - "1014936 Thaehan - Bwa !.osz", - "1019827 UNDEAD CORPORATION - Sad Dream.osz", - "1020213 Creo - Idolize.osz", - "1021450 Thaehan - Chiptune & Baroque.osz", + @"682286 Yuyoyuppe - Emerald Galaxy.osz", + @"682287 baker - For a Dead Girl+.osz", + @"682595 baker - Kimi ga Kimi ga -vocanico remix-.osz", + @"1048705 Thaehan - Never Give Up.osz", + @"1050185 Carpool Tunnel - Hooked Again.osz", + @"1052846 Carpool Tunnel - Impressions.osz", + @"1062477 Ricky Montgomery - Line Without a Hook.osz", + @"1081119 Celldweller - Pulsar.osz", + @"1086289 Frums - 24eeev0-$.osz", + @"1133317 PUP - Free At Last.osz", + @"1171188 PUP - Full Blown Meltdown.osz", + @"1177043 PUP - My Life Is Over And I Couldn't Be Happier.osz", + @"1250387 Circle of Dust - Humanarchy (Cut Ver.).osz", + @"1255411 Wisp X - Somewhere I'd Rather Be.osz", + @"1320298 nekodex - Little Drummer Girl.osz", + @"1323877 Masahiro ""Godspeed"" Aoki - Blaze.osz", + @"1342280 Minagu feat. Aitsuki Nakuru - Theater Endroll.osz", + @"1356447 SECONDWALL - Boku wa Boku de shika Nakute.osz", + @"1368054 SECONDWALL - Shooting Star.osz", + @"1398580 La priere - Senjou no Utahime.osz", + @"1403962 m108 - Sunflower.osz", + @"1405913 fiend - FEVER DREAM (feat. yzzyx).osz", + @"1409184 Omoi - Hey William (New Translation).osz", + @"1413418 URBANGARDE - KAMING OUT (Cut Ver.).osz", + @"1417793 P4koo (NONE) - Sogaikan Utopia.osz", + @"1428384 DUAL ALTER WORLD - Veracila.osz", + @"1442963 PUP - DVP.osz", + @"1460370 Sound Souler - Empty Stars.osz", + @"1485184 Koven - Love Wins Again.osz", + @"1496811 T & Sugah - Wicked Days (Cut Ver.).osz", + @"1501511 Masahiro ""Godspeed"" Aoki - Frostbite (Cut Ver.).osz", + @"1511518 T & Sugah X Zazu - Lost On My Own (Cut Ver.).osz", + @"1516617 wotoha - Digital Life Hacker.osz", + @"1524273 Michael Cera Palin - Admiral.osz", + @"1564234 P4koo - Fly High (feat. rerone).osz", + @"1572918 Lexurus - Take Me Away (Cut Ver.).osz", + @"1577313 Kurubukko - The 84th Flight.osz", + @"1587839 Amidst - Droplet.osz", + @"1595193 BlackY - Sakura Ranman Cleopatra.osz", + @"1667560 xi - FREEDOM DiVE.osz", + @"1668789 City Girl - L2M (feat. Kelsey Kuan).osz", + @"1672934 xi - Parousia.osz", + @"1673457 Boom Kitty - Any Other Way (feat. Ivy Marie).osz", + @"1685122 xi - Time files.osz", + @"1689372 NIWASHI - Y.osz", + @"1729551 JOYLESS - Dream.osz", + @"1742868 Ritorikal - Synergy.osz", + @"1757511 KINEMA106 - KARASU.osz", + @"1778169 Ricky Montgomery - Cabo.osz", + @"1848184 FRASER EDWARDS - Ruination.osz", + @"1862574 Pegboard Nerds - Try This (Cut Ver.).osz", + @"1873680 happy30 - You spin my world.osz", + @"1890055 A.SAKA - Mutsuki Akari no Yuki.osz", + @"1911933 Marmalade butcher - Waltz for Chroma (feat. Natsushiro Takaaki).osz", + @"1940007 Mili - Ga1ahad and Scientific Witchery.osz", + @"1948970 Shadren - You're Here Forever.osz", + @"1967856 Annabel - alpine blue.osz", + @"1969316 Silentroom - NULCTRL.osz", + @"1978614 Krimek - Idyllic World.osz", + @"1991315 Feint - Tower Of Heaven (You Are Slaves) (Cut Ver.).osz", + @"1997470 tephe - Genjitsu Escape.osz", + @"1999116 soowamisu - .vaporcore.osz", + @"2010589 Junk - Yellow Smile (bms edit).osz", + @"2022054 Yokomin - STINGER.osz", + @"2025686 Aice room - For U.osz", + @"2035357 C-Show feat. Ishizawa Yukari - Border Line.osz", + @"2039403 SECONDWALL - Freedom.osz", + @"2046487 Rameses B - Against the Grain (feat. Veela).osz", + @"2052201 ColBreakz & Vizzen - Remember.osz", + @"2055535 Sephid - Thunderstrike 1988.osz", + @"2057584 SAMString - Ataraxia.osz", + @"2067270 Blue Stahli - The Fall.osz", + @"2075039 garlagan - Skyless.osz", + @"2079089 Hamu feat. yuiko - Innocent Letter.osz", + @"2082895 FATE GEAR - Heart's Grave.osz", + @"2085974 HoneyComeBear - Twilight.osz", + @"2094934 F.O.O.L & Laura Brehm - Waking Up.osz", + @"2097481 Mameyudoufu - Wave feat. Aitsuki Nakuru.osz", + @"2106075 MYUKKE. - The 89's Momentum.osz", + @"2117392 t+pazolite & Komiya Mao - Elustametat.osz", + @"2123533 LeaF - Calamity Fortune.osz", + @"2143876 Alkome - Your Voice.osz", + @"2145826 Sephid - Cross-D Skyline.osz", + @"2153172 Emiru no Aishita Tsukiyo ni Dai San Gensou Kyoku wo - Eternal Bliss.osz", }; private static readonly string[] bundled_taiko = { - "707824 Fractal Dreamers - Fortuna Redux.osz", - "789553 Cranky - Ran.osz", - "827822 Function Phantom - Neuronecia.osz", - "847323 Nakanojojo - Bittersweet (feat. Kuishinboakachan a.k.a Kiato).osz", - "847433 Trial & Error - Tokoyami no keiyaku KEGARETA-SHOUJO feat. GUMI.osz", - "847576 dark cat - hot chocolate.osz", - "847957 Wisp X - Final Moments.osz", - "876282 VINXIS - Greetings.osz", - "876648 Thaehan - Angry Birds Epic (Remix).osz", - "877069 IAHN - Transform (Original Mix).osz", - "877496 Thaehan - Leprechaun.osz", - "877935 Thaehan - Overpowered.osz", - "878344 yuki. - Be Your Light.osz", - "918446 VINXIS - Facade.osz", - "918903 LukHash - Ghosts.osz", - "919251 *namirin - Hitokoto no Kyori.osz", - "919704 S3RL - I Will Pick You Up (feat. Tamika).osz", - "921535 SOOOO - Raven Haven.osz", - "927206 *namirin - Kanzen Shouri*Esper Girl.osz", - "927544 Camellia feat. Nanahira - Kansoku Eisei.osz", - "930806 Nakanojojo - Pararara (feat. Amekoya).osz", - "931741 Camellia - Quaoar.osz", - "935699 Rin - Mythic set ~ Heart-Stirring Urban Legends.osz", - "935732 Thaehan - Yuujou.osz", - "941145 Function Phantom - Euclid.osz", - "942334 Dictate - Cauldron.osz", - "946540 nanobii - astral blast.osz", - "948844 Rin - Kishinjou set 01 ~ Mist Lake.osz", - "949122 Wisp X - Petal.osz", - "951618 Rin - Kishinjou set 02 ~ Mermaid from the Uncharted Land.osz", - "957412 Rin - Lunatic set 16 ~ The Space Shrine Maiden Returns Home.osz", - "961335 Thaehan - Insert Coin.osz", - "965178 The Flashbulb - DIDJ PVC.osz", - "966087 The Flashbulb - Creep.osz", - "966277 The Flashbulb - Amen Iraq.osz", - "966407 LukHash - ROOM 12.osz", - "966451 The Flashbulb - Six Acid Strings.osz", - "972301 BilliumMoto - four veiled stars.osz", - "973173 nanobii - popsicle beach.osz", - "973954 BilliumMoto - Rocky Buinne (Short Ver.).osz", - "975435 BilliumMoto - life flashes before weeb eyes.osz", - "978759 L. V. Beethoven - Moonlight Sonata (Cranky Remix).osz", - "982559 BilliumMoto - HDHR.osz", - "984361 The Flashbulb - Ninedump.osz", - "1023681 Inferi - The Ruin of Mankind.osz", - "1034358 ALEPH - The Evil Spirit.osz", - "1037567 ALEPH - Scintillations.osz", + "1048153 Chroma - [@__@].osz", + "1229307 Venetian Snares - Shaky Sometimes.osz", + "1236083 meganeko - Sirius A (osu! edit).osz", + "1248594 Noisia - Anomaly.osz", + "1272851 siqlo - One Way Street.osz", + "1290736 Kola Kid - good old times.osz", + "1318825 SECONDWALL - Light.osz", + "1320872 MYUKKE. - The 89's Momentum.osz", + "1337389 cute girls doing cute things - Main Heroine.osz", + "1397782 Reku Mochizuki - Yorixiro.osz", + "1407228 II-L - VANGUARD-1.osz", + "1422686 II-L - VANGUARD-2.osz", + "1429217 Street - Phi.osz", + "1442235 2ToneDisco x Cosmicosmo - Shoelaces (feat. Puniden).osz", + "1447478 Cres. - End Time.osz", + "1449942 m108 - Crescent Sakura.osz", + "1463778 MuryokuP - A tree without a branch.osz", + "1465152 fiend - Fever Dream (feat. yzzyx).osz", + "1472397 MYUKKE. - Boudica.osz", + "1488148 Aoi vs. siqlo - Hacktivism.osz", + "1522733 wotoha - Digital Life Hacker.osz", + "1540010 Marmalade butcher - Floccinaucinihilipilification.osz", + "1584690 MYUKKE. - AKKERA-COUNTRY-BOY.osz", + "1608857 BLOOD STAIN CHILD - S.O.P.H.I.A.osz", + "1609365 Reku Mochizuki - Faith of Eastward.osz", + "1622545 METAROOM - I - DINKI THE STARGUIDE.osz", + "1629336 METAROOM - PINK ORIGINS.osz", + "1644680 Neko Hacker - Pictures feat. 4s4ki.osz", + "1650835 RiraN - Ready For The Madness.osz", + "1661508 PTB10 - Starfall.osz", + "1671987 xi - World Fragments II.osz", + "1703065 tokiwa - wasurena feat. Sennzai.osz", + "1703527 tokiwa feat. Nakamura Sanso - Kotodama Refrain.osz", + "1704340 A-One feat. Shihori - Magic Girl !!.osz", + "1712783 xi - Parousia.osz", + "1718774 Harumaki Gohan - Suisei ni Nareta nara.osz", + "1719687 EmoCosine - Love Kills U.osz", + "1733940 WHITEFISTS feat. Sennzai - Paralyzed Ash.osz", + "1734692 EmoCosine - Cutter.osz", + "1739529 luvlxckdown - tbh i dont like being social.osz", + "1756970 Kurubukko vs. yukitani - Minamichita EVOLVED.osz", + "1762209 Marmalade butcher - Immortality Math Club.osz", + "1765720 ZxNX - FORTALiCE.osz", + "1786165 NILFRUITS - Arandano.osz", + "1787258 SAMString - Night Fighter.osz", + "1791462 ZxNX - Schadenfreude.osz", + "1793821 Kobaryo - The Lightning Sword.osz", + "1796440 kuru x miraie - re:start.osz", + "1799285 Origami Angel - 666 Flags.osz", + "1812415 nanobii - Rainbow Road.osz", + "1814682 NIWASHI - Y.osz", + "1818361 meganeko - Feral (osu! edit).osz", + "1818924 fiend - Disconnect.osz", + "1838730 Pegboard Nerds - Disconnected.osz", + "1854710 Blaster & Extra Terra - Spacecraft (Cut Ver.).osz", + "1859322 Hino Isuka - Delightness Brightness.osz", + "1884102 Maduk - Go (feat. Lachi) (Cut Ver.).osz", + "1884578 Neko Hacker - People People feat. Nanahira.osz", + "1897902 uma vs. Morimori Atsushi - Re: End of a Dream.osz", + "1905582 KINEMA106 - Fly Away (Cut Ver.).osz", + "1934686 ARForest - Rainbow Magic!!.osz", + "1963076 METAROOM - S.N.U.F.F.Y.osz", + "1968973 Stars Hollow - Out the Sunroof..osz", + "1971951 James Landino - Shiba Paradise.osz", + "1972518 Toromaru - Sleight of Hand.osz", + "1982302 KINEMA106 - INVITE.osz", + "1983475 KNOWER - The Government Knows.osz", + "2010165 Junk - Yellow Smile (bms edit).osz", + "2022737 Andora - Euphoria (feat. WaMi).osz", + "2025023 tephe - Genjitsu Escape.osz", + "2052754 P4koo - 8th:Planet ~Re:search~.osz", + "2054122 Raimukun - Myths Orbis.osz", + "2121470 Raimukun - Nyarlathotep's Dreamland.osz", + "2122284 Agressor Bunx - Tornado (Cut Ver.).osz", + "2125034 Agressor Bunx - Acid Mirage (Cut Ver.).osz", + "2136263 Se-U-Ra - Cris Fortress.osz", }; private static readonly string[] bundled_catch = { - "554256 Helblinde - When Time Sleeps.osz", - "693123 yuki. - Nadeshiko Sensation.osz", - "767009 OISHII - PIZZA PLAZA.osz", - "767346 Thaehan - Bwa !.osz", - "815162 VINXIS - Greetings.osz", - "840964 cYsmix - Breeze.osz", - "932657 Wisp X - Eventide.osz", - "933700 onumi - CONFUSION PART ONE.osz", - "933984 onumi - PERSONALITY.osz", - "934785 onumi - FAKE.osz", - "936545 onumi - REGRET PART ONE.osz", - "943803 Fractal Dreamers - Everything for a Dream.osz", - "943876 S3RL - I Will Pick You Up (feat. Tamika).osz", - "946773 Trial & Error - DREAMING COLOR (Short Ver.).osz", - "955808 Trial & Error - Tokoyami no keiyaku KEGARETA-SHOUJO feat. GUMI (Short Ver.).osz", - "957808 Fractal Dreamers - Module_410.osz", - "957842 antiPLUR - One Life Left to Live.osz", - "965730 The Flashbulb - Lawn Wake IV (Black).osz", - "966240 Creo - Challenger.osz", - "968232 Rin - Lunatic set 15 ~ The Moon as Seen from the Shrine.osz", - "972302 VINXIS - A Centralized View.osz", - "972887 HyuN - Illusion of Inflict.osz", - "1008600 LukHash - WHEN AN ANGEL DIES.osz", - "1032103 LukHash - H8 U.osz", + @"693123 yuki. - Nadeshiko Sensation.osz", + @"833719 FOLiACETATE - Heterochromia Iridis.osz", + @"981762 siromaru + cranky - conflict.osz", + @"1008600 LukHash - WHEN AN ANGEL DIES.osz", + @"1071294 dark cat - pursuit of happiness.osz", + @"1102115 meganeko - Nova.osz", + @"1115500 Chopin - Etude Op. 25, No. 12 (meganeko Remix).osz", + @"1128274 LeaF - Wizdomiot.osz", + @"1141049 HyuN feat. JeeE - Fallen Angel.osz", + @"1148215 Zekk - Fluctuation.osz", + @"1151833 ginkiha - nightfall.osz", + @"1158124 PUP - Dark Days.osz", + @"1184890 IAHN - Transform (Original Mix).osz", + @"1195922 Disasterpeace - Home.osz", + @"1197461 MIMI - Nanimo nai Youna.osz", + @"1197924 Camellia feat. Nanahira - Looking For A New Adventure.osz", + @"1203594 ginkiha - Anemoi.osz", + @"1211572 MIMI - Lapis Lazuli.osz", + @"1231601 Lime - Harmony.osz", + @"1240162 P4koo - 8th:Planet ~Re:search~.osz", + @"1246000 Zekk - Calling.osz", + @"1249928 Thaehan - Yuujou.osz", + @"1258751 Umeboshi Chazuke - ICHIBANBOSHI*ROCKET.osz", + @"1264818 Umeboshi Chazuke - Panic! Pop'n! Picnic! (2019 REMASTER).osz", + @"1280183 IAHN - Mad Halloween.osz", + @"1303201 Umeboshi Chazuke - Run*2 Run To You!!.osz", + @"1328918 Kobaryo - Theme for Psychopath Justice.osz", + @"1338215 Lime - Renai Syndrome.osz", + @"1338796 uma vs. Morimori Atsushi - Re:End of a Dream.osz", + @"1340492 MYUKKE. - The 89's Momentum.osz", + @"1393933 Mastermind (xi+nora2r) - Dreadnought.osz", + @"1400205 m108 - XIII Charlotte.osz", + @"1471328 Lime - Chronomia.osz", + @"1503591 Origami Angel - The Title Track.osz", + @"1524173 litmus* as Ester - Krave.osz", + @"1541235 Getty vs. DJ DiA - Grayed Out -Antifront-.osz", + @"1554250 Shawn Wasabi - Otter Pop (feat. Hollis).osz", + @"1583461 Sound Souler - Absent Color.osz", + @"1638487 tokiwa - wasurena feat. Sennzai.osz", + @"1698949 ZxNX - Schadenfreude.osz", + @"1704324 xi - Time files.osz", + @"1756405 Fractal Dreamers - Kingdom of Silence.osz", + @"1769575 cYsmix - Peer Gynt.osz", + @"1770054 Ardolf - Split.osz", + @"1772648 in love with a ghost - interdimensional portal leading to a cute place feat. snail's house.osz", + @"1776379 in love with a ghost - i thought we were lovers w/ basil.osz", + @"1779476 URBANGARDE - KIMI WA OKUMAGASO.osz", + @"1789435 xi - Parousia.osz", + @"1794190 Se-U-Ra - The Endless for Traveler.osz", + @"1799889 Waterflame - Ricochet Love.osz", + @"1816401 Gram vs. Yooh - Apocalypse.osz", + @"1826327 -45 - Total Eclipse of The Sun.osz", + @"1830796 xi - Halcyon.osz", + @"1924231 Mili - Nine Point Eight.osz", + @"1952903 Cres. - End Time.osz", + @"1970946 Good Kid - Slingshot.osz", + @"1982063 linear ring - enchanted love.osz", + @"2000438 Toromaru - Erinyes.osz", + @"2124277 II-L - VANGUARD-3.osz", + @"2147529 Nashimoto Ui - AaAaAaAAaAaAAa (Cut Ver.).osz", }; private static readonly string[] bundled_mania = { - "943516 antiPLUR - Clockwork Spooks.osz", - "946394 VINXIS - Three Times The Original Charm.osz", - "966408 antiPLUR - One Life Left to Live.osz", - "971561 antiPLUR - Runengon.osz", - "983864 James Landino - Shiba Island.osz", - "989512 BilliumMoto - 1xMISS.osz", - "994104 James Landino - Reaction feat. Slyleaf.osz", - "1003217 nekodex - circles!.osz", - "1009907 James Landino & Kabuki - Birdsong.osz", - "1015169 Thaehan - Insert Coin.osz", + @"1008419 BilliumMoto - Four Veiled Stars.osz", + @"1025170 Frums - We Want To Run.osz", + @"1092856 F-777 - Viking Arena.osz", + @"1139247 O2i3 - Heart Function.osz", + @"1154007 LeaF - ATHAZA.osz", + @"1170054 Zekk - Fallen.osz", + @"1212132 Street - Koiyamai (TV Size).osz", + @"1226466 Se-U-Ra - Elif to Shiro Kura no Yoru -Called-.osz", + @"1247210 Frums - Credits.osz", + @"1254196 ARForest - Regret.osz", + @"1258829 Umeboshi Chazuke - Cineraria.osz", + @"1300398 ARForest - The Last Page.osz", + @"1305627 Frums - Star of the COME ON!!.osz", + @"1348806 Se-U-Ra - LOA2.osz", + @"1375449 yuki. - Nadeshiko Sensation.osz", + @"1448292 Cres. - End Time.osz", + @"1479741 Reku Mochizuki - FORViDDEN ENERZY -Fataldoze-.osz", + @"1494747 Fractal Dreamers - Whispers from a Distant Star.osz", + @"1505336 litmus* - Rush-More.osz", + @"1508963 ARForest - Rainbow Magic!!.osz", + @"1727126 Chroma - Strange Inventor.osz", + @"1737101 ZxNX - FORTALiCE.osz", + @"1740952 Sobrem x Silentroom - Random.osz", + @"1756251 Plum - Mad Piano Party.osz", + @"1909163 Frums - theyaremanycolors.osz", + @"1916285 siromaru + cranky - conflict.osz", + @"1948972 Ardolf - Split.osz", + @"1957138 GLORYHAMMER - Rise Of The Chaos Wizards.osz", + @"1972411 James Landino - Shiba Paradise.osz", + @"1978179 Andora - Flicker (feat. RANASOL).osz", + @"1987180 cygnus - The Evolution of War.osz", + @"1994458 tephe - Genjitsu Escape.osz", + @"1999339 Aice room - Nyan Nyan Dive (EmoCosine Remix).osz", + @"2015361 HoneyComeBear - Rainy Girl.osz", + @"2028108 HyuN - Infinity Heaven.osz", + @"2055329 miraie & blackwinterwells - facade.osz", + @"2069877 Sephid - Thunderstrike 1988.osz", + @"2119716 Aethoro - Snowy.osz", + @"2120379 Synthion - VIVIDVELOCITY.osz", + @"2124805 Frums (unknown ""lambda"") - 19ZZ.osz", + @"2127811 Wiklund - Joy of Living (Cut Ver.).osz", }; } } From 1d6915478f485998a47e6926bd3927dd533d1309 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 7 May 2024 14:26:49 +0800 Subject: [PATCH 0739/2493] Add message letting users know that beatmaps are donwloading in the background --- .../FirstRunSetupBeatmapScreenStrings.cs | 5 +++++ .../Overlays/FirstRunSetup/ScreenBeatmaps.cs | 16 ++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/osu.Game/Localisation/FirstRunSetupBeatmapScreenStrings.cs b/osu.Game/Localisation/FirstRunSetupBeatmapScreenStrings.cs index a77ee066e4..50a417312d 100644 --- a/osu.Game/Localisation/FirstRunSetupBeatmapScreenStrings.cs +++ b/osu.Game/Localisation/FirstRunSetupBeatmapScreenStrings.cs @@ -39,6 +39,11 @@ namespace osu.Game.Localisation /// public static LocalisableString BundledButton => new TranslatableString(getKey(@"bundled_button"), @"Get recommended beatmaps"); + /// + /// "Beatmaps will be downloaded in the background. You can continue with setup while this happens!" + /// + public static LocalisableString DownloadingInBackground => new TranslatableString(getKey(@"downloading_in_background"), @"Beatmaps will be downloaded in the background. You can continue with setup while this happens!"); + /// /// "You can also obtain more beatmaps from the main menu "browse" button at any time." /// diff --git a/osu.Game/Overlays/FirstRunSetup/ScreenBeatmaps.cs b/osu.Game/Overlays/FirstRunSetup/ScreenBeatmaps.cs index 385695f669..da60951ab6 100644 --- a/osu.Game/Overlays/FirstRunSetup/ScreenBeatmaps.cs +++ b/osu.Game/Overlays/FirstRunSetup/ScreenBeatmaps.cs @@ -15,6 +15,7 @@ using osu.Game.Graphics.Containers; using osu.Game.Localisation; using osu.Game.Online; using osuTK; +using osuTK.Graphics; using Realms; namespace osu.Game.Overlays.FirstRunSetup @@ -25,6 +26,8 @@ namespace osu.Game.Overlays.FirstRunSetup private ProgressRoundedButton downloadBundledButton = null!; private ProgressRoundedButton downloadTutorialButton = null!; + private OsuTextFlowContainer downloadInBackgroundText = null!; + private OsuTextFlowContainer currentlyLoadedBeatmaps = null!; private BundledBeatmapDownloader? tutorialDownloader; @@ -100,6 +103,15 @@ namespace osu.Game.Overlays.FirstRunSetup Text = FirstRunSetupBeatmapScreenStrings.BundledButton, Action = downloadBundled }, + downloadInBackgroundText = new OsuTextFlowContainer(cp => cp.Font = OsuFont.Default.With(size: CONTENT_FONT_SIZE)) + { + Colour = OverlayColourProvider.Light2, + Alpha = 0, + TextAnchor = Anchor.TopCentre, + Text = FirstRunSetupBeatmapScreenStrings.DownloadingInBackground, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y + }, new OsuTextFlowContainer(cp => cp.Font = OsuFont.Default.With(size: CONTENT_FONT_SIZE)) { Colour = OverlayColourProvider.Content1, @@ -169,6 +181,10 @@ namespace osu.Game.Overlays.FirstRunSetup if (bundledDownloader != null) return; + downloadInBackgroundText + .FlashColour(Color4.White, 500) + .FadeIn(200); + bundledDownloader = new BundledBeatmapDownloader(false); AddInternal(bundledDownloader); From 0ddd3cbdc5cba5ccc60e72ed97ebe07629915249 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 7 May 2024 17:02:25 +0800 Subject: [PATCH 0740/2493] Randomise which menu content is shown on arriving at the menu --- osu.Game/Screens/Menu/OnlineMenuBanner.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Menu/OnlineMenuBanner.cs b/osu.Game/Screens/Menu/OnlineMenuBanner.cs index 6f98b73939..b9d269c82a 100644 --- a/osu.Game/Screens/Menu/OnlineMenuBanner.cs +++ b/osu.Game/Screens/Menu/OnlineMenuBanner.cs @@ -13,6 +13,7 @@ using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; using osu.Framework.Input.Events; using osu.Framework.Threading; +using osu.Framework.Utils; using osu.Game.Graphics.Containers; using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests.Responses; @@ -111,7 +112,9 @@ namespace osu.Game.Screens.Menu content.AddRange(loaded); - displayIndex = -1; + // Many users don't spend much time at the main menu, so let's randomise where in the + // carousel of available images we start at to give each a fair chance. + displayIndex = RNG.Next(0, images.NewValue.Images.Length) - 1; showNext(); }, (cancellationTokenSource ??= new CancellationTokenSource()).Token); } From ab2677d913552b5d381c91675d8bf6628853585e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Erik=20Wolfschl=C3=A4ger?= Date: Tue, 7 May 2024 17:15:49 +0200 Subject: [PATCH 0741/2493] Changed default delete confirmation content strings for better translatability --- .../DeleteConfirmationContentStrings.cs | 24 +++++++++---------- .../DeleteConfirmationDialogStrings.cs | 4 ++-- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/osu.Game/Localisation/DeleteConfirmationContentStrings.cs b/osu.Game/Localisation/DeleteConfirmationContentStrings.cs index d9e90675f7..563fbf5654 100644 --- a/osu.Game/Localisation/DeleteConfirmationContentStrings.cs +++ b/osu.Game/Localisation/DeleteConfirmationContentStrings.cs @@ -10,34 +10,34 @@ namespace osu.Game.Localisation private const string prefix = @"osu.Game.Resources.Localisation.DeleteConfirmationContent"; /// - /// "All beatmaps?" + /// "Are you sure you want to delete all beatmaps?" /// - public static LocalisableString Beatmaps => new TranslatableString(getKey(@"beatmaps"), @"All beatmaps?"); + public static LocalisableString Beatmaps => new TranslatableString(getKey(@"beatmaps"), @"Are you sure you want to delete all beatmaps?"); /// - /// "All beatmaps videos? This cannot be undone!" + /// "Are you sure you want to delete all beatmaps videos? This cannot be undone!" /// - public static LocalisableString BeatmapVideos => new TranslatableString(getKey(@"beatmap_videos"), @"All beatmaps videos? This cannot be undone!"); + public static LocalisableString BeatmapVideos => new TranslatableString(getKey(@"beatmap_videos"), @"Are you sure you want to delete all beatmaps videos? This cannot be undone!"); /// - /// "All skins? This cannot be undone!" + /// "Are you sure you want to delete all skins? This cannot be undone!" /// - public static LocalisableString Skins => new TranslatableString(getKey(@"skins"), @"All skins? This cannot be undone!"); + public static LocalisableString Skins => new TranslatableString(getKey(@"skins"), @"Are you sure you want to delete all skins? This cannot be undone!"); /// - /// "All collections? This cannot be undone!" + /// "Are you sure you want to delete all collections? This cannot be undone!" /// - public static LocalisableString Collections => new TranslatableString(getKey(@"collections"), @"All collections? This cannot be undone!"); + public static LocalisableString Collections => new TranslatableString(getKey(@"collections"), @"Are you sure you want to delete all collections? This cannot be undone!"); /// - /// "All scores? This cannot be undone!" + /// "Are you sure you want to delete all scores? This cannot be undone!" /// - public static LocalisableString Scores => new TranslatableString(getKey(@"collections"), @"All scores? This cannot be undone!"); + public static LocalisableString Scores => new TranslatableString(getKey(@"collections"), @"Are you sure you want to delete all scores? This cannot be undone!"); /// - /// "All mod presets?" + /// "Are you sure you want to delete all mod presets?" /// - public static LocalisableString ModPresets => new TranslatableString(getKey(@"mod_presets"), @"All mod presets?"); + public static LocalisableString ModPresets => new TranslatableString(getKey(@"mod_presets"), @"Are you sure you want to delete all mod presets?"); private static string getKey(string key) => $@"{prefix}:{key}"; } diff --git a/osu.Game/Localisation/DeleteConfirmationDialogStrings.cs b/osu.Game/Localisation/DeleteConfirmationDialogStrings.cs index 33738fe95e..25997eadd3 100644 --- a/osu.Game/Localisation/DeleteConfirmationDialogStrings.cs +++ b/osu.Game/Localisation/DeleteConfirmationDialogStrings.cs @@ -10,9 +10,9 @@ namespace osu.Game.Localisation private const string prefix = @"osu.Game.Resources.Localisation.DeleteConfirmationDialog"; /// - /// "Confirm deletion of" + /// "Caution" /// - public static LocalisableString HeaderText => new TranslatableString(getKey(@"header_text"), @"Confirm deletion of"); + public static LocalisableString HeaderText => new TranslatableString(getKey(@"header_text"), @"Caution"); /// /// "Yes. Go for it." From 7551cf01d18a4e924ae9720417fa1797e8b38db5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 8 May 2024 09:45:52 +0200 Subject: [PATCH 0742/2493] Fix test failure --- osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs index cc7b37e6a8..8ca141bb4f 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs @@ -14,7 +14,7 @@ using osu.Game.Beatmaps; using osu.Game.Beatmaps.Formats; using osu.Game.Beatmaps.Legacy; using osu.Game.IO.Legacy; -using osu.Game.Models; +using osu.Game.Online.API.Requests.Responses; using osu.Game.Replays; using osu.Game.Rulesets; using osu.Game.Rulesets.Catch; @@ -226,10 +226,10 @@ namespace osu.Game.Tests.Beatmaps.Formats new OsuModDoubleTime { SpeedChange = { Value = 1.1 } } }; scoreInfo.OnlineID = 123123; - scoreInfo.RealmUser = new RealmUser + scoreInfo.User = new APIUser { Username = "spaceman_atlas", - OnlineID = 3035836, + Id = 3035836, CountryCode = CountryCode.PL }; scoreInfo.ClientVersion = "2023.1221.0"; From 88b20b357ae0d6ac5d1549981ec07e9a3f731cee Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 9 May 2024 14:38:10 +0800 Subject: [PATCH 0743/2493] Increase padding around text in dialogs --- osu.Game/Overlays/Dialog/PopupDialog.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/Dialog/PopupDialog.cs b/osu.Game/Overlays/Dialog/PopupDialog.cs index 4ac37a63e2..a23c394c9f 100644 --- a/osu.Game/Overlays/Dialog/PopupDialog.cs +++ b/osu.Game/Overlays/Dialog/PopupDialog.cs @@ -210,7 +210,7 @@ namespace osu.Game.Overlays.Dialog RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, TextAnchor = Anchor.TopCentre, - Padding = new MarginPadding { Horizontal = 5 }, + Padding = new MarginPadding { Horizontal = 15 }, }, body = new OsuTextFlowContainer(t => t.Font = t.Font.With(size: 18)) { @@ -219,7 +219,7 @@ namespace osu.Game.Overlays.Dialog TextAnchor = Anchor.TopCentre, RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Padding = new MarginPadding { Horizontal = 5 }, + Padding = new MarginPadding { Horizontal = 15 }, }, buttonsContainer = new FillFlowContainer { From 3b8b56cbcbdb0365a03231aa151efb0050c12ebf Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 9 May 2024 20:18:53 +0900 Subject: [PATCH 0744/2493] Apply required changes after framework masking updates --- .../Sliders/Components/PathControlPointVisualiser.cs | 2 +- osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs | 3 +-- osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs | 3 +-- osu.Game.Tournament/Screens/Ladder/LadderDragContainer.cs | 2 +- osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs | 3 +-- osu.Game/Rulesets/UI/FrameStabilityContainer.cs | 2 +- .../Edit/Compose/Components/Timeline/TimelineTickDisplay.cs | 3 +-- 7 files changed, 7 insertions(+), 11 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs index 836d348ff4..afc2d407e9 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs @@ -78,7 +78,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components } // Generally all the control points are within the visible area all the time. - public override bool UpdateSubTreeMasking(Drawable source, RectangleF maskingBounds) => true; + public override bool UpdateSubTreeMasking() => true; /// /// Handles correction of invalid path types. diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs index 4933eb4041..93c3450904 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs @@ -8,7 +8,6 @@ using System.Linq; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Primitives; using osu.Game.Beatmaps; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects; @@ -37,7 +36,7 @@ namespace osu.Game.Rulesets.Osu.UI // For osu! gameplay, everything is always on screen. // Skipping masking calculations improves performance in intense beatmaps (ie. https://osu.ppy.sh/beatmapsets/150945#osu/372245) - public override bool UpdateSubTreeMasking(Drawable source, RectangleF maskingBounds) => false; + public override bool UpdateSubTreeMasking() => false; public SmokeContainer Smoke { get; } public FollowPointRenderer FollowPoints { get; } diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs index 0510f08068..bdcb341fb4 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs @@ -7,7 +7,6 @@ using System.Linq; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Primitives; using osu.Game.Graphics; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Judgements; @@ -345,7 +344,7 @@ namespace osu.Game.Rulesets.Taiko.UI { public void Add(Drawable proxy) => AddInternal(proxy); - public override bool UpdateSubTreeMasking(Drawable source, RectangleF maskingBounds) + public override bool UpdateSubTreeMasking() { // DrawableHitObject disables masking. // Hitobject content is proxied and unproxied based on hit status and the IsMaskedAway value could get stuck because of this. diff --git a/osu.Game.Tournament/Screens/Ladder/LadderDragContainer.cs b/osu.Game.Tournament/Screens/Ladder/LadderDragContainer.cs index 3a2db4fc71..111dede815 100644 --- a/osu.Game.Tournament/Screens/Ladder/LadderDragContainer.cs +++ b/osu.Game.Tournament/Screens/Ladder/LadderDragContainer.cs @@ -22,7 +22,7 @@ namespace osu.Game.Tournament.Screens.Ladder protected override bool ComputeIsMaskedAway(RectangleF maskingBounds) => false; - public override bool UpdateSubTreeMasking(Drawable source, RectangleF maskingBounds) => false; + public override bool UpdateSubTreeMasking() => false; protected override void OnDrag(DragEvent e) { diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index de05219212..3ce6cc3cef 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -15,7 +15,6 @@ using osu.Framework.Extensions.ListExtensions; using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Extensions.TypeExtensions; using osu.Framework.Graphics; -using osu.Framework.Graphics.Primitives; using osu.Framework.Lists; using osu.Framework.Threading; using osu.Framework.Utils; @@ -632,7 +631,7 @@ namespace osu.Game.Rulesets.Objects.Drawables #endregion - public override bool UpdateSubTreeMasking(Drawable source, RectangleF maskingBounds) => false; + public override bool UpdateSubTreeMasking() => false; protected override void UpdateAfterChildren() { diff --git a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs index b49924762e..c4feb249f4 100644 --- a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs +++ b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs @@ -119,7 +119,7 @@ namespace osu.Game.Rulesets.UI break; base.UpdateSubTree(); - UpdateSubTreeMasking(this, ScreenSpaceDrawQuad.AABBFloat); + UpdateSubTreeMasking(); } while (state == PlaybackState.RequiresCatchUp && stopwatch.ElapsedMilliseconds < max_catchup_milliseconds); return true; diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs index c3adb43032..e16c8519e5 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs @@ -7,7 +7,6 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Caching; using osu.Framework.Graphics; -using osu.Framework.Graphics.Primitives; using osu.Framework.Logging; using osu.Game.Beatmaps; using osu.Game.Graphics; @@ -20,7 +19,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline public partial class TimelineTickDisplay : TimelinePart { // With current implementation every tick in the sub-tree should be visible, no need to check whether they are masked away. - public override bool UpdateSubTreeMasking(Drawable source, RectangleF maskingBounds) => false; + public override bool UpdateSubTreeMasking() => false; [Resolved] private EditorBeatmap beatmap { get; set; } = null!; From c4d6318c0d0026bb5b581fb198aded74376de775 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 9 May 2024 22:12:09 +0800 Subject: [PATCH 0745/2493] 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 97dfe5d9f7..e20ac2e0b7 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -10,7 +10,7 @@ true - + diff --git a/osu.iOS.props b/osu.iOS.props index 66347acdf0..103ef50e0c 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -23,6 +23,6 @@ iossimulator-x64 - + From de05998421b2c04d669bf08897e6fd82e49d1815 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 9 May 2024 22:17:00 +0800 Subject: [PATCH 0746/2493] Avoid weird codestyle rules --- osu.Desktop/Program.cs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/osu.Desktop/Program.cs b/osu.Desktop/Program.cs index d8364fc6e6..23e56cdce9 100644 --- a/osu.Desktop/Program.cs +++ b/osu.Desktop/Program.cs @@ -107,11 +107,13 @@ namespace osu.Desktop } } - using (DesktopGameHost host = Host.GetSuitableDesktopHost(gameName, new HostOptions - { - IPCPort = !tournamentClient ? OsuGame.IPC_PORT : null, - FriendlyGameName = OsuGameBase.GAME_NAME, - })) + var hostOptions = new HostOptions + { + IPCPort = !tournamentClient ? OsuGame.IPC_PORT : null, + FriendlyGameName = OsuGameBase.GAME_NAME, + }; + + using (DesktopGameHost host = Host.GetSuitableDesktopHost(gameName, hostOptions)) { if (!host.IsPrimaryInstance) { From 6b91b4abf41660fb030f2d593df28159442cf145 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 8 May 2024 03:58:10 +0300 Subject: [PATCH 0747/2493] Add simple implementation for extended mods display in new footer design --- .../TestSceneFooterButtonModsV2.cs | 120 +++++++++ osu.Game/Screens/Play/HUD/ModDisplay.cs | 7 +- .../Select/FooterV2/FooterButtonModsV2.cs | 249 +++++++++++++++++- .../Screens/Select/FooterV2/FooterButtonV2.cs | 126 ++++----- 4 files changed, 436 insertions(+), 66 deletions(-) create mode 100644 osu.Game.Tests/Visual/UserInterface/TestSceneFooterButtonModsV2.cs diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneFooterButtonModsV2.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneFooterButtonModsV2.cs new file mode 100644 index 0000000000..7e8bba6573 --- /dev/null +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneFooterButtonModsV2.cs @@ -0,0 +1,120 @@ +// 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; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Graphics.Sprites; +using osu.Game.Overlays; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Osu.Mods; +using osu.Game.Screens.Select.FooterV2; +using osu.Game.Utils; + +namespace osu.Game.Tests.Visual.UserInterface +{ + public partial class TestSceneFooterButtonModsV2 : OsuTestScene + { + private readonly TestFooterButtonModsV2 footerButtonMods; + + [Cached] + private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine); + + public TestSceneFooterButtonModsV2() + { + Add(footerButtonMods = new TestFooterButtonModsV2 + { + Anchor = Anchor.Centre, + Origin = Anchor.CentreLeft, + X = -100, + Action = () => { }, + }); + } + + [Test] + public void TestDisplay() + { + AddStep("one mod", () => changeMods(new List { new OsuModHidden() })); + AddStep("two mods", () => changeMods(new List { new OsuModHidden(), new OsuModHardRock() })); + AddStep("three mods", () => changeMods(new List { new OsuModHidden(), new OsuModHardRock(), new OsuModDoubleTime() })); + AddStep("four mods", () => changeMods(new List { new OsuModHidden(), new OsuModHardRock(), new OsuModDoubleTime(), new OsuModClassic() })); + AddStep("five mods", () => changeMods(new List { new OsuModHidden(), new OsuModHardRock(), new OsuModDoubleTime(), new OsuModClassic(), new OsuModDifficultyAdjust() })); + + AddStep("clear mods", () => changeMods(Array.Empty())); + AddWaitStep("wait", 3); + AddStep("one mod", () => changeMods(new List { new OsuModHidden() })); + + AddStep("clear mods", () => changeMods(Array.Empty())); + AddWaitStep("wait", 3); + AddStep("five mods", () => changeMods(new List { new OsuModHidden(), new OsuModHardRock(), new OsuModDoubleTime(), new OsuModClassic(), new OsuModDifficultyAdjust() })); + } + + [Test] + public void TestIncrementMultiplier() + { + var hiddenMod = new Mod[] { new OsuModHidden() }; + AddStep(@"Add Hidden", () => changeMods(hiddenMod)); + AddAssert(@"Check Hidden multiplier", () => assertModsMultiplier(hiddenMod)); + + var hardRockMod = new Mod[] { new OsuModHardRock() }; + AddStep(@"Add HardRock", () => changeMods(hardRockMod)); + AddAssert(@"Check HardRock multiplier", () => assertModsMultiplier(hardRockMod)); + + var doubleTimeMod = new Mod[] { new OsuModDoubleTime() }; + AddStep(@"Add DoubleTime", () => changeMods(doubleTimeMod)); + AddAssert(@"Check DoubleTime multiplier", () => assertModsMultiplier(doubleTimeMod)); + + var multipleIncrementMods = new Mod[] { new OsuModDoubleTime(), new OsuModHidden(), new OsuModHardRock() }; + AddStep(@"Add multiple Mods", () => changeMods(multipleIncrementMods)); + AddAssert(@"Check multiple mod multiplier", () => assertModsMultiplier(multipleIncrementMods)); + } + + [Test] + public void TestDecrementMultiplier() + { + var easyMod = new Mod[] { new OsuModEasy() }; + AddStep(@"Add Easy", () => changeMods(easyMod)); + AddAssert(@"Check Easy multiplier", () => assertModsMultiplier(easyMod)); + + var noFailMod = new Mod[] { new OsuModNoFail() }; + AddStep(@"Add NoFail", () => changeMods(noFailMod)); + AddAssert(@"Check NoFail multiplier", () => assertModsMultiplier(noFailMod)); + + var multipleDecrementMods = new Mod[] { new OsuModEasy(), new OsuModNoFail() }; + AddStep(@"Add Multiple Mods", () => changeMods(multipleDecrementMods)); + AddAssert(@"Check multiple mod multiplier", () => assertModsMultiplier(multipleDecrementMods)); + } + + [Test] + public void TestUnrankedBadge() + { + AddStep(@"Add unranked mod", () => changeMods(new[] { new OsuModDeflate() })); + AddUntilStep("Unranked badge shown", () => footerButtonMods.UnrankedBadge.Alpha == 1); + AddStep(@"Clear selected mod", () => changeMods(Array.Empty())); + AddUntilStep("Unranked badge not shown", () => footerButtonMods.UnrankedBadge.Alpha == 0); + } + + private void changeMods(IReadOnlyList mods) + { + footerButtonMods.Current.Value = mods; + } + + private bool assertModsMultiplier(IEnumerable mods) + { + double multiplier = mods.Aggregate(1.0, (current, mod) => current * mod.ScoreMultiplier); + string expectedValue = ModUtils.FormatScoreMultiplier(multiplier).ToString(); + + return expectedValue == footerButtonMods.MultiplierText.Current.Value; + } + + private partial class TestFooterButtonModsV2 : FooterButtonModsV2 + { + public new Container UnrankedBadge => base.UnrankedBadge; + public new OsuSpriteText MultiplierText => base.MultiplierText; + } + } +} diff --git a/osu.Game/Screens/Play/HUD/ModDisplay.cs b/osu.Game/Screens/Play/HUD/ModDisplay.cs index ba948b516e..75db720603 100644 --- a/osu.Game/Screens/Play/HUD/ModDisplay.cs +++ b/osu.Game/Screens/Play/HUD/ModDisplay.cs @@ -20,6 +20,7 @@ namespace osu.Game.Screens.Play.HUD /// public partial class ModDisplay : CompositeDrawable, IHasCurrentValue> { + private readonly bool showExtendedInformation; private const int fade_duration = 1000; public ExpansionMode ExpansionMode = ExpansionMode.ExpandOnHover; @@ -39,8 +40,10 @@ namespace osu.Game.Screens.Play.HUD private readonly FillFlowContainer iconsContainer; - public ModDisplay() + public ModDisplay(bool showExtendedInformation = true) { + this.showExtendedInformation = showExtendedInformation; + AutoSizeAxes = Axes.Both; InternalChild = iconsContainer = new ReverseChildIDFillFlowContainer @@ -64,7 +67,7 @@ namespace osu.Game.Screens.Play.HUD iconsContainer.Clear(); foreach (Mod mod in mods.NewValue.AsOrdered()) - iconsContainer.Add(new ModIcon(mod) { Scale = new Vector2(0.6f) }); + iconsContainer.Add(new ModIcon(mod, showExtendedInformation: showExtendedInformation) { Scale = new Vector2(0.6f) }); appearTransform(); } diff --git a/osu.Game/Screens/Select/FooterV2/FooterButtonModsV2.cs b/osu.Game/Screens/Select/FooterV2/FooterButtonModsV2.cs index b8c9f0b34b..f9d923ddcf 100644 --- a/osu.Game/Screens/Select/FooterV2/FooterButtonModsV2.cs +++ b/osu.Game/Screens/Select/FooterV2/FooterButtonModsV2.cs @@ -1,20 +1,261 @@ // 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.LocalisationExtensions; +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.Game.Configuration; using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Localisation; +using osu.Game.Overlays; +using osu.Game.Rulesets.Mods; +using osu.Game.Screens.Play.HUD; +using osu.Game.Utils; +using osuTK; +using osuTK.Graphics; namespace osu.Game.Screens.Select.FooterV2 { - public partial class FooterButtonModsV2 : FooterButtonV2 + public partial class FooterButtonModsV2 : FooterButtonV2, IHasCurrentValue> { - [BackgroundDependencyLoader] - private void load(OsuColour colour) + // todo: see https://github.com/ppy/osu-framework/issues/3271 + private const float torus_scale_factor = 1.2f; + + private readonly BindableWithCurrent> current = new BindableWithCurrent>(Array.Empty()); + + public Bindable> Current { + get => current.Current; + set => current.Current = value; + } + + private Container modDisplayBar = null!; + + protected Container UnrankedBadge { get; private set; } = null!; + + private ModDisplay modDisplay = null!; + private OsuSpriteText modCountText = null!; + + protected OsuSpriteText MultiplierText { get; private set; } = null!; + + [Resolved] + private OsuColour colours { get; set; } = null!; + + [Resolved] + private OverlayColourProvider colourProvider { get; set; } = null!; + + [BackgroundDependencyLoader] + private void load() + { + const float bar_shear_width = 7f; + const float bar_height = 37f; + const float display_rel_width = 0.65f; + + var barShear = new Vector2(bar_shear_width / bar_height, 0); + Text = "Mods"; Icon = FontAwesome.Solid.ExchangeAlt; - AccentColour = colour.Lime1; + AccentColour = colours.Lime1; + + AddRange(new[] + { + UnrankedBadge = new Container + { + Position = new Vector2(BUTTON_WIDTH + 5f, -5f), + Depth = float.MaxValue, + Origin = Anchor.BottomLeft, + Shear = barShear, + CornerRadius = CORNER_RADIUS, + AutoSizeAxes = Axes.Both, + Masking = true, + BorderColour = Color4.White, + BorderThickness = 2f, + Children = new Drawable[] + { + new Box + { + Colour = colours.Red2, + RelativeSizeAxes = Axes.Both, + }, + new OsuSpriteText + { + Shear = -barShear, + Text = ModSelectOverlayStrings.Unranked.ToUpper(), + Margin = new MarginPadding { Horizontal = 15, Vertical = 5 }, + Font = OsuFont.Torus.With(size: 14 * torus_scale_factor, weight: FontWeight.Bold), + Colour = Color4.Black, + } + } + }, + modDisplayBar = new Container + { + Y = -5f, + Depth = float.MaxValue, + Origin = Anchor.BottomLeft, + Shear = barShear, + CornerRadius = CORNER_RADIUS, + Size = new Vector2(BUTTON_WIDTH, bar_height), + Masking = true, + EdgeEffect = new EdgeEffectParameters + { + Type = EdgeEffectType.Shadow, + Radius = 4, + // Figma says 50% opacity, but it does not match up visually if taken at face value, and looks bad. + Colour = Colour4.Black.Opacity(0.25f), + Offset = new Vector2(0, 2), + }, + Children = new Drawable[] + { + new Box + { + Colour = colourProvider.Background4, + RelativeSizeAxes = Axes.Both, + }, + new Container + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + RelativeSizeAxes = Axes.Both, + Width = 1f - display_rel_width, + Masking = true, + Child = MultiplierText = new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Shear = -barShear, + Font = OsuFont.Torus.With(size: 14 * torus_scale_factor, weight: FontWeight.Bold) + } + }, + new Container + { + CornerRadius = CORNER_RADIUS, + RelativeSizeAxes = Axes.Both, + Width = display_rel_width, + Masking = true, + Children = new Drawable[] + { + new Box + { + Colour = colourProvider.Background3, + RelativeSizeAxes = Axes.Both, + }, + modDisplay = new ModDisplay(showExtendedInformation: false) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Shear = -barShear, + Scale = new Vector2(0.6f), + Current = { Value = new List { new ModCinema() } }, + ExpansionMode = ExpansionMode.AlwaysContracted, + }, + modCountText = new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Shear = -barShear, + Font = OsuFont.Torus.With(size: 14 * torus_scale_factor, weight: FontWeight.Bold), + } + } + }, + } + }, + }); + } + + private ModSettingChangeTracker? modSettingChangeTracker; + + protected override void LoadComplete() + { + base.LoadComplete(); + + Current.BindValueChanged(m => + { + modSettingChangeTracker?.Dispose(); + + updateDisplay(); + + if (m.NewValue != null) + { + modSettingChangeTracker = new ModSettingChangeTracker(m.NewValue); + modSettingChangeTracker.SettingChanged += _ => updateDisplay(); + } + }, true); + + FinishTransforms(true); + } + + private const double duration = 240; + private const Easing easing = Easing.OutQuint; + + private void updateDisplay() + { + if (Current.Value.Count == 0) + { + modDisplayBar.MoveToY(20, duration, easing); + modDisplayBar.FadeOut(duration, easing); + modDisplay.FadeOut(duration, easing); + modCountText.FadeOut(duration, easing); + + UnrankedBadge.MoveToY(20, duration, easing); + UnrankedBadge.FadeOut(duration, easing); + + // add delay to let unranked indicator hide first before resizing the button back to its original width. + this.Delay(duration).ResizeWidthTo(BUTTON_WIDTH, duration, easing); + } + else + { + if (Current.Value.Count >= 5) + { + modCountText.Text = $"{Current.Value.Count} MODS"; + modCountText.FadeIn(duration, easing); + modDisplay.FadeOut(duration, easing); + } + else + { + modDisplay.Current.Value = Current.Value; + modDisplay.FadeIn(duration, easing); + modCountText.FadeOut(duration, easing); + } + + if (Current.Value.Any(m => !m.Ranked)) + { + UnrankedBadge.MoveToX(BUTTON_WIDTH + 5, duration, easing); + UnrankedBadge.FadeIn(duration, easing); + + this.ResizeWidthTo(BUTTON_WIDTH + UnrankedBadge.DrawWidth + 10, duration, easing); + } + else + { + UnrankedBadge.MoveToX(BUTTON_WIDTH + 5 - UnrankedBadge.DrawWidth, duration, easing); + UnrankedBadge.FadeOut(duration, easing); + + this.ResizeWidthTo(BUTTON_WIDTH, duration, easing); + } + + modDisplayBar.MoveToY(-5, duration, Easing.OutQuint); + UnrankedBadge.MoveToY(-5, duration, easing); + modDisplayBar.FadeIn(duration, easing); + } + + double multiplier = Current.Value?.Aggregate(1.0, (current, mod) => current * mod.ScoreMultiplier) ?? 1; + MultiplierText.Text = ModUtils.FormatScoreMultiplier(multiplier); + + if (multiplier > 1) + MultiplierText.FadeColour(colours.Red1, duration, easing); + else if (multiplier < 1) + MultiplierText.FadeColour(colours.Lime1, duration, easing); + else + MultiplierText.FadeColour(Color4.White, duration, easing); } } } diff --git a/osu.Game/Screens/Select/FooterV2/FooterButtonV2.cs b/osu.Game/Screens/Select/FooterV2/FooterButtonV2.cs index 2f5046d2bb..a7bd1b8abd 100644 --- a/osu.Game/Screens/Select/FooterV2/FooterButtonV2.cs +++ b/osu.Game/Screens/Select/FooterV2/FooterButtonV2.cs @@ -24,17 +24,18 @@ namespace osu.Game.Screens.Select.FooterV2 { public partial class FooterButtonV2 : OsuClickableContainer, IKeyBindingHandler { - private const int button_height = 90; - private const int button_width = 140; - private const int corner_radius = 10; private const int transition_length = 500; // This should be 12 by design, but an extra allowance is added due to the corner radius specification. public const float SHEAR_WIDTH = 13.5f; + public const int CORNER_RADIUS = 10; + public const int BUTTON_HEIGHT = 90; + public const int BUTTON_WIDTH = 140; + public Bindable OverlayState = new Bindable(); - protected static readonly Vector2 SHEAR = new Vector2(SHEAR_WIDTH / button_height, 0); + protected static readonly Vector2 BUTTON_SHEAR = new Vector2(SHEAR_WIDTH / BUTTON_HEIGHT, 0); [Resolved] private OverlayColourProvider colourProvider { get; set; } = null!; @@ -70,68 +71,73 @@ namespace osu.Game.Screens.Select.FooterV2 public FooterButtonV2() { - EdgeEffect = new EdgeEffectParameters - { - Type = EdgeEffectType.Shadow, - Radius = 4, - // Figma says 50% opacity, but it does not match up visually if taken at face value, and looks bad. - Colour = Colour4.Black.Opacity(0.25f), - Offset = new Vector2(0, 2), - }; - Shear = SHEAR; - Size = new Vector2(button_width, button_height); - Masking = true; - CornerRadius = corner_radius; - Children = new Drawable[] - { - backgroundBox = new Box - { - RelativeSizeAxes = Axes.Both - }, + Size = new Vector2(BUTTON_WIDTH, BUTTON_HEIGHT); + Margin = new MarginPadding { Horizontal = SHEAR_WIDTH / 2f }; - // For elements that should not be sheared. - new Container + Child = new Container + { + EdgeEffect = new EdgeEffectParameters { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Shear = -SHEAR, - RelativeSizeAxes = Axes.Both, - Children = new Drawable[] - { - TextContainer = new Container - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - Y = 42, - AutoSizeAxes = Axes.Both, - Child = text = new OsuSpriteText - { - // figma design says the size is 16, but due to the issues with font sizes 19 matches better - Font = OsuFont.TorusAlternate.With(size: 19), - AlwaysPresent = true - } - }, - icon = new SpriteIcon - { - Y = 12, - Size = new Vector2(20), - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre - }, - } + Type = EdgeEffectType.Shadow, + Radius = 4, + // Figma says 50% opacity, but it does not match up visually if taken at face value, and looks bad. + Colour = Colour4.Black.Opacity(0.25f), + Offset = new Vector2(0, 2), }, - new Container + Shear = BUTTON_SHEAR, + Masking = true, + CornerRadius = CORNER_RADIUS, + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] { - Shear = -SHEAR, - Anchor = Anchor.BottomCentre, - Origin = Anchor.Centre, - Y = -corner_radius, - Size = new Vector2(120, 6), - Masking = true, - CornerRadius = 3, - Child = bar = new Box + backgroundBox = new Box { + RelativeSizeAxes = Axes.Both + }, + // For elements that should not be sheared. + new Container + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Shear = -BUTTON_SHEAR, RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + TextContainer = new Container + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Y = 42, + AutoSizeAxes = Axes.Both, + Child = text = new OsuSpriteText + { + // figma design says the size is 16, but due to the issues with font sizes 19 matches better + Font = OsuFont.TorusAlternate.With(size: 19), + AlwaysPresent = true + } + }, + icon = new SpriteIcon + { + Y = 12, + Size = new Vector2(20), + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre + }, + } + }, + new Container + { + Shear = -BUTTON_SHEAR, + Anchor = Anchor.BottomCentre, + Origin = Anchor.Centre, + Y = -CORNER_RADIUS, + Size = new Vector2(120, 6), + Masking = true, + CornerRadius = 3, + Child = bar = new Box + { + RelativeSizeAxes = Axes.Both, + } } } }; From d7b658ec76dff55ec15cd2751861fe04ef414668 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 10 May 2024 02:21:06 +0300 Subject: [PATCH 0748/2493] Add similar test steps to song select footer test scene --- .../SongSelect/TestSceneSongSelectFooterV2.cs | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneSongSelectFooterV2.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneSongSelectFooterV2.cs index 013bad55bc..0c7725db5a 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneSongSelectFooterV2.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneSongSelectFooterV2.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 System; +using System.Collections.Generic; using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; @@ -10,7 +12,9 @@ using osu.Framework.Graphics.Cursor; using osu.Framework.Testing; using osu.Game.Overlays; using osu.Game.Overlays.Mods; +using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.Osu.Mods; using osu.Game.Screens.Select.FooterV2; using osuTK.Input; @@ -47,7 +51,7 @@ namespace osu.Game.Tests.Visual.SongSelect overlay = new DummyOverlay() }; - footer.AddButton(modsButton = new FooterButtonModsV2(), overlay); + footer.AddButton(modsButton = new FooterButtonModsV2 { Current = SelectedMods }, overlay); footer.AddButton(randomButton = new FooterButtonRandomV2 { NextRandom = () => nextRandomCalled = true, @@ -61,9 +65,28 @@ namespace osu.Game.Tests.Visual.SongSelect [SetUpSteps] public void SetUpSteps() { + AddStep("clear mods", () => SelectedMods.Value = Array.Empty()); AddStep("set beatmap", () => Beatmap.Value = CreateWorkingBeatmap(CreateBeatmap(new OsuRuleset().RulesetInfo))); } + [Test] + public void TestMods() + { + AddStep("one mod", () => SelectedMods.Value = new List { new OsuModHidden() }); + AddStep("two mods", () => SelectedMods.Value = new List { new OsuModHidden(), new OsuModHardRock() }); + AddStep("three mods", () => SelectedMods.Value = new List { new OsuModHidden(), new OsuModHardRock(), new OsuModDoubleTime() }); + AddStep("four mods", () => SelectedMods.Value = new List { new OsuModHidden(), new OsuModHardRock(), new OsuModDoubleTime(), new OsuModClassic() }); + AddStep("five mods", () => SelectedMods.Value = new List { new OsuModHidden(), new OsuModHardRock(), new OsuModDoubleTime(), new OsuModClassic(), new OsuModDifficultyAdjust() }); + + AddStep("clear mods", () => SelectedMods.Value = Array.Empty()); + AddWaitStep("wait", 3); + AddStep("one mod", () => SelectedMods.Value = new List { new OsuModHidden() }); + + AddStep("clear mods", () => SelectedMods.Value = Array.Empty()); + AddWaitStep("wait", 3); + AddStep("five mods", () => SelectedMods.Value = new List { new OsuModHidden(), new OsuModHardRock(), new OsuModDoubleTime(), new OsuModClassic(), new OsuModDifficultyAdjust() }); + } + [Test] public void TestShowOptions() { From 49692e168eb8a7a72b93a6bb40f83f12575c4976 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 10 May 2024 02:21:24 +0300 Subject: [PATCH 0749/2493] Fix `ModDisplay` expanding on load with "always contracted/expanded" modes This is especially visible when reloading `SongSelectFooterV2` while multiple mods are already selected. The mods will appear expanded then contract. --- osu.Game/Screens/Play/HUD/ModDisplay.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Screens/Play/HUD/ModDisplay.cs b/osu.Game/Screens/Play/HUD/ModDisplay.cs index 75db720603..f8c8232d45 100644 --- a/osu.Game/Screens/Play/HUD/ModDisplay.cs +++ b/osu.Game/Screens/Play/HUD/ModDisplay.cs @@ -60,6 +60,9 @@ namespace osu.Game.Screens.Play.HUD Current.BindValueChanged(updateDisplay, true); iconsContainer.FadeInFromZero(fade_duration, Easing.OutQuint); + + if (ExpansionMode == ExpansionMode.AlwaysExpanded || ExpansionMode == ExpansionMode.AlwaysContracted) + FinishTransforms(true); } private void updateDisplay(ValueChangedEvent> mods) From bff34a1c04f8151c053a1c32b31043b26f4e520c Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 10 May 2024 06:38:48 +0300 Subject: [PATCH 0750/2493] Add localisation and tooltip for mods count text --- .../SongSelect/TestSceneSongSelectFooterV2.cs | 6 ++ .../TestSceneFooterButtonModsV2.cs | 4 ++ .../Localisation/FooterButtonModsV2Strings.cs | 19 ++++++ .../Select/FooterV2/FooterButtonModsV2.cs | 62 +++++++++++++++++-- 4 files changed, 87 insertions(+), 4 deletions(-) create mode 100644 osu.Game/Localisation/FooterButtonModsV2Strings.cs diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneSongSelectFooterV2.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneSongSelectFooterV2.cs index 0c7725db5a..93402e42ce 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneSongSelectFooterV2.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneSongSelectFooterV2.cs @@ -78,6 +78,12 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep("four mods", () => SelectedMods.Value = new List { new OsuModHidden(), new OsuModHardRock(), new OsuModDoubleTime(), new OsuModClassic() }); AddStep("five mods", () => SelectedMods.Value = new List { new OsuModHidden(), new OsuModHardRock(), new OsuModDoubleTime(), new OsuModClassic(), new OsuModDifficultyAdjust() }); + AddStep("modified", () => SelectedMods.Value = new List { new OsuModDoubleTime { SpeedChange = { Value = 1.2 } } }); + AddStep("modified + one", () => SelectedMods.Value = new List { new OsuModHidden(), new OsuModDoubleTime { SpeedChange = { Value = 1.2 } } }); + AddStep("modified + two", () => SelectedMods.Value = new List { new OsuModHidden(), new OsuModHardRock(), new OsuModDoubleTime { SpeedChange = { Value = 1.2 } } }); + AddStep("modified + three", () => SelectedMods.Value = new List { new OsuModHidden(), new OsuModHardRock(), new OsuModClassic(), new OsuModDoubleTime { SpeedChange = { Value = 1.2 } } }); + AddStep("modified + four", () => SelectedMods.Value = new List { new OsuModHidden(), new OsuModHardRock(), new OsuModClassic(), new OsuModDifficultyAdjust(), new OsuModDoubleTime { SpeedChange = { Value = 1.2 } } }); + AddStep("clear mods", () => SelectedMods.Value = Array.Empty()); AddWaitStep("wait", 3); AddStep("one mod", () => SelectedMods.Value = new List { new OsuModHidden() }); diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneFooterButtonModsV2.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneFooterButtonModsV2.cs index 7e8bba6573..af2eea6062 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneFooterButtonModsV2.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneFooterButtonModsV2.cs @@ -44,6 +44,10 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep("four mods", () => changeMods(new List { new OsuModHidden(), new OsuModHardRock(), new OsuModDoubleTime(), new OsuModClassic() })); AddStep("five mods", () => changeMods(new List { new OsuModHidden(), new OsuModHardRock(), new OsuModDoubleTime(), new OsuModClassic(), new OsuModDifficultyAdjust() })); + AddStep("modified", () => changeMods(new List { new OsuModDoubleTime { SpeedChange = { Value = 1.2 } } })); + AddStep("modified + one", () => changeMods(new List { new OsuModHidden(), new OsuModDoubleTime { SpeedChange = { Value = 1.2 } } })); + AddStep("modified + two", () => changeMods(new List { new OsuModHidden(), new OsuModHardRock(), new OsuModDoubleTime { SpeedChange = { Value = 1.2 } } })); + AddStep("clear mods", () => changeMods(Array.Empty())); AddWaitStep("wait", 3); AddStep("one mod", () => changeMods(new List { new OsuModHidden() })); diff --git a/osu.Game/Localisation/FooterButtonModsV2Strings.cs b/osu.Game/Localisation/FooterButtonModsV2Strings.cs new file mode 100644 index 0000000000..2cb297d8ef --- /dev/null +++ b/osu.Game/Localisation/FooterButtonModsV2Strings.cs @@ -0,0 +1,19 @@ +// 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.Localisation +{ + public static class FooterButtonModsV2Strings + { + private const string prefix = @"osu.Game.Resources.Localisation.FooterButtonModsV2"; + + /// + /// "{0} mods" + /// + public static LocalisableString Mods(int count) => new TranslatableString(getKey(@"mods"), @"{0} mods", count); + + private static string getKey(string key) => $@"{prefix}:{key}"; + } +} diff --git a/osu.Game/Screens/Select/FooterV2/FooterButtonModsV2.cs b/osu.Game/Screens/Select/FooterV2/FooterButtonModsV2.cs index f9d923ddcf..7beadd1576 100644 --- a/osu.Game/Screens/Select/FooterV2/FooterButtonModsV2.cs +++ b/osu.Game/Screens/Select/FooterV2/FooterButtonModsV2.cs @@ -9,12 +9,14 @@ using osu.Framework.Bindables; using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.UserInterface; using osu.Game.Configuration; using osu.Game.Graphics; +using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Localisation; using osu.Game.Overlays; @@ -155,15 +157,16 @@ namespace osu.Game.Screens.Select.FooterV2 Origin = Anchor.Centre, Shear = -barShear, Scale = new Vector2(0.6f), - Current = { Value = new List { new ModCinema() } }, + Current = { BindTarget = Current }, ExpansionMode = ExpansionMode.AlwaysContracted, }, - modCountText = new OsuSpriteText + modCountText = new ModCountText { Anchor = Anchor.Centre, Origin = Anchor.Centre, Shear = -barShear, Font = OsuFont.Torus.With(size: 14 * torus_scale_factor, weight: FontWeight.Bold), + Mods = { BindTarget = Current }, } } }, @@ -216,13 +219,11 @@ namespace osu.Game.Screens.Select.FooterV2 { if (Current.Value.Count >= 5) { - modCountText.Text = $"{Current.Value.Count} MODS"; modCountText.FadeIn(duration, easing); modDisplay.FadeOut(duration, easing); } else { - modDisplay.Current.Value = Current.Value; modDisplay.FadeIn(duration, easing); modCountText.FadeOut(duration, easing); } @@ -257,5 +258,58 @@ namespace osu.Game.Screens.Select.FooterV2 else MultiplierText.FadeColour(Color4.White, duration, easing); } + + private partial class ModCountText : OsuSpriteText, IHasCustomTooltip> + { + public readonly Bindable> Mods = new Bindable>(); + + protected override void LoadComplete() + { + base.LoadComplete(); + Mods.BindValueChanged(v => Text = FooterButtonModsV2Strings.Mods(v.NewValue.Count).ToUpper(), true); + } + + public ITooltip> GetCustomTooltip() => new ModTooltip(); + + public IReadOnlyList? TooltipContent => Mods.Value; + + public partial class ModTooltip : VisibilityContainer, ITooltip> + { + private ModDisplay extendedModDisplay = null!; + + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider) + { + AutoSizeAxes = Axes.Both; + CornerRadius = CORNER_RADIUS; + Masking = true; + + InternalChildren = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background5, + }, + extendedModDisplay = new ModDisplay + { + Margin = new MarginPadding { Vertical = 2f, Horizontal = 10f }, + Scale = new Vector2(0.6f), + ExpansionMode = ExpansionMode.AlwaysExpanded, + }, + }; + } + + public void SetContent(IReadOnlyList content) + { + extendedModDisplay.Current.Value = content; + } + + public void Move(Vector2 pos) => Position = pos; + + protected override void PopIn() => this.FadeIn(240, Easing.OutQuint); + protected override void PopOut() => this.FadeOut(240, Easing.OutQuint); + } + } } } From c1ea9d2c9fd5c63f1c43830870e74a0bb090139a Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 10 May 2024 06:39:06 +0300 Subject: [PATCH 0751/2493] Adjust unranked badge height --- osu.Game/Screens/Select/FooterV2/FooterButtonModsV2.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Select/FooterV2/FooterButtonModsV2.cs b/osu.Game/Screens/Select/FooterV2/FooterButtonModsV2.cs index 7beadd1576..ba4abc4025 100644 --- a/osu.Game/Screens/Select/FooterV2/FooterButtonModsV2.cs +++ b/osu.Game/Screens/Select/FooterV2/FooterButtonModsV2.cs @@ -78,7 +78,8 @@ namespace osu.Game.Screens.Select.FooterV2 Origin = Anchor.BottomLeft, Shear = barShear, CornerRadius = CORNER_RADIUS, - AutoSizeAxes = Axes.Both, + AutoSizeAxes = Axes.X, + Height = bar_height, Masking = true, BorderColour = Color4.White, BorderThickness = 2f, @@ -91,9 +92,12 @@ namespace osu.Game.Screens.Select.FooterV2 }, new OsuSpriteText { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, Shear = -barShear, Text = ModSelectOverlayStrings.Unranked.ToUpper(), - Margin = new MarginPadding { Horizontal = 15, Vertical = 5 }, + Margin = new MarginPadding { Horizontal = 15 }, + UseFullGlyphHeight = false, Font = OsuFont.Torus.With(size: 14 * torus_scale_factor, weight: FontWeight.Bold), Colour = Color4.Black, } @@ -135,6 +139,7 @@ namespace osu.Game.Screens.Select.FooterV2 Anchor = Anchor.Centre, Origin = Anchor.Centre, Shear = -barShear, + UseFullGlyphHeight = false, Font = OsuFont.Torus.With(size: 14 * torus_scale_factor, weight: FontWeight.Bold) } }, From 6ddf8f849805e203fbfb0469cce923c0e9af2f68 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 10 May 2024 06:47:48 +0300 Subject: [PATCH 0752/2493] Few cleanups --- osu.Game/Screens/Play/HUD/ModDisplay.cs | 2 +- .../Screens/Select/FooterV2/FooterButtonModsV2.cs | 7 +++---- osu.Game/Screens/Select/FooterV2/FooterButtonV2.cs | 11 +++++------ osu.Game/Screens/Select/FooterV2/FooterV2.cs | 2 +- 4 files changed, 10 insertions(+), 12 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/ModDisplay.cs b/osu.Game/Screens/Play/HUD/ModDisplay.cs index f8c8232d45..b37d41e7a2 100644 --- a/osu.Game/Screens/Play/HUD/ModDisplay.cs +++ b/osu.Game/Screens/Play/HUD/ModDisplay.cs @@ -20,7 +20,6 @@ namespace osu.Game.Screens.Play.HUD /// public partial class ModDisplay : CompositeDrawable, IHasCurrentValue> { - private readonly bool showExtendedInformation; private const int fade_duration = 1000; public ExpansionMode ExpansionMode = ExpansionMode.ExpandOnHover; @@ -38,6 +37,7 @@ namespace osu.Game.Screens.Play.HUD } } + private readonly bool showExtendedInformation; private readonly FillFlowContainer iconsContainer; public ModDisplay(bool showExtendedInformation = true) diff --git a/osu.Game/Screens/Select/FooterV2/FooterButtonModsV2.cs b/osu.Game/Screens/Select/FooterV2/FooterButtonModsV2.cs index ba4abc4025..bb259898ea 100644 --- a/osu.Game/Screens/Select/FooterV2/FooterButtonModsV2.cs +++ b/osu.Game/Screens/Select/FooterV2/FooterButtonModsV2.cs @@ -16,7 +16,6 @@ using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.UserInterface; using osu.Game.Configuration; using osu.Game.Graphics; -using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Localisation; using osu.Game.Overlays; @@ -61,7 +60,7 @@ namespace osu.Game.Screens.Select.FooterV2 { const float bar_shear_width = 7f; const float bar_height = 37f; - const float display_rel_width = 0.65f; + const float mod_display_portion = 0.65f; var barShear = new Vector2(bar_shear_width / bar_height, 0); @@ -132,7 +131,7 @@ namespace osu.Game.Screens.Select.FooterV2 Anchor = Anchor.CentreRight, Origin = Anchor.CentreRight, RelativeSizeAxes = Axes.Both, - Width = 1f - display_rel_width, + Width = 1f - mod_display_portion, Masking = true, Child = MultiplierText = new OsuSpriteText { @@ -147,7 +146,7 @@ namespace osu.Game.Screens.Select.FooterV2 { CornerRadius = CORNER_RADIUS, RelativeSizeAxes = Axes.Both, - Width = display_rel_width, + Width = mod_display_portion, Masking = true, Children = new Drawable[] { diff --git a/osu.Game/Screens/Select/FooterV2/FooterButtonV2.cs b/osu.Game/Screens/Select/FooterV2/FooterButtonV2.cs index a7bd1b8abd..2c841f6ae6 100644 --- a/osu.Game/Screens/Select/FooterV2/FooterButtonV2.cs +++ b/osu.Game/Screens/Select/FooterV2/FooterButtonV2.cs @@ -27,15 +27,15 @@ namespace osu.Game.Screens.Select.FooterV2 private const int transition_length = 500; // This should be 12 by design, but an extra allowance is added due to the corner radius specification. - public const float SHEAR_WIDTH = 13.5f; + private const float shear_width = 13.5f; - public const int CORNER_RADIUS = 10; - public const int BUTTON_HEIGHT = 90; - public const int BUTTON_WIDTH = 140; + protected const int CORNER_RADIUS = 10; + protected const int BUTTON_HEIGHT = 90; + protected const int BUTTON_WIDTH = 140; public Bindable OverlayState = new Bindable(); - protected static readonly Vector2 BUTTON_SHEAR = new Vector2(SHEAR_WIDTH / BUTTON_HEIGHT, 0); + protected static readonly Vector2 BUTTON_SHEAR = new Vector2(shear_width / BUTTON_HEIGHT, 0); [Resolved] private OverlayColourProvider colourProvider { get; set; } = null!; @@ -72,7 +72,6 @@ namespace osu.Game.Screens.Select.FooterV2 public FooterButtonV2() { Size = new Vector2(BUTTON_WIDTH, BUTTON_HEIGHT); - Margin = new MarginPadding { Horizontal = SHEAR_WIDTH / 2f }; Child = new Container { diff --git a/osu.Game/Screens/Select/FooterV2/FooterV2.cs b/osu.Game/Screens/Select/FooterV2/FooterV2.cs index 0529f0d082..370c28e2a5 100644 --- a/osu.Game/Screens/Select/FooterV2/FooterV2.cs +++ b/osu.Game/Screens/Select/FooterV2/FooterV2.cs @@ -72,7 +72,7 @@ namespace osu.Game.Screens.Select.FooterV2 Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, Direction = FillDirection.Horizontal, - Spacing = new Vector2(-FooterButtonV2.SHEAR_WIDTH + 7, 0), + Spacing = new Vector2(7, 0), AutoSizeAxes = Axes.Both } }; From e5b2023155006c15ecb7673d4395c612e400bce3 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 10 May 2024 07:05:32 +0300 Subject: [PATCH 0753/2493] Remove unappealing fade transition between mod display and count text Fading out the mod display looks quite ugly, since alpha is applied per mod sprite, introducing bad visual on the intersection between mods. --- .../Screens/Select/FooterV2/FooterButtonModsV2.cs | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/osu.Game/Screens/Select/FooterV2/FooterButtonModsV2.cs b/osu.Game/Screens/Select/FooterV2/FooterButtonModsV2.cs index bb259898ea..ed558d513b 100644 --- a/osu.Game/Screens/Select/FooterV2/FooterButtonModsV2.cs +++ b/osu.Game/Screens/Select/FooterV2/FooterButtonModsV2.cs @@ -221,16 +221,13 @@ namespace osu.Game.Screens.Select.FooterV2 } else { + modDisplay.Hide(); + modCountText.Hide(); + if (Current.Value.Count >= 5) - { - modCountText.FadeIn(duration, easing); - modDisplay.FadeOut(duration, easing); - } + modCountText.Show(); else - { - modDisplay.FadeIn(duration, easing); - modCountText.FadeOut(duration, easing); - } + modDisplay.Show(); if (Current.Value.Any(m => !m.Ranked)) { From f5ab9a7ff863a2f32870b6d517448b4c8a4b18c6 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 10 May 2024 07:12:28 +0300 Subject: [PATCH 0754/2493] Make unranked indicator orange to match mod select overlay --- osu.Game/Screens/Select/FooterV2/FooterButtonModsV2.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Select/FooterV2/FooterButtonModsV2.cs b/osu.Game/Screens/Select/FooterV2/FooterButtonModsV2.cs index ed558d513b..3937f1e42a 100644 --- a/osu.Game/Screens/Select/FooterV2/FooterButtonModsV2.cs +++ b/osu.Game/Screens/Select/FooterV2/FooterButtonModsV2.cs @@ -86,7 +86,7 @@ namespace osu.Game.Screens.Select.FooterV2 { new Box { - Colour = colours.Red2, + Colour = colours.Orange2, RelativeSizeAxes = Axes.Both, }, new OsuSpriteText From 0392f7b04cfb913f5fe23ddc0da3726ffcbb7d80 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 10 May 2024 07:36:59 +0300 Subject: [PATCH 0755/2493] Add tooltip to unranked indicator --- osu.Game/Screens/Select/FooterV2/FooterButtonModsV2.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Select/FooterV2/FooterButtonModsV2.cs b/osu.Game/Screens/Select/FooterV2/FooterButtonModsV2.cs index 3937f1e42a..08b0407a79 100644 --- a/osu.Game/Screens/Select/FooterV2/FooterButtonModsV2.cs +++ b/osu.Game/Screens/Select/FooterV2/FooterButtonModsV2.cs @@ -14,6 +14,7 @@ using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.UserInterface; +using osu.Framework.Localisation; using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; @@ -70,7 +71,7 @@ namespace osu.Game.Screens.Select.FooterV2 AddRange(new[] { - UnrankedBadge = new Container + UnrankedBadge = new ContainerWithTooltip { Position = new Vector2(BUTTON_WIDTH + 5f, -5f), Depth = float.MaxValue, @@ -82,6 +83,7 @@ namespace osu.Game.Screens.Select.FooterV2 Masking = true, BorderColour = Color4.White, BorderThickness = 2f, + TooltipText = ModSelectOverlayStrings.UnrankedExplanation, Children = new Drawable[] { new Box @@ -312,5 +314,10 @@ namespace osu.Game.Screens.Select.FooterV2 protected override void PopOut() => this.FadeOut(240, Easing.OutQuint); } } + + private partial class ContainerWithTooltip : Container, IHasTooltip + { + public LocalisableString TooltipText { get; set; } + } } } From b6d6a8940bfb15313b8787185f45463391a79f1f Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 10 May 2024 08:24:39 +0300 Subject: [PATCH 0756/2493] Improve animation of new song select footer buttons --- .../Screens/Select/FooterV2/FooterButtonV2.cs | 66 +++++++++---------- 1 file changed, 31 insertions(+), 35 deletions(-) diff --git a/osu.Game/Screens/Select/FooterV2/FooterButtonV2.cs b/osu.Game/Screens/Select/FooterV2/FooterButtonV2.cs index 2c841f6ae6..21337327fa 100644 --- a/osu.Game/Screens/Select/FooterV2/FooterButtonV2.cs +++ b/osu.Game/Screens/Select/FooterV2/FooterButtonV2.cs @@ -24,8 +24,6 @@ namespace osu.Game.Screens.Select.FooterV2 { public partial class FooterButtonV2 : OsuClickableContainer, IKeyBindingHandler { - private const int transition_length = 500; - // This should be 12 by design, but an extra allowance is added due to the corner radius specification. private const float shear_width = 13.5f; @@ -68,6 +66,7 @@ namespace osu.Game.Screens.Select.FooterV2 protected Container TextContainer; private readonly Box bar; private readonly Box backgroundBox; + private readonly Box flashLayer; public FooterButtonV2() { @@ -137,8 +136,15 @@ namespace osu.Game.Screens.Select.FooterV2 { RelativeSizeAxes = Axes.Both, } - } - } + }, + flashLayer = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Colour4.White.Opacity(0.9f), + Blending = BlendingParameters.Additive, + Alpha = 0, + }, + }, }; } @@ -154,7 +160,15 @@ namespace osu.Game.Screens.Select.FooterV2 public GlobalAction? Hotkey; - private bool handlingMouse; + protected override bool OnClick(ClickEvent e) + { + if (Enabled.Value) + Flash(); + + return base.OnClick(e); + } + + protected virtual void Flash() => flashLayer.FadeOutFromOne(800, Easing.OutQuint); protected override bool OnHover(HoverEvent e) { @@ -162,20 +176,6 @@ namespace osu.Game.Screens.Select.FooterV2 return true; } - protected override bool OnMouseDown(MouseDownEvent e) - { - handlingMouse = true; - updateDisplay(); - return base.OnMouseDown(e); - } - - protected override void OnMouseUp(MouseUpEvent e) - { - handlingMouse = false; - updateDisplay(); - base.OnMouseUp(e); - } - protected override void OnHoverLost(HoverLostEvent e) => updateDisplay(); public virtual bool OnPressed(KeyBindingPressEvent e) @@ -190,27 +190,23 @@ namespace osu.Game.Screens.Select.FooterV2 private void updateDisplay() { - Color4 backgroundColour = colourProvider.Background3; + Color4 backgroundColour = OverlayState.Value == Visibility.Visible ? buttonAccentColour : colourProvider.Background3; + Color4 textColour = OverlayState.Value == Visibility.Visible ? colourProvider.Background6 : colourProvider.Content1; + Color4 accentColour = OverlayState.Value == Visibility.Visible ? colourProvider.Background6 : buttonAccentColour; if (!Enabled.Value) - { - backgroundColour = colourProvider.Background3.Darken(0.4f); - } - else - { - if (OverlayState.Value == Visibility.Visible) - backgroundColour = buttonAccentColour.Darken(0.5f); + backgroundColour = backgroundColour.Darken(1f); + else if (IsHovered) + backgroundColour = backgroundColour.Lighten(0.2f); - if (IsHovered) - { - backgroundColour = backgroundColour.Lighten(0.3f); + backgroundBox.FadeColour(backgroundColour, 150, Easing.OutQuint); - if (handlingMouse) - backgroundColour = backgroundColour.Lighten(0.3f); - } - } + if (!Enabled.Value) + textColour = textColour.Opacity(0.6f); - backgroundBox.FadeColour(backgroundColour, transition_length, Easing.OutQuint); + text.FadeColour(textColour, 150, Easing.OutQuint); + icon.FadeColour(accentColour, 150, Easing.OutQuint); + bar.FadeColour(accentColour, 150, Easing.OutQuint); } } } From 09c52f03d8ea7ed896721a9ce629ec58d8d67e0c Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 10 May 2024 08:25:16 +0300 Subject: [PATCH 0757/2493] Fix "options" button behaving weirdly when clicking on it while open --- .../Select/FooterV2/BeatmapOptionsPopover.cs | 6 +-- .../Select/FooterV2/FooterButtonOptionsV2.cs | 43 ++++++++++--------- 2 files changed, 25 insertions(+), 24 deletions(-) diff --git a/osu.Game/Screens/Select/FooterV2/BeatmapOptionsPopover.cs b/osu.Game/Screens/Select/FooterV2/BeatmapOptionsPopover.cs index f81036f745..648f536bb1 100644 --- a/osu.Game/Screens/Select/FooterV2/BeatmapOptionsPopover.cs +++ b/osu.Game/Screens/Select/FooterV2/BeatmapOptionsPopover.cs @@ -188,9 +188,9 @@ namespace osu.Game.Screens.Select.FooterV2 protected override void UpdateState(ValueChangedEvent state) { base.UpdateState(state); - - if (state.NewValue == Visibility.Hidden) - footerButton.IsActive.Value = false; + // intentionally scheduling to let the button have a chance whether the popover will hide from clicking the button or clicking outside + // see the "hidingFromClick" field in FooterButtonOptionsV2. + Schedule(() => footerButton.OverlayState.Value = state.NewValue); } } } diff --git a/osu.Game/Screens/Select/FooterV2/FooterButtonOptionsV2.cs b/osu.Game/Screens/Select/FooterV2/FooterButtonOptionsV2.cs index a1559d32dc..2ed8480b46 100644 --- a/osu.Game/Screens/Select/FooterV2/FooterButtonOptionsV2.cs +++ b/osu.Game/Screens/Select/FooterV2/FooterButtonOptionsV2.cs @@ -2,12 +2,12 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; -using osu.Framework.Bindables; using osu.Framework.Extensions; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.UserInterface; +using osu.Framework.Input.Events; using osu.Game.Graphics; using osu.Game.Input.Bindings; @@ -15,7 +15,10 @@ namespace osu.Game.Screens.Select.FooterV2 { public partial class FooterButtonOptionsV2 : FooterButtonV2, IHasPopover { - public readonly BindableBool IsActive = new BindableBool(); + /// + /// True if the next click is for hiding the popover. + /// + private bool hidingFromClick; [BackgroundDependencyLoader] private void load(OsuColour colour) @@ -25,31 +28,29 @@ namespace osu.Game.Screens.Select.FooterV2 AccentColour = colour.Purple1; Hotkey = GlobalAction.ToggleBeatmapOptions; - Action = () => IsActive.Toggle(); + Action = () => + { + if (OverlayState.Value == Visibility.Hidden && !hidingFromClick) + this.ShowPopover(); + + hidingFromClick = false; + }; } - protected override void LoadComplete() + protected override bool OnMouseDown(MouseDownEvent e) { - base.LoadComplete(); + if (OverlayState.Value == Visibility.Visible) + hidingFromClick = true; - IsActive.BindValueChanged(active => - { - OverlayState.Value = active.NewValue ? Visibility.Visible : Visibility.Hidden; - }); + return base.OnMouseDown(e); + } - OverlayState.BindValueChanged(state => - { - switch (state.NewValue) - { - case Visibility.Hidden: - this.HidePopover(); - break; + protected override void Flash() + { + if (hidingFromClick) + return; - case Visibility.Visible: - this.ShowPopover(); - break; - } - }); + base.Flash(); } public Popover GetPopover() => new BeatmapOptionsPopover(this); From bc6f3b8cb41570e8a8d85491abcd5e75b096c064 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 10 May 2024 13:48:08 +0800 Subject: [PATCH 0758/2493] Update iOS logo --- ...0-5cbe0121-ed68-414f-9ddc-dd993ac97e62.png | Bin 453879 -> 394881 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/osu.iOS/Assets.xcassets/AppIcon.appiconset/300076680-5cbe0121-ed68-414f-9ddc-dd993ac97e62.png b/osu.iOS/Assets.xcassets/AppIcon.appiconset/300076680-5cbe0121-ed68-414f-9ddc-dd993ac97e62.png index 9287a71040091fb7a249018bff4133b91b12e1fc..7b62835cdcfa85d60814f17a89653e2b389a520f 100644 GIT binary patch literal 394881 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 z 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

DatA2W^x6OxQqa_*YszP3 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

dN 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

~Hp0Ng>X@!#&} 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 p)(cq3~%~<$) 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
7teB@p7cU2&ME)tf)cjk^ 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 z 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∓U&Os)M%-|e`T>a3l!b}};EaI2Y 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⪻Vj#%ZZO(pEsHP1rtSEfo>#2PAE%NO?i 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 literal 453879 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 K)~(TTivAB8ZcmZ` From c25ed2cddde9764306fbbdcd490101eb6fb72aad Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 10 May 2024 14:12:32 +0800 Subject: [PATCH 0759/2493] Update desktop logo --- osu.Desktop/lazer.ico | Bin 76552 -> 76679 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/osu.Desktop/lazer.ico b/osu.Desktop/lazer.ico index a3280f0de0b6a935309c84594e8b83cdb7d2e43f..24c1c29ba269aaeb38a9b9c604a1c364c986a753 100644 GIT binary patch literal 76679 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 literal 76552 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

X~%=`gEKj(Ksd$CM;}+tqLyrwl(h2dj8hG;X_w+ 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 kH>Hhmey{g?x&fFIPJqrc*zp~?i`MerS--#2`8( Date: Fri, 10 May 2024 10:04:55 +0200 Subject: [PATCH 0760/2493] Update android icon assets --- osu.Android/Resources/drawable/monochrome.xml | 30 ++++-------------- .../Resources/mipmap-hdpi/ic_launcher.png | Bin 6403 -> 6088 bytes .../mipmap-hdpi/ic_launcher_foreground.png | Bin 7232 -> 17696 bytes .../Resources/mipmap-mdpi/ic_launcher.png | Bin 3582 -> 3280 bytes .../mipmap-mdpi/ic_launcher_foreground.png | Bin 4040 -> 14331 bytes .../Resources/mipmap-xhdpi/ic_launcher.png | Bin 9537 -> 8745 bytes .../mipmap-xhdpi/ic_launcher_foreground.png | Bin 11248 -> 22749 bytes .../Resources/mipmap-xxhdpi/ic_launcher.png | Bin 16409 -> 15300 bytes .../mipmap-xxhdpi/ic_launcher_foreground.png | Bin 21398 -> 35969 bytes .../Resources/mipmap-xxxhdpi/ic_launcher.png | Bin 25006 -> 22562 bytes .../mipmap-xxxhdpi/ic_launcher_foreground.png | Bin 35444 -> 50294 bytes 11 files changed, 7 insertions(+), 23 deletions(-) diff --git a/osu.Android/Resources/drawable/monochrome.xml b/osu.Android/Resources/drawable/monochrome.xml index e12af03bfb..600c070c3e 100644 --- a/osu.Android/Resources/drawable/monochrome.xml +++ b/osu.Android/Resources/drawable/monochrome.xml @@ -1,24 +1,8 @@ - - - - - - - + + + + + + diff --git a/osu.Android/Resources/mipmap-hdpi/ic_launcher.png b/osu.Android/Resources/mipmap-hdpi/ic_launcher.png index 7870430484b7a5d9433afea0af2fe479ac0e967c..a1f717cf6c414a2587644f19444e404289b33a16 100644 GIT binary patch literal 6088 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

e&FE%k)kw1nZ_Ituv@| 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 Rc+vm>002ovPDHLkV1m2~j8Omp diff --git a/osu.Android/Resources/mipmap-hdpi/ic_launcher_foreground.png b/osu.Android/Resources/mipmap-hdpi/ic_launcher_foreground.png index b2ec3e49da191d5840d72a7ae2e5421dd468fecf..36651206e8c7c4aed92db0369b84c279ab962465 100644 GIT binary patch literal 17696 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 KQZ*oxkpBZPcw#^R literal 7232 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 D#&zr+ diff --git a/osu.Android/Resources/mipmap-mdpi/ic_launcher_foreground.png b/osu.Android/Resources/mipmap-mdpi/ic_launcher_foreground.png index e22f2565625261d8ad104f2d09c70ffccb5d2ee8..85f308442952d8b6e5dab778b955d5055e9abeda 100644 GIT binary patch literal 14331 zcmeHtWl&t(wr)e?uEC*^;L>>G?(TtLO=BJ0EfAa#2oAx5OVHp!g9m~qXmFQc2`&L% zXYYN^-KXBIy6;rI`|qr()oZOW$N1(L-xzat&$&8UTT=-aixLX}0N|=9%j+Vpj=xU~ zG{k4TtH~6hcd>;#+JHR$Tx~!$zHnOrz<2&I(>RGy%w6FDMrwj= zi=MWTKGG0o{&DQJwLxdOQBteaGSZbRy1e9$%Y$F;!)irSQjTubhc70p z6|sCtrv*{(d0x=LSdTYur)PHRuQqPSFY$?7Y;sO#b2|d#9xj4@Np{U9pO)W4^X@5F z$LOTUxA=n|;CbT_b+#$KwNk*>(<`-#vt#@vir_Uu`j6GlhJL75VI$$s8 zHg`H>2s_OD_)9(AM?wa&CudRWf`*gdlBFsIj8xM%grSySW!k|1zyV z%!7YJ&#_|BZ_)YiE4wO5GHNq7bn&Cq!^s`XjD_76?j2pV>CKM8&CffLPKN97+=BNj z0Y06_nZ^-s6yGh_>8zxkd|Yq-{Md8v9^CjSc4dg*A_)JZm_bML3?7I2M9yal4Hr(s!~>j1DXWBG6e9F1rB7zsym;gpZHIjyWdE@xia?-KW{hM zH}zA((@5-QDE)PDdD_OZSYh~*_w2aV%AAdDPF*fzk})bV%vVaL$|bln@ZlHf3KloD zLdrBlSl8HldfV48ltOA0Awx|D2<7vRe?R-zKRA@6o&pcW&HVRtA2(DLq~pH2G!C)sgWvu$auFl zrD%lB4$9P8z|3r%T2GV8%lAZ6{rr@M-CbSG)|&$elckA(RzciQjR z9i9p_EXM|V<%d%8Ee-6H27KqPyPZ5eT?zcrHvT}n`_M1FQ0f$19&7qcD9C@@VT9@? z-*m@#>);DRst5~;qE^4xg8xww*#{=|*Iq39Ue(gqCb`ayN7+r^knOO|ba|?raVC|h zIe_1{f1;dX+3*+LielSy^KUQp&Ks7`F7%oSifRZT3w#mi(Z>C~P$dcK_xy>xDu zMG6wSi@srXR!Zv;QstR2N7U@^NhGW>5n&c!E<)rv6P};e>`s+IcC~Wq38XMdpWq$! zG%PI6+?!#aDlrE7j(qT)DJ8!ee2fNHW#u-+AYGe7_;8xIaP&%_eII37@vt^2LO=42 zoc|=erbkQ$^G#}09h#DN*5!z*Mz%P&UE|(PS=$7$_F#Fqy{sd!-}}OEy7%a_Sg80B z(abyfWWd$S3dXhVk!x)acvD*rAKsR_MbT%+9!0;Atts<-T=%x(Loqg(ePygo>PXTQ z>wU#D4&T$jppP9_YUAr^K>-=XQhI?B(T1(xMOLB+b{a(WUv)`vebovu6eIiYh$54@ zrufY58kb7akqj$APP$Ix$M`68od!&PNsr zQ&%|B0hmW+Qgy=i75Oyjdaq8s*(tQ)AGRPluU;wR)pYIVEgkuH@)cJE)mEay$h#KT z{RZ^jsppNn0i_k3zSdN^7RO6lJz*D(9x5jkp_MUzkG(mDdM=|MGW0e9@Sy-X=}jXA zl5yH&O8a{^o=}OLihbTrO)L>fV1-xV^NYlp+%nAq^Pv-u3EnK#Xou|Kf_8=pd$vut zWvAfzQEgNE)Ai)fO)axr9#le+sFN*l?8_!S<&^PE63HPd3v5>A#d;}K0Q9j!iZ)Uc z<_hN9Zh_NUs^(YdRB7vi%wJ8~1NMy)q>QSkzC!jzw2==cLABn7&JtDZQW_d(umaAN z!=!a$#x0rsOI1NWVJupo?edvtoLq`$6%o-M2WrJ~+L?@Vwrczb>XFfkywB?Nd$4@p z2&E65cNkGI=4R>ntvBFQGx-)w1Gq@AoMw>d-ah)ZwfX~JT&nXIs48_yC;}U-%XL^o zJ!FM?ThROb37+ClYk&Ku?ep61l)<%EnCJ)Z=Ay$#jcIp7Q9L1(S^XPmoOr9 zEYKitPP9llRIe#W@sliC;6w#-z2Qx2k!m&NDT>}Dn@IBw&06s!3G|KKIdAjRB1M>+ zB;c38UPIm2X55dX3~)`ud3_kOZUssmiP?7Z!qiU=sLZV;HNkd_ipiv5&c?Pq#JDpj z0$zlDLW`8q-zI$ab4x_XJlkE~G;n)ca0*D^Xo!Ah+0iy;Jfa+Bc^#22m=MX1&-z-8 zijkPe3@e+L!F6@^;Ts0}4Qtp~c6y=#j>j)Gc4ZW=?t2oLf!-z>?lmVCT-ye7&b3Zv4{raDtT z(vDNEGA2Kp=guxG!*Zblf25n7hjJVW%i$v=K+K^0yRn*rvZYtn>746Ao*T?h!GKK# zi;!6}ym;<)i4-)OXxR)rt~^tfdt(q+PO^EHx?E;qiA+86{FNpS*b!&t)oyt+=8wWn zY*4SC3zeV%6Bg`@>~_ER`_KAD7+J^z->Q3XXJdG_{8V0YqCyR5ifp2^A;J>hiH1k? zOg6-obT3dDy2D46iv0r!7!+*{>52OC`i7wgn16le=h|@&2_K1BOCT|sI)^S$M@}M_pP=Rpi=+A8H-C^|Jx>s$g`mq) zVkq;OlHKEGh7sifew33~>CP0tHIyk*CPHqjgQ8zuqap_)c7s0lCCB(~l-x>nYopQxYdJw~ z#7s5;^-#kS1pi5#_hY1HB8Etdkgf&$Xi1cfJ$)ullj=~F;Bk^Kb|}ZaAi_~MoG$k% zf2rAelCfBUN!=mF0(Fuvld~iuVKWralziiL&7)B*ukFe}iZPTgIt||F{Te&FxN+a`8i=C)lVJo({{<5Y*vYgXIw>lYCV4F zb#ew<{K13;A4Y4b=FEM>b)bZ!fvP)(84v(3z@0+)iqe`_Lngpyk(1j+xPXZ6Z6?;& zD7UYaPe|7|j!009(Ree`gH;8Qn33IM)$x7TRYW7Q- zh{)$_Wl-tfcPNUxhYc_qFGPGCMtg=Flx+2MgJM2VVVkfc_>s(RM9UWT6mB*}}u$C67AS5n(7rHcHt08|Q1-$tdY=)ew;>tEH1}Ys?w4q8Pn5)DmvSG4D0??1m81e;K$f*XY4>DpX zPn1@wT(P#&C|TQdRIWqxSs6KJl?Fcjob`60B>_)IPq6l=^r6*jM!RcDc0Duc)EkGyHB8^xYB_59NWJ;VYVZMDW2IBD5o${Ed7*&YqASBok^U#~<%8czckl z)&nH;ABhydmsSRyV}aXP6BL(ga#hMF?W17srPG+c?;|j6iyrBsqJe^UPeWGtN|kBP zwB($6UaAj%m+o#N((jjFZsp6Y3r_o`(f$eW0s9Q!Th52iVM_nA=8SXqL~}dMQuYe& zIJ-s8c1c<(k;Pg^A$@{uLWAqPOk+`1oAq@^%W(4vb|u zvWvjuodr$k=bmE7x+b57d7Ze%Q(Y7VZz9wvTB(Vte32e6Y~)|4Y2H_9Xw2@WEG&CZ z8Pkr_3<`fxccziQM8)5AvkYw1{xb5&p3!c%l(<%i*syTfdWy0s8M#Xh=D?b5WS@`X z7r`WRVugdnX1Ezr%bP!^nv%^-T4mD(cO$m0In8K0g4U4QmT2Oga#GSiN^i?&RX8Djlw4} z_+Y|9PPtlY5gT@GpH1`g&3A8J?(^usQj>sfroUWbsS-iea+mSr>Xz-g$=?S)N09`P z<~<)VB!U;{5T?9;%&EIPzghZS7Yb6fdKZTbk^A-qFH&2AFiGdZvjyVPN~dN$xK5qU zEgE9J_@n6xt>#4>dHO}3or)i9-FZZ$!_3Py?P4v`?{IrwP4#n3JuVgZ!p0j-a|S;TajE14^KYg#lRX27=5622&6!H8@uSU#%Yai%AOHJDCwh>uMV2FOlygM{+te*I3MJwZFwI~ugj>+^` z{dswllg%^LSw*Wok=L`#{Qs8s}%gQNVF9E4u_EZ=%jbX!gnS5U}ADtIS>Jtx7`1 ze$LwA*)a-jgN})i1^)SK0ORA}f(wqZAJht->4jD*m%zN0?&#r*a4(KRpD51dw0D@$Uk=oVgu*Vpdl`?iAGnHY8ls9zOcqX_3Pni^SSzrTItbIU$vX|0F1=8IM&)NM z!>fYzabzKrdrAR1oOpZlE~STGVw4^p6E$5lDZSV5R&->p zKJ7xX^v5%6kD{2J>KrmVpuWM--K~CvMa1icdk9WdIRLD&bsCJkG6w(DBNXx?7$V^1F*) zzv_X%0`eYCfl!JM_vh!G04}EJaZ08V#HJ}yI9yInTSZRpA3Lguebns0LI2YvLcXXm38`BO`=f4II7h>GOZPo80l2cRDK+YBXc%3L0XmtT8I38b%nxAS zw{Dd$3aY;>bnOW@y4|FfmjV_$fZ?{OWwzredC$@kHT(KagA8y!G6WNZk}FADmE??Y z+OCOwJX+h>H;c9N%f0nDV?#|wl1%iC+ly3aml3ar?`2;`Ggfl4_3iY*(99C968Gzd zU9#2iu*LbUtS3+V2(4+*@Th?PhU3ZK*O)b;qq)SFb*Ur1bI_o6yI?)A#- zRkp-bDFVqV(va#y=@ly!UquTX4oLixi<45|&M+kfyL0BDktS`RKZLC;W%ie;2(wY6 zBl9%f@gk%6gc$ZVb;dijc_ClrB7OsHycZ1lruCJIhRYx2%mqj`OQW$2FcU+}|+3rS77U{0^b=Rj&|~Iq=I%poY48 zI>YYqO8@}R0FKzL+hxgvJJ0RV9+Usov1!NwD0Wn%|-mS8w; z>tq1IttA+Y1U0}Ku5vc^aAkjY8$EwbeVD%kOvIW&N)k)lR}=x@Wa9}1`8qi|dx-i< zF#Lflin#wR=3xN+q2lQv!SF;w8zkrAZUYkF7T^YRDfq&@`4}XzK;rJ!wxYW7ihn^M zo+KFTJw07Td3bz$e7Jr1xn11tcz8ubM0mh_JbZjy2n{X|KW9&RS=r<|Ki*4mOsMzyCVqmf8zd&_CI3( zLm8o^p&=^o0`vOqo{GE#!|(n@tzBSnYtcV$ZH291f+B*vTo7JcUM>LwgHmz!@PsIdz*J%!Yd@e2jLU`o4uipy9c5cf1~n(x%v72 zF#nwxQA9WhYN5X?6#?*v91)GEoVyLw)5Tri#l=yA;kQ$u-Yp5qw9_nd> z00r|2h=K)0dBOT%5m7;iD8CQ~m`@b^H+>gtxUJv+r2TvNfW-eyx-#4Y(ZAmxqCdx! zo{ig|Pk%l-!v7ddAkZJfLKF)7GXxK)w~h55ej>2`Y=YTCo$YK8^T%HW`wuz%zbOV= zE2tn$NQj?{7i-ZX;ud;1R(U zqCo%P3dH(Xs@VS38y|a{-+4e##swBcbn^#ge4>b)@ci$GXDuRV1L6B!>mrC41q20z zxgcO+7?%j25Evq0EiAw%EcCDL{(lMYcSZaoJaL}i6XHLTDbDkMrTb5Xzs;ZsTK<$F zW@f}h&-2fj{x7}|srcVK{wlNo%`QNo|8(+i@%taT{zKQl#lXL%{GaOj4_*Hj1OJxt zf2!;M8(mocwN|uoM*P6@L97J*SQ!iut4K5}H6?k#8$cT14Q@i{JVJu$s%-250ALgU zK9K;K+2jZzx~GbU0{Sj07P=_Y>i}180DwqAMP5c9@h_`vxGRlO>cGS0@=;L5@?`QE z({K#aa2pJ!qm_59VkJbV z@Flhq+pAi-%(MdJ%dM|m_FnI;4B@zfg!SJ2AU~~-A_U1@YN5PNL z+_=?)J#g6l*QI#Y;*VL%O!tVF_5F=qk|=NAzP0KOMSZ~$?n^FB3K)!iomf~(@Zg0W z8wb70K#d1O!FZp@=N^~JgIF0Lsti?*OwG978|vY+=3x_y#+IQwn_4W?1J}22%>DO_ z7Mh$GxoV`6o^U?GjF6RD&2~ZWN@3((^rqg>dZ6u=!R%ZM>GnNhlDvYm5;#8QJt|5x z*A5vx6b6aAJnA9`%;^ptpPikFPfSf&JA?#E6Gh7>kMG=Ce466}C|}^N^}sr1R_Dl( zOFkgHF(M?t-OvgHKvb(=hXdv#GG8#8w^q z01F2nf7Zy*@ctEVQvwNeHC0$f%M0Xq)#YJg&^#fJI(h@nOdHT4;!RWhQL#^8EF{d+ zu+!GsD%ocLrC`LmMbWB`dN70E?zxwj*SPzb5dq`)ZaU?x(p>WhNqfINb02GfQqZNgPHl4*JgtIs94#SWvRZM?3(U zg$w$NZ8H=c7a8g6_#wYQ)@uzA0g$Au?&I;LPVp^JCJ`B^&GuYbyp9aU60j&{^76g& za=TxaQ1n(tj9`fLsvux{F50<|Za$3QGL-(fy*AW5kpVo${S!}ZhF2JE_&p;5`IBmN z=3iAQ$UD@kY~Z6v)Fcus8T85fEpJ~2Qc%Ii7&4wF`NA*vY%WCjG@_euN5@|xFvpU& z%FrJc7V;1n@+(vR0rGe9oG9!bG};yl^xG4Ef#qEt`~*?Jp4+!BMz zC3n;VjL3gY+R6F>HxYTG{W<3_CM^n+&7+yw*}QTaNr1JIl9G|Wp&{+tI@sJWPv=1` z=RO^vW~vLkiGAE-S?$Me%SQ4NLzPW?&sXtr)uu2x$iSJZ0Q1gZ?I~#^XD8RI1tJV{ zsO7#$cnP9RRvsGcwoI!yHDpG-b1^-aU&tfW_4tL&{_U)ymb~18ua>pr?HPaPF z9`9#wX94LnT3vqUOpxXZq{=CN&ej8geA3%y%CMdbc40}<3!M|LVh5AqH#??eaoOm5 z-253`S2=|u9wVV&?9VRP_B;=kJH+M|7k|;k@;)fvs{9Zh4@*HNBR-}d$|$?UjxA1E zUDN=)@e6XaYFBLT`S{3?-t)oM(vaX=(~&HlA1|hd4daM5AeMLh>gsA7k5*h+Pqo*? z#H89T)&NPm{1691TK}lO_A@EU2DN3S^>emkmSWEIZnc4OwJd#Si!u^ww&8)!elPH| z-kNH7FwkHw3N_@X&)C-J39gvW5snsHa&zFV_x4X;LnKRfhhCAzte7z0md|=af?A;= z;?zexBoS#2?8o=mXf|MQH+HCZM5wIgs5>-Nj9a(clbwKs0~16ZHsUl)E?08ezJJHR z{j|oi+sjDJ^3(KH>EYp_Iz}xy;AR9|1NX3pBswdDRo#fUZp+C_IKMkDF+_=cUQi|;`Pt+4T*Jm=%QqZPh;Xz2-E>8 z=-%R(S_T-|r`JwSPI^kcX`IGZ;7`v0AmSJ~SDg`C0vXJ_ZR`sA=d?^{Fv#Vi6WJ_KnOD?3jpX`lR0^cYfdf&Wh*;!8@ss` zb21Wu3edc@%#Ve#;MRpY4iDXbi}?v#4}=n`<31RdRO3GhxgX74U)*;h?*!w3yUJto z+)uIepZjq+4zO7tomO`;gz9clev0TCOypO_du`c#N}}9ySCkTgB0$XWqF3abV;Ul; z*Ku*G43t1^j!?C>TGqD$rc;!ZgX$IFKE-!+0YPo*<985`9!ApKhe@ z>wG}dEMMt@V_9wQIDcx`!d|nO0>Jr{2hLd)BIx26yWKMgDnwDbXtGk0ER`aE97#xp z8&P1mVpFgH=~1ZJ@}&uow9G9Fe_lUxJI%btoIWLBSk?IfHO z)@vCly(Ezvdj462AjV;}^VCG&#U2TO)8R#phSGBxM0CC~_o|g=DYSe~5aWElW`&B= zmWo1dy;AJ8f#0DxN|QOz!;MQew-b-l!wwlhRC)^v4;geCu04pdkeLh<>uN`9UR{j@ zkWr5i)yQ@~XZ_OV_>E7{MxgTjdo7W;Nt?{=XBF(>4R1sGaUpRiVmS&L@oh_(SNq{Ernd9m8tGizA%DeglF-xqi2bA?)7c(X2C{cv=ZjFLf(c z65Tz|8>ifHbS)+n9lG#ldosRlYU(M;@G9Qnrs(M7@hCegmfs71?13IJRBEEle<93D}F-rioNq|cjEgmYCNmQ~5P zO(7)F@+8FB=!T;T7cD-o2nm4dwt4HGltBECFO$8o2ymF)P;lvFd)S$5InvQH?Bdq? zHaf%V7N^o=aMG+X(#>7Rx-z8uGX&6ReUVDD1=72r1X1xB7!;3uovjDOU*H^$)1VEr zhiga;0=gETTlFV>Y8Rn?et*3auT|!FcYk*aH8y;HcXOV*m2H&Ro#@-)AS?BWTzpYl z+Sk$=6R8)dS&hwI9G*g?%Er17vQo1efka2~gAa(dF=)$YnJveTsf&eXN`Gz1V1Hxj zR#?d}Y>7tSoc#b-%>r;^|aZSC@gG<6gPVzR&UUTlHdH z9)Ps#nCC>Sv>)?FCLvgCH#qjhHWQ2kl*Pc9ymO*t9$9oB6Tz7bCn#_fn{>Zj<*T2* z&T#tFLKW~$=p{x@Ru(i82#{uEWPF%hSlCxDbCdv7nl!5Bh~t-vt(k94xb(4@$yvH=>1n77wZpS!br!<=YfY6-a_Ndt2^fx$ioEmnlV$1) z@t=Lfoz&FS`g%i5$kIkeWN~y-lI{aEKBjh=+u|-}D2!3dNJK`yXd&Hihu15$DONS} zYeM&il1FWq{0YiH#lSMAlq3;;6M<4^4^VOF2E^GUHsL9=j66YwU4GE%0MaM0kkG|o z9F3TXUfI+XF*%@b!>uLo_Tt`N;Kt9Hl6qQ_2`>k^di90dSmLNHy*mZ^LKBpn_>P$# zCERs`r56k;4u>1Bw%BZqnlTrL5A3rUGXvg-WLPX9^OFRp)IFpcnO|cCKM4pBD=F&I zEhA}cY&4r|@z^mPu1oXXuSg)L4)^fhygNw%G5E~)3P@^8NWh$%!eWMVt97yBoXB)_ zt{=7BD(ahw(E;r3?ROWResB)-TKO1gbWX8XVP5`LojLjT^7w_cs$Ym(+>MwE()YVJ zZP0Fusi@bZ<$w0?S=YO>p_2T*c zuj`8YkvRjhxkN^#-J`Z+$!|+bcesGBSm?YP%UpR_h3z@_NtwRj=c}Ur$lYLE>-ukX zfo5ehIpbA*BRtM1!l)^_j4mDLGaUgJ2Nc~*mb&5{e3Fvfmq$wh{sVC|NV(%55MUX# zHF#u~bYbdt!y_qg8)K?3ZPXWwwVF1EI}$6t49+f-iD#%a31%|dx4TeP4uM_{9vvNh zv|DQRdhhMyvtVNR*m16=+u6zK4l!uTQf;Du=N2E3>2eBCu=5<9ljpFz6TksxD6jSzY)}z0UN7D$Gps z`&SM1BbvAsdRH9BI-$}RmzPq_@W~RTUqKJPE-pL`UEENUB$xJ|pWe?_JvCZ8 zy*9zgjWP&&POrT)FjwnAa1y3*_LVeB5tOhgoMOH zv)jg}hdNG<<(V0wvSn5<8=b0)v-37$!IT;=b$>-hNlE!}R0JO=?avfy;$*lXMxe#7$g=~I@aeLCht2Rb3A z`93^q$m)yj47;@VCXL^bI#)WmU$lFRN6&rHBmee*ryCUjP)dDAz%k~twNvL_OL~E-p^;eo9X(aOdOgQGmav?}v+C5Ra|*$GqN~EH#q2 z*)9AaBqS6xx_=&2xyws%@RKB|BTN~;FX4zHlvpd&QP`tT9Ia9rRh++kwW1>Og}-3< ztX?@>lOfT!s=ofT&1czfhn`-(y`n;`Q~aQ}&V5Vu(QCzyr%jG!$Gf}D{Iv0%pyJrc z+{ikowimv>SG9;C?PS{O3D;mE;+0~bU1vPM=_povQ8ZgxY&7 zS4zvo@`Wu!B8ITWWbD93j=jM5rDivn5D zlamG({Z~NUn(Lg1-$~I8YtE(8J?a}j+wq*y^`8*p?D|g>{ByO$EZ~3($M1)S#gUO{ oqR+3uGXD=dF@LF?zS(*JxNC=6p119V}|-T(jq literal 4040 zcmb7{CCaGa=dV|aAMP1tS2n^f3i;YDXYq= zL8F;PU0PO1aQFb9E1bYr@*_7E2wS%r$PGJW@9 zmG*ufk4~}E05EpugLre?)V;02uJf%o%gRe6uQxu|ORLYb8>k%o_j%G8hC6_!YW5#ICUxDhuM^U}FC`X|P{hw;-*|n= zdYfH_UbpPh@zbr)P)2mo!&@nm1n--D$U83QR5DNxf1N-Do z*3!gZ531acXU|PfKP^Y+ki}RvdxlVB%~YL@AdYnwZpa4aPh0xTlvtFJhl%s2or49j znViS7FjI%nHIEpo@G4G_#{IM{A9&DnzIg9fHj?Tyh)LvY3gP&^G)G=Ab*0C_Tt~tS zW6M;b5%dM`unuIAKfC<8sjAAo4m}}B$J>0JK^C8_%}Nh77F7X!KtQ0?X}ku!R#sU` z&{iQC7qpNJSIQadh6SQeej}ELC`X}`>tWukBKpk2!oOqj&g4%Vx`2gDiRW-*rq-U+ zPiTY@(_i4zdZ+>s^{Yk`mjD5pTv_UOjQs`YJ1U1ugIiR4FDi${Qr zC+aAW{n7=0%Y*R#u2mgTn@Z`^#@OI$WAXZY3)^d>GaJWvf$1Y3u3`0Tn^3Fyap|%( zu`r0COlk_z6qM@GI4fh|A}lQY>c4jTXTetZb(ijxf1wODssBPH2aI*8Tl2c_6H=7D zzx$I}1Cy8yOf41l!ZyxW0mLEYefLl+sj|d0Oi|E|&wXh+$+QCfNFJ=UpV}#L;G=zn zm1}YREY+Zw&2!q@ie5fHR##V_8O4pTb0i{>Y2zc&(ZTiuQc=Gi`C$4sg`n+zELw@= z0M^(JnZ7V}0%zpJq?;*+fz2J@b;)z)nFZ%}3uRD22fr{kEO2xK=*?9sE({3#{pQ-p z$)e3e{O&qe^wY|e$c7z%D=B2HNBR1Bbp6ojV_8iw&$$vq*$MWMqyl2%9aq}sI_4Es zCz|kiRYL@bzy~%mI$RHYBrp2(dTonozhjF2xXQHFnO(bIFl7GrabiRVma%R>`9+^1 z>H5pwb zbn<)!T9GAIerjau9mnys)ADfnKiM%T=OL-Er)l!ibPUMw>uD+2)86G!{C>oYMNJ5$Jz(;-OFv0v`vJ{@w{j0%nAIAbhJ;f`CD@**2xuGT~FK) z;ZB%a<(?lYDOZl|+q+(h0%AOTr?1Th)j>OB0|Nn%L!Q5r!$)NDqt8(1?4g9{^rSca zmP8V-)3K@T{yOQLWvO${c{y79AF9JH?S?Y;!!oauV`!`8*Vucaa+SrD@Z=G+YmeSx9_ZHkr;)RB@DwPD(H8Q}K0O*-`b`%|c`FLmH z=6}~81ITf09Fz?;l8_0BNeT3}SGI{#J=Ey-ldrEtWF15p@`Y`V{=~QtpwFrr&0jkQ zHnocAJW-0e!s@bBu~~CCQ#MrJN1=6B^R)mg1P6d;xk0ibNH$~^f+32q!xu)0(%Dhr zk#Bbw&oG26be4|*;gLY{?Knps#w{SNOvHo6N8c3y!U$97JmiWy_t?l{X)bp{G zZo5fPXLbl1T8>e3D3t|}(?(T9Wu>JWc+1tgjG4Z*=Q=@nJD5%{b8X~ZNwCs7*Fz@l zjksVyUOOZCBlFH$BJ;X5)$_o+$WMz+wEBQ;7IMS1`$pv^E9}$}n!9#noT4-Co41;r z`JDdH19h3{qUM0P=&bQnOOl;d*8({1HoyajaJg;VkQzckK5x2j_MVnKp)Ge`l&CnO(jVnrxx9v)(yb7-@>#@gV#)YPFlFAS&uo8w ziVf}oWp4>2>QVpUHzm;h4k8O9@iL_R!VjpvM**3?3xKjeVAwqM%ZuxX|KSPPx0FQ+ z2t&Py!$@Y|3(dR`nkcCn17lt;YLnhawL8jmGulU&GnwNZ2O@qRDycDO{qJ}5q-rAe za>Nk#e(XE(rPs&V;}hcntU0OeqG!lA_JjEkYLi_wNw4xw46uxmo-U^DsV%U-h~rd4 zP;UKwqyj}l;W!@9oUiV)3 zQNXh^{?eb+FM~RtqbQ{ZW|hHoj&Bggzkq=wbAR;Av zj;&AXn{R^m_?%Y#;8zb5TgJsQA0q9qyWNHFUMT^EmbaTWCcpwl<+=?Cx;Px})mdGM z7tRQu{%tmX6auc;zQS9O$CWgld{ND22InXU1evjkj50_w)?4(AU|8&)0(lY5^5qJl z0~Ym;1H!1s9~H?kN9_E_e$6*^JUM4C-IGA-C}T*nGKfq6R4rIybxF0nD^NAfpCgfd z_*X?mMaPRJu+%TxhPg{O;2@rfwO{{D9S27P2>;?tQN}Z968c_z8@AQsnUsw?a##B* zGq3{TxYA3?nfkp+L=i=||ET6{A;p^7+ibn~nt+WrH2aYpwx?=(Tpa<7nK;$nz3zc7 zYmvLoV<$_f+=SI&7IP2t`U6W|>X!`d-#>RGe^z&zO7B3qQQ`n!iSNJ{}1HG$BRhSJr zO)+Fzq#y+>TArKffH+UQ>Z_k@k`Yr_>8y)9nidd5kxkAOAoMS}{?HoU?^zkS`ep;4KlYe7_}(6$nJ=NjsgF_TKphk;kp+!J?3W4#m<6g>M# zbIY72>IBn6;Z(}z>>{77h%+7?9D}JP7HXgyMwPmER(JGfw%Z-;?3O~^r<#8zvbFFr zDagxTlIgCzllW?1f)(cD;^Z4TP^woR|HX&5R~xkM+*^kp56_!TJ`lJ^nZ^QPw@K{@ zJpC2jQRJDe;j8)q2Qic9Hd5U^T=T%lN@;XB2;lRs?3jlXK?h=a%|$SrY;qT#EGZ$Ce-RDeLQ+#vNkQr? ze#`c8$sNWmF%>21akkIW924B^Wv&=33`5ByUgmu`$jMs1XrtCTM)h3Wt$NU+}}T;Id9fl8XOt9lIDfHMJ6V0`Zx%A6v)!cv9hv;P(}P&cB}b*{LVt$ z=w1)^mK%jGX$Z+d|7ecN&2ht++Cms^?pEH|J336h=C(2h;H&j>0&biY|mRR3(i ziaHFhSrg^AiYRNDH<{?!!VG9>{x24FPX)Y54-H?g0V4i>=p_2O#yZtn_Tm2l4K2g| diff --git a/osu.Android/Resources/mipmap-xhdpi/ic_launcher.png b/osu.Android/Resources/mipmap-xhdpi/ic_launcher.png index b5e1a9e3798869f72105dec0643dfe18e8634a54..28be07512ad61c4d8caae199e300b5746ac047b0 100644 GIT binary patch literal 8745 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 Tsg}M000000NkvXXu0mjff3M09 literal 9537 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�>IX}``GPV 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 diff --git a/osu.Android/Resources/mipmap-xhdpi/ic_launcher_foreground.png b/osu.Android/Resources/mipmap-xhdpi/ic_launcher_foreground.png index 1cc3fa9072d0bddb5511ec97d47bd80a7513c78a..613b817f0a54e6a0d14e606d07fe4fa8611554a4 100644 GIT binary patch literal 22749 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�d`^ z>wY&F#NN~2t0UM6QKrh%K1W)M)E|-UY8B_yln6)qm>L3hMF7hf!%D)PuEZbcHa@`` zuSD%~uf%7)kDECk#HPQSx!WL$9>ggA@RrTGAj_z1%zfJ*c}qV*N?eEJhdak?MlvAw z(`)W1`v&?in+ycEVb=2|{6ctRXv1>HQqCKZfu#I_ldoPqEhm9@!}Fvcgq%a~3Rnj{ zEu36%(Z)pvmsHiv<(so2wUnuVZrif{{_W6tTF$9u6KN&*<*CnewLdgf5naCTU~YsB zmJN2;75pQ;8`KJn!^lEqijOtzg}Whs`S zC?N;wBoOJEa&U3!sf>w;fw8R9D$y(fJ7kH8OwtQDM5NZg|NA4!au9(YFXmh^4SLp0 zhj3jyrV|tTFBFn|62orlJ4upA6MeG9(F>#L>x9=lO;X?+(-7_usz1T9OU{Etl_r25Mc>2kJ6%)jz)kXH!xK5T_U-G*Uj;2 zCnY7N8}zx<^Ht#a`Q*iv5f2pz0MwLLRjDQSB_c(H21OZwQ?TKg)@wYO!jR zyZnTX&|G&avOQ0tJSh@6H$U>!k6)fw@D+c!`NNE0Cz`(`<-t?qwG`9DOR>DYjQwRkT84X`MNr9ZW3jP`smSco^h|;ty0EcJznf zQ}&#-Ws|BE#0#nS@$5o?coRueM9UXIg*NNrTLg}A(8*S|i{C;)9gw+?C8jqJcW6U% z3l89Q&hHo8ib_F{j7P*b{c_6`ETO6=QLXGBs-(WL!B^cMct8@LPhG5mms`;x#gAR> z!v$H5p`}fW2$X(pH3fjd0UiG64(^s=g>L(*ehMcpL4%V@v;*a*PkMm@=ctHIA0P>( z^kzh7LZXToC>~Eo-CNUfdsBxu9|KZX;7SQ(5ivy-5UvCE zP$_(Z#B*j|MUJi-EHN@b*OPJ!FOQPu%=H^-z3wIqoa9PLn?2aMLj@UvJ>x+9+>BJT zA9MYScXKw=FN;VFWvE2#48S5ekmkWgXMec*AMJ^iveLFQXo1WZ)I62*nEm% z;OVRick9L8RGto!VEHw2`}r9A=;9(t_w)S$arP%eju`qhi%WBC#VM41QX&LAP!9^? z6@i-bQnaOW7d$eR@dBqBOM7PMAV2grIw8rQy8vQQ{(i3Z3(|*ymvT7)4&sN}Ro!oe z`C`=~aDq!;{bs>%SW2QlXtLlCjSY~%-a(RU2K7vC^Z3xdKO^wE<|Ho|zVV5n;sa(J z>EKiq%(h=N4`2nksDNx@gG3~N!bI{3GrqIGrj_tM7p3p&ib}Wa=@f5qa`WhWT6^%z z^T`_2PatDcOB~v#Y*CbScA9)K>t1s;@fHGU_eE&VbLB<$CWxDFHqedq|8yUQ#=~0> z!vR)F+tUx+)jvZr;8jS1w#Zm%-MCUG!c>LrGcm^jkGXLLLR|cBm3ZOQSlj}+)Sf)V z{V=;X6AFXy$qP}+5L#f$ijudHX1(Ar5E2Ohs`V|#lB2I&Kq=xmy`#~ly|`@2_*fI> zCS-&Nui&DJ%Pg4it&mqPqpU>(mA`7QLBbnpQ^$%Ermpiv+Khj7h~r;#1ddHJ-^wdRLf6|Z1M!>F+ekBpL^PFg~6uvM#cBh*Pf6li*V^pDN@9_supeyHQ5=?j@k{-7HQPxCX?GG!kGnD_ zGFn1t;X@=MCt3xD&g5Z?p8#vUiHS+IhVts4({cN{gl47Yc4fir_uSo&Ze69Nd#zbn zSyMQmiLl6duH4YMUzqvybc$aUSrd}Df|E*aSh$j!9VCDAzv8=Z+pq~}blC*{a3J#H z#r0jc2q1g@h~ra*E((@mMdULTBmc~e(Y+_^UzyRZuNdfoE;m(>dlzL#moZM4qDUke zp@H`6vrTv@p6ia+MTPEXENpCS{{>_B$zr}5rM+IY9N_{oa`IK?ynu7ZVOOt6YCiip zMH2z`*><|wZo2RNCWC(UGOQ#~o6eOu$6O_#&{8(ZrY+%TJ=wEfs%P=}tu;^M59vG2 zd)FP%8|iapt}&azG}9asR%h1Y=c z|}`_A>zviqOF+7>7#^whGvoU?{Y zHe5Hn=mEnaE2# zG=z7?i8~5Hx3|5?6A5+h;j%XZ+Vu4F>QCFulEAgaMPb&?g<^pk=fH#4Y}u1-NfP(_ zGbt%4W^x@QmbM*sq!CJNPHI7dFE}Vvt;Sw`d#J3B2@k@epSO11fFIHgmu^%>^Uq3O zuWWSqysHb~uFL1l#(3gbiDickdvI^%cr}hjWL|N;#r~@r(OtY|@Jm^)C}$AQh=-@= z(*t{TM=Cg8)d+s;-Bq}m@6LORZxy(G!ssu%T{0F#7fp$waOkj9W~CLwzH=$oHK3_rkdKP$#h_x<$~|Qm z|60ml&nRX_I{~R5v}D0^lF<;+2jlZUtqqoZM`z^u_3h8)3Q@ebvO!b1;%;?`e1e9O zw*@lMI&>*g&?W!=q|o|`ouOWtM{JM%m;Md%_RxSxBrat_dnvZhfsvny_n=py29voS z$)-G?K7AbeVX}on3*f+y0;f5B3g%PA*BZ@d`N0;P$n%^q9PEar?xU!yn-Cno;ce}r z6Kn-*RAowG(owGCwfFU7@Kr|eT}Hcpm2u{Jt9xly<3pbPzqcJ{?EC@70(v#G&h}W}UDkbgNbj(qpv|jhi2>a}4G>GU*UZ zuY-lj381nD3<75S*uBvk2fdcf%lj6DaP?Q{3fj=0ZPlaqKbN2hZgWNC{6D;9RxnZY*QjfK@g5A*}JP| z1MyiSBX9sikE~&+i9$2J=&YM9L3x<8J1s)2jAC>%r=x^5mX6(#I8V-9ce|33X9M{3 zM@L6oOh5V;A|VqrNYOtulL`|2rQJ$C(rk&l z^?wX~I&P}iCWsKgeQm$0Yq?1+Rr`2fIu-nUq5l6=Z|46{ zw_O}}=Z++MqR75P7(B^s8N*O9$p|G&$QZ)dvy&1kS+iw{?2$%h>{*Jk%`@h4bN zVPu_#!Qi?48PELk{b}ZU&G))K=bY<&-sdA$4~OdIE#|?z3}P_@wEU*^8C~3v<|azW zl>gnk6h>Uy+_vj@*<)Sq(tN(h16-~b34=18ql7G=9l1Hzk%%NR{rSx_PSF)@bV~(RJ$}c^Yn- zD5Y?5$*RO<)?`NLxi}wm?Bs!{b`ie58B5k-| zlrTuxD=V>s&92H3k&%+Sb0VG0HBMe}QC}aQU}T|w73a8{h)&n{fZp@C_wQBdwlNIb zr=YpEp=~u#w?41$Ip2n)uo-kNL9=cO>mJwpx>+(1YaAXOSs!oo$K#hn@orZUNp~-( zz?5Hr!)X6J;btesg8aZlA;WHa<_X^c{ab_g)7thedylC`A4KW}n!^FF-IM??*IuC@15g-sH%dYOx;U#nzGL{-@|R>B zrY{MdhNxVBxP2wN;LSlN_;w&!Dt{X#aySOFL{Q;707Z ztqnq<=OFEGR8%sp+_zRKdlP^iBUyh8Svd!w2E8EI?oeudfG4|Nr1^*qb~(}3DO}#! z*0#xWsN@RwsqV!SVrTI;MpYK$YD)Tk8}No{x^Wy#+rgPyvC8$WOxQ7D8n1YrMh{EN zt)S?ZTE&}B)JykhBCw>q9cu5Mx1mxNG;Dn)`e@C0;KJ!agBJe#8`Lso5*pt=O$)a; z@@Aufsk8(=WYJqHDk^x6u3DaX3-uq;1+JzSY8)RgIFlr?_*i^UB(#K>lq{W#xV^4Y z_I*D6j^Jr5$8)N=oEjxH)cnw(M+cXS@x&*rFL#$`aax{EDCH)8BlG?CxJMJp1HxxJi?g(Bj#7PaY5Z0L`HQ?wxzZ4Q%Kd7W3~n z*2DMrz6am`xFNHC2N}VfW{$2>K6VBG8fZTdyaRUNI`{paZg)T`fYoE#0QJ@kcH&nZ zs`m?>1S1Ook4B5{blhxC`4LDAE`@bBt)qOIp(ND~g98>0)EosTd$;uY zP9B`of^((KnBLV9I=}6h+Cw^;>v1&i*;o(HBiLj|AztF<4x#uHbO>ed4nOKZ+RvuERVLk`qX-ZzUI1g$?jLWig2)2x(HR#y5Q*y#oPVxTWyP>GB zGIw~!vbu~el$%`6r?h#FRMf_-1{LiAFMNm4L8H;q^L<$YYXSIklMskF4D87Qe6G1MoAdxvpOQhaPUbWTE9F| z`62B$wZrg)%gqlJm%OmAp8TodwITgx!zU%8Z^c{F_1A2WNK)zn!}*Xw< zt|z1gL8xGW)Ze;+2c_r@(Ap~kOfR>>=Kb`ymIE%!vYag$?0tNxa5uUowzHcnbMw$8 zGgkD}=$!gXuF2$J_2m(%q~nyDK^m#LEVK0JD&vHAC(6QDvwYk7AV?XHjg933FLMFX z{>N$qa<~ck=I!}Vo7s2G0It@y4!><}jS_;>>>V6PdtD+OAHsm1V=n9KvvB;bCQ9;S zkym8k(J*od7q#4VrAK z{^mds2^ty!2s~6a8qM~z@<#qbaB%RFza>E0c@Eb34%1kGH7()c;koMUTqIv!ucxP% zA-TVJ?H$lr!qST~s;$+ntr59CQzGg;!$uujM*Gk>tAQZ#O2vGl@5<7WT~o-)w;M>< z{-6Hfk=^c=y+dT71K7Mo%1D)~1yr^Fnou$1eu<{fNCCcAF?IYIvCgT0V zf_vnzfvkQYYnvYrMsA4IH#86}VcxK&NG+u5 zfL}v_QH*Gybz|^s$xuYPgM5)QRqtutQCmw27Vo{6)KU;Mfm0EAbf+z>?hf8PdFwW4 zDMuf1wiOny5xf%mra>*@D{E^W@DmnFXe-3YN5{vc<(N>-8w%x_acM4%=VGmt|DnQ_ zzN=?hiSds|i2)yiYz;}voI%DLv%jO7V$)bRajRb(d%*CFNge=+ZK gG?$czpE=|dQ|?uLd0Xl!7@=Y_yJBI2g}cQ554i#_!2kdN literal 11248 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%0D@fov_`Uo9QUkSQ$r4(*a-{?f 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 F{})kc|3?4- literal 21398 zcmeEtWm6o_7w#`45Hu|A9)i2Ohv4qIxD$LAcNT}>?j9hx1WAzKEbg|8ySrWf_cPp= zw`%6iRLxYM?w-?+oD-?4EQ^6kjQZby|6#~|lUD!lzjxCAyOELJo|qiS{`>Dg4I?>e z2~Cjkan8qd(*BIw0aFsUz&Z|I-hZOw|FWUky+MBeZoI4=6g59FRl6c!Ihrpby+=Yp zOm3svlMzQ{achDsv?>gunO-?%ApSA-jF_%&evmz8 zXjhe`rn0hK0lA;`p}6z!xU!wIL&4mzpdDjP;lCA5yFrGOIZaKuF-x8q?d5Mdj>kJ0 z1jL|NA{G`FXfFvqR03lkNVtRjL2Hu@h3csq(6j0uw1h-uaU44~qp2#9Ql?SyeL!wl zzb!(&v{T|dz*x2pCrxd1ffiTDilaZ~Q`tgdW~^j7J1I#^D4g6xNeM~Y(vZC?eX_TRCJnoTz221t53)$MzLXKM1p@k+*K~J)j_+{~wO+NLM zMy07rI)}ncl&mnIeb`?>tz;h?NsDoIw{mHcm^a3DoIb4#m*a;}*E-yx1z0rw%Q;W7 z=qnlHDuF_^)~?p!DWGp=W=42}=xS_@y7!8cJ?!KlJUl!i6tFWPfS&youZfo--gWMn z=C+8b1o^Lh$bXXg7om4}@p??s&Yiei5X03zoeFVGSR{s!P$tGQ{3UWN#*f^g>EwD?sLCM4Bh8Si6x_R6DrW=|>TON;m;(U6~^n{=^ z`PG>T)D*}pA6l%QtSSk+CA@!}$*a+-;KLz|LQB+K_@{*j3(N;Lwp`A&27MIYuwK6L zI>NI~W)Xx<3jda)yLGD45*3tn7 zOdVc=eAN5K=aboyIO7u-F&h>)W#*JYTHXl&Sy1&EDHWWfAe#Q1qE>x>fMb_Brn<*q zB_lc6oh!F0IxG>$N?fc%NI-C#GdDL^Jri=kA^7cJI-1^ZQaLJd%{2XJMSR()A|Cm{ zJ7_prcOUq`QRa7$^ z?>vDmX%hddibgtA<>0{~N)MKKS!MBJY(7)!kPEMZ)!g$J&>f`2k8HEUCPa;w^ghRiFZeta? zA#}DGiS%Ti5rumSaVheds2!|j!9dp9d}rtnoYhQ)MBQ zE~W(`66hWhN6MsP8egQl_Nl5Ejc7ybKu7ul?#R8670lZF!dvL@&5S8v4J2b}X(@It z;=9&gILQ8L?fYrA35QPE*FbY!loPFzEXR>d)05x$hSdD#WB7Q!5ysjNJDQoQfuNtZ zaC_0mQt_J^(O9=JpO7=Yq4QV+3!HcVxC?cNd!yB@yICSg5kmm|!|mdCwgyMn4vR$vPug!WXQyKUlUuI8{W2%W3Zs9?k$H=QtRokwYPPT_ zw;h2qDB{mhf^NdRj9RUXW6ums1At0rZ4I8&yVmK)XE(EUM>b38iCvDGtui0LJ6#!G z`EH}WW9FiHYGMBfzoJiL1rXXOi`V`(cuFiD^;NhT{P<4#oMLkz{`JT1qZ#224*Wu+ zOnLQlAA1jT*q5B@W6jp*;%v7VMoxU$Kj@z{6FuG?B%&0A=-!2q%&`|d@)In%|9uN0 zCt3_06m7Z5JIKi0aoE@G{9}_R3YyP&)5lw3BE+~xVF`u>>VBb zWMg7N!&}et9>RM=9gq=ThlMjds&MBFMCgYGR!;nrVt&}g?fs4nWQ&@@$wV9P^a(VK%19Bt(T2ucoXb?6KH3DLg8E%?mVJ zX@q5tZ~iq*LrdDb<$W86xG}`cgaC60n2z?(vSr0}1TQu=7C1A;FU8!M$LsT()F#$0 zm(+kCrnLx$(_qX0;if_Z8S1XyjlJxq=QXkPHTVb9Mp@~v<~oP(&s!5%IfXnQaZuE` ze=~{GBkh*E88;7CI9j091ak@ysxd3d%RkhO5O<8RkAnZJg`>S;Ld@uTyzf0M!cs=) zb<`Mr6UzT(hyESfIF@s15WpX#EIebrW_L5uXP+d$3dHUNY}2 z1bvx(Vto2DQJy2@wYC9ogQB~Qi=&I>rKNf4c)2A_TBT#%RhX-3RZ=x)k}T>-Bz0mplL5I(fDPyYri zYZvXo;WR0PPNffW?fm;a)+br8E+mN%ukvWgG{~Oq{=O|7F0wvIvin#9%j<5a@Kxm6 zydM4D&2H$W)0%qoIi6lQkiI6Rknr|QUSO_)M9|LO{&fegj32(wk+$l4Ic8$_hI&_- zwFGsEyZ2T#w#c2p;Wk`-f%Y2cYJp?H>p7qeQe?ejej`F}Cs!H!_yN3ypNDnb+VS_u zdtoht$Nn%{v-OzGSRpL5NvGl-4sBVqRBU^OYL0GdiiykY)Kt&1kpl}L;TPl$5#PG{ zdRNZj1ifrAp{3Q3uOD1F9!>qIiOMSkq$DM4++AiffC#RO*#d4gj<&XomZ{V^bHm>9 zPlrg0{v}rV<$NnZG+5#e3Yvu#sfY2kuE{$z<F<$s;k8w+Gf_4bVUO+E*(%3l+uGW0+d;n6z5LV>3&|-dBGT`S$>Tj9 zID6NUMxM;`*^$tD71Y8S^g(7*4d;*`R`ho9y)YB zcVxbATSQ@@1ANJ6Xy{=@Xu!JO?6}+_qb0D>@vHvXNq|-bqgL%y?CU$3f*RrmiA?5= zQozD72@t@XeToAe8og>qNt@@p5uWyp!lCbCu*j^iwhBM|l2^vM#k)VeyF=6(tDIVF z&8y*K9({kk?>;v>tAw{MR2$RFj+FN}TkG#(DJ9jb-Y~Pry_2T#RE(F*fqj`Y{}5GB zw$W1*lo3Wc;E2|Q^)Z;XQ_4Br<5P3cxX!?D`0{Q9WBGi@b?zZ41?*uC-D}AUwWX$x ziS2syYI6sC0ru`NcqaA}>t^i66gUN6*~t^)E4Pu{d^A*0)67vQDk=)tf$K4&Z_0f7 zqbw(PPoXAmHzrxMXH#cYXd3hmL^U@8`;E#k{gth;K0lh>$e~^I5NHI0m@srwUAA^E zEDoR~4_NIdYQ;5MP2T0%_g!;wt@w8<++~jn8pm4ioWDH9KN^eWMqdm2ZDvBHv~ngd zPsy9o?N%0dboQk*G^Xc#PCIWI33PIZz6KKvo2Gy;CJ)o#daqG_{_+f1A zMN{OV`Maq`1*wwLq`A-<>!Op%teV`oMDpDD+2Z_?>bxq)^Rg5^JG}mgP_@6ybfdL1 z5pPge%4^kQewOtHJ3}FRL=_+%#BYa>P1Yzwi_sf3Z(okkIas+7#nM+t3NIv3g>pE! zD592sLGoO_Py^6!raO5*JhXWm{^tK=s~!26U|ijM?QX9O<}WEaE~;eC%UE?OyWE-+ z#N2%hJuJNdxG4O~p9c%{D;ch>9x55Qy1Jh2h~~}>7*6?8(a;oOqK4{8;>NaF;_^9F z@Q_#@c?(2nvy}tzKwhF@x?CV0ZI31XQoOY&j&HpezM@JYX~;nC-cGBiq#Q(?0SMbA z9&~g!p88v_+Xa^E$KK$MyGuzQwwF=YIbYmgA#b{c<>}T?Qmsc_+Mr>f;Z4^1(@y5j z&d%*qj1?_}!$g2b_!0lz4rnX$~mQbOF=H{D+V)sKTtpgctTdhZ>Sx)WsH8oDLdi3s`1YLA!KfEJ2QPW#gQL)sHL;j}c zoSBN12m1w~&j-RJAN)+a-HUl*24ez5tFs6>EFlZW6*UU+Uqh6?4S$j9JlpBoi}c;|3L)RwM~hv0|xT3f=Ov8C~M zC?n+z-91n3osBImWza0X(=689&K^T*!T2t^0obrsmxe}Y59eCqe%4IW z`YZW}C(Lr%r37>11o7~<^lVmBv2ZDS&86h|!E^?$4;cx_P50?XvMLFG=gJG`rZ^$% zxOkiGJZC*}j)JWIX3Zj%i)Ia+3rCAUCwGM|6s(I#)86)0F%YKt>l$*Oa@e@NwS|t; zjpJX&HGP=)zOJaS@TH}pq2VtxO4N^A8hVtfMF>J@a4`WV1my$LV-PA8YE@Jb$v6vf zm%O)vcdLA_;`Gp{aL&bL)Gt|X(>am59p|vx^R^ao{5|3~}UWbD~{K$~jwLdXr=<{Ev-^Nl3iT#2t6k{|g4av8-*<1_1rpO~;IN%iz0C zdU0baEW}@iAhjtP4z#kIeMFPT7;e;v+GK-)yPzI#GaGA?rc`+u4=v0A5xjWaqt3WAoy`>`qSRQSY#d_he*bv_f7dPhJGwc7glb zhA%5_&{fLH5e_RVRnwousERf8{CkRjH;lFNDe>z!VjKFT);;yo0uRh8(QWgOCn^tY z5p}q-cin2#{yfKJxM&KyRLc}pIm9azNUK!KLzA2%%XzE5ciVGaa!=1rsqf?2LKVfJ zuWdwDJMoPJ2GA1HWx1?!-(bA(cbA}=wcXM@XTvOS1CyngmeJa^h>nhq2*VC;!s+P5 z*=_TIxK)PAk+|!UmFvfCZ*O}3%Kv&cEdS!DJ(gBTI{D9G*y8$3iR~VMG6Yf;6`MJvyu^*Evc-kPDG;ZZ~ zFxmQ?7MtR`A_*?>eD~0baXj=Hr)qc7V8*I3zBSNhcWdj?=#@Gp#DtNB<+<#BQ^&oz zNdvxFtNLY5h;*7(`>MNvgcJ4Z_Ap}$QP$tBS1f7k+XX}W6v0gmP4@zIR%3AO>%kWEO7EFCMg9kB&nw zVX5K*ih-^R!VnH4#WDNI%|UW4*tP`j*rw>d&AFzxoxN8Vx$L%iZdqIbs{L9mXGY%5 zZeeawMjC3bR}eY8-4A6)=ZmczTS4%-eTAy9g0RZ-%V-A{(y5q|X4bTtgYcluS50|1&}p0Q(vJ^o zr`$&XWOF7V$l*=sBl0gLV$t2mC#rHQemFjgGs(W- zvqbvi9{b-75PF*Jw_gN(&6P;Q{;o-PG&tmit;Wl{6(yMQ8ePrqy8K!>a$HH5)OI)n zeWyFb1ejA+-b8m1uW1(-7mGXb#zEH?*k67oxEa^Qk)wX<6W1maK}1;=&ztSyJW7kH zUuwQG-)ZBoLHl9U?g{iZ9!3pot*|TNmf{~fR5buOT1wJiZJxnV!}Z|Ad&F1JR!ohf zj&o^2ZPC;P$&OKbP7HWh3rQ)4iTw4u4;-737#CsJoc??A_p;{3MgG+Ql^G=0`IEN+ zPEAg#bpU(ADOy2@q@`&=0eAM9nVF;k&st^b7E(g^K{5rB1&LZhDt~{>T33rB+JBQ` z%l6b}6jrCj-8BdN$}KGl{n*(E(75F9kWD8#u>ezzYJW--aS!t~+g^8fHasCN9yHRmpOoeL35US-=-BZ~|aK1HxmE|7X z^aL{sE>bLE8^iT*G|{FJarEihuW680E-^1HAo?(?tO>l+7^FC13Iynv-K|}^OT=I@ zLkNZRh36hFgFYVI3HOWS>wQ4;>Wej0bexS#N_|t!_lV9P|DKWHOPA_UE@Q@%3S5ws zwHy>Yvmf{KZ7GaTzDL27iVTd;w<(R=FYTG-J zCvX$OG<^n1&HZ{_&cnGPX4KTZ=55v6o|^%V|7Uc;IS zb&Kcq0EZ06=N{^S&Ux;CG+X3B^R)^_VsN@Q?;tasnM;n%!^6WFvW{7L?jl4z&6Kc- zne^kw*B`w-D#sXO$?Ps|hNJI#LMwAFSVx0#e{M%}qkh#TS2*v(&=h_2t6U~h8mnuY zN!5`%E@_%E)>Cn$<#Fv6V9l#s6O^LmS}%gjkqAp}=hak$QM6{CPOCHN% zrawqCr?fSdjx9Z0nN18_Fq*7V)TZ~*D;Iqwv%KByB1hIs~5t9O(57MgD?`b z&8Ans^~mb~4sTi^fsB!dhsR));&f4bWqKypihM@=3T{4V%QwO~E;54JR4ekm!mejBts7Vi0B7~TFgtC54pcAY zXNBeB!RZgJJk6>og~OQZB~KyKHF8E2HSfnhpA6_^$$9S|9XYIa`8^)9p6CL-QSx5@ zN6L-f@I2M!K#h0`3i&tXJy$);E7Tn0)-k9zYzkHP1X+#8_c;_d6-~n0jYO;4B?|Rl z7qtkK71($oqlK^KhO7;vXi>HQdmqUjn5JL%9Fq-4k_d(r+=|=|DRq(Od_Ati6_C|! z`rEUSVTq}CK(lif<9?avr?HkrHv8Lr>x{0rM7YoN0~UGVlLmVq<~l>=@s8IQ0&l|h zbhEEvAAK{P_ovG^%)@jhtysOcfm zUljTFuaPG!mt5uwN-l%}#u~@ueA}^l<`|+k$mUd)b#Geuua(KFiWm2A@J<`eNrCBs zCxh|X9HpwBhH*(;sWw+>F{_}$CvyhCd_gE?ZWS)|alEuAd1RyOOIxhq@=+F6B_^%< z4g`ajr^#Q)tL3%0;j@k ztRs7JaO#seKSo2PQU`w>SvMnH$s5=mR0#6qF~aN1c^e>02V?|K=jy*+0&&rw)ILW$ z!f|R2b#v11OX(qQsyzJ(`frx5AXTN5bG{fgB_No$|7_Czs~_0 z!oiR`fDo4nXbMdDu_A06aw_Dupe|2Z7izajCTpSgMt9K9g?g6nTFWxp$`hrW{wG9l zj*ly^TdH&ac&$&wMq-^dA7lJqh;N$As9*ehkX~Ood}cbf8)hA2 z3nGGcpU^|_BH-JZVXZ32q{72}Kv-Px!U2}F`w?YVM%hzt9}f&F#Ur$_R3FZoc$-S! z*L)Fr8C-{lhgaJQ+QXPeOOSGEc3$@{yQjIO=BBsPBrFNjjTTHt92!!^kb+6RgOWG> zAjY{EOZF?r3ijIBZ)&VP)f8Mfr4?KW5G2D2XSu;5KY#jKDaVAYyY2LSSAG1BCz-0F z7Xi8G*A>=C`I_S?Ota5wD(Ho6vhE$%{!IR&yY~cXn~115#O#&G-Sm&UyxuKU6OTJM z=FAmsO;p*1Dms*sweTlwq!>~ny3GvXkp!GCA?lHlcNuz{$osmh^;}BM3O);1OE8NO z(hZR9cvl|UWTAfrxWsw}m)Fn)^vN$iI!A7*!}@D3ZcNioA~}<;)eQ_DoA1c{7W?0{ zw`kJFdqeQu8O&#lq7A_uP$Wqp&xRHZIbcJLSsF$t39CQ|w_`#j5H1WRIvu2SqOxV8=taS{~Rv&_K6=L)ouFk~O4_a5{WOt89mQlF$*xrO-is<#pCRTju9co$DgYP#-i&Q)D zhLh@Q3g_mj@@&$mW`J;x06<@(1W=MbRc#C={q|ZDk&L_V5kf6J?I*M(M@Lr?QP?6d z+HW)+fNtbCN!Ot*)$^j zz`K|Z%Pw_7nXdQNQ+0pY6cS^I+Euj;s9FK*$y7iI6tjbb@B37l(9y@EueGUA<^;-A zvhN!1-2xYaESN4~xVvvz|A|n$?;N>MscY>?79y7L4p=xG)2xgPXOzba*8Mf)0zh~> zt8%cri{7?%;>Wi*KMlvl#gSXS4C0OTYbDbozNO!7BYI$8*ZlM(DxM(M#bnKw${%kz zOx?cXIn&#Q%U#7!Nn35ThbH8|g7);=Eh|R7)`;PGvQqfsZ~EIr4A(ayA;AC{3qGK{ zDoOS!c+z-Y+Lk6wPox_ZoehXyT5#aXOq;7GW?cQAe)0NLc@!J2^C4v6Z&iILN4A;F z8aw234Pvh@r70M0y!i+%+Y+nBq=%ftSBitss;zhY{A)pZ6rxO+9LH_dalyJS?73zi z$BV8K`g4`jw|j~(ik6%{V40)3bc*Iy0LzEVFbt)^1=_LyLEiD?E#lxj_cIvrlSgs$}*h{4n||!{sIK(sM?^5LIwlt_(_3wh(F1@&;ct zWlAzfd1iAK-Ax)#e~Yba)4cx2ARq$>DVhw4_K_7l(**(?X~RKyOdaLP1+XoDcf2Mc z6f?~)GB;T)ADtgXWuKw_xYCcj(#X^&4B-6P0vZwNI*LIl#UH_~E7;OTF-@nR9BcMN zdbacQ!WZCzDU>@u?7cm>*8qDa%!A#uYGV)zjOzn124Y)TmM`RW;CZ}#p2zN5$i))HR5#(!j7ZOd_< zLhb`7F+d0YB)NXJPNt86hqr7ZeZXi2tBa_>U_v@!1LQ(njm`TCzhXox%+HW*fVBGr zvWqg_?D7>*!IDl@T;fVIgM)l#R0s7Ol9CMDKS~^stxjAsi$lhg1mGAkLloJ>Q45!k z$({fAm&s@)b9-jA3?Et0tI~^~Vb@_wDE|E(YlLO>!}y2E3L_ft2Ud^ZvzmkVKHrsy zXy;;x;^)&i{YX4K7j&2uJhM~pi)h}F6I3qJN$seQ-2}LOwh*l>jJkk_!+lg^2legue_5OerwqDB)X7*Dh+z!<* zjFkL%iYTRvz(STHXrA@+Me@HN|4>dVvlgic1(WFEMW#}KDXT6PA*&<$H5d8uwLqIc z>`s`omqqWb)weE5DmYo0Hl9`jk=YukIHO(v>F2QR3A^4BXuX#4@}p4;YK2lt=6kRy z@ikJ&L%r|R<&VDtQd+44&_Bf!apzq$lkyhHS5k8giZ~?B0=Msbr!cq@*q8sIN0cCA za=X|NZaedvemRsuHY*;XftaNsCbrom27}Hv{s#RoslS!CMQ%JR?xiO;xd{{6gFG6e!VXo?g;(y9>pvfyN^{(9jdWj z7>k`lSOM+Y0V$~iq?jc_1I!Q!#C1$iIN5s;j5n&SLKh#ZlY-#HD@(OX$YMO zzKF;d8#VL&i`k*|dlBl9YOG9YC5KR-yfbDBAI~Ni&Wih#F7fd}spvfw*M47~w(8W9 zh$emp@g5nqXd|ws`c%24>!1=mK?e7A!ES(8TGP7d<5HtYCQW`68?CQP0kTlNL=`6X?ogv{>=Q1#?ue4Z9=HVv$fjFMp!bd zw9v>PK`wCV4}15aTB?8pk<7JU#>Kj;6IxAm_3AMk$oKZ6eHt}xci)HB)#Thu>3kj{ zm$%PvIFb3oPES;NQ_bN^h#HLev4;T|b(WbeT~qw^#_ zj}tHu|EOHzCuOa=6~m(G%9*%?aHUv8C-{6n&)~m-;EI_ot7hI)p=3g;{oa`+hV*ky zz!~)-7jxb+;Be7$@VVa=3l)9E6Ej_w=$&8;rLx-duIj}Gdxmx)0uv;yHpuTtj0)=!3PHDn^&x(j%P$U(A#<=RDpksWV%WiALJd$O+#0s3rJkFE=GH@gMG&WkgYH5l`nFTlo( z(6pmjpQeKro^{>vK_cR>YR^_9pFZ;(t2H-;>u-LmKCK;5{}?vZ@BO=`(Q@6U&0sO%&rFDSSdLZ*^Nt?ixM18uh&B*3#Ihl^A*5BoYiAK3cTr>4%r<9^6n8z zWD*6eNgp@GTB#dftc%F0N$se)uP?gRaNwuW=RS5Bj@vwi z$3#G)WWTK$bBNfpl#yLR*>k~GIT>LD?!4me6G0&i{Jn-C>Z-xQg#_XUqSn)>La_x=Y#mF zZLnOAc@zPH8fn*nGfQ81tKl%%H-yJs#*nE));*!`I!jhgDzt2N;G$7AR_uAfHP6k% z)#P6H6F89^o`9JAd$#Seps7BWQ-sULRrVe=Ir|I!#Xxdz{cUyxW0Us0#*8kgcrMVv zqvpz=!dMp1Ij1z$?C|~%Q@{LbTfh?YDb{&1?02u14Qf7ENw<;OWrb59butnHfxKZq z_t@yPS2I@EEpN!Yq1)K?o25sQF6NFX$fo%&8PU!{C#c8Twh{mqt;#i8un^R1?=X*zW~gHV~pHIsyx&@KyiDCGAw;_Z%u@H5j4t=VE@~ei)jK z(*jR0R$d~ytrz{|NWBfSW{f~W;TV0{{89-LW89!_-yXnW;I!y^3Q_GGb>)bvt174Z z=O4)h>IVn-jWI>pcq!uDR&*&C(Rc2k$ZE*JY3vc!rpI%HPO-X+sw5>PbpaXC#P_K0 zfb(zUw)$-D+!yWogyTxyG%}kuMpvBGcM7X-82R{LQIGgzTIkZ85 zuumhgJSBJ<*MC03!^bPC%?qHqU5EU$$-OBb)&&uMGvJyj> zE3(pq37^Is%^|%HoGJOb@2h;?!xt?wC0AVfpOs{{TTI57_)@@6w;5lz6K${NN33vi~sc@2#9b@GHA=qp!Lg5)ab55 zp5E4pSejW_&i*auYzcn)BpY%{;yy0Nj9r5QPOfH8f{goBR|W3llhuF>`FKPfe5wbbT0NIW^ddlwJ4KFAD!<~K7SFtl?6lO>)ET*? zFRO5Pnj%N%2Tivw-;F;(f0d=2Ctf1-2+KuF%ox+y7p=C>RoyiZ6*Cxr?R^)QmGfQr zG@xZTjt5bZTo7{Ylmw~RdFRcBw*_LCI(m>&fNI>ETQrUuvgqv~T7Lb)uyoM|2CFk) zL5@AkVc+M3P3BCuf|ANOWo7iVX#M<7>@h3=^9r4?X4y44o}I9*L!Q8#sYll7IQL=! z^#!9C!1~K>NolFo`WWB(lM+F9c=A4Nex_Sf`OV7O+M3gAkC|HR$S0q?A2Kk=p^#3q zPr;w^>Ubi_NJ-k|f^faC`?J7(+{=r@8BMG8nt6@PVS9SJr7Cxe4RirC#P^+4*z{L~ za0KUrElQZyn17OfWsVS%H$vqEQP%B{j3Y-r>3uOeO4@18Uw*l(pA=-m(HCOEhq|jL zC`y*I?%JSkI;8P>{H8PXqM(W})?v(ntg`!wbQ4w~2>YJFwV+UjJP8!5*C#VOTnPmg z6*aZ3-Kv894M~wkgPyPF*B54Hwsb4y#URDs)bRQh(W)kNO9?0$5{i7o)IEsv0Y!ZH z?+doSXUY<2z<_*BEenDrtHI#Gun4pLJt-ikNO5!zlaOg6LTnrn@-`MMCl_Q!l#=~5 zEB`sU)zqfKHh*iDVj}S`mt{L4tlvH({ivOZy((aJeH#Z< zQ#z(hdW8lvLT&E%Kt|3ouNpB`2JMksrsr|(oGLpP0 zUrW_*WU;89Xat|1ydU^2ko-QwDX@2e+9*?M4IIBfLB{1kWDSjLi`?A5bKlJ8L!eWM zV$pW6gIfllTU$oalyBQEdfHuB3~<3h`t8~H{n2QW{bzsZbnatr3jA$f2F(4;C`sn^ zMd`oJ*>uZ0Q>6L=d9Q*sjc`8qC;nr!{mUb9lu=WX>rHOTZFtAOT{(2{67-sJ1v|}4v^~MGJp`Mf zNu`gr5%Qjiqp{~Vm7FXt3s2Bg9HpWmA!*-lYlaZ0p6Y`eD4FB(3Y*q68uUf&Yue&879w`ZhVQ$mMGKk!MMe+0Q0Uze_{*-vwao^K z&UYhmmXz#SH(V*`xss%OYpHwvDknE&zk_Kq8KvVHr)Q=0do*}agJd7Wrmh%@bX2ILMPUOP6k*aLIlWy&aL|>TBo=$msA~c|X;6VN`Y!Ah*@< zZ3it;*1r`W)CuTu2L~9&oZWGrK57>8xnUCbuYsR(ujVOorX3{WF~c7ajjbJ+J`4=y6Pcxzhu zCEt;upXG7QG*1i3(`j2=oBNPQm@ZLa-(G#kI!3==6G1KOsQ~5H<{H8%FxmGU^_&a0JX%ez&FJ4;o=J3fupJZ^sL${=-@4{;Iw8%afG<`}V zE%tfEU^ZMf+Mcg=TJilZ9Xe*2e#@n)bIiBai(#hjWB83!X(>e zvt!3+x8Hg)ca*YtMJWB*OxiZ%gg_W?c7JdHm^;pd0t@S0_WXB?7p9;fgV#R!;&=&a z3Uh)!aHh8d_Q1hq*st^DrjEFAxqJL6{*I!LUj?o7Q3@F`xlU#LZbUz3ajVY8k#ff$aAy0Y zkq{G`owV)dQb5*+XIY)ZEW9}g{oRf?Quh~7>q2+9b(Q zslK7N^p9THq{hVP8RZx@2bCZw@dwzDY44i}9~Wy6rG<@-#{5=Rs!}>w(cydzn>j8& zuAq8))ho_C*AsS-PGm(wzSmAV-hch)_f5D^aga`0`{UR(N>a#9M1;n6q{SuE?f*(t ze*4~Ethe2jsK1GQmAnDR)h%np4){o9KLdk_=qkNt#x>(BRnqZm2(0L&3i=CCtXgfo z+e;;o)y8HwGhMPrmziSLoMBoesF%v|2@Xc)eN{pZo^!hWTnhtyM^h~*RQ zjsWEW{CI0h4|7e>i%&v5cm4JMv9(~klZ*O-e%$#Pp2F8Gcd#Wern;yY84rkX5gIh< z-}EpAHnq6L28QpuW}f79!qQ%1Rd$SH8i8XVPv8Y!f#2>!m{5ihF<^V}(Rvs>vco|* zC!vU|rRvcoS~J1w_?&LkAr4a>IFB(CnY|T1SL|&vXZ|cWgiT?LXJbiX=j5dPy3H04 zIgaaJfa^B2L00_ShoiE>iYk$+$BygR6r23Ci&4+MFFBD@5lp<-5FdHK_~EVo>)Y~> zJ60eARd2Q#my-%}Ddw8J1Ix0rbwxVRZ9T?qeBivxar6WXKGpCM-DzNlYK2>SYa0rs zS~2<0-ycILMMeGpY3UewgxonA?!yn>=D9#n$4OC9F@1gX?T^<^ZrT?9{DwVpLk|ZYyEdrK2EYCi?qU_{L6Jv4tH{D*PI~_H#3ZowCc&Ybj$ppgQ8S$--PJ9;0lE*(74$ zyKsM-9?h(3l-Oj!@`_>PD$%}UXQnI{fA!qkp^A))6!!j2f;o$eiwgaxu&)Fvv*xU7 z>>KzZk$j~kC1Dw~(q&%+HCW>UKjG7=8&BUhZMNs%_v^s^@!Hz7{>&`QwDziDbpm{8 zjIFrp2(~FbwcxdV?4R7khybzX(^GMJrxVF$uug~$j9ngGOu>t2Xn_g4qSxFH>HL_E0J$H5-KR7rY6Ple^GCpc}Er$6ui!^tQ@s6r~B^8-#ylU2g^)q{O5P$ zV^&HCVlF);z31%|cY%iy5ZwoElPzWTg5%yA!5{=cyvi3L>Bp4+fRy1r`XL0jy#a(F0oa3#=NZ)&?ZTW4zZ?J7|L2L4$jE)Yp&%t+OEup$Lr-;-b>-Dz)E@@7y`QGrf5k<1#r1`Ve`oqZA^NY9O3U2FG#$-(vU zW(=Y;`cM@%&}xRB>U$!1`^QlN@(t#0^Iz%yZWh*2L@#og;OACe=0Uq~lW!quq+#2g z4snR)Q(3QvU+%lC=mdp?mZ_+EW3>$(=e08t_(5TyuS zI?}nc&;k(!1R_nk7z7lg7%8Fmu92(Kiz3oQdapt#B7p#cA|)Xp9Skjo&U5??@7H&X z^LdYP)>wP*x#pg8ZnJmLC~*Rs$%qF~UdGgt$HnDR=CVR{xrIvuG$ia#v5VT|-IgN|4K}M`;3J$3h4IUMS;fpFTdVXTEp7 zp{XyMex=N_SG4_^tYA58sIIQoaOoU*Bwb#}fbzAJ)XH*!dgjgNJ{GADOrNCpav%Cc zL2sePaPKPnScn9|H)`;_P4M=}&i5g?22wySW_Mn;*7v5Vw z{U>vB@5Nn@LGQuos714}@TS)-`)@rCee(fN%ApT9JkNUY&kjgw z(XvAdLtD5DYt7J^<~i#wmu%{m)S&w)mFp0KZyYf!7LoXNR2mGiSctW6@w)>dE0EH< zSU<}foiSB0^*vd9kJCKJ0!%A+YqVEt#}!St9hLjB)exj1PY%HtOzOiy zOmzt1Dp4PDOx)6PiM>!oTXahEOY32CxYlNs7erUJQr39VvZhe+p~ZXPWrMn6vVp&T zreli0qeqV<^(^*Xw)oJA{A@K&>1T>#1S%){{V(yVY>HA)DkkAviY{q)M_;)wq&IX^ z{=|%fYZSPM$pTvd120}WSR^Mx#7rk+1f@(fL;K*7MO{P?RjHKh0qDi)eBahOeKhXS zRE^<>fQ-1L27mGik_gy?zvP|l7B$$1(8dF>oHDbYWp&<`4*du@oFca?iHf>jXXKqU zG|#iS<%2GZ`0U5{$L>bx7lfJ*J!#YsjwPD^jGDA>p5f~-EnIp9VZYfIb{e^UpZRsz zTXbhSRx{N__Z6edWnV>AnKj1f21j-%^n2gK!CRD4dHn7fPG|sxyw`6Mx}68Gj&XzM zTq5f~E?-XiWw?xHBGBqi?DO6aqGPeRB#iO2hDD&`L|l;=vTvuFPRxAwX@_v!?yxTU z$N*f^<=5X6d_KYtKhxr`CDKl$HcL_Sg5JOiE0oTl_qLR`=I^$s6kgGsGt{7pDmUbG`QqPbs*yAm$ z6xz5yXCQ;JQ)s=n!`oC5!|2;~cJ9k1&UngE&X|KhB~q82n_HQhC|SPOz9-$WSlt>{ zE}>w=hT2H@@jz^7b^N1lr9`6jL@S_2tP=xFbI+r;Gp0;`c8KHUWL2C$i(BcNEz2Fgb)|uXJ^1!H02e0?9mnP$=|w zyOx3&m$vBo5cLEyeI9}kqU z=0VF+k6(mIW0TUL(T^bl=3I_@T2Z^we5p5mmT~)FK2EJ@&%LzOt!cOj zC2gd%fVx#r0}=sac;&`zqNhf!YnX(Wov(8M#j{+y|gDNW+XR)r7c=Bm@;6*d;yET*@Nq!r$K$jD2KAewnnx z6txL{RcMs)v|%r_J!w42?#T0D?7ki~ zbC_fZ{#S=resgqd_!{i-vwUUj(caeSd^M9p4UasbDA|N&J#k~I!7FcidfLt{)zVf? zZDBaOK>TV!L%?dp%CkArqr1G2!2$0!H z)0-2ZZ7xogoX?YaTUHEOw@8z*e27nI=&?w7VZQ4S=J`r zdDQV=(;f=L&n-@=d&QI-e-d{6?+N9fR(J3GqUhi1xiZ8HF?r1+m&i>U^;4lx7)({U zcnHdtQ+%@sYPuTfzf1sRhxMleqe31}>iY=ZMQ90v^z@MjH*ts%V0>tK@7HHl%eo0P zGp^#@Xxm7qQOC}^=4Q3+g*3<%ogT;>!{`_=#- zH+y?~Ax8VlwQgT4-bzvAdSBQhMs$~0&t{lZ39^>0E+E|+Pk1O7Z?bV9_Lh9j|DcRa z+xpZ3#Le&C9bFYTUe-Sq&b>j3IfvsNzBBf&kh6drXmqdW96W;Nkk$y<)n3F~24(ybz zR~zk?uCDGC+ORFdvhOE&ic!h81{9%KcECMaTO!3gugxjddqt?Qw3LL_*js-UYi2|8 z77W*2yzK=+yr_!4v%j#N^Id~6ObE!w$-2PCe{|K#iVL>$A0+|9mEj~Uv@4-#X}fUpH z;lX8L&AQyDKd42SK=jSBj()-=wE}?aG%mka^`IcYbCX4`fGJGc4(XDflun!hP_f&l zzRt}oZk9hC#B@3IeI1?7pw6<)`S{|y%4z0i3niRi?GNRIHEJ^QD<3pKL=6AA@h;LE z+jDp72X2j0xV+-?26;P6Kc#CLS5~n5bE+g@307(%!}3@-Q~U3g%m)wH%ma#g$X701 z0SS|Ux4(1>Jp0(~QfrTUrAqV!DHwlI`6!_OKjG^JhK6i8X|jRKZcpBSUu6Q&dw$o3 zsJOzqhQZz%QzEP(I$uBW@t?{V2f=Sh4yWefl}OPW&g~xOlVKdjzxyS{Xdjft{bw{+ zKFl%NZalwH9d`s@;dH?Gn46D90&|Pgn@n3f%RbIYmYA{c z+jdkpLUzqZEnDap7!oV$nokg*Jb|w31f)V&_}TNawBMF#3rKuc`)d7^=X^aPcBMy~ z^Px%Yz;^KL*-nmTBxv5m0lV;qIVL7X;E(gs9MZrQ=&fc*7ZMmE{hP}C5Cq*``O{@Z z3coWj^O<(V3#u*4hRcSUn3tUHZ1o9rXbB2B`J&NiKiPpKd#T#Gy7AOEZ}_IF(0zHK zdw2noPY;)s7!HDn_d|dLt#GhKcBGeMH{$Xp&&YKkmQCQ~Xi4joN+$(TOClp9H2|69 zLg8mS@cqj(06DqW*x1NL)H)6B_zT!iSE?sLRhdeQ;gZ0DU&hg~PDiDq_lnQt$ZY0% zM*P?t)@7Ti@u!kpQ6HeW-&NVB_7y)@!UQ7|5DZp>S;}5}#Lz!AjsZ*-Q1ok>4pmS;ooTKvkM(=%6yJm5H{>lJ zM8OkiNt}p?NcrJx6x)d=x;MF#e0oBc^W4<$3Z1Qn>j+0Kqx3Q1+f0IcQow&?b9{AI z?76)09K!po_jAxu`@r@W&z6N$TTcN+w_&d$ql{7i;{zHSuqnJB10ZLcKp-ds?~s}R z3#)Z$gmZlI7XaDKXG%YQoSB=*0O&zoRaI50h_LV{ZiG7Z@9EAmP6m>S$UAF}rw3L5 z%QJ4G=Wt3{W!J7{MwAIoZZZf)22Nc|x(39>aKHf3m`->pU%m{j1y*5Zb8m7@* zOwuj0@ZFQAm{x=2!8;}If^eJNuP>D7vFBc4U_~%*A@naYa6DidiLUINohc!L`g{U{ z;*)lUuO`SSDdm(cb3sPs+~gH8AaC=`ZKSbY JIpTig{{TBU2Ppsm diff --git a/osu.Android/Resources/mipmap-xxxhdpi/ic_launcher.png b/osu.Android/Resources/mipmap-xxxhdpi/ic_launcher.png index 65751e15c9432447c18a4af6a03ec970ad82c945..2e722ee884f65d1932e8cf3683d2b10c28251bda 100644 GIT binary patch literal 22562 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 JPDHLkV1h{Eakc;e literal 25006 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 E0Sm8>N&o-= diff --git a/osu.Android/Resources/mipmap-xxxhdpi/ic_launcher_foreground.png b/osu.Android/Resources/mipmap-xxxhdpi/ic_launcher_foreground.png index 05c6829a47625e8c647bcd3141594d1d5cea7b3a..f22db0c6168001e8f817147c13be1f55b9689cb0 100644 GIT binary patch literal 50294 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 literal 35444 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 Wm9}oz-ruXr00f?{elF{r5}E)w6xqZ8 From ca608a8443cfaa7ac6b8f1204235e750fd9fe89c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 10 May 2024 18:36:44 +0800 Subject: [PATCH 0761/2493] 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 e784e4e0b2..5452a648d1 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -36,7 +36,7 @@ - + From 4add1bf8163c4463c296b04cfcddf76b77acbd5e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 10 May 2024 18:38:11 +0800 Subject: [PATCH 0762/2493] Update metadata assets --- assets/lazer-nuget.png | Bin 11930 -> 17094 bytes assets/lazer.png | Bin 328536 -> 448852 bytes 2 files changed, 0 insertions(+), 0 deletions(-) diff --git a/assets/lazer-nuget.png b/assets/lazer-nuget.png index fed2f45149d74aee14421e74fa75dc117469d01a..fabfcc223e742536a3939e4ebbf0187484146450 100644 GIT binary patch literal 17094 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 BHID!Q literal 11930 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 b!4O3$rBAdmT`k@`=Kuv673nHTP~iUodp;H% diff --git a/assets/lazer.png b/assets/lazer.png index 2ee44225bf02b4bcd19b0f6590f0695027f3f1ee..f564b93d6f58e2e2243b8b07d1f578ff6c8ebbc1 100644 GIT binary patch literal 448852 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 xr?ZQv-Cu+`CC5YiIv=YP{w28A0vz5KIH5x|r z$3I>l7S!*&0$yCh${Jb^al+u)*oKvb$$+K$0wRyZ!OGx%k;o2l@iVDyVyRV;R)^j$TEdIK$iCE&wT!N5U;lZRUALopWBJ5~Pb)BRqlQ5ys# zaq=18O05O$Co%!C(@bL=qk>~K{f5gjTVtkX`3_4sk;ZXE#t3Tc2gdMOWBs{PN(dy6 z3=0$^(TOh@SjNik<}OZ|?4@}A$RMBGq)%&2wPKph`^oErN@w<{ z5VxztU50=?xBFa1$CZj|8OtFcJeRn@HvU(sAxMnf?{7F}x$Q6_+f5DbIbb>5gox4w z$38&Q8Y?E*4(B4*XvM9RjwLBYAvGw&nN~LT8&SzrXWOS2Aop-@tzNm zI?TLagvF;pks@MMjHCGj#nPywS6n&YF*lf8J4fm*Wpo3ujVu>1-VK~It;4r`rEbp< ztA|&@5HKzEv_K&6XR+|gbS!W5k3D^Q_jv2qR86}qrW%+S)SL1t9S&{*>B zrUNYFkehRuXjsRO?s=>2Q1-a4u>EuLX#F4T25~^9BO8pkM;6M|MpgV=`_3W~?BNy^ zMibl#&Y>7fERqHqB`8i)>jOvfL@h_TGxv3W-0dXRQ=e0iaIR|2;&udZN zb-Pkbi4%wp^m0>=?fbG;9F{tC_>bmiST{$`o_+6sc|KcOWjiHluAL(aqh;uIDty}V zw>nt{S5FPsd&K=p^duZUN#pXY@kR?RuL=5m@=iP#*DK$t5!4J}4sT8S{fP{*z81j4 z-MHi#Eg~;NJEQy=d_AQyQohs_yxde-bO->jUWP?GkxwlEW9vP|*1abZ&ZL1^Hk)Wv zBsZm)Qz>Wdtj4g!-4b&srGx5U3ZAg;dV$JyRt}Pvu7^iQA$Y@wp0w`-FA;M;I?+p? z6S}W3@vq$;OLeWZZ>egchb)aM@@1(wUSE(sHF6=5vLYtg)i{@QhW`vrPv1IIwEBLs z_muBNHIqMBe-kP3;W~JAf$|uIP9=sOqibfKhCy+ystAw$PO~(p$$%1ryAyrTHcD@~ z>G{v`+QqQ9=PVY9QQ?4^^b>_2g_ki_GT%p)$EtO?DYr)P8Ha<6jh`Uz` z%rng5ps+M~bhBw6%+heqbeQr2+>Jgcg4j5g5u@ig0st#H20cPmx47a?1%EayC z>12K!&tK##z+)c_{#WcKps8SYBP5509q^_l?gM+~T|Wb7alwD(QQH7Ei~Y(M9Mu_N zV9q6vqlEq`o3}IZP73o_nKxiaeQkJH?FOw=0M}IDm&SWUFioZ`D5T_zwg-&`uAEXt zIuF|zUP$PtK`4v^Rw1f!3kQ#IH{t$#MtJK=PZO4#n+O$)_O#>a)F^)R3fx6K^^djW z2p{@rb@i>Jw^uFBGBN(Bz{!r{!-)!@Dt1m#5ym5|D3r((%yT9GmevX{usb*;(QwqD zw8U#&V_FK)Sesqao9Ji`>co;eE~=HjLE^~Vx!4LH=FMWx^EtDSlU=JkB@iLW=VE)hhHxjyZfr|F6O(F9(;IgBQCOg z48B--fTl*R%BaA3+sx%E@vD#WTUX(Qdo5H5P2LW{72ozP@Lhi;t>}VCs?mq9udJvo z-!#+zoSXLqEH(i58BNb|ULbzBG>I8Ff6?OMZbk!79qu!Buovv$jYh?u-*S!9Yf6|$ zOL&Jjr%(VBMPDiS&HX;{2um^Zw#vk?hHO9o*a8!w9Iv6$q*|gME8VSqJX`nwGh*>! zp&7G=7GVS-lC?*4Q_99f8;{tIClUU;ZpITLOTI|AihT)azL!FjJd_G)hixRnDrVAyoX0 zJxAf*e0BU51OWrKx+R^awdOL`83%mH%PaemvVovd9{vDL3PROEjtdYXtA)sTej`Z4 zDssURevJf}9ndhleCqub7OVS#9S4Nyx<0s!qVX`U z;|0MbgYLbYn^QZki_}xVK17h*T2G-vS&12bk(u1Q^B>*(@A@+c^A`+Vx$RV`)WcN-z_mgu_NIY?BA9m4nVauV5&PTJO%9j)g&v_OiLz$?T> z)ru~}WST7w*l~7UG1us3Of(}+!n-#v5z_50uad=KC)VhUxb>NKQ(bH1l4Z}(o+eSK zk&1a>MfPX)%EdG1rv7hm>KbLQW^;@=`}Wz+hLckQskz_I=E)jIqo`vRS!$cqNs{>s z-$j|@LK2>0V6k}wyVYsEB|WPU6%G}2uWtGW(_=##vB^g^fM#RBZ2Z?Q7%z6SmH0_D zhNP!IHzX}^zTV_Ap~KOA`fz5o_K!BHo_n6@*YE-0J7R2lYZS)d6nme$?`kPlmT#)k zo^jW=12l3S!jUg!!O>=$8!T6U8S+R!j@>i#Es)}zIUyzU?k;20NgF(7<<4_gG^})R zLn`%x-5AA`&2dA9J3kYl$X<2!)MyFul(rwipUwcD6!XP^2eyquT3d@x+}VE^KsdlZ zHOZIK6zRPBTC!VXKN~UMa-~u5?3kY$`LG2_0Q}Fd=+nL-EebmPVJQ3vi2ut<2}jI< zUR1Z9{%%-)0-sf$q0D*ydAO}3gd8wTU#2Sw-z;z=5qp06SP|i-Q2w+tClv>9vzW@q z5PKyl*99=?wXiOr@m~yANj2)&=ZB5?D9Vt1^?%HWF3z;ZH+f6)lN)MXbJ*mW8|I(T)#6D1^P)F?dcYizE0gM~N~8_G-#j!)o%L zRf#9>JXyTamTE0$!la@Ushy<=HTyH;J{MuLPdiu`UWTw@_$3M$>s#+lMR{_^Ma0_H z?-+h_i+z^@V;GU^KjUG`<_0{!ny8XVyqPi6(0GMYB$c?mf1D09;9A@_f-F{df%*%RgN9``>)>Ty1Z@X znPXypTqMOUwqA4Uu2o@ec`SqW$ z7GsfLUkh20F_T?W;{9EH{$9b6k2LnbBNiA(&ij?w9sFuIH4UKH%td_T_=8B6CO`}= zZ3iLbLQBaL#156bIZyPRBVGNI8)Wf(Kzg1CvRf+WoFXL0!;XxX6>rl;gb64qCQH5n zZ6#*%;$O2GVwOEjeOzn0!L_cz`jj!v&WA{E=TN5-^m6jH&srY-(cSHOZa04D`Grib zQ}{NnEU@B+x*&;L&w|VOi%X=!Yvkm4q)R1JRyB7c{+XH2LUtSOBuLREyUr2)H9i6d zb(kB@3&Fq=zp&eNIz+SXq9v5ViCXbkf&CB2#~FE#iy%wu!y-f{3^y-}rq~|!JY=#t z(2+j7OIqxs80JPA%h-PK-c9-3W#B6TJS{a0{q&i%7uxH>W&yB7yNrJ3hRN9;Le#4V z2ijb2z%@-GpXLU3gliva9wC)cBC3|h3$V17;8!*q zC^gRF2#n#FVPG$E2dtI)9508f=%U(G1&cSF`(9!-HY{q0qZ145_;sFl-w+pzwAxKr zBz^r!z5{%e!<$-JbF9SmE9g+&GEmUc0wE;aW<6)d$_8BVTE%OFAM0{ z#bzE5%@T-%Ro2jFGOvLTp6a~N_m z?+qs2%q6?xf`kIBU)31A)vBc@#azW=sutS&EUYDU@rRM59S2DNqHKXYut3y_bRWTH zFsPKkDnD#(BLS;R<3rXuYGjX#`w8FJ(8FR%CDEtKyv0oPcVQYOk=5#+*U+O-GQiac zHOkU36<}l}@l-NtVNPJVD0mjFd=;!qtI9*JFGn88h%&Wz|8l68DKxd#1@j#x?@N^1?l8J+s&yeiFG;bKFUb;VA}`8@#vx_e{vfpKXS=3b8&5R%$eWfo3=Lq8tSi) z$>9Zxa#`6(IYx*nvvHD(yoEVV@h#-;H844R}_J%jWY>C--MroNE6 zjoam5X;s4VLLkqb_0&n|ccrFLtDO^EEnhe{BM{y#)7yRI41Z8U;yh#J*gYt?jC1sB ztmTyDu{2pdO=Ags&%WJhzmK>0j+@3;lzyk+a85F_x4z|HlJ%UWt^*U6BRJRPDB+M0 zVntzO%0a__Fys>z=KL=$tO2u`3s?A1)Z0hHPELqBeytutZu=JP>-E6db^2;yi6+x^ zxsOR6ovFzKQ=Bi2r{-I^%eXC_|9M|sUie4I(tOj#lUEPGoDWFP4SW$^~mywEFn-)T)JC}V_0S?9xu8997 z(m&`yGXi+1mSF19QsfqHXhWqc1f<}O`m7-&9kKXc+zA%$wt_ws2`AuT5xR9B|CvhR zvnKE_hh=jg4Bxj5nlmtJr<(s{`~(bo!a{>Fx1-JRg707cW@6BMHsk3{%7r$BM^L#% z67Cq~L}EpNr>2oJXRmV#9F$q2KL}_DqRMT{cksyJ_DrIYm~=Y1WR)U`ZxY#gDI3DE z#6mNmPr9I(zVSe|UUd?lN7UIc&P*;z%}Hc2tRoY=7btEr!rQ1Dw@U>JDtXQl8V!a1 zHo^6@->#qc4#!kCY{dDkIArBOVU#Y~Gd_i7jZ7gc=E#+2(NRoS9!~q&#ntLC2qqr z`(FS=@7D1x-0V^|#WTju{<7ak#q5$-0Zv{Fm)wBG=W*}geqjnn6#c2d9}^ZloQQWX zbPVYK2E!K&dlE#ozKa2{iK|^cJ9N(FqaC{<-)*2vV&JNw61Vhy^oH#9qcy9vfybdC z1W&v>F!VshG#(1R{;diEgjlcfDTk~io|a_u6nqL%_x7b(aosghm-t5uXsi9AIF z@CgjnepMP9`03~_In(m*sFWhX0T4af;I=zUDa#bUpU;jC8?p|#;{2q1rSui7X8ZEITG`dXU$^hp%1>$HZ^l8i3ij}DPZYT3?!Dc>2dJ>_x{ zWrkeV%HiGjf<_sW%8_W1esb1K<+|`tp6;d>i=%q&iFcw7_IX1z-|9}Ij&2YtNMN}Q zqg0@X1Vob$281~yc~gt^60}juTL?OkDiz-=k*uk1_DHrfD4^tW!WW`V_6IW`FN4m3 zekSq46$!kG+c6;nyY28LDI$C!;A46_?rqQ>I35}Wn~hu2rSaQ5Y_J_qPZ#&fVTv_F2Awv z3_>F{gMP0=5hE2OAZD!xrvld}QDzJ2NV1u)E_()m=tqFs?d`j)Xq$A`-1oov&(Mqc zDHM_21mwQWLWm0bKe@gir1`|xms(Zz-zyO6f6w2@USNJFRzhRaYxFperp<5uQbWo# zgKheKEU(%POW~XV>p2mfSPmU8!8W54*FLd%79{9o<6v@yjGlJ26kud%?-2af^<+kR zX5Z&-&2eG$u>H>DW)KnnZS?l-Tja2~$j4tD%=N??Nz7T1DkMMLikp(ohvLj%_NKf< zuAncy32WW^uZ))TLmJD>_sGPiM2X{XML&F^w-uO{6yJ5k(rYQCvZ0K1jHgrkv*R^8 zzP^TUC%8OUaE?AS2hl1(T z;WzSpVYV8%k#}yMMB@S0>x^D!%b|s}Y$$JY=4qDO+A!pV&V7`MbzR9)RDa^GpdJkc zD-GwxI~)m~`&>kBb7lU~s)IVi@1&#vr|nOEA}|Jg ziI^#T-3@+M7)Z&W2_P=#lKM#{_&PS+$n{58_MY0uss+w@hRh-0!B6t|Y(TFp=nk)} z)SxX!A`Upa*F`FA^|UKX?yS&u&r$63z5w!bAJTJMe#H2D7p@iAZob1!;UK+cad!0i6YHeR|> zw-s=CtlrOzl9DE$VZ#dv84jJb+S9|a=xyP6u5dih@P+Vvc+N&4r0{u99Sq3aNy?>+ z;zZi6d%vVeGN>ycX@OT`aF%H#IJM_xkUU0A*weEejH44|)i3NG)okx7eVW&XI4TQG zLo!a;rS&;z21gu^UBHt+@TBZdI5EnyG^x^vDy$iE92Ag=e-2D#^7zSSO=7Q?JJLS#j5Q|juJkD3C zGnn|8r-x$)V5Xp`0pi%aKZaHp-zYzT=XmUv*b9Db1ASVObuY-@rl>3EU$+E=o;)DL zS@_e6jVq7agKUYZ3jD5xVdhv=nx`oamOGrid1cxM+$cj}W*4to$h{OFz#z0&;$c>? z4QAc1zcq&LYE}5*Lh<*uUF3Ltagbj0{B~;*41QRL^(%{`zTt%AKDFdNC4@jfoGe+j zKD;8z%go!usANN0!IoRl4Dku18Ru~$1C=mPN*Uu1BT7%Snw-gheyH*B=Cv&v+m(7soY#gZ6}k*CZ&XYvqoL5j|6>y%>e_2u#8*(1%; zm`J^4Or?3t8CeHx;hzj~kqPDv_s7Y-{V(>0>b5fE@9C)~h~E$T($qO%Y-$Ti^V;p= z=C}?e_Nw4(Nx*w}c4jdO|M!U#{L#eQLnMN*RfAB3{nwGKYDV+!F*_orD;%63 zj?;HZ119FLv+AA34n5KUrm>YL(;gtXiF2Y!Oa&v_J?Iht5W7bDxxEFpzED#Aaw-K7 zO#ysQE$Ou{FA9ZP@DZfr6O!$_OW-HMDEZn)==;uq(WFYmqKfZ6Hs-}o+R)mgO zq{8Jy$rST@I$XQE^qWG-5E zG05Cva}re5(C-*D`kh1^Gj{kN;0TW|08 zgEM%66xAtaCG&HFF{Up?Gil+PpA={vjN19AMqkLWnr1`dyEsp1oi?cCm{am7+K}o_ zmSk6~*|faf?!Y{+iL1Wj{6NphF3!4*=zOTKt0!ZL!>#oxITsVh%S`F1r{1{seqv?2 zc5U?rq2y@BKwf8Er#Ia$AB6|-C+JQf=pFlu8o(;5-a3aDJ3dWo+qoF%_KB7;vqIJ5 zGbO%*QtYG|8nUP7_wfyUk_eS zDmC2f+J_780Ue#<4`9xFaK0gkZxTm@gL2#7_F;X1P6By$Vi6&?`-(zie~IyQ&QUR1 zAQ{t%wzid%*Y-_q-5+q~-n$2$$jYi@63=39^f18BwI7oqj{!GFbJI#jRc4<(^f*D% zJkoxoR>v3{*FAlFW zi=)iy#d=oF9*M;$*3$7?@gkw=oXwB0N~373*Hv54je94(Nm|NoMpl>w+8%K!Sx}Vy z5Z7wum}K6I@57lW{LSZ_ex$sqQxioK1K=+YFF=YI?CwiJepCtEQ-K>{*TG%Cna65xbh0sjFMuariZop} z3DG|FS;GD5Rpl!|@c#9C8iE4_i^`li?2XN6hT`z>aQOvQD>NNgQ93e&dr~ATWRU}; z#eUyH{w#xURy$$ySS{af7HH-hl3xr2-dfTDEe@_T9{|hyo&AHN3Ai<^DzP9tm>Tv~ z)}Rk0Od*4!@d;A9-hicyWE~Sw=$YC$t%>(FU0U66O5C zW$XIerMuWU`e?E}$5$9h7gGo4cwiJ$PTJRkmp6+s0Ye-R3B(LW|AT}!+Ot$?(W$iW zM1@X&$mnhss*BL&W|zFcfW1c?fO>iT!P#d5#j!xj;sy6^$KPYIeE#91kHOR#SnOF3 zc#J%WNHiDB`|DOyN0UoG=JGM*&8SV;~5>_TCwG)+&yC=?#M_{>X^ zFRpTt4ZM@i)XJ_xqsu+R{i9xvbAs))!jTuLGnxYa;RcDY!gcp-RiVg!FylQK2L%2k zrR&**wgUQM`wD$%>I6a5d5PC6S4y4685rP?K{*s~`r&|q>&XwwT8kcHSfut{+$7c= zYM*Wot8x6<=7mW%Bq<(^#@-hGh>&!sB?^8ay*iVVl-|u3@`bHVV3ot_L!BOto~mmi zI(?a`+iNv@-9e^cOH#kw|H$*Ig1-JJ#2sK-!CQ^@TRA+Gy@|b=ryYCtKV0@m;W>J| zVtD`$w@L+oCGRaMgkH#WxH3yWGJk+MM-9ΝTvK+yE1&XHz`YHthniw>FjH07F^P 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Π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

-wwC`Zj%52 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%0AV1l?--1cn%xmxk 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

bTTg3sarrv0>eYawBo=o8tZJLG;Z5he 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 XR#}gv+Z#gw^pC#Ib?q`uOvL{IyBilS literal 328536 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 z zkB>5gm(81v(+2bI1)o?0DL#BfFrd@mP&$$kHQdy| z0z>J&(0%-EXoX9}8+O z8_{>O@bOD4&&VP0>#CJt2nVVd;z}@{Bn#8%isF;Q)WPk>&f*{KuyxS%r~Hy?(woWl z;XjXl7OBrKE@kw+umg}+Oh5YG5Kq6T{c<@c`mhF|_%nE>9(2YTR7r4Y9$?NAD}XFJ zT0y4!gTXoCM#vlz+vt+vbM)MVqdVf`a~bFll3j*$M`+J<`|$0FxHsqSv?KW@KTp8a z3HSzcjh-A-M*l1L_8(-MIdV=PzipKtH@FBApw(gzACgzc*4mN#R*wg+%F?p_uSFej zoT^{!C;a!lZ7;Q-U{|x7rO5#olM`bIvTT<6gBhfDV zJbqojof3dO^^xkAZC7l~6pf9Rk#@8RO=HucKmVcL)C=cyD-51LZ8Q#g1{lF1N z_qtP$W7xHH4w(Ajn1%YXEbC1*;k-0$`eXdp;Q5SVdL(86Js0kXkx2-2MdIQJOlYc)6A&7S87j+Q=n?{K2>7m#AT#g|f5YkP|E z@w<1In~uLaZ+4|U;gqBQQ#fw>&%9}Vg)M?l^+KU`1dAu`qOnw!(TdS8bbL+GBxjvj zUST`lI=vK6+AXtd2#m94dv*hpW`>&_B{5=WOTG2g6I)NAtxM&h{;f;_K}5faHss3O zc3JtzoV)JYAE}^Iek%vplXt@+`sMD+sQ*EvU1oTf36;4FBh(W;0gP6%@i0X69LQ<} z9?p&h@oF>XK(egJJKiYJaQJQ*Y%e!ba_3#^(5H<-;#Mbq^t}Wf~fGkpHn7@{)uAASMZQ zK*@C0i;CWm!1>$A)#()ChaA=W^J4?Gr)%6E$m>yLE?v6xf7jH6)>0~D(F(;el?X-laK^`_YHs+}y*L_`A@ zlTPOXyl~{qLEHfWevMHr@XU3?Y5nSmZbbds|5NHGRyd9rOerdfSZ8+Mq)Id!o>o78 z&v1%VIVrTfpFUK7>|AjC60^=XmgdG?Fgd!H9&Z#k6?}#&`((Rp`{}%e9tk;&%;=&! z^ZU}xj965MCHps_0hCdw0NP~$9sg?qrA!CxDw8Mj{dSasyY3!Em-Bzg{}sn!u<&3} zYheHgBl@!O>U_jMkTnM*)Lq;C(fS;8Nos!C;z*x+(W)EyIQZN+XbKshLKN>;6TEBv zcx8L>r_hax)RG2KF3L`a?1fS&dF7rWw3$NOpSrWWO%P7(sf8aMz)Lz_01GZf|`D)ncbaJkrEbi z3~1jdI&p|rR-wfv@7cI8{IOoXV#asTXLXAcWyj|5wRM&oM|0?hUX(iB7v9ZY)b4nhPXaA^N!iO^EJx+K9c;LQJuYRXe%#tdv+TRpk#Y9F_87DdMm+?) zO5Hi4!e;#Ak+|!9(I`0Rzu{YtWHBbWM=mo*KeS!C5Mf}WMFU+rpXyjjl56v{0_J^9 z|B5bNr_xdawTKc93>)WZ>;&y4EBeHS+ebWGF3t|Y#%GV%e!qXI4bs&DBF3`_1^HHc zqCPj@WbS)U9>xucwD1(-TJ0Idtxqq_uyg_Y=?<^(E^}^*S{q0UCOpZ3Gig$R z@TL8U^w}v$qG;oxB7{GEs_dfV7p>%59r1aN$fIm&*VjdFB#Xs;EU`Mp?UCiZ)EES81llFhwyt&-W)TD;SuABdivdP96CCojsB=E+XW0ifogU9pLi5@wx0!}!_uUvHw` zAT#MM^!=e5f`y5PbbgGJ-2xK$1uiAc^;$T|Ja>slO54e*rKN>t@f-od2!t6l^}&5@ zl9+^;(=VqdvTzIT-+Cn~oYGScXB@8ePf*7u0)PJga4Rd$w9epg+pTOX)TcBAdqwyY zu;`&QqBQ&90qwVswzJyd3Z{MF$`aLqE2G79$~oKu)KoKwIx>Z0Iwi3DE0^(xqIl`F zspA2Z?K|d&WZkbx81mLkgS@3%(5WSbs)1G~znhv_l?q(+r^3;)jO|LW}SL zd_+MEQVjU**B|&ygfE2wmau>y9bsDntU;ksy+P{PX;Xh9&*7B4P2pBOz~v~=x<2XaPFxb#89h7K41}- znnTFt@OU!y>ZduIY}H!zsRrq7OLmNX{h%B$i8O&be`@sCad<@`Dhym;hP4oL4;r6T ze2@At-cV-RM*(yEN!ZGdRt-t7XdRjWNPuDXI=O!BoST`AKKOFZVrKju86|QqK);kN z06JkSPI9tG=;)KQCDJMZLw&JzLf4ae(kJjN`Ytq(%=$(I&R{Vaa=fRneg}z_6$Edu zD;O4?51o%D{6DBT0hWwQL_XK0Y0G3CTy+Iq!dDO5i+{}zp(J?44BJcahB`%IrBl7I zur5{39DaE!y=R7yZ^iD&kCT; zPMlm9Txb2!WFi+4TKb7+-74+62ph5VOgdV97~yJTbS@s@`a1EBFde7~UC7Bv%lawg zUQb|e=`*2dO9fTV{6Jy;W%s+!7IDxB0xO%TOP^Lr*U5uZT*;zehP4jr|5A-TlJZhI zc^FA&v-Ue3#uLNF;kSMSUh0z2sP3x$sMfOU zZG%`j;%+&q?8t{HDdair)Qc8r_d{G7b(qrI@xXOT;zYB;;cBBM_e5OZfscO+2>v9o5XOAu|oDh z35Eu z|5+!`uqKLt>f-LN&y>$DwvVIj?wu!JdRwkZWCvSFo4MBV;quzEtr`#p3ml`wudHiI zoaj!{gOut_4i=!{8^c3|%rtRa?lT))IWu~hWOveto=KJHy-LH$`2M#kj0jNoOK}6HP(rIj5NpAs^{~0W9kIH-+ z=#&R0s15$UtAw4y=3lpv0xEu9`6cb#eArJfWuRf%F*XxjpjqRwG?;_sH`P!lY+x|d zEl}aV9}0t%c5oRJ6RodE4v~j(z|&8K)om%9YYob4wd{!9u7lg-ZI$#=D~jKo&ef+ z0$B^`7Fa@&dDbYS3&SaPprT4tde*UTDvz#Jj~a(ax+QusVFJ>ani}(T%mkco>Z=RW zY4VX4^DmS@X)N&}{uT^HtqV+UyV~8L4xY29T4(KoAimd0DD@FQY-F-X`#=WTv`g}3 zU>~9akfrtHKow3u-&7icpEtuUKx(SE7gleoM^p*vqX>P1b=(E!7u0UU!TX@|v^jKU zgp#l4((7@SKS?ubn;HD@^@ES}p14q06P2Ei{`vhvwL`p4H@X(S!S*W&tNYXT3Qad} z#Y!53r6H=D@e&RY`gu(;dqnB3UBeiU*h@_8Wse~|4*UCt7k?}|rvbc5r`XwPg}K|O zr}67Txw1I!;rBdP!(ZsUX;dqFgL`Zki(Pi#=y~KFyk-}Xz34Z7-OA@%anV>CKmyd| zA$bWo4Uc=M4hScsUPf>6S^)Zxwnqu8-;?18HqL*^QuGyMdR9t~7(6K3Pb?1PiUcqp z4gy5QUFrc5%+f7*?ZgISLHfw!R$UdX9zx;NT;q1$K~!$ z=>0oSoK&7^AwG!pBJRx7Xm@V5geNwf+o6|4ACSB&k0BWb+3hY^tNCW}syuzdN-CGX zwVqicum4Tjexa7hbhokCWLM`)kx5rEdr5t4i^di0^I!1u5wCyTy-;LQY%SFj*>Z8$M@93-2e;EklzFYL9tO9at^cFp{3kLho#_@ACr%MZVvOh|>L=I`50n z%2F|T>K~JIZ4Jpduh+07$(Cgjb~Md(?5+KMbuUp*I>HIigg?G_6dlG5xP;?l2_VB+ zdAdKQMeZnARqm*uT>ZmI26Ozr*B3v0S9sI=#Z_tCb(!}(r!V~t)6?HUR^eV#h7g@v z{8`sx!&+>zd5K|VaU59tOFJi-5Eq_>t(0y;nO87GeTFtOXi5H^c7qDrrZAe=0fFYc zVmk@(FA9;da?E=~>I}c((EWRS#8rmZ(S%c4BsnR%w548<-%Q&E>84~dC-LBvNs_rpC|D?RI%J%)E{c8BP`Ov~+w zc;M)*8%klI<_w5-dvu{BA>JnO3U1P88Cp1XSq_?-wq*ozGG1;O(lsnyUN!Y(%_%Hb z5P8Bql*O%Edw@fD?}L*RUDf(bk}*xYq*orVFUy|_GO(e$knPqi8?m}}^3RJE*iq#h z!a5O7`08qFJGBZ%lRY)T*Te7^1u9_;8A~$I?`J29UyNw9 z5h-qqe(FMpxQh4V$ulYKoi*M%{iKMe683-Yhj$mwjGdQ69Z75C_Z6kIF% z_gh7i*1RH5asjozcQNhqm~5W>!3`%K3~AYQf`87o{rEq$}Sz;e^CTqW&)j4I|t5}1zbvJO_F)qMYv_UH6k1ccGTjxe()5D?xvlsZbV)p2;^*}&l z1ZcVP{53EqqP&FMUb4HigxK$SJ=-6qb*VAOt6$$)N+~a}cR&*%)pqKBq*B^(Z z3Pl%sU$oJT*69vH=FqXqIl6tky;O$Yd3kM5)A_}QDsH9LR6WY=qt2q%DG2>)Z3e%f z4BQw9zidG~$+<;(JN9{y5Vir2RSCa-iz8Y1!9yO5o8_IZ$B#rY*t5UU@yko7_*)tS zrSS=%(*7(mStvT6!%jpo%j(>epf@v3Z$(xC`}1paR@s@i-_wHfaB#M$JK87 z$v~kRInc6Odu{x}4}XWwsZh6n*5^LB#97K>T_gDV1_Ox=)LS>UQW$W!!+W=g99r#s zi!j&%RA3AX9d@X3?N7YSIE>4I6{&=ncvwYjhA;yz&U#S9cB~YjiXUE(XhPO6od8A| z1>r`IWJ&dsGT=_1A5s5phu;_F_dgg4;l`r%i1Lnwv=^Q|tTdN7qOn?!vtH`S+e|UIW!KN{PJz?JNaEK&8+0r= z;^lc1n=D#l_{^4+FRNpas`TE~)YigTDtmi`Uo1)rqxs%&&7cJ-iUvY3UJMqTp;|oj zG`uGB<8xVC8-9@w^5X1k^W+rnyH=ZJ*BPSy2ZjNpAyLC>jGnJC6|1UDt%V#WqP01 z+T^@+INENAqLc>-2}!mDeTdn#7CJT`Fltxb-4l54KhA=@l_~ln-ES4PFyNjU=c7DF zeQ|p~xn2Z^xhP=nDXrN85>XIy*hFx=GbD?Bv&glJpVHql}Tsm z6J<&CinUZPI1x>l^G^tz!#jfSX83}Z`jp7TZaKY8Ftg4jnl$;f>D8{pBTX9FmOw;s z46RJo!WwlzBs%MI5W#o?MXqW}RSew7gt6t+ajCmdhuC0XYv9$pYmvC;h|%}op)xvX z3{t0m=h8T;`0C^`%q|84Gm9bZt?3ofyY{b;y@&f-6WB%A3XJ9gRurFKF{f9AyGISj_2Co9rh8 z?w(QsvD4X41xKb#N2ujw`3a6IOuSju$DW4SN??d8#@E$&(W|5S0K@F^h>sP3&dAfL z3qV-9iye?}7eNO|_gIJzLcips_A95SyMk!bpLe0{@zZo5rdkQ%p+Q=f`gwF{nOc|$ z9pTmEc1xxsVc{=3D-Q`2e#!Egw*{+ItH-DQN~vUsmAFT~{nn~< zur+Zh&t)(!XpJpEbgp=>R)CT6P4ztf_JDnW>|%dpJNd`-B<}{@^FYH@ANN&hBo^zO z`6QGS^!ZRpRSpO*HkSSIZJd~ja$fXEMks2Y1{50ow!stVQWd4+-D$j*oxH2OIBXIF zTa`P0-{@we9<-Z50nDmKXd#wS&rDhHzv0nz zv)sdd2a}xux)ykAJx>=;`ssbGpwDtf_^a#|o|i$KC)<1yZlT)5VivISNC|#PU^d~l zLEHuNv6YVunDpHEeCSO)1HB=W#lm=msT?Fsw=AW?c}K)2wWy*eIWR46$A(9qrk2X5f0lWySPhT)}0QX7KrJ}>x14D;A3B57SRU2$Ta zcjO1Qqv>VHBAgPwmFT?a2-2N|7_Q&(%g(%0ODvJ1dlrq5G)%uD1=wVgMl}^rstqZQ zBhNW!KI_8#)>5(jaLIIFR>gAq#GmrBcPX`A*pj)}PPvPV=}Ia5F<}~D+ex@x12dbO zo6+jqIRr0Z7TZJd3?&0D0*z~r7G?Qu_YT`^%icI}g$O`kO7 z_&STlntX)=6ME$KGnAzqFPc>Paj7321Pt{ts0L|c0mBC#e4If)5qNd67%IW8cE}Pp z`BjIV*|LUj;CBN)jpa8ww&ArX#ShUv+E4YJ+wcnpW68Ioo{<1tRj+^Q6E=qWfYi$7 zj-lt=I*tCFlFboUT)tFW4aF`a%1Hl}W!ASMM1KljdKvQz!a<%pK%xnj9Fev7NL64> zSR{^^J=^yW>k>FrdiNa?1t>IA_}bz9dmWID>vJR5EwHS1$TxBEhdr^Z>kFdu`KfbM zaI^<@V6gDzCoE-Em-&~*TlgwH`qiBS>+R+6)!BvmAqp%Z8JwC&v--`v>g``1-8o<2?Jla~ zdOBO_e1c1CPIwcWDsP*u@~;yOP2W@;>OI5yy{|V%LBgoy{s`#LrnjJ*Iy7tG3(2TjJf*XW_1YSq5&RJI4hO@C9Q zd+o-?BZt+%a`LyV>XAP&-q)}zc3;MwK2EO{jKxyWS~Z5U3ew}k^%b|q>mEC=&s8Wz33o5?9h=1V7qvFMuy+)8LRycg2`PV z5wrH8Ghu^%Ah37-crcPIuy8Ipg$@GJP>Fj7Fo{HZdyT4H<*lN)SG{soXlFmbwSmg{ zPln;_ng>hWSb=3D=&X~#MxgBp(>ia~l$WtuoDXNLUCwC5-hcF;nf`WLOk;C?fx;8D zVy@J^>zcQQN~@fr*nK9Wv*f?f9qIZZbf=j-HZA{gV71mTV*L4Z?2ED=m#ot-veMxS z(^-eBe2Vex{fesIqd&gK#W{jKQ zCLi%)+$O@-kHEr#tPWx8?^!`MmGJ|j z5%C!a)b8<-6kk#F<)8{?FOXkXa*QqDjokl5Aak=I)ho+XHuD@E$T|1=_v;7U>r+0E zy<2Y!;lV+DJS=%MA)RJy6f9@))px@mfI_#7qO}6=kUywq8EhBVKO2Okt=IK$F3Tj-Rt@ z_~G%+TxxO{q_;EM6o!u@o;o*cRNugp6dIU9*LT*>kA(fpiLm87#$YR~0_vz`t;vn3 zEwg0zbdfIc*7mtxbPNC=9hlMaH?0wmcBEqy`W{|MYnE<3t~Jjav-V&;=^QV-$JINN zMNm?l*h*S>E>6CZN}J7J*nIV~ca|F3J6lWFn}4S@_t}IR_np1X&+oqe_3s@r4t)NS z>DQGA*2lwH_UybN9*EFGS1r-Z>fAPFYegi0IX{^^_RO)gW-sal~dU zopMjJJ+X!#8eu6E5Q^;k8o#nzR_0C!9rW7LwGP2>=qkga>bmHifuV*5hi)WfBn^-n z5$P8Rm62{lIwb@K1nE{lr9(hzBt&B96cA~UMq0Y#e%J30{s8rP&K&mHd+ilEuO!c? z{;fAnI@6o)m;9SEKU@rO0>QKgLe~Y#bGzV-xm@;!<|dxK_xB~)AM|aD9WoCt=U!eP z(;j6ua?6p}UGnHY7v26Z(*+gK1&r5-IhvlelZtE|A>Nk|<8)2zCOWt9PzYW2*d}aD4Q>W|5gF_G3xz4v~TUf%Qz?dr6O-PKEX zCdl4V;G^gYEQNp)NP|#(5bESr7KT8RfPYwe-Wiqivi`wEhJD^az+Y(h3d)l2$wLyb zOAa5&z-*bk7axw8q853H1uL!%^gKmFoWwx|-WUllJa7BCZb#65Wtz7IH$)gm+j6_h9 z5JRJV^FLCQkI{-C32Z7nq?Vi`cdnWlqN#JqV)45l^BLY&`nQjs=*WtT_i*`ebt=5W zjz1MaUCg%DQ}&%F?NUKP0bVyX5{4cv4sbkXb-8JV$%sX6Eag;4ia_{kF68+_rAe&P zW2NT3o0FuRF#xLO7H3Ddw19ZMr3IwuEJh*|e>B67Y@~oeT4$s*ESCj%d2!Dy38r!v zcL`OUjH5D&_&>|zuc{6-cC7is@(mJ|R zK@5`VYU1I!6IhZ?i(2Cyh#2eaRO^u!Isczv8(haI+MapzOJaL9q~Tq$nlY)HHC!}s z*;Tv#$s2e76CpGcRfnABP{=#+H(qpcS>O0p|K@&SG#GDX&{Z(g zMg!VCh_;Ri<(X@_dzSN4o*Vv{o5?z#fE9*blzc=@U$Uu9Db%uN5%q+|Q_C8PwqNbR z2_yH4l%BT0Fjf$Li7i#b{iN0E0Sa^(lgYOm7v-wCpb|lG94vn3@$Z_hiUYqX2bW-( z4&F={Ym}Fp`7aP^!_~m1Wu(bTiv#2UF8|(oCu90T<12C#M4fU-1JwVX)SeTOGS}Ph z3O~MJ_*<@ol*&sLdtdf~lwfG?^WhP(Vm(}@QLd%PCx>}{Y>~NbPC!-u?-(uhq3Y{{6}RUiW7Dqn6*mTRMzA z)Vzcda6)>Y=fr@-C6vXXvUvcW`W|gQi+eF=Qpdano_s3Uehr~|FilW?xmFk9R>E*R8!aUxk$R}(ng-MDhKi|A%m7oo}^xUT5Axe`Toxg20?-3Dp zXW_l-S1}SK?R&_g^+<|b0e=f>s6Ej1vd?iIZAdKpfGSt7?w z%VwE=DiUF--*stBk{;rWUh>I0^a*6ElJIB}9uXSNV}QVL11LDPc6aMCg6LP1?uv-R z^7G>CD+_Qxe08rmYc#1U-S_ql`JLk;<-yPQ(m6~O%D#Nl;f&={kI3^uztzPdzPrC> z0|W>#D5!t)vhCIvCbbsFxD%L8nay_IO}OcDU2S8J`KF(r-%EjyniHPT9%&vp@xDfX zrvh;O_v`%Yh)It6Esm4F>VO{kvTxFs<VlPtN=3G$jj`T|7QR5vM6 zlbK|8hjG}Fsp5@Aa53PTk<`5*_ME@(gtSkaoAg$inZD%ky7~(c<1gNQCk>k~C7k$l z@cRRSEM#!UDy{j4$m7(U7|%)i?y7>q*}hw!l7!v(>l=yS!pAAQwA z`^|0T&i_yI1LWC3T<{Ih5XVC@|3f4R+rlKM$`{9hZa*xIr#?la%-P_}=Sl5r59$wYrv~7T?__$GKTDLp zC@F8D^OmXXR2f$(4RE#jlq`IURpIvnwQGrdr^0?2^$YYJkL=hD^3EW}9Md0qcG2^$ zFUoPpCGj1FB@p*Wr}JT_!ODnk>CY9Yf{r0Q))A7)b)!6q4}w!qqKA=ED48KvVw}-k zVKj)Atz-sydY`AC%(FW#kM&!vx8$vEW(26)KnY}+eYnb>AnhJ!Yq4F!pY>J{21n3x zcgdTm5Hb|J$qM{&WBmznjZ-4=vY8G2pFn11pj4*&T3Vs ztpSp)(^tZZGLq2N)Uo8PX1jEGP{e>prin zQ0N=AmqO{sgwUtiiB}W0ev;|%;aA6IZj_hMyuY=Dhj%@w=^N%iK2U4akNvsFI)dbM zohI|^ga4il_RWA!=$Ir(--R(E48^l2vgE?U`ydkXZJd4Cz1PU@(c=n5ohsTSIYzw+ z!GA~t5=ikuj!&oIJGa)jEa72vDRRB^s0cKQDYKiP?3py|7855OjQxof@SDo5P_zqh zawm{{bmt+o5nO{j6g>A=239vC3EY~Y=ce@%qNBq7T&;a%3v^F9c zMC8(}%Q7a<2vk8mO5FQ3P(;50EJTqUf{y3%c7p}6eKv~R-PCak_bcQJF|o!)(oa@T zcb3wiK$w5T$!4|_LJlK|=v(*vV?y2@nZn-R`dd{^g?xxg z)zcgFUbaxQL*)73X-Mg;@2LNS1e+BkN%QP35+yt2rM6RK{h5BysmChoJxbpyTK#)i z_#3r0zj(uGV*f3S`{BNAeQ7 zF%$H*#&7>4W)W#5$M?azdI6nAMk+@L#Fn+3?D!dQxbsdgY{~lfl@k=q?)34)2WCy7 z!Tb`#NXl2ALw|wiYgF}4{}zGVpv9w%gDYtCC;%Bk)+)ebB63{104pDTeM2SDj^P1k zP}qV6HQ&G*>G$L-0iLrX;aeoWk~-fZ433PyejfW=;c-2Zkuoge<(+gbw^#_>c&6wu z6%vwN2rdUuC}8Bahb^<|D@zQZ8KObty2Q!sEnVnTp4S?ix?n-G6ZNH8jcYa&aEthK zKkr}ip4QRF;V#Rmru>Lx!RMMlzBGoa3Na1w{5*KSChYg3zUYiL z!P_uFE)ecn_8<{wXA zl^O~=sY!GZH$g_DCC7f@x-WUp1_Wh8uU~>GnqWQzQ3F`BJP@LM5h0rvg63rFW6_Jf z)jhm$@nDq-C-HfPSSYP(;4D(MoFth@v+-pyN&HI}BRHFk^A$`YI-g{ux9Kb4^E2Enq`3XnITgQ0*=|(!DTk z`?5A6vle+M*y?TV$3HKqJnqn4eDQtskf2Aq+&@81}5k&4K5_NwtyM|me?hVgu@?@ z=K`wF8xZfgC{<(u)!ybSvjTlrr84(huE00ae`^=V3a#4G|2?7K+1V~GVjvro?Wcv( ztp8Aj=_@KIa_YkE5?c1FsIK1<^a|f1P0!g}Xup1C;Pju;*if!hWdqe1fx#Cye0txR z|B7~rq=$GhHSc7KX)D*2h|#bvHZDMNsaXbTX7zE?J;B)4 zuDX&U=!jpJ&1v#ojyo`-f)0bi%_I{PiBS0QK4H`xo<%f!i{M_!_d#gbn3L6U7GocD zTUc3cR@n1A_xOIOP%ekosMZ&O)b8EvKNTAiVc4?l%ed_0#ue;y2)~P=9lyWc#B68A zcrQNS4z-|`yU%XjYD;Xa`<-9s%XU9^@|&2`jI+Iq;}CdF3QJfSxwaAnUE9qQQa5tR zd>OWY1*UPwhEB6 zgka-E|G);t-(A+4PpXoQDK|WlwaQKSF&44xz?kWg%BhI&8wJ&Z4ztsU~ELH#e%8Hcz4-Y5gjn!lVhzTAE z8`yv=|1a)FW*CTn@}c+82^>at@qx; zO^{pO!Wt0xxdyJAsjqiJb;bP>>`vqddDV8s_a2*k-MBO{kNs^deieS10k(;4g1NkR zvo14ouW}&Mkku#p-Ac<5>~|K3M1NMkEh5nwTpTM;4BF`}S`$aek!G8w`k0&cdS@r|%>pQ$GAbNX4M(B{JA*Fx z8xE^~bi&2sK93=VJ?q8;5#%ogAp9%>aTO)usbS#=@TPCI!8Za@=}5!KHrf>K9JE;a z9>8~KGrSS#I9^E0CDQsQ`YY1ipex+mNIB72yX@;t)!pl3_ukVb+=y}+ zP>e5CZrg__bgjO!KC_a>rQR3%L!4Z@Gb@uXMAEhoaVJzirDzp#3gvV6m}#>y1rI|A z0PSh0YD0|%&2h6BeOYl8wxFEIRY6n(8;GVRXV48ps6a~S*?Y(4yL_jyRWKwYi=_r` z@Xn?XfGi$GCfYU!nTT|KP`BREh!fLcL#ceoVMr*)c>Qr(X8sEWRzU%r_``npB!*(o z0EIZ>1*nR*JF{bjWf#7)NAp$Zf1wghPBS~ zEIoheK5nBTiSdF2nOjm+m^zdU+S?0!v$WE85bz7ec?j0{FVJP2{IDAWeGJ1Z zj^d1+X-o`V6k^3owtIjzo=vWCIWDfTK??OD1o6z5ywuv9%TPr0&9&ylNV5*wBT^{8 zhLbG>VT&Zmp?rp==x70Lw$)!R)s^g5e4}j_P<>nQ_y(zN;f3mx1lQ{&I%~?0&k`>p z`Vfq03C6x#yb~q7Itdj<+E2`-jTM79VYHm0oYhQGbV9j;JrUQ19$tT=OY`FzF90X3 zHD$q7n%#+z4vQ99VNaEw-EtHDyW#(9ip5ENb@ zdo8~Wcc&+_Hh;h7@0lmeVa6^t9T{Ci0JNq)gGw-sERS4(FUW9-a*-Er0VJ z;#VOUhx&3p3e&>}{hv&y7V;>lY?4=IZn{b1D`&5cYQXr)Q$u>3IzgN#6zEfn+T}H4 z#?-Kx7Pf#~ku<=}`8Fw_Uo(Cc8{7(J_#I7@>cn(aRlt4cC&mo71E9~EOPcnoDLO?a z18j<9k`qv(2OnHDiCtw8-rSEJP|}WIDB({DJKnSuF>IN#y=KeJFeuO)t#+Spubj5e zzCh#0x24{|`p@On3oib0?|-bd)z$A;9pBsi-S(lDnB=uC49!b~=S5-E=3Zsf=_U!I zm_tHNPB}MgMzHw0R}GvYAnsboJdwFsBP$m3AJsIKE>)H!ijuw=uJ3=e^~8b@tYLxL z&o z2a>qK-O`_KG<+PD<&fc&OX7BPlA3vK0as16|_y7PUmJvEqYB4vhEqQ90k;!K@qTu z?;amrd^`$3uaTxvWOVv4I+&lZJQA;dM1LhVg)O%iN>U{dkYmy>Lw|nv)rM%QnL7HB z=cZ}=2bl@G-_NUSUIRop#c^gN!+ZqeredAI3Uj3pAkW=968fj`nh|F&K z4CZ(ET79D2>F*EYI^V=E&kVkGj*Vtn@EI&t}H z8RVqXv)2wLXvNQ-KbxwW9x+*KJL&0ra}2isp(Vg+U##u7tjJjOYR1tXD|oMO`Gtj6 zaqkrzJLzh+xtD+ue#`rBuxl9#{8-5)E zjb5-NlD|~(@aIiqL})CJ=CvF4EsQ_g;YsL9k}Rri`KXNR|Nf#+v*LNwYeHx2wt3(f z!VG*}5xiU40=AzIqz4>?$m>o-Qv%n$eq7F*Smr-<`2KTCO$AVdZ7NTGaebv-Og};c z@M2-;TvA+;1uVA(z0MK0v#(5#|Jz%OFyLWYuG_tv2`2naZ z>3WcSY^4`)bl;l%*}+#-RgS&N*m-zgg!;0sr)F55IAZ^^&dbc4ipcgU1nH!5p%5 z`-@6i6OKRNcKkXRfd~oG;`&jgIr!5bnW#bUDgN5AG1s-|#WAyfd-O4)y`uRPZk4{{ zo-5B!L{*vgTj}`i6L!wqq)Rs8e#3zwC-}EFG?Hl}Z^qG^_1iEY+sos8K8*`UhYVjj z(W5wB$U}*86~oATucT4XirpNBpq*b^UeMb!fq3RS7e40qp4Y}(C^8@`$aN2eanuXP zI--?pSZslb2p~exTJQ|09~?riIKQMtmD#hQc9@_rGmxVl{CopcUzKO-XBG4DN#g;I zl41Zai;-4)yc#`_$n5S{fH2K5S(B3}7sJt+#)Ug*yn_H*0!8-U@;?f!B$b zeo073d{mFH`a5to>Ew4;+I_`uf`$t)+=v;Ig$>q7kb}ve3$g)iAnqEMLdXz0pY3E` z^$#h6^pWU%Ua^z8Q>;U~ChcC!#d>^jpyggu(jAgA-cljitcs~T{?N}oSGisYnyTiR znz%q=T*#6XLu(Md#IM5Z35&nm6X${^n$mB_;kjh z1KvTY(YW^g_o<|&{4C#U$j@aQVmJiXNr#Y-HV_dNHr?d;@GxJTT6?e^PDqHKQ+anB zAbxNa%5FL)yUw!A4OoM#6Pt(csG4`LeS*blkdd0Qn(k#VDjQ&8Z}JhXx4t^_{>p*O zNg%^3Qj2mTI0ug1&WLAQ@A|$6NFZiZvCH0FQ1pDMrwXbEHv6ZZxPBDxN6|^jI5{_Z z;O6(=fLa{r#0@NQ30H&r^WZ_JoApzWgjVI$4k$?C@0yoMzNanMh5 za@R(FR(4JmM!9F0b+i0mSZtW1NE+GOF*UwoBXn-S*mHBt<)?Nm2{NWbY`Q&&_K8s7 z!-uw2#7M|9Bb=0Oo@a=ShZ%?9)76_dPRzs^KR9f8?$Tfy;=-&H(FH;}EAga4g*W3) zHjwj9I2Q9_r6UuON`LiWIY1fMytXak3r@JWzqolV?&>U^mKHFW@EXwu7C!)tWr#8n zN6cf$>9dh42Xh)Hn4!C4R;29+!FwQvwc3;+kxMG)pBc0jN&7ooF zB{b>(lSt{D@v=J8k(lul?8Ff;f@i)qX?|TOe&v(|ip+yG+QMBTTAC`h`{0Vdk3tK` zu`>d`FMn_d8Wmi{rWdfD5iGALWG0zDCRY=>_Fl!%bQ`jRze#_W&ww*WT`XI7>aJg! zZ0v>TD=*wvF3t{juu+j@ZD^oe5>?d9KR>hC%}xHYT#FES+F=-K4r6Qt(80!i&`hHK zx*TUQVymTj$fcvs6tD&D!>=toQ{(Lkd zAy?tl1{diiYp4v7)rEkC!{22YmL?eBWM>hDqM&L*o9_bU=DRhDV_+_pZ*3rua@)3twYE`dmN;QzAv zEu3a>b%ih|+YCGR718m5glP3A)RiVDj~8Ogh;llcZdY|Fy*IA|DqB{(|x;5C{;&sX|I5&5O9sAFm1mJ>6dIS#vq8Zm@v=6#n z-;0p&QPYV3umxxlW$EP@LHY;6w84LuDKm(0o;XXlxAkmuw{6bX?g2U^JGydYxS4Z- ze^1N&4CLADanj@)W_Bg-aP=Msob+vO+CSR|zg_Fx1hh;N+sU6VyELH@xp7%N`O*2lcfvJ`$tYM&m~vh!9j>_}wNm86n{e8*nLE25Sa66` zc_M3??o-26b2Gt0nVUSQ2n$%&CVMM2CDZnL$SbqUsPj)zdDW)PN+*Um$A$gutvZV~ z+H%p#=9yU+)scp#EC9C<86FKgBi zaHROb(^UQaF(E#R2lzX)z-pQR|DH$!7!dRhyq+hK*1CqkYwLW`?p0&uT|f(Ekt>zW zlL8|Y7!v3UE(hr9z)0g4L6?BSDOkdm%{p*G0_4IJd4M`wz={)iKEnO)K7~MTf&LznKdv_&Jzpci(M1yCH5*YbLA$pTLRn(UC^ktRh+%9= zySN=-fjpAcdl07r(}1&6R9E5W1bCS(iyqMcDg<*$iy{fXcbng@iI2e~S6={L23ptP zw+L|}MB~K-gUy71pZfy)@Icn3$t@6c`*_#sihr!ch($yQmw&Z{AZy><0;|i8K}!sf zy5&I#?12y9bKHCBDw{az;B?Xu_O=j|sDY^|pt;}Z+FRX_hIq0g` zdkLrXV_P&$7}~1MhR$)j#8lKoUkA^fWHD71T;L4klv_DUK)!j+%QI@s0FLC1PP4@5 z2SSu?`ST~RH)GC(VRE6uC|WE|7^7r1pJA7C@i>P1%%PSFS|E=0T@ENcL(t$X7*T2x9)9c8LA*KWR zh6>;~xr>ZD7I02-FyLsyX8Tim+*r>9DAKFx;Il^rD~o6ayU(5Wpt~AJR?v4gdBm^9 znp5@WZ8NgwMQBg^Oxg)WuG01Q*3}lXBl(=aEMfgj8E)YS^&4|FYa1~E@*iIQc;V+0 z$22xm^`!s%AJ>5B%Hjx zPi9;fvMsOl2wE4~?PHA#AjM9cwP_#)&prot!;BI-lR8n;f{@r$k8w{k;5 z{K4WALX5{(=uHya{`W<^M^4Z5W-@*|iaMaj-Fh-o^9CgR`Yz6s zuC~4&0u$V-h_cMTX_KGkv3Q@4`{Cx+M(Cvsrv)YtsO!G44|k{c%MqP+gt8ItV^tEO zSBcr~lGC)0h@>3ZRB%T9t~TXAWNFqs!Yn z41bl2VyRFn%n^w1X~idyloSlQ2%u&3-P|>b0wlk==y~Q@65`9hv+u-+@=0hOyW9m- zLk@qIoxi$Oqy~#qz^#R;#^~IDp>%RlWr1DPd_V`oFCc}`)F8;7too*=Bpg2i!#0FC z#O5@KE1r1C3vEQ&<7eFoaodD*_KLqL<3^>WUCSvkHAEPj1_dQ+F*qg)Z2xzwXK4W8 z?bl0g+YQGvvAtx|FHIWa8k`6C`rE}$K~q`PZ|C*=)PTflzzb4bt$0v^I+L-VZ_2wz z8UlU(o4kQ-C9jv#_>(*|G?Whg%bkorF&UE+wns5*plN4FiyqBXo;XQGCA8)a9B&TN zz3|ZVFl}~p%CLjX8+!)3-x5tX5kbzf!B`w`y3t0tXhvj6u67q==1P*>BbBu>(J>7) zNueuyydme4$>5L+>gj2*?8uP8uu*oG_=&4|Nb$t!rMC@tfd$>Yt(5uP)KKp$G#n+F z!?@Fp^@-RZ5A2nTfCE1pI4){=a9oHDCVDtd0iWX~0r+mG0ISD8%Y1)QU>>$*Q{q0~ zt-SG6qOcgNqC9kcMJ|V(4#MRaWQ7MQ<9PTUb788*Vh|pHaqYQW#lwCZE5CS(lYC2L zh3@#on-4&yhaoYJcavxkD$KToKsJB@NPPDNY12UPbY^BIz!@S2>E>CLq%m+c6wxXKdM&o$#F+C%{!}TgdM&p z2==SnCosKIO>7!we#6D7!K{PEN!$F3ZW$nmL$I14$pcVFdr#+<-;n_QApw!WvJDEr z;4}>(eqEdv2#9ZR9kWhH^1v}9xWgxexLjB!9nK{zi|Aqf40U!clo0seJpolVizEaM zK+CpZkEmt}6OHeR%WA)cOO*S!%AY_9{=2@FCg9l^nDvtE{2tKAf_p?j5(yhRbEL-* zH=rWHZ+S1kP&h6D6rnn+d?b4~+*A*_;fCgEfK6AbkTIqo+ILs;1;|f`55JuX4)m~) zIlc7Qpi79`S{mi=4y4|8I4@>%5n-j*d6n7ra(A(Cy9kaJ^|FxldrDB(_|))qdK_l` zheW%W8`9TJ@*yktTHK!B4EwEyhyr6;r>W;82B3^dha zyJF*Bgd2C=wO(LMZWR-Xyqmr6ZZRc*f=bM2pP8QH8(sGZfxfR3IA zEDt8skEcaW!KF)+)>H5~$naD)dIj7N2DIo&L&U#PMre7@V~c3$sEjJp1HUR%I{ctz zACT)I~?} zm2oCSBCh%zMtH9Q$6jnvd81~{K4rU2GxM_Jw*sc?i?mGqPueM2mU>TPU zI!cfMj`s!g8!C9-7vccCX{!0I^28KhVz?GfMd&lZtjRraP0 zN`HgtAVb+DHML%Zl}AkK$+5g#;UhzSe`2Z+AAktrumBrg24r^wCM%zcJcp%G+h|1RYE zGn^5;#qieLX=hK32J;ngC&W;Vy!-rC1U|Q#27y~CIgFY&KebVY6a%M|U$}wCcY$bx zZBF3}0W;wzbSSKT6oPdij=u8brap}Op&|(&NI2g6lWU05S$c|9+1GIx<`o|SyV_j+ z2AzEc;yAl;^SqeFxVeF->oQ-ys2)lULc0Ozn!->$EJolF{EoxZDpdtQ|1 zS2BFNjvspckq`dOAUEN8# znRRi^O&8WHnTW$GU*y*5`^Ed4XOikHHi5sgKJPBJjpkS}Q|WU8wm)IZz%200od5e-zn6l~!k-b;b|Ziw&@nVnbAE8x|j^^6&X?w@*%U}e}kew>CF zJ%@m=4fO3JXRjJ5xpc5N66$RtVbQ)y4^6gi^LHmEZ6Nw29K-q~js*6yI>cJ&4Diu( zxpZiC^qlWwZ+;q{SaK70AWm(2zDTFz>%Z%FpVrLmXmW?0 zNQ{(7=yOz*d32JtrbRPrXIVc*YmjVc>V55&i5Xgpc;3yak9c^uc)P;AThvqhPy#e1 z+B8UEzGqK{EA^usWU;ukfrz&@&C(%9N|qXm^Kl^G!LeFf$HTJ8%p&1p*|u1f3lK}3(9Q8IQ+_c6CCU6Qa^8(Z`XL)4+2h5QGnr;|EfB{Yj<2-)eG1gJBuMu9r zdUOI4j9z_v3$ifj2s-Ph1q$c_@5HMRP$#UqXUsiV2Urc*Mqyr2_K8EIu#}1KFSkYR zO=;B-9E1RAUhqr<;!KOE0E5d}Szf1$U)JRp;J`m;d%M*v6A`PCo{;0poThpq-|pp^ z+KT{NP&rFV4s5DB3y|Lf^ZmjSMO$4)$2ACZ^$vu|ds=W6W(cYnaeL0m+OCX2%!0jb z_Pf+)RNZ$!56k6j=p^>FeH-;@B#l)qc}YY-^eE-BvPH)mXZhauzi^n6k)CupX4XYa z;y<4P(LV-M$Th9q1#H=nD3ou`nDmur*ZePpj(jqO85zTAqmvbj%=(^7p)k*1pjEP3 zgi+X6Bn_(=5(og$6A^7(;2mhpi=`QVDJduVDTxR5$Bza>4ahZpTzSn0^*zvRWZxSj zy`p1fEZaRf0gT;3iOA=Nb1b9!I{_}eBi)$^;GAnbq+BPpO-9C7GL-Hz@TnBvv zlwh}(|2&#U?oQ`A;fbD;6tE_mPaS}c)7DlFg^e)fT5Hes=+za$Ook(j-`FzLE3Ruv zmzfT1D4fjx*}oBmxTx-f<}A%<(X-N*FClu2DlPKP4}u`EFEkc}bfiAuV|Btb;Uomg z-GO!Lk3)n3Ktzbi2=;mnUEH%pSt_X2IpJW1QGaUhsrTxl1KfZ3PFJZD0`b7>ROVjs z$WyTaKMGWE3g2{a-Ov27rQsjAuK`hmzX3{43Y;PtOpO^g4Aa^IVoQ^5;hqdr#;M?5 zcHa@)uzT@h{TM=;eosUk_{Re~+JG7Klc|emZbo+XKL~NYoaw;MDX4>=UulKoggaj^ z?f;vYsCvnUyXF0beeZ{WZs+pJpoj+lTUHvN?})ZlP!Q;H8X`DR(J0K^F_CmW!c2_x zwmn$aa@9)cc1st@zO`RdWftTSl5b~4?o`!4#U1WjP|y7H`~kETl59rCtpAWKy@e|3 zo7#62Lpl2Q?*~0Ilov$~>_XIq^P-_+|8Q&pFW;?0)B9wg?FDy24wRs(pUuFz+yRdW z)wk&0h)XhUo_QVYn4=Jw77f{ymEjj%y-b$A?bbFuZUCUg{M?F-!WUKRsQx6sYs@_K zQaZty&)4o!K<%p$7VtEk4g@e>HPira>LQyvv=(xBPm0Tb4`d!7V$4ZMfjT8kSf(8j z4C*l_PL%zMn7I``o!Q@_9b;i;v?V}A#M0oy;vW7rCeaMz<>u$MP^1H%6RX_mGD{}f z8S$+w_fLdj44Pn8YOa_N6u#+CmrCSxZ4=9`A zuBPW`$F)Y@zgt}uEkO-VJ_|EY9Jxokjx1s8PXS5||D+OzNr>)Xh<*&^;mHtaIMW3F zR6}FQxm$==lx983@wc_Oxeii4k6{|LY;%nn%LnMO>bZjSOgu`r19{=*fW`P^J8u}zy|Pke&hipKObPJJW_55!KnKpzft1r((dY8@W_t-h<^4-3T~=c z(^-9NocBdBC-)%WhpKWcO;94VbKW#5y4GCG-Tl7HgL^bb6SC030IU$Sa%|-^b8Pu^ zbPO^be&l!BYjSnouXNx6K=0|lRX%juoKPj<#jqG(ee#84d~O8+aXSGFJ@@yXB~Q&W zrf>Ug`i8Zr8$2c=h z+euyjbk*ubiNS;6@q`P@NBV5L2E_yUER0Eb?h@dTT*?m zPK#bRdfEC6#WnW7dL+(w8AnFO7G3P4n9hJ*Q+)2a`|5>84;1Mjz)W=ip{;(?tMa(? z0#9MojXG@dQPJD`B2n~h@LV}`)Gaz>(e{756j42%YlxWVLFu|H9vfzX6v_J<5h?Xc znq$a|>5iuh&xYI{w&~M#kf_^d)fP-Xn(7E!z~aP0QBJLm2u`N@{<1|9Acl6C8MuMK zGyUKffX_WihL^b%d=&(;+%~^Hf{I37ndd`bGjS&X^^1tw+(Rh)rb9y(^smsjC|tLR z;tZchu<_&?v;p^EhO5MyTEuhnnA2EoF*5LOVVnxF9?+r+(iIAkZVr$u_t11;&rK|$i>EX!9{>Nz}ZInL{kLX zoT`ViO{oo1?-f+=d&^}pu3R%12x^whfK^2l_V29f?v0RVHnRUQEM?UpgrQ6N#=mb$=quyf1Rhe^xvlz$#$&))fRSfAjl_?I=wNxB>A7J92HqA zi3IB(iCx-wSS3ODj;#P4iCjK^(6uVL;!N79>wM^WkAll?8=n0?hResckCY~{UzSYh zs++&%wad%vFVyg%M}2T*+s{zYjDs5XlUR|38aT`zlr+wQ!dM<|tJ5nQ#$e+tI%5+% z6s}Vti9duQy6?AHs#U?hD3%RAZV;`wg?>zCo|S-C$Q!}p;_q1V*ix^5hI;W}G5FLJ z2?uq$D7!&W$k9ef=FV)2X83qn6Fa($ei+GnxQ2*&_JG+yL6`}B%Q_1GP3&pM)Ai?4 z!X#KCbZ$7c7xi1$$kQVo&t%v3>u>f^z9wZjJn1F!ztiOKi&h5tu4N*+ZkJN_@B({+Fz)%0VX7a z4`BtGKpiC}8CVU5*l;;h1s-gCGco z(F44Q&K`YwRw38Y=?;c>Q*sZ10%5?177E5X4|_tsSOqsZi|@ZFA}hx=VL$ zCJz&yV}TgNT!iQ!mp7Nd%W-Reo@$D6@h@d+>S5sK0L#WwrJw)<<>0G~RsY@XWEvEf z+gR`w6|{(p*g()mn)yF6){wI9Tu00qiWcplX&10-W~TSha(PEItEx3^yF2MkPLe2W zi|Ce^_no)wnG{=3V`vJKUhbV6qT}9rM`6UohnxmoglH6IjT>-_2I%pF1Tm9Jz$Ytw zAF<>di?HBJY~`#@ia^-^dXC0yGQ!w+JBotA)-Omn=kr}pvfF?f3rv)07?8ZbZ(%}! z!BXoYFD`K)R_6l@&lTAe1#NPF|=fPUB+F=9XlxN zpOO}@>{IRRP(jxu_QB6}%jaoFawSF6-n-Ii8WfzDMQ0M0O`WMpu*wWN@Byn|Za^SB z_}|JU@xONzl7R=*M9tOT`WDAlB9qYIK48rU2psdN0fxZ$wP>&m2_VTOBrug@c;AH1 zi_BHo?sqqG=;$WeEMJV?(CwA`5cp)&wmoUQDcw@vijiZ`MJ7ihIy=dNw_o*9UvWi8 z@C_fC`kKxsogIxLC9|QbRQpI9eQ0L|FSrf!O8qSL?uTIXX#I-9JN z*aV=Kwi&qT#kUwjm?A8V+B_SVc#{j|bhIfbM64l%C2uei#cWy1n27X2B4?$DwUCIG zpmsS|RkM`E#<_+IM@DR?n;3{>=ggcl!%V9dZg1T= zMvhla#X>3KMV}e?^UKwn@d5M%>DXKmh28!FUUV4XFNs^EK8KbH`S&Pse{{P2dy8!! z#vyizUS$8=YEA@IQ&ka@<_%9Ean0f#6Sj;)Xl+y;>MgonDVvM5yq#C3V+NUkGYAeQ z6XgJ-QaTRegdo7!C9P`LO_b7U8ZTA#zn&|$^Qh&R$g{wnq;YwGZ5}2LylN2d^K3kQ zp_Vx{Fl6({kN89z2JP+_^YVxz$&-FkRPRy@Yrz8?^dRIue2N>sSe7w~LVc1)Z>)xB zgNT19gI_Ndp5uY8yJ^S2_vq%}zJVWtWYR)QY7I;fWDEfu>1DhM56>X~HSN`P^Y z18UT^anm1R_Le1{lOMl4f9yjVP4mJ~BYtO|tfhrdSoFI`YdNPjJ>L7Z-vh%}qIRtE z&cpCu@l<8^BPGf&vh1%`c1k6$)W^h5d@R46`!Mra+DYycx<``!`_A`;a_FHH1#}XoZ&4x#Ae;28SMDnfL9jYcWj0GZoqu3>^DYoU}quDBkS46 zRgQsS_@Ha<@}e8UZxhD0=+Yl5{AErUsW5JwnX2>bk7N4YZ;c`OBL3d|45Gk;heN-l zLNREaN38o@*or7rr~O}+@^m5$q}YW&VI%%y#*htTKkK#ZJC}b=vq73nWBq6u>&D@0 zpq#Z3YZAg$iE#46AhroP9ZL+FNSQICY4KLjY9WKzgcJIG&j-6O$noH7MiA_nS_+Jz zh@D}1Ktvp7gn;$QLk}|iu<|ur{X=(NQg*{<6!AH^=3_8Y!iqyD~BQEmi z05RwyL@367AtnKzDu^~JvFa&4D*c-rHu~@Peya6Q0)hHDY9;TF>gWJALXi%EmV!Uw zfRxJPko9|BF0dOHj0VXX?0}tvBc!EjI5+Tb+d*W_S`J-06fxutL7oqz?Cm@eeJH7$ zPwdH*E>IQjHD{yG~6u75{_O2~Xvgo&+_(pz)3MgIY0w&xIkQKr)7 z7|+GS9OJ>>uTJgs=Gi0UhAJ67n1eI_<_;jeN2xY1T|%X`{KFIPricJU&8_?{0oZBos>i(Dc^_L^Dg=>oiqtA}^r zLpS{)-PXKi!W%r;Q6;3uAJEAQy@Itsf%*#CeA0Q1FF6<=9P7zTy-)PqeFW}JtBA#E zC8q?Dm6Sdf3&l8D4s*bd|1f9?DR8D^(V6?6yYOVe+B$IX+xig$?X6~-Z($Z{ES&!wA8)nA37ZEO@aaL`rwi6 z^rk93aqln&u`P>`ltlwpuayJ53}1@4&*m}xq!a>QHGn2NGoT&ekK%l;0vPquaY)9V zlT6^za7XwnAO(M_GV`msZ;Cb3;{7|tAnu8+r0RjArHn-_Ah9ZyE)Gl=h5hnzAzHzx z|L1avLnfDE&~3gqkc4dHj!n$7b~LP@JuZ7uedOp%dYu$>fx=PLo4@xrN2tOoq~tl0 z!819&F9n6F$(f{)>8@G+ccV&PBLG42OYSvE zmZ01czJ*R*3@5 z-W3-~6^ERXRqqE=Tw%Y?z$ne1QWq3V5I%<=QuBQ}e_#6V$cJh4+75hr95!{fj0QAr zT%02(RrA-~8Wx|{x`I0aZ`t2m9ey4&`@XZVyv$J&QoAnY_f}RIi=PZ$PHmEA+Q;>^ z?i?YUDQ`S(9~=yIk$rajt^Z}#U!v$?Ptzc4@cO3mj=He;p9XGmW!XJSmR9b8A|G_EY3%9s(0j-+m;V|=4?o(w=v^LEPNnDR0(7nAp$KdNTC~< zyl~&0Ae&EV^J8Dv{C4t=8agS-pkB$pCd$y#Kqlq}x~ko9WjpY|bR}95KQqS#VW9Bs zv_Dc)+|Xn+|n(Q?KyG{q+yjt-VAKS{N6Z^zcYk(f#gad1U1_6Q>5zX^U}) z4TtDFdSp5%sLZI?8TWhSX-tu(HhZ;4%>}7poYHG5(h3sg*ONl)GqxPkj(y*vx+-fg zW`i2qE<^;Oe4Le2Hk3SrNul@~9gf5iE8p~RsKl>wA7dLf=)zfgPMEwx9CUnG5z(g? zXMaKbPW2&9DQR62_6oV0HPfN1YQf`#XJ)*}SET6lx5+=Pi+s*;<_ON!6?dHWspI@fi8;+Nw9Z*$RS4?{~>un{Hb9UFem;ab9YS0Z6w>1tXQ zn|GCmQ|}(ZMGA}?P&7ZcBF&>UZr^w&t*)06Sen-td1y(&1fCppx`ml%jxONg;$2Cp zRN?4y7ojrk8-2Qn#(zW|`<;YvO46TUl-ij&^tPN>M=-JIz=`O<@aed`=YTN*5Z=?O z`p=sHC;`b{IFf1|1HrlrMZ&@UbA`|IB5xE;k$lX8tf|YA7OMLgdE!tg!iLsY7adQ< zWt9`*LfWjG5aj9Zb2t6Io*XLaH$?a5mW~~Qe!tIx>;4~Sm4U{_y8<>q%kpzsaF-B# z&L|0p)}jwQ?g>%G06Q8>`J25iKCP&ikLo>SBA59fg(kU+P({8s$2 zO}0pmB_b@xEeDx}AKZ;G{1Xl)w&|3{`~1bT?TX7`|D;&=xa-?WjGP3&1QN4F+~2Uh zeHP|wq-3kfU+*9))0_Q6my4CpY$G(umQ!HL_?`P?K@;Dg-^%e7mmA$4zUDXCz;t6j zV!K_O{XiKHhAIksWxqoMe?^))V3z%pZ7oNA?RV`;+T!tGV5eng=9HA#tU!^)-VENu z^fY43)lmT=4dx!-XY>IiiCaC7%ubtc>G$`pA6QEL%u*VLasAX4jB2b+<|jI*x_tJV zh?>Hyl+wic5!VxsIOH47=<+&Nrq;gS{KduTxGZ(pAE}fQ!gPB5vE5@oeCL>_3EY&4 z?tOc6=FL3YYJC;&Jd^XrIAKk=bFexms^x9)t!`1gLs(PfhZ z@1Su@7(>{B&6HIJO@LL7STeJ6(Qj$zEg!FX5C;z*HCrXcv@MV%Mjl&NG*tn3cFDbYXx5f0Jvw1V4L5ihl z_Xh+7cf}1!~!#erYl+=8^ku4g%+k>>eAg75}&PmW>@n?7jnVdQTK! z!_=3t`26^Hnct*Gm~5CQPHTf7_?a4;ed8FUe@J4TR7^aX1z?g@`?D(7kpT zzxLH1TQ1m2oZm*ibjNv^-_qKhkCd9s&$XlVxAwS$O0T3Damk47^lX305El1Qcel1+ z|D4waUK5QkQiq}yBVro+XENoBdO6Fu`JccPzHJVzN$9sguH?L;?(&v7Lg?oe6|dJ6 zj79zm`J|a6-T5XL=_MJ)1gR4-qT}6S%es$SNtB~b+QMiD1r?X$EA>leLYV!_NfKFQ zRdRc+;r%360_?FT3}xUfR;48aUz`c3zkna}t3izE6hYOg5_l`RC5LO<@x@1UG|tUt zl_FE3ShSoSQuaf~`C^qgWon3+-z7i;1+@$XLRVI+a1Rv88k@na5x&cxKv?_&elsyXf~HY8Xe=O#~PWcv{1 zA>4gl;SzjG3NZ!*0z~hzjhNokB0?n zN=Fgl_S9jVj&Q;h@r~>9wPrlR;TSbuYfln6F!8FXS%|12J@t}xS-;7bv+ju-F2Ch^ zCek%Uj<2iW5~TLo zAIU{5QjuKq+{a1rW9up+*kWDc=}wnYGQAUs7wwaR%VU7JpR-guS}`poLwK*$r#g#P zb>tF*ZX0KitCvvUqB@1CQ*HziRli7mk>cCvfe?d- zP*#!+uYwv_Egf8TGTh};Td2tp4GnD{q)mr-QC%w4Nie%WX1|keWjC)3L;-spEEm5@Q01c{U<6 zwXTJD3c?!0(&4xv>BH>$&xDuMWP5(G8i z1%28 z7sE_9hxzf_Q1KN_d%aK9tH<|NZlj@32Zv zsEzP^hi1wjMIdewYxyv3^4Xr{!qZw)y4^SO*OEz-N2Zn{mqpK9Lk&MGh`7t%c+#7= zw-j|<<5Zry6EQws55ye!^x&O`>`#NAhU*@;)Ra+W4N&!BS36Z`UswDoylINaRXhQo z@CPrJh)R-hj7_@bhpfQ?X_np9u<7{i@_pnEnZ)m0Q<;dw6Cq)853-J@$D(uYuV*~n zY)vSFq`rE{@d?)3gz&7Mo?vvhNvP(i=EXtadk5qjoOhWFhD`$|Xh;$vbi zPO>({WkEKj@RAqRd`J`3prDb!l28dy0Uc)z&HR>JtC{yWFp$3b0$9nzbztD^dwnL` z@KI6Vg*@N2+|)Ahj_8lmL#54rODRsuuHBQihP6p!*% zsC>{<6BR2V1g)-s2NdE!=waZkEQ$gk)=)JtkdWCoJ3H$*^JIa;01zn&P1_e^0~h9c zs_;LmtA78C7bA3@iu~g(I)SsYt5>o*$G%eD;^hdxzO(uJHTCp(gHk!rq4xL3;Ew^v zzDh!7nY`6gEeruJQ%uILYy5iklm4Gj?Z(781_v4HH&#r2Y5v8*-->&xUo*4$8J&-P z_bq;u=ByKt8#XWT{`l9UDZ%W_D%y*0EiScrHoq_FGZ{ROFCD3UtZNDbo&PZetW(i0 zcz<{BQk_j0vnVxY~p{6@;BLGH)9Bcr@M z%qfQ_YaKswkI>N_Wg)*OJY);Kx5Z~9J-Hze001@-U*W<7&6&WL(jUvjT{+8S-L~aD zFr8JqRnnbF)4z9t)F4QhsXT^{(682Ds=An%fwQh!Yb&+4jX7k9A&MXR-dASzP;qH> z?W9nSS2&^f!~5sky~z-o4cJsRZaDfDfm38-jt}R=Z!2zfSQ6m}@8EQl@gKF70xrrI z_2D~KCT9Awyc5lWzI~0q&wZt37oP5Yo-@mw6@<#0DG_bzA$?GJEAO#O-9B*kG~cd^ z4`&p{;NSf8!8Uolgqbg~-_o2J=k4e9oPt#hb95XQD7;_a2d_ra?zZ=#vvd;HTmai6zVDwU zC?x|2vrt4ZyNavHNB!&}Y&LtcOZimxe-4iJch*<#!ie0b!G{bGPT_~*t|{D- zP$3+-%Kr17smlL+_#D$d!r13Va)LjQe)702X7%|;=N>i|T4>5^BL;C+{4#uiIxw}UN+2D7W)98M(lwJ|n^bn>=#V%=zSw)QpsZSCR~oX_0;{pqQoRI`5d z?S^*#V6g0E+TouYuSA*J9qdD~?*VE5@B>eYF)^>`M@{tdq-jCXIfhRu`Suv&)!FCp zN041o0$}8QNd?T6->mKVLea}kI?i`CqiIT?T&_muiCUq+I8GP=hYm}nG?*c7ozLky z=uVaJT{8FoNGR7lZ^F6;2y7K{D8R;s$YaSyfKz&5LnG|k9Tw6dkAHu+K4%Hq-*;kw zKsxXMs8+x%xgp?l31Fn!Lj?h(9g5q;+MQYuNT1H?YX>FI(y3_T51j;Ylk0}JV$Ln{ zH92;|0Q(fG)cxZuaKxRfFvE;k5Q|P+^F3ckF5qwP zGVG`J=(d~{cv^9#ao7IZOYhwoj!;rK!xY_7&@^p{_!cqE@f!vOJ5;LhXQIw)`E?J^ z>rNIs%R2_mp_a!_Q@QanI|(6{4Ev5rzwsUSt{pL7%J+Y{O^L&w*Ynb;+6~Nd@>0y` zH{0(j#8B~DUrHOXC)OXs6a6P`-8Xss*hwaCi978@q@d{8&+prC47ZY-mH#?I1_Mck z7PC$*d2@sB8I2|H*8Rs2TN__78f(iWv`e*&7WoT5c&5)%%_m|_Z~XI?WU=gzBl#w# z>wS5t;mRY5S6e(x`6P4=+fm>Ell0f>Jz<&%aVur;xC-yozE-xUAsxVs3O+W?eglZ$ z*hJfr?Q40bBAe)Mi$FyxKAEE@rP6$>d+XW7Yan3#6ryO{AeM(WV6}JSWRWD5dx_D+ z;Pp_a(09_>u62`Ju)by7>_CF2db}8HOSwJ~Srb-RzlkU9#Q4ZqnEl{0hQMU#c`@9v z@0_HO$g(h<28X%c91oBQz>tq2vQOWPQGs9@Bqk;p9H_?`{%yiV4LXanw&t^^Z`;Ue zte%n-+-;@^tJH)aeO!|M#@&FC^cN$4w9Umvo&%UIun>ad2yi*!Kb*U`K)Zmp+Ia8TE}XAFos zpE3bX@2AX8P$0YTu%0P(EDhzuKf;P9pLv4}`U^W-g`pG5+4RH}bZgqwJs!iKzAEd- z#2qy}=6w>PeQ@bAd#rAHK@;`8doUM^@)gIT4c~}mCAAM+?Bo0V z$3im>)oL|(BUV}$RG|l=&ZJ3xm4{sE;$)v;5NkW^&sb9O`#)AgwGf;Oe8@uTyTXBV z2S!S3M%jk=U)R@zs3F3(uCA-Cv_*=DKYgJxad&ZCcaaWXyHT8>xgo-3ixlAR20e@D zy~EZKWR~w^>N%x^AGr&9C^B$uBlksg02lk^^Y<+HANG*w_llE2@gE$fH#&|F(txUz z$iI|v+A$$A1cYdv1d;WP8DZ0k`K5cLGiifjfe%ZXciXT7?;brCGx27uQ~MM5hK4@? zR`|qF8-OejJXOPDWROQhzpxVLIa==yDf$CP^8acjDvalMt#Txt5dKHI3r_+dRqS)~ z{rZ|{ea;Ik#NpxLji#o`$~DEfImz5BInmyDsnb*x|UrO$12o2oRj5zUg2!8_KTYSKixOG8q@x?{? zvbiXZ(5f-AxVl6gYWX^Ubg3}YmbhuPM;JlAo0Na32d$lRM{`qpCHmfpY~Yv>2CC4R zVDzX|0CIvb5vK0AV59AQi4B>$$U{=ht>#SB(se@HIfacI23pJ?Oyb8zTqL_{VKK%N zS&r`QY_8&LsGuPVf9VXQ*1{AZ=!YP{pAaDA5Qg`z#37gn7z5E1MItvl}a3*Yu|Ri^%0EK2BISxCa^Q$QO1b- z^*6RooeBgWah(6!LBC!s*Uytl;#2~PU>aBoaXcCwcjrEQ+h# zGx8#2lRQygY7jEhLCT3*Uuq_va#6%1BcV;C4jQ9f=& zQW0$|*ltGEFvJ`}5W!ycj?$jGV{=hA%XSBeUu#cBG=+OdZmJS777v#9m!rODunqlr9;OCv72+LH3Io{q1Kl^&`%z^%Uw?RXQ50X9egiZS#Z zGH(u-3;=V3TgX7Y9Lh;IyRl%}~6 zPSa|iA$GDGuq}V{isZYEh|W>K^?-t1(slhM*J{{7q3N4oonH;FatUx4cg9Fr^c(yB zvs*gCb9f=9eL*KYD&KSSTl)Shm9ExEgd4fZLKvL{{GQXDs`y}Kn1(=c|EVz#6=5`X zzF$-NGM%*Rjs%su6&Yz+gJtUe#~%XoiyeyJ%+D1lM$hjb9le_z>yZg3`xQ8|vw7`) z^KqN-YjD>;?Bz`Jq0o9nm%Bl`aB6G*cmwg(Pd~gD`nV=Ah*dvzUZB2nv&N!yFka)B zu|jf)euxw$9Hb?L^Myq#$srC|J}!0VTmM4M<#g3NyA~I`Z~Nh}pgx3SM5~Kgh|pt$ zM%S0qU&VcfTh8H2dQxchk#|l^9+D2Zut9+OSF^y6=wZb z=zv({tz*z3u4+tMUp3nC79fcQ0oR5e9svKl$tNBZ#5i*xMBLl9(nD1Ef4SOJM1UgL z20)bJAz86dasWvI8Xe+3*tHS@X&@`IBa>Wd-SO^+=!9kjZ!-jzWRp2=_fP!dD#5zWy z+FqM3+KmHP4&q*YUBx6q>BkOb{vjEx3i-ZdzkSNSvybJ%Dh)$dkQTlCcmcJ2S8iL% z7o3$+9Y@`a!01$b zlZ~4#S+~xWTbRc+EC>mW@Z`aA$gVPNWGSO1s@uJ!#Jw}2fl>c$|9CkUB6IL&KyG{^ zB>j@WuV6E2|A(-dN0RG4$JDd7=5+dz4ITHN7q&L(6WHGCuPcr`pHE`tmu!>|#IXrY zq%Ny%aQ%mFKr=I7P=A-Fj>%O@ldmSz%n><{+w&M*L|;!BMU6kZ>tM2Y@Gm_)D0JZQ zos^2nv%g2UFATBXh3mfkIo>O|tBDM1|X$s%e z*-W>8eCv-VF#o~Ew^8Vz*y#wGjdS^Bx~v&u^EG!%G-#LQSz|`v0|eSN->##zT&kJ_liyEjg!XIb(Z^uI@f_io zy&fFpT7xBk7;08b>udn|bvUt!P&*rL0wF~DX1bn1)jIzDZ%!nR}*=6)Iz}0)4AqNm{nWW%0M=Idr zm+AKBcRzN6Z(SSguLa0~nKRlEB~--`nun8T3i&Ex0pE^3FnGTFM&tZMm1xiA9RaSBJ4R}R)~JC_yM4vL`0#L@+yt8{+Z;6JRsZunB`;2JqSpDN}@*T}DxnlHcpJ@I7?vpw~^ z_?ynD`7DB|r$}J`nJ%*rLhyVzaFX%~xytiU)j$7iv2Uqzk@g3ii>y7}QYsX#{N*S< zq1<`v$!A$n66#~g&z%9Zk;3Kdl9-SLW*MT(sLIpz`35>)ctzQJp?lfL+2Q#{^?CM1 zFv;21f5S~j^U5Fh2Mj37^Q(K8fdki0yQNq0-p|K=j0sm^V6xMA1Pyj$_fZTWS2#LDdJv^9*-kuA}{%5 zzxRv|7?5i<{lp;{^!y>YyP(qjY?%Ea6`7Jmith8|-2+FmB#*gneYG+O;$C8uZmR&F z@G;s(s*3GuNU9t`O6ZGvQy^Q-qwAfz`^y>*oggOSJNvW~dU1(O#7K5-*})4S)F8=6 zadTE>qMB|dabiP_Mi>WfNhHh6q~}*{Q)MvvnW;OmyY>a`F57d#d{lV1?|iQN zf%f)>H+=@CcDD6Hche!Fv69b`rbM;q{^O-=z+`QAk4+cWj+FhWl=n?-oELSnDjnlt zV?a3^Ws_jO-h|EOh21ax=!V5uNK z^#}Ghzosp)z0yCD+2P0t#M5sdkz)Jf3BUZTmFFgyF}6`paopyA<>@3v^`AZEqiIjm zb&K*;=75jZ1?PScGm3gNi=#?|)C-q&bCZN1DybfezElQ?XprLCi&BUBT&*wJr=J>w zoz|xD&~WWSkGB{HxI&mQWykM2 zG|jLE64vYZS%*>zm~Nf`P(unb3Q>_W5LJLcdwjKaF}vc)c0K=jj=G2y zZL9C8Emq<9nH5;ZFF<5XviU$^1Ip&7fyP8{6Gp$v)Ww4YqiFqyK05Xq3b-YzwwXN) zU)epW{z{VQCa%erh`gwHarVHSnEO$bXPk+)=qHz9qw)1$C0DN;lKo0ddQ(fSBz>Ho zyi+b4rj!Wzmdk{D4MGK^8s|Ax4JTd{q`{*}dCLUN0E{^~#5D`9`{Rg)0Qs(H!NW9j zR~MPAvcZms&h*^Q`&X3_IeeXys{Q9Fq9)NV?$B8TCUYAP@pR0uwy^k#0QF6cA-b3= z{9?c2aI?xK1SD8)o07#J$4yR2()7p(?8n=1pUx`~cjh?!pt#Z2QdT8hL0XVpY%b19IhiLUII2A)tDqvAj*afsFKHM^+F%%j9Let? zPQSm<*TUNg+%NoXZW1bnO$1yJX@Bg*KE92t2fwa&NdUqBs*Dnt;~-ZleIN)-#1MFu z{hS;eQ-Rp`*oE^iIJMq%8QSej2nfPlA)QT77$4C<`#ZaCCz^?`7D=d?Bd)E?Czgq; z|6LP%1^xLV7bky>X9>PszQ-ENuc=nyA{}~Zt-_ktMv5$V)JdVL2260yYBZ{o9YcG5 zvH~P4E?CC<13+w-F92`=1IYD!Nx#32qx*;Ba~AL-f4`Y~U-;}DxsWrVE`W)Jz;nk*gVk|54~ zvRwEn78V3|P!ySv-et#gL90=m_@6irK|$FB-cimP+g94k)kg_0ibRitpVrjA2~?@E zvPw6zzw9q|x-8+QxPdwa6W_)@@6o+=7dIy58xymX{~7uG+)bcnqMK+VgX*dWy${r$ zZ&Um<+2h|Lm1p3PL|N*`GZnkf{3?`iOJVRejc*+T5b1k5VuhFe?t#cWmgK^R3MCHL z&SzrC_Ml7xo0SpZ6He`-rIvKZy5@k#!BUVxW$m|TJUdN{{y@*jpeTYoQtioqH&FT; zii}f#^g4p!uPKYE8j69yZ~Z57ugSR;kPB+~i8JoPn&SbtyB!8=JBKGe1wv(%){dit zF_G82{tzfg^1wrS09kcF9-jfQ-v_wGDS^`$I0yh-Q=X)fAd24s1R|w&kq#<&@fdd+dC;c))oR8606!&jQSlmbY$%I6TiM(1xKjjClD}zb2 z8c^`68f1B+TX7*YG!!lKXNG6$1KE0%`JK!zYBlxSTAa2RKpc?QuXG(p@&;a@L&UMr zd~QXyR)dU%IG9z#0v{ZfNiBh@aI)#Jm?AyR)Bl!FY-F9)8;F&s-=8`665fpoTz*@E z+=>0-7xXtcTc2BateN#p6G}f~MP?+M8?$f!;$lpkSM(#}lV`OUp_997&8(ZzBckh) z(FqW;#!dR`YHGHQKh+ixsM%CiJEm`_p4W(0)Z37Y8>j#+_JtTDvP)qS;!+{BfpyR9 z&nB1SDcN5%biQA_@9TBZ*ckEg~bVUlLWP3vB=9WYrx@XnD&>Nh{6_mQXUH0NxJqQQ2!3B>F zKmSlnt&iTMJ;JLGPX6`_p*$}E$scC6JL28#WPX)Eh^rADj(Yv@HZ?JV{QaHG_ori3 z8v6R)UoKmJ-(M3#CnCP#0NHmVPRE)P@SvwB)Q%8JGD!cra|=G;fjHQJ!q1G`uHaOP z7#SHOl%p=XV!!#H1wU+^|L=~pHD`?qVK>Lm&5HnClC^ikpT24G54PC&_f zRm?!)b0n=Wd7YNq&CrCQ>IUsY#1VGUffCmJvQGq@YA=BA?)e+I%v=Bbb9SBujpvF! z>h8<0Uts}6UNbq7oD7$H^|*nYLG0X}M~$|m7PCFhcbRn8SuLp$#DB zgh>~VGgbF_4Vh~PX^GsyN&#S71aqVYsj>DblT0bbJJT}8x)v&(K{e^ zO!3S6(c=+Yc}qFOkDe|fy!#*i-pS7)!=$|QxyvsKggf1qf78Dik=i~Db%BD7SjVz1 z4rr1Cko9j9gjss%z|a!{OOqD$!JtfDh!xW;2>0(;i`zyo}~vl58P4j{f<68 z$P~HX(bhzL!qTM0YD#1dAqYX?kOz8A6Y@V5=d~H?$Pw@XMmB&pnn!~@ zgrTzy*hqiT;d6)9AnRO$*KU4w%>GaGofEA7;a`RWz%I%X)Mo#;i%Jn{Ok?)uyX0Oi zb7IqH`m_A}xIXEb#;0dl$4O&fmc3>~-ze=hePs@F=fCr`{VRC;r}jWG^OTI_DmE1t z!?o;|#TqmFQ9WvP*@dF%t$O`EKLb%EjsM1*VQ?0gapYj152IKwTJY4%FVFMryROx4 zz^wmHVN?B}w_<4n1I4F9|NfN3rdzYH(mzYmCqk`=F5vs`1*a4>vQ&?E^>3vf7l?S1 z!!zGibvT|3XyWOgOD)L$S0#E7cML<}W@)2)7Qwzhc*4wAH960%_Ux(!>f(uYw0ZN5k62@WB0Z-zr1U%l;-FKc7D zDz~iZ!|e_#=`}mZSvL5UeCPUvSENo|3eXD-k&}kn=T*=5Fs{>J0CW);XEeTUkp2J# zghLO}!W*Mv!v_M!H&6!)T4v~O=pTiOpf&tJ6~F2Wy2y&zpP^fSpy_WzbNIGqqP`Fs zm4-ZU=3TZ(A^{e@*$~#6hm2prp5xj90 zBzXeFI0T&0LeU3+QDY8zEZ7_$j`3O(UuCp%XRIsh0R()^n#`e^(yTomA5fn*nh zU|L?w7Ii988L#R}kB-uG2+~y?l@$T6t|E7EawWS4M^LJDlz|URAwO z8o!bB@vPgM#pq$G2vqbTZx4z~bkxX@W^dL&J71BkAc+%NNKHW?e~IC*7~4I+^vYpT{Y2{8&mZ6uM!{XA;}_6TzxNp~A}sFn z9H|`UF!h}~Q*)7X#U}U6I$Re-^Q{0}sEZsk_NkhWez@67Bv}qTiVK?~L;=bOkBax4 z&tCrh_~}f`SEgmLIi=8mkmeOHu0>9y3{t@mcO#tfHFpnuc&iIh!-hycr#}{)&TUb8 zR96UzNh?qb3t9IKV4tL zp`tb(-e_73!WCPBjV4m2g>#g^h)bKoP@d_p<+85N0@@81bT1+6y46CfG>sU!oof~f zmUE?QwZDUOU;)bo^9On8BQ~FdmUmAmS^xY5dRbzU9sLJ+7aOABcpG(HQ4z9Rv8cWr zc=hA$sVkSGwK^VHq)hFi#{#?md>_m5z*J2jTIraG5@X*bRQG1iE7B#40DyEg>a44) zJIT+K*DgE6N<`aP7!_hzKJD5Ewe+!7@he$z_E6j{1d{m!OrhTWgG)+eDuE(Xj#Y4N zwFQTa1_q@feC^h(Q<&|KOzX)b6&QBf0Icd>V#2w8Fh#mq^jfq)hy=9*n38xQjCc zA{`VBFs*rBTLzLvjLG*%^UsT{wYWU~TLJ{HI316A0T=xb9Six?>? z2<90bx7)#!x>r6qk0ClH)Nbp#pyPrrwm5Jo?`2mBbX>P`KYUw@uSKVt!kC)Dq?)QT z{k)B?lh=V9QN!}BM;_rrDD*2MyYlw~Vo*DF>$qGqc4Kk(!EZ-HMnvfHkk9Tw-OLoS zX6=ZkV-Xz_p%C;ClqKXh*AQC_kVH7#soU61B%it#kNc4D@I z#^_kE7cl1L26u!2E!*yA8p8@%3d~bmv}}Jw^;LXXjyc1Oj%WSam`d)NZI^i+#CBQ} zLXN2%ahFK_{5)ik{NLqtkap3>VJ|hyw>Jy&32B`AHJG-|KT*=zXF{l74|Me3(${wu zie%H9%s}CbD&^4Xh!{(SWkp+ydG*=luI3=fdiVyXgn$h#rB0NNSowd>4Qy9CsGR@4 zr2o}SILd0FNqzShhmt|6#hn)J5Ze3>u~5<9YWb3}o)Sy(ZvZT`J;b=kM*0txiAurN z?$J-(67l+)Iq_wit|Tff0h^#+__3_;2fCC`K@bC5-m9VIBXVNFhqs(F_HLyosE3O( ztp=(W1T>PTQfu(RUk(Zd@A^dVr{x+ylomb$(BrJaf&3HEH0dQkiitRcP94HP^ZS)R zJ}_3~0W4YF00^{$@=tqBSOLVS10^DYABL0ObVS+ttYTV^qxe6gN8Fw;M)O|**NIAp zm&Nnm*+Gy=p$+zZm&Y$=xGj?mCLi$|Dx%hRAb=r-a-*omRZ)|4=-a}=vtD$;PU8~Q zZFj59;Z9+qaA1sY!+Du?9!}h&qiS1#jn}qa{YsTC&0CJA_(Q6rh^21J_=@b)S0)fy z7aNliGW8V$6~a`m+(rtVmCUFnmcl&vl7YN2jLE1}XNq&C=5Y#m;cnG__~=^deDdsQ ze|<*CQobP~a-J*3n`be?1f@)KJE?f=E`GG`%0n;pUJ+kpxK(~{c8K1O0^LY(4Gxs$%QHYlEm7OnGY_Z(+?xh z>qPGn_&<1j$>gDs_Y4Cj(Ue?3LCy{oeoXtum0tlS-?`8(#B4&M6Q1E0c|T{0EOl=4 zOr?6KMl%u%1=>GF%-gf*=Z9!2W^p8p@ zt}t_t@|px$D-5kY5_Jtt7grUIHLkx3)zWEaZi1IkrSble5!k@hV z8xr}T`Kt+$hhjAZ31k&Q0TAMMg&9jR1PeN&kq~yY{fsyFnFjd!@OXHi)pK2U+pYA7 z(-qQ!q2ZV*^^B)Gqk4q;>+mG~ysaT$ISg@{wKGNh^`4>KI#Ns)ADx32sQLlioEl|U zDmjlYUG%gzLjzNOj+k--%SbTO1hoN-|3QXNWG~h!z!?Of8|*b&w}n?LJJOeJlL-k4 zUDj5jS_jVjH``UEfcknQ;@G@rlb(^@3?uYy>^l^h4(Ly`k(>i!Y5t=|O2sI2%_c z<5ss$uKp+g;Jrl|nUUw<-;^AAmDxd5l|`i*N_9S> zQmD4)?G2z)*G+%6r&a8&N@%9<|EOrj5;##{2=tRMUi-_|5+Nk^gNi)Om-^48b5`&y z^173#t9WtGDd0rYa@9JhyyF7>mI7@yQW^hgvo>Eb;XFZ;*p}>c4BOr_WBjtWs}=?I z<{ypeUGUT4P!Wjnckktx?w1pQ|*=t1r946w8u*zYBsRu_ERWqrMX z`0)4x_)yC;Kl#pb(9Y|yuQ$-MllPpK`V zeVK6ND(pck993$3D#Hi(*3iHi2m&)I*r`td=Z_;_Y8IP23TIBTxZIj-mDJ9 zQkbHPc;hh^+A$IBd1*Fo#r`4A#)O)*By$+>!8Wu2aHyG;_x@G=#jjDFoQG>F0k>+V zOA7{y3S}#^@6Qt6xnFyV*)z$$z>R_KT~ZD@_vSJL@(NC|H1V0rCNIyS@hK9RZc}kiG)a?POz_3KhVmpPa`A>;$VPib z*Y@l9*?+Y7)Fqb~b~i4%`<*jQccV?+I#r{Rm{9x&K$2uu{bC)nNwNIM@Ax@sDx?g` zPY(XRYNpoIHf%7b1jGG9cml*9;Id?^{1)O=Xd@QcC%_4Ssu)Kcq{4PB0D4gHhs#_L zAE^GTB43@8=IA0kA?*ts;$#dJ5;8mH#r*S`4EiOe;;Zy4_AR01^0^y%%W=Ov;A<{C z{#a;EFuFQ$=&J`NGPn!Ns6TL$ZWQf^N7&!+{!b)cwV$6~0Su3^^kLy7)UxPW5ytqe zr#lE=0ATyQgvkB|8xYX4_GUc@m1P13^@KZb>O{uZB+_9Za|(jc)m-XtZWrc%_1Z*@ zzYAk2MA(SbFel-Dd0llHzW#`XQY%%v;rZUB!Q`BM@;YA2kEC(#6zYdM8u8cPUuq8+ zKfW_Pb<7kE91y+YXcrMcDZY&~9v=}Qt@IRV{X;QVbrHPdL4vhPSs3E>Yzx3PDK(T0 zw#(~}o06Bh809Tfa3W^H4nhjq1Kz!6)qAY3KVNcWubnemu9Euhc!X~Fs}Fa;{Ooc{ zxl#@rl9)*o0%e^dR(`kUZfox6{X@#Uvq9U>EnB=jP7BfC=P{Nv7Jrw{gh(S5+a@ef#dq@# zqT{;6f)5Tlt*NZCMG0e=w58C_BdCGFv@W8;b=;|RNYX!mSjXjqKwdKeRd#BC4~&V? zmTKs=!-II{j(>B{EIRYt)*^kI!7ZH$ps%Mn4*X=dCkWsJHU^Z{eakx;MUiG2^pv2M z$8D$tW?k2~AxpN^HQa*lIJik05l*V~!&~iJx}35+-n|lYn?G!xJI1S)Maxl8jH7AB zD4RtoXDq8PPfal?4(F#83TGx{-EBr}Jq(x=?C8R@`Al=fnz0d#swhxTw*>*oyoa>x(Ei#k*%2XfELqc2-0j=M7b>;*F?4Us0;vowIb_ji= z9AyykRFM}u={@v6qI24Dyhy_Ryz!CIx4jxhnCy<2gRxrIFJ;mnswAphN(KM=d{6|Q zGeG>3{6;L@6X@5hf)`s}A?px%X`QYa%s3qB_)O@3PJy+Rb+Voy5MZQXFW9l%#{*7i ztU@K@I2xQ&gI{o3O6-8{vu7a-*VrSFn}OG|-?x?1UW{Z*q)CF*@brv){vQ zXJFV054&}I_3HS_TucGgYIBlP&ecDB3=vX-*9Oej8lN;IsCXjAE5Za#mN*hVm)>P< zFI_8;i^UTigC5e6adR!!Nd!)Xg0D80maH|ruz&kp8W{^(t`({jEav<>b5OLIZahb0|F2`{ryfUNeo- z#m_zoYwIATc2c1Qm?PnA(PtLAHouerW?`nf6{UHhd$*(b-{Luq2gtlWj_6_n~l~RsDeP#yRw$~ca_I5 zJiSy40LNl)+w|HZU1OtCl`J=dt5pT>v4TeLjL<9Bq_Vc0Axc&C0}_Y=b0<^t3wZu_|@h zFz}=>#&Nf{N@ip_KDw=z=4fnp>6zcv%mW&tjP&vDLfyo#z*S z+efl4skQx{gnz60KWm-gfMp4Q9loFP+JQzuw&mcGQ(~gHoV{s|R&(*uB!H{cIeP3* z8ZfL1uWYLeRt_9uKW!*5(Pq_giOZ%tif96iOCP|FZOcb~o2UPJuyq(IV3p<2+}Z3%tZh>IM|T^6B!=E+IKEW8V!_` zlrk)Dg0ZutyROU9_Aq@5Y6FO|IN?9rjs&iu7}XZuX$9z3DxhUuP66;kxB zUzGar!>(0}uC%Gn-r4a#IC>h%DJLSrbSQq%+~e@Ka&tuC$%CZe1Vv&%)8%dDvrHHf zXWhb{5GgG>tgiUb<0?!#g7xO{_5)d>l`#*gYtKFm$wyBMJTwLJUDMfS9c4Zok?2n} zmCQ)vAVhA@%!Jefxf8$>S>`gnGb~8Cr9CXn`8$>6}IJogtSwVzbi{FW*YSm zea5svs%+G?jxTRt$-fa9~@+$>Jv;i=a2M~rO+s6Z-zvqPOb2f?N z7z#7WUor#t_ugTMz-IvURZMJ24w$W*RO(APztYk#<#`2;&a*TSV{=%aFR7a;; z&u($(owfgyYZN|qC`&MUcBkLai3JDrBj~^y61%`$mpAEoWCawH36b$39j0R(%?$ySVKye&!wT~NV-d)wN zfqhw(INN(>KBPv0dDhA86%tQ9_P!P>p2avLbcH3D!lZjzzS3BF;}_3ZO2#uD0yUlH zn79YcQOLt7t6{p~{^-g?qWZPWHJ3ta&(<(5vf_4_$vm1sx3OP`3lSB2G-&^Vf7VgO z{tfh?Plkd`Gq_sK06p1zwX@5A`mCv`6&hABwKi7{vr)-c92niMFGgYg6 zM~XvO21S*Cd$fH*$|5lp-QOEV+lBmi^Z;hhhofUgcD`u+{k|JJ?0Zn6Y7A`t5l&Pn zEz#>*&MN>8x3Qu_YLTa#frGV{H{tqTXavN>-I-vZ{Mco9?Mc38lR{DGm%boX;>IYa zqJ0^qAb^1`=3al+f6|NV_PFO~L;o(i4y5@uDM(&4ddSRE17LMv29hzg@F{)@7{H%G z#jepkQSDcEpH5#M80#y1AZKl$5FC%ah)FrB`Dmg+BD%yH6msC6-hVOw*FiKjoFb-jy=` zf}$J$#aoF#Rqf1gf85bO{MU9Rx7?_Pq)g2SYbK#MR4g3uvyi&~LQ0E}dE!6nq*VIt zs?eRUt2JC$S=|<*&XSOu3u#S3l)ad$F-!bdaqs=fICF3Xl%6+JIh8^;npdm#gSth? z>3^8g+xb`2UZ&02&Ft)(SoP?pY6|U4TYKX(q{Ntd1|{=IiNU z)G5FyvxA5e3QgHzXo{;xU_1?pq4)i3R+IAesSgw+0$^ZbptJ|*H#|5DOkFn@Zuv4D zfhrEE>oY|2WTbSn?A*RA{|@8dZl?eTG9aE#c+OSsYJA7=E3Vj zo>gubFuQD#p64?9qf&5hWyR>uT;N`F;lw`|2;i{PX7$jx0qjUt5J3GFZiWmHzIg&# zfW6NozX*U0m^sZC4qD|X3b7QhoyJiF^v&A!G64()W*7(6+4Jsdu4us;uF7)Kiwy-y z^z;3m?(vqdjC>wtUn9^VuG zrz|NjPI~-5&VPaNF3vZB>g0m{Ey>tmw;N(BL0D%De4whiaEQ%8?iZpjNXx{LT*% z1rWWueqSt#td?Zf?v_rHoE9&BWi>)NMwNsg#y9Y`Mo}M^&gb1~Ha`2&wEKR|q=CKs z2Sj8uGXsg$LYM$|blKFmmNDtHwS?2|N1 zKjnKh)c*Pah|IFpdJ4xL&QE8U-6wYaU82XlyK_c`l9-qy8h-&NWUQg)crr31p6m$`0B*%6JR#nS?= z5Wh(>X4b1YS<}kQXVMclLT-torMLV>jOE|%pFfNgWv5ZF zr4I5-vwL5e^69%gsSdRzEhe_PlZThu?3dFY0DNzM_ULkJRB#5ky#f^g9?rzxoF_-} z`DlTT$l-M(v~=s?B>*J;I{ahYA3y!pL+A~D%neU~ohksJ@Ww|+$rn@&ZQYqOy{UWaSG6S`=K6LgAQWdi#LrZOmOoVvlsSz&}46BtKAO2Hf> zAO_4vnIPh!AaD`9b?a<<1m-FcseMCxugA8?Wr_St)+=o$QX3ep)-d@;jx?(r=Nuve z3OT^u!xP7L`J<|%`w$FrkUBN+`~`BRpu=jkm!^6 zs5|i|AO5c6S4qO%-~pd|hC^=cHGGCHQzwlZ4pU2iTqc`o9IiQJDP{j$d;7Pf!`6wj z$Lzw<{g<<;dAxkB@VB&X+)Bu~iO0^Z-jX7QB~s>rb+6R%pMJ8ne$RW^mcK=wB#bW{ zvG`?;KIYUHkMZ=fk$0r-*MBAOC##guGeSHr*M|XiB+z}LH|>Rj1I|~l5(FO3dYLT} z21p6llzAEcdWn<;aVn=&H|I1L_6JypR(jXd`F$jst`QmCIa#0l&p#gvdX=`y(Z{FI z*1x5eQ{f)f-6?SDBpLz3pzz#H0BC?i3Iqr(N`4H@@maX!%b3i1l+pW2N&Ef>XG4@z z4D#;VttI4uHg`L7$|X)Mj4DDd5`dAcM9nxDNL9(eeq2t*|LavRBe0PF2g8(8TBI*U zW#5mbuN9S)l>F}e`#aIz3mD(RJsgI(Lzxg5LWZjtg9u48VgRz+I(!#Qfr)9I$S`+_ zb{O9Ir75gk+ZQ*{L3}5iampcQ^Vz1fN)VKjjf+Nfuai;SNM{RyiUny3N%dPylMMF~ z`*yNbDImuc@V$mlI)*0ZRPN#`3%6S3>S}3TC0$vjMqFx~INhRE&UrvsNS7A&t*Fkw z*WcX#Jf8o3gKOC`=bxa*ac}QXWMO|8401J>>Mald@7jpg4|H7>Wp#q<)^E#CvNVAu z!CC0M<@|GxhEhTGuxpwC@ifs0^;W-y6X_e}64Ett*+uMKd;<3!F=nPV;f*`!rtdSl zPc%Q-r*la>6DeIcmngNJ@AL3r^!%TC`+#1&vvp52IthK<_v1 zwe#`PAxQR=r`Aqvz>J}RtF{zqsgC9XI&wbMSXt?SP0^;0e{XRT3YnsTwgkXa+-M!& zhhf)_-+?mH91&|Bp~@~q4-*?C*Mo#v*Lc9%*--Ps6nwt#GD1dG9}FqVHO2eJ1q(SO z;(`KE@(WJqutyGKhHhrRlmWFg`0_J2+HQ@o_(u}#V03e#5AZP)kaBygI3~;$hQCk&9g5GMwCoxMqcO;)s;G#-=Q4Szk`nEpX zzn>@Te;A;8!1iT@YZrl{@tL3%d(}GLh$||$D+rYm|CoVewI zWe)qcfM6KRnBe&On%P2*7Qiy|fLoutse?}xhmCes-09z3131m61#7ox(v_}&I!NIl zwJlP3;TL!JM*=0e2v9KyK>>RaD7F=)H_dDyz)p@IQS%D$<%TL|83Z_30GMc&-QBCp zR}nbt$hURRPmA*?q`24h?NO^2kzxmoVmtKLVIbSOR^Lt*dKCHpMgh>k2Eiq}vzZ0{ z@hDKYm{&9rBN-`lq(KH;Z9m+5po3%vO{8W`!(f6|9EGc2!o^-*a%G_c&Ux%+Jv#^C zC!7W;O_txWMnn5tKV2w_`fbN%AIUq9Obs_Vf1Hc0X%?)UVNM8QbiCD^kj~bdbtPV0tAZ`eg4e9B<2C7;QnZ0o+M}hYO}HOaP*hPyrny6y&%J z@GetRQ^9naZo>9^UikjURp8@K#tadz)Rdzo;X z#46fd(;qxwd6YIFNyB|dg!wUiC*w%_GE51*d7k6JslnYh0=M= zV2Dg~sBHnRCO~Cg4~`>0sTXY6z7+jILMs|}g8@mKfHnx#39zHc9!1*{8r zF3_4?r}G`oi5P=Fnp<{8u0u6#aZI4G#Y0lSDadmp%yn;&e2|S#;5K8|vsVi*oSg|t zae~ayK^m4=aS#Feq&ubWY4Ondli3U~?q{+Zdi!abmXFMW*0hsPq4+0to6o#Y-{=;+ z?y)N1&wOBEM0W?vO-hSo{Pw$M%DH}e%RAt$^5YpYSJV5^RXo#%j_P{TQ}^BG)M-Ka zHWEWSqyCCPJ?Z)0F6sq5V7&-|4l+R4Db9RckM!l zha>}JD2YN>aG4SXCZQDx@buZo?HvAA1+}$4<(hxo@)we29`k=-zJD3kLdmBvxViLU z@vHNOg^e=#J6{Ispx|=XS8k$Al-e#L2mkISot-N-`duV4fy1EV-;s`i+y8YKgru&j zYU!E?R7uwF>-~PIs^A{zC^(Yd{znm|yS`ym<@`O!!ltB3>SzR+iVWwmmd3V$&)%9> zuEwl;bDV0emBX#(omAvH1)DywIH;J5SpRmJcU*#U+@o%@)QhQ#m-n&52gN1jOt4)2 zPyRMZOyV_Jfe*Uqt&qOkYTYJ10HWosu$FbC!9fo&bLF#H`DyQ6de9p3W}KN*XX%*s zoa8Z>&!)bl00JxzLDR zruQ{3xCk6j9*`ao6%m~MgtHn7iwi;U2H;NimP7tN_H8iw zy~-cc105Z$EDX#cvw=JL$E$R}Y+8XL)=tja_{uQ7zt=mG&s!`=kL?CGPhSy{+4`mar`7d`+1sw86j^Td z1dE7(FE8Q)D$NQ4oc7zcM$ekJpP!uj{O%=Q$WR~7HC$u{EH;OXH=dqse|xrl)+D^}!%V#Rv!3ATP>V$<2UY3F*8 zFx%ce4^8^{l6uy(%*ne$OZC$9Vs4yZ*Zv(n$XtsDXam5H`-gXNl!ifBn*0z6uPD2` zaB>OlBocOGl?3p2k|Qwn(2)8irXs!%atrB^QkWp8m3)cwA5^NeL zOTb$a0L+j`rq6h^S45a!aGqY#Y-@@HH1+fB(mF^;z3GRAd-2%wK+DZ|1VmA6f$ zls+nb2=N|)^G5kTe;%z9ymoG)KM{!|EsMeDF#cA2%QmrrIHS2?_mbga7~HTT1T-By z@!30HOnT}0_gIRCRQpiyxO!dSx%LgWQ&i&vx5mdiFXQ98$$#zU9w&|wt;s`QrDYM- z^PRn<#MiXt@>VrjXX0ilR^y&3)UEbkb@uYQW$Hd{Bj8;c=hxo&?=IO&^N!rn)P;g) zsV1KIV?}jl{ogTbQsdpFM^Sqds_cCVUlabEl}E$03TCfIRMk> zJtPJ&hu&#qsQdBmqsG=XvNXT{HPEyl*KsUJ9{w#ZWw&4d*>_<|W&NKCj{|&=A^CLMNFv&#WQ*mMipQ7FzR}|EGaYPa z-L}25y|=6O{d*)TGRq>Jj~?u~oo}vnQ*4Fzj&rYkTa<6O_|tW~DCNBW-lwxk;C4g4 zqtA$piFg6j<+8z%BJ*Y#;=^P`(%2t|N`YUcwN&W2lH#6r*jbeu=DWBK6f@HL=0Eb> zJ!XCDd^E0it)3W-TrEwwhvR?;P8eC*O3$S18CkY-Pt47`u(o;F{bOOmg)%YuGHJnJ zBshVIy?3vN2;I|SF7Y(1Hn<3DvE_l#&RO|*?X||ZP;6}F!}$HP zgao5slk@K5c0Tql;p>6Hg+>*@HSs@pJhJ(tRc=sL z+x}NH_p!oK4a0z!QK5jROmjsa`#FL4S^WmdIAMm`$KAFhptgE^Tq}Xn)6;nB@r``& z_?^qCN=!#Sn3C=}M0d{IFPqDvMQ%jUGD%x-+k4)L8B%=L$FLlQvIr&23G5R+6y~-c zdxO*|Vr@eGI5+J(%0`;Lq!v{ijZz{_faXAXFNfb#(i zp1=X8^v!ck=_7L+oBirOKIbRgn2O(={%Sck5R?$G;pgREKn>c#qk)Hj(|ABc42&?} zzk8)r20Z*2rSw-|lM>L+c=E3P6ocG;R%nXfSlBx5U5<6k<_LOEfSZ=iJ%Opfx0%E0 z1#Vpj*gmeX30*k&g(_2F0$wb*iUhfkg$-D-IZWg@LEoUWgp?C}tjWoywF7M0=Bzl# z&eStr2Z4#@0l6{vUMpT&xBE?^LDcEd@i=RDGr}1D5fkwedA9w-C!;CtEaX}xk?*cv zvy=?NtVHRRX>-nB_u}f&Hz}Oo{LTwyj9C)a$4kf0yR*-`iCdOh91f=sfczmh$jJnx z;GC=c5-=-j2>~GNgGCKBRLo%hXUAOdFq=^7GEUq-E=6b^6FG!QW6N0n9WD%>TPZq6`SX))KneA2bcm4jwWQ*DI1 zB31^Aa(}&Y?o<3`sU_jY{PbMRdkANcdiHzE+4%dvY3(xF0_C|@Qv~-&tl4$f9!m0d zO4`9E{JKqA{338tzZhYxXIhbZLMBWt7;|tMPc+2RFr5OT@FaZ$iCEn38MW z?Rj=XuC2)zAMe;ce*Z8-($>w%O=6D1vSy%jUKl3;LB)hQ6gvaQ6Be0Z%V+lirxOn# zHDh%|8AZ^jn=@8s0@tzK;pR)gR~7+XByAB|kTr(dfm-5Y4t-urj9C9AM6_iVC9HjF ze~7zAidZx%`qq$L#NO*~M!<;y>2uO5t4tI@;?lA9OLrSF@ZC$u`{g4YI!C(yrK45j z6AkH$DWh6xO-)VW0074yEC0)SL9zm6t2d_;5~Q%6Jsp$?98S5}1wcC`fIg`V2s9jJ zRyh0V61+Fpli(#92UjcE%kqh$sPpyPFSg%kzM^x*&jl>L|RV1HU0;&FXVUiFwEkAmvI#l$%lw)FzpL}! zef}D>&#H8L!4>H$1cQTsF|?j*+&onffx#*P++bU^&{KI~CxC&pCHKjJ4mwBM3nU*{ zhAY|J`59ceHeYaPIw9}7#eParR*x6nIa}R)lDj|+k54sdr-n|=P>AdW#9X;^r;`%= zG%JmUPaFWhd1sogog4m>tg!njHo0+Tk-_l$V#8A;SW0a9es1Nu6`z`9CMR=eeA7N> zV5f2D+2F+(TH7vmbu07W)`(L$HGjL1og-YeDl&I`9mn$53fCIj{C- zjN`C0(XUcS|h{Xy~vMAI%Jb&-flZs6J2Y ztUXG8nODXU8d@_7$FdWwUy$&71imD+D(EJl=aNpjDa{(o`z+HdWP8TePFssDmDDLg zLy9xVktmb3xEzq|%RT09bp~635Kfu2uCa=U>ti z#4nabaVO3k1#rPMxpVrl!^Erl3#`q13{25>GTa4$A)8axL>I_w;TjGJz$ILg9@Zxm z#%U`87_?71A2C8LnL9NlR)O?_*)-(hVCbLaZ!LDfS^cXzqkLP~hdqD7?ZSMS$48~uzO4+NuhDwHfawUq9t=_* z=K%$AO>#>;56S*Acq0dEcJFxS_GJt_x}^Iq7S7 z`L^ZXv==*AuzI|Yv! zBCmZ0*dgyBcbzBw)=aU!!A$jDlgwflJm9jOAPjfjWu;SG;{-aY0$*}63p5MIO;3i3 zc$5tPt1vhB(U))g#g1IJh$tE!|CM>e{j}-O<+wlf$VM);jI9`RG1C_`SM}Gp)P!eV z58Dg2uGjdz5VXi-fP*`x!6%DDbe6%qC?G*b1ps902mYhOLgkVg3)p}Iz)1>zP=bUe zZVe#c7(yh7aXc*iO7biJ%UA#JI)oc30jG5TX;H_eugCKNd_9nOXndt&K>gquvP@*` zgt?MFJEW3AC%C66sAP88<5-q{!!}TI^jTY4!O^#}^npL??HflH+RuJi6k4V*-Dsro z6n9LI9P8_|G1}GeA$+@NCJAl!AU(I0UrpRp)l2@oELlI0lYKBCOVN>*ybRfiw*`4* z*sOoLOq%njvYXPZq^hV0K|8T`F#6$dFcWOZRY7Rm#bYzSfGBE&6gz{dmMguF8f(pm zXb_Pe2}PyZ*Ku7}huyO&)6{Zet~;vad{LCpp8Y*OzqN}n;inD?5icUTls7eVBu@Rv zQRvy>+ObV6g%M_V_uOXhn9FNloo4M*P`&zO!C|psdit1~kM*8sU?3ou;FZ&8BZ1(shP%hgt_4DA$%V{`2DfS zrySNUrls1~MAB?kS)eOT;M>1UkPHI=W0Ao%{Ell(Jr5m-$A#K`zJmrB=c9L^Ym<@+ zB!9hyz;!~MC{95ONy`J>QZkIz5ZFU|(8NzU3L(Mb7omvGYWn5hNn(>_(U2&Mjp_Cc zNRbYt)<_bqVC$$g%|BcM`vun2eU4>;@dj9Tm;OV!O4NFBCW~2l6}TsaM%boPoiB_} zA60jKAwiOYGV6YNd)pm(rSWz4l}oC zVpQ&ITJ0rS67v1{zsw=a9ugDtY1O|d!x|n1{!F2GF?TS&eRfTs;(rjIjM3d*wSA846A}0j!&FgYR3-a)?VAp(gdB|L(fS4KCm@F8!cB z-~ys5kSubCZVj9UqX?v@NsCdU_-&ef#<~l8G_QasnpbG#jKUm!?suKCcDMGG2niV} z1HSRZAd4CM?Lz^^Fcl4c&;J9)M~{23@vNHPUMv}ibY@SkQZtbDmb@t62w2SB zr&6S`t9JlqB$sffKQ4-{Eq*#gb_yk->H}U3ct`gR4F?w~k-|X3(H-44M_W z-gD*iXoyYc=w!z0Fg;in18 z_EpXCMU{z5YNv%Ftt(g7n|E$`do%B-@p5In_zN5Xk%7%-X6n>Kn5ADfiCc!6*^2y^ zUcY=LBqRb&Eczav=!;julbIMwK+t1p=+!b3g?ov@(bDMf!T~K4_&2eAteDb*0{AnL z=%5}VQMbv1!Y6nKxL}+b)c&0?ydZ46eYcDX#wSXkke9{=`loN)rf_8iwZu^9#YTI#XATFeGOuH;a8!kbU$6| zLy`A)uU4_~$7Ku!oZvs41ea==z}<%7>hlPX65X48j~dt6LPo&=igHjms}iS#wXBSo6i7F91q;b_ zP@E;MM!W*U?pIL6^xsyJ_WWT=MEPQulp_Vh3I;_zO$``8M!li~(eD+x`)#Dsez$+C zByA0`=ykJM5>fr}I^lP2Gp8gnB((a6NMOit=WH?)55UrqNm~`im-yeluQllKb&HjC zaY_IRD{_=?T2yfNuP}y>Pw4-6iHYM8voNt1RdUe&tO*=hG1kB={skjRn3X^-Da&=_ zL(fu*@e(S$ouPEP8JsD5io3jmr_~*p^XA%QPnUd`v(FX81Nm~2jR*W^!YE|N;9-JE zh-8`&WUV8KPlvCp!ScmOj4l5cBzs)Fe61dIcwj{+Zpk3 zu6V(B6t;ZJS`V6@lH}t46ipA-?mr!XV-ek)p%PvF>hrRide!-ZXd-|as}5-xUAJV@ zG<4Z8wm(!DgbkVczm~cZax@$e)Q|m8r)jv>c>DB{N6_}Kb*k~}FD|;qxWWhULD#}g z+V01gzrE0-mZ6*G&f4`MvZRN>W0DmCBET#=4ECO8FF7s;rw=EM>9GX!jaw~oul>HB zjh`tTg5$n^=~|c9=QI&}ayy~KZ~l)vusq;Tx)WdVARVxVz4S^cel>Zck2>a>6eT5A zXQ3Ei1LIS`M{0o4W90#obufIc00)(`nbs9qQqybBd#}VDqP{f;pA+EOy%%W!!`6Pt ztr5F_r(lPvM;1!Oq!~M1N)ED8`HrcVA-j!XMDSdhciZt9ibJIjUKPBdg(iW68cmf` z2UM`Hpw#5dqgxUAp-A4MuTv?AQBaScMx#KN=zjVwi8IxQUq2miAnTW}uHE z*b@brZMKdWO5ctP#8o48t|q7XiXRZ&j5eTC#tf*)ftovgz?95WcA>ba_LR$6hfq1p z3Fk{V(Pw~AeI+!e|AG>Gi5+_l4sr?EEYLAN&pDH%yc)3^#Js3_!bMzof7in%9GdqA z`{c>!*5Njj@FCk5mA@5fKx`Hog&F?1n5vHieE9q~LqbwNDz3+jwcJQg%Q)>k>e*vE z`{rLTx$UGt?C(K3j+we}ya9zlvh7-qr9|{MbDJu0%N>_fjaqVON%zCv9s2nCU6}u? zW&T&Y%!F?uKk>GxQw_V>1T5JyES7noSjArg7XDwVA$JzaQZG5}5gbPP zvdZL7+gI<^m=Ec}9aOao_Tu_LXZb$n@>{s)yAq`x;Yb_@CsqpHIB!)cmy`aOc={AO znrd?WBKu&xt1p+(yk5QxQ=1t!bF@sDmL`ZE9j2!FXLfoLX^;0<-PY zjjbFW4eWB0Lg~N@kb~rW3&4HwmM45Y+)&CfU+o%B#MwVCqgejuX@;bxOW(*$M)Ts% zw@q7S&U+IwEbVWGxCFA>1DFB4ThuSkoK|gGfp^kEsX-Bi1#uRRXVWb~9OG}+-0=R= z0v4r=hm!RYSLJJvfL0Wo2W7{`LqH4De*ghQevz3CqF0Gm0O*-#hAKy0up=G{bKRZq zD174Q*7$VcaOO_$<`sJA|3dZR;D$m+kX6fd`|c~)g0EdU5mo1*K3}nO^8M43_q8h& zgy+r%-D8R;io;HfOPpHgCrCoMG;Q_r)*w*X;?d2LJC1L))^=61Rb}3oU*^tg;=3NA z0K5I%@Z?c<$ouGQp4AJ=^R(a3G^&`wgYreW0zU2AFs%+WxU3AS8%lf{`l!0#A;5{* zl?Z$oP0hRhQRxT_=KaoUHr|}Ilb1i&2b%wph}e;Y5%ozwSG_%1an~@R1SV^hx}EAxj4o1}Y$>_MyQc?jPCpY$uYg!!*he z>dc&kF;fvru+?pB3m;rDP_gn&gq2ZR^r(_EexSfWE3uwBA1&KjY`#zv0n$24x!s)h zvN-*=2icMAfNpZiPBi>_?A+QQ4`R{-y52*i&JSvg3YP?s3Kq!4AG52Hflgta-)=R` z=jUWKY>s1rR|XW%2!2a9UYy?=8P8`IHp!;%lUkZ4T;4e;d5v5VGI;aWi}tJ^`F0|0 zfoXi^r5iN3b~yhNMzXb$!t(A%TW5;=j}dnd0O-Q*ktPBl1LIZ(su~~?s+D1w{HInT zp#2oWuphMxnS9@O=U?R!-tw3x_8$GtIwJ$R?jjJ0hKH z&vGxz1DhWTdON~u(b-93r4kpk`wqAPPC2R5F0zWszYhB?IM?Dmc=u2iuRRXs}<_Qa$#vXk!UK$OTrLXmQbi4=p)`a&s=#Z6!^ zxc)HL&FOxa3pqXB$z0A;wsP}pZFd8YQ?_()L&s+Fz;D61Yh^d6dyL<1Q>P$*XuaKJ z)z-@ZxbmA07uDotpFd&8@||D_5AP@VI;r2M0xv;D=QpB270fYsKTX#Gh)5YDDE7X~ zd~*NlQRlOULK2Vwa~W!_qxedz1Fc~vli_zS5`A5GZw+`atXlmLU4xeySH^3K=ygI_ zdEY!xf_za%ahul2e`<6&AWATcg?VP_E^$UGly@r*WaKHHi*yMkqL2>x*+8=}oq zViW5QMK@m3;%Vyv5`~q8J{bDQCuo{v6aWM#91u|`L(2}+s z6)^_bq(VW=l}o0`FN%w|o9{K3|4jLAd*|p}(O0OKKGjG%Ise8ZtFFhn8xE~LOBY$) z4wjxz>>hpbOe*xqjb19|vBrd#)5NPdbo2XcWGJ}RmQrU=5yjEzGkTxU!6t?gH|6vZ zA&)PTHidJrb5~L45;kj%*K|rpHuxMex$LW$`t5I9^aV~eKO<=i&=sLy6AZsw_A)f_ z)7vZSjI>owBgwmXX4)0c!GmA3=0D)R9n}x$blmKVzFbkY+nA>Wc>QjcG37Njt`Y_e z$keb|G)oos>{*zCygCX^rChfWz=h4tW$vhK+OE{g<^pECE8Tx}U-&3e;JIblz60E-{`hdD-L5xUI~BtokS>pWiF&t~`DJSR_^7ao^52e^8j}afNMT|MqCT2_uib6 zf7hDlwOIQ5P~jpJfXd(UFhCubMNw$LD2UAEKX3gmFN!376kJq+;OETR(%xD#gYTC( zSU*e-^b2!N-1#{o*<}|oyy>RUtN?9a-n~OG~qyID0Mq7N?td^yY<;oy0erTNc#Za#E_v^@e z|JVV!j=G%Qqi3RuqCcbg7NPLVf4v$6`UAgVKa*V$!L?vZnDn3SedtA#=2r6cF;j@r#?$b^!Mv!LMZ9s~^u% zvhHd!b|s2ZL9CMU^ldAv{Z!kxr^P(7yu1edPH*@IIt>H>{HpTKwp^N4=#+?R>wCT* zfw-fdIzpVY3KE+uVfLUvKW8H2hyr0V&o;dZ6_kD$rDRfy1t#9>m^@elO7+e*Asqug^BS_$nzM6t1 zi)DH|VX&k~1w_MSZ-fuks&n7bzwQCuyyUIdY0ocVn8o4Iv@l)BE;w8`6RG1F{NFAY z0^h%kTdiN5o{cz?67^CV1TSyP??}D2AE6X+gpr3XrOxGAdxI1$19iUMYg~m74m%+_ zu2bM}{_akBeu5~ZDES|0kt5F^aBd+*AbXWAyaE~MGJ}rcwjT=1u}=zs;KD<~--m=F zh&%A?8^hJOm@J=Z;yMjP?1k))|8pm0B)LIKiM9JE_y{SoFUWJxh3~$Eddo8)` zw^|js>+wXb8rflg^O_9`iI5s;%fSRKg-_?NunN@s4&inwBS!3wcl#x5L?fQQlon=^*T^EJV1VeZCAR?e39g+hmBB2P1 zqzv69rPK@|7Ns;wsB}q#ATX$uQqn0P64FZ7%=f(CANlPy=Q(HZwb#9NUF1bMzM&HV zYT;;KK=U;r{maQWvk91z!UbP7EZxf92L~Az+Vg@JCNvYQ5a+ZBDN;sszO5DL{2M7D zL)?%6o_(_I7}lG3&~NoXwRL)p?rcQqU>_0Ruwm!q@-e~Y_MEcO=`FJU@v)!7B;Tzm z^eU-D)q-raR1ClLS1in)wJ5lB*1|pP;I~y zJOTb39tp%E&;84@;S8QZ!%sAh>$HOUd#~W-5>F#WlaE`7G|Jj{aZhamV-$j5B9w}m z{9OyR?Pu!mtFJyua%BW~5#jv;-5+4Zl<1b)p=JC1efG)pGBMsA;Nx#VgfxyEfpJFy z2-3hhvV#Hd@Q{g>H|yrc~qM);dpCC=f? zK0iKd(k@4f11&(!z56*S{(krSqMkg}QJ%zQ?x5mFxjc z^WK(|6Hz&>uDj5z=z1I66d5k}H0HUBm8cmgJ^EtT(`8mhqqz92lnLM|SvP;RECpu^ z`J9$c-7|>`^0zzyO@@8vG)gFm=^xp&=B$fht>v>7(o(Y}Rpmm+1m-Qr zG)?;qW~vVVYRsD5a9vhkmLXD{*2iU08VZ1?G(`3&CaMtiIKfQ+x>~*AnvyT&Zn8>u zA?IBES11Oxmi@!mSurSv$*ABBj3AG%vxF}^#+XHFfeY?CAN6TG)Sj^A;An^uBVgS5 z&U#Z~t4}M%+`mGoIe}^@+b5kpou3BeBH`sMl|xK`Bb_Cox4$kE7WRXi@Ton|3UHjS+6G$Df&}upF{@7~TU_P#Y~{PV zM`}|kV7pPelxM{1_U#*BxZDVQta~icNT0_e1ylN}&*=G*`T;gVYaGxdK^ z35X(~IYIYjzyi(#G?Y$-kpl$qaOLe2VlNc!pP+QKIrYk{h`8@qztE~q1KogMpJTY& z-|-)8Zg$sUXL{QmB)O00M8a$0)3ozy4SE7tBzI;f>A{@*)tAz%)Mxp+&v_y_w~}sJ zua(zk-^9g}iskETo#PydkBQV;b1xv=2bQ@XxJxW&|8;Y@PHPthKb7m~SNHWg-tkYedr*l8YmA<$dMXD^!G`;sn2}x|@ zYG%EIVUWByqxtN<&C=bPuxYpr;s&$`kVghc(8H)&_^~`75} zr#?x0nPREaegx`<@b97ev!ZU8RhSceo*Rxcw>QKZNVzMZ{(VY;oSD7{%ny!eSxB|r zX#Qg$y|qYuaM)h99s8FjiNbqxvEG#8@F5dOi%9wMtnLjTFr2?z)jcSq;PrF;tV5yY zub0XK)KdIDEXYXe6M;c5>VFbpD&#OaF9x<0YJ-6=GJMGMZIUHp7qw=>+*jk zZZN>Zqa6PW3Y#c>Aj($if&|fD0f(zRaY1U0XZqtsDd*U$szp&jr4MVZkuW6kNnx|I zLl?0+Z1}lOxb%$7G8sit-ciH3cb3Yn4PTpu(sA}@kpr`eNFoJ3`QHcs9f1Y<4E5@> z_QK`B>%!u+;(f9d*PEr+vTZKe^p=K4yQJLt)Bo3uhAuPXr`Noh7##IV41UH31*gQz zZw%9M2h~);I;t%&JV*~{VE+BkZ18efd*0c~%%>6F<7vBcU4ZAZfk7kjj-OpUHC62_ zqWKk1ji>|x1G$xe&SM0=DnD>P-)=o9YicndKBKISq=_ucA+k$QijRMNn0dXemGV&e z#?;;C0g6Kpv-|Ji(VtRb2=nw>W($pg*a(}Ib2*DrRQP95twqTQP_I+hQ*6E=y!S7( zsg1=KI{bPFo?;nncscn;84HQbIzKA!FbHj)^^ew5_=y3AQiI9+FdzbL;TDJImE|?F z#mW>z)8{BZ4E?T@s1;g*X)xRK$?5djd<0haDqBxUrD%w~O1C_atLUGc*lEQ7@emv@C z!q;*5-NhU~ttjVeIkOA2Q2LU)kud)6b5FVqX^R3JMgmYM5^4!&kOhzjH4vHxs*~a* zA?fc>BVHH!qjI|ag*3mvF?G-AiI`Q>JY9&-$0^YlVg9#2I^HbWoO;UzQmVeTuL?r= zK@bMJBI08E;|u&AHpxJMO2k&gbQ^*SVdyxPDE-pyY@_t6SI(^7-Yht3c(m83ysg0f zsf!(MUbjzOOY|O3^e4LecVt+V{Sj{FZzHeXxlLPB&oDhiC=_9)_W$vT*XgE@y1dc5 zc9DFPTQnmB_9GspWu8G$`jf`DF#*&B8}XVhFU^ll$Y4KT=`Y7sUEBQjSLSkWXK=6Z zG~;bOHEGut2^iiES@1L*(X#?8Z0Ec(@?GcYgR-|;&^BvP5g11Nr0B)wcFdwDZXW(5 z1;;#54i%VitI9~x6vjiroH3stQcRb=a30ujsH>DaQ8PH_-;7zuH|T>gbu;Nns@OZu zBi(2q|MD1+v(gy8#gHKeq%bsYh^272ZBUl@vY|YtnF0Rv?Gk@Fj+NprxUCPn>dX;U zmCY6-7=erUi3GBT_lDf6u2-4$lIbNw3uOQv4E~gcX*&Q= zD4apGZ2x(C)Sh!nJv4IorEX)kLxYH;^mb92{O~IyCKL~~JEFi)&Ze^DY;wU_?sXj` z?!m!nFuzyWFNjJNq|1vV-?XE|Vpjn5~Q3Kd1B zc;0_ByCIqa>~}4%?N3mDgjx&jiPWcFKcBs}{XT2*_~7=`9r&cN>8`hhp&>E!pZPH9 z$y_U?cy3BNg=68_a);?T{g@B#e*AB65+mYs8Dp)#ekRFVTL|nIw5ewX_>V7`lOEHx zwVKL_Hu<3LMCL5!V(hAE%|cMuTnpRmqVGiJiAu?tBY55B<%s&v{USsesu5gvtp0c3 zl{q-UP+G-|1dIqCbVa+)`bqT$wTQ~Dd@-UAHBRa?4nnWEoQ^sM9=2R5xA`?#(E(l9 z8h95NlAT<-)o0&q++r(n&U3L z@rp_PX_DC5js^M}Fb)uonONkZM00Y=+J zW&zviS@@Vv;3qf-@h(VG@-UUao^?k;EveElm1@@3ySzrPZa z)2Ee4?C_z)j1}J-A_J#;j3b3Fp9=o-#*gJDyLx7O00mloMvLC-wH^;?U6ca1nbjP%Z{y|Bu z7@-3YziP<)4YIVC(OX&XqzfZAG0bLat_7!YSyc;B( zKnD)$Znt-bZsG2S<-RUrPJOsc^``8i&#Q<=FFS9sCvYHXdh-MAeFg?ITEh$-kBCf> zU=>AWj_T3B-vxMt^3&fxV7y_TTx74yCA0D)^pbU;i`vak{z%(^!KGWlCz1c?r;K$T zL(gNo3;o6^dbpi%U9^dTPe!~?li{aH$FUP?vu9Q6sYXt&ruF=%tonA@FU;8*kukIP z0P=akyz#q?DgNWmn>Ij}_9Y-(7q^ebOc`5PX2d5-th4jtKN1z}JX|RF zAlkOM=fble0>V&oBC1u(ILW^(MQL=$A}*gIt$8!UQEiPb)`FpmROgtv>*iQ)_WnDz zwY5f*Mk9vH%<4gh@38!e8%4Qy%*Z1r6g?Z}sUg>ZK1@4>?5k!v74mAC%w2vwFCvLC z$2Pr~6_2qt2-g~+zEPtO;f)DLog5|{f4@3z`b*6jcwg=8LXsxS2ge!|sPk%V+#g{9 zX1J>Cp-@L*Sx?n%2A=%eNbesKjKlD#!M{V5#@6BD%*whZb}F_Y$6#M=0%1HSakUIg zbgKC_x2X(V26<<^*JWQci;V!xUep5^j13BPpur2m;lptjrL8_aF%!f)VW@gOYh zE4YxtcDLs^()3dL9AO|%-)d9s;ejg`4F9@c?PPtK0@yUQvaCkfav_ zyLT!PhGS13$D;CZv_vVB2i!@}yo=0TVB}82R{=YAvYZWFn}3BTi(hy}%TLtT0`$#9 zepv||X2;eAHlg>~SN)c?c-3E)*^R0r)mV1E)12N|9D|Yq5FyR6Vi!8umoJY8$vvWR znv~**;A_dk!)RzCihhn(-ki)fP?mEblSJ#Hv<_!~h+^7d2kpP`x0`Cp>>M?b8@8dT zdTW2fwjPbiT7@-rk6tm7!Wg3lAZ-^_mXE}Assf1-&5vq34;&f^6Ea(k$E}l582B!U zp&?~_->BfeAJ)50^Ys;;cs1h|PS?7fmA^NwROcoJMuq725ur zI0h9_+v=WDjh4f#Np|{^MLMebli>2#gRwP9U?>@rS#^uc;g%c4xo3@Idr9Sqs*=~b zVN?^h3!eC5G$ikD^-_Uh)`EIujt0ONQ!WmST~Tzr520raq8ApQGIXpjO=E$w;6)fA zUDG4iwVIkBm2%4vX}Es~0MFU4RoN7%vP0+OcqDhL#(`3sTQoBJVv}$bz&p*dV$TxH zA7$BZ^Y>7|oK|(kpgf7X9;?()xyOJ|cF#K0zcgMBQZ4H3Xz{lmz73)WXq!xDgyK7d zB&6QbfWx+HP)h=`Pk#%m3)#|SQvw21Qwd*LHX?^oFqIbKk}hBKu>T5Op@bNn^@NfE zJ*ujGepShEb799bn+6GL)MLT=kp z3bZ3;gQi2#LMkq*K}*CzId*X;2EB@xoNE+*>v`>F>kZ@;j!8@&dAd*upm;Q$9%iW=}}iuu4Jh>F|G=9-m;C{mu!_6PAs9UI|?pEGbAdm_>@U&I$u zp#*XS@b=r)96P^W77%e833Mqy?oDI`7!$bL7m0#w8ROp#spdI>U1h&bzm|6*6K?)I z*@Py8UcNpQ(if#a_7Ga$p3e$QV?LUxR`OVZi`OGzbJr}|;44X_kIL^=qB+;Ea^f9H zjHT^sa#P@y+*ObhWd#Pwffxl1_{hfO0p2ER;D7jyoCBC^r=QeTmTCa#0D$JN>Rsxm z$4nXs{N$xw7>nOEw7v^HCilz2A}L zK-Y`MHRxFjV*-bQr1y7BO!{;jqU#otN-nnlmOKlcs$pfyJm(nOFyO_+{j?atz2c>^ z+>*J|{ETYpQU9Mv3`g&oB`I+@{JHO!$Fy)<;X&uQzA@zUH_xw&|6z|S-nl?$eDZrF zr@(DSgr}>e)+;}CsrmhAv#R5TRbstMZ|msVMXNv?=l;Bl^XZ{|FaNIhA9s56Va@wL zaN>~jYw!_kL@4TC1AoMkbSMulwbO40pC05KT5irBqmfVrXF*cTLal&#K0c?8;|>GC zIAY?uRT@WlohsP^(5O;#r60j1f|_ zP}~P5fttG&4l2>?Z+xKc#cK2ZLA2hyz|C#ez|zxu&JLy)H8rgz*Qh5XK5gDEPcF1N zSa=MqBSts?1uv^6{>zloD2JD7r(N7ES@wHOMwPuH`NA3>9gzjucDoZa@0)Sq*Q{f> zV9bL-c%<`={%K( zp;#hAj2G@_qtoH>_J-d5xjN7lU*Y<$XBM)kKk0N#0kW@P3@=<0|8xwVBV+6JS3EyE zuBNRlH$1j{q$a$voP#tuv$fk`Hkni6+?~EVbQx4+<$RZtV}n_4bn>hHzU`2FXulln zA#BAJA4yK%mteX>AJX&3uAc%b<1syu5`m3^Nu~*MHedMqgHzBTh&{y zPy2>M*`mS<9BnN6vKlHgyd^p7_ivQwaPQrGzpM2@qLc#e8>SV7nQUr$J;Yf<;byaS z&!n;bdN0lDOvX|6*0mOfVa?oygaH^f zNL~6Z+xceq@j1MP9K*0!jlw2J1L7!kc*pcZUQP z31)Ino>)0q?f5Zv+LX1+S(Z zui=Icb?dEF;iq8~5fab@Yg%(gy=tFgj#btM8&8mv_aC*%Uyp#^pGO?wK&me?_p`!> z;419y@x3S6T(C5RIhWxZGd_S&?7*tatbCv_r<8miec9NFCkP2sxq-ilq-CWzzY}hU z?I>mMaI3m&4s6BQl`pP6iZ#q%>qrgWjs$q=FcmQHMwb>stY{dfWp5@(@mF)4n7=na z$xY4>;EQMl+%>Hj?oX{?p{2Py^-)^bLZ)_mm_M|b0)LqUWL`4<4hSuG)Ll3uNECpu zHzW0k7A8GfA@jq=yv~uoRle!I-D$CK5G{VB2nkHfSyMr&!26UnKMwV{`Ao}$D8Z9l9p9=aQJ zy#$K-Vmz>+#E**mRCdDqyGs&Bkv;MWb=1R-p_vLs+urK^9jbohpTp`;U%Y1}CdyxM zysf-hWROdI(Qd0KSpm4HFD6J8Gw)2C%UK)T;peuke8c~5m6Qs73+0f4FHWxlpKrvR z%o?<{<_s82;tGFU!+;@k(p?(jcg?w0?Z>AzS=k!zY1idm#I!<$u3zu{=kq-=`HxgR z5H$5iAslVtXu7Yb>4%Fz z>t-gST>ZcomCxxu)p=k4n#7)h35xLT-Vj6<%oLusT<=VM@8DJo z%;kct6ymHkxs= zdvQGSDXy2NYpdTe+j_LW7C$jpO$=%MYI#TNWR2~(eN@w8pvWDOhO_%cSIc=w@d;XA-fzz^x)szhNG8ptI9Bj|9l`&u9ZCWGBW~04YDNg5D@6= z$iev&(jUWkf#q8Y3JSQ+yzPdEg|vz1m|#)H`FGs(FBmN*w7xM{SBw> zfK7s`^0|yd%afKi`Gv5$Yo_oJIpCf<% z@Q3u+-Z=H;*v{HS_US&+7YT1u(5!8%Sp%;vPEPc2J5l4=cbB!d`GXEph1^a#cfU{A zuZ+m9I6)3E$3zEawnUl2re6(T8hzL#{~ zJ8V6i$#@&5QuVe*td+8dg6yJ|xty}*6y~`-oMui1`v_b`NI_%eIeo!i{tvf<8cj|0 zR-$Bf=yNDLq+VDUKA`b-jE+MCSTb>~+T#^{1Um}D{@G<@f$a~^6ErJwG%oiJ5?aDc z>CJM7($3t%FmA5GQw&OaY4B(H(7^$DK4)IGV!qEy5H_n?3GM;!ka+!6(c&vitFgw` zaBl-Wa9$w%QPdHl?)ILk<@a*!KKS;WLds40Mmh$}@R0^Un@a$e=VomtknAfnL1j%-A%p54RHis>7RU--&Z1yf)Lri{n1W)RPTh9=N}uJoKT9uaB`f z2cIn2IH99>C|Q{woEY&#>;^S~$?OVCii+i(xHRF|>lPMMfv;!)Ba8-Od%97e%T9wF zn~OxxGU`ItB8Za5SN_*)T!rK`PcJXrc;2>RYf0Ofp(`cE@bY3l-wW`HNWXB-UhMkJ zeNUDwYR;Eu;n{Y43Ur7nvOx2LtJPcAiv^}$*F?9+M0ftUo+hi9Hmcs?XCC9CW%D>N z0~=AH!yZ(6pRDVVXpOYOv|!} ze~*5}v?H zQv~4v|DmcY60GdzD1jcLZx(~0K3v41&W>6R42r^a{-YJ^kU6r(Zn-b!+|QR9;BN2b z#bEMi_lsh@+R8)RP1MHv5{0{pIW;rG0=u~=Hy3`7Gg;FS>hQ-LWH1GLrweJF@+CNf z3!^?M%g)mO_W>l_`RifBxRd*LTPiQ^lGgXJwI%QiaAZY}bWIb4`{)_ z$cWr`=v>yn=SzIo*yRdpI%{99VqEF%M|Y$MeOhgT1` zjfSwzS5q?YKYMU1Q@4p;B~eAL@Ix!zw&}&&E}R5ul^u{9j2T~?$SBc@DLYYVR(r2- z#p}zdhazX!G(U4n$1^V?P55atf_HN%U9XR}?N7_2&8jBzk;Tg@A3OKXW~%Aj!$>{{0UCxPM^k+-c`;x$MD`R0&~ii9{R|Gy z+SV4i==Tbfp~)c|$*2k7T{JYVar$;cJvGL=5MY*nk1%AcKkmo>HC|S=0Esi!6*&Z} zZT7>oJ@)Oh!-v}Bpw@FFU1RNDbF>}nNn;p_@D6H81_M~Q0F+)?@Z>$&i^Ys3FH4CF zH1u#s%S%jFph3PupAL_WfMMGAUAOpqbZpq6Vl~R%eA**-#<{js0hnQ}<|i}z++;ss z{MUo4V?e#H!^RQM8u`@nP~eiU*muk?#Q9G6nfhUOvqL-ryB3#T{SCg7R$&XUktE~j zG}G-v?aKQ48&(KRN9~#Wsa@Dx_v3$mY+oC10a*yg zDS+fS6YyGxV|9Tom@`&YRb7PO$#jL{W*IoSnM-I!cyD2+12<+rG%v#GRdEzXeJtYK zhXr`v^4ARtv5J*Y_(n~Q2-oZvBa*L3?!S|?<@X)@)a-@&rgM1L=NAC`jkT);+hx;{ zsOCfdHw925aQfN(-btR^AWSptd_~xi`VCsIT1y9~3^2wQWp${Lk$Q7s)J&U~5{=g&*B_;M)3>2~~v#BagOxd1 zuu#jJW}4N=g#TIao(V{cn0APazI_%`@#Hbv^W7rx5k7hJ%HCIS<6_^XmQ+{^1sj8i zq1E0q?a zeZOpc*bZ=>`EE?hk{+GCC1W$;AIf~DwEG`4)hzNyk{(kKFUjrn%i}1+%Lg$dR^N$d zJw&%Tui&FEHPNKsrPhrl*Bhsw@J}DFaMG6*0A$zlIl?y<2FvUQ{@d>g=5Y@;1l+`( z95sXLZP%Z86Mxq@jDiZ)-`xc>=>*8?D^ni}H@1Ck=PjF{sgw+RyqW>kLiIhZuuq9>|E zD8gsK_;qOu*D|Ggq*IA1ka<=azAF<{ppx~6+iiwKd8BT)q!~|z98AH;fUV46EBs3< zw@JOvffO6~*M*3v+@blu)C~XKkQ*CTzZd$>gB&YR1&3ZW$wAb1+$1nZVHjY;$5aR? zW#|h62;Y2T`cfCnjp)*YGS?2< zMz3w7zDC19wiwaXdH68mf%7-;ZcK2%Yky^_f#Fo**-3iF!K+SM+Bj#Ssrg|8hM+;j z4gl*XUh$1TodQ4B@H!nRe1zPP)Z(g0(vIdf{VIv^sey@e>>EdpuMDTbYhJ?FIx}mD zZ(ASbOfUS!c0Je+*|-sSBO;UDYx;EDqQ+`sYBeZtc+T{%>nh{8C#?GwFeR5xGGWdU z)nZ21P>4No?9{NPk_>|>A#?)CVva^n`2BfrwQLLqSSn422N=7jvncDa1YaG{ruKb$ z4sTpY)H5LCY{C=cQ^BHy0(xj~>f!XO2QS=eRX2YPolF;*Nqk(eW>m@_s{!bW(Y+kL zu+UQWgl2mJmj$vT9M_dmEQG7~)+RZkR&_`2Nn*yLu(`WV#4=UWuKin#Pzc-V*`J3p zJNy_ARB>BrU+mzx@5mG8@k0JqnRQ)&#qIDi?h)zPWY5t4vrDokw@9YIX{Y5u^aYSb zEe=9GDYIVtTH$>*94?vfF)K5V-E9uAW+YKo_7&ZugQ&HFo0;Ytb_U|G!%{D;Y%y34 z1^U$(NyiL8w;?Z|HUa~~lSGq1;amg^n*W+@89m%>ZlF(>dfVm7jZyqXN390&CIFv! z6`<{yZ{Y!3H;W)VTNTt(TSWvU3FatlU6bjHa=2jVkj$B#wwn7HXIdyt<3TEYE-41> zS?LplBiI!iDe&0`s8<=t zZr{tWoF3F~?}6eYw&OVix<3gXsyDB&2EO0=<)uPUs^9lGy6N_EvB&uu2Q>>yw}xIf zhdL*Z&Y1C3^8HmCLW3$ApQLrogvGl_Qi6`SXdbvTx##ClbgOwGBd;~ouCfj>%$EDi zlb)-lJ?VkR$Cd2F8-h-%o>DNpAhSUJ%4G%Kz}7`_-D_$L0fgkUjm^{i%+25Q*@2z> z$BaCZ0G=nMg*L7<-~2*Bb)FFMHFTZVb^RxX0l&k&CJ<|;DUJLjs?#-f8Z2yJru9iX z&7XP>NfIWj74`nEe}VVG&s#=@W()e*Tj^GxL;g*W$d?)rwkMm}8*vo0wHK5Bvk9-N zD7*Y(g2^u-0T80ET{wTD?jQ~zy8O0m8k&HCP9frmx*~qq`f0!YL&Kqn9#t(Hr;^Sb zSOq%v9KJyf^0Y-s(;HA6J(Bn51zG0eFlPog7BfjA&DE2rkMSRMdfCW6 zMLOy|p91^QmqA-<91PTr$1(@N z`^vS<7+0~ig}7_(-9!5)KH6Gvh`*$RAz)73YREn%`oE2L^Xj1(e$;)169%68A7{;` zngDEb|DjvY(S!~H2!WQn%}(GU17t(PQ6C=ziALXzzYFw1o{kGWf3eT z>U!aJ!+FR=Mi!{b334yJG%3(|KXdgFzvJd>+7Lo}%!^briVa&GJ;QhFOWC~saxdtv z65+Pbs?oEV*Dm!2&cPr1a4G6ViLnM#jy==z$E=d*Z`|Vqwm~m=&1VHio(*B(;uAC9tqP zZy~3D)0>O{y@Nq> zfpBW|nYEj0$M4q8R*gkU{#?FKn~#Z~M(N@4c*T91P0mAd!H0w?7{PAp=9SKK;qIA5 z5uptRe=%BXog1QJ!YfRI4cI0fZ+^Qguiu`@7>?{{tNMBrnA#SS=(D?Pt%isGBN70> zpEx$1y+7}K*)q)>1t6V-EL+o{VQg$8eem=M0ly zxh(_e+e)B`krM0MpAAS@X&o#?u8a$~?-icZxVp?udWsZ!@7bwbW#OHMAS*LM$K%q%*_ip?8{6X&M?e$B28r&z+=s?#4WBtNEPmWWv>i z9^E>dDI&_TE#C&qVxE2JfA)Qa|5BNCAu(NvNAl+uO%Tj}SQD#ULh2)hua~%RPV(5_ z>1gChgwlb(19lo}Xs8Itz;7u@@Cx9+RtVDI+iJmi1jaL%7D9SO5OW8rXPRr0M8#e} z=w{!Cd^G45HAy>+ywGhpFurOnHU+m~y+J-bd=%u_@{#QmkCWTgR|G=)ORl%>D>^T_D8-SE@l*1y`BP-`>V?F_ z?5gS$gaH(cZmHA;J@*t+1>oa3Tf3yt>O#6OZYXX&fr zmh3z-#_+jL@f8vMQ6?%XFt4N{;%0Srko7NoOlLS-0zKlh*J-Agu@c< zL+A~h!GAH_^Y-714iF0tMvWSJPM+{62MnQ4Ud~a*cz~h_Bko3^(jfiV11yup3X*}8 zwXl2|AkE!fEN69nxJRXnuVF_lMb*0w6G8ULea><|wSJr$+pg5GCy&PNt8#W#->2-lP(>;{Na?Shefi;gmWGI4Aw`MnN2I!Q!EobB zds5{yb-&u`?0_jUFm9}Ux^&q3?cr+w5$nuZPKu!$;j2A@Um9It46Yib&eQ;#n zxGQ*H?UyU{x7aTe0&J-feoh=nxK^)!n%+yTzLr5bC^=5hoG5AB7*ZI%#j?*-)LzUENDxJ`omw$;tq7CxCbqExx7>7L{c(tSxmL ziVc00+d8zep?a}gk)ZdKedx<9$zdV2!j3O!XnR@S7NqNfI z>|q8$7KXX*-6!n}Ss&WKVf%duYh0hGeqAL6LiAT3L;uwrhqW_+pXPf@qGyBrc?@6| z;Q8-xT0$iV$f1gn0^2dr!1%OiLv!)VJF+DG<4cKEUo(Wt?4VnG!PbFcU*NUXVMEt% zoX6#hLj&R>DVm4z9d)>lHoQ-`Zcfr$g^+1#<)6TmG(^*vy>fNS! ziix+ny8gSCn3(-9rX8_=pXRa2@so0n_ANrUE%F-t5s6KpaUIL{*v)wi`2ttrUH%&0 zg851xZajy7{&+_fRDl6j5P^pTJq<$OYFwTkMMd$@7>gZUj%<{J086C(R)E}nX3E>5 z2*C3=4Bqj5gIi{7@6^3h=PFJCgujO>?xK&a4?0_q&wslQM-Lc8;~Bk%JA3WicbsA1 zgpX!7{ZIT9&xO@f59KdWgE&97?_}_Cc_%5=5Y;xT%+|XL_HjS?0W^y~!JWJ~$=PFDa8@;S35J>3u3SDmYO0b!Up%_$&tZJ)z5k-FzrM=5LItolb)TVZv!GCWJGD%RV>sgghHb|*C=3=B6LlC^ z3?nc>cFr0E9uDlG5eE)(z(psII zglPWwRw}YS@NvD&z*_7Aj<^o6RtDzAl0vvfrkg!;Ks>@4Hs|ajvkX$ z5q@R(fqS=lOiws}ll@ZgA= z{<#ox;^y`wl}0ofh*;9Wabw7UkubcA32#0EoTa!_OKrR|MRGc!P=*KE78pc=1Hv%B zVkhq68s-(D1J281P&a)-}*w;gP!~GIXo;*z>66V<8DCm>by2*v_V_ZVC%L zymFN29uQ`12cM(#2hvSo#a}MHl!t$9SEonD5?WUo_AfD;3gAzBlb~PzKbip?S%3?i z;QqDBY}njgwopp^xFj4pyZEXP?^1ewXgzH!2cfJl!_(9LS$jX&KZ%=xtw?msj8IY? z=9cRPRZ8kaGCro&wrOBMZ!_9@Z6eZ^ax=fw``*;eMp<5uHEUnx>%D{PtyH9EH=;9& zNkZ;YqH>cI#3}uzR{yuE_or9me_b{Td>Ayb_AcK0d)|g({Uh>~msY3hGP|^eQ8&@Z zT6c$ZF}$|Y4X)u=WZsxs5D=HO;Tm?B+jOQ8)bS@DxnN*Dv$Fa;<)TjRx1qcYek^?Tq``+@1CW>`DG0IiL+fzwJ7l#6H(oy% z;Yx_{jVeBR^kJ<@QvF{l#n;CB(PTmwNl*;B<0W;wE8iZzW9z`Q?Sl^#VtLDuA%J^k#-80m%4E1xDKcv4X9e{E9 z$wHv)(`vq_fpd;*@>!bMrD&KvB-{-RPMi{=!G{)~;uJN`$hqQzJ|`nnYqV36H2HU5 zxXh>{?DPDSb=m%M-}?C4Ndi(J>OWWp3OhEhxFp`Wn4M25s5M+=NZJskFwIDJh-lz7 z1@6=h1sFe$e3OPXr&)u$7L9dVXFVe~3%lGiuU;^+jY%?S!|Um1737#->iiaYYsHN> zenjmL%J5`{ziDbror7{zJ#LNEyf#4JaJU{eb`WXZaG7%Wniko&`9V5k{qMH!2I`R? zV&dTDCJu1iw4*nld7_jZOYMP-M$ts@KjW`nIm)x(5H7MVA880zusNYI8e(Cdra ztRwxQM-rgdSO_HlQm*N0EZ>nWUDVi?vS7!XDzEVEvOlDCR-k~QfFfxJA|c9Yyl_p2 ziWB(b&X7nxR$zz%2-B25e-Av^4L>l70VEV8W-cU2gSD8}ektn_!wNbWu2rSPL77gp~kP;UF&mCP8-SE+MV= zP-gqy3(-0x?6(y2)ukCnD=kRLtNo0LlS6PQL|}z-0jM%=M5! zM-t?Wr1DJR1bDkR`~u%TdnpB4F2vMHAE34T{L;(iplE_j6!aX6z|h=!&_uAT6PVx@ zmOkeJSVl(#*_@XkX86Bl(qvwgp4^NCAv!J~x&eW0ac8v{gR+D>Uk-m1bc~-LEB!R4 z4AcAW%=vYiLt4)?RLz0uhFPZqp|pdmD`0a@@cTnz0Bj@-BC2>akPO z-Rudb946K9J1Kai#&qkFDT9aGJyqmn&%VMlRMFI+?*d3Az{izg4xs#G`Oj+L>_H-L zd47dT`jxkbEUcuvECcbG=CURmKTPFpOA}zwH#RA(DP!}Y0u;CIMKwKIJbPJqsz8aV z$qZnR;`II%+)pWkZAQqTolzAkSS-MdndzO4^{7XZD&$MjiNr@!-;K^Ew7unk-B--{ zX&U!=ed$5#m=R*mYbXz9$c_NE|60*2cf+eI@?{8&#`D;HC1C`9#`o9R^yVM_*4CVk zS<~+Z+uUvmQg@r;Ac#gx?X<%*l@f^QOF$cN0PA`!z(=SYdMIDpWfp@jawG*1VMt!J zPr*EQp#*#8Q1IlRF`$!5f!!K=oWCyTA zn6)>}w;;IXSnEM4r^!pkLa0ZSZ@agk9_c^DRO$J;yHRLw7F4`>&4Pu->8APj%^*#& zu7D3T3^Cz#fH#y8;Lc5O2N)xyp%~O*xB|llWL$mrRVcw51$u7&zPk0hG*M;jouk5b zOPl*?fX43wZc9htPaOaH8Lp7xLE62tFXOMjqA006Tt=Pbo<*@MSjL_fy*C%&4(qOM^CBGT^>+!wlFz zDg33_aruYpTcH=y3PHZN&KaJINNxy-Z*A~8&(z9Jz;i3i)J8LJPBSY{`~09$lz${% z^;C>_k-^<^0O{%{86;;Sp!tT6D} zhzj%|5p#u-qvMr!2GjCsLOc|p8;;$E%Pe@DJvQKPYQ@i{g#VAFvkZ%>eY^fO!O)FJ zGfGHziol>Kt)z5FOG$SOAc(Yrbc!fScPR`8NJ>bjBAp^#^X&Woz90F*F|haSeVyxE z>$gzO@w9@bg40B2GHacW>0nUSS!Qz<37RN2=Dso=?w&Q=H-Z+2^^nS@8h~{|dz?=+ zNanGZ?iyP`{CD8bt%pF*bw$kILCblae~ zhaU?$4%}URN3|V_id_RXsq`f~<*4#-h4n@w6sD=+>+3!KEB}QsE-XXy9}N*$RgqyK zu=@!m62jD3G2@UxM3Vtj6Ip4VSpP5_epTc4$9L#^oBNWAgx7_u2y@_XxuMN}X96w6 zQTRTBlH05Id$mXIsc<|JCll+Q(yrdtT+xn9HK5iCfbOo8i0kjx9QneAwwUCz_wmnH z&vrf#^K^(hS@RcapHaq%-^(Jn0nlS6ABS^|_MN435^ELb)~2}2$pGj}MQW0UZw02o zVhF~FDi~1>R%ZG=P0!M>tx>OZE^^tMO==-DwQy@D$Mcb*Cdac1NY0Ki}x2JdP6%=pm-lNCW-58{=l6`^Q(DmqOD--1YWwSVSz3I*Pt9d*O zkj2qpAAAGYcHuPW2Q*v;4*uNqcCQhGu$X+n3R-w07G|2ge!MaK&u4C}+tyUr6?cVI zz-^g(Bj1MhZRU!-4aJ{8!TcxUz&JOKCF!qJ#Ch81;%mDrzr_OtYpGG^*vzRgUBdct zIq^G?A>~4m0mRKVOMfs|r;w?XoWG!s&?0_Yx6|Hm8Hp&|537ujzZ)qVODvlFB|yIR z(y72m-F{*3@*#BXkK`V-9x}q=Fk24SwBOI(Hsp?z_1?=6&8+*H2BYiqB5ZH^Y_1sA zUA)v}Hoe8_;2STp1;%Ua-Z1%G|49c5M8^4YEq}sfXdnfk&g`}C@{c#ppZi~YKVvhq znR}wJHc$~XeGqI{EB6)y*{4M0`AE^s*vv3elTzBphn2U2o)VK8v)>0eq)g4>BiH$` zd7FX(LUQZ}Z#Zy780qPEcq*m(+`aC=*r62xqdFry%4^5?K4Z8WENukP7Adb@jb>26 zG@Y4oJq&>75#R^j1y!|3@cf#6m0T>EsN~@u?{T5|yvgxbi8=^VYOr@UcNmY4RFgSf zkoh9K^NI!r2LczLPcY|4n}?n0`JGKA{JQdU=5p|0AcS}mlAliT92-02Rz#6g6xF_IFs$3OnUgUPKp3rR zh!qgH7oWvz^m-UzU$m^1n$5l`CIQNm-*61ndbk(>9v2H_LNJZ4_fK7j;`2!|sYgHd#1MQF12jyGg(=S;jV(7{3Pyfo%1=MLC`)pIH9TgP z6mS1(yDaM987!oH>3KRhHg%)oDPAk=Ba0?NPO)f@W{7bsYT%hJJSC z-6w3!znz<9CcS)nTUB32zc0p?6fq%TUcv0s*{M)O0EJhBVXn^;{r*65wYAsBe2sV_ z@CR{7;PYui8TO7;qybUI#Hn!OF?(m^!6n_NNVR0sx|!Ux%abpE{a4CO#&153O1ty) zWsC@0oefE9i5n75QbeGATND?M^POSw`{f>Z6K19u&@meJxLnSp?(68)jd~jfP*(Vc zsh%1kaWH-$`YCl5oXaTTJPkWo8l6Ai@;k)*Q+*SX#NH@m2BUI67tV)sU`o0`iBH(; z68e5e*uLmsoLEl)%(b@j3w%%xlXT3)>n!V!XDM0U`She=`XDM34)62qPf|#^2|4M; zg?K)IrqYms#bF^+cD6Gz7R$T@@-a&W&1Or5v;tUk$zKfAoh&=7aA>>tC|6A!459&M z^yxQxTq76o0Hqy@p*EQ*r3eIAiLQ>jOXdE2fKqhR70QB+P8lEvZ_wDkF++-o05Nr* zF$RFEven$Z#e?m;lvj*Ni)?$KttnAOCTIy@WzUS|9@$-`>ieQlz$zRxQv?18B=G}W zeY%%8D*g8`jbJZhBkthBx>95Dn&@)MRK%P&Y`6Nc75F}18w;QlW+W853;i4kE?mLV zEp2ApgZx_odw=!eKzP#&qL*5CSQCT0pn1Ye+|VJGLSG^4lR45;!0bFI@2F_0r{`bJ zia2e(mUfNcQ8`GcdVW{k*wkrM&Bogk+oGgBpZ41D<9x_KeTavA4}dR(ixMk+l^}t|{__3K(-2z? z$sevhVIi%93B_bq5CmuYkMk=bF2bBo(UoohBws7ww0Y{0#e@K45-lZ&f{WrJF)Xi( z=r@lD2J}83g(D;ONp6!(pG%|PDb10^N(>u@4^9t*3*D-n_rc5Nj0}nm`jW(agVAaN z=YU*0AB93EcJWUxAx%a_!1x*0Sr`UjAq0|rv%D_c-Ejt5)8uKh;e^H|Jp>@+Px*kaBDpO z9Xwe%+2Pw*u=Mc>B4BIC66F(mko0Xo_@lS{<-k#o5GT=8s0I7%WqVD<7s{@c_s~(G z!loFWYdd8^jUm8$m_K$%Y`c2+|7{(WOdu@B7D{3N>?)~-pquoKFcui_WYx)6h68G7 zpNRley(kdOR}byc&#?NXL{>8mPd~H?AdyLq(X~6UTQiqH~Bv(j3BRH!^F4 zEGB3qRS=`sva*y-;a0gQzLp2}W4LjT@%!d$6+XFYlw51trPJceq6T=wQ~aC3Gex&+ zPZ#o?w%>S(ACE6QB<~388kcIN6#6KA%Q^eWoNw3Vw!^Waj%g-zVpc(t%zMPs)Qz0} z))d9vA#xd79yqv_&ZMK38YmHJlTw;#^h?^MU+5m4O@)exOyN(M)!I`yFtLw3!g63u zaK&>S`oQW%4HpsIC0!Sb2&yj0el(6d^Joe9=JG-ZjaV&lqO(-+Js+J59959PHD?bef7ervdwoqnc!fQr$ zfW5liioN#=cF?{mn-T_$ZhxEeb$E=a>X5=d7KWAFC1f9~au5MMJn)R}rjCiiTQ`vS z1Ze5ijQaaNQR~r-beJ-jM7iH!GsWrSsah{O`fqb!ACtON9UTR z(8=R3;d};nQu)F^LI)RoCdAY?#eFWeg*j$-yEV6+5&>igaoS2ubsZ9?OSO#LkWF}& zBnmmx|DV8+Y;?sHNCwCc6IekiAiD^(_&d0Q@P$!%a~A`gMUuYF5F9|SQ^+|EVuDUT z?`fZSUQ159tr#;txYy>#MhNEFKNR(fN8q7efO-B_Pf@69J0!>kh5MEZ04V@GHi%uc6Z({Y!BsdCRz?ITf}VFfZ}A zc_Z)OqjC>>%df!w^3X4mR|I!E9zyw z5-qJeO53)Ut$4dtqT;Sc^pJ0&wv8(<@~tK#Mqrq0u}*VLZkmRw7({u^j_D+|+?h`v zOL-@hs675yR@rc|C){`ZY|Ej_&E`i98pBe86jek8i)Ne^Y*7?+o{l^wI-mT{hPCWw zGbConJBj~7+2=zD1FDUwI$kUhU;dKn5rR6wz#$mWPL>t{3&M`R6SmT#3Y`vEDDEP% zXmfZ~>?Xh!xU&z%Gvd++sO4Ox?UljRmH~q`I9R_VaRrm~Q&qRl{SFU8id*?F1HtE?x%)%c`Uutk? z!A(LNNlRu0+VdhLeZ0{@+z?wU8d801ck>&ku_wXU7OnN{jU({o57PRDqZ=GqfN|&( zP>N7iYEBV;3rzw5Bj5maMU>+gOGt4<2rC{ktiXT0OSZ8fw5780i58#m2SbXWQA07H z{*XaE|F9})<5SJGlSmar%7@d)F+zqnmq5_MN?T=0*;dc3Xe&RU5+>+L|G=Fi{#D^O zRdQD?9zvUuY@WK6$}%r$sF`EfGsk}oJADIS$}ZU(Q(QiG-k_JI+ zsVpEUtL@@VX`Xfk7>-4Yb&?fjAdfu?U>v>NMa-Djvu|NggcA_m*oLNChhHp>2jMES z9qlG7a~rbaG-mHsn|w|}{C>R)GHcw_8zi(UcrCxin0$}Ek=Ry_ua30N*q#8ykCC*<03UkNTA5db-<1}P$&8?qx#*&=(XKgh zDb<|YM;>15k2_ZioJMkYEwGErrnTolb_ZjFdnt>FZI)t6jmWZ>120Dzgb5g{=e`OW z!TPl9nXM-OQE%jg)(;o|mGSufm4w}ydm5m~K{GiuPMH`NL$tu!0Q%m;(DeT?VZ3({ zhy}{Pz(D#|I<|L~auI6RRx@JL1XTKI7=c-t*jjK)CNn@MCF@?I*$%l9e+BPYY)x0lchu9SMpfhjL=ZihN_rs%~H1yZ7w?x{EQ1v2oNf2_V29R2uLdN#F)&Fban?v4}Ehh$=<5J-Wog`SxjGiswI3aV6<@|xlv ztiq|(tBA@x*iPnU_1tSeIdU0+1DkJug1!508(L}?+BA;Eg3dsx8U@P@S*YKWXd2Nj z3xjMVZ?m-;WIbn6ImoYDNlP6MUv+lrB5coqHvF;-4Spq`Ugakgpd*BbijyiX1H+Hk z>^8KnrcuDb6agm01p+|#rF0gRmX^+RWkF5I^w@kYD2n%l2e$j%`3W_oQQ=gHap)<~ zbH|tw1C4O=VN3LpJj7zi{15|U1w=x!9*4C;S}N0h4(12`0954BEkd0QPCTzN|0sc*V2+FwVxOr? z8qnjEu%HLdKnZYLJpN=$p!MymR`DQamDlU3tkOT)SMCdk%=*w*(x_06Yp_%qy}&St z;jPIom)fKehfuEd3cGT2(sKDO+S(nXtqKjC@)a&bUu-gJ;jN6@Z^o*Rc}_=Q>hMmO5}dp}~d%tM^}+Tp*+8 z#M%>=6>U|r=O?m5M%uq3!;H2Z19pZ;%}GrbDiV}Ih^+0z7XWaTP2P3;Ztxn4K`nfA>6K(|qqd?Uc2DuBi z7gh;@wseO=>;)5eOOA$Nf7CTLDyH?!y_8#@eewMEycEFkK}tp05qwzV!|+0~eDS>>E;Ko3Vt{f{?;I%8A7l-C#OJa=}p zmaII2weE5NCgg&?inpcQpDDAm^;r6}3r5_^eFdhvw zo3QWw?|l-yHlH`R9ZfMcpn;TVw*d>LxnuG^d&vdkq-c!atsd`oYWe+YMo<{{4%dIM zyVE77-MaYD-nJ&5zFg(t*e zuOhdKI~}n)%9O(9aM(WhL$XZxYlEWSh)M2o{pr^?mm0EQ`QAW4rnALl+6TYGkO_LK z`63+wB2C{{6|X#$1MaV8y@4)dEFOizVog^#m(>=Brlw9A7{EiwCWfg&LLYOmo|zif z_XT=)VEQ1F60}aB2to+r!k=>gSbVW8!-ztaQac#$3Wd|x*7%<$dWf(K!kuI(pmeRw&RaA1PJ{&y2UR2 z!$21d5)sxLowiRnq3Y=HMOEg+>kTunIxV2PU?b$J`AGU$p#eiw-2~r@V$f9HA&fAw zdnr=F1+bgl;nn{jfKc5);D7-y0_|Hc;T$Xo!zQ0lP8v2>VwqRW$beBNa_ke|Dz|b3 zc5#Rri^ergx@}|_Zk==SdL^7Jr?GE3`>PI1)5)_W}&AAs--CGn2a8}B6WeQ1!V^%U$DI5 z(d)u4=d0&YjKNwje<>Avj4|pVthg9KO0p}&OzD{dPHH?6+}K;!B3&hc!1qP1kgszk zKi|IBu1FsO(Aq{@-U-%o9>J!|&~5^+)&98?`dctPdeLM@*3WibY)69@K*oPYpm%W& zzpS5bZy6SnsrYROBw zoz(i@ zY~cxMwt{t7huA4yU!zj=dR(2Zt-jcVEnvBS0|AV6#lS8zrS_LZ(IMFsV)(+Xgzn^l zZf7%dMTAx11U$7H^Mr&8s77n&-#s6?BNnPfiSL_j3+SEQ=xUxN-?l2mhTgrF*?GZ_ zJ===@)G39v9n}>O4`b}>yv8|KMyA9+?C29o3<$LB_2+$=OA)#7c&jN zempLf!XvmQU>sG=xypV+g%0exC|3JX)Hh48y|lfoNRq654r<5?1Yx^U&~Qq`1gu37 zXcTw}h3(7B%P-z){QB$W^RAT7BBER8U5v0Zi#H3U#u{p8s0*ZGX%!8f-K2l+xzj3JNmOBoCxR;_zz z-ce*U*jM5yJ^`&co#peGd45B^(=A0zGx|ZN?Y8*RP5}&Lr_AIbSOx!)xTus|`}VEV z6CoE+ibRvfl38meX5;_R6kz=8V^|YpVXQ zVEcSTh~XVk+ag#2aXm<$JP}2tWpj}-E5GdNBcE$rNGIBUb=zIIW^eHDiAbnIAIVc@ zb_GQ*Tm!*>#scc7&3QZ$>P0i00AX^U1J2mtq675Maj$y7%jAMM>GI9gYY=%{-JBOE(P zSwMKs1{Fbw;fOi}SY9p% ztgXWb_h%uyhV~UZgF*}HDV}d!pqc_hrB9N1bB3E1z52w;Oq_LKLUxco_E}sBA%+_$ z!_Jhjja>ttPiBtpG=B0f-0-(={hu*m^`_vz``B+4is>N*-sBaOBYYyJ+Fus(H*PUn zQZbgX8?r?(LzMfk>v3tPvQ7-()%#8O?)8W%9^8w7uRa!xH_6=lWL_%$^*A9tVQ>8L z>&b=kOg|xgBUAmEEG*=_li=S7F>o|>gup)=m8fd^KRTyjEay)+p(QMrv-|ACU5Kv5 z&0**C8S4!DCv0uE#^zh)-X>RDygWz$Aho{zw9x0ZaTWCZ4wqMg$~3MVkDhc4aEeaH zJg+a*JKs9cqUu-p!QD00DI6D7{fnQS-{Ow8V7{drUh2DSX8z;|-+Z-F+tr(zjzQgN zudg71ZCR%Fbwq)aXH@Hq{|r=|A^P@#R1mGQqNpz^e>L6YCSVzx^RkPfEKuEZe5E_){6hHU@bk9No1CnrxF@G};~WF{nwe#!gM?G2_(2_%S)M(In7$H$37c3DAUt zg{hvfHE7%nJ?nDE*ode(Ya=qgqVEWh%!&1$pDAGlKy2;4Tr0=PCM z8U^coE`(cEnc1*BZkxUsB=I}1|FS)HCVlVvrbOSTka94VsPz{}R~ECSjD8Ju5YKkG z7PC@EFL(sinkCpvC(#So^YhtC8_)OAV7(zx6pkssS_%Ur3(0T?+oQ~jeW5eby0Dmb z5iLf*$Hus|ViY$`RvHuk`U@`_~Dng5uYwuXrP|^-kN~_-64Dp$Fkego(8k~n)XS$fed*w1~BT&JVt;a*E)z2mg zICmJMr`}%q3x77fmtih&lDkbcQ2?wC>9N%elj`IO-$6+h4|7*!=?qlxY;KY${=@(Z zxHrKQAIMj*6M)w=01JM?2fOa<;4%0v>jZ35i~9??lg#m1VBOS@06WblSc2aMoK-6PyXEYVaQ8FYM> zP)IDp@*xP{$%FfSePS}Cn%FzES8K&_NDEz~ zV0(FVgG1*~KJ?>1rv&GesiFIy@+Pw4#&zjt2Cbc%azgBDL(3|kpgv6!N>|wvpX$u_ zEl`a!Mtp%xh7?_7pihh3-Do|knZDZ7$fN^^IwXhvt~e%@p`5eCpwdtn%Sr-sjk+>N z)+sjBp$VwmfCIEIL^A7r40+n%C3b8+7sf6C%D142s2T$`oosMY=pxqhLK~W<(L{5I zX*bwilG}u|Ap&UYYE~Ngh{$hovrB1nL0&&vLYT{akBV4puH13^Ny8`_F|L;obG<83 ziptSWNn#^^FQ8MUzEQGd?9YU5|K-S=t$b=seQ0xT=|jc4?S2C%*WoEQuSS z7x-^h;S{Y^c)9Q00wQ+-wn7|wg=`6$Hl)s616ETYkoo!EjyCka{4ZY}3MCS7N!Jcl z+D}I-NfaPXyQz>lq1b?dU9UNGqI@Ix0!CHZ#`Zur&68u#m-=@|DS!KP-e#YDmhL=E9efzyVS(71y{@*zRkD(3f`BmRhfz;fF3s;;C@f4Y=;LT zA-5b94G^~sjfSr`#81+CV1hzxQSRf#4s$TxUK__8eyU` zmJ{ug zf!HC$^GOfNNAW9LH?Uv5ynQL3DWw{wWU6TIyhaKY5Uk_hf1yDD?P_*H zfG+0(WY!vn47tzlrv-3E7&VO4MSQpRbjz%Z09X>chlzQFRq1wOJl(hBeqGosx1`0U zZ);IizoiZO0;?V*Un{9Ot$LRpE!EJoy4d#Zq(kx>>L)!}FFVga6X_$b{>ntP(o>&m zrcVjR@eN^bZ*`uYqTG9lX(mn-N@T7(a_y(eCr*XMXaq7R(5F-s`$tuLDqS4rf9}yk zy@9pMu7Ub=rYk@0^(Yg7%S;&9tAnvXbFG&$f=(GgGwYP~um76FtixfuD%n!5Ee{|< zwvhR=m;rO2(GgoEA-<%?jUE@i0!>!{aC?)p@B7lUH%Z$*jQ!vFOdQ#z+vS*4XtRJz zTs7`}4KKaC%W-h|356_umGTzkrs-Hv7CSZ~mGm4=`bDQc7~W42r)T$HbgKSG@lvu0 zpF!J|!`qNDr)0|OIISQPRWIh=YXz(85Id{2(JXk5J}`fo&=hikkTy$d2>5|mB^vOy zf>n9nj?bkLiwv_fs4D)W-e(?ft_Z4dx#tIk`sH`)EzK(_c*d@e70_tL!hi-ozo($w zwAOd?>x+`vgbtZ@M50t-GABPp<<6hKGGpnV=a#q;+ z+t$Fn&#Jf(;fVV?=6>>u{sT~_jVJ5HK(p=y2R$Y!0~P7{*5|&(7Dn-IkoX|p!!4!D zn7v~Bsxh}McvDIa4W;*X?5^~tY3BP^@u*U^6P`zvVcYFMa=!{a)>W7U_}(S5LI7;_ zx+){kJ~MoPL;;P{rWI4P@$~?8Y0DI?1aoLIF zDt}x;mcX;>Lv^hA$M5D&o6TK;_<2tsMgN{HR%ybZr*Lq*?ei^zxyNF0;@cVhAU*y^rKe8@(|HM{CLJvJVrx{?45LOW9GY^^%lJ=`zWaRGKGUaGhNx z0t{RmJ56^xe!P9F55QX?mC`Lp8J5lu-s?UKF3Ti`Qh|>Xbgsd9R+`8Q96)?3_#<-( zwVnVN*yYjChwnZX{#poZwGBF8F7`Q$s3PJfiw|pv7 z2X7X;7{1r@f8TaPeu+cg!Ms@eL!u@WD6=Q4J50Eb*q9vEUz*Do7vr(7u1NcK+EYE^ zCd0;V_VIUO=wtE>cJH?@wzwT>I{Tb%Xe%mhN5a4NqF!l=!OOZ*GR=K_twCdl!ShsO3zV=Ia{I>hxoyzSzP9ls0>$riQ zQj|SjNhu03WmcCXp>O}}%Q+`V{w#%k)+I=V9V*G;zdIv_5+$Uj8+JH0IwzPYxFGnK z#dD_UuEyST3s^Wr+~VDUyK77XHhR=Uf%R#+#u^Q%#p7k>VJ5R@8XSpDVXq}Otn2J&Z_u5!uwVY1ZF?s1@c#KlXrsd>OX_o0T>!i|906xXHMH#z zbuK@;F99~|U0^kjWoaMEN)m96R9=db{SI_IOW)nu!Dc$Q@6@q)z3_F|fNK)ml($zqVoj2o!3&Wz`$fhre_SLElH!b!9M=Z4LtiT4^AIg4GycTepRAp0+Abg0#w@ z5X#mPSP5zXbq@L$_LV;P2oy%e35F&5qgJ=S)dzrB@a^iavP1Vo-G-*Lo8II@0$?cd zn91eKr+mnXE^sv+9GV)6&H=TY?$<^Uz8Cka*K{)5>N! z`g9n1Ro3^UW+9E-YK6$v>aXU=SY0b!L|}(D3SC??i2VM`Ycc29+>;eA3Sc9$aeLF* zFSQI##0(}N8TA8%)62_I<$IoQa+Ht|6EN(1Jh2xve>NAk-_f2V3+3T$hB(2(;zQp5 zRQ+gZF|_>}iKDcxK`!V)E1w(n$b}K>d~$%BKkuJEe+JbinXAr5`79!I=0bHNWV)>& z62w4*ag~5wx6u7&=XF7?q@73aF9NhfOkZLG^;X^(C@fs%r;L`m4k!E5%P`TknK>`7 z!wivW?-#R`e!=tvY2V0I1`GQeD#ME+MDp!Q2Gh`Q2ULzv1f5(xZeZWL=kS2hj*V zG-&7eMg#ia{NaP4CFR~aZDd6?U`1m6pL6sI(J_@sMh^h}B70e!vIxv-m7?G?wv`ly zEYQGFj1o+ID2Lx=28U^c@XlK#5TfTS6m5m`o6V-RZ;n))*=(_t716!En>|NZIp;1v zA2e2fg`Wr`8ALMafsX`sUz$9T_ye88_XZa1$Q#q~a~fGNAO^JI&^I`-p`0?QHirz= zES!De^Btmj_;p)aLA<|eBgGQ_*LKQg&xrS+?jkpGEHwI*Zw?aRVybFJZEkQm7(&)}MHZ9(|9$MR{VqD6P_spG^ z_oX<5Jpw!zkce^ND0X_YOaXQAIE8mUnN*-wDO5;N^4T@nhgA|tyk_E+%7;{aoXei# zo}QyyT@Difoqv(@XX9Y%x6h~%kMnQ|0SUku!nwGRAPx7x^|A}vycQ0yFQl-MikigO*E~?@-w{Xu$G|}KeT7C!`!b6l(Q7s(Q<#P5y;+^J!LxwMi(NA4u>LHFx zrfwSx8Yn)iN_^OFS(dKZ`&tuvZVN}sK4Jh&Mlscw`Af8c@@w{n`11buuUpN_TvVcU zf_U{_BO9{jm%3n5bf6vKpS$y)ah5ysaz3Y4PxW99SzT-DDoo1hh zByn+19X^N%9mDs!<_{sKo%yg_8CPIWZwK>f0F$e2t1!?PWm1#Iz+!(DX3LLzN>uQd z7FFHF+{%0j*NS8(QS?39S=lfeekKG;tHz^fUYo=M>$01MpI$=o#xH!Iozy{1_$|9r z<}YHsAXVu_k@!p7yc+@rz^VfJM<5{`oFYD})TW8Es`6CQ=uovYWGD68_U9Le^P0`c zMBfcUog=3u1Et0;E@w zb+OD9zn4Cg^IbCC#a5_MvL>uP#m=Zj=Yc3w(9&KBN$ zPAA&;pqi{?k+zZ{FaE>|UwuM)7(mwf@+<&?-vam#$*&e$-?If z%~zT?j)LB~S$PW72sK>v9yLeW&HNb`XOIAx2G_->TvNtyJ1(nK{lwgzb#Hu{GYN62 zMB41hgy;G3U%Vk1h=KO;Z@f`}DjoPUcVAK#k{3|ChK??9Rfu?qU%{VOE~$L*;k|X! z@Z)A89SA?U{vGU z>It{tOIekQ+in(ZQnw<=sIjH^IX%aGhDLJuY%Uq-_tldw%=j|-IE@eL@v{}u>Lk{HGL;h3(P*d@NP?w8mP z*C~!=>&b`TW}qEs1{Uoi@sn9W=rt#8X8kIt}nB@^g}HZr|jW-SG-{9 zhUv`Y2dSfuC1;Korz-wt&hl#}@5+l`%b)pvL7y8oaoXZN6xKJiZU}jqk4&&D6d|!& zS|eltGjzE-q22Jcc~aT;ejfvw8se1HS~fCr9wI9+?*D73Ug53{h|?{6IWOy1tcT0k2e$X(Wvz3)m` zt37V4y>;~duqUxP>G^h#O~uA!?)F$0n#_OODsK9y_=?-vs&T06_$D51i>JIAb&BV7 zOb95ndApd#lO$U9d^5cDpf=NPWl{FqVVUr>P92E#3bn&N+NRT_xlJ8=Vnz#Oat zpllBANr3G?Ra}s;xf>~nLSf2ayTX^>+K&}0EIzf&7w@j71^(&rASD8+JV0Ch=AUY1 zVoYV0dBMJmYn4@a@e-xmKb71tG9VFeK_-t?60kneQ5$At^p4?r+C_#yYd35J<48vj zJ!4R#7h6fXOLP_+3L77|HLUc%Vg&(kXNtksYnGI#+8h3R52 z?1JWFZTq!JGATz7V~PPD!sKzM9lsBOrFuyL>K^JOzEIE2Jt@GdUj-ZFwVDGMAPE;^ zgjw69UJ=@PWd;3#x@y{$FYc3d!`1{=vBBrVY%b_uGlAp!depZFo0QPDpj zpU+EVuXFR;fVTatoh)YukV|~-(+JVxtHABSh9lC~N!Q1j#2V8G7BU|iJ!qW6$CREA zaQjuaZ4~`|a^KuraKqAvDu7j-bhAP}>R(kSByAbsg>NMP5an>b-L=WkYU3|X1}vUM z9{3Pi3JuEJKD*r$N(n;z?*%5Up5}i1wCnn zcrd>+&9`?mPE6nh;i{0e>XYl|$pIzP-%q16yHIhQCV^oJsrr}PBzX0U8!eM&Rmh&V z(YoKp$@AKjKcFwWjKV)=a{;_!&<-Q#xKA;0XXoX_!~HtCRAn8(_kO<8_I|#;Hn-1; zZ0?MVyitwi{K#8irIwk-^L$n}?j_G1go?kZ2tjhejk_DxtqB)UfoLdX_jKNAX{zf7r4cd|0z{Hod=snPeX=$)3B zG?UxI%jLUNU`mQ;UXMOrJ?GDfrv|CaMPs7FZY4LCQ+uJX;OR?L$@iRib1Q|Zd;y#0 zsjf>viPa4R1-@Bvys60|JT}Q4H*I7ItB=GJy$i?W$zg(1;f@P$7n2q`pU|$-!j?YF zyzm+y{qVuYC!PqaR-l3;`pJPtBgS~25t;{QId)ZzD?kko_ ztMr=o6_2c(T|Z{7%AFjwW>PU;dP$RR<8krjua3AQMc><0$hNJH3ik7Fh~a=y$$Y(P z3dek(?;KPsQ;xCZ!f0hywNmkuT1KL%L2SHnrvp^$=70Rm zN@#3;f;vinKLNlX0b?dph7v*;_{*ouW< z+iAI-dL;Je#Jn8wr0FnRH6KJD6gQU3rKO)!k1@F8qqyuDcnP*Va)1Jw5{jbBbpK=~rhrn-u@e<1C!B znNUBze0_^KY`5`N=+_XVA~Wt&^Ow8yyCQavj}a-}eYbEUi2d=jl;Stf9X27QUri^= zW?jE*UnTd2QEu|lM>QLXSm~^;H)KnA6$~CaOIR$zV+AM7&LsVYgy}|d$CjKmUp9v9 zY^Uys63oAoxnVwAAoP}i%zy2i5{~us8ILqMi%$VODFkL0m6pms{(PlUyDF5mmoFsV z`fOm3;EFzVs-6>8iP)(s!#cUcOKcrc5O*@*7=%M}M0p%u;j4K!WLSd2MObP)Jg)&X zuysli{VT9LWhz)zBiK7KOaQ0~rP4r-eJ}*X5GxX502mOKvF$SagcXliiU*C+W$Ou^ z_0{tmp1nSqD*;00qO9#f{SwPUdv;sjg`RiQapJGUPF?eI-*i08U$V$J8g7IsKTrHw z^bo%naE&144%iF{DV>zpCI$^R@4S!SP#^wGpIZn=XJ-clX8wEEY7wJ&Xui`x^IXZ0 zooinQm8?r5xUW8e0q9kknzmt1yVl^nocHG|@038n=pxU%z3&?R?OOMH*k}OfBiSRA zy=#k5>@lcw>egIJmL;0oqlF4V`X-~jeXX^QO_B9h9il~u5cJ)5-K9BhxFs% zJW7|yzQZsym8fuxa5Q!=45pTXyHJ#2$8M}Sb&MAql)moFceXjUCUC_E5?^Z3Af-Ro za^zf{uz1;1 zyqux|dn`PF^TllX#m*-I3)O$V)573n zvUzBqpg`9ePPgk0!fj#UlY)F1V)pu0E+w%v0vI0wqcC-YmotaGDaqFsZr$Z|s=JH+ zp{K}S95XJ-`Mc_oJl(8HL46;BOYYQ+xAY3C| z=}yqkG8!LT&~2hJM(+9#zge|g&qKVdRLW(;sw(@fl9uZl{WM%X^Eh$`Sqnbm$vV8r zFRP^mU(;c%{M%_%-745upn_O8=6;*IH*?SvEolXLKcCGB)3>K*P7&Zxa+`zKIECB1 z;z)fleJ=nmz%kM}9C;0=5I!dQPc($$a_6$CNQP~E`o;L3WW)td91tTy1T5sX@=BBb z?kon}bQp9Y`}g#6K%IsUhPzK0YM5^`y^*pg5qci~zHat$w8|8Sn6YwTN*>k^sJh_m z$2+2*O^eci!G~CEp0rGt`k#*xB$%qjIW;s)J^FL%p!!xl`G#g1oTwpi$p8&M?Va)e zQDpzcz57xj!$JMVC+qdSd2!b#bQ$tHJ2aaTK@WGtSos^X-=>dc_833Oc%3gSncX!Y z;T)xGRX8!YYBMovHjFDQEFCk-6G3 z>EE$M5}rr2)9ZPq*6h_?c@I`PE`eG+&V|SR=xik|i1PB{d%)LunLr`-g$*-Ca?7r+ zHyg83PD63am5?T#r5>mRLVTAE~*$#9<_#02Y6yJ#|ED!jWn~VdVIjfio#KI zXMTJ-iU7~0%yC8rX# zBW_5P`)Qbpmxwj4MqFDG8_4ow-lQj|OA~u^uzmHAMe>TxzXb$378M8_XVFWWN+1rO zBr~~@Q8LNIaA=;TRv+T$ci~uF_gnVy%Q|!V$G#_(`L*SdgHmpW6m&pnkbGu4yJ^CD zC+w5f+I!2e^&rp}>9VJ+V^bsK&ZT9qfmbUzNS)6`f@$sdoXjh_-U^<3!-N?+I!MpC zRoU3-bz-VO-@22ptmN=lMD8Er_LVWbs-S^9-j^(cLJXshpbK5W@&a{a^q|aOgdLpX zKI`t&2Sk`_yGr=3yR3jisdhbYKI_2dT~RG&!!&AH`A)jzzV&kNzJ=XW`KF|8?=t0vOVubT>#NT?0Y7Q|T7z zZWy76bV`HrC8WC~Mt6uvBM8#nAhBn^=U*7?y?f7nopV$U11xW-gR0-$hBUT%R{9`}(-SC>+dQL!wB z7HgNQ!i32~9NF6U*=?SgRllUzh`YG+FrVwY0PFcM_rK1uo8+-Y?WBXz!Mr(wadp+y z0;6b47XJ8emt&Fs5Zc&p8Q?-@x9>p%J>5Dww&MWSu%wt9aor^qrW)Tq7Z>)Frk;)t zXXBeKLQCqa!_K|J@r`La@K%hGGhe$=kt)+kM4(NzENsW{^|fDXnEI%jz@gy2Fn zcO82?KOIMHGl&&;cU8sNM-c8%ccfPT2|Shju|Foe2=!V7>MU69G(77UQJ z0JGmYL6McFrl#j4ug!I4KVg*5;MX4+2qsm8#WwL`n+8W86iZ8q2r1{E63{u)=55=f zHm}f8L`$oVVkAkGB4zQ>Bgs{RFFtnjNCJAGsXAtDjiR0M#fuxq;-DQTJ3Obvr{i`I zS`d)AX8R}d%z*rN%tDt*lhlYf79C{v6c=^nkiFNWHaa475 z7n%9w+=NHMA3?+3ebCe@^&o;n{f=t%IFKM5PnUkKte*Q$U8l)25I-U^)tY4W%SoQ{ zP#)=u1jefNUYWbeXY9XzQQ%^o0vi6m%>&038w0Bp*U=V|qoR4DlNOjC;{kGAN7%=W zO87+fxNi&>>S%E6-*;M}{iA;!uLDbP?SXgI;}XrN z5%k@WH!Q7LhU#kli{^1Vyq_1(MJyepI_4b~+lYc~E{6YWoWoX5|H*!ZbC7vE7#WUiqEe&_Yx z$zKF*h!OM=do6T|tj0`Y*ibi;;3O0gAF}^RQ8GSC_tXig{*O~hLR@^S?-`Jfy5H_% zPY9Mh5zzoyjQ$D+!#5UC-zFgiQ7nK585FMfzxYgv8-DQG^SHP;5;bshz4~2c%6DR* zicZYZ5?=ULzx(s|KH{JV`^!s*fc2g` z>OcoDx%YQGY+z=%(9SJ&l#)>iZdbaOMCSB2gX@DwKYl8xG+e&uON{ASad*nGMM8kw zjF#}g8&EPZwJ2kwNyN?oU*rn*+uy2|e>kw0x^WG3fC#`A#bK+E#)t}(ugkUetqfu< z-xL_AKhn!1@hup*OI5}!^4`i%ncXMt+Rh5SY2-MN5fkU(qpbhLOble@&fR|GMDVcu zxxzLIeIfu{D+7ISYvfwHvu;DItz(N_n0edRjq1oePVn0s%u?-weaZqI19a?28TbQ; z860Gbv|zy~z(@Gg6M8*`XTmG=;8$R6ETjam36^^Tr>VI+B~#!3#J#2GneU5Oitp5Q z+Iyv>JPuIg-_;aE^)@JiZ@RmzD8Upgz==3i;5rdtuYn?<#MxNt8mU>kCfk;}d~O2a zbV~0iF6h@dSe3tmzG6m&XMu2`^%!`K3_S?x^YduXie%Jr5|I#WCLAFo2(qY;BbIli z66-Y)O{DfXbXtB6oDBGSzXY>0*N`b8Lchb6(aHnJpB5Z?C*J-T3<^g{5Zz}tQr<6h zwGIHUBXA$(lzBg!I2QV=+Ar0V`+dUYfE-8^3(nyCFQF_7czn6+jW{Z*ZtVQ`?{^ zT@pA>IZ%WCgd`sg!G1u{?Wp-9RX&h7NCR>+_V>T-%7VJk9ZJ}Z3~p+J5N*bGD~v+s zog2att9kb0$DzU!{!@kj0%wH#Szk6h;M9j*hw6Bykl!LFfl_RG2sTwaM&%bH!JXFM zFW(qk^|EZ~^iUu3HqZ&DSx_lt}D0ot$Rm9BBVHDF|1Nz zYSB=;3WdhyYqJ2boY~JJ1+FKv44Uhn$68?I@bai>diE0_U3Kb|_AQmFr?`0ReoL)7 zjZNzyPV)3)g0=odO26I8VORfpnct3@P=S@B{AKdT7A@abamo)qD%v%6T5GEW%78`C z47Ju{`adviV2TRCn$FdGGHTS&KA46Hi-Zv(U|RhNEX_U%Q^X)j6VQ@-dwmQ7xGXjP zphdIRND3{$cVi8Y=%@lmOqW8SbW;+5T{sf#~w!oo9R7iiz}f(yi(W&kMSS zIKD+QkRUE(&wx8Wn|O)9L#k z;9~tg_{aW>F6v3uc(ruS+ zM5vhk=CM@}wQM*shJMdHB4y+6N4a=iboQH59rVG{ZNj?E7bI#Px?)2aEfNleZ=Kij zEQw_Xf=pEE-J?+dac?|I~=5u%F%Zco}jB)6ocr9F%dWW)oBOXz)-Yp)GR z1PZVV6zD*15C8kTT-Hpm5%_!CP$DpV1n4~A06N>e_h*k6HY=3pefnqFWoihaKwy^+ zr~t=>@@^;GIPMrsB15lAIKCvmwQXa7(Xh$mPErPLREIsek(Lh7ZA(1${NvichzZJ4 zjF5R*cLxugYFbAA*y;o zZ@Vr&>)m!$yt}&;B8f$Tu;H{5yV!2_NdxacTxTuT7c$MWPLzDu`JUOU;!5?XBBmVw z#A7cyM-V%JZSiM-+aTrW`%lBgf4x}w0)`rSs-z(Qzo`p3g5wok8@_faP_p+AtfQ{g zP{f3S3b5nnCT&HxS1@jLupCPQc6z1+Mp!r?lKo!5yqIxE!8xBWPGZ5zV>h%QxVS!o z81%y+Ty$$eqlQpR02%%S1E)1V~2ZUfOZ^ZxWx=fIj@aNUOVhJT+YH>|C2QZrEcOf0i| zd)Dr5d0|!Xp1RvwZ);6-{j_i}-Y0pDAEGcpdg|5^v}eL7p+rVXDs5q{)&#P_M)du+ zk`9!DEb_6Cmn3MY;jQgcANma_g2t;Fx+Z)GK4}#B?Ppyx6jIH6wA04^FdTaS6xWF} z_@w|b;B?rl&}=v5_2cl$#OdcVVaAkgv;5I*g`^N!I?O+bH**A8RQ9BQoKzrusAGFp zbP(??B_?6Fb4z-FR0P_;AWgQE!sXdbWxK=-FghPMUFTcjhiF+PH_?Q2L8Sp6PveqI z&^|gr>lHOKM>wi8 z@gCUd1!HDqVQY44=MuM}6t+jAtD**DUV*F#%6rdclNg2g&+L95#)NZKQ|35NjU4J_ zgEw;%4Z|yWX$5qbVv}OMIEmAlzZD8Z+}xOiF9pSJa}+cy1M)wxiFvK?hYVcHU~gw9 z`{GF@3*(9Z@_#Z0LvL~vBHo(}wlZrxMEnhS{`xn! zPO**!zXh?LFh130R=aGCcK6*)XzAq>=vhF!<`?*HL%wIB_9gHM)4S_aiO=}GrwOO- z=P#Z2sFCEEm{N9cPE5%xCp|6s2crX}$o%rxQ0MJ1+637u^z@eoU3J!!qrj2YLD z_9kvIeO+tSuNBRYGO$u72FlR+qliO2dux5F`o)`Hyr3O4hM9Yb(gW2zE1=PvdoqQ8 z#ofGY6-Sl>UK6IeH&)A+exR#1{qDo>zMnH{sJ`NA-W^#C1q43vkbgNP4S#JjFu!#D zn)ih53e>&Pax6vLo(}r1NO{s4&PVS`QTtJ?gc3FU-{3ai(Dr^b{W;w&)pAt+bKDp5 z(g#=sP7wBP@c7cJdh|GolqB1+a-V;3?3~hYcvf3)1#ceHi}PYmwsTEl&D9B_$GT z&eP+9UR!eB$@~lnu?hI`sA+msJ|#hJ6a5_IHu7^l=uYIAI7b&L#CTxn3ax zZFG1p=t3GeOYPiQ!w?Qy#PCV)0=ou5+_9KcECh_a`wR#x1JCdxiNNgdzX$*<;I>uH zk13#RDO;dG9*LJ98R`v_&7kVi7}9R2%IjK=x!_x z*kDiOu0Q_d!bac*CM5%MDr@!FVV@>B4{s1FRhREd3sk;n6`U7E1XE8w?{VZCwCYT| z?)5K=l$vE!S+ow;iC~U0+}cT|Qp&Il`cH~xZuGaPdqw9you^0kPJbFVFS+qbJS@<=sn7` zBS6SMeZ-14eS)}?f_?(7Du1iW z!PhWQ(wQRoAOtu+%og-7@LLH+G$3~6&EMk_M@o+>g}L25VS!*=-~cr(&u%co1()T> zLO@{sY*CVKhblGT4xInAKmaL7nDdtd!V2sTA%4jL71IJ?LSiZE!!w5CUve*ps+OIW zGC$!{&DVB`NU4h^?7Aid$HJ250FS(%iz-=_ zjpxcShRQz2N6N%uwwd5?Mr$V9Q%f0rRsOPR8LmiFR_W%XWoetlXz)2asE`h66Y(aH zKd0hTY3W*}-1O6P>l_#%?oZN+fUb{$VZl|HkHE)(Sd5u~A8RsZO;L^_!GY*Ya&5wX z%J=v+%0e5V$q2RR65Q=*#@>Ld*XRSRz>O=pxQxk$ojG09b^kml>&404Jxi zl|3CO;yI}1e-Ao3mQWeJY+CrD-}+H@B_e=HG5$W3*SvGtg}9}WH()gtw-){0YW!a2*1K8Wg}gt{^H%i%H0Go_KMnnfJwJPFpt^Wu?|myz}Q_?3XYyd+_`V9sp z6ujpus{o9HYZJWCaRag}P_QlBf)*w(U{=SwZuPkcypto_AIk}Gi=7sMt3x-NR<+^3 zK+MpfE};BL;=^7@Lgsa!Bw-mMKU&^XKdIH@@oVerrct(u%*A76-%JxuZAuK4GkIko zHBuf1V17&o3b3FdgI__KSy$V$Y@_7vliIq{K4IrL9*!94yX>H;JtgsUTujh9o(b=N zhE#STp-`nisJnBpGBOeEp3HwIO*qS5RB>};b^hbgW;6WFx#4pV{C!ps8%yoqmhICC zfDG#NVfFJqBUY#x#_~;*1Th@4V|VDMEB%)B-M&6HBA&9&0E0iuf0{8a{9SXpPODh* ze?UQk!&J4KXZiUqM-*j`;r{0Be;pH8%$+Z8$Rg+1;)w<$hgxVit)<%nA6KyAxlzrzxOV+*;r&ODN) zwtcT(JY5DfWJjVk$TTW_pv~TZst<;AL8BVLw*&)T^xh3~YO1jp7s%R2-z4aMpO+GX zbk%=_%Vab4tGiiuY7bZpI*O~UOka;}xz~?TV&88b|2ghSqv@83L;1X2MG-5Z7+7%Y zQItk}5wG&0LXW*>sw{{A`$x7?YIMkw%)deTktGd%-z*w2!3t_hZom{9DSTLIBH-5c z&6h?w!}8BM$vKe(=q;v9fXm>7CcTt98J0xAkn|QdeIVjD;BqRGE)N$}6*qzJ6xf9! z%xU2paAJ@sR?QH`TlyXQj{7mgj0cZM_!;6j`Ux=&6=5EE1SVFH7pBlTTaExSI0oe zMDY_cDwCz+y)oGi_!DMUK^8t37P}vvIF&$zkOsk-41^x~Zui6SQgvogTP)NP%8Pq! zT`oSa!3dUjYM2E^al1%b+D z*k|Jc71YFv-1aevOT<1AA$+X6&*33M`$8vd6vlBLuq-M?SRnIo&IR3|6x^F~P_bIK zyYb^(Ik52T+sb|D&l<@|s8)(~FPVpWcD&oh2B^+%x1JcYef2OP%N>dwlv~Y{vdpYZ zSNiauV|1UeDX*)r^72H!jQQ!fYsfiD2fXH5^oV*cd^N*-u}yJ{#Pz$j8?ZJj-NdT@$s&Au4Md%5wck%Dr_j{{0XOR zIV;KM69uu~w4`ha;pT16JzB43l{=%R>q$J*6Pqaol&d&?j-l*oN zFxr#u7YG$_OnD&Uvbs$~2<3s;@g`|@oD&7t>|m(lk*i7c*s{7|Bab+ZpT9%R&%|<1SDo-4TMG($rN~IKZrR*c zT^|Mk9pj1a2h`sy%#=Ro$^53C%h1m>Y)Pc9t@hxjz|wnw(ARb*_*1LS6s302jfsi7 z<+CA93OByMf=by|qX!;Z`$S^Xd+X}v*S|@$A$)-fTIut+T<$mnJFd#Ta29VROg)Gh zS(qOisH?FcJ;O^8HYK09hiHfdJ^~HTphm2~e}~L~0>Yw^1$^|7%L19wI8jngQNCdH zl)sBjWf}63c%dj;ht>5wl~%TOPJ{nxdGJMAU)o{AcBMd3gne5@=|3-eB^!hi z7=S_YEppv9=5SsY4UI-@@3te?&=-J9)S9=+_uHE}_dqj7iIv)eUK=6_vg(YosQd^l zgzh)}q_u8W)n7R1H#addj`ht~Q^T5V=Q0+CG=ewP(@u>ZLr%`+yNf&;!nP51ZNx2! zFgW3)6xu*$^Uz{i+$~+O5TY2=V*Q19O0ModyfZ=OMQ}0?mGTC+oWx0a>kQKBh0rQhh7;+I*x=1Ja zGdzJB{YSp~-TSXqR}*$K)rYZbw)B%rW=Q*zb~t^7%$UGyOlBo7k7*oA78N7DSJ;Sm z#DEd9aQlI9m+2uwVQYH0o;7-@b6dE8CpY%iiqb)B*yI|Wr7@@j#A=GQ14aag&(XOr zDhPE_*c)mhdX4P|X87&I@p#m_Q>V*~P5q|gx%z43XL*K`^b1K8@&(Gh(7`{r-of8F zG&<(f4 zJ=Ox($D}tSFs236?5S!;g^Y6Y&b-#6`DPu;kl4Y+nltp8I4Au}GPxL*%cyYy01sqI z1*PI-$GW;UQv( z6B9c?@syi{{23D%%JnRxf2F*_D6=p%a<<$4+<27 zh4Mf`2~DUWG+4|7qNL37AB|?syB!JA-gUc=!HtP3l>KESt1R!yY*5CE0%D}hHGt%- zX%CkP8wrClVKQcW{$bGgIumlgz8vNI1QEmuP=g90%l&}09JO_y^}cA|#z%CI8wyO238?!GC`*?=J)LfOi<+i(=kg+Q3P}${mgC>`oA$>>Akz- z3!BxM-6jFT4q}o!hoW;IttO(mm-@3w&<&6^8Kusju0=8SIZ%^xZj+OJ(IOeJp90k; zlF;L4!T1rOlvv!`{c4V_9dMpDzSze0h^2?MZ(H*duTuIB*T19`A4QD@y;L6e5{y~1 zZ+5^O@_A>mJUGu^M`mp@WQr)PoMz7W(*AlsX93f>FhyW6{8@u|%Hrm)` zFW0nflzspzA~3{NI&pjWoSXcHU>0d^i+WeLwfDIDPlj6PeR_ihH^X9yRGrtcp(R zb|;FqeXEw$rny#2$e&sKo2>=TQX7Rtj=s75glJJNpqInsvtXtm)b-=4v&aPEDH1`@ zt@vAJsGK)~EyA1F$ee;+EA5kWVB^*jR0L*#?JGJ#HPk|r~o)3(eP3e4f0YS5VQ)@UrN(k>$|pF?8C6f&2R8*&{bY@0M5T&ej{z=5MNerXygzIlVuU-C2&@Olt zlQBII?GQ3~@+Q)gUNpYfBExa+DBQ*ll}T;A`tYce%ovF@BPTe^+41S1{bzeB)J~(Q zm521ifyKY)lO;o$?9S~-x+nn+KL9?t5&dtwWu?-D8)(Wg_e{ESqlOJ@_ww2Rv5$;GWE|bIOCjmF8C+bZ9FP!l~E;2=&jjbawX?WIR z&ep?WePD5FfdgFNhP3&4)f131b9gdh+A}q&Kxj@kGx1OMXxiusSC)Ti`3E7Ptk>&L z&;qJ|__A>D&E;4GH4$Goy%8vHszW^Y`E2pk*|IDt33o%3ctq&rAzi4U3K4xR&q!ui zx-+qpgZdxpaRIk5ZIKVjufy!3UT$=eqDj?P1=L0#X*W!7>9YQFi4rzgJdla^He?NJ zN|+@8XaQzGMFC@qnU=Vc83;c}QB-o32-jvnK<3E87 z-_j$sBNr4g#k=j-O$aalStw5uvwU-xE`}il_JyT)HOUK^o!NXuGHD?BeK~NjMaY^@ z^@vouANo-rmCWehI_?%C9-bFnP^s5X2Es}C=eg)aAvO^gd{)`rzXMX(U|-mqStnkB`?Pe0TKFHLc>D*ByjKDS1l9nAKJP-y@S;n5+(>> z&FmmT2CSYgvNGE`#IdTadQK_MaPx_jTtE6KoAX&4OmEw+5?bl8-|@mnTB(ZJg;;l@ zv9?GNSbk@%xdfhKE&Q1V40*}|7v8`P^a+4d0=U@%0`pqHe77NZ&42}P!bUm^2?-^z zO*@?JQ0P7)<#MwD(5iHrF%Q@NV$@cCgXTwN4is?t&co#+#;B z9nj}kMD)wT>gH+}8&86Jjz`zzlmTu~L68>NCll&{I6(h5J_daF<){T~YTscN`70sR zmA-LrrH5}_>YMWO(-zN`3gI%Mjo*hhJS&FkVC}0 z;qvChze9sVmq1iY5qD(gO|Gg0t4mE{>5JVScM z$Wu+wvHW1P1@Xa_&!!l?CYAKu^o(3NFIrMhF>a>`M`2~m^M-ssAoRsR>B*Z;Qsr-R zUm7=ubrsrcUyh(_Ar3f3Grc;d_TGJMwX>jk5s|Cu-I16Fh3SJB!g!}H)l2AN#We}G z&(~rlm5&gBXp4vaS;(aRcUfRaGS)if2_g}A>kS2d(ZJVm`r(2mFaOlSu?d;pC>J+9 z|Dz?tIGOrekPxJ9t=zron7(%lc zTmXhIJe4Q~fhKXiff_ZHu;gYL%B?&*IJToknQc2au~Wwau*gvG5F9$^L>ccIo}{4Jap z+l0eMc#p$}Uk8_{{WietYCZ1o{D(T#?mKw}cjbp|a3Ia4+cHR!&h zpyb+hUUxvIWd769y>e)hdQ{}*r%C1jahw#Ajr}%ne|Lp_^?3j6=WT>N-h{P(yp3*9 zD;qY#{orn_4uOE3Y<_J^EN(Em4iiV;Eb)YQ$>_C|17dLr5xD4(JquY1uB9KtEo5lU z?~qIclmL>5(jfB&o(aFBFWnR_ds1+W*#TZ>5s8>#){?-iNcKP^f2+=N#6GJKHC^OP zwq($Lwdbvk*Nxu%LMWmjsvSLOYVv%k=$XY}3{`Ro`P0I=ky?KN*7F_%gc>PJyJ`FX03h^eW(j?*h{0dDLA~K@G<(=)+Ro-{5-dDLs9(D*>7F z=eb$dSMD#tR%|!U4v%EjjtQ}zS}wAElBrx?ofVWXjFk}%a$q%xn2C@RepQK!*?+B^ z@v1rX+S7W~{W&rqDSLm7pMM31?(1os-+tIBi~)72W+=oF31SUoLR`uWGO73Jm8WnvHs(uM%FSHc&L@5eh{+yr;UV9uN;K{>u4Kz?l|?6$HY2|{DB*h)q-8Z)UCi|V zuWIwk)ts;}-cw)^vt|-&jf4;wDK3B_@#|;~5Wp1OATjXVpn{z96t&q$V{2HI`ERu& z`Q5Q@?-eghS_wdgNhB`YS)o4W*>5}y7dofT3KJztU>k>SWDIas<18G^_s7>+H*x&w zAN}QJt>zxah-UPG*-hGqqmt~(2e!=F?b$45*C>OhU*bqJE;Dv%_$C~K6$|CPr_G>K zJA)!Wzw%quX0g3_y4hEs{zAyQhOs8B}@g1hLJd_qzUcq;%Xz<>Kapr1UWsXYj z-s=j>E9C|j7hrkSj!2hdP0ot3&xVgoUSMqnhcp9Rlo^(k-*LlGd06=(QQN^58r4;N zWXAZJv6eusL1BNHRWO8?sYqYc3Z->_h!kR7;)WN zW7`U-LKAI2l|lR_`es0QW1@FLwef@n)TAMx7sShAS`%^K^Al%dzzY$umVNj7%g$)6 z`6)pp6Xb)pVx2nZYc8Y!Mo(lB4C)KC4wdz2+sq>(uD8Ms_m%kY0s}_{6duc<4x}Z1 z`#OyyZ)(43?RtQ!wa$U@R_egLOOuAZcp#c z31j)_W5(!y805b=V`(kidyzPu{NS4Vn0}ugy|;(3u6{FS)Q?Dm!g*kbd2u)FIHL$L zqhW)1{!rZ`2nvh7k`CwU1;*bXY~mI*FiI!p&I0%2(q$p(*_&bF9N_Ys)Ev{r<(fY= z!K1Om4Hn-`0`=;jOLi*+`DnwrkLH)--hbDzuM_Ffcv<$YUdzI z^a#8$-^l}XFr(Yr6JH_XftpHS^Ykf*z9NjsPl!Z zu7NoloD(=|t^{7AORww_*nHk-c|_88xt!#nhvHGw1NCAGRLGU`t0{=GF8elE?$weZ zkofJtNUgiHX_!{TYjTjvRNGy5|JK>=`S4*8_rtD)eTQ{f)VMxt2+`i%h8?0(T=*z_ zG~I!6K5?P{^jnNWlk7{GRX#^-MnF~~{FzFIGCCan{qNuFb2rbM9M-yC);~_{hBjlO zSin$JD~G)$CZ2UqdMG%^)k5-0Pc9K~n?TX)WYLBUn@GD0|ET;`R?=?Q^l1JS`YY8%MyD zFcmzJF;bi^!5>a(voY_(xxtE<2g$imX7KiNtS zI$;Tg`Cp2|>UJ@`;-@b%*KxywCef;$Bx1UXY&sh?oVQ?ahq|QJ_0+hpVkZ%6Ef@1A z`e33u5|ZPZpg-GWmo9xn^kfiOevo(&=S<;uoPU=VxvQfq=*1I`stft4 zn+K#t*n9H)^)FT72z6n;s6~i=gg)M^tVpU|LVj^4c?jGdKoibOUj9<`iL~Y3jVqOX z6_^>iTtyYx6uL0i86;`4@!_w!<|;4b+g!aoRYJ-XTaW+V3o%#j#1*Bd48;zajnf&> zEmh|9`?p(TnJCi;G*@S&d)2%+|I??izP3~sc#DOQ2Cuy$1UyL4nXX*rUn8;^fi`U* z=w&Jdb-DMeJlZRJCm^JdgF+I5le)KjTk-Iu$ebu$(5YU*rmFx)z-;RE5;Lev6K(ZV zo@AZg6d?_oY}qBhgWU-TH$e&`-Ec{xI^91v9fiLXA)PbEQs-Cnoylf){oJ?PwrkIu zu}E|-L|FWVaA`&ROw9_g?mzuBJM*tE8uR%gg9Wq$L|FP3Zjx5Wvv z4_oW$QM}XcTw(9RO^II=zQxO11Fh9b+J={f)rVIwPF2RHc7(gr#*=;Rc51ort(}An&BU8REl&Fn!($%#L`DB}v53hY^4M^M`Ew*ZHuc zuYa+7H3udCPuiG^%3+KJmeqt2d`ii!^1295N@Ku-8nWx_o2LwmrDlhqR1;l|5YA1(VD{c+K>xbmm_6~y z9Gc4IWVatK`y}g85i(>l^LRJR-|#RR)CqJ4UUhQOO9Yhoz=D|u6jgxIF^cl-P4@4v zpbjw^#ryB^&{|R2b3=T5Dq-}<3`V8_r3`Ci7%Rj8Cra^T6-`)2jMuVy2-<>o&@%-F z07clEOQK;Te(-%3T7hdtT`STB>!m1tyZYW=?6>9x)oUq8I3o`p$u|P?<>8hvk`h8* z^R&EXQ7pC8X-2_7Q~Y)u=AYwg)p(WnDEJB6^Hx)mg%Mn~Z=p)*i{|nzi#{VSYmw;) zlFaG0OxLXcT(>E&GCv&izN9xkzJAt39`*kHZ?kua4|c7Q7)$Rs8m^|5J4!LhcN?XC zR@lzW#5RsCJzcu&L}f@%c4!FZkKH(~h{Nk;X3$dO`=fK=HwAwgW-(+5Cm%h(DDQ?I z4q=quY5r^49+#_EP%+k${z3yZIQ3+HQuwF&87~Vh#r%{7zu5ZgKu_~E<)f<77xKls zAxFmT)xejVQzf(d+3{HUf|Z4&8w=TQs)~bCv|Hbha{^wiPoD%R0gO9}PLs@4wk016& zDv3(Xe!|h}s;2p35?uyxc5UOJn2t>84qd}WE}2ol(WJ6K`M;do7FIw7tcRO}O(D81 zjWG@2VLc}+P#)LPzKdkxF zV|rBt#y5C8Xe#Wg%Ih(7w-fHyC_v28w-7hlV8HQZ;EJ4@=Sjy3Yjk#BviIu0+2+TX zzVuLgYc&SIrT%c+`M@D%N(GQt0njb?!B9AFQ^B(WPQVC^b_6tm@9Q44(I<|ChhzJR zoP~G9Ei_9yjwYv3APE;eVp<<`PUJ13ECN zW^f31nCRm(*W(5^z?gz<1$K6L9(I~|eAAv1Wb)|0C^k91g%b$#Hhh;fYeC*w2A(NH zs+@JIxHd_P3i#aufDQ=;1(XUlx&-fjuC4yYEDd;T0D*TDFx<#o49caPsQ)+dS#5YS z|3g_342p;?AJG_nz%56+pC7(ZT4E!ViZRk4b{S3lwGc$#K(@Y`q@&gBe<=CV zv91vn$eKMZD+q@joalj;-c!S0V+M<$eerq_Q4)<(&V0weuAL&gE5uC9HUAC|?UI248=dM(IYV|*lH7;%pE#kG` z-->;1f*@%{EQPN|PBT*2?%u;AR<^Z-U}`=zef5lqSe0}hEfS~ykN!6&UAD5;a#Z1MMiXp`06LMOGU^&G67dC)DJ+-D1HD+i`HHzGX(2zsXB7=v(r|By@< zDQbh=pK&Z^Q!fsOuEguneG}9_Q&EB5B|I(D@9k4~B%eniZx@IF_`}p~EtlFHQSCA`d#A;c9V`uo|(&8e5Z{(A-ATzqXK2@z+jQuwF8=Fb3K zqIp9bPa3m9Y`!2mXC?eXFBb*L8r}6VP2;muHi}oELfV-Au;DOy##;WurQ`7*1dh*9X6~ zpV{~9=uAj?LF~jtY-%)NWSIb1_#qg;FDkpdF=rfK7o}S=Burc}ONvaIR1s=w(a2h7e;jN*Sf+qD^ z^)gTt(3Ooillw<^lNmZ&r+m_d%EZGpA?f_37TPB&SeGwcMY6n5zFR~zeyjP6oP}ubkY%!IL%pmS#d>9B(u#$Fj^YG)Dc0$6ykCueN;54$Q zz&8T~IWN1VPh7q$4~+~uV7_~-*842G*khhL{iuLfVivC=x7blq8BpnprYag3?Zp4+ zNA8U==M=Ps08W0{r|I0S3O`I{7$>*n9*bM~#AQcWYe~$Pu1BemQTK*bL+V!-9tx%@ z!5)?JLP~GfpP)!bIxr4Wzm`(6M?N&OWle+Ro}lCwn>$+;aw_*;7{=%AYM10VlJR7! znhuVA8~5H?`zloY@P&*kNmW23_p9o#(Z3<03b00udju<8XFzuvTl$ z)dA*RywKXfu)`&QW+^FquZB^9D!@%8U`hr&uS}H11XC2p{eb{shYn_;&o)5&EyeoY z{OEHXoYVU#Z^nACED1fsJ!vFCH1!wK0q_!M+n{~@_-|SYh%ykD$1f&iBYp#gC^4At zSoauM*^ur=(t!|miY9Ax?zi?GGSW=l#{*JDsQWzF<7Z@Ji%TEabM01LMExYRtaH?9 zTow+)0QOM12T$KU(Fdl~V*eI;htLWW$Uc7xe9G$f%)q;bn1PscT4Tm)Wu84OiHIjo zkSU^S@Ib!%89`FVr>*zDe}KmbQrpC1x2*)EdOSe6)?b@3NC z*bJN*CW-WcF*4I=cyuXXLKgN0V}jeM|c2ty7IEUzz&kj`}PO!ibbQvc?@O#3``Jot%HO{W>hIZA!} z`b_$#BlvSZEvadZ|AO}=jzzc;!&Bm`*@ zkVZsc@TI$@8${`DHo-tqKtNhTy1NC2(%oGG5|Yxnz5Bg?!k*Z3-RC;zb9Tpsz$lqu3HZQ=k*IiL8anFX35-^Y7Zd=7iW+xT2p-8{|5gH{ilqddc5dJx`Qr zjgpmpo_ImLj?Ze8k<-!c%rewubZ109a4RTV^+%F~ghYEjS*;V)X4oETzM{&yz!mt!39k4cOIM!9=@?c zCWDvv=0}$2Yb|J&Vpi+H)Z8)!7{(vH0)xUwkV*$&i)(zoK*zeEe%>9w-udp;grM7p zI3Tb(a1p%mh=P@Vg_$DDU>CT<{Eb1UHs4Ha61w|9aU^~TW1ix4F#WAN=If7rNXrjx<|HeD>lH^|G(6%&D z!K5FaYj?F^c~JBP**|vf4-GJz0vya&C}Ei)FY~Jnf&^+TM?~&uIkXsN^2sH&)=J?6 zB9Xv&9mceB7B)BJl%fBMGIC`yv&GLEl}btp@_;H?J&$328F4c z$j6mLwT}JD;$Yty@d_{3=!lOy{g}M|%=#(9_z=#0x8+W-60xp_irpxC`HzU`5Jp|-DHJo^=kjoe@Bd~%{bn}f+VI4Lg06;oUHIeCp@l zTX$~8J@@$pgsGTN``h2yEHNfd#GxsJePlX?HG%NU)CYS0`_Q6H89v|14@*y)AV(QI z<syp~I6g#xQ-@Yc*0;=ytMDq+`jm=UT5}A$*1qUNc%g17VN=5j3+_^eUBd)prBv!%Iwv3WFfBKbH`Jrk1}@n=zUeoPKcX`3(8~)cuAlQYpJIz{ z34G7-Nml&$73U3+iWM`gs1!ywMuDXuHug8mOn8!#Rr`8`yf8+77#gURTLN^rV_H!2 zT^BzeArnqB^E(63<3S{v-IAb$Bb@$>a2UAL9In`dWfgbaRHW1M!Nwr zAUDbOal|jt{fTC?$Mz#0wudbPH%hIr>+%OW6kWk+BG@$1z52sBl5_b)r~jtMGo@YY zqxy~ne83StIIGD{jo<{^dxA2b3$WldKF;|8qXS#<*dL$%xKVN+-qWNpe1ANXGH|1f zN@H0mkeEsthKF+MVEGt(l#^^8ioQ{vUvGHijIl{VRilTxQfD99mDKLD^fWLY=k@#n zDO4{W5G4I;Ir_K9i`PwIpoz^vQEsA9`1{rX3VJ;I$nPZu9&*&w{++K=uV_7=7o35P zb|=sK9;h3Iy2X!{<79o-T*roGUafHRL5|EI{riznqS`2&qC?x=A+ z(pE8n?N%k%v-G(2u%P;6tr* z$U#|!i^e;R>pR<;;O$SCTm&M4Ax6pY-{lE;)(MG<@~1C}fuD6U=@#DkY!G{wpH!+I z`|alp!jqhnzW=SA>$7jkh86TTMb$-k-^)Jfd+_g%8PKl(L!FbBcbtV!^zg?P5Cvr>Lb zWXV(BAh$(%(#+q1hV@Jxe8U!I-9J`z?j}By2G6m~J zLU+GdZohW)SsJ8*@7|0LB=Ecx9y!RH3T|(ujkzyWC@DDHL(jrM(252#IbQPPSzqSeFa*HiA!V9Sx=xycN`vV^{-%7 z3!E!g$wpHGxc|+516npoz~Yq%!6In|zeHDrqYeayn=DWn?$_Lo>*5x<}AsPfvKY|f+UY6BeQbPlONj`4G|LB7=K@t>Lt zOacnBmuVld8@;Z>B`@V)TRh3(Kpb2N!|#P9x?(7W4AY=l{eeE$I>G;zMwzQBU0Ky4v z@|qi&3YWulx%cv^dAwe2cU@!LJpSO#wPUvBjN|BxR`uGGgj$pK8Q))F1?jgdLmkXx zdeK?z)P469YZuw-xv$3WS6tw+UP;{Kmx?Bx-$xG?T4;sp*vz-xP#X{kQ6hS?uve=X z3%Qy}qNRESBF3yVP%&;M98}euz2j*3Wf1Fv-S0b>{<*yo?#ssLdtcr5PeEl5mlnmFy-v#xXI3s{N1(O?K@%;`1?8l z86&;wa4UBVT+ls$nBKG~WI)`6=F%)!Iv)!-hjg;LN9P$bgHF^J%vjOUj#B^I6 zCzmGhvJbalvUJ_)&qy#8h!xDjGxtb}tq`mX*pRyawir(5Xn6<_iJzSGRcoZkUqD`~ z(om_VY&5K}y>{FAH3ADye^cf###`eZ8-h-dn-9l_z^>X4<|uWhELc^)J6JB?wmEn5 z#oC(X(YE~kAUjI;;5P@XC!s;(GcSbUR_55QHSF(t$QecMrZDEhTM*-E`!MQE5EuAD8K9!yW0la z@7?X1lw7!ucmMUtqG-d>5g3`+c}q4}$!*K8F4gZJn-fj7Cm{8Miv)57l|j6$Q*3U@biFa4_H%)yFdyZ=egBQ z|3@eMPp$MS(?#PU;YkH1qi_%bjxfF|abIl20ICF$@34y_T*l<6vcjXYsKqcsjsV*tKQhY6{=+PZb;0?JpKom% z9tQOm{x?kxXBFTrQ@!|&SP6j31YPq|AXjBg|Eq|+#%k>08BiS6)I;*m<0Kp_(_p#$ zDWF$2496KD-+qp49~zlblYDsoxryk@)r!PnsJE^~u524Y1iEG)PxE)ONiS-kDhlXu zjHrP@n~@z|o1r4gM{0mv2l7cys-;nZxs3C%=mC1KA@Px`#6J4E`elFBU4B#LwCPL$ zEx~Bu8*jq<>PcrDu|f<82Hm8qo;Ft>8M@U{v#AAh`#QBb!!86SbDo9k!Hf73_$1Nf zuq_hX@e_8|!ktC}#D(Tp%N(^5#svm!kLxq4S0H8m*Npwh?fdQs3BuL~c*3&Wtp$lC=ovBkcqbQ#vAEmoT2E=GxW+m9 z@$R_$5RbHdU~M;S=GOFwXo(z9T(vJA>VuFtEuaNq4Tcba5@@At__STf9O_B6KQ_{J ze}A8dvTrf7iPcpk@q6gIbOq5r<3Iz3Ym6+V{(Bb0O2RgK?k<#v{)jQi$`RX#w(l0b zWDCmIU@cFY+fZ~5DFma6ZAADf4y$aYbfbe=b9}mAw(?tFA{F#+kG$;1+~g?{Zww|N z6Oz81w6XHoPNVz6zZiV<%L$yKr0Z#NSL9=xE_%W8G(*@P0WOO4l*8Ig7Wa1N=4MN+ zl658LyrryWv>=Pd? zLnzYmQ*Qc4z3uIcLo(JUuX_8iAUew|4s`T~A76`{g?y((gaN^cQv)D~js2l1H1>zl zr26yW&;;_F+JXGzt&wDlUyjXgG_Ms*MHX4t$tH>I{}uc>`uqREqwSf z6C9@oC2*P4Ir93dWJnAbooPXt<(B#K4mFYQaB3T^58F@*HEs0Un_daO`8QBS+ufX; zSLJkr_f>I37&Rwl>!4joAuV{lTLnd+d&*7uAd&ID1}1_0pd!6BcF?;vhke8kj3x^! zg5G^I%*wIB2D*)p2)Ki1LbEPpKoO>+HH-j!0$)MwKJA?@45-{g+*P%iT3Vu1gdfkc zs6Rp`jS^PI5uK_vu~5j*;*_0+)YGceKQH$yyl>#tITmEw?SuX>4|N& z2#2k9DBC<&Y;}mdRmoh4Md7;m7hWmr)-e3(>-fmXW#w2=)7Tb#u_@-5+p+XTWA-S9 zuQ!aiD2WBX>f$A8;$X_w6(X@4tK3bv_>s-X%V7~wQoAfx{9YP}@YZ-Y>Hc58CHAt? zgADuV(h`*nHTmd$QhMxS287!CEOA-37LxO^xBk0@oV<5i;Z=^T{YB5#tR<}&iE3|Y z&8PI<8ETE-#DcnCTk8$F(9io2ED?R*78b6JkzSKc`#>|Vzk+XbAKsT>vG`vZ6d4E^ zQMr@dBI5+qd}3^J-?0(A9M%cOxenlu%UMv3i(udqdv{=*CLY1xU?^BRd;X$QD?(%W zg;8b)blJ4Ca>BAUn$Rly8y-T3xAgt!dg zY?TfKPW58egEJjux4snwUn2jl;p>LVT-7%vGIik$5_m4LtoKsh$D}iGAmq_B(vRiW zWqp{*o7yWrDW4FHC4>{zF;>;b#jI8a?3cON1}ia6Qq*I`k;d=}xY$gKiOy&XR0Wy% zvGcPcd9IEt;Zyl0)xvSAceS?y z>UUzx-Xonfo2SuSd~G}z?JpzXm-iCxP>MW;x%N0cE?*qcoa?G5?EyEk(>f>qhTF-z zmWF_RGe`LjV|jSz-}h(zg$7h&l7=PFx8>zk$1yd<=w=$WX=iTrw~v*st6qa*<8%!7 z&}t+oo7?wZ#ZF3HEBa*$IEYxB*fl~3C?tgSChh2T{|veaXb*O5+G0a2KNP;cJ3J6p zo!2t-P5#DC&sqdU@Oy=l0XgqQ_!~Q{O9#rZ4{xqPq3W7}cePuLDvFk&;M=0!i~0Lu zILz7%+1qPZOa!Sa*a{uAo^mT;2^`1hrS+f%tmU*V+TF+#e%+{%X;g2?rhy#uGby+F z{Q%XPx#E3e6ZGL%yX>w!5|o7mZ95gwF1lz?#a*7E_yIeo^2gXnIl3hH7Xg7A?c`k7 zp5@txGNMsg8~ugYSwW9NRf%TuRHCN{J&7oj`h;SkgD7}7cxD#WzWk%UU}OMwzoTls zVd9(s6jbuwpjpv*fX5KxB)k9a z){tw~?PoQ(Qtq=@u<|IvaZ4uymq+kaa3}-3vc6YKK3~8;97Ao5tdQ zy~`&ezg+j*UzJpwRodjVeP%IclBPaMobumeFhi?z|G!=J#v7)!shscziS3F zFlTs8C6b@toq9#d>9?T>JKD}C1c))reyKyYRf&xdP;wZkAlV@s0@Rj{``_9nzIrc< zO6>X>_7iKD772cfX|;i|nkdfDpIwu)(&uKIu11ZKu7*W=hQo}?=_?{J;am5CbQ0~v z=#@4{8q=f4jV2A(l5nM>KGMOyLUa$qFA5+E$8GBBBqPZ($)~Wdi3mF95mMFTO1NVW zj+R_WU;5U|WvX-Tt7$?YV#;KttBt6<%X9yZkL>`A7X)uA`l=qB$TW>vYLh^NlQf8| zX7p(eu{=5X2|#J9Pn$0@DE4-6W0quD8azk8G}AK!86zVj?F)5aa(0{&Sa~u3kGTV8 z{s)2$XQi%g{#9tcWYB_`rG2K|nc)E{>wm98N;zTUh^Vyl_ta5C~mC=wy#Ah`QN z_u7|xoJB{sLMb715<=r$qf3^>e^RVewohC`8tO3g+`tDh{VON1v`;7AF47bP>~4&|-$*4tnl zdECit2W}dW+=6K|U=$xnh{(c84FFU@`NmZ<>(sWhg`C43=X zjEz7jA=rzK=B1%Bz%h-yg=G;HX(AEwvB_imO;d_QU->V*KQNT(#QErnIQe@@ok=cU zi3S*W8#=!(;&JOU1K+5B=wF+R`f-wym~pe#EG8`TBs&; zk4gWDeK92v@!XSJV#_+zw}f@aVMO?WG{F#ZJ2;7@6! zuiq($^);M;J&o#~G4#<#qS^HM*)6LQWZWRgj(r5I&D&XjeSC0;3 zNNqCy>LQ^RjnH5fe_bDkYK5h=xZ_~5`zs=(yAvJ;lF8s5wyY0wIl*-}T6-G;!-1)Q z!W%YINPi3^IY67#6VVA6q+q+fcy*p!t$Q*`faD)70xDNPhmdOc0=q>NyTu>z2;U`{X{mTTd%{~BPA&aLimFLh7t3Oy|5_aXYv<;;%nY~lY3WD* znZ@})bBgjmhCg@bjqF7)eP?EUuzqH$EZ%&Q_nGU;%t^r4xVLPYMaGT>f^Qzn>F^RU z%sHS(bkb}?X{?r2j*ZwWP(2ST-^qGyJ$l2`X%EwTH#a$NbAoB-fZapPg^t+ki#u@z z3rZ0gKZ`(vzL05M-F$uJiA=2a}I%xZ9$Z>?qJyl$C*=!QW66#Bj zy*`_iRjlbe2FW0LbKCxmbiA|+*Jj$GD=Pb!>&PjqczD8R}~*M*xMr5nGqNxE`;XczpEO7eCXtrjJbp zH&n6B_u=!lD0%S}=}bXf}>D^>!XTC>Y{6Ro{By{ z%ESJI`S~aPDGoYoUh{1a(jA+c;wNKB*$SlWJ@@Xb=TNWbBlr-Q!TlQZ>8Vy4NKNU( zri|l=f7k~RR#oZM5T;5fYS1bF=Yu#ouk2=wYl+{9)9rr@Q4r=iRyV#a`Wv z%GDPlJ-D-@s!7DPv!pZ-cKOkgbl%*Ad$)~Z*KV;?6_Kf|C<>?Wgn(SP|>Ba zpPUT-BWp;VV7y3H4uzFmczg7&R;bWecbT@Ycxe44 zg~SN8u1bmT!e&`@jDCF|7_a=T2|wCTOhZ4Hk#g1Idn9qA9L5^hgaHHbxSUth{|x#h zAnkZ|Z$U~!;Wst)S*`(|NHGZ^FZM9!bXZkHofQ+_s6)y2V`}w1a%Ef>WKjRRx*Jjb zyX}F^i7w7fB1GKWJpbLI%MQ`>SG#g+yAMA&MTSSr_^+iO=wZsKKhymM2@iic+a>*T z{L?y;njVasmM0tXlls=Bxhc-Hqq$22t6W7l%wZ!`XR4&j|PKF(Sgn!t!d{}G#sZ`7_^!KvzT)?R1lXIJ@ zmjpdiR9l!BUOQoj`rQ&#kQ41*UWToFDOr8v-f}xjCBk)85C60o-Mjp^+xRG%uKigQ zZPR~%^)C?Yk;GJa%?XGNauzvM|EsLaDSN=#=I*S92YOfF4iK;c$tt2iDBPjdnaVHy zH9ws5plp2Q9VT`^)3sA_oU&SR8_x=}B|o6lA~Yuk9*l}J4~u=ieYczl z6@vK~MRD*AJ#9a6(jN^Dmo)SqU6fHac6pD01;A|xkqn#tA8Iy_t3)>Ae3%5LS;V$3 znB>i!E+@^O1}vJ(ctJ0|+^h+)$HgizO zX4PEUciH~N(`QP&Ms*V%mC~Y1f~h%P`}uW}Av-U|}d`va;XD<3tWgS18+m4wTFR+f9QIc!X@u7bEh<)*-awi+de9)_^ zqST74J=`}ZkEusy{*~WZvMTOdlKB=TWrz#8M#q_jMZRLH9nC)1ymXCtkEKhu%%#y@ z%A?7ifZHJ?ozTLF{hk?sqV|oW=7PxA? zfd?&u7n&>x+{6K^gxFI^uP*?@1V~}9;U4VO_UP5;&qfFgIy*u$@xE_7U8?I+fpt7o z%@{q$1o8N8P$UXU9az>l-?e=H?U<%>sekDnmzD82S^KsJZzka#pV6 zM*U<_Q(ETETZj6+K}9U7GUuA~Q^KqS(>iP)Q{ZNq5Dly$K8#Vih%#-|vbi*T#}6A4 zAgvjajRaWA^9%IXmrfjA?Lnx+ps*^3?}#ry4`iM4d*}2nHMC=fQ6-;KNvL)9uJPn_ zmF69v{#ogFn;87k5H+~(NnY6MwUI0Nt$iZodd+m=XmBCle9=s;ld?*I@VM;alc+;k z&$R}G#BFoQ@uU39MlB=AJ+*BV5hvp?ZC+W~$xkQ!abMw1x(l?;XUT?L>nJ_V6%o5H zbF-eMctAt{i)i>7U&3~reh*A00tb8s;Vstwx;(l}lw8R_Zr-ID{sg_P7x!x9jln?J zt&~1*j)j$ZTbw~f-`HS;sU&9%zaY-HZdj~ujY1U^#LZ9yt8bpA&)&**QHOSnVO7nt z*hyb^wd1DU@82vwkQ@j(A6A-(iyo*JpOHDG#3;MuYxG?(Xz6g1tt_9wZ&CnjU{kw$ zUorrhGDA9S+NNzeunMGQzou!^+FT8Gp@cP}98G-^-q0ejV|^@^IGu~VF=cJ@d4A*X zxy)idek3#oY9TAr3Bynw=b`q5c`D4aqB5}ZnG~3oa6u7P=SMqeN{UCYsAe5}ascRo z5T=Co&&CF^XtzD*%pQ_)cX0E3{YY`%F$GBHr!luWM(!ux!Xme)-Of(R_H$|iDbMaU z)p|E6g&O|3D_8~h71Bn@@_9r_&EIrwoLhLWY>!=kL~z;Ob5r$ zlO0E}Tr@wnXjK5l`?Ow-`{d0g zX1kGs@iIVc4WQW80zr@EXV_ZaUFe2F7@PC$!kXJZ2jT|N#i(RI7Tx<#V3VHGh(Y`DLSvUxCDVVH&iInj#~ajk%7G%2~PK zp?wbgptt&zqdV#-Bge#DV4u4B-_ARlgYd@O`D3sc&M$9m?UV@M62UG|*K zHN#u$gUW^>ftM zrKxm0Y%r#%Q&@dUS7ZFUqNDwash@P!dp2^B8?{K>R*(Jos_o|u1}<8J-V7#M5n)+B z_%-AAagp*m#q~dY!{@DaLeC#P@UFZ3AyG$Bfu5FMa2BK7vR%5oZx%D{E- z*|E@0Rg?|;$@gQ|e>47Pzx_-+N~QTyV&{pWe{bpbvqiq!4)?*emP}Fg!?1oo!7piX zIlYE`CL3x@-ov8Y=0~xToiT>uq(_;kv ztrmlmt)8l1m*pZW|37&S0_v~{&8(9PfYhqL*&DN0po`IqpoYa^7et)!S0=s%|_pL|bMhTa}9-o3sO zDfKd&gxoz49lRdWeNSFac7}R_c!Ag0(xb6Qc6aCMJ9CdOHe!HrZ~O#Bj4~tdu+#5C zin3x?6g080ovkB3W<4n@VCi5wo~@>-R!YJL9(-`+dQ*^*@+|OpX&5>>osfZrMB6xP zZGlxR;0b{Rrx^Sjl0jsw{^!z9sDqM*0^@}43_f4zCF-01@|9JEd6O5?{it*o_Ifa})-38Cdtsi;P7?F!W!hdcl5MxT(stPo_nzRsjnl~+d$_de zuci+LwUkSYPoEdMbun=pzqKuqH|A4xRe~}N-~nJgBite=ziw_RV`7q;p0MFGT?HTG zs;O@U6&5{aX{tEDx{uI5+cjOEw5}>{6pnK8nx(?f14D zq^Q!zR*2z$_%da6;GanT`R}9-+lBq^HzCh)3WmP3A{-nHeUM>vkoSC5K1(y`w=gTn z%tNN$1uPvzPC>{1C(!x(?%DhBRo~W`VSL`BtE&LN3mT?KG+gD5tQq|f2mH1)ns$FD zX%@;VrK~Jqg}Tf{UuB}B2tkoYUfl9&?UsnQZNdOuB(;0YV0s;Y~7i!G)b|$U` zK$FRGSs+Y}A}Qp82-&?6uG{F8iKd|Fxcx)R+~M%=F}qOTC`F^@5>Iyca1X7Cj|8 z3`OHKgkW#4W5Musxsulj&`zjSbP|Ghi%KmFG@+#azIz&!GPW*oVHPMX3ZnL`#{M zs4?K_q+L3D6L{DzYg<1={c4HMibE=Uqj5)h=_lJs8_*B|YV(ALW^x$Iq6z={Ay&P02pB3EVaQihfka?7?j zj9b;laA}dL^}jTS1|G%7$1_Rz>;<`fF)aEXs-c@RytktPJ!>!3LDKpf-r8sD?QhwgYh+Z&e z^$EN->G?Oa+7LtRLMFt8< zTAyNSSXmo5X%P~PAXZi%BV4K?s1VW*jqpb^IPieVZ2Sd_-@&5fxc!c<$Qe5YG~u)V zjOwMH>!Co#opk=MlfW0+k$xR5b@>-A`dD(XUClDodAuLe=-A0M);aP!`hA} z6Dug4wv_pDMxVsh5<;41Y4_KRP4|3YJ9x5f^C*@v*y&mR6);JK;r(!who z^XM3P|Iu!R0$AC2Iu7gC@tZ+CT+Fpj{u=GFO#p~b6-GF2G3PesZbkqocg;RygUal6 z;~yD61OUDSt`2K>#ct4tq1}UsK2^2o-KR1uZ8IDGUw`Zf<4STgQO}}na=RMP`lfA< z^h~nG*%C;>>GjIUZu(UIn(V`cyN>W|(s!jd!SCB6Y|HZQzfjx#Cj}*k@q_3?V#faI_!z&8KD*V8oP%6vpugi1 zEy((yLW`dz{CinP7s{0R9{o-o#rRVGzq5zr4*EuZ4wnd4D)yUD=X{WbGJ9a7 zQNT5kvMP4)P_Tcl`uk1$y(Rcq?e>lWf0Ov(-vQ`UO<9g+;qa+`RYvUr@{q*>-S{UH z3(VFnJ%Al`T$O03%Lq&|iy2}$n$<^ywfPtq8iu~Lkai4*5K8moK#HGgcWvDpzlkVs zm0i#DBZb2OK6b)w7STu#-i7)QB|ZdOMu1-hd_h*;9DYy{_dsQ068QyegM52Y>QdUG z?`7xH>;7ZM!NDO@-FNERV5dcRnRQ}@#jHv&c>srkMJQl(#KXr$nBxM(;xsvNq?^E2mI&+d%x)>u% z;Bu*pl!lk{Cxiw7i?|a68YYK6dCA{Be(w1U+A&P-Fqr07ITt4p=V`NJKk*QO-?3iX zWoE9ty{`|;mT4<`DrwT^xxNH{SV{C$O}D&eJRPlk-?_k`<0woB5BR8?+=?XkY=;xx zHRav~eEd<|7A4ri5%c2Gxf$odk!5b=uM0ly?ZvZ0Y7Xv4lH>P%E4Cgyr%w01($FNr zsdaKL>MGi-ZEtYKNT`U<8IzN62?p4LY-fl?`yB=Sp2Jb<*QNZ_oI`hE{;S5@xrh39 zB}p2Fhbgyr`bjtzWLbHJ`F2_JckgLRoD28Ko^INB#PqlOx(}!*|IrRMYlM@$#%D$VSvD_(0PqV~84(P)C6A9uUIJ4ox=)E$EWMr;eQNhie_{jyKvF5)0V; z4lC)e;GXQd-r8c_jo-9>Z-Xl z=J<8AHY!YJAYC&Zg1OfLIZ4E!L!~Yv)RJ%4+SB+^J=yAgYnh7i)SHX;=U9l@!Z(%! zKQq7SzY~VoF0r$rwmYF=6ui=M7jnIa__r*F~@auQ2kri0jFpWh)svdW7S*ngyix!#8 zmIs<%ymq#c@S0rZNp%l=7 zN@8FP3EMSWsn!CF)s&x`eWnbg&#NCE3K@Qlk)!$rEidl5X8%3(o`jh0OY!go&uD?} z#7R?OKTm{(zX0sh6KMYpMGhZ6c6Ga1hb1?C2c!^eSu*xKtU&F=C z&0k^fjSn~cm+@W|5R@vm$iehLZVNM5EReLPyACto^38E__I##6V}L>96cuUa)aaa8 z{oUqIiqYBD-{KrWGQJ;CgQH;iz)6yU z53w(upTcZDLGZVj78qMV>&x_MVgINerB$?B;lT|)oseStUZ z<@)Z->djo!!pO@8u6WeQ-8T~2yYr_}q8P?zCo@89(Hn^z6h974b82K7SsANNQKS<{ zlh$sFK77*i{>bFNT_r_~9a-f9?v8G{Hdzyi7LOa><2Ui}=0LvaW%Gb5t+EML7AY~Z zSyR5TxIJo?iPjV{Cu6ULa8%|7+YO`btX#Z50iffbb*{;CoO-M-ssn2}i3jx4*LtptT>Zvi_4EAy};|&5P5t zuzy<*``txcfyqBg>vf&l(Jd!6)z2`sV>GN{E#ZYzgupAv8MoHe$S0L*da|wLTgB*@ z7-@0M85srLdm(Pv30I%Xl2cUq1ecnR=>|M^e-O7 z;H!%LXAC%t-94KrC(1I~e(HUmp5!BCvoKD@q=2GrnAMMw3Zoq@7Z=#Fr z#wKwu;YzX}anX!10pI||-uo*v zvB#b@Zt$9k_D<7xEX}qJ_u}I5FKIV?`lI^E14yJn4$i|&iNzR^bLp%l%tyMP>jMCV zhJW+<1}*VYBk-)M>T!HqtU2BFLKcVTY_!y}lQ3s-5i>8Gc@fA4V$` ztm7o85Yfxdd`VkgNyW1K{*ZlWH!q`|Fe2+6-GepNKjz^e_i5GTKRC=^7ktKvg@Uem zv14V(^v)9mwFeYRtO`SHG?uRyQ=|Xr-A2u~)1u|&rkmir>OG}-YbSZB6`xPXQ766k z#{*vyDm{22%fO?ej*6Er{-5Gb3Up`(74&%y=*{k@+6gde9Y2#yeYRTBzOyv~0Y}^8 zuMHV?(78vv0C9T?5QXHarUg@{Df@f;j1YwF+~r}y7H7`eEP*=UBaKG{N(B&2C5o$O z)^0Dopz&BeZEqQ~FV+juZ{0$b{#9Hp>bIhcP$N}*o8BR=N;3Mf0z*7@DpN++vLD4( zs!1xW9F@T_uc8-twkB3HR2||&@|wiS{}Z&&6h^eT&&*|mZ2&;nW{2^s=I!#(m@!B(oqQGAa4ooO^REBo3BhcG=GoSp-mDcGEov_{qZg7CHcT7>XOM*?pIXy zmLB&!MW4fA%=k##ND&`}XB1wfPNCl!yS)f}KC3YQ;X!mh9e{eUBJ7|{Pc!z?cmb8c+O-w@2qBA2g0s= z6bEpt%F~~`@RPX?24METi(gx$NABurLiBi;veqQqlKs=H+dH3B{BGk3@peoHQSex8 zu(Q%p$nUTxL&3d`5}U#J+vq)%KF~S-paFZ7#Y>CeZkV)x6tVS6OKTWbbniFYmK2zs zr-UMWfD<;L#6y_zK?{>SphpmJCAEePm=TRMS)Gv-><5&-Jya$yZM;^K@gc+6S7~%9 zhKJ|@4%9$8mAAPZ|1aYd7LR|Lk^@wfR$h&n9}rpdVJ{k~%O$b1!m^#9=}YLB)e&mZ zy`EPaCi=pENw6}}TI|^mzKjgZ7NO;}-#rhqKggVfXC7=5a=8f-_yiif6t<>j)d9r6 z4RTBV!$oY$FbkRY_>|2S7;4@jLp;w+y=>#zy_jfzxBJqveqAKp9d4S;M2|<~-qwg6gD>S#K3!GR=QSSItHrGkp;at{oc)Jw!;?u;C~hH#lH3mViY`*YHzh!UwaRiP1U={S>-HtLSNDGUMvl+l)s+hddmPY1b3N^4*aprSbDe(_PSU+*e+SO!1TZFbsWkOXL_?b# zyHK1!MUpRO0n>=tyLkQHq4rj&xrv-}rQ`5md)AAxS3RQuQ0=Bad0_X&1nQPH`qmaW zP9tRs-}2BsNmuNUb&H{%%b zq#*E5)s#S&_rVN(B_*nf9PkF9xhOvXz39RT^cd|tJUk@AZwm>*PYRJ(tq@c(U=xXVX2A^F1TS zsSvl0WuA|`JPS)jVi=x00F9xi1Bt^aisd{gfJl-WP|(a%9R;;{8M0{c6T(lPIs$JH#tP?S*7iZCD%t7yO}VIqQse%@dy59+d$V=+Q55@ymbfD|?f5ch$o8N<$`|`c%+VO1l{@ zh`z~44w=k4y<-9DOK%=WMd+z{ht36r{ViIU=Fkv3%Wqg=h{rWM>ye_*e1ff6|0F!; z5(I2lwBg(V(9;n6$GCvj|L}^IdOB&L8Tv|e<I^&cdiNtU7Grfnb)1V!)54Sjwcd5DRE7HbWk7UenAxhye+z=4hJs1?1;NK7YSMu% zTo6Fg5|U3{Zx!m@{MGU&vt~@mfp^<^GXS_)dn*i>^WZYFn6ErTHz)bBK5N^dMghS( z4}S5n_F+tooFt9!cyTtXMLo0f5cXP4x}ZScLoFT}stdQ84s)H>im^kop{Ro$a(ncj zEF0=m6E&;Zj6yGmrvCO6gbvjm=>ni{-)KIa%E>|wqm+J96LZkXiU99_c!&Ioi>rkNtCm7}6#Y1o`6RRJLd`-7$CvhMMRr0#h?6O5hgl z)-qu*k^&FU0pPRBGblR%J$sI^>3;l@=lS#do&234b*8???y_n)1^046JV+*woD%$+ zOP&<@qSdAo42b&TGM;;6f!m&3A;!BVYo=*%l`&?`uos?O&)B{fHKCRt``titiB(dy zS|)%Eb{XJ+`YQQ+Oko6+K)UBOE^@2e%?wUX7M481Rb0coN1~Gd_FP21cEo7DgIj;Q zN=S?=-HvL4>-R!blTJyqxopc*jv}ZuYH!O8B{`7|!Fwut5%Y3aMOP5C`Q=1*TSNo= zvP?Xi>IKZddES3|*?Rp?x*&(|Y6kcK79)slE^sI^=?Fx5<=DoHA*sAyWezsx8Tw?F ztw8{wt*2?lyR~q~f!cMZ&*cK`)vS()&zJV4xnEVWm^)pW2%Lm?iGV;RZWoZ;&j`Rk*67IgD?Fg23@HSjf7{J9uLzPq{I*C6%}ZVY zyXCGLZ0L1A(Y%Rj)qJ_|5*QY1PW4~`0nrq|;-XVZ<-X+axh+`IrZ|LV)_c_bf?mvv z{OH#yxulCN0Av_q0^)k;7R}N%WuV^Mecn!8LE$^!YiZf{=&C~+ETHCJ4l!Wl`oDE->mDfZ`=S++&rLa6>dP_zL#-vOJpgcP8 z(U5oZ9;PdJU_gxnx4xCw1(o%t!X6vFBXGQ4quuyS3=?=v5%RK>l}CV&l0ZJ38``H{ zHApd`+&Nx+vR@@raB{hOAl^3;&oYL80HntU%$nUEziV9Sc;tetg7o%ptNrP1{PA8D zPOOBA4vzL7JA-7`-7{pv>y-WQ6rx#TN?(UP7?MDb(?M>8R<(5)n;cNLGT{j@u6$ka zXOY|~9ueDO)Ek9FJq};M>(=?LQIP%cO5k;MWZ2C9zk1s5TV4``ecr_mOp_MZ%C-he2tjS!;wGL zhmeKYf7+KuQ0^84i@e5)+_tCX49ZzzWb43aHIYoFFYF@;(1)!5_?L$>Gul)#P7J>B z+#z%CX#b@?vT0BAB{8T(Y7}=D@k0i%VnypqX19Pm2j-#+u$gvTp52Q>pQ5`F6J+(M zQC$!Z&}tRIcS?1Uu0<~n-;JcUaBy~3#s#V{(2_+42Gmu0kQN{VHVx=K{->~R(f;O0 zAD%p~HP$`|@H3+7lSBTMLsF9l45?(0Um5?7*dR%Y<5XuVewSAulaHBerEKuLM$BmI) zS2oze%Dl{LZ2<#tp zT9V4(R@>Z|)wkpjNsODxeS>_s^T7a2Ds)>Ty-(x;{=-$hRZXKJ?QW-@HqVxwxZgE3 z{MnLmgMPl|U;o&xg2RFWa6 zSP2U_S!BWl{A3UvcV20dh{a_cd^if{3WbmM6pVn8(n%mn9AKq_wmv`~F*5UFI)DI6 zoW3S-2}uDMAcF*5ZV!tXOATJnIFW*Ljz0j?h38#10zY13hp|Zg75|Rifx1ELDSW4} z+&>?kT=}8Q+?7iz%}{5Vpdo*5)+G}hI?-brNTjk0`(?FYDV2YSFR$C2K@vT&(C`?{ zW+E1Ms+uO;=?}w=TvgFiJ0Jwu0b!61Ni7LeTdE?#23(;KD=eALh(MC4rcT%y-DJ*X zQhY1+RSmAnTan%zU9345x5GMtfEi5pu5&7*NJ#KP(DSr%86W1yWo3Da_A7&|JgUqMBEW( z8N&_$Q%#G?9nE6@es$I2XdEdoAkr045>0X3;%0TntcqtIp99`fL4{ zuS2z}o^`p4`XzN61d*Ee_*DSF5A?A}MEAcb`sGat+`)-|KZ{wxz%1*nhI=P>_iReZ|n1;BX6zIPef84tyYl_JPnnzHV#i z-oi-Af@r~8Enp)p3T;mW*Y)%u+pFIQj&CS1$;Yq`>TJ6*BR91A%o&>OVa@ z#m^#HFm2WkQ}m#Tc7f%cD#LIG1}zl!?{HE!Nv;63pYqN11E%|q3HSfvzNuI6@z<@bq?`Ha*xIeF*`O@TS{;AiA9`k7y8_vX=@$3N4Q6;4OA`ex8zP zROT%Ffyb7x;P!jw?$#gjvQpZcx@tjXu*-Jq6KjKwt7>De0hHud5TgxXDy2DfCN_x! z5yDG5y2Jf9_GhfiqHLx7u0knJFg8>Sr7eTE-9E=&q1$+q zUR7Uz{-+HCO8LbPfSMsqO>HEBK0JW^3T4RxY)XNQg0MBFr@ml41MtK_;!J(*UVGoj z8$Qh%b5gNKFTLpvT}pvvI8nd%Rbf?M9*zA10Bl?CW>q#oP~f=lTi<# zEWLeb-Y7B8In1dffHSegrt8%bU{5Y8KTk`oEMDdGUGNJF%`l}W z(sReG_E?&1kLvkM?g5kQRRM_;Dkp~5K!Nkkm-wmTuQ~E2ZApLZ&?3HU2IYp4r`O9} z0#4}DhG6aQ!Ike?DY&6}Xj>9BXlyN6uko1J?YLCva+RAp+BH5{_6;9zYq;sl%XbGJ zHxF!0N2NO)IR^NPJARsUeJx3MblC{lPm~^|=5q&5g#owmy^c_?=?m41Ixp0v7^bJl zpbaDRC*a+7E%gKaCTK@E`g~r+_IY~L*4WOECQ*u`($Ysfnflnk`M0O(QGpNbKR)yJ zi6O)x+g|#D_nP}nxZ*EO4dU&m(^B(L_-_Na(|T6Fz9cCa@TbZ(#+~rYW#@{K2Vl<$ zdnF1<_gh|fk^C{Q zU@;wI*mJn_gSaZBR3&B016Ux^ct!QV*J?~V zITQ-Ogc|j%sU&mCH(pVq0!HWwZN#Jcs!4HuczMh2Nc&Iq6B+_8$c4Sv`xDVGsH-R9 z_7Zd@kM)vNy^)=_EXZZj%UkFp5!@gI2 zrxMpI{8t0^W#+YB6jp-rP-a^&38qyZ%V4RI$#d?{KE&g~IF}=2zckY5_GBgO)Iavx z@$;>DD8&js($$Fnwz$qKauMSZbc;QWiqZ3>AW8HBoF51ck&<-jlfF#~&x)ir4 zEv@kPo1QPkuf+C<>4la;=MbT#6sZ0p7d9JTFtEKQR^owK&^D?ROjJ3aE{TT)3=Jhl z)$?X?_uItA5#~aa#K~x8kUs`XcyH@^;S4Hm3%0QQSBB33HMpDYzC)C^^;ISeoPL~>EZ41J@Sx8i@CX!n2GgQe>H4{jIVE{z2438awe03THL2Vw(y!_ zi^1w%2XT%a$OkGE%S*O67mnYD3E_~TFWv)kqWl=*`cGShE-Op9kr5MQUU`STp%c#U zy`G4JZ=r?^fJGP&;}vaWayal>ryMA==^{*;bdOq+-X`Al|J}n{*|;g5VaSVh`ubU# ziuGtzY3Za}W;k<)nKBR8eaAytEf$n#tw2?nCnJ4mHfy~JW2JY5=!?JzS*H7M(#MBp zQ)^huypm`53Vopz9kC`A5+qO}K2E6F?~bXYS}n<$-)k=rA7cwRZ zrcyutQ})(YwAQGdq~Ivi6j<-XsK@_+Rqr(uWc;WjuS(V`I#65Cc{z?>!cWfC;3Z7% z%mM()>c=^7_ddE2Vgmn&q84}VfxDYyx`1KDyzg-S^VNne**O~dk_j~9O+ea1Z;b!Y zTF+G8gnEs7wpnLIh6T9Rrbme`{fRV1Pr}SZz5V|mLe~Hw4(zK1FajK{AYjVQTM&hM zBLtvzhXYJ2P8!e4o)ak$2XNlKyu9p)wsD1|VA+j%`X^g)d>ixek?o5g)Lb60>Ec=V+LX3(iDt0aR_P*qNa1I zq*9KzdUpTjEQY=r`6b&r=l}>eX8vqf6u?~Wt-F>}nYb`Az2#yH1XujjA{n1Xj!I}( zPs^6VG^2SvA6E3jeim#SVgwHnUpE=^@s9pNX zPv0K!)wo#N4zMze7qsvkcw_?~BOa^lei;v8i779B_G(~^xX*JqY3p-;TCuUXkMhE6 z1zx=}iDgA7mmr^}l*Trl=)P6)Sb|Zrfp1Q1eD4G*0#Apl`2B0GJv$mee$)H=>c4r9 z!wxkIXAl-io@kvw4FMJ?WhWdZM{B{2TTsl~?-{l`u#8ilfoL}!ZyEh8+P9zr>hT;u zo?LRegov&kG>M-_OVIKmJ2kB?v7|{jKEAQZl zQkEBJ3V?RF;G3EYEz8=vK9yL^q_73+1Nj(R!3PQW}Ymy zUUMKQ7+NY*RwOz+UtCsZ*I4uDrijlC5_=+!;6hH6$4OmUn5x(vy8I=;{iBPCv%rXjnt_crm(c zGmYf95=T2z4Qu*dnHwfn*q@7IBefRBwN>NPF)%-vhboDq7DC-N1po#=JZY|K;;8zoj0wPUAO9mSkm;t^G(($M?a?L}oIsT3-el8 zTYq>w#ygGUUTms3jVOy@28qa=arh~g8^&W5ZKisR*TQ$6NB4Wq7$fZ`=WG>TJ$)pZ zm-Hkyaw9~XKX4b|X&+&sQWkxy(6qw3M^d{)D!z2VKcy>M-8=1eZ@g~dt7#*T*u zrEzOPfTBD+q=P~MQmW=QH%0Z=A#&f16X_;VPvbZKTh>RfdmyY$Uj@@>AcJ4H%if! z{-ZIrQ!h%A)b~)1OEpE~nhaE3O4fGt9Q%_0q6xp47_9u8DEa|>4&VZP5`g%9FCS2$ zx&ID{IwAo#GX&55??OJT^m<7G0?(jl#ONhqZlqx8w9+Lfr(ZpvGPC0vGbq>P`dvt{ z``8Hpwc~wJo)&Sk@GA}OV-bIuSR+z;Fb?kev*2pO*L-STy#;Cs7DP^2BJyZP`};~U zX;n2g^aZZV`#5bFknKW_D2Aq6?l`|MQc~H23()@ne9?CeXA(4*k|7vwq(eM*Q15|v=R z&F~cQA>7=Ar!CYsDtjCcWBvJiEaU3}iyEK))*?3?joOfTGB0?~1JA`+7FV!Q^oQuP z{QyCNTcbyn@|*SR6GSafNz)er&znR07as}RHDXX_QunaS0QxXCxUSb`9 zguM}>#O}k!lftfVLHHL2j$cFr$bz3nKqUdH9%}a?*Sj}y0{UEqs;7*qfgk&#A+s-v zLxhmRIELA>KtB7e)5EY?5+3NHxV^jzA2f z3zTJ$Z;tsmq6?Vg&Z8mQq#-3QpWQ@5lut3NImh?zEW}tIwCazPoGSNT#?ks!Q}0Ty ztBSiX6MN4_(F3A69-6@T)eR}uO`>S`4CSj_?xTTlb8QSe-Fw!4KR-=4Y1!L;DdUFPyv1oN{Ch3o zBgr^X3D~I7wz)PQaNdkg{(;&Ge>`4s0RaW~%x3o3O5DJ3!Ofrp9zAx@!kDr+Y<<;T z@Ky{~^Cw_3g@7!x^T83_S;U(jeqlh2`6kAB0+k-kzVz(OZO9JPM>ivmoYbQ!%R#}g zfDi)tUN~P809X$E5eC3a^1ng#&xR#Hi(l-ui&hyFp?O4oFk(8%>%e*Z9GALJ=?5J_ zpTa;oC5-a6Fvl`%nv1A5LMaGEle0>F)TI0kEam~Ad_*NB%X`~EU?F!Cgl`dZ{P8^G z$Tn*e9@PU;T9k=%rTyGDsqq6d;}L$V8>2+R9_DN~*pa!jME%J^`;1Y!unFUi8g*d1 zuZC-SvH!O7?ogWf+sV{6Y~d>?|4c)4w;kMzA!{|yW-J+=FFLyX2(%B@#P&w$5=yv3 zFr!ht%XhHKDN>r|%6RqYIQ)u|5F2kj%M&?{c%uv)I<{zaCZmT>3M74#er6v7YO_l_ z=86H2Kf>`Cz?@$VMWij5!~wz6AUs* ztVcZJT8sUV=lGBg6P-meC@U7~!R*;6)-u)m7WrBhK33tugF6Oa)L#2TyiVEC{lsTH zR2;QvA8S2$tKXPuXa{5#5&py}zWsJHjnDtd?ziQxU-IZT00r(jz#Hp*7w{-8rkW>0 zx8yXVbup(}B7=1!UF!S^{$t+!191Y{w`)T_Ry=!%b_xqXK7x@EngNERb z^Ry;Ni6eF2Oyh^9DMBUShs0HCp=|5t$PVFCYB z7Pvm+E{yxWt8txzD2qK8mE_+$T5@YycnvuM>i;3F`zJDszQqcl%}61#f(y_uY(FjV z)Pp1}{?E;ItL$d?t1Bd8nnVpzqc!Z|!)3!gpGO=YV|OmFjnF z>Q{rqHC-yMnAH3_1B8_@ zCMt5a9b;s^r5wT{j6!@>AOH28FLNjNW8vT4YMBZoV48BuM@k+)!<(!f!CTZT2hdNz zoiB)=R_eWkd%WNzZAuE1-H&BHQ&i`)ziUWpUyoDacwBKxjR|0H`S{pny#jV6F!^6f z4a9PXugzFU{+l6ZghpHas+m*aTBP*V{2q9)?fLMe$!KD{3Z#dt6*Ni9Q!8pXVWCEL ztMS>%ynoM#nYmc+7DMMly{)RCxsf!}o_?Fjt84scVDqVOI-|W+p$8QxdSJ8DSgq%5 zUwR;=HO3dMW3TioIEJ!>x8x}JQDf00F5qfV&d1fqv8!|hY7Y8I$R2`4sWG=-ueQ(( zfLILbFk8h>XIx7-pTNo_8UxCKw)EtZu22Q>yD{I)3=KdwC(mZc1!Q;Q45fl8(G3nJq2;xjk=m%q{sTDy*WRf;p+`YOD$cN%!Z;9!Ij|R&(30h&G zq&ypJ&8fV1@=$0}5vfc@P!eU!7zXIPT9LL%YsMabEqddg`64oPcV@V0*iFFKOv3AX zuz`j1w@KvC_M;ru{3o(v6j_%6%*=GGEiqfi4odsl6AR*`e(#=crXnku z$NO>rEEQt;LUiwo@;jgOUf)Ox#4$-%s`U73U{D+04Dqd06rPaY>BjuoF@;~NNTb!hC6hQYE>lwbfT zUZ(uIjP8MWI~b@nAL))y<35=`pOtVL&>#GM&&IB@6y8ezd=2%i6i$4DhFR`vJ{@Hh z?^Pw_eO)xsrvW*vK89k9vw71w{#qCCD(P^qSDnlu2+n~#Q4+T!2``AcuSjGy4#D?N z^UHqg^s6hqg=$U@BM*YOS?KI%x5)lsAf)A_f)A8=0|kH;7#utRKo0>li-6`0$)U3# zH73kkb8Xyn_vKc1ls0-t#)!$f`j2eq@hk=+(IchBlD5D8O|v|9Btl_N{TVU+V>+)m zD<4Xz*TNTeZ|Y>#ivLL#uPV)xW6Hm$PZ&sIoRm!!R!_wymyaxE{no)?@5cIu25O*R z@+#t^hEwH-I^u%J#)8 z$rAlrQQt)(^Jd4qaYv;U-E?x9M&XSj*$3%U8|FFCIHLIrmo^ryuX({k^zx|OeQi5-- z!d=0YApL5{?=xBUnRV^#WkR##T4KbSB7e5IzEU@Ct=k#@O90?}pt0$yJdsh|bB)L} z!VUvIj=Bq)x5bNr+c80_8U3|Lpp7-$1K9nBj}iW;4?G&@tt`UVp(MiB=F3tJ0(0vc z^zAa$W!x*%j4eeB>X59Ril#8^QLN~S1&dy;d_0r&G*mV&JfHNDB7x7=$_*fm9=QBd z9d|96&Ax`DQC&r^?Jz#eZn9if-4y%1{=*)`XsB=8;uaSJVwPL7GWzY7!Rh}#OQNfG z>&SMSPRcfVu*-VA^J-x?f;Gi$OMXBFeL1NzL zoL<|!pNS;M&lAR(Pswy`l1(gve563UI5!^JCB-VCLZ}B2N#*xw3m;5!f2dC?d7pyG zQL>zx7&W72e9L0Ckgq=A#6SrKOfo*vq5EUZ=#+FP-Rh_65hoXzIP??*J#SU%maj8DO(9*GF=# zl{lMHU5R)k;lP}ag4-D6du-iG{AIrONN>D@h{+qh`eXx$ z!UaPWqVs_*h^=ax4CGjOpWkyk+3Y^Mx0OZ6E^qWRGVNzb!{p}uSBVjy^me@N!helK zYP<$f5&d^R278w`j+`wswC7W@`|}CJA!qvNXBP2``(fI>r9)U?rJ0sO%|{c2*W~hBXYJs;)#Bw1H+VJ=3t(UM z&%Ul~{4+V1{y1vX6=lJ^^U(W0PA!j?v_%L&&oQ8YJR_zW&_@pLD(>hA3?H8Po9yi< zcAcbU3~wk)#{VO5&wB8xBV#Sg7m;&paiLAeaF?b^q{_H(Pr_;)Cn~Q=6XA7A5$%pMTBH;O zV8rV%DD+~>xU!wr@Qo85zCP1ncX(y?Y#-y1tq2_k?PC%!=xK~8u}UHQR)CL55oA{Y2LUQI421acPo9KocAXLcFGG$fC|+*yyR{+H-UNm z?t2~S8S;F=4o$6Ckxs8CM3Ud|m|e&T%(YrAP{vNg7<5efM@M4gF`ZzrDFJP^p4CV61sj#xpSUl^&0I|}3`D)tci_urA4=lJU z4dIi>kky;<7tI-ChBJgtn7mVL-5x_0^MFa(*s*%IdHa^}8`RBEM|S&aqWYz$Zvfe6*FR4ffCwjQtUgmxY3}jNm8*)!#~gQfxwuqZAq?xj9I%VV=1fTCB&h6J)fJ14yv-Pi-I`l_x3jiJA)Pm zqVLnCFd0`WJfMDi@c$B#^d0kE#RE11tIr{U2sy zB?2tlNvwvz@3jD5gC7TI>!xoO^uCFH!?er)0z>eH-#9uGAhMNER)RJkpEP8?9h`z` z##fEo872O+SAZed<9US*iy4n;m9btwL<*bGI$B{26LE%%hE00Z3TttuGMXg; zo$f630`7>>y5o&xa_!=XjFlj6Gpv#F9($+Kra*7!774#wuYFwH#1u-bSuE06M04}6 zxt=C1mEiT%Ob5=aj(C2JTopx{VaRw$HZ0~8qE3Js91D^QUhRTK436~&g?^gkV|IC2 zMzDtnN%b~-&`2q~IurdP)c3xhotE_ACsRsv46Y<9%lVDD75vkL++)Di3O+dQ))dC; z{)kwa?=K_ZW8Q!I+|x7@VCu7t;l=`q`X%6X`QCi5nMQVKGD71)xZ!Ot8q#<`vpbxl z-eK=;zOzhbj@wv@rPl3cXp6qd^ipL_sG%7Xd;MawYILpf>DVQUA4BTuiRjEOTENT` z7&0%t-TUy@IO;c0_qH$9Owg~vZm<(+6r|2Z>p!rSf8@h>`{_ahqC}uL7FI!P6DpLl zIXe;b$=vK1Blq!z^tMyW*;^*v>7cUK-i%#m&~3oy9J8-S+$sc8YP0gqob$LO^%~qB z+Q+ht=J(mHq83yJ^T<^)ed%q=>^*?)y7e(&oL7P5OPhei-X|=f`iX3=S9J|P8FYx- z>)xOeb*X>bC*CFpsVWIY!**4#Z-n*|wNa`Z0KfbJN)}+eMs-xi6tpMBeX2pE@tRA@ zgP^@005ltwzA`(3f&=nJpmPxrKt=(6O^43@8*Qry(+1N)QRq9K^!8EVPFdM~&zIq${(rsAiHTt9~<>Ii8DT%DfOXigjO<#P+dn8Oz zARp$Pqt4!CTV-KpcbM)bsH*WVV-_A?0HiBoV$IeRV5qB-*Lj#I`sUkGy9_Gw9eUawYN2>QPnJ&_;_2htxnYiyvaF191T}gfaTu2-JQ=EZfi-`5_(e%aDilXas(Pg6F9G-?3 z4Nrz_{JQ+n8S1>AzP0fS=c$Bi>B?S0o42O9bbEjPR$A7T$-`(sSq~QI$iG%-vREQs z>y2x^gzu>(2zv34>>K0}!m9*epq87008vgEzGhi(Qs_{J;TCGM`A& z5}Yrb@Xq-vXnol!D&+qEdk3>@$aU=b^&j4&d;I4w{34fHN)oDVd$a9Fl((BtBW|V! zNU~Ka){}n#I_{s_7V<$8CI;XhzVfF;**~&(Qy zel`(f=`w?q24;mIx%`%`NjQ0RoOhz}1Byw!FlY0?$Oj-(#8Re z9S5C}IX-oIO9Xp1r2(XTyvd)gJ`q>x?J-Ad>UJw7GU5_t1pooCM*^lnum0VrfD?1~ zVxcTBN7{0hK2HKzmgNz>tYlP{nm;SX9-jX(ADSTZZ!z~9)$9N9x3@^N z_g@R=gMom}X%q-xy+UmO*wEDvJiCaIUc5xYXwP0Y4F~JWs`iz zl`a$<&zvxB8$|P>Uc!r}Wxz9}axEBNMyNc&h3Z{x&;Br0ng1~jN7nP73mx|@sWVHQ z{5Kk+Ss`5Cpvl4VTbq)2-OUYreqT@4i!n?QyU&~c4wUxG-s26S06jX$yrKy=%E99ElrCZkFq&;OeOSHtdaVmc4U$KbUFtIF|! zMR)ZHmU39#bB}=9yUEp!d_#Pz!qZ|!{VTVg%21osN;AfGzUI@Kzw3#ssBZ78y^P-& z&z6}P7#GMu+*GaeXI5OXO`C`JGj)TctBSHLgo}XI z{%?kWq`m+<`oEVy;Gy@M-FCl^}n5-4aL}#b1m#=D|SdWUHH1L&Y1RM>3vU~OVtk;nBBrGCQmKLC8G~($u zMg=OLN7>aHUo};-t6tls8LOy&DttU_Tw%3BMg@QRBcm6yn$@A#DI&5LOP!+mzl{a+TW8?~Ckw%|nTqbK4WToI;9sv~!ehBl zh1!s1WEk8xt{_g6%6~s-c`zRZJ1scu0v28l^DWEQdQZts>o81AlEd`}r2nxf}hBkhr9`g6{>R znSy+!b#w(h4>M9VgsvA}cNCA{rjLp_ybS&mD754a4<9}1cK7luyaW1r=6e6~TGDb6 z6q5~6VhJ~V#0$DGTeJRB^Jy+L(g^|c`^N5lh5BZYvVwfBg$}ktzlwd?LeZdEM+0cp ze1;xwq9mFqKCP&$%m30MT514GVr~r4|pSX2Gg1v6y)Z>h$u`W=KH_MbkLFg{V*_Xzwr0D7ok%8Iz%y(f* zpcZ<~ippZs=dl3Q%N((jpA6Rtrz(Zp{a}y@N8HqShVS%rs>eWbIGDGD#JC6u(}}g6 zU;OoD>S>}T1O5%$On~y>ZxyLuZv(0yI=lHqKgGBd^gW3^J%;-suxXa$kdvPt_4~Gl zKG1~l`9m7|KQ$G$a|et6t$uAkIYHc@^+GaV_9T-TvewO9fwPaq_SKD}dFh+Fz^msOP>jUMI=wsSF0i#4&ty?-7jANkK#H2BX>Y|f27P{D>E0gtS?_nn)j{OI(Q zRLy?b7vDinq{DOHxoZV}?wU}&d}8e5`ZFPqpKFEi4ZlYYI%ZIsDmWhC7>!W)8!qaY zbvD;pP*Ymxp^!#!Wfpz0)}8m_&t!(xbE&A4{B7fHF>w0$T^!?@7ytxoriTr=VgYTT z<4!K)LFuFBA|Xz86Kz$;&zo?09Mbhn?W(?Z zPF|1Yc-Sn9`hz~m6F9tdRjd5K+qp(>n11qY%_82_MgEwPb*E^qXNS4(v5kHTA$Dw{ z(lrEv5bF&(W8gatN;Z5rj5qT@Ay84WiIrazNZ$XAvrzd;1LBgYaUH6Rq;FbZTMJg7=Ujz1l6tX z2$W|&TUlH54e`-pe+tF{wZgsHo_Z81?P_g4cQWL+s=p)zW=Q2Jq?`*HyUgTst~(nU zDdnTz6ybjTA;TpV_aGqXDCp5Y#1;7OwM$0am!j0920rABoGg^xtQ23i5kw!vdqmfQQsZjz-cQZixM=K~G z3?xKqbdEjyKd<*{pKbT9>zs3)uLBWiK0*ozehWAN!Ij4NNJZt_xwMCrxeB5c(uOk@ z(&0VVVJwYrKlxL1sC#&=MD|*hxP=a0Uo2|%(N>Ln(61L){QZ_pPe_sU&nPaq&IDqL zZ^e=)DkH9$47`|@AgsJ`tcX#vIIMs&aA&Q>_@xGaIFHGMuTfg_zOTKz((9e$*U5eq zCisVOi+L*ofv-rC!v)PDr^8cckED#{#Tp;3Yz?--$$L!rEyE^<84R zo_0BN1}}OdYb%r|`0!Nk~1+%$7&vPUJ zV+r5OA-Zt012;N0!^huJ~hIog^m$K)AM88RpX`~Tr3=(AhppOtb zqqd6xBNyM#Mm3s2nb4MD-sQNdEf>+7Ggl^wCZ$SA<5%YFXvW#hw~fNh6-q=q7Ii0t z_te*tPk>gZlIQ|K;&grM}V38}6XJ-AHILR`> z)5HhuGuFXh=4ILP|GR3+2;!#q7e*5cplvRS+iI`Skpf=M2SuHqvKODH-uo^mZ~pof zs_yXvi+rY9$ba zHJ3JBv$84{od0K=e_l4ohg-$v=xt0)Xz0yuXI_6kf^FVfJn3ZUvPDy+a*!uuKs9?@ zBBV>fo~ND2qjP$Wg+t)^`F>ClOB9Q@KC>KGq#Q=pQ-PmPlhjvaqC2NDa@FyBng@T0 zzCjexy*1S-{nvD=6T0?|WFklg1mBuD`JU9MA|OG^KzFCswR#`jJSPYP zDmPalTq{+t>SS2O;oJADWUd;$aO>Cbl@IGQfgJCsIv0qSW?hCVudGtrebfOXaNas4 zg-9;X7u80U$Z=Y(PV1sV?HQ|hV3eqEO z2+&aQj;FpQ1l^O;2FXYeRADZ51OO9Jl^jZ_ z8?^1knAf&DiChnSUpMSBx#P=1l6m^`QX4vsW&-}h;jd>{Wq?LtIH9BnG4GGb;JuHd zR$k&)uo@y-3P&8UuyZGKWA7LFeZIJopXPaS7VrM+^ z%9xO-@2B&i6@x6uHX=U&BfBsw3p;j3w`qDySS>&7CJ=HHFuX*pll)40i zISq^0_>r-}f~OgdR9|95sWq9#p3#|QB$gLlkOBHw#n130m{;7dt$&_ysLDMKIKUJ`ga4scE~6S%m)6#YOo{(&3l!DWDWsa zjCTZPD+Gi@gKxHTS%vvCAgEPLEz-00$D!6T-^bfzRAY^UDKAvEXd1pwHGQ=AO~sdP zE#xFrWxwogjqwGY%=UaQ+B5hRCn9s_lLE9oRUsD>One;r^YT&fY+O5)ZYFRp z&}VvvXi!VkM2rj66O725a2kC<+(b1Avv3T0_HA+^#)k{2leG|%991HazIgrvSzd2L#??(&K>_B@H06Rn$xiE#p6)?H3?SaUvxVwb3y+nU zQGgIK0(wT4%o?J_4T+C@e+MV`p&R!tt5Ga}jK_lrz8yEXeBNpo$|Vpaw0j@_45;%c zgCeQz1i?r|uNl-m7XjnlwYuZDlu9r{YRBoBN^osjuIHH4C@?KgCVQ?$MJb zMJ4}=&UOA(Rz4xGzW(#o%fSc58NO_Y9$fb21PO}OqGzh*CVJO?8BOwJIguvkZ61?T zw_gkqqCJ=yDc@6dnfv$jWys-#*ZE6YZC%>L#*jDwYy2~8OaMM(!O@kz7JGlq`n>o3 z7ME5a=&^G3_16swXKCj~AG&%FU4H{v?9I`Mvl7UUN%nrB_7rbbc}V&@$2a?6`AN9@ zw)BeR&&2=YRy8vwXi{77lT1#<(|sU^7h0ZNas1PXQW;)}AO3L_|489Gl3bjqA2@+m^V zIdcnj+>tqm!BRbA>DlZ#MI&>hdD6%)_3R6o0#l^xuKE=f&sm-R{SG!uDU@H#A=2L# z5wi%18|*DKi?H;{L*wl;pdg{Hr?+R7nWaL)z320fYS20lkP0kl9SHvS10OS>3x;27 z0&F;N1*Uvl01Q3CCSL^6F)=-P!wi*Pm&s?czx+||J#FQV*I~JBQ3Q=O&Yoew`;b7% z0!sCs#1Bv8qsa)**spapQSGsdB5XBdvF6no>pT_E^U$x}s`ZSnq#~qLdq%4h4p<;d z9@XA*QfBI{O57NYj3T)Z&?-GO)tsG;Z9z};2v)6a;OiB4Q(Hm zwg6D?D6&xR`{edxe3pw+{bgztI;>aQW5C&}Lto_`QHL^K$voMM-dLVx(!E=nx~p2#P!yN9r3`k(4iIwt>rNSG4U&itJuUW5PGI~v;C}qc zz?o!~R5qLg(Yh^V)CKbi(ZHg8mti9}!6o4}rCLs8WcOw{=@C63J9hnGTI~4rNSeU; z`?g8_vCV_Kk9*vQ?j|#B(|^E5ey58o zueQ;1Uq5iA*}i>t`mKC>{NaZ28ePfkB(jWuejn`qZA0Q;r&D1kd8mE;`%1ccLx6nM z!7gE-DK{X+X|g}=h!n@B>T-pK6rFnyS&nGep{TsD{{QJc<%J~!@X2E%zvvYpBJvhU z-u`UFu5yMXIOgfOx?4v-tkZOo!kg_wpX`17^8*A8GR!8T`s|BW(Y?U)Oce9RFvpMo za&D|@3uM=398G_WZjoOSnmMY~NswO}jylw;HKk%}eEvxekNr&77A0D{A?Pwgsn5^# z&7|koJEL(^2muoJ0ddNhJemz1bhvuaD;pwmUbH-(f1z+9M_eDs6BeA(;K3^!Ts$Bc z+2?-N?t)?_bbp(KJ-!8=czB}dOO=*Ebq42bk-T9E{{(`{C_FZK(74Kh3D}Um-Dphz7av3$1Zrs$*dP7?O+xKQSi^sKQ z^JGLkSa6jX+ycb8A=f^JMU&y*;jIsM1iTrbY2RjbT5nnD>3t$PFYmYn9|(ZlkW2ab9i{0lM?b3}bD z<&0Qozp8mTgLZle1Q*OI zdghF=jj=}6)q;5}y>3oSiaMQ`JIR%4=mJTAJP5w@JreRkpGc~y?qmUdscvLZxtH%< z)V*62`XD6&XoFM=(cUCL05Ky_b81fmV%i<j5o=nQ=jbPhehK9%ce)n!tquL&%E-g^{(md zI;xqsg7mCk@gIqeg-$qrU+=%iErL1v@gbH55NV)dP%O^zd&*cQeM=(e`CC5Yk^-LE zb(&TA!&g~#fXlkgqsW{>_@pp=X=1R4y#Ri65f*Q(WP}uGL>>9jf|*+nzG< z{jQ(~E6Cht25oe~nxL<=+`z2WWs|@N6s&qi+H~j0U-w3QOQP!*5pC5F*^s1;rj=p& zjLSRhKGV~x`JPMo(simlgC}iEvGo0x-=x3rD1+e7Fbl3kQ9Qc=z z6Um>vuVdigTAu%!ZL!zFH^LBH6_RIm3ZL7%W9Q%yo__TEk6$fL+GAT;9xI5s)5k~6 zqv8kl@0%l+zH4@0C-&!x3l{lHPsIku^+3Gtq{CCF9-^<%f5Tc(9_Mr=)KXLa>iz_9 zkDFq7+14j`gKp3%qsQsNf+MvJo@^YQZ~phhPRFd50R+Vq>}+9Bioc`AUXPuQAkIJW z>yN_^7R-c_ULN^TFqtsLt?pcbiv(_tgG1a8lUYUPIa(k>3isHIo5z)}L>;OwjGApa z1##%E1XZ*?!1wE2+Ef`i%oQvJR}u~u1Rvs@1atjOlFb#&&WOgW=&@V{l2{OsjNWgymi6X!ilXU^v?z(yurhxWxr^Di^pf7$m6<; zGXpdH*Zp!YdZ#Iv4Hls3pYNR-b|jx%R=b0|Ad6gd9Ry!~W}9W2m_X0VM#_zD*%`T1 z2!FGq&qt}=?b6`fDba(D^8K>JJDr2=OyYh8CWDEoyqoO!=!lU;;l(q)Ge3%*V(s~} z&tfj(%_HLaIZc02D2z1NVbe)5=|c>t-!W%L5Qx&&Q`B!yr+wQ zbM^G|97pZ*ADu|fUs+w>LWe}hng;K9SkY!l6s>#R1a8McqLD!6gAf_^^}LuGV+ zRZ~iO5=bSx!FT=Dxqa;2u%kNn7zF=xC1YjKvb@6LXF-HOyJHnK+#-orFsJk_eukCY z66?n1ghx$MGvWO8hoC;-=to>dvdWPauh*Az0U0WbKFYi#%A8CyXS#y3d83`%YQBWk zh7l9*onp(Kghd_>Z3^K~7k8wIe=v!Crlu z)K)vSzv4&qb5BSo^(Bhum90~?7-)?zGQUi}IMipvZy__8`wP@v z|8h3jy5uiX5}@pL82Xe`=|($nc0RN+lR>Ev>8!2qHUL`k-!f0sd<&_)ccT3DFC=}? zHC2b}nt}jLoDFOHB=a@I>Z=W0$ySHiT^N}?C_lXN`emYssSW{>y9&FI#fMD-oBVdC zez(A2zjsFmQ_Wah&}$Ve#9|tSvy;JXkUiCWLM40$_u$EkuwL}zf|Ge7?daT9I(9@<{ zqAaQc&I==A!=t!WZYj?qLaW}id!moFuKZIe7;;7vl6~j9U-4Pt3z>@{2o7X5xRmqNx1Cz z&H$>=iUV2QQlFq4I)UsAv*lW~jri5T6f=YSxeTlcb|ceOZ9&uG;oK$Yr2X~{9%LY+ zYl?RH^%J3ZbUbSb#|zw#`n+Y?Uj{biC`DXVVyGt#`kio`gqEB5oxVtt{tP#Jweiuq zbN}s|pE_c95t-}QAO08}q`4SdCH+=hFlz)svmt|?<3fqpKb1oZPj4rC4KEZq{2ZBf zQlq|fq=yj8V8v&>Mh8&|R%y*=y?aI7hq8YepjdWh35MzrwBtfW(Cg)yWdIv7pn3e{ z44pp&AbwyWUI408(2zWCLikrOMhv&XL+sn5Zz(58_%Qn+EvtOy`LwOp2ZQ{Xk&?WZ z*amz+WO{eERICyQoTCb22eE#8_s5-Jc{A~lfrDk4e@(jJ8SV2dNlp=L2Y6HtjprK+ zhskeTNPda{_EvD73E?;SD5SeOUNTjw+TXqCtZI>8>v>EHTPDV#B0*8(c03=Qv82xP zB4@wDu354})^H;f_Rg4m;Td=_ta*Q<$E6#Ma9vo}*i}NVZC#21?@C@pek~L33|_xi z)R30n5*~USw9lr+tqR%iJAd#Yc~(^4`WA|n3f_Hk`kAV=sb1<;G{H{${iftUb`z_L zUJO?Wk%+Lu#91~xsg}GrxJbHHpX4>?ierBQ( z-yU!k(A6i9>+2=RhwfQZ^sLC=Wet8VPw-#Rh(SdGM0f&6Mj}*^5S?+l9xB<6`g%qO z+M?rJMSoUbRP~ad6B0ozX9HL=o-R^8KQ*yc0>z6LjP2kqSXAN}OUWTsS6FF+9NkCx z0cEKN9KT+>h?s}BPkj0?bI-tJeuv#R$djlvZ-8xx86rFfY;VcL;kLXFQva<3evE4o z8NWI72Nh0KA#2#Y9}k05UGA!T&SL-FJ&YDg)NN3QWuwbh-i}GOiLqjw%MMy9d=tZ% z`yLayl3LToM}H(DuPj3dy_JX3bl(L}2r`XxxodMXye~_4?h{~cFbFtzm>i+lhP1;% zCla@=OKU|`O65NsQ@d_u6Zy{cD%o3ecEWav5Y@DG(`C6+$Ugh!h2bPj??x zZ%Uu6I-~3q%#Dkctw*qI$ew~Ui9kF?0yIJ4cY`lipii>(f>bga zV;`t^NMB{oaqcVnFEvmXsZ|pcq>ToL?eJ5_5h5Lgp{LlwAn?Zc&(TpOF_e_?Kw%0A zv6u=nDAU}*T*NYF6W8bBf##5pLt9iiO7C^~hd75vJw!xYtMrewJ4bsk>E}aLwK?AL zAhQxA@^K`Q=+R<#M}^os(SOD!#;rpfu;2N+6mgXulfV@@Q#=Lqxbt7~!2UxBvkk$g zY%?K34Nc(E&iD&eXlv}QnXiaPz!Xd}bXbcRi=P8G%O-#kSgNHZ4#TbAjoe4{M?s#P zc8eO~X8Yj@SBnL*_pIN0<)TXlJSdXKnMivajGK%xkKo;Z%e##bJ3M7@Wp6QE&#cpm zxC8&T&dK|gyZ4(j4o?9MFtO0uqJq;mBgv1i3QU+qOdjX*^iz;D{Ll&U-M3?#-)5_1 zY^3eZ6jfOwX2W*3Lo^}ddcMAy?Zt}~>wGcdByKLYPG{!+^~u*p)|Klg#IBL%_SZyf zVwdIgUw6uWf4O)4{pks@wekk^;?jC}oj)O|Tw81_p76r;iRu=iRg+luqYCbpl!!hU zC$<=&1?4JLj$>|GGJNdQhB_kj%QETHz{_jqcs2pYm~YLO`=p&0jL82gR>rcL&3MRs z2s2x*ITdu9;?>Z@+T7?zqJykiRQbK)OAQnbGh5VeFn#zxrTuI97q7F|n`Db^5}4$t zZD#qZ(yi;>s9mF9wxBsJhq{LQAl-tps5D_74O=_R-Au`c4;-r+Kr!~C;3DAg|6gG`K!~g77}ElJt?DRfy}PFO zqUQG1yufdOR68Lb|7qjvX3CdOQ!fxdxCe#x6T+-XCVYhgOvy8^AC_|p>-g&wU8$Z* zlH@VNO4|YR1(r+ zRSS0MVD<~v3V&Ck5KDkq@^`^XK!$<%ZP)c#_`U9&xT~VH>N;~~4*BFx!>cEh{677j zjWB$WFNlh;^XflxoyM7lLBA00CQQ%fpiYQ~ha8OkU~KyY?VmL zLw}kLWdXJQOedO{(BW1Bi@|Y)iQkplCh2IIt z7GgdbJXNJ{e{&nNBnoJ*veiE8coNnYXJd_|9ubW5u|%kM27L3LS=dI3Iw` z*Rda7ABmZ>PYzgdMMS=V;!za;jYY5XfX1!~}9vv65Z2 z)3M5LwxdR_{XyFGs*h zKzOY5sE~A>2+$V?KhmZ8)1&~+jFE~uD$mTZ@89h*&-nz;YK-R*{KKIjU*~|loTe90 z)Q7@JFX@mt%N8&mg0ze)wB5%%D+Qk)&zTmNqwifJciE##*VR<)s^DKYg$esBuVJ71 z0bOOs#$4e~EK-EpKm3E!kmyIS9OImc!0e<%t&^6bbhPZ2TFaMruzLbT#d4j8gGGxs zZN$oVT0M)8`%&TkOT|}4_N}WiUz;fqvd)ZsWAMv^%sytF59cWsM^Am79Cu2#7?t$Zs@Z&sx4 zD>z-NDB{{1B1O5q8TO*GMeC?~=E+EZ`%3d-%yd{;Coa>ngJYXpBAL`=dA z5Cdk_=UcIKFJfk_ZI$*@g)RM7ka@-8{=dv;0VLgz-#T?`ytuXq-^{(=2=kq? z>E2h{dI0O&cZp+Wbb3EI50GB$lZK)UrJAvfiV5db$p1oigwPHJ-&|#J+?SGnl;0Pd z-?CB|Jf_qd8WWfDdp|J8L|necT(ID%fUdc>KDYPBb2j=irx0)B_Dbll=YJYCPdI*x zUR33X;tXhHZ>;(+#=>6+dG6CSari#ajCdF!&5e{K|ZV zDXRc|ig#ZC-Y^6CxQOqdj*jML|65Gkp&cK8Cj@n%%pGbi-H3P`nH&gBDhz}sU?HvF z3X;UCZ?#ee>ay(^8HxB#n(Q?Def=VcC{LoAD3{1=B9Hv?q&C9*VmB%?!PC+Mhd}!o z2|)gm@G11z(MS?ZSNeEtD{1j}v)68V4<1RKLxUlduwKBSFT(Swk%X zwFn<8dF?5~4a0I|I{Ydk>JQcdkxx6}oOxBd4G`v?)GM6#X2p}Oa93>O&+cGW?r&qW zhM?t50x{h48B49183C5RLI06+&PEW?^<$S9s$^3RE3e_s3_y^KD=J5vk-MS=ehNHwZiVJ>%&<8WcW$3x*?E9- zkz3;*zgBE>PFjEH{_bR}cAPW6BXDx$F&2nqVzI6LEAXJ^B_4DFIrqCAuq`82+tVU4 z+o>P?y7kQ9gH|rVSGDG+M;LU#IU$6KPDDQv>_D@en=7tQOTz$RH=ZQ65 z5LQ(hDAk(Dpk$E|CvOW2QHE@$oZ-q2>D>%lcGZy zO;LuwAjib72c!saf#zQCMJwLtx+kMsd2Ir5hpZo)Bf0k$@4OcU-mLW3h+>T~a4wI! zC0p>{!R^(9*a@|R2pjHki)}-mt7Ipgg<)AJO`hREa3LZpnvVb%U(xpWTEZaO?d8Xz7G5@qF_j6WVimAx$zn0@&C%vkvIxlCHMD@f>@_&mgxArFGgi8T>c6=5Y2Ze$1K@6%}wzgYp;{KrBr_R;(c!PA~ z>UBQX{bK}wP|EKXAkU`@W%H@thFttGC!=GY*+OSck76Eez(y1!c7 z{2e;K72`@{aKEs|zj^U@G*-(ozq%+ni47ak{9SIID22h!NbT;CZypzFtl-5-b^v^QACK`ovTpBEq9uCwqWjou-)O?35Ndnrfqq_t>9 z^fUfjCzyFpYCGCI7&E!s@F}32PclakmFMG(9RCyCN|+U@2j0d~rSGESw`WqIg-<3A zz8;m?_19>6mTgRE=*sl^7cY~mMXI<7RkB%WwlARjLzCB-{%=qjJ`n5_Qk(p$53jh* zR|$+PTq2TEo3l>17>M;lS@{~pDbw@7!*_Z?rM3s*N3ZlhJ+gXsg}NS;uQl4A;ZZ#2DcCo$KRthHc^kL!_E_NvXROMhCUzq@G0et9pGcLSgrVpLtm1 z#?J2WqSIHp##sGOAO$C6>%W^QHaUB??^5w+~^QCR`*A{(j$I?C{eAEn?&($HN}3FY9xhEJJsv4WfQH>PF>h(wE48R%g9Hg@o77AAjUG0#}elce0Ars@**iDIqq(P7mUjb?~BKGpSfC zT2Nf59)8r0=j4-yxB+OWo%c(raiqnaT6gl-o?i_-;p8IyI8X{e*dj6?iyhfOvFE?({~43 zSPy9v_)t&Njs)^mNFA~#pDM!r?wcWi5??f+tC~4;Vo^wIkJ=`wr|_#G0VCVQDh;23 z=XhGusNZ&z!94)o-k!GIk*n)OBZezql$E=hs07hQ82(A#>GJ74{LKVoG1b=M3;SpT zh~-nwUj{1y+t-Mix0@)|kFbTl3xsCb2)>V=uk00FiDF&T&Xupe)sz*?$krfvS|EyG zm_`FkAmU}&xuem2?QT~+BFO96U02eFGGG0#>7F8^SH7~Ad@X7edHH$VC(1RQ5`hLC zb=8{6wp*7PuVzyrmjrh3j?`X(`5zaQjEn_F6~DuN;OA6f5ptfQ?GoD|vRHCV23Sul zx7Cr({I3a5r2Y|BYf}+0@~Er@uMSE$qCIqG)1QxZVXFSkSRrv|M-KBzB0 zSm}9&@6xoPcKjl84Z#BRY(EMF;%#_|X-(cS_n_~Y3{mzRvUg+n$O*(uCWmnO3QAe? z1f$OXzJzc~t}+0FNJ!Z;tL=Vbw6_%)NdDhS9g@&S&c=Lu7lScVPm${y4R3)p^n3lV z&HoQHhLtP;b!fkN>7c@vL5vuxcN8!trjTWY*yyF&a7seBsY=ykhjRVrj2N&Yk_}ff zd2YU5pM$SLq*1G|5$a>BOJF!`^Znw>Qr*AgAF$-DN1 zx-ZR4-yjfn4yaPQ8Qu!EoJOOG9;8755Vymd`Zr_llfyVj{;eml>F98JrV+@aV&+yG z2leeikuM@=azu+Bu1R1P>xN(arQC!-=U&p$meaN6t&LW&q(VH)QW_k|<>Bp@yl=>^AD_9)vtqc+lx#=&d92^)MP?;dVa1sxR#IOQ^HeShoGc9H$|mWi8WA zGov%EIE-_7+Pq0N+iA`f{Y*~OkwmuU+U8g^N~BA4P`O?#k?o&GsN>LgpuG=&9^7)E z8EGW_Y0qcO(K33pkNCHA|4^{~a6{7Se(%{L{Y_19h{_r5er)3LX)ypXRsWE=PVwu| zkH#a}U-w4eefX1*>wLDX+R+V`$d{X_P3r5qQGCFiS12ZT?<^MdQJ8mnz^!hztdqmXDN@wD!Z$r@{_x1w^td6aL)`!g^Pw)|!97kB;bpO6rD zYIJA}I+kfP;<{hx!5ouH`3*R*6k#|_%z!(d8%tYD81f%LE+n}n^Y-8O)N=Twxcxh5 zH^|^GX({lSL?;Wlv%6K_ zty$r+J_VN+)kQznE;eLZFs#k#>YOjNMrf=J_2#;(^4Th{HJ>-~QlZt^Y1|($Vp`Y8Q`L z>X~x%OsNn1+Ld2FSa{D6M9hfj`yOMc@-C@NqN2S;-Ahp6^})cpyr<++uHh+xjbZI~ zUIp_IboF8J9c0c)t=1_<4)67&Dp9pF%gbGQ&T|NJ90j;XGk?jzOWxD|DW_1fMAb1C zOY|his4ds0U@VmvXZuR8n=h|(@cX%)fXRT&8RVkSWiVe8{oTOZv?cybVug{nk)wmQ zo(-|Y?N`tZUvowe&=q~cles3m89iUeH#J$i2uk%ig@>vQ!Q1-tfdnflTd@_O^)Y8#+D zP3yX+$4wLEV_-&v-(l4_*)(R?J!bQbmoZ#JcO<$puJkM|nUAq(vF!Pj29C{dphJXK z1mELGkCpb;_pJ+BOL4BLD@{vSiGd?^f-DH{z%nl!{@57zPLZmqec=+nf5ObpUS6!^ z&p<2?J8rsY*xDKN`D1X?g?%!?qzuz+@e}ydNe?|5QG&?Q=z>>tk_b_wLztKX%1`*m z*5DP}20a+FV-xNzbV6_YE6}-nXkSg(syRC?{>_#eGBi?~3ed0V;m~j4ULDCP#`_TP zioOTqUX8wW27a17jzsyjILE3~^x+88!plehVpJ&iLk7m|<2Y1WLXUcDq+#iiQ4paYpGYLdTZ z;Z#_QUcoqt-7Tf$@x?E{0cjdulX4=sE$rJGMV;GNTT+YA?I(OMyFb@B(7!pk)|O7G zO&((9~PSN`d7#^sKk-vBKlSn zP8w|9<$|y=Mos3e!Es$b$NLT_>K+1b&faxF7uSeuUn}oZ3bTgE@tyizBR6v9FOQ8d zVdwxjM3s&J9dwHBy@XU;^+Pe{APJ%tQ|Wo!eF&t0phzKiys@`{l=oT~No#U&Yn8L| z-TuP=+v0oq@e)8PmczzA?Q?pQ;ZJODzQg0M9BRLV^*%%A*(Z~ek2(GlJSM;CIZb#a zaNJazs`K1(Hi~||1kYBm>LRontfDzhk4tV!#+Em0LF|6qN<@5H^`%G(QOSTq`rnF5 zv=rGYwiFiKGbR1U8itNCYo--7tTqkf7WZ5SWHKU<=t`fH6N7{8Ru4o}=1jfs?}=wr zG1B1L7&s$Vea7y__bbL3-+~Tu{<{($LKWt2eX{`!fvvB*oa8kSDX-y}*KK{cSX~l;PRHdY6v(`~{ zMPvakt&gL5&SgeeUI*VaHhHOMbHRP2)M{SYuy_ z>mje%Esd^oC=g?-DGBEZW3gDis!MZ`XL!Bn8~TQl)Uu%RBjSDX_`187ewP>07|p}G z?`CH|Xg&PuNhNAM)bpYLFO&(haX#HbbKV!P37?^pW(nBdVE+AJ&%q7DKrINm0F;wL z%RxWofZT_hV)RY1;bcNMX5p+vFNEvN)X1AK0JpMEDjL`TXM-5lo?}0Dt%_S(g~n?y zzaO&`37?q$IYlP#$LD5U3LKNe1~XBrpk31}MWuS9m=<$;GOPhv-=1#e!wB!RSc827 z<#rL`9>|yNLMm({I}^eo?j%%gn|?>*n*T;oK-w4HlULTW7+Re|MUb7dx z(WF{gc|iPE&ETK3y5G%PMrCHHj}&X`k8Nh}@mRn6?y)>MU^VYF$e5(s5nh9UJdg7m zX-2P^$dc_7@pyaU(#`GU4LLxgv#-uNrblttb48F0y0oB;U-)yb*u!fLubvR(bk%-4 z^Wj*duXh4L&vNc=h|nhrWdB)Ja(};bts#=v=amin8}>qI0JUm`te$gfDT*U?vE;_~zc#Z@ zq6I21u{D>|`5&}Is=w(&HXg8_iLRi-w>VjE%s*cjXQ9)Oka2YTA-VgKLXU!3&@(vFL^}l-6~e^LTN7w8E*Uj7QET+{UVa_ z-OhoH;bo%a;Nu@L{8Gp>c<3AU?c?WtQ`LhJ5#i1QujGH?2TY?jK0H@ISR>UPP6;mw ze>(Z5)=un;z!vE{VH9A1aoD5^VgV<*IZ#>fRfXWCSIqti`{~8f$+pWQ+#FnNPN(hS zDs+$M_0;l<^Zf*AQ0pqF@=J7TwoWm=0GpmY%&$;!E&8m>hxN6kTiw^zCEVxK<6Ci_t-%~tSNI8 zrXUf%7V-1F%6{HMwB{+A>mGe*cc+ZGv1z1yNW+4tu7ob6WuV zu6mDA`O@xuehlzP4|Hni=aoytw&3rgTiBR(f3wp!8Y*W2Z`03qE1k#n0w^^R@LW9YJn~;!hT% zd-_*wm695`e@jhOJ-)7am@PEAZMaYXVY2GW}aV{oJMfo zXm!GduEZa}6O({MiA}oBs+|1$<_+}c!U7|sR!SI$qDbTOZ5caG{XS!@oacjn_Zxae-)_xxG0(u5y|LKJU zic?6!I>-rv1V`v7Do(8#JTch_-_@x&IEMv-5wW2hLU1XHB zdnXd7vFcmfrR|38d(AXqJAc$3FruftJ^szGN8j$WRjXr)+L<2POvJHd`-*II22RiO zIoo^1Z{pM+vEgjRCq1U>zu#+E>zvqgof7itwG^)xtJtVKE>Jm=O++_>;Fk~U->Qeg z&#M27LayZ(xe|Nsk@3p+?UWBQhv?ONNEx^b7bxRgCIwvX`dO7fr2{sb&x=cL#&?1}(U}1W|L_hr2)L7RdyUOcI=6yzz*RkWujjvJOWW-ms9L(} z!zr-HsM3k(Lmmvh@k)gJKx3S=wy??^(HZzpUeUX$>p+N-an6_!k?AwOkHKU-UQ5I&X;6~87b zk_($gZVGfx!6jcMOS{<~_0-e1g!9_Bd31sqQ4$#)FwQl6uRG<-hamti2i_yC zc}f)`8-9Df@GMsd)iAgB4RFAeqd0QFYe8)Pp`~9q>42_N#P0hIaDhTxgeIV!5Whs4 zo(yP%F*OJ=Nsx!LV#wE7ci>K+yO*e%QRP{D3RXH{5ATPa7f&2j>pe^Ba2zz*v*rFc ze?PMvf>uUE8%1nJDMn1;PbE<+Ef9VpMJL|xjP(v6SUfs+;sz%fe7HZ=(#;kQ6to%U zDE?>Za*Y1|<6$c^m8T3SCek(jBK^oQa?E>3i8#X25#c*C7qhKai$3E{CmIyz+fsMX zF#a-wH}Uz@hy5ix%?|%c@VfbnCMxc?$+yW;FI^a^tx&BtM3-NWX`yFw!XJQPUwM$n z6k3M1qZC49{`0bk$o#w#t(Ytp97yv!E4ze^M@-IAgiu1$jdWiEMCBB)0M0h58LY^e z|1cpNWBsg_M7Sx*<@PXfXl|LZwyrec%!3m;X}q2%YSYTppCoG7nGQ*wEl#7YT@ZV8 ze^LzQ90bRW!IfChy*F6$#NU6`7KP?bQ}`<3I8o517j`k6>N+pZ-Q+v!jSb=h4RtNpy&GC6?K_+`^%lZ41388Db82kcTHdm3bm2_zO#k zeN11eS!yC%vfqntsr9(o4_}5DylS+K&8&@ZQrM1DeAG4Lt0!b~D(7P3v86m!%PzQGUq z=re53>xp5Uy}jddEIOyPH(o?;{C8)~PW6oN4Diru+*e#75st)yO|`64ibL7$sP#Tt z)W{sy(C_1kp$Wm~Pa{g=3IU->KLP_EOAm zwPO}~qZXQaqjnoikQ)2v3pxy0B=jV42HxOI)Y;SnTPzJrPCNyUZN0#fQ_RC|?pt$@ zAH1|!-p~1Q_^5Jl`H)eVa%(75+$s;EL!V!u_t8ZAPZvqvY&NJi^W84)f@LWQp=&WfV! z6Sj6+;wu>VO!%$_6u#5m(M|H}9 z4eT`c!Jy>*iCb3#=9ngaueI;IOfrvQxYcACYEF^LyNvX22W2HB-ericOyM3P)T+*+ ze<@zJd2|#J@jcw_bQC?z+i~pFffHcl$vAbc?!bMBS37DZuH`#&NnU+HdHI@O30 zD$=wpVa@8K6{noTBmS7++Q;5^K7SN^IwJ22+Oy89^#}z8-3(^!ypW2Dm?snd45j*O zZsTv!+vv_8T&FZ%`2SadtuJLmzy5d|trO!JnmGE{V56|)I!*f}O#TvDH3I@n*|RvN z<#xaPy5jG$9ruOhzgAD%wklJ5q35b&Mkt3XS%sZWv|GlFYkMtS2OTlZo#$>013B?2=67I z!lkiN;hue0I(8&}u>@I1YPC4?Q8SBnu6_72D^lQLhJ5*TDhf^85< zI#Mk`l4W0|dObdzT3HY~m^dIrhdibeu`m3F$_M4H2@JutW$X+k9g%f5rkFXW(FXr5 z{NuyRF9WZwuRqI?xoWNF7f5&x3COHE9)dfV?yXV%@m0J<(E&b1&H6BqCCm!^f+apC z_z2Gf(7ZUi6oi$myY~RwOT&H!&;$JWX*M}O2c#GHEe|>5oMk0N^#TKI5_NvYhp1el zC4pj;?c_N|esMrNr0rO!*S9Z>iNrqfUFW2Vya+B0-L56}j~kc#9U5XNySj|z#6EVF zJQ|sD>#(ZwfXc}~o&901S)z0yvvKczjUcls zduql0(Os+e{lYb7z${gb5w!m~gqx`}`FSXk{>n4QW66lAF@B)2YnV?4SjOIW#wjiH zVQYCb5rrE8v!B59nMrM)(h`p1;-{!+=&53wh;Nfo<|_F(Te-apZ=3wMGfjcDm!?Y6Ouu=@k!=y-S#r=k7yJ0)5;3v2K0#D zX0955zFp)Z80WmD8c zv?j;$Qk}>{Z7fXFtYrTQ=;p97l;69dJ1Dh2&kBRzpuUokQ18X+Eyn;?e-CY7z55z~otD=Lz z>fG)2?&S>&us%N+0lvuA_o`6%Q56e~gU zedX!VeA~%quGnMY!6`lQ;VMU3uF6zr_>bc%oQ+mc$6Er$9^G~BR4P5aP`e*utNxUf zTtG1xd&ha-!-Tj(4GG47n1K_5rO_PFiH+r$U<5Nz#niljFCwVsNA(`&z8l2HxxfaJ zxEnN4s`e8td}M<-bezNJn{Udh*c1Xi>O9T%OL0cK>eIb3CBJcR0_z%KaK zK`SrvI8i*?({la$d5T_iZS7`{L7?7yFp&#<_t_Ope+k}#@K5j(0;p4la0=vI6H6FB zNtE!U)lG$v%S|1Yz7o+3u%*9T3Jnd7yWWs4P(*pL*$PkCzNK6SyBIQ6++5x$|SejnCIia`ml8lwn@@a%i%=eW8Y6@`X7 zD)znigKOj6?QI6 z#|h6oL-$^9v>0G6oJF$;3F>8;d*=;K!;QQe7-#zZC8SHU1mX?p{m*TrdnpLdzYzs{>gq4?3TMT3)|@USHv|Dflx{(AB;+ z)uIwDW&Q!oXXLbj>8qgDiG%F9Fv_bm&^?t2wF+FC<^H8x5%q;u}@O=+nB{kckA)F6! zk8d}SwYQpNQZGq-<+A}*yL0rA1ZRj%i62v(bdeqX_{>>=i@b@0Xq)d`jMc~C%#Ki5Qr>V9Yh zNMt3r0V_g~!896L_dzMRK+pzHkM2$vwK2h%_0pL#NTEAJ1qhD`hv(#v@u|jfym0Oe zH2kWWuCrIV;c!hO;ptP0K5IP%lZWV0d?m8ry8W-PvKp>m6ZMq{-ydYo=yQ{P(|->g zUHO@%xF^!Ypl&31OKK(=Ge}ZF#;)K%4cn+f#;wheBvN9&%7wgK9G0)^wp!LJ%V5{v zd-Wny(r6FY8()#SFp>gn7Pb3|YrPeHzdo057H*w$C#s5~7kKwNoxEhz>k-Py{{dLa z8$9NeGPIaXr z7gw8r5jWrj3tKWP;T{19xUEw@u5`IhtZ~8Eo-L}9)F$FTIs2_@RK=9;`+EOTvxf0f z;>XhKuhhLX_Ej16PwV9$_M4qYduVb0D5xTY2m&iLwj^pHq=I?YO5EK+JUzeg?7a1` zV(Gx8KN1+ugfNtjFQf!B=m|bf4;sPp&&Q_ulx&T|yVx)hM37B(=+2%>;wreoNb@5nbC*!6q>d-RELM7wvti&O2=I%8=u{2BR=(jwRR$zv6Xw@ zKA|eEMK^a>OsVN|DqTcBZFzyxiAF^Gt;{|duSscUP!hegto2Sj1w}i zm>}ej=CEIJ-+(M+!`r~kj^z97oPKZ9bt z>o!y1^#tg~#1Asv;l6OTeBg(UN~$;)I|NT#dy7E!KjQ^3+yiZW>nGEx+1j(swM<>F z@geyW!5e*XJ_ywl{~jgwyB3PG1pwNeT(pmwpS#ysp4M>6ZMvCW@l1bHeX1+m@MgR3 zk54SL4TmDwDd;kd&di3ruT=L#T>8R_gY9y2-xgIu6OMbsoOCh%PS0`UpJ@qFt_}$i z_xKZ=XmqU(OmJ`d!N65g%4|SmI0KJS0sCiqazx?hG_zGVG4Yn@y z5_v$%-0K+C1IQR@#djF&4!O-3s{&IVuThLe0GTd+UD)nJ!0FK3V}!&<4|S}7+GGK& zHbL7!Sr%!NO`qu~h-?g2<$^9(?qLEScORQV)c3r5Yy_idw)tJEMG-dM;&ncPAf~tO zFYA+G`R7zopWih1>0eQXnOpPb^UFxpqwCjsYfC5&d-j8*w7wJ2B~= zVPn$u(#C;74-Dt*Bs`A>*Q&|iATJzh2z*??;z4wMa*I>VyNh1q39)Ll#sLq>khrxe zeT?Bzj7Ok`{buB)0VyK3O+i5u7lr*|+wJS@h#V5?`;KfB_JHx7u^t1B_8d1mSM((sb0lt8bIyDBu zQPjJ+Z2uj|KNWD@Qszl6Ta7Ko6+vJcK_2-h?BIm)8HD)SSr9^wX;5@=FJf|}*2zwK zLCtFG^rB;3Q)6S+Q#52$dss@i+^p+_(BZI8ZgMJikLU8s8CWlNtU5k6aP z<&@zG-e6}WC@zEme$of&6z}z9JQ*SA1AKs7~SjedwGE{8YbIzLe?z!{NFce;`GjU zFbN^sXE_`I0y}fXGT(|_dC-hrWX>`4AIp85aI;1|_Nkv=U}`w3v_k)?fsH_YQBSZ` zVc?nZr6@(|fTh@0&pk51m78zWJJp7FZrlaKX6hvg89i)tCc-81?;tBbtV_kub-hbSMP`Tbd+Yj`Bh%Ytlj!rwt*86cQ_!19i2yTyxqAWLyZ8b6 zSZ?uw*5%_s&li}mUPp^NAM~8`8Xf{N*CK2o@cfv>goZ$gC{;@{K<@9W>a-aA;=2ok z@Cd1T7?$Ob8r<~aUcbK66)M-sZyz$dc~| z{WCcJ9rH{dP^uo8s%1Z!l#cSR$DJsqKk{ew*LZ>y@;{G+WhS7$3Phm3-e^K<7{7tz z7llRzXmR3F^|)M|?S+fz)y2}cj*(#Hle14g1r=YiIV$xX){ZzQaiuv5D$)t4T`Tmj zpuD077*3@K)Np1PO7^F4C!dzyUJ^IfsUiVuUaQOUh z7)hY!m_!d`*I8~1{NbV&z=S^zT!3;H&TzqiOHEC!^kU%l;%TVqJ1<|CcJyEuf369D zR^>c`>8SvgD1s$iM1q5m%ZfeQdQU*`?4Qj*3$jO&k}9Cu$Iu)zD5apFCi)VKQU-kB zU48WjHa1*l0_Rn+mUkglL*FOwc1GkKnq))&(lB%F@aLVDBV3Oy*MRccJUKU^rE2op zf1Z-#E6l&A&PCMeWzo+qjD%NX_mCi7IS^DG--mIKWB#f>@42kA#r&F#sd|_s+~rL9JoJtaZCxjS6KI-8Qof`F==mi?DXE77B)t$J|-587Li1s z3o+07ms6)2ke-6aUQ{Q=1|=TiSL*~B6OA4)?^K9T9~?XW-oV4Gkgmmz6Fxy=CBP6+ zIxuG@h^&D8Q;e~UOUe)Lt^-E^!!=5NCM8oa_{mxmT3n)T=@!(o|LfbeN*Llx-W%X; zI^`oU{>|dt0zeu8IIo}J7s>qQ_K*Syl&j+En$ocX9?R9kmCdE`R*_Km4WE7Iub24rnc4yy8J$?I?D&){;G zbiCCvWVfdOr3_}8r2XHhX1>sAtzP!9oum=*W@Hc)U8R~YGod2Z@UmJ8fG6CMQtHDH6Ok^pAl&%Y^0@WL*Ra%5dns zqK-aX0z z8ZKiFN6>ENpnOG?%5-wr1)fB|Ohye2w-C-zN+wq#8zSQ=A~`9%69EZ)0b$||tHefR z?t2=1RGw@kh>>V=th>!TOF|anuJMs2I-Qi-kqrG2c? z@P0;6=SLb`LDxPam!=IP5ssIdr6F1lyUgsMz9TDDrFEL!3X}`wK3CA-*6L)i@Bo^r zqJdf$U9YZpO{-+{fMEMANM~#3_b+W$h<~!4i2wz3AyN~XWDy2JhR*{MASCS0aWb$_ z#}1W1Gt$QZd&y+TGl)wIoIZN$gLB1V?%NrgA=mOi?82u^5K3p6pyp7BU6##f3AOZ!$?$hT*^;I zL8o9BM4~oHIF_MTv; zK7m;Nh4Z?AGn(imcdJGa zya-zYjTkv!!Rz8@zJl_cQUC|+DHLN)X2}6=|8y2mtkI|xyN<|!h@!|_IhqWd$?CMM zO-NJ>Y};EAYND|sP|WY)FaKmV@#4_hvxm+OjTTE-0rp5yF$Q2cOuOeJ@ZnMm&FFz= z92Abxe3m}%O7`6(Mqng{<%Som#V`m1Y|+9h!)dSed)vlH{dFzZfi6`{Vx=v6-SVsfIqB%eZm!nrjC?-ImOmGt@gCHMBH)+E$Ahs?mtmH;wy?(kN}o^Is1E7T4GRZ+rm0l zHt}OG(@~_AKZg}oS-k?wC*YoTg*5G0)ac4WPQ6Uf3}3=OqM8=;vx#W8y*(l+V0Wno zk->J2lsg0uh}MCO@UVKn9UO$XJ|~pw0~}$U z2SM-$6F34K%dU&P?E)j&-tLZtr=&~E^Bi0_S&P_!UQFP_Zf2;&K~5bAU5hnl42)XD zbkAzMzYDV1WJMN1C8iW%iFN!f1Or?bBxJJMLTw+|tWv*LHWsw;^Za#B^v!#!p7d%x zZ1J=k5F$u5dj{u~B1FTZBHTXD@P6!mrZbi6-RoTFpFQ>{!M^3l-pUhmO^g#M^|6&l zsOqJJnTdZ@%TF9ub^5LT#PeO;Wm=f6wl}Q*_TC_rPd<>bEnayo^(TvNb97BSf!Z?& z8MnWpl-+~MM5)dWz>w>vbNCfg3`?~0O9@<}cI3m3=zWj8{a-3!ri^RLS^=N{H)gyF z?*NcaK1t*Rb{NzD3herCL~2Z}k>vLIvHia4G!d&G^A7(IBNv&l;RvwiH`vrhw~XLh zfuhHEI00<}_4r6zJ=<2>d?*LUzG;QdLVD+(;1H#YKIO#1@`3ELJ-8%eg2Z#`|x1=>aQi@Mvqr){CG%hm)nV5ob+W9XVy9 zODdkRG%h$<%J9G9nrifDBRcs1AOQ({w~fri90Hg>!dq7`WCaY4T2Z$>cq3Y)W_PE@ zku)x~0sLj@%QfxOL|Hm5-bm%Fs<;&{#=abqoHH-NBD^Lg0 zTg3&=PU0boIkIYp@)N8c{Xc$WGXvUS9|DKfxPTFWEsIsc7+8Vi12Vs^M8oRAx$gXb z$eN=~u*O9%MtX|oIryT=XW+2g>#V$=Pyt?ogs{qpz9_=eH-3h7T>GTbm(c3TbS+b^ zlLA}d(pTT@Xs#)XDZ7VI{Lp4kRWTY0PsPcpqAOeUI5k)vzBhfdlTT~(_Xy1y$>M%f zyz~8&C=n;YGj!~ccChVXaK!+Fi?WZK=K4NY>i*Kv zAuANGGkfd6$cJKM=TjW!V{f;m^qR{g+_KKzWKxDxK9#+?8C{>a{P}V%2-$8x7vw2u zxdL4*se`dTKrR(T*?JaVih>s=vf91|vMvz(2Hb#~f>dcyWtWWWd2$qej{1YenwpgC zK|doE0+$0z;su~uIu{DtltNc_Ik=o(>+4d7mr{!c!Ee8BaL$lQ8(!9B&8u(RTD7P- zmDN8ePj&pI>07LJEsjII^yDG*@uh~xHKW*vJZ2~~>;GZsf>p~MTI#7DogwCQ>c7Ne zgvS6-N%iPw8+i4_?n|X9Csei-hHT5+L5)-qo{F;UW4@Eml8xf|-r%5YG!V3V-GuG= zeOR=A4_)9D3Rm>)d7ZGD&EP1wx$ieJ59;oz@cx^tqVVPe#ak_q>gO@R zT7uY z?T^)=cYh{z+6Hdo0Fux^>1lW3YtLz~|ANzHg~FxLFSAgD8ErIrZ`d`bWJ|bvL+p_o z8Tj>sh-pIlatG;-(I1S58Sd`<#GUS3R)*IG8)Lpr!$}R0J5c=fG||4yJ6?7!WZu2A z109lRuLQoW3Ajh7=RcV8Xm5gct{H3wRA5vo&Nhx`c)~hY=E8pVu|0~D_|x<>E{#ma zwvLA>11{;`K_e!QK8 zR4fk?tj!l0^4t^W4nX)_-Qxgf1PkS%p5$4Ws|Xe9RaH2s!a(eswv(4U*Oo_Ww7yPn za@1e)?N1mp?HcyBZ1}1R?YgINze-4lbSkL+zhG1IvNo)KG{vvj4xtg^u zR-Ipo4X9@*tS`>#C&b2iU#2{eFEd_bn^ZjR)4T0i8TdmI9sQHoATtY>sj+O8N_fu2 zyfry^yMdU2%l%^!tuUAU0}MpXwDIkaeY#db(+;I+hhR8oUdsF2Zb#a__RRZijGJ5U zbPb#wmeVjM)VzsWAburc9-A%J^EbPMGmkind_&q|ZNGvEcM;TN^^0NYN1t3ucU%FB zmDN@lgSVinG z7XiwDLAHIxck!1Qa;UNYrcxVnq!}fdh0?Z%*kj}R*v5reuCNehgKo=LA?;D$Kk^cV)p2GFQSDSqT?_ zP5=stoOUjPR7)pGlferBhE;E2htEZkYLNY8=7qe_Xz}B1oY%eIDr9L}`~bg)Vl@lx z;I#nDHq*ICuDCM8Nv;T!c(^&0?;~*swsYx7m?>A##AE-_3^+f#mLWxEQG8};9;r1x zW?5o5C5*eqS}83SMaz3*tz$(Z*cW^$HxcH^d^Dx%9*e@ZA<2EJ)KE4Es%t=onFhRb zoXc~B#M}E1bR)h^Yqt^Aye{X6Bg=&OlX~ME-)Uyv!YEuJ-;q9vg%8PuOFSP)M&*yg z^D4YxP6NUXvvTAvEQV8eh)HgKGF;-A>pP5WCU)Ho)dMh(PVZE^;mS`Y_HD?ky2XIk zceMgRwA*Bdb)9$LiEwAijS{c}f~m5Dw+G8eU(lDgm=L3P@^#IQL&)Vhj zHYy@+&nfPXy0amQ`j`k)czokX%1hJsGNPF9rrNpjQ{%YQ`0G45=HV>4XGU+2iZh0t zTf>V_qjUWiIZ~TX{t+vtX-koTDr8|cI6Z%*Vp%_=JQl3odk-uB6y)IK4C#j~;kUf-XsQ{s;KUONoUg*?Y+ zx9>m;L$Zc~?;RHe)fqhCDvyC7M^z=b5dikqSM!nfDQB6EMWDe7z5}eb8voviN0VSw z{svu`FzF%S<8L8CRlm{tOfp9p(8RXq)UAI%T?bX~-jooji$UQJ*-2^0$V^rbh@mHa zFFXh5lnX6m)x&4<{Rda7lrW|HTMBWZ zy7<|;lkL6^tED_>UgeOQ1=sN<1b@IKPcpVp3tee`wXUOqSUmM+9c&mp7TV%O_K#H) zJ%MJs(F}#a$ws5+C{kZ|ykE@a;o!~16DMpUfA?L*_`LAp+oH^B=c?B0V}S9nt8K(Q z-Bx7n*e+Dzg{5uzib;8hVOZutd1$r^FLaTK7DPLob%psx4C}|g{0V(mzj2ACSvTJ| z`HHoatWzTDz*Yv7X0B=O?6i@49g|GJ6F+GoAM*0pf38;Z!j+Yfsc}f%L+Iq z9qSKrxh!ktodNDtAr5kC-2fBL#C*syJ<2*k5K;LMa9|m~05sB}7BM02>IVu1ta5;# zp^)%4JQ~*%lxYU0aNDx7}@{X1J>eUNtNKDp4IHTEsYi?PK4E$**74y zN620GYpecr{4`@;TTrFxnK{;exPjqJ#Rr4uSBQ*%YV}#&0dq#y{KSrV$pB8Fq@)CN~4>&0|Oj?VFKp zzPC5UrbvdTEBAwN69zkl3k{N|DBB91_Z&6exDC-eR&QS_WsjVo zFZLB)uWodjhUdq`a4vH{f*Gy2?i;od;op(DS@N;bD4qOic%CxG)XZb26Nf@pQ*ppL zq)#w8ajH=zG3wLt4&;{ABaDLvC~F~oyr0HAlnOpv?(p6X7YA!-KMcxbuG)K6AqoTpOe&~_~8 zEJIrmZ%EpFr^Wc&pTb^#iT=4|z^kC4)_3r*y8D}atclGCk?!Au!*(i%6wMNZsm%l(XtC1fDOP>C6v_`jibo#+h`}Jo{ z!}=KB*Bu;3&8OY@?kgse1Ry6oR(3%?r2Y0ql2-rQ@|N9Os5(Mc}98d}%Pk?`z zJikE$1eW~m3yca4wn*JXBpM|rL2uEi@h~FD#)#A7q|gVsAVq4geC_i^aL4NU*ufS* zlFABn$ZHD%r@^qL+CJ+e#Z$znqo%GJ+sHai&5chaUAo!=eYO6nnc&pe>8P8$&yA&* z2F6KuMlC*=6ls=qqSs!|!7gf{H{E-m&>*xos!;ER$YwA3Kd;cftr>VO9sQ}=>{9ir z@K5bw$~@U9#{B(1{6dEh!#g)vauaGBq?v5!c}cWWV8Nv7bDp8Me=PRK(jf>w92J+2 z;#+aeR)o)DrnT)!g!oGuDTBO+cy?0dCSNuY^NxFjDxAV#t)YQ?0;k+T2scJ<8e|DA zVt%D}di$6#$y?%`BcRp)HV1k&bSS7V1fzS|94s`;I7<)Vn zrm_;e)(-arwtZFC3M6ACq2NXP=avZ6$u^v<5Rj#2wD9F@Ndpu;_s`^ zGJ}z61iNd%VeOv7iFq9S6~+=pRMRWQ#`OQHZ{wY;$d{&SD?I9231TPblLJO{lEyxq znYc6cXa6Yno2BrvGg}v@SdpPsyO4UZ@I=D(C^_jvitLg;4>koc|IZ4WD@okkq6eDY z#iSDLLy;n6h37gL+#Q^4Y&^>1Z}X?jkI8kx&;Jvh(6%P$E+@EncX}QRxD=?tc^x|A z3up_`c4RJqb1J|^d0*~&FZhYI75Rtq>^pIl4h^?zTF0(<_r$Ig$*`g~KhMl+Uv+FM zx#X93(QpaJRyxh{0sOR%_o<*U4dy`2uxI1JGg>Qp<$_;!$HY!CcO-kr+ACnq74+03 z-fatCbP#hs9nuD_yKmKhg#4=k>{*}%7=Qe4MDvEzGfU3IR>v9`&PDGb1PX3$o-vT2 zuFrREw2k)NXd6w7cgQJ#>xlrUtf?wtiQ};6iLsn4w{KydP6{wit*^W`h6jwV)r-=V zCet2gsb~T%b(0l4&O822?z$%q6aw76y(Jktxo30bWZIXme5*WU-^8**rfD~AB(S5u z-8ui{%{kysLbnk&hL(uhnhDFjOHR!G0RKiBe?lMk-N_lb(~gV~KZKh!9!E%(savgR zh1P+WWTEEa^~NYzx1pZQ#0NXBFF4v5$~1m-?99HeTmA8UUo-ude|2}m+}5z`nidr1 zEWx`gkgDeSDCotW4n0{`$W9~l_5A>saRhq$o8 zaa1bf9^j!4ACM!!VwB}3jlXH|uh~y&UuxMZ3>_X5zild6+^fa&tyI#g&A#kGd|abj{DJWqOY4FH__|C(v=TzK3puQHB#HAgf3Z zh@vp*R)mCgFbGSj1ncYs0iohuuXC_8b5^{rF_4+UOylN0t43-)56=~r(76V)<)R6c zzDnvUs-64sUa+O)x90+Wd#8o+sn@D#>0mDQ4}D;bAVSmaL~6dPMEY<62QMuIyA`F; zzaED`EI;~LzM1g>&Xlg4{F$=lQn=-`*oNJ4^C(&vft;jvjjp!KO z#zLBvWgGD*t~DXR8_KSbnk^^|w0vC~ZMbT&nud$YT@e|(yG9eL_DI5xG@E+EzT@Ay z9AX9JpnpDRc0M>p`2{9n!Ag7S&e)uV?(Ff{P3F5LyxViU0JD=7cm{Z+;t>mb z@>0O}t4Cli{!x{PgBy^~RuoMLaYY1p4Y(*`|Cu4tP&T`b@Kd<8g@U)9wz7YAT!ie>kYk%Py7i0g~~wTI>Jzx&dML-5s80#Nwd z@A;rZ`(trTsc(E>S0g533I|!o|8Lz!UyG$WaQQ>gjZNUk% zC(6~i_w7$BKqGNbxjV%14aum#RHN>c`~~vV@y{Gmol!X2XN6I~a~q9n-6~}Nan_P; zv-V$-l*P=gbP90&x_b<*PwsNSQ3LBt*nWv1a%fKK%g#1lk_y)(Bi3sULQSEj=^M-M>~$);lhuYqe|TS0npbRgPDkor0f zdY)(J*vcI;EqS}lnNTB;VEIIKCr^r;f`%~4^ycMk@ zp6AH6y^p4aHyJ#&pvl243$S7boc`JgRUZGi`TT-i@Yx#Eaf_*<_5vCVtDxBXD2p<0 zl|kDJelq00g6&t20KyMh0ZYyqh+8 zzEbahroJqykMbdHvj+$KQNxS+4*qZZcQmNvmTSZ+F^Qpbn*E251Z}Pu_dg3g-i~?| zP&f$c;5zA!DatMV7~oz`yNy251$=}ONYB(y`}FoxHw4GDGrXE3b;-}N;a?@lygE>x!CAPdJ6=>=ycmZjR{OZgO@ zFY8I%K@cuG0oY}&pH1ES3AJFjGqdJzW;z5V{bB5k;Y)hyV6uK4*hGgyKO-Ij=%`5} zkVOV=(ibo~aqk-&?_1cwJp!a~dIpC&CxH+5AfqeB+%>TJA200$MK+slP8mo;?_&2y zE13gQg{S!&1FYhy6)R{?Zn3AXK>zU$Tt%?^%- zxL?T==|VWXHPM1L_dR-@NBK`9GxoBXRiucpRKL+$z$8v#lzQ@^zCq;v*;t62oC*_m z_`zt|C}#{+uhF+=wL|_Dx)A{$FkyF0dF-VNj^Xx(Da#`MkCna?J9OKT!lz3c!4R<6 z*9Z5N1jIrZo|djp{H(BX5TVr53lm@ z_bL+t*$;#HJ7e0r!C@TWyI|{G{M~~x+{Z641ssBKCy{x?4(hUE0VnpT3xsm%@k2Kz zq7jFPjv_tcUjy|RW2jkGFa}~y3Sl{cYh>YEC@MN_L?}kQq;|C5>QO>0k) zdBZyI7X1sF0%+2}jG5)PR6BLxsOw;{MgPPXX!Id(kgO1`7?ww-c0_kMo$U71+!0RR zp>Sp65!LHjp`5@2Oo#Wn*vm|D$`VD|z1~y#1zUSV_@VwpZr=()Gs^Q-&0)GtEqLl6 z$LGQtGk;%8-52@q`o4Y-y67O@GTO$Q+`jIbUXW3&5Gk)uT-Nn?<=fo?!1XgQoj*YBMtYpZ`*TQG_I{g=79F@+9s-ZMyIs^8JiI zH4q3l3)k_Ml>5%MqNT-iN`g2ARl`4rpSB@$<8i!@9+Y;J&TwoQ{2l*FL!^@>7 zt`1~0`KPF~s-KXJSS`Qr;X=)P{P69eQ3L-IiRmfd2s+gD`bRbyV$XP!dD(bBU81tE zHTzfJch2mNEx4;>U0%*rrI03V*=Mmr0S~GvANCA>3BEI@&bF2*F={|Z(zk7Lu>36B*(^gJe3vcljCHWws@AkUZ6jS^W zbo*Qs7bsi^qc-415c|YuGnfQMF4ci>#pEWd6V9x7W`3cbl6tC=g}%hY+cLJ{2+gu--lUvVl&cVNJt(aw1;=^L92{dFuyvG zS$;-!`0Vl3$7Bkel=#ku4JYilyQ#ye}By zrjIhTFXM7MeW9)tQn^8n`r0413-cMFsPV9)HUj87m*1zJM&$`=+qlm3d|1{(fg2Y$ zv^pCuZh$|oc2l*2q}4Win5~gj_AT-5dW+!0=6tp6gM=zdl2HY-SIrVEflIhC-gRaj z$dmq^pEmbxKA;Uqu?F;5TYcpclwQ8iKvUbyVC4Z@^MlVhJt()V0S)hT>2^3jtF)kP zltd%tjQP7eI$7bz(}r#vi=!N-`HY6QE?!W0_V{Soh0%k$Ge67Cv`L3t#`RKAZc3+eF3u&#-h2+`3 zPLR(_9-(VV^x>zT)Z7*XGG{UOk<^aBs;in8)-yu#aF|ZTK0JmINyEE$w}C!?wb%!% z#DL2%UW+AbKv~c>=o~iR?jsq9U0@Hb0}Qn}JZ~s+5T>L#sid+jecGkq*^XD$ffGm% zUTc62IUYfHW&{j-##PE*7-)-Ev~LXerE27E?0vk~{inaH#m+;shngZL|MuSZ=cjQG zj*`FI}*x=~KUjO9`yUV{EhOC}W%{LKE z#=~myDMBJEnVv!|2|X*_yqIxjs%5oW=ZP8kx??Smfbl1sJmuI@XSY1iw2iu>zs$a0 z_D%-=vy&L#H`%zI|6BgK37Oj3KzzE1YVx-8)ga}T-R4E-8cv-ko#S!wjobF}_6k){ z{MWU!Aw4f^rf6S1`QzmK_l)c+rsy|8z|wV+@}&IMWzw|dQ)5yx{Ya+6S&eGt&cr@xPKhMW zL<9I}z(NWBA~7U&a|CrB4Rsk8=KkzIG+vr(_ZBo74sqzS$Xl78l(yB)jnz5{1} zf$;?!u-;%;v{PuWosk!61}*&vQqB2Ns`hSbr0@hj-n%fM?!^MjqT58u2>|lD%rD>dH{^-KC-c38eZRPJ#*$4 z&j${X-6gLbRKz45S<9n;;p890Ay=0rrx6m?=^iHbaKC&dfIjyeu@)lfaXGi6O`ozg zYb6Vc+YT4s$FT~w?0+Nv7MKXjWyQ4m+3UA|!`)vnR5YU^fMu|pX)ps$8}s~ZQ2Drp zcc$q`-TE7@wcnkfY3Cpl1_edZj=j8yV zkrG=nuIH%HXsjW7q8$6~vLA*1s>W|+d8-L!OWSwPJ5G>;?L%+>kEiE3>{ESsT6W&p z^tsu&*xw=P#Phv%qJ){duAN=VMN9sDv9P=MXM8D#sx2)i@A|qSIYS<;8!yrO!wWY8 zgO>I^VB;SN*H1s?doh2XpU)kWF8@g|9e+p_i?fx4;g53JRQfEXXB)a(bN8Ubr;NC6c_Jxf{wBY+1O2k zuSCvf)jHR1Y|RdLNwa}09TiI0fu~_LW-E$*n0>MYDX{@VOTVc_5QFQ3#MW1{9v7u@=|U9!+hY6IdL_aZ5oM-jja{E|f^T;ar-3)v3kd~rtV&8$eBQ#%^S ze){a2#T*i|x+H81FIEhWM9h8>j}LkXi8@F_7NGRr7`w@S!U*_K-4aYX;8_OqHf@}c z5wKhls;WHU%5|)c{!p_t48oPhksco;eF6jNfzZ774hd}yL~>vK+K)d!q#A5j`JxVp zg%eU>ji}rR93Y2^y7@_$CaxE1k+MJc%-&T%=G}0XWiM`8^IBE`U1WX4V)^Hq=I?JO zM&@m9VRFxm6IF^rf4fXU8E=o2JG!R5760BROS9e+=Yf}%N3&fd0~f+j>rcFg+*_tM z%+S5^f}O~UdoO3d@Q3{nmPMe~w@_zWh}~WCzo*{sH!E<8&BWE|^JfQPf*f=lSo#2p`Yk6zRWLtE#D+hyFF{4*%t?0t zm)$O0+}xOr7S#AeVPI7o()Kj`3|g9cN{AKP?mzo%t9IGq*lq5Bi)w+NOUFgJj)gZ} zAvSPt{UP4_MHX+xmmc7{uBRqGL@PcvSKMzox5SEpH1YaZpAqh-yC{m>b9P3My9gnY zk-XEY)l&x-$+@zxs5&$*tQj?J5xweiw8e$rh(msjGNvs03@c=jgtCjxaH8;%a@QSk zP5&e5y5p((|Nps{YhBs7WM>wxkzKe^GRmrC+{}s+u9fXxG9uYh$Vfg7vd6uJi)HGV`eLVbm&UwFI<2hb0wgh$b@uDW`(W(>E6g=*;Mz62^HB9 zgk+@s72hUfqViw$@riB6!Mq=6<`z3?la<`#uZRZqFih|l<{~ya6QgpC|551N^Tf(3F|u#Cp$3}I3ZT7x7Vy-@=>xC zFW;_d+kj^Klix|#(nGbk3RWk{;Ol=PGOVa5`>ns)qYplVD^$55{OMzpW8E!s-_p>G z12Dq~$N@I&m?nTj942l%-)+vZ(F|2q=d{7YUb0|7{}a|V`3(IYM4z)XTuP2$se?(K zY77*hC;bRy^tp;0*!?oO5P9(d(J9=0@>hYM+H02k%2DzdqYI>8=XsZ6o`xZnVI9vm zj87$ch6G)sA@(s*bgt!-2$L)+A&09{3E%XC-qTay5ogSI45WZmz`W}cE)UVlAPl8- z+~w5!rpXr^JIfm)9Z7`xwQoa~p5n8xCdwj~UGoZ=rj1&YvN6fsb)p^p8N9T=SMVY} zKD5IUG|-Ec1Pd+y#fKsA-YR~OQo}?@Jdmr@n1lC0Hl}3+W=x$|I6WU@fZ?Ot_%Ah? zaMPo9v-LTP;3cB;);rD7!+e*4olG-!;6BU>Cd>%qbO1!J4NywISdxy;iDj(?$GfW1 zs(+plP?uqk)U4=1l>TJ8#LRH1Lt`-v`3u{iV)lt=S^IM8d2Bs{@^EGpQho2Z4S}U0 zPuwE93HT z==O^$jG#+)Z8j~Hz6d@Qa_7qa4I4IJyK*SZ&slW|R%7?^8dhL>u-sNblFUwRQcT?A zD~icCsBHyc9J6>Cm^O!>eFTEs(`j!3hAL3E&{fv}BbK6{N~SwFK;f`3)*r?% zDGp0;UbXm+oQjdmKCqZG2%7$3cQsSHR?&)`a2m=$!Z@i0{kH?R$^s&pHSz^54P)!? z2uKb;J1*oy36~mLHB>EP?7NqXwKN`*fW}Y5mgjp6h*WP0Ck}MxYoz*GHz#_E@+|G# zrnbEiwB`kF_dEsM+-^FKo^^|9^PO#S`GfMJ#zU!<4V5TA(43vGJr(+fBa99W#Z!U1 zkBj^XGT=Vvn&`OeP;UV)j>K=>)mqi9)VB85l700T_1>IO(9*~Eq4u;Hn|7Fe9bkOepI)!DzcKyo zSkz?H$u-nbDP;!HAi(`q_LROLV8GCBiZ;FKnu^q48o(_-&#%1EWqfJ54UwvrQPFSx z>3$5ywMuQA2;Z~YS#1SxHv|zh6?Zm#mwKSDd;Jyi*@gK@7LbMxd{R4E6ABBiv&LqH zg{hx>cZhSD!+#RpLB5!QClkGdfL98hg6ZFxv1*-Io}^?vpO(KSBZxfDMEKfE#qRVW zhVLYUZf6q+ZRB9*Iirg{fBJqayAFL%^7^~kk8c|r!DpWIT7#dD$<5Tle*D=3dIZL0 zx0{CTCQv(m9wPiWxn& zj&*<~Oqc|uazKO1VK|>pu=tKQWJqPlKr?Y}DE%nq?HC@7QVVV>7WCcqNZUBSCV-li?39q0hePBkpRF^ZT}+R6^# z)GtO!{IbpDk@pr{jV+)*`=B+XC~us9A3h}}VSs305EW;|@HA&~0+lZY@aNIHR@j>_A@bd1JKX zlQ+>9hs4P5wM5yah+Uo9#!=6l_i0#B8$kih2-TBCxOXoP}Jb=u#HVMt6E zG%~37i5^Ps7m_ffYU|GleRazN@^f6gh9J+krZvlxvskzWz4iM$8*jc)?7`0%4rSZm-KyyFv;a>2j-0N<#P=hUZ+JLs*B4 z5eI^$gL5Aena4J$R@dMO$0tj+8@~<(+Uhm5=l z-yo{i=z()i!X6mt2StK4(&A9c$_`6%r19z3f5e&?CGKh19`*ivPh>ad&qGHZ_vE0# zvZYP`dt-@ki2X^JLkOSwG$|5!I-T~NuMLO%Pvq3sNy^%_7mDBc(zAu!#cs@>-Gu8) z0lBpDr4E28oti|YDc6SLvYHGp@9>Y~0W$K>Dkgr>fKqR*@jGVr*PGtV-7NL&U!KNQ zx7vPYBH8X)ZCG9GIWwYav8{#^S*^tyI6Mq)YG1y0dp>R{(mSu^C~_5Iwmya{IDO=x zIvHk(I*pz#>wO|%+0|8Ry7r)qI^HTR8fVleGmgvXqU4NwI_~P_rXL!z7qZk_6G~lNzfE@ zBka=vy_@%yEULwXLoCE$5Bb{a`2EH`*GH;chv_^F18a$LOW<63zI&{dqa2V=>+(DZ z#*+$_E!d^AsTU-sn65JMA5~bxZ!<|9om5Sv)E6Hgob0c$C1y3fKjsDYkAIA9d&7MD zoMrF8f9T#~diab;n!uy~EMJsYBBzYEl$=JMPnCeC%*_)t@S?=NgV2BG!g)ucFpvA} zBuj7!qk4VHa_t>Q8&{FukmyBbECcq{cdWnmkdQF0LmRFL@`*RVpHRptv|tXq`*BLw z+bVqh*-`RxWPAGF$#$=Y|Jc~?Jr-_i3Y4hN3l5|~5%q)|A6Gbg8u-_JoGPTlU4fqL z<6HmJdvNGTY$g*_cynI_k+yp-YM%}m3=Q$XS3cb1Ftje0VQ1=N6$g-1U3~@yVv$oo zgvjt0-4PrUdruI$kir5R^$bpH3R`{^7gT8N->;AVk3Zbe3_Bt=-{>!2&G-ZTB|wHn z4kzMHS@eoAC^XaiMGnT|v~~ZW7?eFPhklLR8e{jm{=q@qN0>4`vPacmVmk`7ggmZ@ zH{OH36X9{8>1*Cu7s@8D2?-I|hz$&sCbOAvK+wnO*d+kb$N=84o)#(tXO8g4Rv;T? z&O}Q<)7RMpN~2{|(ZifF>R3NsWe-}m^OtN-Vg2mzqN5-U_;eN!=sXQ;4S?l}346gx zQ4JR-4|}x7>w^4BRKYdf==|yBdS2g~QkeCVc{4jj6p&8GuOkI~f{RUapB)t;s$oO# zHYNVHc%DdiYq74cr#CZPQ8Xa_=TXMPUYli#dQq}Zf_nkM=#P2$eU?IxG;$1diyqN0 z6sOKzgP#i^}yf>~>V)P0;RVXq*&cNe{T z{A2w;eKcF_3|tnu2p_7OgXjLvq+3T~rNWT}25Q8Xv>bsc+SFT>)9X|PjmmY* zoY8T&M`(pg%Mt$V3rbA~$ac)Sto^wH-q4PPj?U!`)>W4%+S==wD-kFlr1%zXDG6|~ z+Jxus-jX9gwy`m3Il^5V!RLVsa?l_8Mc^WuPZ~MWV+%xB-UeDN)f%38Cu5`LWj<$M zJ`v1@#yH>(I(dm&A6rQRo-HE@_pK&#sM!~HM7Gt`-|KAshCTMS`P=m~n&IB6E`R8* z`%a{%gK~OF{wvJ&N6qCEj##$Ac7%=uYU&(&jqrBhexz#I#^e?32HB2{NCKFve9JlP zalboV?0=tc&4rbeG;7ebIb75Y!P~oUy}C>h@O8I_cLX(T`jm8dJLN=a36*jfdH0NX zpJHzxpcj;db4n=4U#pMylgMeio1(p~P#fP{w%8D0zQ)@dv);U}9-u|Zd^lyhO4{HZ zK7|EP3r*%49zNH&1o9$ShBu2McfK zgOoOz#?=0`>?*ZUj1{Q*-I{B4^yt-{A#fcJ9_J)}0pGt*ql#L^q<}Fi%>`g{#nJ6# zs71hwJz#}O;)@!BfXNk8)-@to9@76Xg)e*<-@ zx2HHTE**cL*bVJR`pW!NzMy?!=VJdc;^@-f$sc2bp}Ul`L_#@%4JTe5!n?=>g)rI@ zBBs}5o;~eqR3~*aXl#uWHBQk%dch2tg{{2KlLWfVtm4~4i<}JFB!(Ev$&YKS?u|bk zuNX~mJ5$TLDCMAeDxY{q-D2x(mfs5o-BQ3@#eH{WWfu3jTi#)&@+ev~+O$JGmGbm#I!Lnv zF$GeoNPj<3H95Ix#yLvow%yh<@uJvj_Uh`-ih>pV8ebnCHU0U~BlH8SvGt$X&kA}2 zm7|d40Atjz#d}SR5fQtS?blVdwP6KdyCHHl4eJPthMvgBd%{)v{?TSVCqfq~gYeFt}!cB)XXD6o4_-hmCp6*GD z=uLDTgs=@Mjiy}-Kl+TLeDQ?sPyT561)f8C1UojrGQo!2Jt)ViakZ?&ZJH(We%&;5p?M&CXaxM^s zvoZQJomuJg+{QC!h`3us;VuVQ$@2|u{ScO*mhug<;^^5Xd`(B*F8N}9diK!^=nR7= zz2J3A@Zp<;ZSNuU;ZtPG5z9RNl-f+|n=^Z#6f z(S!jAMm3r<3!2H?PeC>4%tHOlm<1;j)YckoKTHEQ007Gb3oBPU?{K<}=F^4YvW+AE ztP9AP?U;=$<#yMrSC2v@d*9Pt)cF!r-c?|2*TzzP^k%uyB)*&H!_UR<8?>6!pJR|? z6(m6a<<6-^yY~@cP+3@!EB}iKw6xEZaa`TfdgrboO?PQ7T<>eXU7rgNs`?^C8%dy3 zDeI^?Tb1CcBL#QeM)4TTS2)UtJT;^94|CPIBZ!tyS7(<>9j~pQemZ8H=|Tp|7Zg&w-5XcLX+-tYxwMzFK?k=1&n29( z0SA)77Q=Er1O^APe)fdgfSJ=UHD2K+z&83GVFh8I0)r4}MO1_4{`hPfsv+#IoZc9k zf;xvJUati#Ua_#+0^$*HHhzJU(U)u0h4d+LjphRR%FBUw-?S;fOwVj_edr*bdAu70 z?95^C?k07Ddk@obmht2JZ+uO>LW`vHZrc<>5LUM9;FmAYOS_Dko-W#UD+C>^iA}?G zM00_}c?-+96KFJeC=61TAIr_t&^L%j#Te^{hyj$;m zc^^5-H2wwqM>xKB_FhftNmn_(#|y%q@+{Hqp3FSNqWPQ`c8@8}PkFjy94PNeTh-)4 zbDyb0RNbdzlOd2?K7_B}M;-V2f{ZsAzzIvxZAH5X{QDZzJ;A%YaJjA_1@s%ilQ}TA zX@$$lKT>JmtqbKWAWEHk1xspyC%>zdN0nUJ17I?z#R;-VqF0ejZkQPDobj1rbHkuG z17qcbIFI7>LgyBh#i|LK%Hv<=`wk!Jy~CC@i1?L4 zX_Sgr`B&*HQ}Y?M$t&^eRR}02q8SEB>RL%DqW#;tB09@_9oD0R|I$(eb%r#BCkw;P z_YtbRp}N16Ul6H{d;`#!Otg|R>I;P2Tc;p1wS07aK&8K3r^pJ$2=vs*TVn16Rek*D z%=V@I2?-{GRvDGgS)!nNOIWuGEiX|+;pU9t3WM7`*po1T6^Szl6zm8rhYKxoi1&AD@PaUvnG$IdC9lfzn_}N z)9@u!tB&PE$7SO=wQFxk!Z~hM7+1!Kf_r;R*)qy}D8d7NnMaz7-d)A}U4nN*vkY&V0zs9A9V?+1jY z1l}pwprG*0tj)VyS@JA!Ax@ljyy-TSY6&neSnhd(FgNPG^5E*n;$Ox=eE2VMzxi z&OQAfzJ>%;I?owQI)cMi%jD0;Cd)%d;DfDa#)FjfIegZE$<{}!Wqj?99mILL#=ev~ zCAD@=ijijQC+Jl$49|63Or{+aQ(X4%G=*t1&wJ1T+OH~xWB5uqZgsCP0`RZIBE zpcwX-aFmy&lK_^mk(Pjrql;sgZA55gx5~6Q&!|aP)lTUIx#Apn_B)n!X}PK5+hj57 zg;sfklN$Na&H0O>N^*`ME9Kw*0Wx%XCaj^H1>x`vk1o9x-^)1-+@mah2hW~JI)Z;I z%z%h5V4iXU3Uf(_kT@6!8^Ho;u|ULdrWy)bG;2zj{cB>@f!OH0Q@_Rh6tWKfW{1v? zW`*?V)~1}kPQyI^e&s0=z{7yR&}*#t9D99vV8XnrEI5$uB|4QEf8S-_^^_5A z#oJh4fTpng(%@_4>I$NPQ*sYZKwgqYM8%}rdS0HSNsCQx#+OpPl?kf?-TwpZ;*cpd zc7(4x<-~^OW!9uESBB1#FJ56iyu^RJGSgoawhbDz^qAugi)$I?m-gs748_ypEdnYK z{3}{zLAPg=nqn~Pz+-5}rAxBD!TVmXBPb-6r)9Peby%g;whLfe7qtcYy$k9tfPERp zjD&tJQviAe=G=+iu-a^Ei>p9KvidIHyLBS-Hsr+?v3ITc)- zhKyrIH~1>DGs(Nbz8cSD$+jRc6WX-7wt@A>v)uU)8J{P;$4b4k>&x*hxzUM8IEqZ3 z38x{d8oZ)Ij3^nudXTOD6nR&+6%j>`ZK#J6r~aTal8|aBNzWT`cGx|WcqCRW1Ir^w zg9WV)?CxIx^H(Ov3zQ^nc{D?8Oy+K0DDRT38Q=QhzoIScwt$BqnAf2CUd~Ih6BjU} zaJS>cWck15z<-JQ&>MEkjGa&iFsvZhZo+HRAle;Cz@3?99$kNfYM2h8e>?3;Hdr%$ zuD|_J7e@E^Atsz16D1H{oBe0ujaVT?z3N-sQdNKPVppb6uI6IMQb^zgrDuomn}wJn z@z?EWG=SL9UfF7Dg~`6uc-9}0PS1BGKwgMQ=%W|YF|yAT)5%oAy0B7o3ips0Ewoq{ z+_n+Zp(NI@lZiw-NV1{;dK7=}txKqJ^5ua&bP*Q>)uCwhDqe#c#w^MfnZD1X!R zgnMCa@RBIh_(;!uio0VM`TEF$6!7KSC{o^Gdd&9r9Jan_kaJM!@79fBLCAE`@JgcsOLAp zy3XhXP3ah;?d|%HiZxlPWY;-*u?ywex*mR|<+ZdYQ^M>m0a% zCxU$Hofyaeza|weO6GEWCA9MQUPf@2NYomihsX6IHXe*Rd%?0?ykR_YK-0sEPgV|| z%vp_XrhR?;;y)Q-N!sfc2%hT;G=#I5I}B_->a@2gZ>F0)N94vf>X=Vm*mGt^AMt-c zF86EA9=As)DhtmFd<-(%pX!Tu)}-nA+jpQV8t-heU)1xNuw3Y8!HJ53>Rlrys}_=s z{e8t2qdgX$zkjwOkaw{QrpZ?}FZ3sO0W8mIbZ=no{lERoJLh(g5f-!I%_b#^F>+S8 zFG4z8Z6+yf1;mDpY^3GFOgKn^^o9)v*!1`pH~(~r$1I=!y)J|zM0sDHOcy&@=4y7O ziY{vppX7vI=#30 zm`e%AY1m@}%|5rW(@)6J@CGsX{CSaY z0yml>_s&An6%}-9yduhwa|LDi;ty)1Q^o*08$$Ycuk2NuBA(Q+jKGDNUPMi8*}YXS z^rc8XTP6>l*f}havSxnUtGb1Th+t?cf)}iB@6NAef(4`SnNn4#+G(($l}BHwK1(&W3BZA;E(PX()KtZ5Q|Ja7}DuSL#3GRUS%v4x`ER$1VFV$XmV2hkmDiAE|GUxUWV9#s@;P(cUI>!O?6YgwN+K^cVV5Tf8nOD&+Nc zMdno-Tevr8zZed3#*oZZ(Wz$|5Uj_MNin#*)E{z0&ES*8CZb=J?~vkl4x6yj16;P} zdlsw<6FN0^MrU~k$zVe=Ji^hHc)THfmn!1NO3|aG(1Y*z8|&b;r;5QsfArV1mrhpT zoeiwttT;0NN6Tcs`bJ*+`zKQkVb18fo&N2(AIZ2cmf(OQSh1^JxRdkw*dy|P7q@^5 zNG6qY(6N;;*mFUYD+Ucfl=bArBwVZ*&82Te`}_NH371@h{8r)uwGA0xJ&-l6onMml8`Vis$bHZ9K!b&~?W-h{WpAJR#h$!8Nf;r}J+cT3 z3rwCL?Ejj#o^On8*cU77hhHytdBY*SIrm--&9KyoxIU_4SIgE!wx2k3ROnxC#iQ*r z3+dLGA*I>x?%hi9w`O-4Ca8+2~(v;4fSyEqi zIuc#zTOSHdIbb*>HelbGr=8vTc4TrNgrguGwWum;gbiZ^?RBQXQPSVxp=)@fHGbqB;$-dt$&i4on19_#}EL*$}wR z(FYZhv49?@%5P)?@q4ou45tW51L7<8*CviF=d6O3j+fh!_GaQD_LC>CIC5`Xf-&g! zlipVNGAvoaV_g$}EL*F2GRr)g@C+@wSUnx^Q$-R><0UaD{5iou==v_k>I+YX>PVoHn&o%>9K|IY)Ij6F{AU55SnK| zl2_aeZ49~IfkX>m{x9_w=X7iX7-XDfkV2K1gNxuA2SP8~Aq}u8{Ng=|eh{#&Q-zh9 zpAP>m7fzP3Ixu-a$U{yjEB5y@X_A_o=F_CZZ?* zP@?@?D`W=hKOLxaDh-tV`V0Yo7{vTu+kmTu>B16`Q>pYq!5fceHP>%Gkec}Shu-RIkJVSq5Iut@siK_8Pfmd3nBB~rm>6^P ztyFJk&-Fv@k5gHl8CF;+5lK`l6vv~LGLy;>~UbI;@SgzGFc510p~LxzfJ{`K#E=zHg3YQyyDh%3R$NNk7`NfC}JC(J?!Ql3A0 zZXLyQ6$gvO&g8}(zwfX5{_B<&9eS_{fmIE`XN+lM!CyxnkmzY>7yocj6K}P1Qr#vx zd8R{bVm)y9E48m(Dq>)!4E%81TL#|jcZ4JoCQ~DLPZ0mQ-B@HB@ACN*(02$hl*ls? z{H370*0*;B4dg3a@PyJF#8vpiPNwL!50<}0v#vvt=wf}KU**FiohKc(@A{<*5 zT0HV6&igN;sov{rdK6M=-M8j$x!AR*5OXhU_YKScFce+-(~A<|2&I~^a{SJlhDd{9 zxebLpyS{xyft6euqIP|y86TAj-yy4pTYS6>)$Q0Cza3D9v3fMgU((g5=mBJn$dZ67Yrcp()<5TkuZab6oI zZO6A)Jb@s$d}kk(kr;u8cHey!Vq8Yf|pC)a{h%-_e(i zt%Q~eZkKdV^sZ(v5;EA-vb&OCzu5Wyl#2~QYgwYc9~!t$Ef{fsJ2L`R*2<>mm#Fuy z(qv!f?)uk{N#)uJD_tc6*EJxY-%{B<%Asi%yS@m}@=G78ywPa3EzirQT&RpPyJz{| zBK{lCn#)h6%sN2?F|^Ihf-F3&+fP=ZVW%#Z%p5+F*%TT+5jt48lmAq%mg{inU%!%* z%)foCvYC@gxnS@;A)$pTOa~R1BG}@?{ZgieDl3axx_Htx7Zy3Vo-T& z*u>F8qcpg^?AN=70j1+uDO3YuwKLBDL6q-(7VHOCvg!TD^710+xQNg^9`uy0+T{hx zy6(*NS;ifV_%G|J@NZo@-i=AWCZ=OoOR+%{a{6AKg{H6dO5J4mWpQ~HXb8!CQ)u+_ zL`cbnPnUcf5PL)&2p+mQ3_hi2U6W1k+P>bT?MAt_fag0F@}s1W;M1TN(+oxrZOQ6Q z4ML6E{?;Qicx{hMt0I(}tT5rSNVVOvHo;@-Ifs8Oixii=S*6t{cp_igZGfk=@TX~q zPd)SL^!ZMuWxe-Ct}9XBLS*Ut)96^bt8~t_5ngTA4^(Zvsd6sqO)33Ad#f$dc=(Ay zt&b+MpP$A4rE}(%z3S%Nqj$fEraB*`o;Yba!({aC{VeL8W8m4$fmqT=^NM|B7_xQ{ zvvoZJ-uFceE{j0VohMtcq4XZj>jXXDL1skLPESX>L!?g@@}S2w=JK)rpKBdt=3y}M z()-;q&7(j0cFKw|V$_S~D`1)4fokgw=uG&Z5`mOBnfy$E)tT@L4vdoRCmH*fD8i2- z_Wwq&@TJ=Z{tbWzqV&tv4xC^6&9{p`>M~QkgRtyzzoj5f@CTaffEh!Ab&<}S^FifK zZ!PnUtg6x&fNKGK^csc3gu>mP!P!zxC&G6D=S*#+TqvLKdca*C0c>Kwk+(%dtg%tq zCvOsFbn-fO%IR-t!L3wCj9qV+O6i({GOfrPzJ1HppI;FyKi z){>SP@Jf9J=)TX|mo-$QSKgMAbI>f=BW@L;nF0YFN8g9=*7Y$s8Aov7Gr0L3gi>>c zYIm}#El1yZfZ5R&fE503MS?UPV()cdLR)Yr)&XLuhSw1(K|w+3R|Ko?h9p2l*D|6Z zv0QI?Q-1lhyRgOkVnOG&>_;{dEhnY10)`pWgS_jk>O9NFXH zc?z`S>jzfK%ExLL4>466CFRhdsv=M+Jw#@!Y!{kWph#z6U>y4C1;yFwY7tm@sr4l# zfA`8SD7cz%Qd?{^poS~phvi1XZD?LDNhGvf#=iV4VHck3Ys?^ZM!5q4QKil~A$vk* zjPak7qE|QZU%iXYH(a`T8ZabK4RtVdIxxfX2<}Vt8Ui_CNkx=Lgyyp1%YpAk4NABV zc0&0V10U2hWW4x)LZvoiyT5shBf_iJs*-=tVqjh|K>b7ngem zv3bgWgK{S!jUqh{Z3)pww{KU$Q8=xb<2zPhP0Yq5%8&A1y(q!0H(Qe;R!KJE0N)tD zf>dpg#$xEk1UOiZH{XX))S5L=&zOmc&{un%PO3^a4Mn)Y;4y?j&B&L`#y6M?PWA>+-@f}@=Dz|O% z=gC8u@Xk+@!stHbf=7*2F^scy&UQWX%;!>krl2wC1Sjh%JK>1X#%{z(iGGJ97!+kl zYpsfA%Sc0t-uNB~$r&kd&*$nABxeTGOVeLJcSR;ao~Ve>WA&fV{3+pLu+_e$7^LKc zFO!|`gDeKW$RFEJa!RRWE60CD%>~eAS$oxane30b105W}PHN=B!vS%ao%?BEwhnO2 z16q;xvCThjb;A3P3?e>4?k7mw_#d>5fEopt5%>EfK8Gk}&I4FXfTi!-H|`l(p~v5> zHFD8>5*XP~C*@3Oq1G;Ia?;5a-3iKB!ZHQljedDo`l@35O-_y~pYv7#^TwKiAqy8^LCJ@?FnVE2(%G=5&W2Z^s~fW^l(lfIqqb~e@5G8&!ZON)0H;M@ehYUwtb?cZKL49hdnN{;;NE`aGuJR z6!=-R^uu!VYQ4UcRngu?t1Z!%>Po)Ai26k|;yp zWfau-&BMpb)Walf97Ga5b*iZ(>F-}zu^*7APT3K;)a4O5;g~|P%&JlY?2Ro0Exo5T zC<3}_jriZJzs~NYugGGZuXx`6nBu^SgCXG>a#lH$rT z@4es^^S1mYntibDKl}0eGNd(?$g*n6urvAq5f{&_sM16K+U{;wRsv5W#Lzv|{D}nq z`A49cw@8;dU`5Cihr?B+zv;eL=^3QvqT#$-~Qs24h4f2P08w01$J%f>a7%qA zH9Z>|Y4@8pPfa!bhjfpd7&!gl&HBxFR;)eOfE*jt56GhI9o}8ZSi+ltW!%#`@9Cj~ z!Gsn&M(ETXA=ojh!SV|A`ZFD_*F#ZF9JY^SU_14qf&Kd`yf8#ys3WgO*N?} zzeatH{rIN}rHJ=<$GMKRchFgWK&I{Y4LOJH?=5D%%ulakivvX&Pxu&?(WG8}Oh-gtC|rK3<{*7)CLMRP>Oqe*rGqq`Ow z7q@U@wkQlMc->K{tHm1@z^JBjYYnm4;e)Rvhhe3hy&T$U(1YW0fIIrnBy7x(bQH-w zr9*?=Xj(KX;zyYJhGOH$V9xi=i=N^G?a?ZcVD07mt`x<($ zl;b~b_>+!5(&8yr8VO0@5_v^^$kTX`}KO4gHfBeFFq*=rvjza1e$Wv z11r5$`S37pS8dq~!o%Wf@qUaFftue68ysSc$ko&pT!POfUEiVGb>yLAKkKiRRsU&W zr(Ol66daBh4$gJldnmXjh3Z9oylKqa3Dx}f6Be?7pLJbcx>tmwTdDC$t*-#aY%k3J#g6 z=on~(pZ&Zod4J$($Sx-x<(KR&(I;xNC$AUvzg}x=mT9}-C=kFkxGT^*ZFvXBcKo$3I9TAk?gyDObUOUL8 ziX(TGvl}6d{d!oS9+ko zZ}Cn^*eSD{9s(PLP1gpk6av={p3o0&>PO90KT_at(w9vUXZRGD+4bmk=;WGn$*F+Z zGG!bj*RZT35)3xJY!}~#tEJ%_UF}7%4W!D8ik(yvG1tkO1{J&tPYL`|!s%)DB#$!hws*?#+<*;PW|gE}abXO&L?>#h~K(a98FPL&&W?WaGgV1vCY zsWX{xqtRa!0;ir1>zPo8|7#!CD{n@%%-|OeAe@KfNKq}1VZ2lQ8vMMgZLtlW-pl|+ z_OvSvJ_BqR7y$V`q3XuaN$HVZgSVBrNUmM?zteCc#+q7!4;YjpBPHN@skM@B* z{lx^LXtga6(uyQN0=N_d*fb;v`%7}M>5ntI7!MxJhd*T{x`X3s!EYkq1Po(` zKlo!BYBBTLLt*Q=21k*33-j>pdKYxNuUezT?J>f>YwSE^UX}e_Zp&5xi*EJ8^3MtPbRcNwHnzSf{33C;%~~XYv_>;*m5?q0rHH=}0Lq1mT}CMl z`ejjcpl*T$hyzw99`C@Z#pYFinZpI$Io^h28dUrlR8(fE9Z$9kd8%IT%Qd|zon7n) zu{iELAz!N5GM@TsYSMX`a+9~gEuCuQGX;@+LYYYd?q64{O?XL7V$^(bNoxMQ0WCBP zfcIY3E+okKv?8=bjV6M9;c$Db4(50qiAFH-P=)W%U|-i{Q} zT!QoMobB<7E=tv-t5*bwxkZ~Y&uj`W4Rc8p>I#qu<@jeGBX-O1@%{o(H8K^RP6^k# zGo>>@l$W1ub(4GcbmHS*JP)xi9Ltk&i=snSa3tO)D} z{_q}BBo(gzXdm92OEC#emj#m#dcRP|%JDN{k3U2IK%(cHd%f-QBAa$DdFmYSSfjYc zE<{W)FoUqyg%M?P?@e5>YPcr_v@Hi8L3=l*sFdTJz3P;s1)3kNyvScjVYAa1r5c){ zNTWOS?R^Z3u?9YEH-fO*Fw5mU=9X;k`vza6x%te$RPsK7&biNTATu!4QY2P8o+%y|P_zry@ zNST|1=kuVMQQe5vcS|Tj@OI=3G%)@R@~I`qlseP2kUOX}JZE^zWZVls=PLSo`pz)c zyF0RVNvMWu^shgH3~}$9Il&hRl$zT;rrSA)e|!K@Jdf@xJWikN5gy(|1ODDWO=fW1vJK8^GyoZuKh>w>_Eqhom*+cE( zE?;A0UIV1Rsucd;HboIM(*MN?D(_^u{?7xcxmw7s>W@y9gvMLT&G~0@FX$K`sWjX} zyFyLc4!n|Ze$wW}A9*44XYIyDt6WRcIvqU{tUwcqEcyDCoMvi*-0}V=(NqfZCD^Ca zwFSp$0k;s4$3Tn=umw1n?3KJ`A-%9wLrqTL(ntKpN=0w+i%V{Dnum`E_tRZD3Sc#{ z9JxaCu#`i{>SRje0qaQHVlCSAugT&L^m_?nX)5FI`h2w=I-G!izM4P6O(*z?!*f6lHqKK%yk zO2wdF6lO*{m6BpU*LG$a+#S3k@sUX7>@NZvceN|~AS_ZKKQm(;L!c%I9CHd575@imRU#*>>X<4Au> z!d()($LU=JOSfRg6G|1 zyA2m;g!OL|dJ{^Rh2Ll=(=2>=13~11)#=L;eMDLB``5nTr!hN^x|rlzsHN9m*DLnN zhE7EIB^z#;Kb5}gTcn#eY2}r=kZ=s$-`X%_cO#-^>+BAain|O=4$1~jz{hg*Xh>eT zgFLkepP?$CDu?i?N6VS2FL%k#Yl=6YWGuY$yQ)>w*x2~9bvMWUE@*P&3pIb`-?)(J zoOt}_!j&8zjep&xP8Sm*QM--33m%)w@;zWPd-B zJ15kzaT{z@us$zDaCvg|fPP6;z2QsO8LOHrxcMbS|4}wdsv+%5X~$Ev=B;h&RA*WW}vSzTG{eEJ({Z_a$ybC34OcsX#IyhNGLd-0;hMQM|2mr zPc7(NhC4t)iHOj!*+ITeMDQdH8lxcp3WxyRRQ^uZrr{_#W$oW(1LFU>j#W7KPC>oi ziLIz~oF7Fs>Z+)X{tVg4u^9YtD0d(HV@aegXZ%BitzEu|S2;m5DNKQUtNh+HQ@}bQ z8eZGzvT%iQ!y=QdZ*W8GmJY0*x?6&eTMAlxe#z~h7NA;e8)kYf`xac7KR4>Ku23fU z)eUj|%cvJiE3WLRwBSqizLA5~6R9X9#ruvar1TE65dM=%ia-uvLxQST7w z-;LL(Hs79P`k(ouZ`=^tVAQZc^GuTVU2hSsaBZsut=LumX!tvETCpsC$ZVrVfo5fq z#kXoa<1F@T3nU&GL2VpUgHiw5SuO$q899|Gfi^bmLr?SZJq}PbzwFj?VEDKTyu?{e zw}-7YwW4vFZ+ryY=hfa{D)W9*P#y<;;crlUrA4zn0w+C;CEx|ShH3Htg7Tm#6^+ZY z<+&~+E}1G*(~X+8TZoMzf>pXny{k#r1P@~^sguTPG{vC)iR$ko4_B(qw6^VD!k%~$ z2${QND<5SrfR=_*Kqp*KK}1*}lbZ>`Kt|sQI;R>IJp4DlLo}&v6$OT>lH5duFgbHH z1i0{PRv^QHSmq$>@C>ixoZhOvz`%h;EYEk?Fv$)qf82qDX$2uVoABxZ;xyDTksk|ng*8)j%x z$WTJazKgN1Gjspf@BgVj)Q9`dd(L~F=RD^;BDX-rZ8NtH5{x3@!~H$zY_8aMcp)_R zcm4egUSl=4V%Oh;>-(wV8;mxniyR9rNs1A2h#UeBP@v=koVF~| zX>YFA|Ld04?|$@3ZzQlwG?9XLcIsOl;CU508PXQGye+*yRCe^wQ~d3R`<)RFoReSB zH8WzzxDn*AuXl4NIAhM;QseA&`r^qEH1;|{#GM{A9I8BL;>L87FNH_ zu^JgqchF1p`85Q)+4k;Mi}>u<_0kCETzjT{iOPEm`U@`}CbF9@Xx5Zc^{Joek{!2i z{d>uaT6z8y{JV~2g9ksga!Y0APJdsINiMoNvlF`FqNe$+ZF(GTWq=xlqY@pVbt>J? z&WhdktoREpf)W;X{oIxL_R)}sE`u+k8pdWt>PCN%6!lkRbk1WH;u`0mT z27s3%Ih7YWj_xf8arAB*e5j~c5+6McyXO}!x8UO>d6$B1mC(<;d&(1#V-Yv9ioLAE zCu#L6pgf79t;ke)HUbO3|FPLfJ1~G}Z2n>W?Xb4h%I`^&BW#WkJ;(jqaDE-`2Q4;D z>1;#%QwiENIycs~O!oNh6@YvDr^CT2nL0=PF+;sF?%GTuba4J;um`%U2goR(^j^M$ zCfA_xx=46EFO$NYoIljP`P!o&qy$o5LKEgkAPY=A=aS}wf24p)6@kV&DT@Jgvtc6& z*Y`C_(3j<{>7L3FGuhlB=)nno@b8+GzWr5@BEU4>tJRMm#Fp;*Uj^B#5klhO^_;CV%MmXA`HB&QB4uXCTrM zTJwH$esm{aECFE^wM%&{4~h=sIZ%*~5scAwg*k-#8MLm5fBIl$XaW^Ac*KT;DLIZJ z!0QQ=zo?O;;CKgn^#iJxJl~vhRvWaRbgLq(VFY_%_rE zKgc12pcnOg-aqB?JN>`5nr;of)+H!1i!j64+ZSWtuo|fBJEYO4iXD1b6r~{j}@T8#SlXn@R zmVcdsbwTW+lb0m58T_+eDfCJVX2qLaTPQ7U4da#(Z%4qM+Q5$RS$J>S5+D`gONHiL zl@H;2#=T6uvSeOsnD9#uA5%glM>I`M^?`SUozpk%RKvuRXhdJ;_9?WYFHujba3lQp ziH|E6n6t#FpUa6t1|ytFspcze<~KgFlp*-bT9K(8Mx)BB|CtTS|Ko~Zhxc=1=1AGM zmk8yY{oHKCZd;=I&)7-hd>x{?wF?$_k%;ORN)`y;Me}>#;DR@y)F!|%-!*u?De-Ls z4Ch`XGzquz#FF>>Mxz{8jP0{S#=n{i5Y+aLn@e9w_GtAyDt7N_{EqJTCvuuI@*DWn zC`Qn@k)vA09zJ&N&_BAuw2|YEr4m>ffScQvQnb=$6RLJlCeY^VrSr7mXOQr7&$wU1 zp`D{DY`W*{3H(JWr>}Pj~i;`)UIc&f8MU##8z(*?A{;G{f7bA6XZil( z+M}Q&F-nbh_n<9-8ehVr-s4V{?V_)aj|wsoGs|~f{Y(e-WG#;tDRXt?D~n zziiOYB1rp>AR~jQIQW*C4Qyf_;4Vc}5-KOm1cm(KN{*lEQ_A<%I+b@mmzCqCp2Ee= z7@oGPBEJU02l?|Uud4V%v7Qx~n`cjCI#6un&y)rWqHahZohLrXs+(%Maf-FSd;5)n z2fML{2{CU7J|brS&o5N{4s)Il1#x^}b455OP`-}NcC3C|0O==LUJ zgpXs%tB-T=#%BjW7Gp}MyqsA0FXGnJdWGa3c)xnV$bG^)A1vIYp{OKf+Uy=ZrWGFn zznoIgr*#ZoUkh2DNkiAF%oeDfk`WwZ%j9qackThw0rmt1=qWKP{xUzkYd)fpnvU_3^(l8<*MjnBTM+mIr4OA_FBTbXD?xMo3|qft#KwHdo!u%&@Q;~N*edSAUF@~* zyXT`#d&Iy99p4M#ZqYUP=>+6C z_b<&0bx2}zD{^n(#~z+71P7b#Y)8V?#-7tgw^ohcvx;pG-64{{eMT<*_<`KXw*DHu z2OT3HE?*@brIknfw4T%rhQFp!=EuT83WGRQt+zDX5VZ5=LzmvCXok}tBhfv}#khRX z&Z|2b9HXO}WF0q+1Q=B-9hFB_f~5u|;Ao3rizy-G5;>igB3!J5*L7nyZ~w#pnVJ37QN@l>+O?cF z-U4a%@J04|p_zBiYnOgYZOxqkt~`o%HGH|3x0uxA*w^2)`1G`9dUpX~yM)NbyaYRI z#~Ua?48BmfUlI)Wm_Xb@?f`WNqQ(r@znptaW=!20(o699`qe2!>&|-X%-}j?eFMei zczs?|Ladz|#M47AJSogT%3mv(qq2;J*OR(Ybo@P`C^VP|AvZ#*qt!RIOg z>M3Qw3??qSi>Xv0uO=C+eFKlFTI@644#iuwWafy(?VMKUMRO=V0wNRP(A3h=>piR+ zG3TNT!V)9(yl!SQ$Z&_RxZabF4n2$23a3X&)_}p2Q>lj&XIoakM>5rxzde^c};^kO4g%=6(34*r$XnL(DClhY{aoi#Pc#N81)Q>QJ+f#aUBj@VhsyN)Oi0c zRO8lX{Lhz@_}LX=WyqK5My=5d{t*|^y`)7o%TH zyap^9c8D(h32_g4r#klgnB`d5YN;)U>)AGDtoH*f+oZ7{l zYq$BVTg^mrl1)@Oy-N=3IaHneJlI%;ly+Z0NgM3rmWMuAzQDXvcG>S)<}V^>MKHNP zI+-s@6)uTIISh=dhaFP_)Wn7BEe#$*MN{=GJF)u?k@su#i#6l?>%_c~=oy5x3VOq} z`b-+WML>yJFB2J&cpS|u8VjZRC=}rhWN6I{oyQgtkb9LwUiX*j$`YK~H5)Msj@aw4 z_;79REYpoY-JnFdU4^p4^ARD2O2wY#dfAFpg}&n8x|Awt4H-)zm@s*m$*VwY9ZPR1 z5DQOk&(7i)D}eMPe+8M-CZvW1$x>V`oXgOTdiJb)=7|ED>@ z;hRVce`|3DeNlPa@d%PVho?g78|=+Zyu(6Gn~9}1t^eCfr|yVK=icbYS-RI)^lvYG zbXu4nvqM;)b~W_F2CW$jqu^-Z#>;9%JcH_!gCxAS*IzMi59ORR&cVSng!|4H6|e7U zI`qa5m=G5Vhyr-hzULQ5Oo7GPOK9^muNvQoUXjnQi7m}+|axD?M_nFF1`z4Y=k6zkn_s;Rh z*w!^xg1J3lfe3JcIV+RV;j3{JDct}!|BhgmkfpopEB4{vObJ|FfLk2gQ6K?Ok>7AV zc~VbH{ne(}%m;QE2P)6ddiJlXF^_HMopQwfI?DGjaGjzLd05D=u0~#9y?(F$uv-a& zKy>R8ET%7zk-B}u zef-pZofE|<|Hpk0`tW4h*Pbl7j(2M$9D}ER-=_R&`fq=zD~PXdV5m`zls0l}lb~8+ zh#@DsN7-ZM0HbT6f>zts5I6=v&s{7$@AQaqNIAXm>6auM*kKnKcW?zv))c}@+GFfa zwKsR0zr&Wo?|l^6qXpkgceuqfKrsavdG(3_5Dkmqj5M{|*_W}G)R-rksxFTfI9HvQ z-*!G#e;wBzrW$3@wN9GdQ2ERh@=AI{@@7#5KV&hCg**VXgwP&(P_!4%A`}gw440}1 zL<%!W(FPt^->|cr&Ym#rdyabRU$_b{Ty2XsSc_DH6Er7)>@WP3Ow)`@bvG;jj(8u%Zw< zL0Pw7Ardk})#@X*DeXv6?Xl=BGE&ku2zsr%4-uu8FhhD3GFD$cM)Edw>I1BFXH-ED+y`e&*BAWjL<UqFxmVXtD&NC< z_{@=P>G+So*`0rb-W^7lphe^)OoV8r8xRoS2YcEGueSyE+Z2dT$)$jnD)gnaW6nH4 zDZpQe;s`+ObwbKiwD-;$;5r&GJ1Lkh@P4_eb4o%%4cg1 zH>=pr^>^>+>R*m67{HyS7HiCIrjIDh?*<(0GkLw>xaUA0UJJAgJHFto@g;0Bdp?svFv;i%4uwFLFwyAMU?H=7-p*>z9%}n---r`{?1Y+2DQrtdK6;_OQiQOa2#Juo0 zy##2!Y$p2R<%#HvHDgd^S>H<^b=lxSu?_2boe+aPAinh>hMXFkMZD=JzAX!f{@)mJ zObT`58#4aU1j3o0B%#94P!*!da>mZ$A6{lu{4>=UR*~zNjwtcl?yxhR&A{tzfZw9Z zKx$Dp-i}dWfoXfUMkRuF!Lh=C@ox}1z!O}6DbULtTMQo{N_sH|AYgtwJQcH*K&5ux z2J3VskYY>tevgqKBiYzD6_vBU$>Ix)Z4tzZQjcEAbZ3+1{KLcR$PAD6fH~lMe-jJDtPXB8B`IjVp%(QYuQHjBh@|Q z@Yy7cF*v^J6t-0}#Wht+h-H4(I#h2cEOGoERytk*eUsw=h~xyeXj)V&qi>cSK?7Xv z$PfkF4gPEJh#A~p^ReiKgI9E+6G_NL*^L{h3t)E9$!q>CBl!7E%O%GBnWM)@;N9Hs z>=F)jxBm8@*|pCH3usceJRhhm4sDE@j~n5kkebD>FjzTy6$N%}Gzt(f_A*I=z;4KS zs4{%mAi>rM4>*N60}IgYksI{?*;nC&zj51m=oZ$u(M#>REXF7vP&)+713gJcC{?MR z5wo&y`aTAEe^J~O_Is7Ncr81p?c^TQ$M)IH4gdZTQG>X)^J9W1L#~8+Zfve*Nk$Z1 zn7r^Jkd;i02o|}>Cz924f%u?+7T2G|0lytPNj@sTBG-M-het&<2&;#3ogPBht;*tNB6snBABA#zioNv^zVoQ){-Wx4QcW1>o)R?VI$pic) zTF)}2@7C4lOV3~SKO=V6`VEDfzTbVUMjef2{OWzgK$SCwUbsd*{IaT`3R-RE_9;#0 z_FZ|qCN;=FaP;P-+3~6m3S#1cca?zk!C&1X?;sQ}B6QtHh&ZV~4MDR3YMo{KJkJ!G zHzA6SrH-{)3RSDYQ=PAgvAzgAZ#@t5t))&bZwu@o1VEg?*u}G)h4!s2xmS;@*uorW(B4Io24kE-T!Up!4 zE#QH@y}kalc+=I}nEhbtv7~#$HyXW;zLyfVU>$2E$eRK&7nhhF1~<*@!~UsDtl~Z! zMrH{y8^YO(_5Sf>1Y=^9iw(-nQdWm~@+jrGVvmHTqkHmS?ltXEDN@+y#l*+SAFkXE z%MPG2)AmocHZ;Er_?8cev+30*?WB{tk6(ND7roN@4B|%ouq%J{LtyI=`!U!SgEEDq zZPQU6?KP*C!MV%rY~A`{DfCg+16Xny(kPnFAg1;+1@=`|X%yMm{#8o=_KTxMZpByK~+22sY`DG0HfZbFGgdQnyuqUM6AzT$8 zAR6I$l7t%GpZCuqtTutfq+TZ7@Xdwa{m-+yo9%;Nrj&<|9fn!)xaCx`sts2oaS(NW`dA%;ksY;a36(z zd;7^YDRdAnKkVf1MnNE#?kU2xz_DCczthr8Xf0K=TLbN&z{ZBSGygrsOSd`7-1wd` z$`+Z6@;JF-ybhV5XX8#?tFJh-`x=ZW-y%i0Nof9^+LhW}yLhyGIvO%?2+a> zaqXPc&kK+P9GNVN=|_y{8^4T>oyh;&Yj$r33EsS~=GYm1{i-P;QW!RPcpj+z`LVLI z-0-IlE?9E$V;j>L>LksCBQQ5deCNHPrbyKB+}3eQ%0>w3!J`+|qn|%Y_9Ee+l&=D;!LJblbWjMbQ+ z9010+oJMjkNk)HHP@l(v42)-f+LhY|$1aVwluwL%AZUi<>;G#B>dMeXq?Tnz+2rK|HD;I?WX~T!opNCL@1zA$LY0yV%e# z=5q!3Wy7Z&(Or&@z_8QnaG90J@oTO1R+4Dou>L$2d^oX-n$Gon*Fu8a9QrlU&xyRh z#|CvnxBh8XaOU)_CDl4jgS@4yoA-RcY~ek0#i!9ZW03v7l1;D?aw}jWL~8@QPd0&@ z_JFTod@=lt6VuNv*@6lfy8`bKz9ZJ$*8f#7ntRLQ;q4pXW>~E}vJmdhhF`>Ol>BwQf_>*=v!<8g{jpawx z?oJKS&t@!o@8F?@fXt_ott2}<+-EZe3uM~I_~@CzJkCy!=(d4 ztWd5TB7)w(@h`qQG9Ih(ZZcvD4hr;-e0jaRRuJ~CVJVt6syT6ry%eC%*aDB?P&s1$ z`%)!q07&07>zfCp5$R<LV2Xe+G(KM9z(Y{?>Bvw${#?9g zYsBp0DCppG-wQo$taV8i5CaB%2e!k ze#5PSYsb$H7|k*uO~)*qKQZlvOXc*ajNPNU_XZWAFU$A*t&KYvk1GOw>$OJs5)$aOZ$k~ z#zffZ;^1ZLUuIHriOk*~qG4ay$jKAhY(AgNs;eb>95|#sKYt4m0UivM-_Nli885Al z?DRawSqh_TmiQ?W%po_ZZGH?lV5Qc1f@93~G{!hGuySNR4q`!W5CI5ucOn09;V?Sa z&VE{JH8;e;yg9HKC$hx?pza!v=aghBIyjA)NrW_M67ER*qtwc{JcoZXmV22odsN#{ zF31p1$@|U^4aJ`KwLCGM#&doElzZH2XlDSL^$H=sD8wo=f)U9x&vSN zG-ccwqNXw0xQOltL_oG+e{@up1<*O%bxro_ShT@Pq-jtR@*fbvDR+)^VKN8rv)z^h z=KMJi%^+OyT{{C!42|4_{3$8P;h-|rNOfb>bmI_&T?=UCWnfA;D6@U=@rMNw(n6%v zN}y5O%}1H+y$KWvem|=tH-F$}KdWf(V_z8$)V-PJv7u&he{ferA+(s(I50QhCFTUj z&~$@-j9BiIq>k`W$7j4d(MOy7z0wic78E`II62ROD@IE!(+#0kt-+dXZk@+@a_(xU zMf}>$^5(Hqg*vxSQG`b5ek15zS}=%)^Ie6%-j_ziGPYm4bVZxY$a| zHEg-{nDT@GYlz=+t5$)2)46zn`lOF30K%zk`P$M8unXei;?6#|O`X6^nO}y!$M7XZ z>QKkqibq;>A!CAT{!QvEiT|70hAs6O2Z9a!IJZ&JQSEo8YWr^{K&emR2>E*+L3Hp| z&^?VAjh7EEMD~@gTf@{t)v~s$Rq8QgV_BYx;pvPAP&*<4McXzkprp_5YQxcK?<&V} zr$!P44mDXXz`lXG89s*oRUOCSY{cXUXor;us?6_9zju;_3=h{AWybj`uwZS}{l0na z;9gHFKO6lp4yOI!_`ZlYUarJJJC+T>? z&Pm_WyQ3~s*nrr(@s|gr|G}a48eEd(6t}xh796A zcH=X*a|QOKIDrWGbo@ES*ee$p<6El&7@fGKqQrCDS6tz~`RX57kf)r;S=E?#|Ep!QeC*2eX!EmigmI&dEqb7SBi+ z%-!*to1rDjL-z=G789jTaz9Z}WMpWda`d4ujbn}fez9qig?IiNBD2xRE_enKt@$$w!umm9sNISp)_jlB5k z-%Hj~s7QJRH}@r1-v_9&;;KvL1*U|@NTH+8Ob{1xG*1+Sg>JnV@g@0( zGa@K2z?~JN7ef~G$X&tl{-`0#kDbGTgQy?bT6gQ?0vj5h24sGjH`vVUq+84esq9cf zuORllu$kkc^;M6Umye%4RkffkCm@`6mxpoUH)@5gFp3l27&*8c?drx%d(!C2yXn8K zBr8JgCrQkSmm5!iv>S^Snm|~u{Q!iTRX-yg_ou+^1C$ zW=EAkJ_>kECETP7YFyUTmX#8kDK79wD!3L1|S_w@;5i z6~u1QoKy}Ho>2^8Dd_2tUfSn2_Aw-|_uL0Gyt5Te36uT}(;4`@0>Z~2PiBbH{iP3v z&Y%>)hwjt4Y2lH=fZFlkEDH{!@k-%JH|e&Z_-BV#mS>$R#|=G<%(fOUoONjREj}9)qW{tpv8NmA6h=Z=KvePGRh?fS;vYGlX z?azm8HdU8!e=Ng48RfX*X#kuL=#x|?RSdOKb&L&6$r(wfKOWM7XLXN`l7^?^x0dez-Eg5Bt_Nwrhwm*IiRu8n%Wq8yztGp1 z2%lUqxO1{w<)X`_iMPurfufe>u`ftSw{xmVq~T!jdC2(58$3F?!O@4t3`XRtcnz{3 zIEjgeWonIm1SGZ`^@!gzemVw(P84LyQSS-RY1RDm2n2pnxL!Vp1PO0iFb3;`|; z=;{DZ;Hi9=1=!~&aEsc9e;^FEOKs4C5113ct%j>|b9uD=i<5{g z7HcjzTal!@PRshkyGz;M;U|hElWNQe{qW*uo@o)A?dJxg8Mplu z%mpH+L!2qHdHelL3*nwk+^ZE5P&S!Cwbyd|+@+Cq}6N-4nn81A6CS-v&=UuZ>r zl|)V5Z(npxet>_`7>TF<+cfI}3p@OOyjlZ$Z7k_g9t%8gJCB-x)oV#ad}@n4Rmuc( z?l2l84Xjaq?{FOOIEV>F!XZd6q$`W5R?L%da5e;5huEoivSDo08m6ZZb{9g#7r`$S ztY`17b)c&#g?w)7sV{#Ft~X!rbFKdNCbwbW4*z{GwPQ^mGM=4eso^fVXF(UzGE)Ee z5L3a7JNN8E{OkBD?@FZ`!=5|Zs|AdBKC_>7WEsQ0waB*d`xLgYBv}$h+uu)0_xgU@ z@6F=NcLSoZw7TZty~CO%i7wY!!e+NfVvy)UHT9Fe2v(kdG3{+!P;penec~DRoF1Jd z-Z#k$i1V(aiPxIojTEL5s(t9by2-b<&A9>*Coe^;jT((y8jH3YM~Jt9cSZeenY$l7 zmmoH2hq$NFUp_%Ku4$=_U0D%dfyhT!1F&R6YLiy=?e#1kTK%{9UwQlS30OHNrP}s{ zAw!jFCn4#t!?M?^92q7}V9PqxbcG?)v*bj*LC{psplM9 z$00uB!*%KHugjiG`jMsLaiz1*z8cH;WHuf= zUL=7Ct`+teQHH_rl(=0OXYFp)>rYpY<;vCH7f5<9c%0N-p44(%{v6xb_sOcUh?1|3vh zfee|88^XZ%5LTr?-(c((p-hs@{6bPh8*Z+`zs$Mvmm0o}hnkbyo=-Ikc*O8yjju<^ zQbexx&%EkMT(F<4DB(m=X1P0;a8ktl0+*bywB({KRVTOgQJ$G0>&nX(s|uvIlPK{* zs}Z3Z%lZA7&Brds;1of(NAm+UsIG+ zK)4FBg7R+7^8Lzr$WkVlq35_kf$AW=`R$|v{I`9tX;c)qROhTS*t_z*TGql8 z_fU#f(w=~M87l@lh%Pp_(1b%zOkggCfMc+3zJq|(0_;p2+=2+1sn6JW5t)}C+&F}EZ0sQO1730t&Sim}MGGhioZUgs3Jx87H9sKh7 zo;Y_KNW9@G9O`|4^tr3f>2K zRYtac%)d2Xnfv!Rl-|56eGbi;l#yEE5^G0D%j`!1*-PZCSanP(%$y1%x%g+D#)~>K_ZnW zjOen1!zinVcKYZsNps{?YXVDDJtpF(-%M{!0VRi2TOP6?B8%1n&1HN#m^>MD7KN3l zk4=u&Csd3z-DDo)DRMteIER>FRlkg}|HcBf=P@TkVC3+q&$I1aNX%F?8F|TD z7C?z7#G7d0Pt-}x7v{)k>8r}YzEv=@C2tQB}w zh8L`8{uEJ{r%N^GCWJS*whoMv-<%c{7 zfkDme$jL~HY2N3|2(WvBf}@Ls1!deuPQ7MKV>80m%or8@3p8X+Wkf||}V1325h83zR6t&L^A_Qc~e3g=b z1pbYHULf=?>gnkXZvFeS9{F9#kKWIEk@8QzIC#r?KR=2uPxTNN)FK(=WmmVYl>R-# z2WTKqhDUU`{=V`FU!k_?ywIpE)F-L8uW~6#j8b)Cw%l=G%K-mqJLmV%Ge#HG?y;Ol zz|qimA45wYo&LuU*kmxar<>?3ug|H?9- z`sn|4qNHES51a|;s+e`TQN>g1MG9_GT`{h}$$Wf&asd)2K*Z;IYd0}{SSUF@MIFxD zh^~)ldnLe-wta~r+&oBoW4Qjy;wTLo%fzPH07;R-#5uLUo$CDS^ji&XMf?&YoHi-t zdN>iSw8u&@sxWHTYnP*fhKKc~ZhFo?jICaif;8zw@XK4MZwfJ7nCArmB*3o@czDrA znNZG}{8PlO6#{7Af5JT{B!Y)m<*QjcU-RV!C)T9doFi1Xix6bU#_>RQ6lZJ93kpk^ zn(6L{GNsuOSe5^ze)gM^g+>*`%r5nt7OE;PG=6m|sFgEr)ZmEk82mAZ$ zh7+G3dA&jV`>>cgA;>37zFp0JA)dv`1ou^~vu3x6&CzW&_nJ*=GHd*OL&Mxtyq^O( z!e#j3TYEmpEU2fyz8$vF(YK#*`;Aw#BleZf7pBQJq*7CpQwnN>fx)%0V9nT4!S(qQq1#<{EU@7H}_W7{U}Pp=*{z&<1*4@EotoXzzVT)xq3Hdmw3`9N`cnMYR!-Xm(6vIt z&{o~+;Xx!|$WqS2FoT8+5rI4%?TdUn5Bl5360BOYy_yE?*dMftG#=&bx^fHY_c$Qo z#t=l=Mh2@sJRIe_yWL~n1p#oV#m|jS6Et>GJy6z_u%oMgzd#XI#}PhA-5{lei}s&6 z(|eC3BQt;M3WnsXLBdIo$;gmBRS%K(tgR4W_JU-js8shxhm7x`X|@Bt1-5XOmcPX)ruw61c7LQm0GtO z>mkQ(O#JDRcDrw2?9iqB?`(NRY5x}k-{7o0llAT^{Y4!0;S?*+k)!yi=vn8QiRfBK z=*<=;ChPG)B3d{WaD0SPw+lp)^(QU?&h-M?X2ROLrcj>)`uS<3-^Cm_A850GEv6Z- z&=O5Yd)<6?{;fMCqyk@xoqhDZZVl|zq|lytO!AzV&Mkf=2)c}b?rK!1QvI^V@G+c) z*7WZs%|IJN)d6M>fs#EJ*f~f__7}#8(}Yy})<*@&j#>xoOALRaVkxWorQ3zN9AojC zUAjxJGO>Ln+Q15o1=zmtoeZ#ak8eC=S6D)*c$->s>NKrK8nUPpQ+T;W9ghcU6kF;_0gZ_W_ zMJ2L>1{d+4+bjDfEn{o;3#We;;NQ@k{yg*W;HKB*;O#doZ}`&R3DDx8-sa~$I)5Zg z`XB@nnK4}{5Nug2G&xv9;R{hsdQ9RAdQ7o=V{`R5gntD_pS>v%UTNWb^g>;6(rf30)i7r|fIYnIQZ((pk$D=V6QaEDN%*z z2bnAmKY*x#=3U5h^D4AX6a7N`*GTrqPKXY@Rq=0FIpC9lFpOo07m+N$rOsKzes%Ce zo`G7aOq#AFJPOov$Xq!4v+df*)7&z|mCnVmiM9{CKbmU(JY|t-3>Dq(vsf*2JBbrZ z&Qe$yvJWNku8v>2hjZbEBrqme^JcHk{s@#^VgfX4i>_8juk7@mfAU=H!|+CqW|h{% zpo-P^8Bn3Gixs|c0t;xwBOOo1-U@2RpHc#UPUezxAoBX1r;(=!BCP`pqDMm}6LQ~b zl&8B~TVn8T;fqub7)lwtBN?`T>aJY*bD!ZdVssHw?b|a>e}t5a@C0;vHH)na?C8ys zLlOE+WHv$<0Cjhlf`!M~d*f{e?sya53};RQ>gjoD;5$+D8_$9syIcUlAEKB#n1LPGF9gjJq$!e;B4?T=JNy^*GFCTKlZf zqX1%#0><1XQf%h8jywj}6|-;N$e5EU)|Hz1+r-t0y}q1t+obp5*SYL>&!T7Z%tN<%V&EK9vxa*FRnLfk#aqQzI> zRy^2PLG=XDW4PxW&#sc*heTY`fx=K9t-kObS!$~DFV{M*MNnvD*(*G9 zesxqc^f7K`)c)y(_oGwK-(0xiX`@n@2&ok>>S{Qy;#0lu6@d!R}=Okpt=S4L>vnoJnsb*maB8Ib?YAbU= za3A+Y-d-Ex3*FcKzQvViu^Q7S3hf;4xLEF}E=z5f5?}6}43t{Pfe3FuI`+=3($RgD zSs4189vaQ&n28czIR5$Gg`h7;m6zElwu&Ff5K3)ysbq)b&U*QD{kf3*4>z&NlNxKr zW#dYaUabw`5(G#@PqgDnXGGr5=DJDnG@+{8MC6kHe1uY}*kN~w4Tl8(*Rt3AC7hY< zS6yrNgZ@J#7Fx(DdfN~^`ao=gC=0!W5oE_KLPwyH+)zO1=>k}xKeVKLc@&vujzvut zqyW~!E1x4GA}loE|Bjz{y%gru`G|xyaSw^(6oawRzjy8U$^GoTU$(a3@&CK4e;ebXVj=a_FFyly4ZyroI}VBoBhSP z*B`=4Pi3PXy?BDsgW}BpX)U2F<1A$(TDSHncJ56witQp9dAe}bI3Ll5*mNE+THZCw zH?m{A`r!&H_^<{L0LA4NVQ|t9uu_dG;2;WBhrGPNpCX;?c0zLS@nXrfL(zv-cYI{n zW-WyT?#7l4Kjgq2p$^dM+x~rWovOt<27Ee(Y1fozzZLy+y9{C2*>PW0eNy}!0YZ3v zD$H27dQSs|#I`1_6X-Xc`p_&;B8spEiC(RXkd-;#;erSZ!lI$uC$SCD;(&t<)r=Q4 z6V(mc>j@(a74^F(KtHu9*EKpjG;{ePp8~pmSRb!AwX1MgWW)5ah2dif+s41SQS`#U zx#c^S0SBx${(~K3oc>jsB0O?~bSXegA)*h^?W|B=SAKBG{=4(a0{kI zs(-@U4GBWKnOxLioD9R_NUgQQ;bsj)$d!-C;>=%WFs%wgNV369O2o-A>Ls1@rVHX4 z+BL?Fa~rI@wV;{Y3)Ft0Z+`Rb7vAT?`us5xKNTseA5b3AZqu7)rr7vdgYItDSdef; z_+PgG6>4)WpcPsSZwM_^vQZj-7IO3JWWz?wYNH!+bO*Z6+5+79ip38m2in>JR@BBJ zL<=q7e$uu1#D^GHf=%Qdk7T$%*MH-JQa#Kn)kUX=M))HBoV zBT(ZE7urjgfxvQ2b}e1BB-2@JFL9(Ig)lNLB@3D&mBMY!>E~+# z!lYKVO`_%5fQHmy<%7W|TQ4r2Io*5eVf}D<)Vl3A)fdPUZ8vr2;!#4kB%NFEbQ|?l zFeCiaGJHDZm0AmWb$8JAc1AHtebbus_l zmj*5%X6!5sWbMCS1-xNtQJd1>ZTZOfB8uP-ou(^34^F{ya7xuWTDB+*{hR9?Ka?)d zxWD%LUCWp-_(o|e@&-x(Zs+dN=}A+$nc?V~t5+|L!Ag(=yfw100zFrTu2bPp6K4&> z0xjsPb~FqX9K$w({szA%XSjsN$M)D)PYt)63Q@v%G3ZpS&9jXcU5RG||V6s}U zgV2_s3h}d(K-C-VZuyRL`lgUQg&wRN%kPZsqkUkUtYwPOVU16WmImOfQqyPwGGiA@ z>4w9lU&KOqR^JT z!do`tmR4}QX2|}PKj6SLm+PyRlW&_Mt(Qp*56^s**!de;Ha&mYQ~1kbC?u{+W1>n> zwt}q%)b)C{ps$uykJ!dP7#y6VeISA?d>rra4sQg-L*g#FYFNmSQH@w+@|40Je&pcU zh2zmqdwP*({q12d;zq#|E#&#N)bwH$4^zC|5aKZfBmkOSu2rKX869kMCw1Z}x+y1) zO&Fk8lwMuwd20YX1CtY*lXBmg;2@D|D5eh`4t-{T2Ah%csXqgUG|y<+JpY5X`K3o6 zeqSykuvif4!sHT#oTir$u6;aA%$|A`_fz7h%}|nll|nGrqZ^Iwi{BK!1z?cuv%YPC zzu3BkW#1Cs{B#DD7fW%!143R6$F{(YR43GO14@#Dfc*Lo;u9(G3$=2I1{ax**QDJ! z0JW>u=SxGasjoAs|My&qHb$e%3m{VYT{~N_4B-`%%v*7LGeH zyp$g}n0wiUQ4~E4);OQhCI|icw}#`B0Xf*!%Yg8-II^ClA`3lX$1Z$5{%MwYYzb@D z*a_BIRQq~t&{FaGTuyAWSzu7t!X@)ppX*^8b#b9>7&n2CS5_gAZa!I;84w*SA`@OT z7t}l`YWD_{b^#~!k~7*85leoyBt)c&ovkRl)7IrjE|I<{0J|V8X>6aX@RA^#QZ~=r z^;D`CEOED;&26U$C{C`Pk48k^A?(WD=EC3q6#U2=#^Ny(XAQtptHgSf5h;dNf=)*L z-~&G;w~rVSJhpxMqPVegi=mM1ue=knL)j-8e>Tr)kO+IFkIpUae^rCA$vrdz*dV&A#nRbDuUH-n8C(R4r_uz>A&^L4a`zY@$0S>m#PW)tc&f zN;y38kkxa2Y}GWcBrzmvLwPIu3*(z@)vrR1XeNqppH(Z$&owu6e|+yRj!MyX;TGWw zxFSgtao|3GZ4eZz}FVWup66))UzB-B8A~S-w@@_Iv9tUxJ1bldM zdK7Y}5F{YNkY2E&(3^~DF9E|5WH_7fX)t?4Aai!XA$S2RDT3-x+P&Cz0}ez>X`8KW zz7a04gfaS@vLt}>bVR#{z3`ittcWT4nl{zGfnO(AQ0lBW4a4AifY@Ls8%}7#3_-zf%d=Z1UqP{+%pf03w%r4!#A&Lg=am0cd z{o^3MeJ^4X-YDYQWVmtm4x;}60cPAf#1(c3cO5Cy+AAjnZ~Wv-0=OQp_bgrP+GO;k z#3}nf6Dntg(?`K1xY4E;XU?E0HGb#Yg6CC24#%%kB*MNFpQ|DDNLokuy z%guJSn3Cpn3C9pCK2D7QeTsP}ztvV(RW2xdvZV#i)Kc_{Kvp)G6FrGSoy8GM(KTRKy;*xj(JONyoGUI}NzhVn5I7@#daLz)uuGHk>425uS6R^=Q# z`{D|+L9T5fZoTzmdt*T@(ATGb_I^R7DjR>{8B&7hLE}mHl$^E%;uk?-c?WAOj&+e@ zUF%1a`iMwIv%VhL2WfH#e4PBYPSkUE0O^eYjV3`7Dcf#K8Pi1ffel%UesZ4iWX%)NiY5y(1_LwOa({q3sJcG8X z+Mi5guvUUzjmp&Z4MVB}fyecqMwXn}J!q#0L`MzDEU4nr@T+!kw?S4ppRW57a8dT45t0(1H$!JnhrG-UpXS3E z97F2UbR)Rc&`I0Y^E;@^z#jcKj%!QRnH@%Be3B+piKBm>qwqixmhxff@*xU3IbOPcT8^%nHDzl+Qd9aVzyZ5Ud6 zS9$dG)ytN@K$*Fi6L4>U^{9{~M<~hia5H{q^BsBgQ^chSD&OHQA{VSdqwy9x)=Ccx zQBVzFX;xXE-LIV@{}6i4<-#>V50*JZIaq0u*L5G2uw535tDDb3>aD(yA@#$p3%D}?g$??gIpQmkVHZA#NQGWDkT zW~bxP6Go%-L2G}TumeU1h zjgr_~Ol_Rd-yOBXC^k9lpiFi%WGKIc?29_|MkqC7ew~9Aa*O2p4|pL+Q-R=O&A?!A zCSCeC#d<;0z5BT4-ob@lY^G!Vl2Cc|&ELGS`NvmdQH~p#W(jKira(fArl8$$;0P9ec?NIjh3q=NcU?o8ZrHZuWq)Rw^vd^J z%bSK{nsAVVa^U%%;brI_3iV3Ig?7aV7Ok0?gHPqJczC>7?em5+DS>}61+>_HnpxEv zVuL(0Zgrd7H9|WxgN<<`O|+s;oyv?cgoXprCx@$HTdI(YO#WgIpaKP#m;jZjdOA~Q z8*DX-vI=(8wlVsx%$xLPsT%)$i~LK!#SG;-rwT8A%GK|3V{!ebac1KYyidw&J|Q6Q zNBh)7>vM;|9@ymox+NB`$I#85cUXI=I=L$Gc8?E^vac2=p4##1@M-A?jnNyNXd13) z8vYdII%X+`j98aMd?SheI{TnG^3c5r;ki9?>MHL`QmqWEj+%5HmQ5n~pq^biWb03N0+2#?|^qD_H=tp-6e4(A_TE<0~=C7(3!y^Dv+;+ST* z@r;mRyuUVJs3B$#g?=;L2`)712zBJ94Nxc4`z3W%Iy@%kj#hl_+~3s(q~##s6Roo~ zT!MoPp1%QTfIPkfm3Mn2?q~k|R?ta3Uf><;-6r^}rnJ{G!;2wiD*(6Wpe< zJ3VM4#CTuj)j1C;;$J@Ms(oTV*6}EidW8foJwUFH)~m$i()S*ypf#1V<3eksh$p(ehu1q1;;1ZBmVk#gq@dE**CVy zehl4|v;cw8K-%N8T(bwiOrBH@t>(RD(ep~o9=U1_&`CFTB&itNxFkbp_5N!AsYFPh_ zcu1w#(Ry-G8>ER3CQo|^C)S@Bx(vQ)FK<=@oT8M7H|ZBwOn zvu-_EymqP5((z(~j6eKMqEm(#D(Ra-Fqx80qepxeDx9+4!Cb4TRCd9k-x6f|s3yAw zs1QHHzjMgfs#O700T$`a;0=xa$z%47vu8Ll?kKHM+o5jN{1;gICwlTIH9a>pzo0END z`E+%FN;=4wiLI`@5G1U7+BQf#2m)_lEn{6?#%jAkR9zm^alW0syIJ;*bt>gFoA6HF z&A9bRADTIkK{f->24t>2Y=pt4UGp{eEVkRe8s4koFDUVo)W$VL3}N#O&9|M!pUI2z z8UhsT9baJXQ&=&hG`jQjkhSEAS@f~v751Q%E&+~g;l>GZfsi(i$RO?5v-L*kj!dF;GD+r+rj8e79Bpkm z9r5p^;RYp2KovBHS=$0`9yejddzH6VMd{(MpXS302S;`DseGuH>wZx@VJilic|j(F z4MTUXSSoamu@&-I<5r8&06kL_EX;`9=SK~XaM2Tpoih&X z=vv&sJ$x+_TetGB$+WbosA;$>NSi97aXM{lC+%#Sn5iXXH&zCI6fNVALhK)nswVnm zKc@6??J@NpaR~aIb|N{{Rk~*KpU*83^l2g$BxmEvjFZ$7f{-jJKEifiR)MxaUTpzi zpHrC{oOIK9QZPVI9GP&2GiCKH3?0M;!v%ve>Ze=M_g9U4Z-Q2D*43_r#z;dtgz3zT z!16xzEFe4sojk*meX?(MqW5~2$HNo+@g^VaBo%J+;nJm&Cegq76j(Z@E@?e^PI#-S zw9G3aLC4^G0Z}$KXTx_mnAk<1!Zr75tBLbaec?k(YhKVRbMOGaLN+1YAhLN$dgbSM z0c*1#j%wdqtD1(b9P#Loq~F-~=j6mGYJJCrTz=fUFef>ZlAleCSPW96tiGyhk7BbGI4G{+s0`Y^`N~O$PlcK?+@%1&@??l(0f;V!*1zr%)4JN8{L^Sz&j5CYg<9o%vM^tEQ|U%%+W6_uPzT z>%GdT2T#+#u2}q)NcZj0xgb~9m#yV8C$nYbEO{TB@`gy^1w-aKNf(}%wT8A;etpH7 zbbBIhtRF##$-K4iY`*bcMApQnU2 zKheSEi`by~N)bo9-zzICXLcXFvL!Zy-^c~#)}HGWJK_e_y|U-vg;=7ex0CU%oY zVT`C5<>N8j?d8>h@O$+zO8CD4b;%LzSizr`nHl|vE7rP0*y>n@$tQL1wQtAUYjvfLCl`Z?2 zi{rSI-mN}>f`RRc5qh-rMqcm$I0WR=irbX}xIbky@WwA?N!-;3^UzRtxDB19fdkZ_ z&79v~EBc9BfSm>df6)-_bT-AXC48Ly{u<^m>7zISa#&5z*0V5S!FhOJ#NtA4q;78g z@&U8Qtg-0QO@$4MNS;{?$7AYmDP$7NWtWT_q5{u)O)!CJ9?7J1gmu0}Ab=td8u)5U5bralX#*%V zT-S&SWNpow4QeV5v^e`iff_-amdZtY4Ri?PCh13Yi3+Q;;}c|4DxyE#8)4^B z$P^waV>s|Pe5WaWh^NWgZa>fGBx5AqlWql;B~uk<>SO#-sJgM^*7Lva^UP>*-PY~(73Xe{i5vx2tosc9;;(G zCZ@?dn|2P#`KW&K=$kT&?+pYdS_OeaMJaJ>bE|`c_HmWS=%C;>gb*(dZ$L>?Y|PBe zda^r>$~!}lyfFVx^uDL>?@x4q7JGn-lspq$0wQ(+S6;=9GkCsE=h2@d-vOdOje0_L z|3(_TtL;eL4y6rj3cT6V_s~6-%xH!a(BwjH7O3ru?Q})a#3#;YUaw7J10L>A5zF|- zc~cj0I(P9JBXR>@Z5KSI8#|_MGKbz;uB(N4HMBE4SZbw{rt4xX_~O&5+bwV5sInZS zZLKi;$jZ?LAHkDg@vpG=zzWXPnI#dT$?EuaC9DZPB<)31{5*#3A37Sng+dF~JJ?v? zw?CKfF&(^N)*%8PS8CoHcZdgrL;(2iWAaho38>h5t^)8yoS%c}{hd3XUqShYPd*y0 z_%CQuWBn?${%8XdT`PFlRt%t&&Z-=I0xI^2F5HX(vIQBLM!5!5Xjf&LRN zRPxZ)tdOr;?)Meu*s9bzM9FMq?BV^9G?P%ra4nkWHM`3XB3hgltDg-O@1cJe>>S|5 zbt7dctseqMHqQ_8jKmFQrYse+!hXcH7PGdT8;>i!{evFa78JEn=@+qAHfgkI1%ZE; zz&Z^-nst$a3~=Z3ynGdFzTdpCgvGj$ zQaz}Uu0!xmd8C3Lbs9pIEs?!@{@a^_B*y;Bf1H#QCZM3Y{Oi1f3l%1ybreQJm2M@v{gCJ(Mj!5@1)*J6wLr3HOwk+#dMFvF; zvp1pFnf+1vB3AdSxeq4D|8#I|viBYRjoI!y$kibjaocv?b}0Br-18=H_$tTH%`0#< z;qlYW_-NL_`UbvC%E3}Us+QpAtEId$NEbTd6e`i6{-e4I@Bp*^ebjO3fX zK2R{eSRnl&b$l@x-|XIUpJ?}+&1p-y!l?B`WHMLPA=9eP7JuMG+~3ystg(NF^tiqm@+lDhW}m{pmxwoP=o$qQCZ`)=r z@7jj1*?qYEwPXBrof6-aXFHKGY(vlRkWuByAx2Gs*10d#Pj)a(zx0RfJ@mhGlCVh@ zZBdZog5i&otVtuFi`m7G8YvFQx8LRjTw(xcX!z7%avbG394JBs1qBhK=oqK>%mlK= z*PCzj2^H}deH2g-{BR8-uD}Fv8pxjw{5Zf+Hu3s#Oeh}!0WM!JH@xEZuk5k_Bsxb* zo(|THziOB`2EtZ-4?}Z%{{1;%P_W@1*~+fIz-T`rA;{D-r)kP9crmQBgsi#XZO&_c z^yaI#>OW{v_KJ>$EVwoIzsDK{N@MaT_xw8mcgrW%>kU6k0cTC)^gVBH$x1F(E#S7Hc)lEXHQSe z7@_AsbE9K`UGzUgZQvQC&U4`CQh&j(;?%{#p8da9JhyM4E)&+@pr1z?1N4vtQ8fF- z`&?-r4TTVEb8V>~uOZ5CIEh2*2iD&$o3r?`az>yDDOA(_y76mg{;`p^R}{7f_la7> zT)x!#z*3L)L(R`JDJJsW`ouc&{hnN8&#d6)qtn<#GRbE9F@IRd?js9Frhia8e)t$G zYG^_qSme~|K!6hsgKAz$ci&u+Oc-gcWEE(E;kJ|3x{GCd{xx8to!>uX)BFo;7g(G( z+zNw*=5ONO%ABm00ct1WJb%P(jM7sDU#-zRPe4tZPjS{9RS@Zi>5&}&q8E7Z6MExw zkEoXbjJP zN?e_RRQ)0BMbZHs5bR(~J5KN(--HRFV@%kV!C1oyY(3h4Vp0hRz>183$@q0(Ox9Bp z(u&Ij(s^*t>2BsKEarhG0k-c_|E;})WKq}TV?5G2F6d7~Ka1d&7;4hKWV4Hw=HxCV zwE+O%o6q^QV8N1mYZtPLHU^+Q8u{fCN5`ZEb` z)P#R?s#G;DV=FSqlUX}cp-|3%$9iCMcL`y<6oPfd;cq^#b z4Mu!0`r-v(4MFeEuZdAMi+@UJ{KQ)jFX(y)EQM{-Ai^~sg zGI53jC+ipC06mbvgM>kRuGvB_;LOHOpfdqxZLsmCEKD0sEd#xwZS3vs3rD+xgKNVE z7(KtfDHAqU_&#>)i74F-%2kXQr|F+);5xT$*pwcD#k~Nx%}8ddGrgR}QfCQqd@Z=M{VNXKp_=I zuPYFWu>i)#q(vnE{&X_x#=ra^PvWCQg*U|jvnU}6S*GGR~l^kZBd`^5^?Zo?ZPTw4V68)!N`|@Ae z2j37LRFfj>8xN^qLFsUIa5^A2AGLG{!rRK`FKcPNhsWsH$a^RN+xTTv1*`D~E$VdJ z7$9pg(5(1IINF=B`y>p=XWLB8ptIk_$h&Oq308Qedr zugl=`zZZSNd~zyXv&T+F8p16{Yz8s=-hc`E=#^-76h5#Wn|zq0^^P^^{txide$=i9G);pG^Uop|Gihl#%sTB9fyjk==f^}JK1^o8J~JYB-ED@E^9 zS_ztqwEU8%gbkt)-rmIPv_aydTvW}(S`ux@eumw@(-rPM%G{1vI&=Vq{q<|k`uh4M zpHi51$JOsIU&;i|z;H$Y3l#m(5<1EFG@`~<$?gOc9QI=a%}>89jBjHUYPk(IuuJZJ zDEzW!+cYlKOt_YR(v+@*x_65i4K@H2sx#EmCSK2{)<}(A-cL1<51OvYk{WXiAeXpG z+P$so?Y#EJ9UG##OA%}5yKZ1-Jt58a-Pl5*hwWu^!Jq3~-FG<1>z2GaO~XHDz%%k_ zRVPsUA;gr)vB;aD-70|0SFJ7Az!m*rq-W!SW7<+`$_cS}j3DZXXsI;sAmZceR|M?Z zQfgj-@x%(92ICYsbnhkOxe&b3?IWQaVZvrN)u#uMPT?A*nbmC8$q-Oy5_~rw+M&GX z#h+934Ji+%5L3@r$nW32eY?w@Xy{wV0@eh$GJy*Kn92M+;V*>`;f0|O@?pzoc&-NC zW&8}s)lWEuZG#r6ORMvmyy-Y4!USKCXB&#lG6OC8$V~_Mb$l-8#Su{tm>}cMF=(V4 zb27(nvI-zg$57SXJNh`7Z_x9%-b0!9yc+{02eGx8moP%^-%n3mO!10o{OFzHqA2pP z0`B1!+xMpf7r1ndw^!kxPUecvr+-k!^u~U(SByPtsuts?A_hv@4AU=CMvX)HI6L~ zmq#A{R8@kX12_%@^^^E%3#5t`5T5EQIf!E%a@Lj^-+|#qKA%m}213>|_S>`<(DC9{ zP%yZh%%mzZD#K~c2__1QP|pw{>3Ym9$Z#Zb1I9{!8~ zQCR$y2c;N1Lif$Qi5n$@!M_+aL%aHWKQ4QVs@5WG6-E)NG&IvF zc9ZNdlXRTMI5-S~=*#2SwbV7_%3ih|s(|ocEcpb5Jcpy-GvK;wc$v@>WM+uQ`Lq~7 zQcD>Jb|E%-UX0XX4b*th z8K#_|wt=w>yC~x)tT0RNoZ-amDgO4`xk;8W^-q&2o%2vTRe6%ypU%nOm(e zZ-(0fw9AY1MQ6%X2R@opAoE5iy`WrX_pOXv?QmRe%r)M2pCg5{&K|SW>Gfh3K!UE5 z)X{_n-|6mm{DVi$dn`KQ@+6z#^jr$0L7e@A&sv6Od=_a*%7t~kyu3V@Md_3pWsu0I z=Sj90r|B8)dr8Vu_-HBrz$t=$OXO$rD9as_1V-TU|Mnw38YmxyxErI{paKj!nSdcK zQh?{q>!FjdgbcdKsT)k_5;$YNR>aXVcqb!^QfWWm_-;my?wHr-UiouV@yi_QCPoE=@<-?MtWV{Kf9a@Ih(R+G>1;_g(ya%johArbUraW5n|%IOs5 zh5C-NElnGQAU6>nlz5ZQ3KU@vt0>gk3QSUF_M-A%+n&8M{qgREL*7N*9>mb$K{qv@ z;FlpkNc_~mC-t3Jc6#vY6vBt*Ycz#bp!^pv%(neD?&r!g0o94}Q8G**=_Do#WYA5+ zf&7jeOdyA z_GEL;dt7rnVdP1Z_%4bRM)P_Pa|5|0+~di)@*oDzY`h5qZMiC>tOz0 zaO15ua2yh}B(_0}kKv7cR6nZ=7Y7vA;LUh=nJJwPD$)YfL`b0}ASz7A?>=XL>^ne@ z*ag*g-%CqN54~3%zA{yJ|CwFT0c&YPtbyG3N!-;dXs-onxaod;7UWNLxF>0$7Qj58 zUjm2Qw0E}&U+Vj$D>Wn}vw3sI+HtwE3D<4F%Nf-Wt@96B3_`GK-|GfR%pgEK$>Ohh z{SHx;fvL%c_nk_gtN=>SpldFZY}27w((v$U=;kc%K;eP%Ke|uPZu_3|CfS~45};|j z^XTVLdxCE-Iq#ER6EJtbVTblPmX2(tRyR|L7G1{=<)RB`Q_r(t;J_c`KBd-TU`~NN z&@_QzE&uQU_t3o3IB+NtQ0krhGpKokU*F>zh%?AwCz_8GEzlN z0Mkl)yP3s}J#ij3HR>4DB?eMWolh=}>~t@zmd)Sqbg2lbzV)bUo3yRyFSP0BZa`z= z=ai2|%q;nJ4V}Qi+$e=|GpAe1I3{JDE^6JobLaC}pYpbpwIMYcnp(4-Ph%QItRV`y zB{5}@C~t4Ue@0XYWVh!s(RnBSbMO$i2UyO<%J zN_h`-0BbsbPT&Fzr)79znaNM;Xv*m`=mu9|qxi~uhOsG_tb$T(TZK6UY#D$U^Y7Hm0XC4nv4j#0h$&SCH_!-|^;| zwlr-`t0KU&^SeZ`TG@}%=PLtn%6?rE*Qp>hOqvuCj&36SEPx^@owcqM)Fm}4x(B(T z+|$pvgbm%L^Bm1%tsg=lDAco32hZz+ZuNkXh91;^Z=2ZB=F1nrA`tVz4u$zAgjc88 z?r%;~rSl2ZxyOXxsZ6~9!$#Cvo#Pkp@EfU0?BgP$0<97YJ` zLF5e?a$>ZX{;WPE9jb3{x2!vHEVt*LZjmiwZK-SmQxqij?%V|Zt-tSBEvczprdI%& z4>u(FVQ@+L1FP+%Ts9t>Rb9g57msu>W@U}zH ztBDn;D6eCPU68P`BOc#&c4|7tseW7J<$mNzuLGbcRw}s7iq@~J`kn8otu*-BKXi0>ZR+nWpbZ9s&)<0xm)e_ zAA4>IlP}UW7YC%IYxsez=gg3EvPeTt`0TNuXF2M?!|=ozZ)O!bIsAa~9NEs>d-e1c z(H+ql8(+DBQ=|RQmdH$!(89Cf$>1qP0{H7JFc+lDWwgRwU7(l+JwZspH-;Ss#>4Vg zv=X*9U;h`F;Mv?&YOWh!-4acT+9emef~r{XCE<=qTd#wrmNxSv&+qHj%sU@cved0m z`HZ%*4X3au+UXkC4P+Pt{(wYsN4?nVibP2ey_PUXlmY_iy!@pf6}vX1O5-X;4U)YZ zkMV-1Mt|=CHuRiAJWMmEZ|eXRkJ9L-8OL1VdMCPDfS=rHMkkog;H~I(C1qu?0tWO- z?^T~8rbftxxwQGYmrFyfvl{C7dxREW`mOhnCuyT7CEU=xf(W|I5A6yVm1gz9D{9Z& zSp5sUp6zL1njP9O|BJe5>w9?0!@sI-*E_l|i3FMqVjZ3WfEU@VI=h6E#PxP;F6G%* zrP!CX0d+fty#0fjXLs9j3?X?XcSqh_%LZTD1+oPekwLL@V495?#JU|FbdlS*NtB3E zl=2+7+=wM-73vtF`hQ%YL=*t%ex$QC1&|HkDF$&eZdu%OEm^?F&G=Z%r82b*{hS4K zu>u%M{g3xHh)PfWNRPk{qq^n6t^wyK$%YCz>@9`y{x9zNx$0xLTXGReW|}_HF;@h* z>a(ZCaWW4y`RbW&aTI5@kwmJ$BD|r_hQ0Cun)CAHUkeQ_N_xwroA`*^e0qnEUXVIA zUu}v;nuJE|d|F+Z%bw8P^cdc>)o21tK{ZJCOuWz1m>!vPU)<12x7tq^Q*uy603%*jtOmWvgv1MgT1hUvdAfeMd)F0@!=Bnq+qER&dRd;L`s z!jlBaXxqlx7rp9P25Xs#r@LC*sckh;w%YV4w)Dnclft%v3Hb ztGEk;)TFj=BZjL|P_6Vn>2ct@IQKvtw80z_rH0oc9Cl0^h8|p=qg|W>0r)}UK`vDu zOgIEkIAPewK9{NQtJOaLfvFOzi3eMpKY(rdC@s|@Ay`o`PyzFA0rs^o^gtTLffQPR zk0l=Pv&5tOWU=H`?}@4ir-2l*F$n}z{|yazg198|3eM3*V^&`qU{#ViD`oUL3Q#hd zLNDTgS4r#?+f;8{D9f3}DQy*_-+{HGU7NC<^>NjW*T|8O*y`-5&c!{#htT*Hoz2l> zlBRtH>Tx)4M-6z)J`p!RG>Dbja}FTA_Uvo%SUKdqdW>5ND)~%K_(Zo)UyQ^e)>d;i zIaVh(IaJzK4|oT$qNHbvl&&5sUXz-Gm?M4~=KJ3|8;!#i&<-QIT^LUE;vGxoG!qA0$9JCuFlcwvj=C^U20jp(NU z3(R(`84f@uM5it(xjZOHc6XBfIC@fPR~%}03weLSK1*mR4y9{bwk|LvbWwXyD@Ize za3|JxxdC?lniwD8^Q=+EEkZxqpUwVdN-Qh%q&LmYOrAfKpYVt{)*K z(YRQVz8%E)l-L?su@8ZN!RC(YQdtu65f&rO*P_5q3Gz|t z0%`V;mU{p{4;aO3lC=R0_zUPQPTUTZQx%@d@}^K#;8X&K^c~iTCx9fIOx`v6>=L{+#D3T(LR=ru`F;qv zb5VH7If;;$^uzwj_%?RltnP;z)ffD(Mh*9WO#?hn1^Q@k^qFm5Ng^)+4CQmvdgH`L z6zUe$1kz2+&PCntdI14K9P6mZLJtX|DC3KDpoa%~OB@6nKryBbGtgyW3=p7`JIWY= z;d+>hIQkAVrv>zgLqKJ#<&>l~`4fBZ`@80HDZpik?71rgV_7%bkiaK~!ze(c_>Y2B zue-dSBmj@E@CreB4P|3|+z!KA9Pe~{a~vSNQhf^k3_4;~be=EkSon?7Bfqce~H|sV{ct^c=&pB8w?wW4TKwNm9-_=2%<+9 z@W%2iFL5*TOlDRXekCU0B+B1D(dHY}N7NuQENZ?T&iNkcx2 z1NI8t=)rWsW-n7F#){!7{MP1V;MB5z`KJ{flVvE1_eWfKI0EH_@FLQGaE@`{UWvGk zV5u5s!`#`dUp}mEA<@?betjIl85D!Qu|566#yKLf?ljItpzqBvWbD;gc@z`$>b&Q(Y{G;Dtn*Hxf}eYSzAcs7d*>7 z1k|~la(SPxe}V=_teX7PE5nVr5e*&0ZFj(fdh4(AE-3>PMwEwC{~wa`c2`3cE*dA0 z77D=+GpWM$aUb~6y?<)NkM(C~U`0+%JQta8Er2CEr#JKRqs=b|k&NAsU%+ef+&7;{ zdmhWrK&qnQn*{t4XPPY^8ecmp($j068!)SdNG1HvJDj}TANz#V*Vo{Cm_;f@E`7aP)PS~Pd?Dwr5cOL{#F}rN->6c=@WTwC(f`+*YZ)@1Zfdy{ z>7y5Eu&$j=BdX)L(D;K+kbbQEqT9fcdgw-4cH8d9&IMc>Y*qVTAkjBtM_*-ueh#e% zMNU!ZJL2dC`h--Lj~te`?J9BblbRYM-Hiav3P!r4f`HyjOHxiPhf(|c`yC5J7=yx`XgTfxyxh3#FKqv$d1jXx-OZ6}@PS`%`PvV8D zU!bFk$Q8dPlj(!{m(qdRxr-TvN>@BAe8PbJ8XYU3_+|>{0vh>(Ufn2NyZ|yglxFbx zR_}bMBR?@J5H=*<$N^)FM)vN}MNgbhWrXDy;Jx1DW7_6S@|B4K3o3D_m@R5=YbnTk z#eotQ+u^UW>wb7&3<-9?YK|zJ1I0=N**0oq-9r$6^Ghzd!)b3SuqOLui~sk5FNpx~P{=KP5Od^~1(I8d zmZkTA$3@@XL(+KRN)lURVA!FD9z~pTpQ`oX=(s3U|E|d{Sjxf6nznJp!Y>r+6NW4K zu#Yx3<3qcM@=x47TYys7khBnhy1wZvdAP&mvq12|1y4?~sL3SN@SF-Zk{C^4ch-gX z@%vA6wgJ~&)EDb?2CMvElA2`XKyiQaJJxGWXih7mkM5?yb7z0sEP0WbuZUil|KfiR zW_Y%`WvJz#kI(bM?m>s5kr4l{i{s~QK`1`MnO?XU+omVA=0!1~%Wv01Et8+6qilm; zkYm_EH6_UGRWE_{?1a<*uo5TXZR*FXoW}xybwEV|1mT}wf>iN?m(^va0hVwH0?3P_ z&y7=f-a!U)KmeK@v{SJ!_cS@^n@x5Yc;dB`7xx}Lh|-GZDS20_LRug~%| zxO$ww*DlwiW~i>buO@=d8==+`7^T(`7>>RboY)eG%ndss1=gj$q&Q=`1@IwV?W9hx z%)0KH&4~}JPx;wqsXe00DEVcVXMdamwc|Ml@JvbO$_1PY$*vQ=!F`F6agdls{C0Y& zA4o}EK>2n%v6q`2#6kXZ_<~(Kmr^EOVXv5 zSHX6IM$h#3QA2>%t!Li>uFMB0wK=-06ccI`^l`%dE`R8^OZ96Qv>5g?t$}=o#B)P7 z*f*Zsh`Y;&pva$&wH0!=Cv4C7N155Di?k0m(77pp>%9wrKt2J)?A3AS0{t_mGbzc2 zG)=+>JPRRzYDTU%vH{E3N^z?Y+>^ox(gwli6G!5(ujbS!&m8~g?(Dkj=+A`EdHh@% z<|Ea9HM!oXnxs9FZ%xL4RP(CH7es)dR^Xa7rR^1}D@==EqatYSOe)est?mEeQh|@2 zqs_=+o2`QiQ-P^e0MZJiHl;H*4_x?XrzRMn3@de1Ia{Bp>-E5;4!{62N^R)D+Nudt z|N9ctzD{ytP!_AJa@O}cjr$(DX-*Da2QD{xR&KeaJOT4jIYKp3B`)hf%rSd;_NBh~ zjqY)q>s*-&Ui#(wV=YXsD&Kkqsx#AL@!<7w|FZ$R!Cgp=a|`t9TM;+7c`W`!beeUs zP5DVh_@AWcp*|6EZC!p9=`)VX4C8S1o@RK__y(@g+;?ANa^1I@Y@EQO^wX7e+FlLN z$+yTCzESj|R%EgcC}mwI<``7*fqd1k;dwcs~M~QDY)8c^=|b^U3umY zfvLnyg^q%IqRhZ2({1b0w}yI!BF@NJW(f((e+5_7yh86K=S-({41!-}uBOgIt!HPK zmu_vmUJvfvY`S+{r6K=KqaPbk{w+p-ekV`x)+z6A7P$Pexi369N1D(_k@tj6Vjdlb zI@Ju6iC2-gUnDw{B-q(j4}yC!7nC+)$>Mvc97APcK(A|Ej)B}Yf{mozMRFMtC>Z}| z*EGk^z}|xa+`mY({R9jUIvTeh-ah~Q1Ka0fy5?g)v)YTf+~nI z5iG!FVSwdKl@<0`4!5uE|wkTH7~42xq|zXThV^&du!@ASf45d`m+`!NVgva0>HBk#LpIH z0hPU0Ac6RBC<}mJe7b^fpaQj) zcVe>Fu9uHcH`qz%B5Bl4@4Ld?PQZsV<{ANzm%E;gODe9@`L5Y^c#?(hu(q&GosZr2 z>=Rb|MN1->=FB&s%jX;UWf`1-0Ab6oXov>sf@C9p>2asQcTU_$*gtK3O600aT zF>plgze{euJaeO0Iz^ZIrv&)EZ2=BY-Zu2N>3Xc{wE|E2q0KV9xXXs)@<8uN88V`&_|FFrOY_8DtjT*Kc0g2pdR6SxQTN-3u@8s zzlSOoLDz$$s3Q4QfLxHO4@id1?=D0I>-e5-#lh5cC$}eCrO%@JJ;9ms`ohw;(QF`x zmMX^p7JT55W_ zVWfn;MZl$>h380-!^q=kT8+9|Go>PF!`uw9>bv3sq?{jv~v*o31kvd}CyUSAT zj-Xv?6AQ$J&vbs-Xkqq+8Jv|P42_;oE;6@f=@2~4vRtlel6^b1!4~s`$u6k%dk_p{ z!hb>z|M=CAn=QhOo46A%@3(ABKUal(8qAusccN=>y;MM-cu%miFrPT!^*AUGNY+-% z(WVZRpZl(v?FNM4;Hn+UiX0K@{t2!|Mq7a+_uhLJuRviF=k>co83_8P%W zh!l=G(13D27X8n32iz{8I4*#II)VD*2Ef}yAwp#ZLeIq-vvvFeU6sHlFxEK3hmV%) z!A2rCS`ruo6rwJl;DRQ-g+|J(_sUG|mkZONA2&O=E4JQYGNR$3{llpSG*O@E4fw6@ zKLHn7px;xmv6q5J^{WDER?Z=I$Lfu8IdI?b%T71)bARv0k5+{Upjb$n)<@6M3ClN6 zSq9!}h0r`Z@;|nTxu`oi7xEh?xAgRbG^Z8!&uD4q-rZBDQ#wsg)PwLl6lYt}gspGb zi(WE7gYU*V?s$u)zO-gF;q%Q!&`o{)uDwya;OUC`dEdxI@{BDi{UCUeEWMui5{|AW z54-{@(wvs^tH@gWy49qHK7r(`IEVJrzcA*%;sk~OBIxD0erkxVI!NO&+-N@N@hwmt zm8=ZN0@~J2{R-TFjg3wOw#2cubS((8wiYhx^F*VBuguSPrROx}0sSd~g&PR+D9 zwm%Nt;M;IyEd8Qoi#ln5IC~9R8(ra!OniQjGgK=8f^|F!SejU!0IlMbwl6$);AYc) z&v;&qj^|9*iCw`{JtBy+KV=-)+j+xrTiSWM{MkW?0GVd%Z24~u=+=c$=ww6kd|Wt2iw`AzJi3GY zONB{FTnB@8up1d{j_J4UW3GWfueN}81-3V=wLnT#6J!H#9C7!X0)g^4 z5yFw&b^!yU+%on=br|2orxL3j{mHEeSI-^>CK^S%Q=9Hx3l29B6-|XC2&;ij71{p) zIk(}L#L=qo?VCntXK#JL8So<09AP)aU1MR^Wqn^y_tNZsG+Pk1D@EZak+;M1EEx3k zi&2pD4xhv?8VJ!wnEX|BC}*o!->zVu#oo>_m9~!ecLb50v>g>dH*@&&q;qcBnlv_q zitE`oZVhW$b9gf3a3q~{yt+R+13Lv4?lJVu;816uhLUK_lQbWGf^U3lAm{B-s3ni# zKl)B=+67>@$N+IDb&6j(;GQR`f(qXL3jrk?sK7+INIDh>oj=w}AF!s_8&hI*SEH5) zIzOl(SKfn@cwu*#XX~g{?_ZQxJ~7OYo!d>|RcZd@YK>>0@$$UuJwim5SU)cE=0F7b zGIldGnmPp9USJbp<7@$aS$H9Q**qtw*z0m+qR_GwN9Em`DeJ9tqoOI=l~fMty2aDS zc4wN@#55o~{12I%3<8_ZceW3O-pafROWHYvp`5HgmrLg2YAb#8(TY=^}0( zCuGzozBb=5qgoLxBc1D*&m$kF8_8PDI9xeTSsuMA%ZLYifBChFH-z*}sQgG2pcp1r zSUQtJKEaPp+S^a9+Z|j<<2!Y=PmK4Mt;i&H6#7Q&hVeiDdNwV*1}f_e1I=&<(h{jG zp@3nm2iIT5(twE%BiopgUizqgTOWPh``?pEncT-Sc$5y);DF(j7%JTaChBhsOJbw2K!`~4jC@iH zP|>pRx$}stTsoDIpV^=h{lv3rVfBLTNMFW_|2F+j#LP@sCnB2ZwU8E$t$ zkPDNej9nyL#PMRfZ=~B+X3jxk_W@pP>7;tEkBK6`p7@9q(j-s~#Lkh{sLtK|DLlT9 z@W0skY}%CYjEa~JwY&_#NXoNQUGYJGLu^0=VD;dpS`db3Rv1l3#n#e~NEEKXMRVH| zK9`h~;A(1Xeaurn9(4lA1J|Q9*6tt6A9|`DqT~;7UnXzgSn``OSzw<131UQbJ1Vjv z^w9!zu=ZQG=~Ahx*1=*Kqa3+c=d8riKgRUR@M%2@Cb;GgYhs1Bf6T2_@B{E1lY3-o+qEp2{s{qPF<72mv3JY&RJ> z{0yn-LE}->0555uC>!UM2!%E>%^D$H4ZP(=Op|Hq)b(% zJ?V#`cv%QH?ZM4RwUF!fr9`H?T#3?(gmsW+-O&Z`fgEX>!o9p9=0@-&4wCN?D0IR| zj>>%0udgy$hQ2cLVetNQUcPQeG5!>5`b9Ey+8xe7=U5KEWK1J02Q+QBL;zrK5a7bM z^~fE{vCgD8_pvl$(L7$A^6)h(>dHT$CmF)lQpoE~pF|ajgn??cmC(_B%(IqI?3Frd zp!sxGOO$@W1H?`jTT7jD04y!DqxA+q|CO^&8U*OT@k-~9)~9XiuAQ5SS&euj?RKYT zp)&zfU=fy^SATTR*BiZLX)R|fh-hu`H{$f&yMj>lX-ep7J7AdmuUhElBok8Rt0f_yq;Vi3)RZqmralyotoV%-SqUsy<&dWIpgPkpOIO}#7svVflzKU}!I?@jdW^dID z@n#Alq)>jmm~-AMyJG?|4z_exaO}RKowz8oL?~7@u(DN@qOu!l3DK)Mzw9Ts{ZUc z5twr$ZLZ83ERyXzfr48ghncqcHyqM_oAgT2teqO{zDf%byaswtN1%eqOJG~`IYET7 zc^dQ)zf%I8r-`G$MN4Q7C-3>kSFq(YD;ZpVGa<{l@Zhbfc!1*YF&X;QS6tdX!)()C zQpc5*7L0Ms`b7Eqj>f+-j#hx~N!JDG)OZ_h2BDy^kJB)2gJZRksZ%(c=OR~4Qdtf#LJAllTn)``Q^A8lJscN1=B+>GG$LLD>iJ zgj@<;r+^#j-Bf-U5I8%$KHalLWget!p|W=VbOi$qYLT^|yYYHp&1;S{udtX$ksuWb zQ0;UvT4-JfdO;6XCZq>D&nGtU_)W??ssVxXD+DkN2OL4cN8rP_Wq>&u?B{sv^$ou^ zi+F!Ige1>~RwGZY{%OxB*?I4f=@Dd`0Y`VG(_byQbcfun4^a<{ZTkrd5+uF&AZ#fl ztR7O6ir?$yjz~ECPQUn+BYR_|YEPzIE@%4nD1Y$m;`(5x(KY|ih_as-CBEec^$tgv zUca6h$=$9CwXD*n+OTa{lkPa!X1g#;0 z07kJ(?5c}0NT$6vSS6!2H|*fqP&<|Cqo7L;&BOsbigKE6VofP{+<*+^?yi0;|yI=3;HJTYB|KH3Ep7E`6E0y2Ey{Z zmReQ?bcv$@Ocyr2vaaUZPfFF{c^dJ6fEFb*~v^P^ovy>vtK|2Ee`ej zakyVK+l^*|s}XDcX=I7gL)%YIdj=h@`cQ(mQSOwb3!yhwzeJEXgP!_WSo}JYnD@<3 zM~((=j1 zK+msPk+jgvS>pqy)V}uJ5lizTiZ05;0)35L9D#VBp?jWj4YeG;9)6T_(uvsW{%TK$c(e48aBPue_mvs~}fH#nXOz-f-&QZBQa7L0D* z+RhgUCWP%hau6jZx(blu8OYqC&g-t13Pu^aPg$tS09s`U$n!pSA{r=qG_VBqIXu2P z%`ZJ*T>Ok^BD294r$PUW2D94<}Bm@Sfxb`yUKfPi1K*EwpmOXp`$BPN0Ag&z0~ zJ)01;NDrn@t9{)nmZ>HIbU-Vf$<;62Cs1%BhNaXfOYxn`rlxe8#9ZVPZ^d6zh@qYd zOVPcb2tS$pAXE4dTYdU-A<2w@yOO%g@xAhE9t}QOkP-L(p~ecaIOwm{Eeuz>NYE+A z1Im`c%+NHYkfO7i@1pi~AtJoRw(+KMyj!rt^1Y-<#hsmy)|%O1J^F@Q%oxaAwvuun ztZr_;^wezZOK!yF{o^W)`1{)V=~|Q6-w_c=mY8#f5(KGLHO)OS=#yeh5FvG&XY3h0 zC9-xI8eykau?bqER^~r*W7; z15)KW>mTuiJu*Xo_0q&)|91<` zC~1A(Z(sCDs|UB9RpH9|)-Y7_eZM+p8Q;Tb=REC${}%QWvl&hwWc_%u=IQjz>1dG| zWpl76p6GgQ^)^*Qj!I{r`Sq~_7CiHtVq8O(_)u8yge5(OY~%C2_~)SO)|X{|{o}Yd zV-!QKc#`zd(`BE)KAfAtK2Pf9aIb*c-JYXraocN7qH=B)Ie9xjk%S)XDwN-r4o zWm%w<{HmLyh{O;LS6R`n46evMd0r(WcQW)2K3(!1qr*shfAp^!9Re^#8Eoc@ep04_ zwqs1QsYZ88+ikVI3Ec6CFa+}vC z(8{!}fhAnQGjY?4m3ssDFq=l5Yi4EX6H0DbtrNF!cF>l};Tl^RR8SFSP?Jh}71r?S z=&~LLX|fRZx&MeGOpU?rY4O#(5~=`g}T#2qsz~?HewW?*Vo||N5{kViSHYUb>rQ zX6uCIDC!Wv6jrWyVZ~p5v_6WLzr}RU&|ERc8p@(xH)=ljIVufu zZ|w>t7zMn^{eUEU<1ZC564Y&8n^Q?wlMK&(#wwaGSo|oIm87jP#nz;M^+2qC@|=^= z^_5=EUD(5WK6e)j;SN64x>ZhHZ_vM2jSoy~(ceD!Py6eU9NX)=+1q8k<`$gZ?+KX7 zs(gj{y)P#tH?E5(eqGwcwf7~QYbovHJ$$vI20LBHP~<+WQam2N{Fr$~WwOcnY@bTfcd$HeR+>{dV${~GG}(S0PEirW z;V%$HO4yFoOe4Od3&b;T{Cou*A$qy@nf3GP*`JAZ7aT{Ek?Qr^f7<5~t$ReHQ4LGS zUo@d;Dwr^@#v18(wVGpSO<0%NZk5E537Blr4l)3Zqu2MAZ zWz|J0&Fs65y0wAFjb5Zv0k;o7`WQT^_D-6!dQK8ct5+>anjf0#QHT6Wkf?5(t27z+ z$7fm+ zmX(a8`*qO7+^5p&`7iT^p6;JpRis=+5?h7y&&wZ5$%Mc@ka4WZw-(-c*<&ml)Y_so z;bN_iirye;?ZvTf+EA1zGyhdWsV(6-#xSo17gxJIs)0&4NO27r;tQtS`1l3I5fyk8 z{Do{rXeT&aS<*Qbk-VmL3jQmcSTiZGDLdO@d8E(|)T+7dm95D%Wf9!s8Mu%-oYu-cj3e><{7i$?&+?R zcA~ZTZgoRMOia7~G0MWtb0$06`)+-XfM;m;PAxcG+M0|{JSSJk9q}o*EgCbq6C90c zJQb@%_H5+ARr)eFDpR<~audGX(G=W+x$;*O;j=4J8N|O@1AbaOi5G+(XXwxjZcb78 zd4RmB-0yCLQTjARn^$!IsK1ls z>+Ih*eoRzhT3CMrR!H#Q{~#SEn~gm9cG2EfT5U_FusuqE1L-wl{)xQwUFuk_sGUVy zv!b1Wl3Apa(D+sRIag_0E8K`^_ZoE{SoWH_oDZy#2}YGD4p9cSj-LaYz+%6n_SfRf zP}S@C>Idzg{@NhWyEABD#RzkfB4qG0D1kY(25kpygu}&qg0&uOfGI*(RUk9kLLfpRFO?Tfo%i=fuOXt#c;ni!wfmW-L;G6y{_OFn7+se~Tj#k_ zsT4j%JmsZs^1#wW16^<|d&?%3VTa|_CL2L%!cghQA+3g~OOXbcxPbi{jz_Ldd2K?s zc8u#9in121*ZPJvLf?G#VLWy$;r{P?lk@y!R33&ODRW|To3sLcIZ)*KQ)s`Arwyf` z98kPUsxR|t8}?eP$2`MqmhO2}dw)>g#_>^WZbv(9PJFf{;O-P};~j-avC(lDPIA*O z7bA*y{pjPl_4atRlx2Yua?o#q=cwPK(F3KWZ0l|b5pM}~9HKk@e%b~LeDw$e(u%er?-8??bh%bGonjF9V z?shna+hxZ#K;hjASN!TdcX{WOcjMSsA*K>^2mbXGwG-0La@lleoNZ_?^?WRIlTV)$z5s>gqs?y<^{NsFYvh zhX>gN-!_y~#uhkivJLekpF&*LiN0p&)ZHOGOj`Kv2CM0CcV5o z*@~YrNxaWL=4M9JO^*!#zFYzbVeXV_gH7llVp%wA-|B$EsBAF-tAhjT z06iCFZu`d}MQQTZKANKdD`D()^M%d=E{hMfypf;3h!-R8D{SOpmX3KFFiOWor(#=V z&7u4U43yzHI2YOP=r8h~tyY1UiTboAWPzKwum(cVW8r58U(bkNr zir!SQwp9|QykH%6Sn@6JSg}|*+Kzd}y$2HKLOMOd1a!U<5efz>4*5%)fJ4zLudT0- zUbd~@K6~O-Z_7YAjBCL8oFxZfYPMO`$@fFo8BpX6r$oNrDw}z5W77PU-0)|Otq#vo z`Rm3{r~blQbe}FWOzd1QOja2h>)@iJgic^~E<(u3`g?*6>3Rha9MG4s1Ced59%wm6 zuQW+WMvxLB3zWPWcfqG&@%2`chNKphrUMq`Rc7324d)}wF2<-TfMD&J1@r<}i_$s=33+zy|>r&Mqw7`m&K{ z_7`Z)tgJFJKuzZIZ1#J)?F3L}vEp6kg7x=w{W^&C6?Vj>JRV@MxTRg-FEwD8Mz|Ny zUby%07I26PloQZm`MlI9_sZ-med*QKeCUHY8BgcNdAumuV@oU=rC>wgNEE+An6!eQ z+`U8${O!9yek)22SIi{;bF-j|QbWjryBJ%qxCsw9Bp#F(prwt@xbRM};IjC;CI`H%Z#;%S_}~H{qD&a^}aJ|86H-VjZmA1KGTbXykEpf_e*~o4L+Fe|vx` z*)H5{*GT}~f5Q#~8j0Hkuu70ZYnYrk#4H~qCn}mOiYZAMpa%yJv<0jCl`h~ zAvKJMtNuk+@J4hmi-SR9Tr|8gAP-}3YBwkIUDM#7n8s{& z&fXBXutEYi^B6}p2_lN1-rl}{O14{%iS1LCQ$>>$?t8X0-XD|f%`^1k)1UFKnM$0WXVvTPV*Xq4kXeu-Hi zaFE&@dPq6L7^^woL*i$GOy#_>?#A&Sun4N~)uR0gm=m7!?q|Udl?wXlX2%dEhRm_9 zk>Q0R=C7Z8)CFC5V(npaZN2^224AqUlOc_v`6XF>0J>#QO@0XbRlP@H)P;m>fPfKCv zC>oB^9FK$Bjb1`M2(U?kyJju=#z&m{=PoPol8`1i0;zyDeqq1?Q-=KZK$uXC=^!3z zqrIs*LeZaSRP#YI#|FG$vyB*6^f_I*uFX*FSxei8hxHHVx2(U09w^>o9eXz94+5bz z+}7$`d$J^veH!?ty$iIAi00+^B3P zvl>nsFU29{H{Gr{Vd`angk36u^OG)P!(vV8PL(4E_M_je#0l%m7>jWQbf(z-~Z2~k9 zxh0bh$m2yjao-c4tlJjX&jJduUAK8?9)vn*i{rqrk5{{;|JP`yf=b8bZyf>qRu@8MaIt>e{{0#V#Zz#Zw z%nNl92F#ADE;(%#f-qkt*n7Yx&Ax)JobpIORvP!{W#6M~bjjSSwEF#M+W_^&N)i^tcr+(oOoK^B~m%z(VPnHhJ-as#I2a!195|p4DZ~=}=(0^qC(~xA+dyx@p%Kp!R5?BRSY43OjjlaKQU8 z3piR5waNRRrNHFK`SzC=!j@U-cAGP~=Z>tM$?ZRD^52q=O_BGvxLwFz;$8XNqP;Sl zWMDZE7ukLdUrILp#g&O1x%d08QenX_h+_z+mkUJj?_!ZW^aCM;288f5!FVQPKnw_2 z3|WA!H^EO06!NNEC0c5C?26< zL;-_T`?2_leSX3Ku{CWtKIZjL;;OR3=Fty8<9vA-DfdrF_kr5W z6t?H&{+ln&C^g%bx+tZiYIBO#5~~~PulPWvG=2eb;0D)7135-#ARb`3)JFT$10)6U zABEPRM|2yruvq}2^IHcupGV+?dVl~_w&;dZ?(MwYHV$KA#vLkq;c=-uRDT>`3qEjf zxepbGJ0)nyzAt{^@xHCb!G$c1$rqPLk5m;+^+0M)9-u5g&RQ^h{2_N40f_R&C(b%>$IQhoG+ zsbD-q*pU%MlQHG@kl*nDYrYYW7-yyX7Q>3Su(45peTQnK&;SH-M1jASek?kwrTjjs zA2L!;5FuZ9u1CzY-Dfm*J}CqaDZk_E!-$$+5hx)IjP)6uZXz*&dtUe0auX#7_2HHX zVxlO&gTG!HMBp2N+6KU;w_ZTZ6G0{3ROj;1h4G;Kjr@L3EvUy1KzXX;t_U6x@zZx= z@uq3UY}*o$MN9h-mr;QICVBvU9`g@Wf)suN|Lw2^*kBiu*R(fc<2xNO?vfsucBGm{ z;8^sJu`@VLRPiAv7?5Ino*D*DID}*1Jg46a0ZPv9B|IY0h!`!o*O$2Wq~Wj00AZJh zL9y+sE^!Iy$KBTS;)>w={-6N~NTDt$pL1~FJWYuvctBP*j@?UI?5*6jg0usM0-@>x zhS^oiIMtSyT6b8&PirTAzEm_ep2)>>mK9o+eH51h?$xFRZwhqZe*E*ZwFmw>rjk`%~Xy6ec_Y`HrD)G|t3Ygln{C$||acUMg&7B5tf!t?F7DJ`R zsD+p6TPxS_?{Wzm`tf9jH)+Q)g*Yw3=RkQ>=a$MDRNkSA4F7zoJPN;pe@o`OAZ13e zB5?mE4$t;&5l*czT3d;H|6T}MBXU7Y%8=PlCo&uWWv1uZMj>X2plm)^0;o_KQf7=W z3dJUyo=pm+X5;-x8ygnLn+}`34gPe#Us@UZ_~)Mz{|A50wk^U*N+a?MYg0PS_S4zB zgnF{ft*a(+cSK3uCi@Bv)*dCKRUeeO%{~X?5ORG*AjQ6{GXF9$F75*+aqqd5%bhg< z(IVFp%uf1seo^nY*`R@WPJ-4sO#kc`?5GMEm#er!z8K7*-ppLtq4;JFK4)(l5 zjK~3DNd%6~8jQt^4@S`}`;5pJ2NAO94;h}*P9(b!YKuZG$58 z<=MMKh*73lK^%cP_55oEZv`~6)JSDIUlX{mw) z(r!mHe&3jvhr(V+o$M`;@9j9^&8Q!{fCiKy3_T#Ha~(`#(Z9(#ycg@$SHXjHj$DOQs~uN= zDnd8nLDeohu8377um-zjALu@{x^LnI^~2up&$%(pA$vij}pdJTTXguEYcs&gm#?zXf?b1_KC%DuB zcLQq12USTzww?TC8bBL{%pkQTNr;zGH6N@0D)`D-|8?+N18C9s?Lr3C;rEagH)VO2 z+vOlPzA-cB%1`HEXYPIx#d6dQBPv>p>7Ob^eX{$Kx6=+M?>u9L6aBVvuQR=IsE1F7s3LlK33vpMD z+Y9q?9C3}4coA~9$gr7Q-)}FgB2;&ZQl-scr_JL0u+(UXgi?lhKoL;F!=u zHR8&d?V*g;?a1J-Pb-<=xX;?MlCRg;@yqCRBJA2JEMMU#($PZ#~Kw*LCx(FJ0jGpDXchCGWL;fzllk(HG{ zp-Q!9-?j(uM|9hLr>DW7@BajS5ll)EPLhP)SHQl9W>iTQtf7IG4ag(F39RodW&0j} z`uOp}mj^>7)Yh++3Z6pH5e&@x6QswhgKvx469rH*x(A)97&`;5i^`IO#fUdgDkA8Q zkgn?ir4D0)S;ueBppdA&Gbjgu@3)Y%BjvY=@6;KQ8?;e(31OKiWDq9pxM&;DKHxXT zntb!>BND_v3v7DpBv`EnRH6(KzJ^tXX0drG02WrnsQ)dU76x7Pbb?8C4U{g7NPU-c zz$8Aw#P)hp!{L6$i5x)Rgr9t4g@^alJQzY^?thOD?u3=Gd_D`>NLz}~Usbx2?C^n@ zrLkUC-J3L8nY(Lw?rhSnQ$ySQ+XW#8gT60_!nMX? zB-0t+=VU8FSJ>3W=51U=9EOWzzf_JN)ymxGwWO2_CnGcV|3--*tXM&fB|29UK%^F6 z15@bDD6}tCaSuqY&SRJp;00ER`f(0PLRb7vXxJDlqrPW@C)i>cz{|V^ml8`H zO)tPMxlz#9p3qFgn8A1GE`9$A9C^#VVD!#qLxR|jXM)3{sTV_0`56X;V{zuMD`NuB z$r(eqjs|${{!qkVN3+wQ9!4wMV*s#+L28?&`Rt68s1$(I_8FAHe@=FAnfQ&{+Ep+Q z4M@rn1K$JhW4EtHzT2|l^oB88S#$ACB_G9;ldZ!%Gd=pnMf z5490iv0R)UH_#cOv^SIcBdyyE`X#9*7`p(=5j290grd5)egin)caVR#HCH{@=Q@c7 z^o2FF$E#$tS8!qu1x!sL9V-O_+juF@If)m%T4W54j-PMseyP`#+ZEah8tALhhMUKe zZ)8@kB)TF4C?@-2KnAD1(1E~wU;M-gs-$QUZMLtTiD{PkLPB=@i-AGt=~l~Eu4qp> zD;^81Wwehr+DEEE&@mev@InG&dft%-(tIAqez$=pDj=*y0rrVu>>1jER@RzqGi6|6 zN&WOfLfB(!Id8fEuj$5v2f)6WhK&T38Nx5(@t%XI*eMnsw6(t6bpzS?wXd2qNle zUC{bxIvhAur<9>u7yN{G#ML5Y6rmZl=V2k!A74E~WZ&Pq0+FwSfc%gd|clVdFBvTjNPp@sY*J*(xPm7F?BN=eb0T*CDMYlcFk zziKucUQ#e3Eglb%?6QyOjoE7To+Iz;hiultkfI@Qh2+C8dMKW0-MW}4xY5bi%zT-X zc^{00&Ej{$`NEZdW(RcW2!-LUZo}~01@Y*PVO3zkcdR@j3;dp`q6g#&6W4>p$gldx zatZSr1q?*JyMjKK1rBiL_vD2H+zv!%MJdU16t90Tr0I%(Os6Uk`He+=gEOALX?ro< zq4~kd{%T@>-X7INu)RU_AWiCXY%`OO*F!#Bl}pzTfSiK^(_|{L0eBdA@w<1P2ea~` zzu+r2QNv<|gf*OwB z|Dp$<{*;1@+v-x;A8N%(e*7JFnF00JtOD%K6SaX$?-?pm-VZDGOF}nZdRDrk4cQ$h zl)$Ymcg`c~xQD?3<8SH5o`E8!0idJYQWIbsgr-aMKB^&ARKo zPtgQ+p7?%sU+I5CF}f8A)_;BJ{Eb_YJg;t*KLD^q!M#Zk`?;X!GB$YuM>NW3OIvMI{o}IzJ zAX2?c&X2q1%{eWG6p!vk%IqA;qdYfa{uPt~-a@Jg60Zo251~#~W=YjS*s>QCdD6(f zJwKZYelfx z8KuR{#=ZaiZ=8hn0Iw>yoXpnZN({&X>eizK`E&p?9?50xSZ^&kLyc=V7N z{KlMyV08_D9+A$N2M%Ed(!F$ic{)t~50iA57;aRdcUb5QBhM*yTpDNFE{R$b!qzNB zA>}>HX|LS8*LI}w&U>=9bb#R+>mA0X)2%P0r9T+&XPQ*07chW>{v*5B)B8Z_^6av~ zrTugn2hGf$;~C`Xre>eU(Nv#L^YPM__e9NVcb`UxX6>)32)n>>?aIC*nXi~~YPiDB zlf-Ht(mN-3`F`reVrkd!3A_O@swV%zqmyj9m0FYSun@oxJqXB;(+S)~mgH`?I|={4 zYC7h7Q3<~s?hHjAF~|;cf=#Ja`_RI7lA7B;GPq}V1XKb}uXHO)A7!(6fSm7wCA{VcWMmfYIr8K?r@q5}TCc(ok;n9~I`Btl=W zgBWDG83A2!d&&{=fu;W$5WM++g$i4v$HZNe+|NR0;Mj6zKe_o)ni>MVJ*3-ClKLsn zjTK9}SDaB}wglyD7GQDGaX{XJCQz$C`sk74A0%N_<32$5DzMsr*2!L?;L@SDtytmh z8Mgs!;>8aLg52BoLk;AM)k@?Gfk!?}(~sv9rj6AWQw93hmpG-S>o_7jd&T-nr#dxJ ziT(J^k<=q#r9rRbmkMVz2L8TvQ#8jbpU2~6QJmJC4pr3tl) z!%)&tCqwx@xrz@4p#n(4T(#mwer@dq;ViDc@jror#~H6T61zc)^v#@#Y_)q%h{W%= zLJeGpoD?z{h{;4N!@R4i3wEMLRIp`?#WUap9!Pib z#iTao#%BnH5>ENC$x_*)u(NQ()8QM-@+w`fu1h%H~sl8mmr9M4TcQG99oyCVc zxyBj?!}z_>7AWi0{*YBYYdbkHwpPhf(?@Ej-SWI&Ehmz3U4#R*567q1m-}U;6x&sYnw?|vUq;`DCm*Qq zHJfRpE)gOI%2SXjzkEf=z1=dtrTJT>XDCla7D!_||L>8*RJ9Ru76^I0z4ss`)&OR3 z=EUL;My}`Qgq7L<%PjcBs2YuEl!tw3kA51d`Z=}Z+6T5jkA}Le!BI+1Lq8Rwb-Buj zEziA(PG5;uF)!)AAv+b6RnS8%^Bhn&@M9EX~P(mI{u@Q(_Ei#8^cHAO#D6vB`-@l%`CWFp3%+dqqDI#)VSvKa0w?;uBiKbP0>xQ`WWT7ZZLI_Kp>?`To zCTUfFwN^_spB&P=up*G0tL7NrK^?o;u(c1luZvvO|I;6Doji8y2T)jgYvf;}4i+2{bZ62URImmDBE_akBAtLgKC$AZ;8!-7*k0}Pjg3CwMx*qBh8 zO=$Oj-Ekf9-VEiwN+x;NyBdGMjbS5c_ZoPmt+vND;>VP03HqCqj$XLX5BU5KWWM(W zHZNDMM4Z{vxW2KK=%+)RG5JsTUfH0nI}Od*=lfbO(|ureMD}FgMwqjgqbo7IBC)eOV!80TmIdPNiQ4TNfD!rBzWB zFoO7xs)%dlzOv@`>xe2Z0uoBnlxtwY3H6bXBBCS?2wtaEVV*#{lXUej)|XS;{`+Wl zyH|Re&J8nF%XXO87g^Yz^lN659>?79qeY6r~Df7HP+_lv*HobojT23-!~nW+Z3jn)tt@~EYV_2Kk|&D zqLt$VSRaJ(SLIf8L9{`Tf9aj(6jp{V(_^vkA~Sa}^ksl?a==vV^6+hH<_i#+m39(! z_j6r9$!ez)R_{Hva^JY6*%s!%zPpaF&3_9k7m#&_HY5(?4Ec;5OwHCe^?PVliWg5-oo~-FNVe<7tAl!zo19!w|}7a!2R=&h^h=+SSrWwBas4UY!RU zq?+s6M|kzdUniJHmS!m&$2nlPs@*gkl&UpPj0(@$AY*y7DQl}!Bv|%jd2!;gTUdhO z_T77aXvTxC)Mm`FBQY3MTJ$q=oX|FkNk`|g1qYk6+sH7qgPxV}xm=i|DfdU18ako9dBW<0Li@aM*4RM3?L!`P&M?%6Y>4>toKv@kz zafqst*C04Z?k3dX$>uYrfMB^Jt`68eia@6fhfVT%?P0WP8BM)K)v6Y7koMhiI(fc0*q&O{Mvsqz}~% z{!-rg2djcYd#8=Oq@}5@Av~@c7&5v3Ge`t_EA1IP{A)EjxnO}Qffo~ zYS8(+-@f=da?O_44d(=EQGM#uTpSS}z~5oBujg=n;6*V^^K+4+F|y+{J=; zACtSne4;oOAHmsWmbT+a!TV@z<@pvjSzQQReP{6js*;B()&j~>RjEQtAshgUs`KuE zF4Z$-Ur)Ng`{c5w4l}O)YWYq72z>33IEv?f4gw@z_L)?dfixqIzQ~Ma6#9fBDiV^G z>NUqnjTj&I_$%grDsNmAdzOn7_FIWv_s(`tTdiE0ZyQ5?Ll@13jg3S;=U53o)azw1 z^Vz0$F~4lCPRwf|)kPE5k2zVN19tK%C_B(NdGXo8KunG(eZ!XF`KaE%o2l~@7Kutl zoU^)EMOys=mvj?_)+Iz)!oaVaczX=+<6$+P*8KZjSq5VBwbqA)FnBRK01DGCz1OV*cXd=Ea&nbEUr_+FAp zgL9OhOB&sK3OAqP4^vENbS<7fRI7dDlQ8|cm>>SuAPE!JJ~6tahZkm7UsK>ew6gv4 zaqM&<`LUXfU|c}X5a!a^4Byq-KB0MmsSEd(7dZz*9t~i;AA@$SDI9ePSX8gLs_POX0MxIY-k&QXZtMJvkpufuvqZBy%Dfvt7=>x0H+4pgw- z1XOR-^@}pelUVFuIQxiP1f#x)e&;vsd zz5ti-h~A8kx8S!e+mXNHV71p$Eec};T*T)&)4UQBVe$2V{so*e+E&8cCb?QtuhUtU ze%ks9XYMBSKauD5$?{XAdqYcg&$UQN_ndAS#{%$27S$t_4FgNrfQphU>ENlKlmky^ zJWj6Pbl!Hr+M=;Y_Wy1w9ZAmqg`5&T-yc6{QwSokk&)%r(;1y(sb+#xU*1JkT)l$n zE7NLQO9$BXy>TeBRw`d?_ctNNxB{?IWOwZM_&;YkY%IVA=I;oP>4)ray}4bjGVYUp5xRTM6h`h=xFwCK%*rKq=S z@22HxEqgx<|H#NtyUgcMqSWwmYq+@@7F!+7a7Rz#wG}Ahu*+QLmi^7 zl?M}=JL^vW!O;R3!1cjLo9m1SH3tL;0|D@|^DH`BXt{dmK1e~_&VpFZ{k3sIT1d9B z(%x~Y2>1~QSa5iEO|C659AI-cHyof|-JzBpCx2W*-c68^vNn72Ax51{&Y>XDvE{lcY?UX6+GV+=SR<|KA`uK z>JQ7&s6k%f;kYhpSf89d9#sL13$;_R5}4Qs%(>se=K%|AIwQ#2=!{#MlN8v!xTZ?q zn9pGq-mk&RD&29o1Mn+J(m*9cB=M3G9gt{$ACB`NzLE|dE;nGHpH&FY`n9FJSu*{2 zxxT~8D>e~lH96=h8cI?V9inh5%TpM8SIUIFCU)ZIzY9X*<=gG)O@v?Hg{2JKk%NnX z5`M)GPJ0aBPTv0SpEsV;ngGkm`Brwm^T2JqQB`I6mW8cS6>hZG`r+Ri2Jibz_hve? zC_gSa5W^5*=!;9 zEHxK*ZFq5%E28G0#P)A8n@w+(xn1K54#e~Q5$#mhVUAeq5b^F>oZl?-(+P?+GM5SG z+<*=&ZY$ke-5We&f>ekGBt~fp)b!?!rE=j)*hHhtW|wvx56#2Hi=#3&gNAn&!|y;q zC1R4*=M0iEeuQ1y zXMiy`!ep#i_~qU*eShYgP*4$uRBiVcT;wj+_A_m~mndgZ%}YJ0)a>}#Jr=9yuD@C{ zI^5~gLVr*s1N;eEl}ic(DoxtD2jIQuM@^F8b{3=Rw$&TH5wRiLj@M5xM#{n7EpS{) zH=A1@ z2>QE_BplxO@=2|9lR|cBGFJ`ybSqv6O>!5wQvMOJxO;2RIHx4sNKf(gWcvkyB zMIn{>FT1WI@vlIZKO4#iaF_enF!=lvZ})SS@Q)e8z8Qm=KUUMHWHh}x81%a+!-?!ho;mL2KKHrbq}!l`8u4PhM#+9n%Itil;Z-5FBu2rjoJ8WKs)VY_K#JVU+L?R@&MTE^Od*oDR@pleXOLYnownk#5>Njxb|-^TI_M3&NRSbmBi$>NS`aifQbbX+j!|R*X*r3vX#Bp`@bgFcOC;k_VzQkn! From b026309e36a2bfec75ecc893016de53472445af4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 10 May 2024 20:32:21 +0800 Subject: [PATCH 0763/2493] Add setting to allow hiding all country flags There have been enough requests for this at this point to implement it. --- osu.Game/Configuration/OsuConfigManager.cs | 5 ++- .../Localisation/OnlineSettingsStrings.cs | 5 +++ .../Online/AlertsAndPrivacySettings.cs | 5 +++ osu.Game/Users/Drawables/UpdateableFlag.cs | 31 +++++++++++++++---- 4 files changed, 39 insertions(+), 7 deletions(-) diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index f4a4c553d8..affcaffe02 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -200,6 +200,8 @@ namespace osu.Game.Configuration SetDefault(OsuSetting.EditorLimitedDistanceSnap, false); SetDefault(OsuSetting.EditorShowSpeedChanges, false); + SetDefault(OsuSetting.HideCountryFlags, false); + SetDefault(OsuSetting.MultiplayerRoomFilter, RoomPermissionsFilter.All); SetDefault(OsuSetting.LastProcessedMetadataId, -1); @@ -435,6 +437,7 @@ namespace osu.Game.Configuration TouchDisableGameplayTaps, ModSelectTextSearchStartsActive, UserOnlineStatus, - MultiplayerRoomFilter + MultiplayerRoomFilter, + HideCountryFlags, } } diff --git a/osu.Game/Localisation/OnlineSettingsStrings.cs b/osu.Game/Localisation/OnlineSettingsStrings.cs index 0660bac172..8e8c81cf59 100644 --- a/osu.Game/Localisation/OnlineSettingsStrings.cs +++ b/osu.Game/Localisation/OnlineSettingsStrings.cs @@ -79,6 +79,11 @@ namespace osu.Game.Localisation ///

public static LocalisableString DiscordPresenceOff => new TranslatableString(getKey(@"discord_presence_off"), @"Off"); + /// + /// "Hide country flags" + /// + public static LocalisableString HideCountryFlags => new TranslatableString(getKey(@"hide_country_flags"), @"Hide country flags"); + private static string getKey(string key) => $"{prefix}:{key}"; } } diff --git a/osu.Game/Overlays/Settings/Sections/Online/AlertsAndPrivacySettings.cs b/osu.Game/Overlays/Settings/Sections/Online/AlertsAndPrivacySettings.cs index e7b6aa56a8..7bd0829add 100644 --- a/osu.Game/Overlays/Settings/Sections/Online/AlertsAndPrivacySettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Online/AlertsAndPrivacySettings.cs @@ -28,6 +28,11 @@ namespace osu.Game.Overlays.Settings.Sections.Online LabelText = OnlineSettingsStrings.NotifyOnPrivateMessage, Current = config.GetBindable(OsuSetting.NotifyOnPrivateMessage) }, + new SettingsCheckbox + { + LabelText = OnlineSettingsStrings.HideCountryFlags, + Current = config.GetBindable(OsuSetting.HideCountryFlags) + }, }; } } diff --git a/osu.Game/Users/Drawables/UpdateableFlag.cs b/osu.Game/Users/Drawables/UpdateableFlag.cs index 8f8d7052e5..136478c7bb 100644 --- a/osu.Game/Users/Drawables/UpdateableFlag.cs +++ b/osu.Game/Users/Drawables/UpdateableFlag.cs @@ -3,9 +3,11 @@ using System; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Input.Events; +using osu.Game.Configuration; using osu.Game.Graphics.UserInterface; using osu.Game.Overlays; @@ -13,14 +15,20 @@ namespace osu.Game.Users.Drawables { public partial class UpdateableFlag : ModelBackedDrawable { + private CountryCode countryCode; + public CountryCode CountryCode { - get => Model; - set => Model = value; + get => countryCode; + set + { + countryCode = value; + updateModel(); + } } /// - /// Whether to show a place holder on unknown country. + /// Whether to show a placeholder on unknown country. /// public bool ShowPlaceholderOnUnknown = true; @@ -30,9 +38,21 @@ namespace osu.Game.Users.Drawables ///
public Action? Action; + private readonly Bindable hideFlags = new BindableBool(); + + [Resolved] + private RankingsOverlay? rankingsOverlay { get; set; } + public UpdateableFlag(CountryCode countryCode = CountryCode.Unknown) { CountryCode = countryCode; + hideFlags.BindValueChanged(_ => updateModel()); + } + + [BackgroundDependencyLoader] + private void load(OsuConfigManager config) + { + config.BindWith(OsuSetting.HideCountryFlags, hideFlags); } protected override Drawable? CreateDrawable(CountryCode countryCode) @@ -54,14 +74,13 @@ namespace osu.Game.Users.Drawables }; } - [Resolved] - private RankingsOverlay? rankingsOverlay { get; set; } - protected override bool OnClick(ClickEvent e) { Action?.Invoke(); rankingsOverlay?.ShowCountry(CountryCode); return true; } + + private void updateModel() => Model = hideFlags.Value ? CountryCode.Unknown : countryCode; } } From e887f93eca53131304d345517b798421fe363829 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 10 May 2024 22:45:59 +0800 Subject: [PATCH 0764/2493] Always show placeholder on unknown / missing country --- osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs | 1 - .../Overlays/BeatmapSet/Scores/TopScoreUserSection.cs | 1 - osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs | 1 - osu.Game/Overlays/Rankings/Tables/RankingsTable.cs | 1 - osu.Game/Users/Drawables/UpdateableFlag.cs | 8 -------- 5 files changed, 12 deletions(-) diff --git a/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs b/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs index 7a817c43eb..a6868efb5d 100644 --- a/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs +++ b/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs @@ -160,7 +160,6 @@ namespace osu.Game.Overlays.BeatmapSet.Scores new UpdateableFlag(score.User.CountryCode) { Size = new Vector2(19, 14), - ShowPlaceholderOnUnknown = false, }, username, #pragma warning disable 618 diff --git a/osu.Game/Overlays/BeatmapSet/Scores/TopScoreUserSection.cs b/osu.Game/Overlays/BeatmapSet/Scores/TopScoreUserSection.cs index 9dc2ce204f..13ba9fb74b 100644 --- a/osu.Game/Overlays/BeatmapSet/Scores/TopScoreUserSection.cs +++ b/osu.Game/Overlays/BeatmapSet/Scores/TopScoreUserSection.cs @@ -118,7 +118,6 @@ namespace osu.Game.Overlays.BeatmapSet.Scores Origin = Anchor.CentreLeft, Size = new Vector2(19, 14), Margin = new MarginPadding { Top = 3 }, // makes spacing look more even - ShowPlaceholderOnUnknown = false, }, } } diff --git a/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs b/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs index c9e5068b2a..165a576c03 100644 --- a/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs +++ b/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs @@ -165,7 +165,6 @@ namespace osu.Game.Overlays.Profile.Header userFlag = new UpdateableFlag { Size = new Vector2(28, 20), - ShowPlaceholderOnUnknown = false, }, userCountryContainer = new OsuHoverContainer { diff --git a/osu.Game/Overlays/Rankings/Tables/RankingsTable.cs b/osu.Game/Overlays/Rankings/Tables/RankingsTable.cs index 27d894cdc2..b9f7e443ca 100644 --- a/osu.Game/Overlays/Rankings/Tables/RankingsTable.cs +++ b/osu.Game/Overlays/Rankings/Tables/RankingsTable.cs @@ -99,7 +99,6 @@ namespace osu.Game.Overlays.Rankings.Tables new UpdateableFlag(GetCountryCode(item)) { Size = new Vector2(28, 20), - ShowPlaceholderOnUnknown = false, }, CreateFlagContent(item) } diff --git a/osu.Game/Users/Drawables/UpdateableFlag.cs b/osu.Game/Users/Drawables/UpdateableFlag.cs index 136478c7bb..ac52599bc9 100644 --- a/osu.Game/Users/Drawables/UpdateableFlag.cs +++ b/osu.Game/Users/Drawables/UpdateableFlag.cs @@ -27,11 +27,6 @@ namespace osu.Game.Users.Drawables } } - /// - /// Whether to show a placeholder on unknown country. - /// - public bool ShowPlaceholderOnUnknown = true; - /// /// Perform an action in addition to showing the country ranking. /// This should be used to perform auxiliary tasks and not as a primary action for clicking a flag (to maintain a consistent UX). @@ -57,9 +52,6 @@ namespace osu.Game.Users.Drawables protected override Drawable? CreateDrawable(CountryCode countryCode) { - if (countryCode == CountryCode.Unknown && !ShowPlaceholderOnUnknown) - return null; - return new Container { RelativeSizeAxes = Axes.Both, From ea35ad46899b3a11ed026377a351f6f3d46593de Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 10 May 2024 23:26:43 +0800 Subject: [PATCH 0765/2493] Fix nullability inspection --- osu.Game/Users/Drawables/UpdateableFlag.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Users/Drawables/UpdateableFlag.cs b/osu.Game/Users/Drawables/UpdateableFlag.cs index ac52599bc9..6a587212a3 100644 --- a/osu.Game/Users/Drawables/UpdateableFlag.cs +++ b/osu.Game/Users/Drawables/UpdateableFlag.cs @@ -50,7 +50,7 @@ namespace osu.Game.Users.Drawables config.BindWith(OsuSetting.HideCountryFlags, hideFlags); } - protected override Drawable? CreateDrawable(CountryCode countryCode) + protected override Drawable CreateDrawable(CountryCode countryCode) { return new Container { From d0c8b55a0a67723d58c47ffa20cc8985337da106 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Sun, 5 May 2024 22:26:00 -0700 Subject: [PATCH 0766/2493] Fix fluidity desync by not autosizing right (total score) content --- osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs b/osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs index b4f6379f06..a2331f1fdc 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs @@ -41,16 +41,16 @@ namespace osu.Game.Online.Leaderboards public partial class LeaderboardScoreV2 : OsuClickableContainer, IHasContextMenu, IHasCustomTooltip { /// - /// The maximum number of mods when contracted until the mods display width exceeds the . + /// The maximum number of mods when contracted until the mods display width exceeds the . /// public const int MAX_MODS_CONTRACTED = 13; /// - /// The maximum number of mods when expanded until the mods display width exceeds the . + /// The maximum number of mods when expanded until the mods display width exceeds the . /// public const int MAX_MODS_EXPANDED = 4; - private const float right_content_min_width = 180; + private const float right_content_width = 180; private const float grade_width = 40; private const float username_min_width = 100; @@ -153,7 +153,7 @@ namespace osu.Game.Online.Leaderboards { new Dimension(GridSizeMode.Absolute, 65), new Dimension(), - new Dimension(GridSizeMode.AutoSize, minSize: right_content_min_width), // use min size to account for classic scoring + new Dimension(GridSizeMode.Absolute, right_content_width), }, Content = new[] { From 8a474f7d2231706819056030f3db268b4321c5dd Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Sun, 5 May 2024 22:27:21 -0700 Subject: [PATCH 0767/2493] Fix broken avatar masking when hiding --- osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs b/osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs index a2331f1fdc..bf5dc3572d 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs @@ -212,7 +212,9 @@ namespace osu.Game.Online.Leaderboards new Container { AutoSizeAxes = Axes.Both, - Child = avatar = new MaskedWrapper( + CornerRadius = corner_radius, + Masking = true, + Child = avatar = new DelayedLoadWrapper( innerAvatar = new ClickableAvatar(user) { Anchor = Anchor.Centre, @@ -592,16 +594,6 @@ namespace osu.Game.Online.Leaderboards public LocalisableString TooltipText { get; } } - private partial class MaskedWrapper : DelayedLoadWrapper - { - public MaskedWrapper(Drawable content, double timeBeforeLoad = 500) - : base(content, timeBeforeLoad) - { - CornerRadius = corner_radius; - Masking = true; - } - } - private sealed partial class ColouredModSwitchTiny : ModSwitchTiny, IHasTooltip { private readonly IMod mod; From e8967ff3c572a2f9ef62f37fa622d85cadbf2f7c Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Thu, 9 May 2024 22:39:41 -0700 Subject: [PATCH 0768/2493] Lower username font size a bit --- osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs b/osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs index bf5dc3572d..dea134b4d6 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs @@ -266,7 +266,7 @@ namespace osu.Game.Online.Leaderboards RelativeSizeAxes = Axes.X, Shear = -shear, Text = user.Username, - Font = OsuFont.GetFont(size: 24, weight: FontWeight.SemiBold) + Font = OsuFont.GetFont(size: 20, weight: FontWeight.SemiBold) } } }, From f64cf5c037f839da3ee3d2a6e686bab1c1bcad0a Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 11 May 2024 09:25:07 +0300 Subject: [PATCH 0769/2493] Fix button extending beyond unranked indicator width --- osu.Game/Screens/Select/FooterV2/FooterButtonModsV2.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Select/FooterV2/FooterButtonModsV2.cs b/osu.Game/Screens/Select/FooterV2/FooterButtonModsV2.cs index 08b0407a79..2ceddedeb5 100644 --- a/osu.Game/Screens/Select/FooterV2/FooterButtonModsV2.cs +++ b/osu.Game/Screens/Select/FooterV2/FooterButtonModsV2.cs @@ -73,7 +73,8 @@ namespace osu.Game.Screens.Select.FooterV2 { UnrankedBadge = new ContainerWithTooltip { - Position = new Vector2(BUTTON_WIDTH + 5f, -5f), + Margin = new MarginPadding { Left = BUTTON_WIDTH + 5f }, + Y = -5f, Depth = float.MaxValue, Origin = Anchor.BottomLeft, Shear = barShear, @@ -233,14 +234,14 @@ namespace osu.Game.Screens.Select.FooterV2 if (Current.Value.Any(m => !m.Ranked)) { - UnrankedBadge.MoveToX(BUTTON_WIDTH + 5, duration, easing); + UnrankedBadge.MoveToX(0, duration, easing); UnrankedBadge.FadeIn(duration, easing); - this.ResizeWidthTo(BUTTON_WIDTH + UnrankedBadge.DrawWidth + 10, duration, easing); + this.ResizeWidthTo(BUTTON_WIDTH + 5 + UnrankedBadge.DrawWidth, duration, easing); } else { - UnrankedBadge.MoveToX(BUTTON_WIDTH + 5 - UnrankedBadge.DrawWidth, duration, easing); + UnrankedBadge.MoveToX(-UnrankedBadge.DrawWidth, duration, easing); UnrankedBadge.FadeOut(duration, easing); this.ResizeWidthTo(BUTTON_WIDTH, duration, easing); From da096376af7b84ec7c6590e976a1505ab33a394a Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 11 May 2024 09:31:32 +0300 Subject: [PATCH 0770/2493] Fix DI error in mod tooltip --- .../Screens/Select/FooterV2/FooterButtonModsV2.cs | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Select/FooterV2/FooterButtonModsV2.cs b/osu.Game/Screens/Select/FooterV2/FooterButtonModsV2.cs index 2ceddedeb5..02eb2028c5 100644 --- a/osu.Game/Screens/Select/FooterV2/FooterButtonModsV2.cs +++ b/osu.Game/Screens/Select/FooterV2/FooterButtonModsV2.cs @@ -267,13 +267,16 @@ namespace osu.Game.Screens.Select.FooterV2 { public readonly Bindable> Mods = new Bindable>(); + [Resolved] + private OverlayColourProvider colourProvider { get; set; } = null!; + protected override void LoadComplete() { base.LoadComplete(); Mods.BindValueChanged(v => Text = FooterButtonModsV2Strings.Mods(v.NewValue.Count).ToUpper(), true); } - public ITooltip> GetCustomTooltip() => new ModTooltip(); + public ITooltip> GetCustomTooltip() => new ModTooltip(colourProvider); public IReadOnlyList? TooltipContent => Mods.Value; @@ -281,8 +284,16 @@ namespace osu.Game.Screens.Select.FooterV2 { private ModDisplay extendedModDisplay = null!; + [Cached] + private OverlayColourProvider colourProvider; + + public ModTooltip(OverlayColourProvider colourProvider) + { + this.colourProvider = colourProvider; + } + [BackgroundDependencyLoader] - private void load(OverlayColourProvider colourProvider) + private void load() { AutoSizeAxes = Axes.Both; CornerRadius = CORNER_RADIUS; From ac7598cb6868764478daa50b195e4a39aef7da91 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 11 May 2024 20:25:47 +0800 Subject: [PATCH 0771/2493] Move localisation to existing file to avoid silly new class --- .../Localisation/FooterButtonModsV2Strings.cs | 19 ------------------- .../Localisation/ModSelectOverlayStrings.cs | 8 +++++++- .../Select/FooterV2/FooterButtonModsV2.cs | 2 +- 3 files changed, 8 insertions(+), 21 deletions(-) delete mode 100644 osu.Game/Localisation/FooterButtonModsV2Strings.cs diff --git a/osu.Game/Localisation/FooterButtonModsV2Strings.cs b/osu.Game/Localisation/FooterButtonModsV2Strings.cs deleted file mode 100644 index 2cb297d8ef..0000000000 --- a/osu.Game/Localisation/FooterButtonModsV2Strings.cs +++ /dev/null @@ -1,19 +0,0 @@ -// 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.Localisation -{ - public static class FooterButtonModsV2Strings - { - private const string prefix = @"osu.Game.Resources.Localisation.FooterButtonModsV2"; - - /// - /// "{0} mods" - /// - public static LocalisableString Mods(int count) => new TranslatableString(getKey(@"mods"), @"{0} mods", count); - - private static string getKey(string key) => $@"{prefix}:{key}"; - } -} diff --git a/osu.Game/Localisation/ModSelectOverlayStrings.cs b/osu.Game/Localisation/ModSelectOverlayStrings.cs index 7a9bb698d8..cf01081772 100644 --- a/osu.Game/Localisation/ModSelectOverlayStrings.cs +++ b/osu.Game/Localisation/ModSelectOverlayStrings.cs @@ -14,10 +14,16 @@ namespace osu.Game.Localisation /// public static LocalisableString ModSelectTitle => new TranslatableString(getKey(@"mod_select_title"), @"Mod Select"); + /// + /// "{0} mods" + /// + public static LocalisableString Mods(int count) => new TranslatableString(getKey(@"mods"), @"{0} mods", count); + /// /// "Mods provide different ways to enjoy gameplay. Some have an effect on the score you can achieve during ranked play. Others are just for fun." /// - public static LocalisableString ModSelectDescription => new TranslatableString(getKey(@"mod_select_description"), @"Mods provide different ways to enjoy gameplay. Some have an effect on the score you can achieve during ranked play. Others are just for fun."); + public static LocalisableString ModSelectDescription => new TranslatableString(getKey(@"mod_select_description"), + @"Mods provide different ways to enjoy gameplay. Some have an effect on the score you can achieve during ranked play. Others are just for fun."); /// /// "Mod Customisation" diff --git a/osu.Game/Screens/Select/FooterV2/FooterButtonModsV2.cs b/osu.Game/Screens/Select/FooterV2/FooterButtonModsV2.cs index 02eb2028c5..d0351ac348 100644 --- a/osu.Game/Screens/Select/FooterV2/FooterButtonModsV2.cs +++ b/osu.Game/Screens/Select/FooterV2/FooterButtonModsV2.cs @@ -273,7 +273,7 @@ namespace osu.Game.Screens.Select.FooterV2 protected override void LoadComplete() { base.LoadComplete(); - Mods.BindValueChanged(v => Text = FooterButtonModsV2Strings.Mods(v.NewValue.Count).ToUpper(), true); + Mods.BindValueChanged(v => Text = ModSelectOverlayStrings.Mods(v.NewValue.Count).ToUpper(), true); } public ITooltip> GetCustomTooltip() => new ModTooltip(colourProvider); From a988bbd3cb6125c4f300f3d092d32b5e962365dc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 11 May 2024 20:58:51 +0800 Subject: [PATCH 0772/2493] Tidy up `UnrankedBadge` implementation --- .../TestSceneFooterButtonModsV2.cs | 12 +- .../Select/FooterV2/FooterButtonModsV2.cs | 116 +++++++++--------- 2 files changed, 65 insertions(+), 63 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneFooterButtonModsV2.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneFooterButtonModsV2.cs index af2eea6062..4aca9dde3d 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneFooterButtonModsV2.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneFooterButtonModsV2.cs @@ -7,7 +7,7 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; +using osu.Framework.Testing; using osu.Game.Graphics.Sprites; using osu.Game.Overlays; using osu.Game.Rulesets.Mods; @@ -97,15 +97,12 @@ namespace osu.Game.Tests.Visual.UserInterface public void TestUnrankedBadge() { AddStep(@"Add unranked mod", () => changeMods(new[] { new OsuModDeflate() })); - AddUntilStep("Unranked badge shown", () => footerButtonMods.UnrankedBadge.Alpha == 1); + AddUntilStep("Unranked badge shown", () => footerButtonMods.ChildrenOfType().Single().Alpha == 1); AddStep(@"Clear selected mod", () => changeMods(Array.Empty())); - AddUntilStep("Unranked badge not shown", () => footerButtonMods.UnrankedBadge.Alpha == 0); + AddUntilStep("Unranked badge not shown", () => footerButtonMods.ChildrenOfType().Single().Alpha == 0); } - private void changeMods(IReadOnlyList mods) - { - footerButtonMods.Current.Value = mods; - } + private void changeMods(IReadOnlyList mods) => footerButtonMods.Current.Value = mods; private bool assertModsMultiplier(IEnumerable mods) { @@ -117,7 +114,6 @@ namespace osu.Game.Tests.Visual.UserInterface private partial class TestFooterButtonModsV2 : FooterButtonModsV2 { - public new Container UnrankedBadge => base.UnrankedBadge; public new OsuSpriteText MultiplierText => base.MultiplierText; } } diff --git a/osu.Game/Screens/Select/FooterV2/FooterButtonModsV2.cs b/osu.Game/Screens/Select/FooterV2/FooterButtonModsV2.cs index d0351ac348..44db49b927 100644 --- a/osu.Game/Screens/Select/FooterV2/FooterButtonModsV2.cs +++ b/osu.Game/Screens/Select/FooterV2/FooterButtonModsV2.cs @@ -32,6 +32,11 @@ namespace osu.Game.Screens.Select.FooterV2 { // todo: see https://github.com/ppy/osu-framework/issues/3271 private const float torus_scale_factor = 1.2f; + private const float bar_shear_width = 7f; + private const float bar_height = 37f; + private const float mod_display_portion = 0.65f; + + private static readonly Vector2 bar_shear = new Vector2(bar_shear_width / bar_height, 0); private readonly BindableWithCurrent> current = new BindableWithCurrent>(Array.Empty()); @@ -43,7 +48,7 @@ namespace osu.Game.Screens.Select.FooterV2 private Container modDisplayBar = null!; - protected Container UnrankedBadge { get; private set; } = null!; + private Drawable unrankedBadge = null!; private ModDisplay modDisplay = null!; private OsuSpriteText modCountText = null!; @@ -59,58 +64,19 @@ namespace osu.Game.Screens.Select.FooterV2 [BackgroundDependencyLoader] private void load() { - const float bar_shear_width = 7f; - const float bar_height = 37f; - const float mod_display_portion = 0.65f; - - var barShear = new Vector2(bar_shear_width / bar_height, 0); - Text = "Mods"; Icon = FontAwesome.Solid.ExchangeAlt; AccentColour = colours.Lime1; AddRange(new[] { - UnrankedBadge = new ContainerWithTooltip - { - Margin = new MarginPadding { Left = BUTTON_WIDTH + 5f }, - Y = -5f, - Depth = float.MaxValue, - Origin = Anchor.BottomLeft, - Shear = barShear, - CornerRadius = CORNER_RADIUS, - AutoSizeAxes = Axes.X, - Height = bar_height, - Masking = true, - BorderColour = Color4.White, - BorderThickness = 2f, - TooltipText = ModSelectOverlayStrings.UnrankedExplanation, - Children = new Drawable[] - { - new Box - { - Colour = colours.Orange2, - RelativeSizeAxes = Axes.Both, - }, - new OsuSpriteText - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Shear = -barShear, - Text = ModSelectOverlayStrings.Unranked.ToUpper(), - Margin = new MarginPadding { Horizontal = 15 }, - UseFullGlyphHeight = false, - Font = OsuFont.Torus.With(size: 14 * torus_scale_factor, weight: FontWeight.Bold), - Colour = Color4.Black, - } - } - }, + unrankedBadge = new UnrankedBadge(), modDisplayBar = new Container { Y = -5f, Depth = float.MaxValue, Origin = Anchor.BottomLeft, - Shear = barShear, + Shear = bar_shear, CornerRadius = CORNER_RADIUS, Size = new Vector2(BUTTON_WIDTH, bar_height), Masking = true, @@ -140,7 +106,7 @@ namespace osu.Game.Screens.Select.FooterV2 { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Shear = -barShear, + Shear = -bar_shear, UseFullGlyphHeight = false, Font = OsuFont.Torus.With(size: 14 * torus_scale_factor, weight: FontWeight.Bold) } @@ -162,7 +128,7 @@ namespace osu.Game.Screens.Select.FooterV2 { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Shear = -barShear, + Shear = -bar_shear, Scale = new Vector2(0.6f), Current = { BindTarget = Current }, ExpansionMode = ExpansionMode.AlwaysContracted, @@ -171,7 +137,7 @@ namespace osu.Game.Screens.Select.FooterV2 { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Shear = -barShear, + Shear = -bar_shear, Font = OsuFont.Torus.With(size: 14 * torus_scale_factor, weight: FontWeight.Bold), Mods = { BindTarget = Current }, } @@ -216,8 +182,8 @@ namespace osu.Game.Screens.Select.FooterV2 modDisplay.FadeOut(duration, easing); modCountText.FadeOut(duration, easing); - UnrankedBadge.MoveToY(20, duration, easing); - UnrankedBadge.FadeOut(duration, easing); + unrankedBadge.MoveToY(20, duration, easing); + unrankedBadge.FadeOut(duration, easing); // add delay to let unranked indicator hide first before resizing the button back to its original width. this.Delay(duration).ResizeWidthTo(BUTTON_WIDTH, duration, easing); @@ -234,21 +200,21 @@ namespace osu.Game.Screens.Select.FooterV2 if (Current.Value.Any(m => !m.Ranked)) { - UnrankedBadge.MoveToX(0, duration, easing); - UnrankedBadge.FadeIn(duration, easing); + unrankedBadge.MoveToX(0, duration, easing); + unrankedBadge.FadeIn(duration, easing); - this.ResizeWidthTo(BUTTON_WIDTH + 5 + UnrankedBadge.DrawWidth, duration, easing); + this.ResizeWidthTo(BUTTON_WIDTH + 5 + unrankedBadge.DrawWidth, duration, easing); } else { - UnrankedBadge.MoveToX(-UnrankedBadge.DrawWidth, duration, easing); - UnrankedBadge.FadeOut(duration, easing); + unrankedBadge.MoveToX(-unrankedBadge.DrawWidth, duration, easing); + unrankedBadge.FadeOut(duration, easing); this.ResizeWidthTo(BUTTON_WIDTH, duration, easing); } modDisplayBar.MoveToY(-5, duration, Easing.OutQuint); - UnrankedBadge.MoveToY(-5, duration, easing); + unrankedBadge.MoveToY(-5, duration, easing); modDisplayBar.FadeIn(duration, easing); } @@ -327,9 +293,49 @@ namespace osu.Game.Screens.Select.FooterV2 } } - private partial class ContainerWithTooltip : Container, IHasTooltip + internal partial class UnrankedBadge : CompositeDrawable, IHasTooltip { - public LocalisableString TooltipText { get; set; } + public LocalisableString TooltipText { get; } + + public UnrankedBadge() + { + Margin = new MarginPadding { Left = BUTTON_WIDTH + 5f }; + Y = -5f; + Depth = float.MaxValue; + Origin = Anchor.BottomLeft; + Shear = bar_shear; + CornerRadius = CORNER_RADIUS; + AutoSizeAxes = Axes.X; + Height = bar_height; + Masking = true; + BorderColour = Color4.White; + BorderThickness = 2f; + TooltipText = ModSelectOverlayStrings.UnrankedExplanation; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + InternalChildren = new Drawable[] + { + new Box + { + Colour = colours.Orange2, + RelativeSizeAxes = Axes.Both, + }, + new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Shear = -bar_shear, + Text = ModSelectOverlayStrings.Unranked.ToUpper(), + Margin = new MarginPadding { Horizontal = 15 }, + UseFullGlyphHeight = false, + Font = OsuFont.Torus.With(size: 14 * torus_scale_factor, weight: FontWeight.Bold), + Colour = Color4.Black, + } + }; + } } } } From 4cf6ab40f6534dc0a8f479b3d561b5a3bcd8b403 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 11 May 2024 22:33:18 +0300 Subject: [PATCH 0773/2493] Use `MustDisposeResource` annotation to appease ReSharper inspections --- osu.Game/Database/EmptyRealmSet.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/osu.Game/Database/EmptyRealmSet.cs b/osu.Game/Database/EmptyRealmSet.cs index 02dfa50fe5..c34974cb03 100644 --- a/osu.Game/Database/EmptyRealmSet.cs +++ b/osu.Game/Database/EmptyRealmSet.cs @@ -6,6 +6,7 @@ using System.Collections; using System.Collections.Generic; using System.Collections.Specialized; using System.ComponentModel; +using JetBrains.Annotations; using Realms; using Realms.Schema; @@ -15,8 +16,12 @@ namespace osu.Game.Database { private IList emptySet => Array.Empty(); + [MustDisposeResource] public IEnumerator GetEnumerator() => emptySet.GetEnumerator(); + + [MustDisposeResource] IEnumerator IEnumerable.GetEnumerator() => emptySet.GetEnumerator(); + public int Count => emptySet.Count; public T this[int index] => emptySet[index]; public int IndexOf(object? item) => item == null ? -1 : emptySet.IndexOf((T)item); From 5e8f6df79952b26793c4bca8b74cfcdf1c0a90a3 Mon Sep 17 00:00:00 2001 From: Thomas Mok <42684333+tomm13@users.noreply.github.com> Date: Sun, 12 May 2024 01:55:18 +0100 Subject: [PATCH 0774/2493] Add glow to footerV2 buttons --- osu.Game/Screens/Select/FooterV2/FooterButtonV2.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/osu.Game/Screens/Select/FooterV2/FooterButtonV2.cs b/osu.Game/Screens/Select/FooterV2/FooterButtonV2.cs index 2c841f6ae6..d856780dfe 100644 --- a/osu.Game/Screens/Select/FooterV2/FooterButtonV2.cs +++ b/osu.Game/Screens/Select/FooterV2/FooterButtonV2.cs @@ -5,6 +5,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Shapes; @@ -68,6 +69,7 @@ namespace osu.Game.Screens.Select.FooterV2 protected Container TextContainer; private readonly Box bar; private readonly Box backgroundBox; + private readonly Box glowBox; public FooterButtonV2() { @@ -93,6 +95,10 @@ namespace osu.Game.Screens.Select.FooterV2 { RelativeSizeAxes = Axes.Both }, + glowBox = new Box + { + RelativeSizeAxes = Axes.Both + }, // For elements that should not be sheared. new Container { @@ -211,6 +217,7 @@ namespace osu.Game.Screens.Select.FooterV2 } backgroundBox.FadeColour(backgroundColour, transition_length, Easing.OutQuint); + glowBox.Colour = ColourInfo.GradientVertical(buttonAccentColour.Opacity(0f), buttonAccentColour.Opacity(0.2f)); } } } From fa6ccc854d5324d08baf4e6ee77ce7373d75ef0a Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sun, 12 May 2024 05:24:20 +0300 Subject: [PATCH 0775/2493] Revert behavioural changes on options button --- .../Select/FooterV2/BeatmapOptionsPopover.cs | 4 +-- .../Select/FooterV2/FooterButtonOptionsV2.cs | 31 +------------------ 2 files changed, 2 insertions(+), 33 deletions(-) diff --git a/osu.Game/Screens/Select/FooterV2/BeatmapOptionsPopover.cs b/osu.Game/Screens/Select/FooterV2/BeatmapOptionsPopover.cs index 648f536bb1..d98164c306 100644 --- a/osu.Game/Screens/Select/FooterV2/BeatmapOptionsPopover.cs +++ b/osu.Game/Screens/Select/FooterV2/BeatmapOptionsPopover.cs @@ -188,9 +188,7 @@ namespace osu.Game.Screens.Select.FooterV2 protected override void UpdateState(ValueChangedEvent state) { base.UpdateState(state); - // intentionally scheduling to let the button have a chance whether the popover will hide from clicking the button or clicking outside - // see the "hidingFromClick" field in FooterButtonOptionsV2. - Schedule(() => footerButton.OverlayState.Value = state.NewValue); + footerButton.OverlayState.Value = state.NewValue; } } } diff --git a/osu.Game/Screens/Select/FooterV2/FooterButtonOptionsV2.cs b/osu.Game/Screens/Select/FooterV2/FooterButtonOptionsV2.cs index 2ed8480b46..555215056a 100644 --- a/osu.Game/Screens/Select/FooterV2/FooterButtonOptionsV2.cs +++ b/osu.Game/Screens/Select/FooterV2/FooterButtonOptionsV2.cs @@ -3,11 +3,9 @@ using osu.Framework.Allocation; using osu.Framework.Extensions; -using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.UserInterface; -using osu.Framework.Input.Events; using osu.Game.Graphics; using osu.Game.Input.Bindings; @@ -15,11 +13,6 @@ namespace osu.Game.Screens.Select.FooterV2 { public partial class FooterButtonOptionsV2 : FooterButtonV2, IHasPopover { - /// - /// True if the next click is for hiding the popover. - /// - private bool hidingFromClick; - [BackgroundDependencyLoader] private void load(OsuColour colour) { @@ -28,29 +21,7 @@ namespace osu.Game.Screens.Select.FooterV2 AccentColour = colour.Purple1; Hotkey = GlobalAction.ToggleBeatmapOptions; - Action = () => - { - if (OverlayState.Value == Visibility.Hidden && !hidingFromClick) - this.ShowPopover(); - - hidingFromClick = false; - }; - } - - protected override bool OnMouseDown(MouseDownEvent e) - { - if (OverlayState.Value == Visibility.Visible) - hidingFromClick = true; - - return base.OnMouseDown(e); - } - - protected override void Flash() - { - if (hidingFromClick) - return; - - base.Flash(); + Action = this.ShowPopover; } public Popover GetPopover() => new BeatmapOptionsPopover(this); From 736e15ab26e10134b29ccf773505f8964161d816 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Sun, 12 May 2024 22:21:50 -0700 Subject: [PATCH 0776/2493] Improve fluidity states --- .../Online/Leaderboards/LeaderboardScoreV2.cs | 116 ++++++++++++------ 1 file changed, 81 insertions(+), 35 deletions(-) diff --git a/osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs b/osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs index dea134b4d6..d0a264a7e3 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs @@ -52,7 +52,11 @@ namespace osu.Game.Online.Leaderboards private const float right_content_width = 180; private const float grade_width = 40; - private const float username_min_width = 100; + private const float username_min_width = 125; + private const float statistics_regular_min_width = 175; + private const float statistics_compact_min_width = 100; + private const float rank_label_width = 65; + private const float rank_label_visibility_width_cutoff = rank_label_width + height + username_min_width + statistics_regular_min_width + right_content_width; private readonly ScoreInfo score; @@ -101,9 +105,9 @@ namespace osu.Game.Online.Leaderboards private Drawable scoreRank = null!; private Box totalScoreBackground = null!; - private Container centreContent = null!; - private FillFlowContainer usernameAndFlagContainer = null!; private FillFlowContainer statisticsContainer = null!; + private RankLabel rankLabel = null!; + private Container rankLabelOverlay = null!; public ITooltip GetCustomTooltip() => new LeaderboardScoreTooltip(); public virtual ScoreInfo TooltipContent => score; @@ -151,7 +155,7 @@ namespace osu.Game.Online.Leaderboards RelativeSizeAxes = Axes.Both, ColumnDimensions = new[] { - new Dimension(GridSizeMode.Absolute, 65), + new Dimension(GridSizeMode.AutoSize), new Dimension(), new Dimension(GridSizeMode.Absolute, right_content_width), }, @@ -159,8 +163,17 @@ namespace osu.Game.Online.Leaderboards { new Drawable[] { - new RankLabel(rank) { Shear = -shear }, - centreContent = createCentreContent(user), + new Container + { + AutoSizeAxes = Axes.X, + RelativeSizeAxes = Axes.Y, + Child = rankLabel = new RankLabel(rank) + { + Width = rank_label_width, + RelativeSizeAxes = Axes.Y, + }, + }, + createCentreContent(user), createRightContent() } } @@ -214,21 +227,43 @@ namespace osu.Game.Online.Leaderboards AutoSizeAxes = Axes.Both, CornerRadius = corner_radius, Masking = true, - Child = avatar = new DelayedLoadWrapper( - innerAvatar = new ClickableAvatar(user) - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Scale = new Vector2(1.1f), - Shear = -shear, - RelativeSizeAxes = Axes.Both, - }) + Children = new[] { - RelativeSizeAxes = Axes.None, - Size = new Vector2(height) + avatar = new DelayedLoadWrapper( + innerAvatar = new ClickableAvatar(user) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Scale = new Vector2(1.1f), + Shear = -shear, + RelativeSizeAxes = Axes.Both, + }) + { + RelativeSizeAxes = Axes.None, + Size = new Vector2(height) + }, + rankLabelOverlay = new Container + { + RelativeSizeAxes = Axes.Both, + Alpha = 0, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Colour4.Black.Opacity(0.5f), + }, + new RankLabel(rank) + { + AutoSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }, + } + } }, }, - usernameAndFlagContainer = new FillFlowContainer + new FillFlowContainer { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, @@ -279,7 +314,7 @@ namespace osu.Game.Online.Leaderboards { Name = @"Statistics container", Padding = new MarginPadding { Right = 40 }, - Spacing = new Vector2(25), + Spacing = new Vector2(25, 0), Shear = -shear, Anchor = Anchor.CentreRight, Origin = Anchor.CentreRight, @@ -287,6 +322,8 @@ namespace osu.Game.Online.Leaderboards Direction = FillDirection.Horizontal, Children = statisticsLabels, Alpha = 0, + LayoutEasing = Easing.OutQuint, + LayoutDuration = transition_duration, } } } @@ -483,6 +520,11 @@ namespace osu.Game.Online.Leaderboards foreground.FadeColour(IsHovered ? foregroundColour.Lighten(0.2f) : foregroundColour, transition_duration, Easing.OutQuint); background.FadeColour(IsHovered ? backgroundColour.Lighten(0.2f) : backgroundColour, transition_duration, Easing.OutQuint); totalScoreBackground.FadeColour(IsHovered ? lightenedGradient : totalScoreBackgroundGradient, transition_duration, Easing.OutQuint); + + if (DrawWidth < rank_label_visibility_width_cutoff && IsHovered) + rankLabelOverlay.FadeIn(transition_duration, Easing.OutQuint); + else + rankLabelOverlay.FadeOut(transition_duration, Easing.OutQuint); } protected override bool OnInvalidate(Invalidation invalidation, InvalidationSource source) @@ -490,22 +532,27 @@ namespace osu.Game.Online.Leaderboards Scheduler.AddOnce(() => { // when width decreases - // - hide statistics, then - // - hide avatar, then - // - hide user and flag and show avatar again + // - hide rank and show rank overlay on avatar when hovered, then + // - compact statistics, then + // - hide statistics - if (centreContent.DrawWidth >= height + username_min_width || centreContent.DrawWidth < username_min_width) - avatar.FadeIn(transition_duration, Easing.OutQuint).MoveToX(0, transition_duration, Easing.OutQuint); + if (DrawWidth >= rank_label_visibility_width_cutoff) + rankLabel.FadeIn(transition_duration, Easing.OutQuint).MoveToX(0, transition_duration, Easing.OutQuint); else - avatar.FadeOut(transition_duration, Easing.OutQuint).MoveToX(-avatar.DrawWidth, transition_duration, Easing.OutQuint); + rankLabel.FadeOut(transition_duration, Easing.OutQuint).MoveToX(-rankLabel.DrawWidth, transition_duration, Easing.OutQuint); - if (centreContent.DrawWidth >= username_min_width) - usernameAndFlagContainer.FadeIn(transition_duration, Easing.OutQuint).MoveToX(0, transition_duration, Easing.OutQuint); - else - usernameAndFlagContainer.FadeOut(transition_duration, Easing.OutQuint).MoveToX(usernameAndFlagContainer.DrawWidth, transition_duration, Easing.OutQuint); - - if (centreContent.DrawWidth >= height + statisticsContainer.DrawWidth + username_min_width) + if (DrawWidth >= height + username_min_width + statistics_regular_min_width + right_content_width) + { statisticsContainer.FadeIn(transition_duration, Easing.OutQuint).MoveToX(0, transition_duration, Easing.OutQuint); + statisticsContainer.Direction = FillDirection.Horizontal; + statisticsContainer.ScaleTo(1, transition_duration, Easing.OutQuint); + } + else if (DrawWidth >= height + username_min_width + statistics_compact_min_width + right_content_width) + { + statisticsContainer.FadeIn(transition_duration, Easing.OutQuint).MoveToX(0, transition_duration, Easing.OutQuint); + statisticsContainer.Direction = FillDirection.Vertical; + statisticsContainer.ScaleTo(0.8f, transition_duration, Easing.OutQuint); + } else statisticsContainer.FadeOut(transition_duration, Easing.OutQuint).MoveToX(statisticsContainer.DrawWidth, transition_duration, Easing.OutQuint); }); @@ -577,15 +624,14 @@ namespace osu.Game.Online.Leaderboards { public RankLabel(int? rank) { - AutoSizeAxes = Axes.Both; - Anchor = Anchor.Centre; - Origin = Anchor.Centre; - if (rank >= 1000) TooltipText = $"#{rank:N0}"; Child = new OsuSpriteText { + Shear = -shear, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, Font = OsuFont.GetFont(size: 20, weight: FontWeight.SemiBold, italics: true), Text = rank == null ? "-" : rank.Value.FormatRank().Insert(0, "#") }; From 9b84d8ac2f7ffd662c190e18b096bf5add9a2bf3 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Sun, 12 May 2024 22:39:22 -0700 Subject: [PATCH 0777/2493] Apply missed changes from old leaderboard score See: - https://github.com/ppy/osu/commit/d11e56b8bb6ade5c4f6e47de5fa288a909f6bc66 - https://github.com/ppy/osu/commit/7d74d84e6c24e6938d27fae0d7322e113df4be94 - https://github.com/ppy/osu/commit/07f9f5c6d842d3c7c564f96576682b6fb54c50b4 --- osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs b/osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs index d0a264a7e3..b9ae3bb20e 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs @@ -439,7 +439,7 @@ namespace osu.Game.Online.Leaderboards Shear = -shear, AutoSizeAxes = Axes.Both, Direction = FillDirection.Horizontal, - ChildrenEnumerable = score.Mods.Select(mod => new ColouredModSwitchTiny(mod) { Scale = new Vector2(0.375f) }) + ChildrenEnumerable = score.Mods.AsOrdered().Select(mod => new ColouredModSwitchTiny(mod) { Scale = new Vector2(0.375f) }) }, modsCounter = new OsuSpriteText { @@ -671,12 +671,12 @@ namespace osu.Game.Online.Leaderboards { List items = new List(); - if (score.Mods.Length > 0 && modsContainer.Any(s => s.IsHovered) && songSelect != null) + if (score.Mods.Length > 0 && songSelect != null) items.Add(new OsuMenuItem("Use these mods", MenuItemType.Highlighted, () => songSelect.Mods.Value = score.Mods)); if (score.Files.Count <= 0) return items.ToArray(); - items.Add(new OsuMenuItem("Export", MenuItemType.Standard, () => scoreManager.Export(score))); + items.Add(new OsuMenuItem(Localisation.CommonStrings.Export, MenuItemType.Standard, () => scoreManager.Export(score))); items.Add(new OsuMenuItem(CommonStrings.ButtonsDelete, MenuItemType.Destructive, () => dialogOverlay?.Push(new LocalScoreDeleteDialog(score)))); return items.ToArray(); From 260c224619289efa6e6f603c8cd8584ad445eeed Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 14 May 2024 01:23:40 +0900 Subject: [PATCH 0778/2493] Add failing test --- .../TestSceneOsuTouchInput.cs | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuTouchInput.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuTouchInput.cs index 5bf7c0326a..d3711c0cc6 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuTouchInput.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuTouchInput.cs @@ -19,6 +19,7 @@ using osu.Game.Beatmaps.ControlPoints; using osu.Game.Configuration; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects.Drawables; +using osu.Game.Rulesets.Osu.UI; using osu.Game.Rulesets.Osu.UI.Cursor; using osu.Game.Screens.Play.HUD; using osu.Game.Tests.Visual; @@ -578,6 +579,25 @@ namespace osu.Game.Rulesets.Osu.Tests assertKeyCounter(1, 1); } + [Test] + [Solo] + public void TestTouchJudgedCircle() + { + addHitCircleAt(TouchSource.Touch1); + addHitCircleAt(TouchSource.Touch2); + + beginTouch(TouchSource.Touch1); + endTouch(TouchSource.Touch1); + + // Hold the second touch (this becomes the primary touch). + beginTouch(TouchSource.Touch2); + + // Touch again on the first circle. + // Because it's been judged, the cursor should not move here. + beginTouch(TouchSource.Touch1); + checkPosition(TouchSource.Touch2); + } + private void addHitCircleAt(TouchSource source) { AddStep($"Add circle at {source}", () => @@ -590,6 +610,7 @@ namespace osu.Game.Rulesets.Osu.Tests { Clock = new FramedClock(new ManualClock()), Position = mainContent.ToLocalSpace(getSanePositionForSource(source)), + CheckHittable = (_, _, _) => ClickAction.Hit }); }); } From ff0c0d54c9127df3006bcb4249fea225d5cc3f6f Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 14 May 2024 01:23:55 +0900 Subject: [PATCH 0779/2493] Fix taps on judged circles changing cursor position --- osu.Game.Rulesets.Osu/OsuInputManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/OsuInputManager.cs b/osu.Game.Rulesets.Osu/OsuInputManager.cs index ceac1989a6..65bd585e98 100644 --- a/osu.Game.Rulesets.Osu/OsuInputManager.cs +++ b/osu.Game.Rulesets.Osu/OsuInputManager.cs @@ -47,7 +47,7 @@ namespace osu.Game.Rulesets.Osu // // Based on user feedback of more nuanced scenarios (where touch doesn't behave as expected), // this can be expanded to a more complex implementation, but I'd still want to keep it as simple as we can. - NonPositionalInputQueue.OfType().Any(c => c.ReceivePositionalInputAt(screenSpacePosition)); + NonPositionalInputQueue.OfType().Any(c => c.CanBeHit() && c.ReceivePositionalInputAt(screenSpacePosition)); public OsuInputManager(RulesetInfo ruleset) : base(ruleset, 0, SimultaneousBindingMode.Unique) From 20e28964352c140bb71198c4be743b6189a5edfd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 13 May 2024 18:48:08 +0200 Subject: [PATCH 0780/2493] Remove leftover `[Solo]` attribute --- osu.Game.Rulesets.Osu.Tests/TestSceneOsuTouchInput.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuTouchInput.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuTouchInput.cs index d3711c0cc6..bf0ab8efa0 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuTouchInput.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuTouchInput.cs @@ -580,7 +580,6 @@ namespace osu.Game.Rulesets.Osu.Tests } [Test] - [Solo] public void TestTouchJudgedCircle() { addHitCircleAt(TouchSource.Touch1); From cfb2c8272b1db1d774678d127a3b2ee70e264b7d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 13 May 2024 22:56:15 +0800 Subject: [PATCH 0781/2493] Set a rudimentary lifetime end to improve seek performance in scrolling rulesets --- .../Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs b/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs index 4e72291b9c..e70e181a50 100644 --- a/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs +++ b/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs @@ -136,7 +136,7 @@ namespace osu.Game.Rulesets.UI.Scrolling // Scroll info is not available until loaded. // The lifetime of all entries will be updated in the first Update. if (IsLoaded) - setComputedLifetimeStart(entry); + setComputedLifetime(entry); base.Add(entry); } @@ -171,7 +171,7 @@ namespace osu.Game.Rulesets.UI.Scrolling layoutComputed.Clear(); foreach (var entry in Entries) - setComputedLifetimeStart(entry); + setComputedLifetime(entry); algorithm.Value.Reset(); @@ -234,12 +234,13 @@ namespace osu.Game.Rulesets.UI.Scrolling return algorithm.Value.GetDisplayStartTime(entry.HitObject.StartTime, startOffset, timeRange.Value, scrollLength); } - private void setComputedLifetimeStart(HitObjectLifetimeEntry entry) + private void setComputedLifetime(HitObjectLifetimeEntry entry) { double computedStartTime = computeDisplayStartTime(entry); // always load the hitobject before its first judgement offset entry.LifetimeStart = Math.Min(entry.HitObject.StartTime - entry.HitObject.MaximumJudgementOffset, computedStartTime); + entry.LifetimeEnd = entry.HitObject.GetEndTime() + timeRange.Value; } private void updateLayoutRecursive(DrawableHitObject hitObject, double? parentHitObjectStartTime = null) @@ -261,7 +262,7 @@ namespace osu.Game.Rulesets.UI.Scrolling // Nested hitobjects don't need to scroll, but they do need accurate positions and start lifetime updatePosition(obj, hitObject.HitObject.StartTime, parentHitObjectStartTime); - setComputedLifetimeStart(obj.Entry); + setComputedLifetime(obj.Entry); } } From 7f3fde2a25d0d78277a6794cc48ec856691e689a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 14 May 2024 11:13:06 +0200 Subject: [PATCH 0782/2493] Add failing test case --- .../Visual/Navigation/TestScenePresentScore.cs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/osu.Game.Tests/Visual/Navigation/TestScenePresentScore.cs b/osu.Game.Tests/Visual/Navigation/TestScenePresentScore.cs index 004d1de116..212783d047 100644 --- a/osu.Game.Tests/Visual/Navigation/TestScenePresentScore.cs +++ b/osu.Game.Tests/Visual/Navigation/TestScenePresentScore.cs @@ -145,6 +145,19 @@ namespace osu.Game.Tests.Visual.Navigation presentAndConfirm(secondImport, type); } + [Test] + public void TestPresentTwoImportsWithSameOnlineIDButDifferentHashes([Values] ScorePresentType type) + { + AddStep("enter song select", () => Game.ChildrenOfType().Single().OnSolo?.Invoke()); + AddUntilStep("song select is current", () => Game.ScreenStack.CurrentScreen is PlaySongSelect songSelect && songSelect.BeatmapSetsLoaded); + + var firstImport = importScore(1); + presentAndConfirm(firstImport, type); + + var secondImport = importScore(1); + presentAndConfirm(secondImport, type); + } + private void returnToMenu() { // if we don't pause, there's a chance the track may change at the main menu out of our control (due to reaching the end of the track). From 03a279a48d476b2529d01d975022cb927eb80875 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 14 May 2024 11:14:46 +0200 Subject: [PATCH 0783/2493] Use hash rather than online ID as primary lookup key when presenting score Something I ran into when investigating https://github.com/ppy/osu/issues/28169. If there are two scores with the same online ID available in the database - for instance, one being recorded locally, and one recorded by spectator server, of one single play - the lookup code would use online ID first to find the score and pick any first one that matched. This could lead to the wrong replay being refetched and presented / exported. (In the case of the aforementioned issue, I was confused as to whether after restarting spectator server midway through a play and importing the replay saved by spectator server after the restart, I was seeing a complete replay with no dropped frames, even though there was nothing in the code that prevented the frame drop. It turns out that I was getting presented the locally recorded replay instead all along.) Instead, jiggle the fallback preference to use hash first. --- osu.Game/Scoring/ScoreManager.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Scoring/ScoreManager.cs b/osu.Game/Scoring/ScoreManager.cs index 1ba5c7d4cf..0c707ffa19 100644 --- a/osu.Game/Scoring/ScoreManager.cs +++ b/osu.Game/Scoring/ScoreManager.cs @@ -88,15 +88,15 @@ namespace osu.Game.Scoring { ScoreInfo? databasedScoreInfo = null; + if (originalScoreInfo is ScoreInfo scoreInfo) + databasedScoreInfo = Query(s => s.Hash == scoreInfo.Hash); + if (originalScoreInfo.OnlineID > 0) - databasedScoreInfo = Query(s => s.OnlineID == originalScoreInfo.OnlineID); + databasedScoreInfo ??= Query(s => s.OnlineID == originalScoreInfo.OnlineID); if (originalScoreInfo.LegacyOnlineID > 0) databasedScoreInfo ??= Query(s => s.LegacyOnlineID == originalScoreInfo.LegacyOnlineID); - if (originalScoreInfo is ScoreInfo scoreInfo) - databasedScoreInfo ??= Query(s => s.Hash == scoreInfo.Hash); - if (databasedScoreInfo == null) { Logger.Log("The requested score could not be found locally.", LoggingTarget.Information); From 4f6777a0a12fe204a399a19bcc7a1fa54a369f2c Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 4 Apr 2024 16:00:18 +0900 Subject: [PATCH 0784/2493] Remove existing per-column touch input --- osu.Game.Rulesets.Mania/UI/Column.cs | 36 +--------------------------- 1 file changed, 1 insertion(+), 35 deletions(-) diff --git a/osu.Game.Rulesets.Mania/UI/Column.cs b/osu.Game.Rulesets.Mania/UI/Column.cs index 6cd55bb099..c05a8f2a29 100644 --- a/osu.Game.Rulesets.Mania/UI/Column.cs +++ b/osu.Game.Rulesets.Mania/UI/Column.cs @@ -93,8 +93,7 @@ namespace osu.Game.Rulesets.Mania.UI // For input purposes, the background is added at the highest depth, but is then proxied back below all other elements externally // (see `Stage.columnBackgrounds`). BackgroundContainer, - TopLevelContainer, - new ColumnTouchInputArea(this) + TopLevelContainer }; var background = new SkinnableDrawable(new ManiaSkinComponentLookup(ManiaSkinComponents.ColumnBackground), _ => new DefaultColumnBackground()) @@ -181,38 +180,5 @@ namespace osu.Game.Rulesets.Mania.UI public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) // This probably shouldn't exist as is, but the columns in the stage are separated by a 1px border => DrawRectangle.Inflate(new Vector2(Stage.COLUMN_SPACING / 2, 0)).Contains(ToLocalSpace(screenSpacePos)); - - public partial class ColumnTouchInputArea : Drawable - { - private readonly Column column; - - [Resolved(canBeNull: true)] - private ManiaInputManager maniaInputManager { get; set; } - - private KeyBindingContainer keyBindingContainer; - - public ColumnTouchInputArea(Column column) - { - RelativeSizeAxes = Axes.Both; - - this.column = column; - } - - protected override void LoadComplete() - { - keyBindingContainer = maniaInputManager?.KeyBindingContainer; - } - - protected override bool OnTouchDown(TouchDownEvent e) - { - keyBindingContainer?.TriggerPressed(column.Action.Value); - return true; - } - - protected override void OnTouchUp(TouchUpEvent e) - { - keyBindingContainer?.TriggerReleased(column.Action.Value); - } - } } } From ef40197713009fd58c04ddc4f516a6cefd001ed8 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 4 Apr 2024 17:11:15 +0900 Subject: [PATCH 0785/2493] Add mania touch overlay Adjust default anchor/origin --- osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs | 20 ++- .../UI/ManiaTouchInputOverlay.cs | 144 ++++++++++++++++++ 2 files changed, 161 insertions(+), 3 deletions(-) create mode 100644 osu.Game.Rulesets.Mania/UI/ManiaTouchInputOverlay.cs diff --git a/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs b/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs index b3420c49f3..7610e48582 100644 --- a/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs +++ b/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs @@ -7,6 +7,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using System; using System.Collections.Generic; +using System.Linq; using osu.Framework.Allocation; using osu.Framework.Graphics.Primitives; using osu.Game.Rulesets.Mania.Beatmaps; @@ -60,10 +61,23 @@ namespace osu.Game.Rulesets.Mania.UI throw new ArgumentException("Can't have zero or fewer stages."); GridContainer playfieldGrid; - AddInternal(playfieldGrid = new GridContainer + + RelativeSizeAxes = Axes.Y; + AutoSizeAxes = Axes.X; + + AddRangeInternal(new Drawable[] { - RelativeSizeAxes = Axes.Both, - Content = new[] { new Drawable[stageDefinitions.Count] } + playfieldGrid = new GridContainer + { + RelativeSizeAxes = Axes.Y, + AutoSizeAxes = Axes.X, + Content = new[] { new Drawable[stageDefinitions.Count] }, + ColumnDimensions = Enumerable.Range(0, stageDefinitions.Count).Select(_ => new Dimension(GridSizeMode.AutoSize)).ToArray() + }, + new ManiaTouchInputOverlay + { + RelativeSizeAxes = Axes.Both, + } }); var normalColumnAction = ManiaAction.Key1; diff --git a/osu.Game.Rulesets.Mania/UI/ManiaTouchInputOverlay.cs b/osu.Game.Rulesets.Mania/UI/ManiaTouchInputOverlay.cs new file mode 100644 index 0000000000..476461959d --- /dev/null +++ b/osu.Game.Rulesets.Mania/UI/ManiaTouchInputOverlay.cs @@ -0,0 +1,144 @@ +// 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.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Input.Events; +using osu.Game.Configuration; +using osu.Game.Skinning; +using osuTK; + +namespace osu.Game.Rulesets.Mania.UI +{ + public partial class ManiaTouchInputOverlay : CompositeDrawable, ISerialisableDrawable + { + [SettingSource("Spacing", "The spacing between input receptors.")] + public BindableFloat Spacing { get; } = new BindableFloat(10) + { + Precision = 1, + MinValue = 0, + MaxValue = 100, + }; + + [Resolved] + private ManiaPlayfield playfield { get; set; } = null!; + + public ManiaTouchInputOverlay() + { + Anchor = Anchor.BottomCentre; + Origin = Anchor.BottomCentre; + RelativeSizeAxes = Axes.Both; + Height = 0.5f; + } + + [BackgroundDependencyLoader] + private void load() + { + List receptorGridContent = new List(); + List receptorGridDimensions = new List(); + + bool first = true; + + foreach (var stage in playfield.Stages) + { + foreach (var column in stage.Columns) + { + if (!first) + { + receptorGridContent.Add(new Gutter { Spacing = { BindTarget = Spacing } }); + receptorGridDimensions.Add(new Dimension(GridSizeMode.AutoSize)); + } + + receptorGridContent.Add(new InputReceptor()); + receptorGridDimensions.Add(new Dimension()); + + first = false; + } + } + + InternalChild = new GridContainer + { + RelativeSizeAxes = Axes.Both, + Content = new[] { receptorGridContent.ToArray() }, + ColumnDimensions = receptorGridDimensions.ToArray() + }; + } + + public bool UsesFixedAnchor { get; set; } + + public partial class InputReceptor : CompositeDrawable + { + private readonly Box highlightOverlay; + + public InputReceptor() + { + RelativeSizeAxes = Axes.Both; + + InternalChildren = new Drawable[] + { + new Container + { + RelativeSizeAxes = Axes.Both, + Masking = true, + CornerRadius = 10, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Alpha = 0.15f, + }, + highlightOverlay = new Box + { + RelativeSizeAxes = Axes.Both, + Alpha = 0, + Blending = BlendingParameters.Additive, + } + } + } + }; + } + + protected override bool OnTouchDown(TouchDownEvent e) + { + updateHighlight(true); + return true; + } + + protected override void OnTouchUp(TouchUpEvent e) + { + updateHighlight(false); + } + + protected override bool OnMouseDown(MouseDownEvent e) + { + updateHighlight(true); + return true; + } + + protected override void OnMouseUp(MouseUpEvent e) + { + updateHighlight(false); + } + + private void updateHighlight(bool enabled) + { + highlightOverlay.FadeTo(enabled ? 0.1f : 0, enabled ? 80 : 400, Easing.OutQuint); + } + } + + private partial class Gutter : Drawable + { + public readonly IBindable Spacing = new Bindable(); + + public Gutter() + { + Spacing.BindValueChanged(s => Size = new Vector2(s.NewValue)); + } + } + } +} From 39337f5189fb371ba91c6b5145374213cbbfba71 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 4 Apr 2024 17:25:05 +0900 Subject: [PATCH 0786/2493] Hook up input manager --- .../UI/ManiaTouchInputOverlay.cs | 35 +++++++++++++++---- 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Mania/UI/ManiaTouchInputOverlay.cs b/osu.Game.Rulesets.Mania/UI/ManiaTouchInputOverlay.cs index 476461959d..10de89e950 100644 --- a/osu.Game.Rulesets.Mania/UI/ManiaTouchInputOverlay.cs +++ b/osu.Game.Rulesets.Mania/UI/ManiaTouchInputOverlay.cs @@ -53,7 +53,7 @@ namespace osu.Game.Rulesets.Mania.UI receptorGridDimensions.Add(new Dimension(GridSizeMode.AutoSize)); } - receptorGridContent.Add(new InputReceptor()); + receptorGridContent.Add(new InputReceptor { Action = { BindTarget = column.Action } }); receptorGridDimensions.Add(new Dimension()); first = false; @@ -72,8 +72,15 @@ namespace osu.Game.Rulesets.Mania.UI public partial class InputReceptor : CompositeDrawable { + public readonly IBindable Action = new Bindable(); + private readonly Box highlightOverlay; + [Resolved] + private ManiaInputManager? inputManager { get; set; } + + private bool isPressed; + public InputReceptor() { RelativeSizeAxes = Axes.Both; @@ -105,29 +112,43 @@ namespace osu.Game.Rulesets.Mania.UI protected override bool OnTouchDown(TouchDownEvent e) { - updateHighlight(true); + updateButton(true); return true; } protected override void OnTouchUp(TouchUpEvent e) { - updateHighlight(false); + updateButton(false); } protected override bool OnMouseDown(MouseDownEvent e) { - updateHighlight(true); + updateButton(true); return true; } protected override void OnMouseUp(MouseUpEvent e) { - updateHighlight(false); + updateButton(false); } - private void updateHighlight(bool enabled) + private void updateButton(bool press) { - highlightOverlay.FadeTo(enabled ? 0.1f : 0, enabled ? 80 : 400, Easing.OutQuint); + if (press == isPressed) + return; + + isPressed = press; + + if (press) + { + inputManager?.KeyBindingContainer?.TriggerPressed(Action.Value); + highlightOverlay.FadeTo(0.1f, 80, Easing.OutQuint); + } + else + { + inputManager?.KeyBindingContainer?.TriggerReleased(Action.Value); + highlightOverlay.FadeTo(0, 400, Easing.OutQuint); + } } } From e3f2e1ba08c79e01b9dc9a0d76ff9bf21f41c32e Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 4 Apr 2024 18:20:30 +0900 Subject: [PATCH 0787/2493] Add opacity setting --- .../UI/ManiaTouchInputOverlay.cs | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Mania/UI/ManiaTouchInputOverlay.cs b/osu.Game.Rulesets.Mania/UI/ManiaTouchInputOverlay.cs index 10de89e950..ddff064133 100644 --- a/osu.Game.Rulesets.Mania/UI/ManiaTouchInputOverlay.cs +++ b/osu.Game.Rulesets.Mania/UI/ManiaTouchInputOverlay.cs @@ -16,7 +16,7 @@ namespace osu.Game.Rulesets.Mania.UI { public partial class ManiaTouchInputOverlay : CompositeDrawable, ISerialisableDrawable { - [SettingSource("Spacing", "The spacing between input receptors.")] + [SettingSource("Spacing", "The spacing between receptors.")] public BindableFloat Spacing { get; } = new BindableFloat(10) { Precision = 1, @@ -24,6 +24,14 @@ namespace osu.Game.Rulesets.Mania.UI MaxValue = 100, }; + [SettingSource("Opacity", "The receptor opacity.")] + public BindableFloat Opacity { get; } = new BindableFloat(1) + { + Precision = 0.1f, + MinValue = 0, + MaxValue = 1 + }; + [Resolved] private ManiaPlayfield playfield { get; set; } = null!; @@ -68,6 +76,12 @@ namespace osu.Game.Rulesets.Mania.UI }; } + protected override void LoadComplete() + { + base.LoadComplete(); + Opacity.BindValueChanged(o => Alpha = o.NewValue, true); + } + public bool UsesFixedAnchor { get; set; } public partial class InputReceptor : CompositeDrawable From a761a7bced2deacea3f7ee9c2e765c59e7cd0670 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 4 Apr 2024 18:20:51 +0900 Subject: [PATCH 0788/2493] Hook up touch device mod --- .../Mods/TestSceneModTouchDevice.cs | 64 +++++++++++++++++++ osu.Game.Rulesets.Mania/ManiaRuleset.cs | 8 +++ osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs | 14 +++- 3 files changed, 84 insertions(+), 2 deletions(-) create mode 100644 osu.Game.Rulesets.Mania.Tests/Mods/TestSceneModTouchDevice.cs diff --git a/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneModTouchDevice.cs b/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneModTouchDevice.cs new file mode 100644 index 0000000000..4c5e4933ef --- /dev/null +++ b/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneModTouchDevice.cs @@ -0,0 +1,64 @@ +// 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.Input; +using osu.Framework.Testing; +using osu.Game.Rulesets.Mania.UI; +using osu.Game.Rulesets.Mods; +using osu.Game.Skinning; +using osu.Game.Tests.Visual; + +namespace osu.Game.Rulesets.Mania.Tests.Mods +{ + public partial class TestSceneModTouchDevice : ModTestScene + { + protected override Ruleset CreatePlayerRuleset() => new ManiaRuleset(); + + [Test] + public void TestOverlayVisibleWithMod() => CreateModTest(new ModTestData + { + Mod = new ModTouchDevice(), + Autoplay = false, + PassCondition = () => getSkinnableOverlay()?.IsPresent == true + }); + + [Test] + public void TestOverlayNotVisibleWithoutMod() => CreateModTest(new ModTestData + { + Autoplay = false, + PassCondition = () => getSkinnableOverlay()?.IsPresent == false + }); + + [Test] + public void TestPressReceptors() + { + CreateModTest(new ModTestData + { + Mod = new ModTouchDevice(), + Autoplay = false, + PassCondition = () => true + }); + + for (int i = 0; i < 4; i++) + { + int index = i; + + AddStep($"touch receptor {index}", () => InputManager.BeginTouch(new Touch(TouchSource.Touch1, getReceptor(index).ScreenSpaceDrawQuad.Centre))); + + AddAssert("action sent", + () => this.ChildrenOfType().SelectMany(m => m.KeyBindingContainer.PressedActions), + () => Does.Contain(getReceptor(index).Action.Value)); + + AddStep($"release receptor {index}", () => InputManager.EndTouch(new Touch(TouchSource.Touch1, getReceptor(index).ScreenSpaceDrawQuad.Centre))); + } + } + + private Drawable? getSkinnableOverlay() => this.ChildrenOfType() + .SingleOrDefault(d => d.Lookup.Equals(new ManiaSkinComponentLookup(ManiaSkinComponents.TouchOverlay))); + + private ManiaTouchInputOverlay.InputReceptor getReceptor(int index) => this.ChildrenOfType().ElementAt(index); + } +} diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs index b5614e2b56..23004e36a0 100644 --- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs @@ -163,6 +163,9 @@ namespace osu.Game.Rulesets.Mania if (mods.HasFlagFast(LegacyMods.ScoreV2)) yield return new ModScoreV2(); + + if (mods.HasFlagFast(LegacyMods.TouchDevice)) + yield return new ModTouchDevice(); } public override LegacyMods ConvertToLegacyMods(Mod[] mods) @@ -225,6 +228,10 @@ namespace osu.Game.Rulesets.Mania case ManiaModRandom: value |= LegacyMods.Random; break; + + case ModTouchDevice: + value |= LegacyMods.TouchDevice; + break; } } @@ -296,6 +303,7 @@ namespace osu.Game.Rulesets.Mania case ModType.System: return new Mod[] { + new ModTouchDevice(), new ModScoreV2(), }; diff --git a/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs b/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs index 7610e48582..385a47f8b8 100644 --- a/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs +++ b/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs @@ -12,6 +12,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics.Primitives; using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Objects; +using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.UI.Scrolling; @@ -26,6 +27,8 @@ namespace osu.Game.Rulesets.Mania.UI private readonly List stages = new List(); + private readonly ManiaTouchInputOverlay touchOverlay; + public override Quad SkinnableComponentScreenSpaceDrawQuad { get @@ -74,9 +77,9 @@ namespace osu.Game.Rulesets.Mania.UI Content = new[] { new Drawable[stageDefinitions.Count] }, ColumnDimensions = Enumerable.Range(0, stageDefinitions.Count).Select(_ => new Dimension(GridSizeMode.AutoSize)).ToArray() }, - new ManiaTouchInputOverlay + touchOverlay = new ManiaTouchInputOverlay { - RelativeSizeAxes = Axes.Both, + RelativeSizeAxes = Axes.Both } }); @@ -97,6 +100,13 @@ namespace osu.Game.Rulesets.Mania.UI } } + protected override void LoadComplete() + { + base.LoadComplete(); + + touchOverlay.Alpha = Mods?.Any(m => m is ModTouchDevice) == true ? 1 : 0; + } + public override void Add(HitObject hitObject) => getStageByColumn(((ManiaHitObject)hitObject).Column).Add(hitObject); public override bool Remove(HitObject hitObject) => getStageByColumn(((ManiaHitObject)hitObject).Column).Remove(hitObject); From cb49147d1e17d14c8b6a63c5a2e3b535a36f57a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 14 May 2024 12:57:30 +0200 Subject: [PATCH 0789/2493] Apply NRT to `ScorePanelList` --- osu.Game/Screens/Ranking/ResultsScreen.cs | 2 ++ osu.Game/Screens/Ranking/ScorePanelList.cs | 19 +++++++------------ 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs index 1c3518909d..44b270db53 100644 --- a/osu.Game/Screens/Ranking/ResultsScreen.cs +++ b/osu.Game/Screens/Ranking/ResultsScreen.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Audio; @@ -329,6 +330,7 @@ namespace osu.Game.Screens.Ranking { if (state.NewValue == Visibility.Visible) { + Debug.Assert(SelectedScore.Value != null); // Detach the panel in its original location, and move into the desired location in the local container. var expandedPanel = ScorePanelList.GetPanelForScore(SelectedScore.Value); var screenSpacePos = expandedPanel.ScreenSpaceDrawQuad.TopLeft; diff --git a/osu.Game/Screens/Ranking/ScorePanelList.cs b/osu.Game/Screens/Ranking/ScorePanelList.cs index 95c90e35a0..e711bed729 100644 --- a/osu.Game/Screens/Ranking/ScorePanelList.cs +++ b/osu.Game/Screens/Ranking/ScorePanelList.cs @@ -1,14 +1,11 @@ // 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.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Threading; -using JetBrains.Annotations; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -64,14 +61,14 @@ namespace osu.Game.Screens.Ranking /// /// An action to be invoked if a is clicked while in an expanded state. /// - public Action PostExpandAction; + public Action? PostExpandAction; - public readonly Bindable SelectedScore = new Bindable(); + public readonly Bindable SelectedScore = new Bindable(); private readonly CancellationTokenSource loadCancellationSource = new CancellationTokenSource(); private readonly Flow flow; private readonly Scroll scroll; - private ScorePanel expandedPanel; + private ScorePanel? expandedPanel; /// /// Creates a new . @@ -174,7 +171,7 @@ namespace osu.Game.Screens.Ranking /// Brings a to the centre of the screen and expands it. /// /// The to present. - private void selectedScoreChanged(ValueChangedEvent score) + private void selectedScoreChanged(ValueChangedEvent score) { // avoid contracting panels unnecessarily when TriggerChange is fired manually. if (score.OldValue != null && !score.OldValue.Equals(score.NewValue)) @@ -317,7 +314,7 @@ namespace osu.Game.Screens.Ranking protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); - loadCancellationSource?.Cancel(); + loadCancellationSource.Cancel(); } private partial class Flow : FillFlowContainer @@ -326,11 +323,9 @@ namespace osu.Game.Screens.Ranking public int GetPanelIndex(ScoreInfo score) => applySorting(Children).TakeWhile(s => !s.Panel.Score.Equals(score)).Count(); - [CanBeNull] - public ScoreInfo GetPreviousScore(ScoreInfo score) => applySorting(Children).TakeWhile(s => !s.Panel.Score.Equals(score)).LastOrDefault()?.Panel.Score; + public ScoreInfo? GetPreviousScore(ScoreInfo score) => applySorting(Children).TakeWhile(s => !s.Panel.Score.Equals(score)).LastOrDefault()?.Panel.Score; - [CanBeNull] - public ScoreInfo GetNextScore(ScoreInfo score) => applySorting(Children).SkipWhile(s => !s.Panel.Score.Equals(score)).ElementAtOrDefault(1)?.Panel.Score; + public ScoreInfo? GetNextScore(ScoreInfo score) => applySorting(Children).SkipWhile(s => !s.Panel.Score.Equals(score)).ElementAtOrDefault(1)?.Panel.Score; private IEnumerable applySorting(IEnumerable drawables) => drawables.OfType() .OrderByDescending(GetLayoutPosition) From 10a8e84046ac4e8abb06e44ab112e2b3274bb2d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 14 May 2024 13:01:26 +0200 Subject: [PATCH 0790/2493] Apply NRT to `StatisticsPanel` --- .../Ranking/Statistics/StatisticsPanel.cs | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs index 19bd0c4393..f9f5254bc2 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.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.Collections.Generic; using System.Linq; using System.Threading; @@ -28,19 +26,20 @@ namespace osu.Game.Screens.Ranking.Statistics { public const float SIDE_PADDING = 30; - public readonly Bindable Score = new Bindable(); + public readonly Bindable Score = new Bindable(); protected override bool StartHidden => true; [Resolved] - private BeatmapManager beatmapManager { get; set; } + private BeatmapManager beatmapManager { get; set; } = null!; private readonly Container content; private readonly LoadingSpinner spinner; private bool wasOpened; - private Sample popInSample; - private Sample popOutSample; + private Sample? popInSample; + private Sample? popOutSample; + private CancellationTokenSource? loadCancellation; public StatisticsPanel() { @@ -71,9 +70,7 @@ namespace osu.Game.Screens.Ranking.Statistics popOutSample = audio.Samples.Get(@"Results/statistics-panel-pop-out"); } - private CancellationTokenSource loadCancellation; - - private void populateStatistics(ValueChangedEvent score) + private void populateStatistics(ValueChangedEvent score) { loadCancellation?.Cancel(); loadCancellation = null; @@ -187,7 +184,7 @@ namespace osu.Game.Screens.Ranking.Statistics LoadComponentAsync(container, d => { - if (!Score.Value.Equals(newScore)) + if (Score.Value?.Equals(newScore) != true) return; spinner.Hide(); From 77a7f475ee25314cc7d68e612f8bfbf2beae2ace Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 14 May 2024 13:03:46 +0200 Subject: [PATCH 0791/2493] Apply NRT to `ScorePanel` --- osu.Game/Screens/Ranking/ScorePanel.cs | 34 ++++++++++++-------------- 1 file changed, 15 insertions(+), 19 deletions(-) diff --git a/osu.Game/Screens/Ranking/ScorePanel.cs b/osu.Game/Screens/Ranking/ScorePanel.cs index 1f7ba3692a..e283749e32 100644 --- a/osu.Game/Screens/Ranking/ScorePanel.cs +++ b/osu.Game/Screens/Ranking/ScorePanel.cs @@ -1,10 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; -using JetBrains.Annotations; using osu.Framework; using osu.Framework.Allocation; using osu.Framework.Audio; @@ -83,8 +80,7 @@ namespace osu.Game.Screens.Ranking private static readonly Color4 contracted_top_layer_colour = Color4Extensions.FromHex("#353535"); private static readonly Color4 contracted_middle_layer_colour = Color4Extensions.FromHex("#353535"); - [CanBeNull] - public event Action StateChanged; + public event Action? StateChanged; /// /// The position of the score in the rankings. @@ -94,28 +90,30 @@ namespace osu.Game.Screens.Ranking /// /// An action to be invoked if this is clicked while in an expanded state. /// - public Action PostExpandAction; + public Action? PostExpandAction; public readonly ScoreInfo Score; [Resolved] - private OsuGameBase game { get; set; } + private OsuGameBase game { get; set; } = null!; - private AudioContainer audioContent; + private AudioContainer audioContent = null!; private bool displayWithFlair; - private Container topLayerContainer; - private Drawable topLayerBackground; - private Container topLayerContentContainer; - private Drawable topLayerContent; + private Container topLayerContainer = null!; + private Drawable topLayerBackground = null!; + private Container topLayerContentContainer = null!; + private Drawable? topLayerContent; - private Container middleLayerContainer; - private Drawable middleLayerBackground; - private Container middleLayerContentContainer; - private Drawable middleLayerContent; + private Container middleLayerContainer = null!; + private Drawable middleLayerBackground = null!; + private Container middleLayerContentContainer = null!; + private Drawable? middleLayerContent; - private DrawableSample samplePanelFocus; + private ScorePanelTrackingContainer? trackingContainer; + + private DrawableSample? samplePanelFocus; public ScorePanel(ScoreInfo score, bool isNewLocalScore = false) { @@ -334,8 +332,6 @@ namespace osu.Game.Screens.Ranking || topLayerContainer.ReceivePositionalInputAt(screenSpacePos) || middleLayerContainer.ReceivePositionalInputAt(screenSpacePos); - private ScorePanelTrackingContainer trackingContainer; - /// /// Creates a which this can reside inside. /// The will track the size of this . From 2f2257f6cec8254ca48432d6811a0cb823f0364c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 14 May 2024 13:07:49 +0200 Subject: [PATCH 0792/2493] Apply NRT to `PerformanceBreakdownChart` --- .../Statistics/PerformanceBreakdownChart.cs | 22 ++++++++----------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/osu.Game/Screens/Ranking/Statistics/PerformanceBreakdownChart.cs b/osu.Game/Screens/Ranking/Statistics/PerformanceBreakdownChart.cs index 8b13f0951c..b5eed2d12a 100644 --- a/osu.Game/Screens/Ranking/Statistics/PerformanceBreakdownChart.cs +++ b/osu.Game/Screens/Ranking/Statistics/PerformanceBreakdownChart.cs @@ -1,13 +1,10 @@ // 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.Collections.Generic; using System.Linq; using System.Threading; -using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Extensions; using osu.Framework.Extensions.Color4Extensions; @@ -31,16 +28,16 @@ namespace osu.Game.Screens.Ranking.Statistics private readonly ScoreInfo score; private readonly IBeatmap playableBeatmap; - private Drawable spinner; - private Drawable content; - private GridContainer chart; - private OsuSpriteText achievedPerformance; - private OsuSpriteText maximumPerformance; + private Drawable spinner = null!; + private Drawable content = null!; + private GridContainer chart = null!; + private OsuSpriteText achievedPerformance = null!; + private OsuSpriteText maximumPerformance = null!; private readonly CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(); [Resolved] - private BeatmapDifficultyCache difficultyCache { get; set; } + private BeatmapDifficultyCache difficultyCache { get; set; } = null!; public PerformanceBreakdownChart(ScoreInfo score, IBeatmap playableBeatmap) { @@ -147,7 +144,7 @@ namespace osu.Game.Screens.Ranking.Statistics new PerformanceBreakdownCalculator(playableBeatmap, difficultyCache) .CalculateAsync(score, cancellationTokenSource.Token) - .ContinueWith(t => Schedule(() => setPerformanceValue(t.GetResultSafely()))); + .ContinueWith(t => Schedule(() => setPerformanceValue(t.GetResultSafely()!))); } private void setPerformanceValue(PerformanceBreakdown breakdown) @@ -189,8 +186,7 @@ namespace osu.Game.Screens.Ranking.Statistics maximumPerformance.Text = Math.Round(perfectAttribute.Value, MidpointRounding.AwayFromZero).ToLocalisableString(); } - [CanBeNull] - private Drawable[] createAttributeRow(PerformanceDisplayAttribute attribute, PerformanceDisplayAttribute perfectAttribute) + private Drawable[]? createAttributeRow(PerformanceDisplayAttribute attribute, PerformanceDisplayAttribute perfectAttribute) { // Don't display the attribute if its maximum is 0 // For example, flashlight bonus would be zero if flashlight mod isn't on @@ -239,7 +235,7 @@ namespace osu.Game.Screens.Ranking.Statistics protected override void Dispose(bool isDisposing) { - cancellationTokenSource?.Cancel(); + cancellationTokenSource.Cancel(); base.Dispose(isDisposing); } } From 237ae8b46a2c66af53a350e6972696d2cda79686 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 14 May 2024 13:09:57 +0200 Subject: [PATCH 0793/2493] Apply NRT to `SimpleStatisticsItem` --- osu.Game/Screens/Ranking/Statistics/SimpleStatisticItem.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Ranking/Statistics/SimpleStatisticItem.cs b/osu.Game/Screens/Ranking/Statistics/SimpleStatisticItem.cs index 23ccc3d0b7..d8de1b07b5 100644 --- a/osu.Game/Screens/Ranking/Statistics/SimpleStatisticItem.cs +++ b/osu.Game/Screens/Ranking/Statistics/SimpleStatisticItem.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.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Graphics; @@ -61,7 +59,7 @@ namespace osu.Game.Screens.Ranking.Statistics /// public partial class SimpleStatisticItem : SimpleStatisticItem { - private TValue value; + private TValue value = default!; /// /// The statistic's value to be displayed. @@ -80,7 +78,7 @@ namespace osu.Game.Screens.Ranking.Statistics /// Used to convert to a text representation. /// Defaults to using . /// - protected virtual string DisplayValue(TValue value) => value.ToString(); + protected virtual string DisplayValue(TValue value) => value!.ToString() ?? string.Empty; public SimpleStatisticItem(string name) : base(name) From 8e16b57d09663ecfd310487bb58db523b533e4b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 14 May 2024 13:10:36 +0200 Subject: [PATCH 0794/2493] Apply NRT to `SimpleStatisticTable` --- .../Screens/Ranking/Statistics/SimpleStatisticTable.cs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/Ranking/Statistics/SimpleStatisticTable.cs b/osu.Game/Screens/Ranking/Statistics/SimpleStatisticTable.cs index 4abf0007a7..da79fdb12b 100644 --- a/osu.Game/Screens/Ranking/Statistics/SimpleStatisticTable.cs +++ b/osu.Game/Screens/Ranking/Statistics/SimpleStatisticTable.cs @@ -1,12 +1,9 @@ // 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.Collections.Generic; using System.Linq; -using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; @@ -24,14 +21,14 @@ namespace osu.Game.Screens.Ranking.Statistics private readonly SimpleStatisticItem[] items; private readonly int columnCount; - private FillFlowContainer[] columns; + private FillFlowContainer[] columns = null!; /// /// Creates a statistic row for the supplied s. /// /// The number of columns to layout the into. /// The s to display in this row. - public SimpleStatisticTable(int columnCount, [ItemNotNull] IEnumerable items) + public SimpleStatisticTable(int columnCount, IEnumerable items) { ArgumentOutOfRangeException.ThrowIfNegativeOrZero(columnCount); From e7721b073cbd09651b48b9253e2d7d963339f5e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 14 May 2024 13:11:21 +0200 Subject: [PATCH 0795/2493] Apply NRT to `ContractedPanelTopContent` --- .../Screens/Ranking/Contracted/ContractedPanelTopContent.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/osu.Game/Screens/Ranking/Contracted/ContractedPanelTopContent.cs b/osu.Game/Screens/Ranking/Contracted/ContractedPanelTopContent.cs index 93bc7c41e1..06d127b972 100644 --- a/osu.Game/Screens/Ranking/Contracted/ContractedPanelTopContent.cs +++ b/osu.Game/Screens/Ranking/Contracted/ContractedPanelTopContent.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.Bindables; using osu.Framework.Graphics; @@ -16,7 +14,7 @@ namespace osu.Game.Screens.Ranking.Contracted { public readonly Bindable ScorePosition = new Bindable(); - private OsuSpriteText text; + private OsuSpriteText text = null!; public ContractedPanelTopContent() { From ced1c79490f61b6d203d3777c740139e1cd7ff51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 14 May 2024 13:14:52 +0200 Subject: [PATCH 0796/2493] Apply NRT to `AccuracyCircle` --- .../Expanded/Accuracy/AccuracyCircle.cs | 42 +++++++++---------- 1 file changed, 20 insertions(+), 22 deletions(-) diff --git a/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs b/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs index f04e4a6444..cebc54f490 100644 --- a/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs +++ b/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.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.Collections.Generic; using System.Linq; @@ -93,17 +91,17 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy private readonly ScoreInfo score; - private CircularProgress accuracyCircle; - private GradedCircles gradedCircles; - private Container badges; - private RankText rankText; + private CircularProgress accuracyCircle = null!; + private GradedCircles gradedCircles = null!; + private Container badges = null!; + private RankText rankText = null!; - private PoolableSkinnableSample scoreTickSound; - private PoolableSkinnableSample badgeTickSound; - private PoolableSkinnableSample badgeMaxSound; - private PoolableSkinnableSample swooshUpSound; - private PoolableSkinnableSample rankImpactSound; - private PoolableSkinnableSample rankApplauseSound; + private PoolableSkinnableSample? scoreTickSound; + private PoolableSkinnableSample? badgeTickSound; + private PoolableSkinnableSample? badgeMaxSound; + private PoolableSkinnableSample? swooshUpSound; + private PoolableSkinnableSample? rankImpactSound; + private PoolableSkinnableSample? rankApplauseSound; private readonly Bindable tickPlaybackRate = new Bindable(); @@ -119,7 +117,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy private readonly bool withFlair; private readonly bool isFailedSDueToMisses; - private RankText failedSRankText; + private RankText failedSRankText = null!; public AccuracyCircle(ScoreInfo score, bool withFlair = false) { @@ -229,8 +227,8 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy this.Delay(swoosh_pre_delay).Schedule(() => { - swooshUpSound.VolumeTo(swoosh_volume); - swooshUpSound.Play(); + swooshUpSound!.VolumeTo(swoosh_volume); + swooshUpSound!.Play(); }); } @@ -287,8 +285,8 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy this.TransformBindableTo(tickPlaybackRate, score_tick_debounce_rate_start); this.TransformBindableTo(tickPlaybackRate, score_tick_debounce_rate_end, ACCURACY_TRANSFORM_DURATION, Easing.OutSine); - scoreTickSound.FrequencyTo(1 + targetAccuracy, ACCURACY_TRANSFORM_DURATION, Easing.OutSine); - scoreTickSound.VolumeTo(score_tick_volume_start).Then().VolumeTo(score_tick_volume_end, ACCURACY_TRANSFORM_DURATION, Easing.OutSine); + scoreTickSound!.FrequencyTo(1 + targetAccuracy, ACCURACY_TRANSFORM_DURATION, Easing.OutSine); + scoreTickSound!.VolumeTo(score_tick_volume_start).Then().VolumeTo(score_tick_volume_end, ACCURACY_TRANSFORM_DURATION, Easing.OutSine); isTicking = true; }); @@ -314,8 +312,8 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy { var dink = badgeNum < badges.Count - 1 ? badgeTickSound : badgeMaxSound; - dink.FrequencyTo(1 + badgeNum++ * 0.05); - dink.Play(); + dink!.FrequencyTo(1 + badgeNum++ * 0.05); + dink!.Play(); }); } } @@ -331,7 +329,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy Schedule(() => { isTicking = false; - rankImpactSound.Play(); + rankImpactSound!.Play(); }); const double applause_pre_delay = 545f; @@ -341,8 +339,8 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy { Schedule(() => { - rankApplauseSound.VolumeTo(applause_volume); - rankApplauseSound.Play(); + rankApplauseSound!.VolumeTo(applause_volume); + rankApplauseSound!.Play(); }); } } From b937b94bd2feb31995cbb2b0e0c0cc8126195b77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 14 May 2024 13:15:21 +0200 Subject: [PATCH 0797/2493] Apply NRT to `RankBadge` --- osu.Game/Screens/Ranking/Expanded/Accuracy/RankBadge.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Ranking/Expanded/Accuracy/RankBadge.cs b/osu.Game/Screens/Ranking/Expanded/Accuracy/RankBadge.cs index 8aea6045eb..0e798c7d6e 100644 --- a/osu.Game/Screens/Ranking/Expanded/Accuracy/RankBadge.cs +++ b/osu.Game/Screens/Ranking/Expanded/Accuracy/RankBadge.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; @@ -34,8 +32,8 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy public readonly ScoreRank Rank; - private Drawable rankContainer; - private Drawable overlay; + private Drawable rankContainer = null!; + private Drawable overlay = null!; /// /// Creates a new . From 414f023817027d0433d99b921177e5f98b0ff2e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 14 May 2024 13:15:51 +0200 Subject: [PATCH 0798/2493] Apply NRT to `RankText` --- osu.Game/Screens/Ranking/Expanded/Accuracy/RankText.cs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/Ranking/Expanded/Accuracy/RankText.cs b/osu.Game/Screens/Ranking/Expanded/Accuracy/RankText.cs index b7adcb032f..76e59b32b8 100644 --- a/osu.Game/Screens/Ranking/Expanded/Accuracy/RankText.cs +++ b/osu.Game/Screens/Ranking/Expanded/Accuracy/RankText.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; @@ -23,9 +21,9 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy { private readonly ScoreRank rank; - private BufferedContainer flash; - private BufferedContainer superFlash; - private GlowingSpriteText rankText; + private BufferedContainer flash = null!; + private BufferedContainer superFlash = null!; + private GlowingSpriteText rankText = null!; public RankText(ScoreRank rank) { From ee9144c3bddd0b16b7bd18dd51222bfdadec5f18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 14 May 2024 13:17:31 +0200 Subject: [PATCH 0799/2493] Apply NRT to results statistics displays --- .../Ranking/Expanded/Statistics/AccuracyStatistic.cs | 4 +--- .../Screens/Ranking/Expanded/Statistics/ComboStatistic.cs | 4 +--- .../Screens/Ranking/Expanded/Statistics/CounterStatistic.cs | 4 +--- .../Ranking/Expanded/Statistics/PerformanceStatistic.cs | 6 ++---- .../Screens/Ranking/Expanded/Statistics/StatisticDisplay.cs | 6 ++---- 5 files changed, 7 insertions(+), 17 deletions(-) diff --git a/osu.Game/Screens/Ranking/Expanded/Statistics/AccuracyStatistic.cs b/osu.Game/Screens/Ranking/Expanded/Statistics/AccuracyStatistic.cs index f1f2c47e20..a4672a475c 100644 --- a/osu.Game/Screens/Ranking/Expanded/Statistics/AccuracyStatistic.cs +++ b/osu.Game/Screens/Ranking/Expanded/Statistics/AccuracyStatistic.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.Graphics; using osu.Framework.Localisation; using osu.Game.Graphics; @@ -22,7 +20,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics { private readonly double accuracy; - private RollingCounter counter; + private RollingCounter counter = null!; /// /// Creates a new . diff --git a/osu.Game/Screens/Ranking/Expanded/Statistics/ComboStatistic.cs b/osu.Game/Screens/Ranking/Expanded/Statistics/ComboStatistic.cs index 6290cee6da..7c91a37b77 100644 --- a/osu.Game/Screens/Ranking/Expanded/Statistics/ComboStatistic.cs +++ b/osu.Game/Screens/Ranking/Expanded/Statistics/ComboStatistic.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.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; @@ -22,7 +20,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics { private readonly bool isPerfect; - private Drawable perfectText; + private Drawable perfectText = null!; /// /// Creates a new . diff --git a/osu.Game/Screens/Ranking/Expanded/Statistics/CounterStatistic.cs b/osu.Game/Screens/Ranking/Expanded/Statistics/CounterStatistic.cs index 8528dac83b..4042724c75 100644 --- a/osu.Game/Screens/Ranking/Expanded/Statistics/CounterStatistic.cs +++ b/osu.Game/Screens/Ranking/Expanded/Statistics/CounterStatistic.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.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Localisation; @@ -21,7 +19,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics private readonly int count; private readonly int? maxCount; - private RollingCounter counter; + private RollingCounter counter = null!; /// /// Creates a new . diff --git a/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs b/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs index 8366f8d7ef..7ea3cbe917 100644 --- a/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs +++ b/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.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.Collections.Generic; using System.Linq; @@ -32,7 +30,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics private readonly CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(); - private RollingCounter counter; + private RollingCounter counter = null!; public PerformanceStatistic(ScoreInfo score) : base(BeatmapsetsStrings.ShowScoreboardHeaderspp) @@ -107,7 +105,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics protected override void Dispose(bool isDisposing) { - cancellationTokenSource?.Cancel(); + cancellationTokenSource.Cancel(); base.Dispose(isDisposing); } diff --git a/osu.Game/Screens/Ranking/Expanded/Statistics/StatisticDisplay.cs b/osu.Game/Screens/Ranking/Expanded/Statistics/StatisticDisplay.cs index 686b6c7d47..9de60f013d 100644 --- a/osu.Game/Screens/Ranking/Expanded/Statistics/StatisticDisplay.cs +++ b/osu.Game/Screens/Ranking/Expanded/Statistics/StatisticDisplay.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.Extensions.Color4Extensions; using osu.Framework.Extensions.LocalisationExtensions; @@ -21,10 +19,10 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics /// public abstract partial class StatisticDisplay : CompositeDrawable { - protected SpriteText HeaderText { get; private set; } + protected SpriteText HeaderText { get; private set; } = null!; private readonly LocalisableString header; - private Drawable content; + private Drawable content = null!; /// /// Creates a new . From 12e98fe55df37690f24de1450c1f59a9e75dd274 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 6 May 2024 15:24:32 +0800 Subject: [PATCH 0800/2493] Move out of playfield so touch overlay is not affected by playfield position --- .../UI/DrawableManiaRuleset.cs | 10 ++++++- osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs | 30 ++----------------- .../UI/ManiaTouchInputOverlay.cs | 5 ++-- 3 files changed, 15 insertions(+), 30 deletions(-) diff --git a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs index 275b1311de..a948117748 100644 --- a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs @@ -31,6 +31,7 @@ using osu.Game.Skinning; namespace osu.Game.Rulesets.Mania.UI { + [Cached] public partial class DrawableManiaRuleset : DrawableScrollingRuleset { /// @@ -43,7 +44,7 @@ namespace osu.Game.Rulesets.Mania.UI /// public const double MAX_TIME_RANGE = 11485; - protected new ManiaPlayfield Playfield => (ManiaPlayfield)base.Playfield; + public new ManiaPlayfield Playfield => (ManiaPlayfield)base.Playfield; public new ManiaBeatmap Beatmap => (ManiaBeatmap)base.Beatmap; @@ -103,6 +104,11 @@ namespace osu.Game.Rulesets.Mania.UI configScrollSpeed.BindValueChanged(speed => this.TransformTo(nameof(smoothTimeRange), ComputeScrollTime(speed.NewValue), 200, Easing.OutQuint)); TimeRange.Value = smoothTimeRange = ComputeScrollTime(configScrollSpeed.Value); + + KeyBindingInputManager.Add(touchOverlay = new ManiaTouchInputOverlay + { + RelativeSizeAxes = Axes.Both + }); } protected override void AdjustScrollSpeed(int amount) => configScrollSpeed.Value += amount; @@ -116,6 +122,8 @@ namespace osu.Game.Rulesets.Mania.UI private ScheduledDelegate? pendingSkinChange; private float hitPosition; + private ManiaTouchInputOverlay touchOverlay = null!; + private void onSkinChange() { // schedule required to avoid calls after disposed. diff --git a/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs b/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs index 385a47f8b8..b3420c49f3 100644 --- a/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs +++ b/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs @@ -7,12 +7,10 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using System; using System.Collections.Generic; -using System.Linq; using osu.Framework.Allocation; using osu.Framework.Graphics.Primitives; using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Objects; -using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.UI.Scrolling; @@ -27,8 +25,6 @@ namespace osu.Game.Rulesets.Mania.UI private readonly List stages = new List(); - private readonly ManiaTouchInputOverlay touchOverlay; - public override Quad SkinnableComponentScreenSpaceDrawQuad { get @@ -64,23 +60,10 @@ namespace osu.Game.Rulesets.Mania.UI throw new ArgumentException("Can't have zero or fewer stages."); GridContainer playfieldGrid; - - RelativeSizeAxes = Axes.Y; - AutoSizeAxes = Axes.X; - - AddRangeInternal(new Drawable[] + AddInternal(playfieldGrid = new GridContainer { - playfieldGrid = new GridContainer - { - RelativeSizeAxes = Axes.Y, - AutoSizeAxes = Axes.X, - Content = new[] { new Drawable[stageDefinitions.Count] }, - ColumnDimensions = Enumerable.Range(0, stageDefinitions.Count).Select(_ => new Dimension(GridSizeMode.AutoSize)).ToArray() - }, - touchOverlay = new ManiaTouchInputOverlay - { - RelativeSizeAxes = Axes.Both - } + RelativeSizeAxes = Axes.Both, + Content = new[] { new Drawable[stageDefinitions.Count] } }); var normalColumnAction = ManiaAction.Key1; @@ -100,13 +83,6 @@ namespace osu.Game.Rulesets.Mania.UI } } - protected override void LoadComplete() - { - base.LoadComplete(); - - touchOverlay.Alpha = Mods?.Any(m => m is ModTouchDevice) == true ? 1 : 0; - } - public override void Add(HitObject hitObject) => getStageByColumn(((ManiaHitObject)hitObject).Column).Add(hitObject); public override bool Remove(HitObject hitObject) => getStageByColumn(((ManiaHitObject)hitObject).Column).Remove(hitObject); diff --git a/osu.Game.Rulesets.Mania/UI/ManiaTouchInputOverlay.cs b/osu.Game.Rulesets.Mania/UI/ManiaTouchInputOverlay.cs index ddff064133..a51a3a605b 100644 --- a/osu.Game.Rulesets.Mania/UI/ManiaTouchInputOverlay.cs +++ b/osu.Game.Rulesets.Mania/UI/ManiaTouchInputOverlay.cs @@ -33,12 +33,13 @@ namespace osu.Game.Rulesets.Mania.UI }; [Resolved] - private ManiaPlayfield playfield { get; set; } = null!; + private DrawableManiaRuleset drawableRuleset { get; set; } = null!; public ManiaTouchInputOverlay() { Anchor = Anchor.BottomCentre; Origin = Anchor.BottomCentre; + RelativeSizeAxes = Axes.Both; Height = 0.5f; } @@ -51,7 +52,7 @@ namespace osu.Game.Rulesets.Mania.UI bool first = true; - foreach (var stage in playfield.Stages) + foreach (var stage in drawableRuleset.Playfield.Stages) { foreach (var column in stage.Columns) { From 390557634a76d457d3412d207efe68417bcd7773 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 14 May 2024 19:16:07 +0800 Subject: [PATCH 0801/2493] Rename touch area class to match existing usage (see taiko) --- osu.Game.Rulesets.Mania.Tests/Mods/TestSceneModTouchDevice.cs | 2 +- osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs | 4 ++-- .../UI/{ManiaTouchInputOverlay.cs => ManiaTouchInputArea.cs} | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) rename osu.Game.Rulesets.Mania/UI/{ManiaTouchInputOverlay.cs => ManiaTouchInputArea.cs} (97%) diff --git a/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneModTouchDevice.cs b/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneModTouchDevice.cs index 4c5e4933ef..829cd0b62e 100644 --- a/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneModTouchDevice.cs +++ b/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneModTouchDevice.cs @@ -59,6 +59,6 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods private Drawable? getSkinnableOverlay() => this.ChildrenOfType() .SingleOrDefault(d => d.Lookup.Equals(new ManiaSkinComponentLookup(ManiaSkinComponents.TouchOverlay))); - private ManiaTouchInputOverlay.InputReceptor getReceptor(int index) => this.ChildrenOfType().ElementAt(index); + private ManiaTouchInputArea.InputReceptor getReceptor(int index) => this.ChildrenOfType().ElementAt(index); } } diff --git a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs index a948117748..5974b76d65 100644 --- a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs @@ -105,7 +105,7 @@ namespace osu.Game.Rulesets.Mania.UI TimeRange.Value = smoothTimeRange = ComputeScrollTime(configScrollSpeed.Value); - KeyBindingInputManager.Add(touchOverlay = new ManiaTouchInputOverlay + KeyBindingInputManager.Add(touchArea = new ManiaTouchInputArea { RelativeSizeAxes = Axes.Both }); @@ -122,7 +122,7 @@ namespace osu.Game.Rulesets.Mania.UI private ScheduledDelegate? pendingSkinChange; private float hitPosition; - private ManiaTouchInputOverlay touchOverlay = null!; + private ManiaTouchInputArea touchArea = null!; private void onSkinChange() { diff --git a/osu.Game.Rulesets.Mania/UI/ManiaTouchInputOverlay.cs b/osu.Game.Rulesets.Mania/UI/ManiaTouchInputArea.cs similarity index 97% rename from osu.Game.Rulesets.Mania/UI/ManiaTouchInputOverlay.cs rename to osu.Game.Rulesets.Mania/UI/ManiaTouchInputArea.cs index a51a3a605b..0cb12128e8 100644 --- a/osu.Game.Rulesets.Mania/UI/ManiaTouchInputOverlay.cs +++ b/osu.Game.Rulesets.Mania/UI/ManiaTouchInputArea.cs @@ -14,7 +14,7 @@ using osuTK; namespace osu.Game.Rulesets.Mania.UI { - public partial class ManiaTouchInputOverlay : CompositeDrawable, ISerialisableDrawable + public partial class ManiaTouchInputArea : CompositeDrawable, ISerialisableDrawable { [SettingSource("Spacing", "The spacing between receptors.")] public BindableFloat Spacing { get; } = new BindableFloat(10) @@ -35,7 +35,7 @@ namespace osu.Game.Rulesets.Mania.UI [Resolved] private DrawableManiaRuleset drawableRuleset { get; set; } = null!; - public ManiaTouchInputOverlay() + public ManiaTouchInputArea() { Anchor = Anchor.BottomCentre; Origin = Anchor.BottomCentre; From 5c9a90cb40320cf2f22e4b6ba9f94a7ab0d4e632 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 14 May 2024 19:28:14 +0800 Subject: [PATCH 0802/2493] Tidy class and change to be a `VisibilityContainer` similar to taiko implementation --- .../Mods/TestSceneModTouchDevice.cs | 11 ++-- .../UI/DrawableManiaRuleset.cs | 7 +-- .../UI/ManiaTouchInputArea.cs | 54 +++++++++++++++---- 3 files changed, 50 insertions(+), 22 deletions(-) diff --git a/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneModTouchDevice.cs b/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneModTouchDevice.cs index 829cd0b62e..451cb617ee 100644 --- a/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneModTouchDevice.cs +++ b/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneModTouchDevice.cs @@ -3,12 +3,10 @@ using System.Linq; using NUnit.Framework; -using osu.Framework.Graphics; using osu.Framework.Input; using osu.Framework.Testing; using osu.Game.Rulesets.Mania.UI; using osu.Game.Rulesets.Mods; -using osu.Game.Skinning; using osu.Game.Tests.Visual; namespace osu.Game.Rulesets.Mania.Tests.Mods @@ -22,14 +20,14 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods { Mod = new ModTouchDevice(), Autoplay = false, - PassCondition = () => getSkinnableOverlay()?.IsPresent == true + PassCondition = () => getTouchOverlay()?.IsPresent == true }); [Test] public void TestOverlayNotVisibleWithoutMod() => CreateModTest(new ModTestData { Autoplay = false, - PassCondition = () => getSkinnableOverlay()?.IsPresent == false + PassCondition = () => getTouchOverlay()?.IsPresent == false }); [Test] @@ -56,9 +54,8 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods } } - private Drawable? getSkinnableOverlay() => this.ChildrenOfType() - .SingleOrDefault(d => d.Lookup.Equals(new ManiaSkinComponentLookup(ManiaSkinComponents.TouchOverlay))); + private ManiaTouchInputArea? getTouchOverlay() => this.ChildrenOfType().SingleOrDefault(); - private ManiaTouchInputArea.InputReceptor getReceptor(int index) => this.ChildrenOfType().ElementAt(index); + private ManiaTouchInputArea.ColumnInputReceptor getReceptor(int index) => this.ChildrenOfType().ElementAt(index); } } diff --git a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs index 5974b76d65..ce53862c76 100644 --- a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs @@ -105,10 +105,7 @@ namespace osu.Game.Rulesets.Mania.UI TimeRange.Value = smoothTimeRange = ComputeScrollTime(configScrollSpeed.Value); - KeyBindingInputManager.Add(touchArea = new ManiaTouchInputArea - { - RelativeSizeAxes = Axes.Both - }); + KeyBindingInputManager.Add(new ManiaTouchInputArea()); } protected override void AdjustScrollSpeed(int amount) => configScrollSpeed.Value += amount; @@ -122,8 +119,6 @@ namespace osu.Game.Rulesets.Mania.UI private ScheduledDelegate? pendingSkinChange; private float hitPosition; - private ManiaTouchInputArea touchArea = null!; - private void onSkinChange() { // schedule required to avoid calls after disposed. diff --git a/osu.Game.Rulesets.Mania/UI/ManiaTouchInputArea.cs b/osu.Game.Rulesets.Mania/UI/ManiaTouchInputArea.cs index 0cb12128e8..32e4616a25 100644 --- a/osu.Game.Rulesets.Mania/UI/ManiaTouchInputArea.cs +++ b/osu.Game.Rulesets.Mania/UI/ManiaTouchInputArea.cs @@ -9,13 +9,19 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; using osu.Game.Configuration; -using osu.Game.Skinning; using osuTK; namespace osu.Game.Rulesets.Mania.UI { - public partial class ManiaTouchInputArea : CompositeDrawable, ISerialisableDrawable + /// + /// An overlay that captures and displays osu!mania mouse and touch input. + /// + public partial class ManiaTouchInputArea : VisibilityContainer { + // visibility state affects our child. we always want to handle input. + public override bool PropagatePositionalInputSubTree => true; + public override bool PropagateNonPositionalInputSubTree => true; + [SettingSource("Spacing", "The spacing between receptors.")] public BindableFloat Spacing { get; } = new BindableFloat(10) { @@ -35,6 +41,8 @@ namespace osu.Game.Rulesets.Mania.UI [Resolved] private DrawableManiaRuleset drawableRuleset { get; set; } = null!; + private GridContainer gridContainer = null!; + public ManiaTouchInputArea() { Anchor = Anchor.BottomCentre; @@ -62,16 +70,17 @@ namespace osu.Game.Rulesets.Mania.UI receptorGridDimensions.Add(new Dimension(GridSizeMode.AutoSize)); } - receptorGridContent.Add(new InputReceptor { Action = { BindTarget = column.Action } }); + receptorGridContent.Add(new ColumnInputReceptor { Action = { BindTarget = column.Action } }); receptorGridDimensions.Add(new Dimension()); first = false; } } - InternalChild = new GridContainer + InternalChild = gridContainer = new GridContainer { RelativeSizeAxes = Axes.Both, + AlwaysPresent = true, Content = new[] { receptorGridContent.ToArray() }, ColumnDimensions = receptorGridDimensions.ToArray() }; @@ -83,9 +92,36 @@ namespace osu.Game.Rulesets.Mania.UI Opacity.BindValueChanged(o => Alpha = o.NewValue, true); } - public bool UsesFixedAnchor { get; set; } + protected override bool OnKeyDown(KeyDownEvent e) + { + // Hide whenever the keyboard is used. + Hide(); + return false; + } - public partial class InputReceptor : CompositeDrawable + protected override bool OnMouseDown(MouseDownEvent e) + { + Show(); + return true; + } + + protected override bool OnTouchDown(TouchDownEvent e) + { + Show(); + return true; + } + + protected override void PopIn() + { + gridContainer.FadeIn(500, Easing.OutQuint); + } + + protected override void PopOut() + { + gridContainer.FadeOut(300); + } + + public partial class ColumnInputReceptor : CompositeDrawable { public readonly IBindable Action = new Bindable(); @@ -96,7 +132,7 @@ namespace osu.Game.Rulesets.Mania.UI private bool isPressed; - public InputReceptor() + public ColumnInputReceptor() { RelativeSizeAxes = Axes.Both; @@ -128,7 +164,7 @@ namespace osu.Game.Rulesets.Mania.UI protected override bool OnTouchDown(TouchDownEvent e) { updateButton(true); - return true; + return false; // handled by parent container to show overlay. } protected override void OnTouchUp(TouchUpEvent e) @@ -139,7 +175,7 @@ namespace osu.Game.Rulesets.Mania.UI protected override bool OnMouseDown(MouseDownEvent e) { updateButton(true); - return true; + return false; // handled by parent container to show overlay. } protected override void OnMouseUp(MouseUpEvent e) From 636e2004711fe91474574a0acdc1ebd3519e8cc4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 14 May 2024 21:56:00 +0800 Subject: [PATCH 0803/2493] Update tests in line with new structure --- ...ice.cs => TestSceneManiaTouchInputArea.cs} | 30 ++++++------------- 1 file changed, 9 insertions(+), 21 deletions(-) rename osu.Game.Rulesets.Mania.Tests/{Mods/TestSceneModTouchDevice.cs => TestSceneManiaTouchInputArea.cs} (64%) diff --git a/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneModTouchDevice.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneManiaTouchInputArea.cs similarity index 64% rename from osu.Game.Rulesets.Mania.Tests/Mods/TestSceneModTouchDevice.cs rename to osu.Game.Rulesets.Mania.Tests/TestSceneManiaTouchInputArea.cs index 451cb617ee..30c0113bff 100644 --- a/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneModTouchDevice.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneManiaTouchInputArea.cs @@ -3,42 +3,28 @@ using System.Linq; using NUnit.Framework; +using osu.Framework.Graphics.Containers; using osu.Framework.Input; using osu.Framework.Testing; using osu.Game.Rulesets.Mania.UI; -using osu.Game.Rulesets.Mods; using osu.Game.Tests.Visual; -namespace osu.Game.Rulesets.Mania.Tests.Mods +namespace osu.Game.Rulesets.Mania.Tests { - public partial class TestSceneModTouchDevice : ModTestScene + public partial class TestSceneManiaTouchInputArea : PlayerTestScene { protected override Ruleset CreatePlayerRuleset() => new ManiaRuleset(); [Test] - public void TestOverlayVisibleWithMod() => CreateModTest(new ModTestData + public void TestTouchAreaNotInitiallyVisible() { - Mod = new ModTouchDevice(), - Autoplay = false, - PassCondition = () => getTouchOverlay()?.IsPresent == true - }); - - [Test] - public void TestOverlayNotVisibleWithoutMod() => CreateModTest(new ModTestData - { - Autoplay = false, - PassCondition = () => getTouchOverlay()?.IsPresent == false - }); + AddAssert("touch area not visible", () => getTouchOverlay()?.State.Value == Visibility.Hidden); + } [Test] public void TestPressReceptors() { - CreateModTest(new ModTestData - { - Mod = new ModTouchDevice(), - Autoplay = false, - PassCondition = () => true - }); + AddAssert("touch area not visible", () => getTouchOverlay()?.State.Value == Visibility.Hidden); for (int i = 0; i < 4; i++) { @@ -51,6 +37,8 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods () => Does.Contain(getReceptor(index).Action.Value)); AddStep($"release receptor {index}", () => InputManager.EndTouch(new Touch(TouchSource.Touch1, getReceptor(index).ScreenSpaceDrawQuad.Centre))); + + AddAssert("touch area visible", () => getTouchOverlay()?.State.Value == Visibility.Visible); } } From f781dc3300267bf99d57b5331aa4c3ed4a48d7a9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 14 May 2024 22:38:31 +0800 Subject: [PATCH 0804/2493] Remove touch mod addition to mania Feels a bit pointless? I dunno. --- osu.Game.Rulesets.Mania/ManiaRuleset.cs | 8 -------- 1 file changed, 8 deletions(-) diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs index 23004e36a0..b5614e2b56 100644 --- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs @@ -163,9 +163,6 @@ namespace osu.Game.Rulesets.Mania if (mods.HasFlagFast(LegacyMods.ScoreV2)) yield return new ModScoreV2(); - - if (mods.HasFlagFast(LegacyMods.TouchDevice)) - yield return new ModTouchDevice(); } public override LegacyMods ConvertToLegacyMods(Mod[] mods) @@ -228,10 +225,6 @@ namespace osu.Game.Rulesets.Mania case ManiaModRandom: value |= LegacyMods.Random; break; - - case ModTouchDevice: - value |= LegacyMods.TouchDevice; - break; } } @@ -303,7 +296,6 @@ namespace osu.Game.Rulesets.Mania case ModType.System: return new Mod[] { - new ModTouchDevice(), new ModScoreV2(), }; From cff865b556399e1b15206d4d080147d473277f84 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 14 May 2024 23:54:07 +0800 Subject: [PATCH 0805/2493] Continue loading even when osu! logo is being dragged at loading screen Closes https://github.com/ppy/osu/issues/28130. --- osu.Game/Screens/Play/PlayerLoader.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs index 4f7e21dddf..51a0c94ff0 100644 --- a/osu.Game/Screens/Play/PlayerLoader.cs +++ b/osu.Game/Screens/Play/PlayerLoader.cs @@ -113,7 +113,7 @@ namespace osu.Game.Screens.Play // not ready if the user is hovering one of the panes (logo is excluded), unless they are idle. (IsHovered || osuLogo?.IsHovered == true || idleTracker.IsIdle.Value) // not ready if the user is dragging a slider or otherwise. - && inputManager.DraggedDrawable == null + && (inputManager.DraggedDrawable == null || inputManager.DraggedDrawable is OsuLogo) // not ready if a focused overlay is visible, like settings. && inputManager.FocusedDrawable == null; From 3d190f7e88ff4beb32addcef83e20f2087de0061 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 14 May 2024 18:41:15 +0200 Subject: [PATCH 0806/2493] Remove redundant cast --- osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs index 967cdb0e54..c229039dc3 100644 --- a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs +++ b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs @@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.Mania.Edit { } - public new ManiaPlayfield Playfield => ((ManiaPlayfield)drawableRuleset.Playfield); + public new ManiaPlayfield Playfield => drawableRuleset.Playfield; public IScrollingInfo ScrollingInfo => drawableRuleset.ScrollingInfo; From a3960bf7155f6019dd552783b2266f7896df2d34 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 15 May 2024 14:17:28 +0800 Subject: [PATCH 0807/2493] Add inline comment explaining `LifetimeEnd` set for future visitors --- .../Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs b/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs index e70e181a50..7841e65935 100644 --- a/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs +++ b/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs @@ -240,6 +240,13 @@ namespace osu.Game.Rulesets.UI.Scrolling // always load the hitobject before its first judgement offset entry.LifetimeStart = Math.Min(entry.HitObject.StartTime - entry.HitObject.MaximumJudgementOffset, computedStartTime); + + // This is likely not entirely correct, but sets a sane expectation of the ending lifetime. + // A more correct lifetime will be overwritten after a DrawableHitObject is assigned via DrawableHitObject.updateState. + // + // It is required that we set a lifetime end here to ensure that in scenarios like loading a Player instance to a seeked + // location in a beatmap doesn't churn every hit object into a DrawableHitObject. Even in a pooled scenario, the overhead + // of this can be quite crippling. entry.LifetimeEnd = entry.HitObject.GetEndTime() + timeRange.Value; } From c4ac6d20a09b5704dd484b633142f517b527e2c2 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Wed, 15 May 2024 23:40:51 +0200 Subject: [PATCH 0808/2493] fix code quality --- .../Screens/Edit/Compose/Components/SelectionBoxScaleHandle.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionBoxScaleHandle.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionBoxScaleHandle.cs index a1f6a1732a..c188d23a58 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionBoxScaleHandle.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionBoxScaleHandle.cs @@ -12,9 +12,6 @@ namespace osu.Game.Screens.Edit.Compose.Components { public partial class SelectionBoxScaleHandle : SelectionBoxDragHandle { - [Resolved] - private SelectionBox selectionBox { get; set; } = null!; - [Resolved] private SelectionScaleHandler? scaleHandler { get; set; } From 21f5d891bb28a2edd835b4d4a2e69895b5ecf5dd Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 16 May 2024 04:36:14 +0300 Subject: [PATCH 0809/2493] Rename and move footer classes to appropriate places --- .../TestSceneScreenFooter.cs} | 25 ++++++++++--------- ....cs => TestSceneScreenFooterButtonMods.cs} | 16 ++++++------ .../FooterV2.cs => Footer/ScreenFooter.cs} | 12 ++++----- .../ScreenFooterButton.cs} | 6 ++--- .../Footer}/BeatmapOptionsPopover.cs | 7 +++--- .../Footer/ScreenFooterButtonMods.cs} | 5 ++-- .../Footer/ScreenFooterButtonOptions.cs} | 5 ++-- .../Footer/ScreenFooterButtonRandom.cs} | 5 ++-- 8 files changed, 43 insertions(+), 38 deletions(-) rename osu.Game.Tests/Visual/{SongSelect/TestSceneSongSelectFooterV2.cs => UserInterface/TestSceneScreenFooter.cs} (89%) rename osu.Game.Tests/Visual/UserInterface/{TestSceneFooterButtonModsV2.cs => TestSceneScreenFooterButtonMods.cs} (90%) rename osu.Game/Screens/{Select/FooterV2/FooterV2.cs => Footer/ScreenFooter.cs} (86%) rename osu.Game/Screens/{Select/FooterV2/FooterButtonV2.cs => Footer/ScreenFooterButton.cs} (97%) rename osu.Game/Screens/{Select/FooterV2 => SelectV2/Footer}/BeatmapOptionsPopover.cs (96%) rename osu.Game/Screens/{Select/FooterV2/FooterButtonModsV2.cs => SelectV2/Footer/ScreenFooterButtonMods.cs} (98%) rename osu.Game/Screens/{Select/FooterV2/FooterButtonOptionsV2.cs => SelectV2/Footer/ScreenFooterButtonOptions.cs} (83%) rename osu.Game/Screens/{Select/FooterV2/FooterButtonRandomV2.cs => SelectV2/Footer/ScreenFooterButtonRandom.cs} (97%) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneSongSelectFooterV2.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneScreenFooter.cs similarity index 89% rename from osu.Game.Tests/Visual/SongSelect/TestSceneSongSelectFooterV2.cs rename to osu.Game.Tests/Visual/UserInterface/TestSceneScreenFooter.cs index 93402e42ce..162609df70 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneSongSelectFooterV2.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneScreenFooter.cs @@ -15,15 +15,16 @@ using osu.Game.Overlays.Mods; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Mods; -using osu.Game.Screens.Select.FooterV2; +using osu.Game.Screens.Footer; +using osu.Game.Screens.SelectV2.Footer; using osuTK.Input; -namespace osu.Game.Tests.Visual.SongSelect +namespace osu.Game.Tests.Visual.UserInterface { - public partial class TestSceneSongSelectFooterV2 : OsuManualInputManagerTestScene + public partial class TestSceneScreenFooter : OsuManualInputManagerTestScene { - private FooterButtonRandomV2 randomButton = null!; - private FooterButtonModsV2 modsButton = null!; + private ScreenFooterButtonRandom randomButton = null!; + private ScreenFooterButtonMods modsButton = null!; private bool nextRandomCalled; private bool previousRandomCalled; @@ -39,25 +40,25 @@ namespace osu.Game.Tests.Visual.SongSelect nextRandomCalled = false; previousRandomCalled = false; - FooterV2 footer; + ScreenFooter footer; Children = new Drawable[] { new PopoverContainer { RelativeSizeAxes = Axes.Both, - Child = footer = new FooterV2(), + Child = footer = new ScreenFooter(), }, overlay = new DummyOverlay() }; - footer.AddButton(modsButton = new FooterButtonModsV2 { Current = SelectedMods }, overlay); - footer.AddButton(randomButton = new FooterButtonRandomV2 + footer.AddButton(modsButton = new ScreenFooterButtonMods { Current = SelectedMods }, overlay); + footer.AddButton(randomButton = new ScreenFooterButtonRandom { NextRandom = () => nextRandomCalled = true, PreviousRandom = () => previousRandomCalled = true }); - footer.AddButton(new FooterButtonOptionsV2()); + footer.AddButton(new ScreenFooterButtonOptions()); overlay.Hide(); }); @@ -98,7 +99,7 @@ namespace osu.Game.Tests.Visual.SongSelect { AddStep("enable options", () => { - var optionsButton = this.ChildrenOfType().Last(); + var optionsButton = this.ChildrenOfType().Last(); optionsButton.Enabled.Value = true; optionsButton.TriggerClick(); @@ -108,7 +109,7 @@ namespace osu.Game.Tests.Visual.SongSelect [Test] public void TestState() { - AddToggleStep("set options enabled state", state => this.ChildrenOfType().Last().Enabled.Value = state); + AddToggleStep("set options enabled state", state => this.ChildrenOfType().Last().Enabled.Value = state); } [Test] diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneFooterButtonModsV2.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneScreenFooterButtonMods.cs similarity index 90% rename from osu.Game.Tests/Visual/UserInterface/TestSceneFooterButtonModsV2.cs rename to osu.Game.Tests/Visual/UserInterface/TestSceneScreenFooterButtonMods.cs index 4aca9dde3d..df2109ace8 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneFooterButtonModsV2.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneScreenFooterButtonMods.cs @@ -12,21 +12,21 @@ using osu.Game.Graphics.Sprites; using osu.Game.Overlays; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.Mods; -using osu.Game.Screens.Select.FooterV2; +using osu.Game.Screens.SelectV2.Footer; using osu.Game.Utils; namespace osu.Game.Tests.Visual.UserInterface { - public partial class TestSceneFooterButtonModsV2 : OsuTestScene + public partial class TestSceneScreenFooterButtonMods : OsuTestScene { - private readonly TestFooterButtonModsV2 footerButtonMods; + private readonly TestScreenFooterButtonMods footerButtonMods; [Cached] private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine); - public TestSceneFooterButtonModsV2() + public TestSceneScreenFooterButtonMods() { - Add(footerButtonMods = new TestFooterButtonModsV2 + Add(footerButtonMods = new TestScreenFooterButtonMods { Anchor = Anchor.Centre, Origin = Anchor.CentreLeft, @@ -97,9 +97,9 @@ namespace osu.Game.Tests.Visual.UserInterface public void TestUnrankedBadge() { AddStep(@"Add unranked mod", () => changeMods(new[] { new OsuModDeflate() })); - AddUntilStep("Unranked badge shown", () => footerButtonMods.ChildrenOfType().Single().Alpha == 1); + AddUntilStep("Unranked badge shown", () => footerButtonMods.ChildrenOfType().Single().Alpha == 1); AddStep(@"Clear selected mod", () => changeMods(Array.Empty())); - AddUntilStep("Unranked badge not shown", () => footerButtonMods.ChildrenOfType().Single().Alpha == 0); + AddUntilStep("Unranked badge not shown", () => footerButtonMods.ChildrenOfType().Single().Alpha == 0); } private void changeMods(IReadOnlyList mods) => footerButtonMods.Current.Value = mods; @@ -112,7 +112,7 @@ namespace osu.Game.Tests.Visual.UserInterface return expectedValue == footerButtonMods.MultiplierText.Current.Value; } - private partial class TestFooterButtonModsV2 : FooterButtonModsV2 + private partial class TestScreenFooterButtonMods : ScreenFooterButtonMods { public new OsuSpriteText MultiplierText => base.MultiplierText; } diff --git a/osu.Game/Screens/Select/FooterV2/FooterV2.cs b/osu.Game/Screens/Footer/ScreenFooter.cs similarity index 86% rename from osu.Game/Screens/Select/FooterV2/FooterV2.cs rename to osu.Game/Screens/Footer/ScreenFooter.cs index 370c28e2a5..01013bb466 100644 --- a/osu.Game/Screens/Select/FooterV2/FooterV2.cs +++ b/osu.Game/Screens/Footer/ScreenFooter.cs @@ -11,9 +11,9 @@ using osu.Game.Graphics.UserInterface; using osu.Game.Overlays; using osuTK; -namespace osu.Game.Screens.Select.FooterV2 +namespace osu.Game.Screens.Footer { - public partial class FooterV2 : InputBlockingContainer + public partial class ScreenFooter : InputBlockingContainer { //Should be 60, setting to 50 for now for the sake of matching the current BackButton height. private const int height = 50; @@ -23,7 +23,7 @@ namespace osu.Game.Screens.Select.FooterV2 /// The button to be added. /// The to be toggled by this button. - public void AddButton(FooterButtonV2 button, OverlayContainer? overlay = null) + public void AddButton(ScreenFooterButton button, OverlayContainer? overlay = null) { if (overlay != null) { @@ -46,9 +46,9 @@ namespace osu.Game.Screens.Select.FooterV2 } } - private FillFlowContainer buttons = null!; + private FillFlowContainer buttons = null!; - public FooterV2() + public ScreenFooter() { RelativeSizeAxes = Axes.X; Height = height; @@ -66,7 +66,7 @@ namespace osu.Game.Screens.Select.FooterV2 RelativeSizeAxes = Axes.Both, Colour = colourProvider.Background5 }, - buttons = new FillFlowContainer + buttons = new FillFlowContainer { Position = new Vector2(TwoLayerButton.SIZE_EXTENDED.X + padding, 10), Anchor = Anchor.BottomLeft, diff --git a/osu.Game/Screens/Select/FooterV2/FooterButtonV2.cs b/osu.Game/Screens/Footer/ScreenFooterButton.cs similarity index 97% rename from osu.Game/Screens/Select/FooterV2/FooterButtonV2.cs rename to osu.Game/Screens/Footer/ScreenFooterButton.cs index 80103242f8..b3b3c9a8ec 100644 --- a/osu.Game/Screens/Select/FooterV2/FooterButtonV2.cs +++ b/osu.Game/Screens/Footer/ScreenFooterButton.cs @@ -21,9 +21,9 @@ using osu.Game.Overlays; using osuTK; using osuTK.Graphics; -namespace osu.Game.Screens.Select.FooterV2 +namespace osu.Game.Screens.Footer { - public partial class FooterButtonV2 : OsuClickableContainer, IKeyBindingHandler + public partial class ScreenFooterButton : OsuClickableContainer, IKeyBindingHandler { // This should be 12 by design, but an extra allowance is added due to the corner radius specification. private const float shear_width = 13.5f; @@ -70,7 +70,7 @@ namespace osu.Game.Screens.Select.FooterV2 private readonly Box glowBox; private readonly Box flashLayer; - public FooterButtonV2() + public ScreenFooterButton() { Size = new Vector2(BUTTON_WIDTH, BUTTON_HEIGHT); diff --git a/osu.Game/Screens/Select/FooterV2/BeatmapOptionsPopover.cs b/osu.Game/Screens/SelectV2/Footer/BeatmapOptionsPopover.cs similarity index 96% rename from osu.Game/Screens/Select/FooterV2/BeatmapOptionsPopover.cs rename to osu.Game/Screens/SelectV2/Footer/BeatmapOptionsPopover.cs index d98164c306..f73be15a36 100644 --- a/osu.Game/Screens/Select/FooterV2/BeatmapOptionsPopover.cs +++ b/osu.Game/Screens/SelectV2/Footer/BeatmapOptionsPopover.cs @@ -20,24 +20,25 @@ using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Localisation; using osu.Game.Overlays; +using osu.Game.Screens.Select; using osuTK; using osuTK.Graphics; using osuTK.Input; using WebCommonStrings = osu.Game.Resources.Localisation.Web.CommonStrings; -namespace osu.Game.Screens.Select.FooterV2 +namespace osu.Game.Screens.SelectV2.Footer { public partial class BeatmapOptionsPopover : OsuPopover { private FillFlowContainer buttonFlow = null!; - private readonly FooterButtonOptionsV2 footerButton; + private readonly ScreenFooterButtonOptions footerButton; private WorkingBeatmap beatmapWhenOpening = null!; [Resolved] private IBindable beatmap { get; set; } = null!; - public BeatmapOptionsPopover(FooterButtonOptionsV2 footerButton) + public BeatmapOptionsPopover(ScreenFooterButtonOptions footerButton) { this.footerButton = footerButton; } diff --git a/osu.Game/Screens/Select/FooterV2/FooterButtonModsV2.cs b/osu.Game/Screens/SelectV2/Footer/ScreenFooterButtonMods.cs similarity index 98% rename from osu.Game/Screens/Select/FooterV2/FooterButtonModsV2.cs rename to osu.Game/Screens/SelectV2/Footer/ScreenFooterButtonMods.cs index 44db49b927..49961c60f8 100644 --- a/osu.Game/Screens/Select/FooterV2/FooterButtonModsV2.cs +++ b/osu.Game/Screens/SelectV2/Footer/ScreenFooterButtonMods.cs @@ -21,14 +21,15 @@ using osu.Game.Graphics.Sprites; using osu.Game.Localisation; using osu.Game.Overlays; using osu.Game.Rulesets.Mods; +using osu.Game.Screens.Footer; using osu.Game.Screens.Play.HUD; using osu.Game.Utils; using osuTK; using osuTK.Graphics; -namespace osu.Game.Screens.Select.FooterV2 +namespace osu.Game.Screens.SelectV2.Footer { - public partial class FooterButtonModsV2 : FooterButtonV2, IHasCurrentValue> + public partial class ScreenFooterButtonMods : ScreenFooterButton, IHasCurrentValue> { // todo: see https://github.com/ppy/osu-framework/issues/3271 private const float torus_scale_factor = 1.2f; diff --git a/osu.Game/Screens/Select/FooterV2/FooterButtonOptionsV2.cs b/osu.Game/Screens/SelectV2/Footer/ScreenFooterButtonOptions.cs similarity index 83% rename from osu.Game/Screens/Select/FooterV2/FooterButtonOptionsV2.cs rename to osu.Game/Screens/SelectV2/Footer/ScreenFooterButtonOptions.cs index 555215056a..74fe3e3d11 100644 --- a/osu.Game/Screens/Select/FooterV2/FooterButtonOptionsV2.cs +++ b/osu.Game/Screens/SelectV2/Footer/ScreenFooterButtonOptions.cs @@ -8,10 +8,11 @@ using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.UserInterface; using osu.Game.Graphics; using osu.Game.Input.Bindings; +using osu.Game.Screens.Footer; -namespace osu.Game.Screens.Select.FooterV2 +namespace osu.Game.Screens.SelectV2.Footer { - public partial class FooterButtonOptionsV2 : FooterButtonV2, IHasPopover + public partial class ScreenFooterButtonOptions : ScreenFooterButton, IHasPopover { [BackgroundDependencyLoader] private void load(OsuColour colour) diff --git a/osu.Game/Screens/Select/FooterV2/FooterButtonRandomV2.cs b/osu.Game/Screens/SelectV2/Footer/ScreenFooterButtonRandom.cs similarity index 97% rename from osu.Game/Screens/Select/FooterV2/FooterButtonRandomV2.cs rename to osu.Game/Screens/SelectV2/Footer/ScreenFooterButtonRandom.cs index 70d1c0c19e..e8e850a9ce 100644 --- a/osu.Game/Screens/Select/FooterV2/FooterButtonRandomV2.cs +++ b/osu.Game/Screens/SelectV2/Footer/ScreenFooterButtonRandom.cs @@ -10,12 +10,13 @@ using osu.Framework.Input.Events; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Input.Bindings; +using osu.Game.Screens.Footer; using osuTK; using osuTK.Input; -namespace osu.Game.Screens.Select.FooterV2 +namespace osu.Game.Screens.SelectV2.Footer { - public partial class FooterButtonRandomV2 : FooterButtonV2 + public partial class ScreenFooterButtonRandom : ScreenFooterButton { public Action? NextRandom { get; set; } public Action? PreviousRandom { get; set; } From e3afd89dc879d6b21c41abc2df749729d314fedc Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 16 May 2024 04:49:33 +0300 Subject: [PATCH 0810/2493] Allow specifying height of `ShearedButton`s Also includes a test case in `TestSceneShearedButton`s to ensure the buttons' shear factors don't change on different heights (I've encountered issues with that previously). --- .../UserInterface/TestSceneShearedButtons.cs | 51 +++++++++++++++++-- .../Graphics/UserInterface/ShearedButton.cs | 9 ++-- .../Mods/ModFooterInformationDisplay.cs | 2 +- 3 files changed, 53 insertions(+), 9 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneShearedButtons.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneShearedButtons.cs index 118d32ee70..8db22f2d65 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneShearedButtons.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneShearedButtons.cs @@ -7,11 +7,13 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; using osu.Framework.Testing; using osu.Framework.Utils; using osu.Game.Graphics.UserInterface; using osu.Game.Overlays; +using osuTK; using osuTK.Input; namespace osu.Game.Tests.Visual.UserInterface @@ -35,7 +37,7 @@ namespace osu.Game.Tests.Visual.UserInterface if (bigButton) { - Child = button = new ShearedButton(400) + Child = button = new ShearedButton(400, 80) { LighterColour = Colour4.FromHex("#FFFFFF"), DarkerColour = Colour4.FromHex("#FFCC22"), @@ -44,13 +46,12 @@ namespace osu.Game.Tests.Visual.UserInterface Anchor = Anchor.Centre, Origin = Anchor.Centre, Text = "Let's GO!", - Height = 80, Action = () => actionFired = true, }; } else { - Child = button = new ShearedButton(200) + Child = button = new ShearedButton(200, 80) { LighterColour = Colour4.FromHex("#FF86DD"), DarkerColour = Colour4.FromHex("#DE31AE"), @@ -58,7 +59,6 @@ namespace osu.Game.Tests.Visual.UserInterface Anchor = Anchor.Centre, Origin = Anchor.Centre, Text = "Press me", - Height = 80, Action = () => actionFired = true, }; } @@ -171,5 +171,48 @@ namespace osu.Game.Tests.Visual.UserInterface void setToggleDisabledState(bool disabled) => AddStep($"{(disabled ? "disable" : "enable")} toggle", () => button.Active.Disabled = disabled); } + + [Test] + public void TestButtons() + { + AddStep("create buttons", () => Children = new[] + { + new FillFlowContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Direction = FillDirection.Horizontal, + AutoSizeAxes = Axes.Both, + Scale = new Vector2(2.5f), + Children = new Drawable[] + { + new ShearedButton(120) + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Text = "Test", + Action = () => { }, + Padding = new MarginPadding(), + }, + new ShearedButton(120, 40) + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Text = "Test", + Action = () => { }, + Padding = new MarginPadding { Left = -1f }, + }, + new ShearedButton(120, 70) + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Text = "Test", + Action = () => { }, + Padding = new MarginPadding { Left = 3f }, + }, + } + } + }); + } } } diff --git a/osu.Game/Graphics/UserInterface/ShearedButton.cs b/osu.Game/Graphics/UserInterface/ShearedButton.cs index b1e7066a01..caf1f76d88 100644 --- a/osu.Game/Graphics/UserInterface/ShearedButton.cs +++ b/osu.Game/Graphics/UserInterface/ShearedButton.cs @@ -17,7 +17,7 @@ namespace osu.Game.Graphics.UserInterface { public partial class ShearedButton : OsuClickableContainer { - public const float HEIGHT = 50; + public const float DEFAULT_HEIGHT = 50; public const float CORNER_RADIUS = 7; public const float BORDER_THICKNESS = 2; @@ -85,10 +85,11 @@ namespace osu.Game.Graphics.UserInterface /// If a value is provided (or the argument is omitted entirely), the button will autosize in width to fit the text. /// /// - public ShearedButton(float? width = null) + /// The height of the button. + public ShearedButton(float? width = null, float height = DEFAULT_HEIGHT) { - Height = HEIGHT; - Padding = new MarginPadding { Horizontal = shear * 50 }; + Height = height; + Padding = new MarginPadding { Horizontal = shear * height }; Content.CornerRadius = CORNER_RADIUS; Content.Shear = new Vector2(shear, 0); diff --git a/osu.Game/Overlays/Mods/ModFooterInformationDisplay.cs b/osu.Game/Overlays/Mods/ModFooterInformationDisplay.cs index 7fccf0cc13..8668879850 100644 --- a/osu.Game/Overlays/Mods/ModFooterInformationDisplay.cs +++ b/osu.Game/Overlays/Mods/ModFooterInformationDisplay.cs @@ -36,7 +36,7 @@ namespace osu.Game.Overlays.Mods Origin = Anchor.BottomRight, Anchor = Anchor.BottomRight, AutoSizeAxes = Axes.X, - Height = ShearedButton.HEIGHT, + Height = ShearedButton.DEFAULT_HEIGHT, Shear = new Vector2(ShearedOverlayContainer.SHEAR, 0), CornerRadius = ShearedButton.CORNER_RADIUS, BorderThickness = ShearedButton.BORDER_THICKNESS, From 266f0803624415acbf6bde6eb3a9b63f7ea9b83c Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 16 May 2024 05:01:49 +0300 Subject: [PATCH 0811/2493] Allow customising content of `ShearedButton`s --- .../Graphics/UserInterface/ShearedButton.cs | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/osu.Game/Graphics/UserInterface/ShearedButton.cs b/osu.Game/Graphics/UserInterface/ShearedButton.cs index caf1f76d88..0fd21502a1 100644 --- a/osu.Game/Graphics/UserInterface/ShearedButton.cs +++ b/osu.Game/Graphics/UserInterface/ShearedButton.cs @@ -75,6 +75,8 @@ namespace osu.Game.Graphics.UserInterface private readonly Container backgroundLayer; private readonly Box flashLayer; + protected readonly Container ButtonContent; + /// /// Creates a new /// @@ -110,12 +112,16 @@ namespace osu.Game.Graphics.UserInterface { RelativeSizeAxes = Axes.Both }, - text = new OsuSpriteText + ButtonContent = new Container { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Font = OsuFont.TorusAlternate.With(size: 17), - Shear = new Vector2(-shear, 0) + AutoSizeAxes = Axes.Both, + Shear = new Vector2(-shear, 0), + Child = text = new OsuSpriteText + { + Font = OsuFont.TorusAlternate.With(size: 17), + } }, } }, @@ -189,7 +195,7 @@ namespace osu.Game.Graphics.UserInterface { var colourDark = darkerColour ?? ColourProvider.Background3; var colourLight = lighterColour ?? ColourProvider.Background1; - var colourText = textColour ?? ColourProvider.Content1; + var colourContent = textColour ?? ColourProvider.Content1; if (!Enabled.Value) { @@ -206,9 +212,9 @@ namespace osu.Game.Graphics.UserInterface backgroundLayer.TransformTo(nameof(BorderColour), ColourInfo.GradientVertical(colourDark, colourLight), 150, Easing.OutQuint); if (!Enabled.Value) - colourText = colourText.Opacity(0.6f); + colourContent = colourContent.Opacity(0.6f); - text.FadeColour(colourText, 150, Easing.OutQuint); + ButtonContent.FadeColour(colourContent, 150, Easing.OutQuint); } } } From 7e8d5a5b0a1d0c58aab8de8afd6dc56094fd4f00 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 16 May 2024 04:50:51 +0300 Subject: [PATCH 0812/2493] Add new footer back button --- osu.Game/Screens/Footer/ScreenBackButton.cs | 64 +++++++++++++++++++++ osu.Game/Screens/Footer/ScreenFooter.cs | 17 ++++-- 2 files changed, 75 insertions(+), 6 deletions(-) create mode 100644 osu.Game/Screens/Footer/ScreenBackButton.cs diff --git a/osu.Game/Screens/Footer/ScreenBackButton.cs b/osu.Game/Screens/Footer/ScreenBackButton.cs new file mode 100644 index 0000000000..c5e613ea51 --- /dev/null +++ b/osu.Game/Screens/Footer/ScreenBackButton.cs @@ -0,0 +1,64 @@ +// 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.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osu.Game.Localisation; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Screens.Footer +{ + public partial class ScreenBackButton : ShearedButton + { + // todo: see https://github.com/ppy/osu-framework/issues/3271 + private const float torus_scale_factor = 1.2f; + + public const float BUTTON_WIDTH = 240; + + public ScreenBackButton() + : base(BUTTON_WIDTH, 70) + { + } + + [BackgroundDependencyLoader] + private void load() + { + ButtonContent.Child = new FillFlowContainer + { + X = -10f, + RelativeSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(20f, 0f), + Children = new Drawable[] + { + new SpriteIcon + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(20f), + Icon = FontAwesome.Solid.ChevronLeft, + }, + new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Font = OsuFont.TorusAlternate.With(size: 20 * torus_scale_factor), + Text = CommonStrings.Back, + UseFullGlyphHeight = false, + } + } + }; + + DarkerColour = Color4Extensions.FromHex("#DE31AE"); + LighterColour = Color4Extensions.FromHex("#FF86DD"); + TextColour = Color4.White; + } + } +} diff --git a/osu.Game/Screens/Footer/ScreenFooter.cs b/osu.Game/Screens/Footer/ScreenFooter.cs index 01013bb466..4a10a4cdab 100644 --- a/osu.Game/Screens/Footer/ScreenFooter.cs +++ b/osu.Game/Screens/Footer/ScreenFooter.cs @@ -7,7 +7,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Game.Graphics; -using osu.Game.Graphics.UserInterface; using osu.Game.Overlays; using osuTK; @@ -15,9 +14,8 @@ namespace osu.Game.Screens.Footer { public partial class ScreenFooter : InputBlockingContainer { - //Should be 60, setting to 50 for now for the sake of matching the current BackButton height. - private const int height = 50; - private const int padding = 80; + private const int height = 60; + private const int padding = 60; private readonly List overlays = new List(); @@ -68,13 +66,20 @@ namespace osu.Game.Screens.Footer }, buttons = new FillFlowContainer { - Position = new Vector2(TwoLayerButton.SIZE_EXTENDED.X + padding, 10), + Position = new Vector2(ScreenBackButton.BUTTON_WIDTH + padding, 10), Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, Direction = FillDirection.Horizontal, Spacing = new Vector2(7, 0), AutoSizeAxes = Axes.Both - } + }, + new ScreenBackButton + { + Margin = new MarginPadding { Bottom = 10f, Left = 12f }, + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Action = () => { }, + }, }; } } From 9446f45acf7da4bea4f042dc68a36e2c7d42db53 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 16 May 2024 05:03:34 +0300 Subject: [PATCH 0813/2493] Fix shear factor of `ScreenFooterButton`s not matching anything else When shear factors differ between components that are close to each other (`BackButtonV2` and `ScreenFooterButton` in this case), they look completely ugly. --- osu.Game/Screens/Footer/ScreenFooterButton.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Footer/ScreenFooterButton.cs b/osu.Game/Screens/Footer/ScreenFooterButton.cs index b3b3c9a8ec..3a23249ae0 100644 --- a/osu.Game/Screens/Footer/ScreenFooterButton.cs +++ b/osu.Game/Screens/Footer/ScreenFooterButton.cs @@ -25,8 +25,9 @@ namespace osu.Game.Screens.Footer { public partial class ScreenFooterButton : OsuClickableContainer, IKeyBindingHandler { - // This should be 12 by design, but an extra allowance is added due to the corner radius specification. - private const float shear_width = 13.5f; + // if we go by design, both this and ShearedButton should have shear factor as 1/7, + // but ShearedButton uses 1/5 so we must follow suit for the design to stay consistent. + private const float shear = 1f / 5f; protected const int CORNER_RADIUS = 10; protected const int BUTTON_HEIGHT = 90; @@ -34,7 +35,7 @@ namespace osu.Game.Screens.Footer public Bindable OverlayState = new Bindable(); - protected static readonly Vector2 BUTTON_SHEAR = new Vector2(shear_width / BUTTON_HEIGHT, 0); + protected static readonly Vector2 BUTTON_SHEAR = new Vector2(shear, 0); [Resolved] private OverlayColourProvider colourProvider { get; set; } = null!; From 7fce4cc4948803047de66133b07060a9991ff426 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 16 May 2024 05:30:19 +0300 Subject: [PATCH 0814/2493] Make `ScreenFooterButtonMods` share shear factor with base class --- .../SelectV2/Footer/ScreenFooterButtonMods.cs | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/osu.Game/Screens/SelectV2/Footer/ScreenFooterButtonMods.cs b/osu.Game/Screens/SelectV2/Footer/ScreenFooterButtonMods.cs index 49961c60f8..f590a19164 100644 --- a/osu.Game/Screens/SelectV2/Footer/ScreenFooterButtonMods.cs +++ b/osu.Game/Screens/SelectV2/Footer/ScreenFooterButtonMods.cs @@ -33,12 +33,9 @@ namespace osu.Game.Screens.SelectV2.Footer { // todo: see https://github.com/ppy/osu-framework/issues/3271 private const float torus_scale_factor = 1.2f; - private const float bar_shear_width = 7f; private const float bar_height = 37f; private const float mod_display_portion = 0.65f; - private static readonly Vector2 bar_shear = new Vector2(bar_shear_width / bar_height, 0); - private readonly BindableWithCurrent> current = new BindableWithCurrent>(Array.Empty()); public Bindable> Current @@ -77,7 +74,7 @@ namespace osu.Game.Screens.SelectV2.Footer Y = -5f, Depth = float.MaxValue, Origin = Anchor.BottomLeft, - Shear = bar_shear, + Shear = BUTTON_SHEAR, CornerRadius = CORNER_RADIUS, Size = new Vector2(BUTTON_WIDTH, bar_height), Masking = true, @@ -107,7 +104,7 @@ namespace osu.Game.Screens.SelectV2.Footer { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Shear = -bar_shear, + Shear = -BUTTON_SHEAR, UseFullGlyphHeight = false, Font = OsuFont.Torus.With(size: 14 * torus_scale_factor, weight: FontWeight.Bold) } @@ -129,7 +126,7 @@ namespace osu.Game.Screens.SelectV2.Footer { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Shear = -bar_shear, + Shear = -BUTTON_SHEAR, Scale = new Vector2(0.6f), Current = { BindTarget = Current }, ExpansionMode = ExpansionMode.AlwaysContracted, @@ -138,7 +135,7 @@ namespace osu.Game.Screens.SelectV2.Footer { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Shear = -bar_shear, + Shear = -BUTTON_SHEAR, Font = OsuFont.Torus.With(size: 14 * torus_scale_factor, weight: FontWeight.Bold), Mods = { BindTarget = Current }, } @@ -304,7 +301,7 @@ namespace osu.Game.Screens.SelectV2.Footer Y = -5f; Depth = float.MaxValue; Origin = Anchor.BottomLeft; - Shear = bar_shear; + Shear = BUTTON_SHEAR; CornerRadius = CORNER_RADIUS; AutoSizeAxes = Axes.X; Height = bar_height; @@ -328,7 +325,7 @@ namespace osu.Game.Screens.SelectV2.Footer { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Shear = -bar_shear, + Shear = -BUTTON_SHEAR, Text = ModSelectOverlayStrings.Unranked.ToUpper(), Margin = new MarginPadding { Horizontal = 15 }, UseFullGlyphHeight = false, From 9265290acfea0c3d746ede43426ae479708e340b Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 16 May 2024 05:30:35 +0300 Subject: [PATCH 0815/2493] Change shear factor everywhere to 0.15x --- osu.Game/Graphics/UserInterface/ShearedButton.cs | 3 ++- osu.Game/Overlays/Mods/ShearedOverlayContainer.cs | 3 ++- osu.Game/Screens/Footer/ScreenFooterButton.cs | 5 ++--- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/osu.Game/Graphics/UserInterface/ShearedButton.cs b/osu.Game/Graphics/UserInterface/ShearedButton.cs index 0fd21502a1..d546c18cb8 100644 --- a/osu.Game/Graphics/UserInterface/ShearedButton.cs +++ b/osu.Game/Graphics/UserInterface/ShearedButton.cs @@ -11,6 +11,7 @@ using osu.Framework.Localisation; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Overlays; +using osu.Game.Overlays.Mods; using osuTK; namespace osu.Game.Graphics.UserInterface @@ -66,7 +67,7 @@ namespace osu.Game.Graphics.UserInterface private readonly Box background; private readonly OsuSpriteText text; - private const float shear = 0.2f; + private const float shear = ShearedOverlayContainer.SHEAR; private Colour4? darkerColour; private Colour4? lighterColour; diff --git a/osu.Game/Overlays/Mods/ShearedOverlayContainer.cs b/osu.Game/Overlays/Mods/ShearedOverlayContainer.cs index a372ec70db..893ac89aa4 100644 --- a/osu.Game/Overlays/Mods/ShearedOverlayContainer.cs +++ b/osu.Game/Overlays/Mods/ShearedOverlayContainer.cs @@ -22,7 +22,8 @@ namespace osu.Game.Overlays.Mods { protected const float PADDING = 14; - public const float SHEAR = 0.2f; + // todo: maybe move this to a higher place since it's used for screen footer buttons etc. + public const float SHEAR = 0.15f; [Cached] protected readonly OverlayColourProvider ColourProvider; diff --git a/osu.Game/Screens/Footer/ScreenFooterButton.cs b/osu.Game/Screens/Footer/ScreenFooterButton.cs index 3a23249ae0..e0b019bfa8 100644 --- a/osu.Game/Screens/Footer/ScreenFooterButton.cs +++ b/osu.Game/Screens/Footer/ScreenFooterButton.cs @@ -18,6 +18,7 @@ using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Input.Bindings; using osu.Game.Overlays; +using osu.Game.Overlays.Mods; using osuTK; using osuTK.Graphics; @@ -25,9 +26,7 @@ namespace osu.Game.Screens.Footer { public partial class ScreenFooterButton : OsuClickableContainer, IKeyBindingHandler { - // if we go by design, both this and ShearedButton should have shear factor as 1/7, - // but ShearedButton uses 1/5 so we must follow suit for the design to stay consistent. - private const float shear = 1f / 5f; + private const float shear = ShearedOverlayContainer.SHEAR; protected const int CORNER_RADIUS = 10; protected const int BUTTON_HEIGHT = 90; From 310b4d90cc49fc8f26982bc0656285458d713d6c Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 16 May 2024 07:27:54 +0300 Subject: [PATCH 0816/2493] Move `SHEAR` constant to `OsuGame` and revert back to 0.2x (i.e. master) Discussed in [discord](https://discord.com/channels/188630481301012481/188630652340404224/1240490608934653984). --- osu.Game/Graphics/UserInterface/ShearedButton.cs | 2 +- osu.Game/Graphics/UserInterface/ShearedSearchTextBox.cs | 4 ++-- osu.Game/OsuGame.cs | 5 +++++ osu.Game/Overlays/Mods/BeatmapAttributesDisplay.cs | 2 +- osu.Game/Overlays/Mods/ModColumn.cs | 2 +- osu.Game/Overlays/Mods/ModFooterInformationDisplay.cs | 2 +- osu.Game/Overlays/Mods/ModPanel.cs | 2 +- osu.Game/Overlays/Mods/ModSelectColumn.cs | 6 +++--- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 4 ++-- osu.Game/Overlays/Mods/ModSelectPanel.cs | 8 ++++---- osu.Game/Overlays/Mods/RankingInformationDisplay.cs | 6 +++--- osu.Game/Overlays/Mods/ShearedOverlayContainer.cs | 3 --- osu.Game/Screens/Footer/ScreenFooterButton.cs | 2 +- 13 files changed, 25 insertions(+), 23 deletions(-) diff --git a/osu.Game/Graphics/UserInterface/ShearedButton.cs b/osu.Game/Graphics/UserInterface/ShearedButton.cs index d546c18cb8..7eed388707 100644 --- a/osu.Game/Graphics/UserInterface/ShearedButton.cs +++ b/osu.Game/Graphics/UserInterface/ShearedButton.cs @@ -67,7 +67,7 @@ namespace osu.Game.Graphics.UserInterface private readonly Box background; private readonly OsuSpriteText text; - private const float shear = ShearedOverlayContainer.SHEAR; + private const float shear = OsuGame.SHEAR; private Colour4? darkerColour; private Colour4? lighterColour; diff --git a/osu.Game/Graphics/UserInterface/ShearedSearchTextBox.cs b/osu.Game/Graphics/UserInterface/ShearedSearchTextBox.cs index c3a9f8a586..5f3716f36c 100644 --- a/osu.Game/Graphics/UserInterface/ShearedSearchTextBox.cs +++ b/osu.Game/Graphics/UserInterface/ShearedSearchTextBox.cs @@ -53,7 +53,7 @@ namespace osu.Game.Graphics.UserInterface public ShearedSearchTextBox() { Height = 42; - Shear = new Vector2(ShearedOverlayContainer.SHEAR, 0); + Shear = new Vector2(OsuGame.SHEAR, 0); Masking = true; CornerRadius = corner_radius; @@ -116,7 +116,7 @@ namespace osu.Game.Graphics.UserInterface PlaceholderText = CommonStrings.InputSearch; CornerRadius = corner_radius; - TextContainer.Shear = new Vector2(-ShearedOverlayContainer.SHEAR, 0); + TextContainer.Shear = new Vector2(-OsuGame.SHEAR, 0); } protected override SpriteText CreatePlaceholder() => new SearchPlaceholder(); diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 7c89314014..af01a1b1ac 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -91,6 +91,11 @@ namespace osu.Game /// protected const float SIDE_OVERLAY_OFFSET_RATIO = 0.05f; + /// + /// A common shear factor applied to most components of the game. + /// + public const float SHEAR = 0.2f; + public Toolbar Toolbar { get; private set; } private ChatOverlay chatOverlay; diff --git a/osu.Game/Overlays/Mods/BeatmapAttributesDisplay.cs b/osu.Game/Overlays/Mods/BeatmapAttributesDisplay.cs index c58cf710bd..5b10a2844e 100644 --- a/osu.Game/Overlays/Mods/BeatmapAttributesDisplay.cs +++ b/osu.Game/Overlays/Mods/BeatmapAttributesDisplay.cs @@ -66,7 +66,7 @@ namespace osu.Game.Overlays.Mods [BackgroundDependencyLoader] private void load() { - const float shear = ShearedOverlayContainer.SHEAR; + const float shear = OsuGame.SHEAR; LeftContent.AddRange(new Drawable[] { diff --git a/osu.Game/Overlays/Mods/ModColumn.cs b/osu.Game/Overlays/Mods/ModColumn.cs index e9f21338bd..326394a207 100644 --- a/osu.Game/Overlays/Mods/ModColumn.cs +++ b/osu.Game/Overlays/Mods/ModColumn.cs @@ -106,7 +106,7 @@ namespace osu.Game.Overlays.Mods Origin = Anchor.CentreLeft, Scale = new Vector2(0.8f), RelativeSizeAxes = Axes.X, - Shear = new Vector2(-ShearedOverlayContainer.SHEAR, 0) + Shear = new Vector2(-OsuGame.SHEAR, 0) }); ItemsFlow.Padding = new MarginPadding { diff --git a/osu.Game/Overlays/Mods/ModFooterInformationDisplay.cs b/osu.Game/Overlays/Mods/ModFooterInformationDisplay.cs index 8668879850..6665a3b8dc 100644 --- a/osu.Game/Overlays/Mods/ModFooterInformationDisplay.cs +++ b/osu.Game/Overlays/Mods/ModFooterInformationDisplay.cs @@ -37,7 +37,7 @@ namespace osu.Game.Overlays.Mods Anchor = Anchor.BottomRight, AutoSizeAxes = Axes.X, Height = ShearedButton.DEFAULT_HEIGHT, - Shear = new Vector2(ShearedOverlayContainer.SHEAR, 0), + Shear = new Vector2(OsuGame.SHEAR, 0), CornerRadius = ShearedButton.CORNER_RADIUS, BorderThickness = ShearedButton.BORDER_THICKNESS, Masking = true, diff --git a/osu.Game/Overlays/Mods/ModPanel.cs b/osu.Game/Overlays/Mods/ModPanel.cs index cf173b0d6a..9f87a704c0 100644 --- a/osu.Game/Overlays/Mods/ModPanel.cs +++ b/osu.Game/Overlays/Mods/ModPanel.cs @@ -36,7 +36,7 @@ namespace osu.Game.Overlays.Mods Anchor = Anchor.Centre, Origin = Anchor.Centre, Active = { BindTarget = Active }, - Shear = new Vector2(-ShearedOverlayContainer.SHEAR, 0), + Shear = new Vector2(-OsuGame.SHEAR, 0), Scale = new Vector2(HEIGHT / ModSwitchSmall.DEFAULT_SIZE) }; } diff --git a/osu.Game/Overlays/Mods/ModSelectColumn.cs b/osu.Game/Overlays/Mods/ModSelectColumn.cs index 61b29ef65b..5ffed24e7a 100644 --- a/osu.Game/Overlays/Mods/ModSelectColumn.cs +++ b/osu.Game/Overlays/Mods/ModSelectColumn.cs @@ -70,7 +70,7 @@ namespace osu.Game.Overlays.Mods { Width = WIDTH; RelativeSizeAxes = Axes.Y; - Shear = new Vector2(ShearedOverlayContainer.SHEAR, 0); + Shear = new Vector2(OsuGame.SHEAR, 0); InternalChildren = new Drawable[] { @@ -96,7 +96,7 @@ namespace osu.Game.Overlays.Mods { RelativeSizeAxes = Axes.X, Height = header_height, - Shear = new Vector2(-ShearedOverlayContainer.SHEAR, 0), + Shear = new Vector2(-OsuGame.SHEAR, 0), Velocity = 0.7f, ClampAxes = Axes.Y }, @@ -111,7 +111,7 @@ namespace osu.Game.Overlays.Mods AutoSizeAxes = Axes.Y, Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, - Shear = new Vector2(-ShearedOverlayContainer.SHEAR, 0), + Shear = new Vector2(-OsuGame.SHEAR, 0), Padding = new MarginPadding { Horizontal = 17, diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index 25293e8e20..f521b2e38c 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -227,7 +227,7 @@ namespace osu.Game.Overlays.Mods Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, Direction = FillDirection.Horizontal, - Shear = new Vector2(SHEAR, 0), + Shear = new Vector2(OsuGame.SHEAR, 0), RelativeSizeAxes = Axes.Y, AutoSizeAxes = Axes.X, Margin = new MarginPadding { Horizontal = 70 }, @@ -847,7 +847,7 @@ namespace osu.Game.Overlays.Mods // DrawWidth/DrawPosition do not include shear effects, and we want to know the full extents of the columns post-shear, // so we have to manually compensate. var topLeft = column.ToSpaceOfOtherDrawable(Vector2.Zero, ScrollContent); - var bottomRight = column.ToSpaceOfOtherDrawable(new Vector2(column.DrawWidth - column.DrawHeight * SHEAR, 0), ScrollContent); + var bottomRight = column.ToSpaceOfOtherDrawable(new Vector2(column.DrawWidth - column.DrawHeight * OsuGame.SHEAR, 0), ScrollContent); bool isCurrentlyVisible = Precision.AlmostBigger(topLeft.X, leftVisibleBound) && Precision.DefinitelyBigger(rightVisibleBound, bottomRight.X); diff --git a/osu.Game/Overlays/Mods/ModSelectPanel.cs b/osu.Game/Overlays/Mods/ModSelectPanel.cs index 29f4c93e88..284356f37e 100644 --- a/osu.Game/Overlays/Mods/ModSelectPanel.cs +++ b/osu.Game/Overlays/Mods/ModSelectPanel.cs @@ -87,7 +87,7 @@ namespace osu.Game.Overlays.Mods Content.CornerRadius = CORNER_RADIUS; Content.BorderThickness = 2; - Shear = new Vector2(ShearedOverlayContainer.SHEAR, 0); + Shear = new Vector2(OsuGame.SHEAR, 0); Children = new Drawable[] { @@ -128,10 +128,10 @@ namespace osu.Game.Overlays.Mods { Font = OsuFont.TorusAlternate.With(size: 18, weight: FontWeight.SemiBold), RelativeSizeAxes = Axes.X, - Shear = new Vector2(-ShearedOverlayContainer.SHEAR, 0), + Shear = new Vector2(-OsuGame.SHEAR, 0), Margin = new MarginPadding { - Left = -18 * ShearedOverlayContainer.SHEAR + Left = -18 * OsuGame.SHEAR }, ShowTooltip = false, // Tooltip is handled by `IncompatibilityDisplayingModPanel`. }, @@ -139,7 +139,7 @@ namespace osu.Game.Overlays.Mods { Font = OsuFont.Default.With(size: 12), RelativeSizeAxes = Axes.X, - Shear = new Vector2(-ShearedOverlayContainer.SHEAR, 0), + Shear = new Vector2(-OsuGame.SHEAR, 0), ShowTooltip = false, // Tooltip is handled by `IncompatibilityDisplayingModPanel`. } } diff --git a/osu.Game/Overlays/Mods/RankingInformationDisplay.cs b/osu.Game/Overlays/Mods/RankingInformationDisplay.cs index 494f8a377f..75a8f289d8 100644 --- a/osu.Game/Overlays/Mods/RankingInformationDisplay.cs +++ b/osu.Game/Overlays/Mods/RankingInformationDisplay.cs @@ -52,7 +52,7 @@ namespace osu.Game.Overlays.Mods Anchor = Anchor.BottomRight, Origin = Anchor.BottomRight, RelativeSizeAxes = Axes.Both, - Shear = new Vector2(ShearedOverlayContainer.SHEAR, 0), + Shear = new Vector2(OsuGame.SHEAR, 0), CornerRadius = ShearedButton.CORNER_RADIUS, Masking = true, Children = new Drawable[] @@ -79,7 +79,7 @@ namespace osu.Game.Overlays.Mods { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Shear = new Vector2(-ShearedOverlayContainer.SHEAR, 0), + Shear = new Vector2(-OsuGame.SHEAR, 0), Font = OsuFont.Default.With(size: 17, weight: FontWeight.SemiBold) } } @@ -94,7 +94,7 @@ namespace osu.Game.Overlays.Mods Origin = Anchor.Centre, Child = counter = new EffectCounter { - Shear = new Vector2(-ShearedOverlayContainer.SHEAR, 0), + Shear = new Vector2(-OsuGame.SHEAR, 0), Anchor = Anchor.Centre, Origin = Anchor.Centre, Current = { BindTarget = ModMultiplier } diff --git a/osu.Game/Overlays/Mods/ShearedOverlayContainer.cs b/osu.Game/Overlays/Mods/ShearedOverlayContainer.cs index 893ac89aa4..acdd1db728 100644 --- a/osu.Game/Overlays/Mods/ShearedOverlayContainer.cs +++ b/osu.Game/Overlays/Mods/ShearedOverlayContainer.cs @@ -22,9 +22,6 @@ namespace osu.Game.Overlays.Mods { protected const float PADDING = 14; - // todo: maybe move this to a higher place since it's used for screen footer buttons etc. - public const float SHEAR = 0.15f; - [Cached] protected readonly OverlayColourProvider ColourProvider; diff --git a/osu.Game/Screens/Footer/ScreenFooterButton.cs b/osu.Game/Screens/Footer/ScreenFooterButton.cs index e0b019bfa8..40e79ed474 100644 --- a/osu.Game/Screens/Footer/ScreenFooterButton.cs +++ b/osu.Game/Screens/Footer/ScreenFooterButton.cs @@ -26,7 +26,7 @@ namespace osu.Game.Screens.Footer { public partial class ScreenFooterButton : OsuClickableContainer, IKeyBindingHandler { - private const float shear = ShearedOverlayContainer.SHEAR; + private const float shear = OsuGame.SHEAR; protected const int CORNER_RADIUS = 10; protected const int BUTTON_HEIGHT = 90; From 820cfbcb005f759d1e0f1858781d762028ff08e2 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 16 May 2024 07:47:30 +0300 Subject: [PATCH 0817/2493] Remove unused using directives --- osu.Game/Graphics/UserInterface/ShearedButton.cs | 1 - osu.Game/Graphics/UserInterface/ShearedSearchTextBox.cs | 1 - osu.Game/Screens/Footer/ScreenFooterButton.cs | 1 - 3 files changed, 3 deletions(-) diff --git a/osu.Game/Graphics/UserInterface/ShearedButton.cs b/osu.Game/Graphics/UserInterface/ShearedButton.cs index 7eed388707..87d269ccd4 100644 --- a/osu.Game/Graphics/UserInterface/ShearedButton.cs +++ b/osu.Game/Graphics/UserInterface/ShearedButton.cs @@ -11,7 +11,6 @@ using osu.Framework.Localisation; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Overlays; -using osu.Game.Overlays.Mods; using osuTK; namespace osu.Game.Graphics.UserInterface diff --git a/osu.Game/Graphics/UserInterface/ShearedSearchTextBox.cs b/osu.Game/Graphics/UserInterface/ShearedSearchTextBox.cs index 5f3716f36c..c6565726b5 100644 --- a/osu.Game/Graphics/UserInterface/ShearedSearchTextBox.cs +++ b/osu.Game/Graphics/UserInterface/ShearedSearchTextBox.cs @@ -11,7 +11,6 @@ using osu.Framework.Graphics.UserInterface; using osu.Framework.Localisation; using osu.Game.Graphics.Sprites; using osu.Game.Overlays; -using osu.Game.Overlays.Mods; using osu.Game.Resources.Localisation.Web; using osuTK; diff --git a/osu.Game/Screens/Footer/ScreenFooterButton.cs b/osu.Game/Screens/Footer/ScreenFooterButton.cs index 40e79ed474..8fbd9f6cba 100644 --- a/osu.Game/Screens/Footer/ScreenFooterButton.cs +++ b/osu.Game/Screens/Footer/ScreenFooterButton.cs @@ -18,7 +18,6 @@ using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Input.Bindings; using osu.Game.Overlays; -using osu.Game.Overlays.Mods; using osuTK; using osuTK.Graphics; From a2794922d53a5db8f2c2748c535d2fc441b6621f Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 16 May 2024 06:23:07 +0300 Subject: [PATCH 0818/2493] Add `TopLevelContent` layer for applying external transforms on footer buttons --- .../TestSceneScreenFooterButtonMods.cs | 15 +- osu.Game/Screens/Footer/ScreenFooterButton.cs | 143 ++++++++++-------- .../SelectV2/Footer/ScreenFooterButtonMods.cs | 8 +- 3 files changed, 99 insertions(+), 67 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneScreenFooterButtonMods.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneScreenFooterButtonMods.cs index df2109ace8..2e2baf6e96 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneScreenFooterButtonMods.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneScreenFooterButtonMods.cs @@ -10,6 +10,7 @@ using osu.Framework.Graphics; using osu.Framework.Testing; using osu.Game.Graphics.Sprites; using osu.Game.Overlays; +using osu.Game.Overlays.Mods; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Screens.SelectV2.Footer; @@ -26,12 +27,12 @@ namespace osu.Game.Tests.Visual.UserInterface public TestSceneScreenFooterButtonMods() { - Add(footerButtonMods = new TestScreenFooterButtonMods + Add(footerButtonMods = new TestScreenFooterButtonMods(new TestModSelectOverlay()) { Anchor = Anchor.Centre, Origin = Anchor.CentreLeft, - X = -100, Action = () => { }, + X = -100, }); } @@ -112,9 +113,19 @@ namespace osu.Game.Tests.Visual.UserInterface return expectedValue == footerButtonMods.MultiplierText.Current.Value; } + private partial class TestModSelectOverlay : UserModSelectOverlay + { + protected override bool ShowPresets => true; + } + private partial class TestScreenFooterButtonMods : ScreenFooterButtonMods { public new OsuSpriteText MultiplierText => base.MultiplierText; + + public TestScreenFooterButtonMods(ModSelectOverlay overlay) + : base(overlay) + { + } } } } diff --git a/osu.Game/Screens/Footer/ScreenFooterButton.cs b/osu.Game/Screens/Footer/ScreenFooterButton.cs index 8fbd9f6cba..dda95d1d4c 100644 --- a/osu.Game/Screens/Footer/ScreenFooterButton.cs +++ b/osu.Game/Screens/Footer/ScreenFooterButton.cs @@ -55,7 +55,7 @@ namespace osu.Game.Screens.Footer set => icon.Icon = value; } - protected LocalisableString Text + public LocalisableString Text { set => text.Text = value; } @@ -69,87 +69,99 @@ namespace osu.Game.Screens.Footer private readonly Box glowBox; private readonly Box flashLayer; - public ScreenFooterButton() + public readonly Container TopLevelContent; + public readonly OverlayContainer? Overlay; + + public ScreenFooterButton(OverlayContainer? overlay = null) { + Overlay = overlay; + Size = new Vector2(BUTTON_WIDTH, BUTTON_HEIGHT); - Child = new Container + Child = TopLevelContent = new Container { - EdgeEffect = new EdgeEffectParameters - { - Type = EdgeEffectType.Shadow, - Radius = 4, - // Figma says 50% opacity, but it does not match up visually if taken at face value, and looks bad. - Colour = Colour4.Black.Opacity(0.25f), - Offset = new Vector2(0, 2), - }, - Shear = BUTTON_SHEAR, - Masking = true, - CornerRadius = CORNER_RADIUS, RelativeSizeAxes = Axes.Both, Children = new Drawable[] { - backgroundBox = new Box - { - RelativeSizeAxes = Axes.Both - }, - glowBox = new Box - { - RelativeSizeAxes = Axes.Both - }, - // For elements that should not be sheared. new Container { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Shear = -BUTTON_SHEAR, + EdgeEffect = new EdgeEffectParameters + { + Type = EdgeEffectType.Shadow, + Radius = 4, + // Figma says 50% opacity, but it does not match up visually if taken at face value, and looks bad. + Colour = Colour4.Black.Opacity(0.25f), + Offset = new Vector2(0, 2), + }, + Shear = BUTTON_SHEAR, + Masking = true, + CornerRadius = CORNER_RADIUS, RelativeSizeAxes = Axes.Both, Children = new Drawable[] { - TextContainer = new Container + backgroundBox = new Box { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - Y = 42, - AutoSizeAxes = Axes.Both, - Child = text = new OsuSpriteText + RelativeSizeAxes = Axes.Both + }, + glowBox = new Box + { + RelativeSizeAxes = Axes.Both + }, + // For elements that should not be sheared. + new Container + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Shear = -BUTTON_SHEAR, + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] { - // figma design says the size is 16, but due to the issues with font sizes 19 matches better - Font = OsuFont.TorusAlternate.With(size: 19), - AlwaysPresent = true + TextContainer = new Container + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Y = 42, + AutoSizeAxes = Axes.Both, + Child = text = new OsuSpriteText + { + // figma design says the size is 16, but due to the issues with font sizes 19 matches better + Font = OsuFont.TorusAlternate.With(size: 19), + AlwaysPresent = true + } + }, + icon = new SpriteIcon + { + Y = 12, + Size = new Vector2(20), + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre + }, } }, - icon = new SpriteIcon + new Container { - Y = 12, - Size = new Vector2(20), - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre + Shear = -BUTTON_SHEAR, + Anchor = Anchor.BottomCentre, + Origin = Anchor.Centre, + Y = -CORNER_RADIUS, + Size = new Vector2(120, 6), + Masking = true, + CornerRadius = 3, + Child = bar = new Box + { + RelativeSizeAxes = Axes.Both, + } }, - } - }, - new Container - { - Shear = -BUTTON_SHEAR, - Anchor = Anchor.BottomCentre, - Origin = Anchor.Centre, - Y = -CORNER_RADIUS, - Size = new Vector2(120, 6), - Masking = true, - CornerRadius = 3, - Child = bar = new Box - { - RelativeSizeAxes = Axes.Both, - } - }, - flashLayer = new Box - { - RelativeSizeAxes = Axes.Both, - Colour = Colour4.White.Opacity(0.9f), - Blending = BlendingParameters.Additive, - Alpha = 0, - }, - }, + flashLayer = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Colour4.White.Opacity(0.9f), + Blending = BlendingParameters.Additive, + Alpha = 0, + }, + }, + } + } }; } @@ -157,6 +169,9 @@ namespace osu.Game.Screens.Footer { base.LoadComplete(); + if (Overlay != null) + OverlayState.BindTo(Overlay.State); + OverlayState.BindValueChanged(_ => updateDisplay()); Enabled.BindValueChanged(_ => updateDisplay(), true); diff --git a/osu.Game/Screens/SelectV2/Footer/ScreenFooterButtonMods.cs b/osu.Game/Screens/SelectV2/Footer/ScreenFooterButtonMods.cs index f590a19164..4df4116de1 100644 --- a/osu.Game/Screens/SelectV2/Footer/ScreenFooterButtonMods.cs +++ b/osu.Game/Screens/SelectV2/Footer/ScreenFooterButtonMods.cs @@ -20,6 +20,7 @@ using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Localisation; using osu.Game.Overlays; +using osu.Game.Overlays.Mods; using osu.Game.Rulesets.Mods; using osu.Game.Screens.Footer; using osu.Game.Screens.Play.HUD; @@ -59,6 +60,11 @@ namespace osu.Game.Screens.SelectV2.Footer [Resolved] private OverlayColourProvider colourProvider { get; set; } = null!; + public ScreenFooterButtonMods(ModSelectOverlay overlay) + : base(overlay) + { + } + [BackgroundDependencyLoader] private void load() { @@ -66,7 +72,7 @@ namespace osu.Game.Screens.SelectV2.Footer Icon = FontAwesome.Solid.ExchangeAlt; AccentColour = colours.Lime1; - AddRange(new[] + TopLevelContent.AddRange(new[] { unrankedBadge = new UnrankedBadge(), modDisplayBar = new Container From d0e8632fbffef00b75cc227e1435c03ff348c467 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 16 May 2024 06:24:58 +0300 Subject: [PATCH 0819/2493] Refactor `ScreenFooter` to prepare for global usage and add transitions --- osu.Game/Screens/Footer/ScreenFooter.cs | 162 +++++++++++++++++++----- 1 file changed, 130 insertions(+), 32 deletions(-) diff --git a/osu.Game/Screens/Footer/ScreenFooter.cs b/osu.Game/Screens/Footer/ScreenFooter.cs index 4a10a4cdab..d299bf7362 100644 --- a/osu.Game/Screens/Footer/ScreenFooter.cs +++ b/osu.Game/Screens/Footer/ScreenFooter.cs @@ -2,54 +2,33 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; -using osu.Game.Graphics; using osu.Game.Overlays; using osuTK; namespace osu.Game.Screens.Footer { - public partial class ScreenFooter : InputBlockingContainer + public partial class ScreenFooter : OverlayContainer { - private const int height = 60; private const int padding = 60; + private const float delay_per_button = 30; + + public const int HEIGHT = 60; private readonly List overlays = new List(); - /// The button to be added. - /// The to be toggled by this button. - public void AddButton(ScreenFooterButton button, OverlayContainer? overlay = null) - { - if (overlay != null) - { - overlays.Add(overlay); - button.Action = () => showOverlay(overlay); - button.OverlayState.BindTo(overlay.State); - } - - buttons.Add(button); - } - - private void showOverlay(OverlayContainer overlay) - { - foreach (var o in overlays) - { - if (o == overlay) - o.ToggleVisibility(); - else - o.Hide(); - } - } - - private FillFlowContainer buttons = null!; + private FillFlowContainer buttonsFlow = null!; + private Container removedButtonsContainer = null!; public ScreenFooter() { RelativeSizeAxes = Axes.X; - Height = height; + Height = HEIGHT; Anchor = Anchor.BottomLeft; Origin = Anchor.BottomLeft; } @@ -64,9 +43,10 @@ namespace osu.Game.Screens.Footer RelativeSizeAxes = Axes.Both, Colour = colourProvider.Background5 }, - buttons = new FillFlowContainer + buttonsFlow = new FillFlowContainer { - Position = new Vector2(ScreenBackButton.BUTTON_WIDTH + padding, 10), + Margin = new MarginPadding { Left = 12f + ScreenBackButton.BUTTON_WIDTH + padding }, + Y = 10f, Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, Direction = FillDirection.Horizontal, @@ -80,7 +60,125 @@ namespace osu.Game.Screens.Footer Origin = Anchor.BottomLeft, Action = () => { }, }, + removedButtonsContainer = new Container + { + Margin = new MarginPadding { Left = 12f + ScreenBackButton.BUTTON_WIDTH + padding }, + Y = 10f, + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + AutoSizeAxes = Axes.Both, + }, }; } + + protected override void PopIn() + { + this.MoveToY(0, 400, Easing.OutQuint) + .FadeIn(400, Easing.OutQuint); + } + + protected override void PopOut() + { + this.MoveToY(HEIGHT, 400, Easing.OutQuint) + .FadeOut(400, Easing.OutQuint); + } + + public void SetButtons(IReadOnlyList buttons) + { + overlays.Clear(); + + var oldButtons = buttonsFlow.ToArray(); + + for (int i = 0; i < oldButtons.Length; i++) + { + var oldButton = oldButtons[i]; + + buttonsFlow.Remove(oldButton, false); + removedButtonsContainer.Add(oldButton); + + if (buttons.Count > 0) + fadeButtonToLeft(oldButton, i, oldButtons.Length); + else + fadeButtonToBottom(oldButton, i, oldButtons.Length); + + Scheduler.AddDelayed(() => oldButton.Expire(), oldButton.TopLevelContent.LatestTransformEndTime - Time.Current); + } + + for (int i = 0; i < buttons.Count; i++) + { + var newButton = buttons[i]; + + if (newButton.Overlay != null) + { + newButton.Action = () => showOverlay(newButton.Overlay); + overlays.Add(newButton.Overlay); + } + + Debug.Assert(!newButton.IsLoaded); + buttonsFlow.Add(newButton); + + int index = i; + + // ensure transforms are added after LoadComplete to not be aborted by the FinishTransforms call. + newButton.OnLoadComplete += _ => + { + if (oldButtons.Length > 0) + fadeButtonFromRight(newButton, index, buttons.Count, 240); + else + fadeButtonFromBottom(newButton, index); + }; + } + } + + private void fadeButtonFromRight(ScreenFooterButton button, int index, int count, float startDelay) + { + button.TopLevelContent + .MoveToX(-300f) + .FadeOut(); + + button.TopLevelContent + .Delay(startDelay + (count - index) * delay_per_button) + .MoveToX(0f, 240, Easing.OutCubic) + .FadeIn(240, Easing.OutCubic); + } + + private void fadeButtonFromBottom(ScreenFooterButton button, int index) + { + button.TopLevelContent + .MoveToY(100f) + .FadeOut(); + + button.TopLevelContent + .Delay(index * delay_per_button) + .MoveToY(0f, 240, Easing.OutCubic) + .FadeIn(240, Easing.OutCubic); + } + + private void fadeButtonToLeft(ScreenFooterButton button, int index, int count) + { + button.TopLevelContent + .Delay((count - index) * delay_per_button) + .FadeOut(240, Easing.InOutCubic) + .MoveToX(300f, 360, Easing.InOutCubic); + } + + private void fadeButtonToBottom(ScreenFooterButton button, int index, int count) + { + button.TopLevelContent + .Delay((count - index) * delay_per_button) + .FadeOut(240, Easing.InOutCubic) + .MoveToY(100f, 240, Easing.InOutCubic); + } + + private void showOverlay(OverlayContainer overlay) + { + foreach (var o in overlays) + { + if (o == overlay) + o.ToggleVisibility(); + else + o.Hide(); + } + } } } From 95840672cb945b5f7a3ca3039b0dea8138d7ddcc Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 16 May 2024 06:26:59 +0300 Subject: [PATCH 0820/2493] Clean up screen footer test scene --- .../UserInterface/TestSceneScreenFooter.cs | 180 ++++-------------- 1 file changed, 39 insertions(+), 141 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneScreenFooter.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneScreenFooter.cs index 162609df70..01b3aa5787 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneScreenFooter.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneScreenFooter.cs @@ -2,34 +2,22 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Collections.Generic; -using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; using osu.Framework.Testing; using osu.Game.Overlays; using osu.Game.Overlays.Mods; -using osu.Game.Rulesets.Mods; -using osu.Game.Rulesets.Osu; -using osu.Game.Rulesets.Osu.Mods; using osu.Game.Screens.Footer; using osu.Game.Screens.SelectV2.Footer; -using osuTK.Input; namespace osu.Game.Tests.Visual.UserInterface { public partial class TestSceneScreenFooter : OsuManualInputManagerTestScene { - private ScreenFooterButtonRandom randomButton = null!; - private ScreenFooterButtonMods modsButton = null!; - - private bool nextRandomCalled; - private bool previousRandomCalled; - - private DummyOverlay overlay = null!; + private ScreenFooter screenFooter = null!; + private TestModSelectOverlay overlay = null!; [Cached] private OverlayColourProvider colourProvider { get; set; } = new OverlayColourProvider(OverlayColourScheme.Aquamarine); @@ -37,160 +25,70 @@ namespace osu.Game.Tests.Visual.UserInterface [SetUp] public void SetUp() => Schedule(() => { - nextRandomCalled = false; - previousRandomCalled = false; - - ScreenFooter footer; - Children = new Drawable[] { + overlay = new TestModSelectOverlay + { + Padding = new MarginPadding + { + Bottom = ScreenFooter.HEIGHT + } + }, new PopoverContainer { RelativeSizeAxes = Axes.Both, - Child = footer = new ScreenFooter(), + Child = screenFooter = new ScreenFooter(), }, - overlay = new DummyOverlay() }; - footer.AddButton(modsButton = new ScreenFooterButtonMods { Current = SelectedMods }, overlay); - footer.AddButton(randomButton = new ScreenFooterButtonRandom + screenFooter.SetButtons(new ScreenFooterButton[] { - NextRandom = () => nextRandomCalled = true, - PreviousRandom = () => previousRandomCalled = true + new ScreenFooterButtonMods(overlay) { Current = SelectedMods }, + new ScreenFooterButtonRandom(), + new ScreenFooterButtonOptions(), }); - footer.AddButton(new ScreenFooterButtonOptions()); - - overlay.Hide(); }); [SetUpSteps] public void SetUpSteps() { - AddStep("clear mods", () => SelectedMods.Value = Array.Empty()); - AddStep("set beatmap", () => Beatmap.Value = CreateWorkingBeatmap(CreateBeatmap(new OsuRuleset().RulesetInfo))); + AddStep("show footer", () => screenFooter.Show()); } + /// + /// Transition when moving from a screen with no buttons to a screen with buttons. + /// [Test] - public void TestMods() + public void TestButtonsIn() { - AddStep("one mod", () => SelectedMods.Value = new List { new OsuModHidden() }); - AddStep("two mods", () => SelectedMods.Value = new List { new OsuModHidden(), new OsuModHardRock() }); - AddStep("three mods", () => SelectedMods.Value = new List { new OsuModHidden(), new OsuModHardRock(), new OsuModDoubleTime() }); - AddStep("four mods", () => SelectedMods.Value = new List { new OsuModHidden(), new OsuModHardRock(), new OsuModDoubleTime(), new OsuModClassic() }); - AddStep("five mods", () => SelectedMods.Value = new List { new OsuModHidden(), new OsuModHardRock(), new OsuModDoubleTime(), new OsuModClassic(), new OsuModDifficultyAdjust() }); - - AddStep("modified", () => SelectedMods.Value = new List { new OsuModDoubleTime { SpeedChange = { Value = 1.2 } } }); - AddStep("modified + one", () => SelectedMods.Value = new List { new OsuModHidden(), new OsuModDoubleTime { SpeedChange = { Value = 1.2 } } }); - AddStep("modified + two", () => SelectedMods.Value = new List { new OsuModHidden(), new OsuModHardRock(), new OsuModDoubleTime { SpeedChange = { Value = 1.2 } } }); - AddStep("modified + three", () => SelectedMods.Value = new List { new OsuModHidden(), new OsuModHardRock(), new OsuModClassic(), new OsuModDoubleTime { SpeedChange = { Value = 1.2 } } }); - AddStep("modified + four", () => SelectedMods.Value = new List { new OsuModHidden(), new OsuModHardRock(), new OsuModClassic(), new OsuModDifficultyAdjust(), new OsuModDoubleTime { SpeedChange = { Value = 1.2 } } }); - - AddStep("clear mods", () => SelectedMods.Value = Array.Empty()); - AddWaitStep("wait", 3); - AddStep("one mod", () => SelectedMods.Value = new List { new OsuModHidden() }); - - AddStep("clear mods", () => SelectedMods.Value = Array.Empty()); - AddWaitStep("wait", 3); - AddStep("five mods", () => SelectedMods.Value = new List { new OsuModHidden(), new OsuModHardRock(), new OsuModDoubleTime(), new OsuModClassic(), new OsuModDifficultyAdjust() }); } + /// + /// Transition when moving from a screen with buttons to a screen with no buttons. + /// [Test] - public void TestShowOptions() + public void TestButtonsOut() { - AddStep("enable options", () => + AddStep("clear buttons", () => screenFooter.SetButtons(Array.Empty())); + } + + /// + /// Transition when moving from a screen with buttons to a screen with buttons. + /// + [Test] + public void TestReplaceButtons() + { + AddStep("replace buttons", () => screenFooter.SetButtons(new[] { - var optionsButton = this.ChildrenOfType().Last(); - - optionsButton.Enabled.Value = true; - optionsButton.TriggerClick(); - }); + new ScreenFooterButton { Text = "One", Action = () => { } }, + new ScreenFooterButton { Text = "Two", Action = () => { } }, + new ScreenFooterButton { Text = "Three", Action = () => { } }, + })); } - [Test] - public void TestState() + private partial class TestModSelectOverlay : UserModSelectOverlay { - AddToggleStep("set options enabled state", state => this.ChildrenOfType().Last().Enabled.Value = state); - } - - [Test] - public void TestFooterRandom() - { - AddStep("press F2", () => InputManager.Key(Key.F2)); - AddAssert("next random invoked", () => nextRandomCalled && !previousRandomCalled); - } - - [Test] - public void TestFooterRandomViaMouse() - { - AddStep("click button", () => - { - InputManager.MoveMouseTo(randomButton); - InputManager.Click(MouseButton.Left); - }); - AddAssert("next random invoked", () => nextRandomCalled && !previousRandomCalled); - } - - [Test] - public void TestFooterRewind() - { - AddStep("press Shift+F2", () => - { - InputManager.PressKey(Key.LShift); - InputManager.PressKey(Key.F2); - InputManager.ReleaseKey(Key.F2); - InputManager.ReleaseKey(Key.LShift); - }); - AddAssert("previous random invoked", () => previousRandomCalled && !nextRandomCalled); - } - - [Test] - public void TestFooterRewindViaShiftMouseLeft() - { - AddStep("shift + click button", () => - { - InputManager.PressKey(Key.LShift); - InputManager.MoveMouseTo(randomButton); - InputManager.Click(MouseButton.Left); - InputManager.ReleaseKey(Key.LShift); - }); - AddAssert("previous random invoked", () => previousRandomCalled && !nextRandomCalled); - } - - [Test] - public void TestFooterRewindViaMouseRight() - { - AddStep("right click button", () => - { - InputManager.MoveMouseTo(randomButton); - InputManager.Click(MouseButton.Right); - }); - AddAssert("previous random invoked", () => previousRandomCalled && !nextRandomCalled); - } - - [Test] - public void TestOverlayPresent() - { - AddStep("Press F1", () => - { - InputManager.MoveMouseTo(modsButton); - InputManager.Click(MouseButton.Left); - }); - AddAssert("Overlay visible", () => overlay.State.Value == Visibility.Visible); - AddStep("Hide", () => overlay.Hide()); - } - - private partial class DummyOverlay : ShearedOverlayContainer - { - public DummyOverlay() - : base(OverlayColourScheme.Green) - { - } - - [BackgroundDependencyLoader] - private void load() - { - Header.Title = "An overlay"; - } + protected override bool ShowPresets => true; } } } From 921be3ca014d2b7cbc04cd554de89ed5174df0d8 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 16 May 2024 06:59:58 +0300 Subject: [PATCH 0821/2493] Add back receptor, logo tracking, and own colour provider to screen footer --- osu.Game/Graphics/UserInterface/BackButton.cs | 33 ++-------- osu.Game/Screens/Footer/BackButtonV2.cs | 64 +++++++++++++++++++ osu.Game/Screens/Footer/ScreenFooter.cs | 62 ++++++++++++++++-- 3 files changed, 126 insertions(+), 33 deletions(-) create mode 100644 osu.Game/Screens/Footer/BackButtonV2.cs diff --git a/osu.Game/Graphics/UserInterface/BackButton.cs b/osu.Game/Graphics/UserInterface/BackButton.cs index cd9a357ea4..29bac8fbae 100644 --- a/osu.Game/Graphics/UserInterface/BackButton.cs +++ b/osu.Game/Graphics/UserInterface/BackButton.cs @@ -7,19 +7,18 @@ using System; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Input.Bindings; -using osu.Framework.Input.Events; -using osu.Game.Input.Bindings; +using osu.Game.Screens.Footer; namespace osu.Game.Graphics.UserInterface { + // todo: remove this once all screens migrate to display the new game footer and back button. public partial class BackButton : VisibilityContainer { public Action Action; private readonly TwoLayerButton button; - public BackButton(Receptor receptor = null) + public BackButton(ScreenFooter.BackReceptor receptor = null) { Size = TwoLayerButton.SIZE_EXTENDED; @@ -35,7 +34,7 @@ namespace osu.Game.Graphics.UserInterface if (receptor == null) { // if a receptor wasn't provided, create our own locally. - Add(receptor = new Receptor()); + Add(receptor = new ScreenFooter.BackReceptor()); } receptor.OnBackPressed = () => button.TriggerClick(); @@ -59,29 +58,5 @@ namespace osu.Game.Graphics.UserInterface button.MoveToX(-TwoLayerButton.SIZE_EXTENDED.X / 2, 400, Easing.OutQuint); button.FadeOut(400, Easing.OutQuint); } - - public partial class Receptor : Drawable, IKeyBindingHandler - { - public Action OnBackPressed; - - public bool OnPressed(KeyBindingPressEvent e) - { - if (e.Repeat) - return false; - - switch (e.Action) - { - case GlobalAction.Back: - OnBackPressed?.Invoke(); - return true; - } - - return false; - } - - public void OnReleased(KeyBindingReleaseEvent e) - { - } - } } } diff --git a/osu.Game/Screens/Footer/BackButtonV2.cs b/osu.Game/Screens/Footer/BackButtonV2.cs new file mode 100644 index 0000000000..08daa339c2 --- /dev/null +++ b/osu.Game/Screens/Footer/BackButtonV2.cs @@ -0,0 +1,64 @@ +// 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.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osu.Game.Localisation; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Screens.Footer +{ + public partial class BackButtonV2 : ShearedButton + { + // todo: see https://github.com/ppy/osu-framework/issues/3271 + private const float torus_scale_factor = 1.2f; + + public const float BUTTON_WIDTH = 240; + + public BackButtonV2() + : base(BUTTON_WIDTH, 70) + { + } + + [BackgroundDependencyLoader] + private void load() + { + ButtonContent.Child = new FillFlowContainer + { + X = -10f, + RelativeSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(20f, 0f), + Children = new Drawable[] + { + new SpriteIcon + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(20f), + Icon = FontAwesome.Solid.ChevronLeft, + }, + new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Font = OsuFont.TorusAlternate.With(size: 20 * torus_scale_factor), + Text = CommonStrings.Back, + UseFullGlyphHeight = false, + } + } + }; + + DarkerColour = Color4Extensions.FromHex("#DE31AE"); + LighterColour = Color4Extensions.FromHex("#FF86DD"); + TextColour = Color4.White; + } + } +} diff --git a/osu.Game/Screens/Footer/ScreenFooter.cs b/osu.Game/Screens/Footer/ScreenFooter.cs index d299bf7362..69d5a2616c 100644 --- a/osu.Game/Screens/Footer/ScreenFooter.cs +++ b/osu.Game/Screens/Footer/ScreenFooter.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.Diagnostics; using System.Linq; @@ -8,7 +9,12 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Framework.Input.Bindings; +using osu.Framework.Input.Events; +using osu.Game.Graphics.Containers; +using osu.Game.Input.Bindings; using osu.Game.Overlays; +using osu.Game.Screens.Menu; using osuTK; namespace osu.Game.Screens.Footer @@ -22,19 +28,31 @@ namespace osu.Game.Screens.Footer private readonly List overlays = new List(); + private BackButtonV2 backButton = null!; private FillFlowContainer buttonsFlow = null!; private Container removedButtonsContainer = null!; + private LogoTrackingContainer logoTrackingContainer = null!; - public ScreenFooter() + [Cached] + private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine); + + public Action? OnBack; + + public ScreenFooter(BackReceptor? receptor = null) { RelativeSizeAxes = Axes.X; Height = HEIGHT; Anchor = Anchor.BottomLeft; Origin = Anchor.BottomLeft; + + if (receptor == null) + Add(receptor = new BackReceptor()); + + receptor.OnBackPressed = () => backButton.TriggerClick(); } [BackgroundDependencyLoader] - private void load(OverlayColourProvider colourProvider) + private void load() { InternalChildren = new Drawable[] { @@ -53,12 +71,12 @@ namespace osu.Game.Screens.Footer Spacing = new Vector2(7, 0), AutoSizeAxes = Axes.Both }, - new ScreenBackButton + backButton = new BackButtonV2 { Margin = new MarginPadding { Bottom = 10f, Left = 12f }, Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, - Action = () => { }, + Action = () => OnBack?.Invoke(), }, removedButtonsContainer = new Container { @@ -68,9 +86,21 @@ namespace osu.Game.Screens.Footer Origin = Anchor.BottomLeft, AutoSizeAxes = Axes.Both, }, + (logoTrackingContainer = new LogoTrackingContainer + { + RelativeSizeAxes = Axes.Both, + }).WithChild(logoTrackingContainer.LogoFacade.With(f => + { + f.Anchor = Anchor.BottomRight; + f.Origin = Anchor.Centre; + f.Position = new Vector2(-76, -36); + })), }; } + public void StartTrackingLogo(OsuLogo logo, float duration = 0, Easing easing = Easing.None) => logoTrackingContainer.StartTracking(logo, duration, easing); + public void StopTrackingLogo() => logoTrackingContainer.StopTracking(); + protected override void PopIn() { this.MoveToY(0, 400, Easing.OutQuint) @@ -180,5 +210,29 @@ namespace osu.Game.Screens.Footer o.Hide(); } } + + public partial class BackReceptor : Drawable, IKeyBindingHandler + { + public Action? OnBackPressed; + + public bool OnPressed(KeyBindingPressEvent e) + { + if (e.Repeat) + return false; + + switch (e.Action) + { + case GlobalAction.Back: + OnBackPressed?.Invoke(); + return true; + } + + return false; + } + + public void OnReleased(KeyBindingReleaseEvent e) + { + } + } } } From 03220598b8734cb93d53d15cc054d89e2d4f04ec Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 16 May 2024 07:20:55 +0300 Subject: [PATCH 0822/2493] Move screen footer to `OsuGame` --- .../UserInterface/TestSceneBackButton.cs | 3 +- .../UserInterface/TestSceneScreenFooter.cs | 9 ++-- .../TestSceneScreenFooterButtonMods.cs | 5 ++ osu.Game/OsuGame.cs | 46 +++++++++++++++---- osu.Game/Screens/IOsuScreen.cs | 17 ++++++- .../OnlinePlay/OnlinePlaySongSelect.cs | 4 +- osu.Game/Screens/OsuScreen.cs | 9 ++++ osu.Game/Screens/OsuScreenStack.cs | 6 +++ osu.Game/Screens/Select/SongSelect.cs | 28 +++++------ 9 files changed, 96 insertions(+), 31 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneBackButton.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneBackButton.cs index 494268b158..7aaf616767 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneBackButton.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneBackButton.cs @@ -5,6 +5,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Game.Graphics.UserInterface; +using osu.Game.Screens.Footer; using osuTK; using osuTK.Graphics; @@ -15,7 +16,7 @@ namespace osu.Game.Tests.Visual.UserInterface public TestSceneBackButton() { BackButton button; - BackButton.Receptor receptor = new BackButton.Receptor(); + ScreenFooter.BackReceptor receptor = new ScreenFooter.BackReceptor(); Child = new Container { diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneScreenFooter.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneScreenFooter.cs index 01b3aa5787..dabb2e7f50 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneScreenFooter.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneScreenFooter.cs @@ -3,7 +3,6 @@ using System; using NUnit.Framework; -using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Cursor; using osu.Framework.Testing; @@ -19,9 +18,6 @@ namespace osu.Game.Tests.Visual.UserInterface private ScreenFooter screenFooter = null!; private TestModSelectOverlay overlay = null!; - [Cached] - private OverlayColourProvider colourProvider { get; set; } = new OverlayColourProvider(OverlayColourScheme.Aquamarine); - [SetUp] public void SetUp() => Schedule(() => { @@ -89,6 +85,11 @@ namespace osu.Game.Tests.Visual.UserInterface private partial class TestModSelectOverlay : UserModSelectOverlay { protected override bool ShowPresets => true; + + public TestModSelectOverlay() + : base(OverlayColourScheme.Aquamarine) + { + } } } } diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneScreenFooterButtonMods.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneScreenFooterButtonMods.cs index 2e2baf6e96..ba53eb83c4 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneScreenFooterButtonMods.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneScreenFooterButtonMods.cs @@ -116,6 +116,11 @@ namespace osu.Game.Tests.Visual.UserInterface private partial class TestModSelectOverlay : UserModSelectOverlay { protected override bool ShowPresets => true; + + public TestModSelectOverlay() + : base(OverlayColourScheme.Aquamarine) + { + } } private partial class TestScreenFooterButtonMods : ScreenFooterButtonMods diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index af01a1b1ac..ee1d262f88 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -22,6 +22,7 @@ using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Extensions.TypeExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Sprites; using osu.Framework.Input; using osu.Framework.Input.Bindings; @@ -58,6 +59,7 @@ using osu.Game.Rulesets.Mods; using osu.Game.Scoring; using osu.Game.Screens; using osu.Game.Screens.Edit; +using osu.Game.Screens.Footer; using osu.Game.Screens.Menu; using osu.Game.Screens.OnlinePlay.Multiplayer; using osu.Game.Screens.Play; @@ -153,6 +155,8 @@ namespace osu.Game private float toolbarOffset => (Toolbar?.Position.Y ?? 0) + (Toolbar?.DrawHeight ?? 0); + private float screenFooterOffset => (ScreenFooter?.DrawHeight ?? 0) - (ScreenFooter?.Position.Y ?? 0); + private IdleTracker idleTracker; /// @@ -177,6 +181,7 @@ namespace osu.Game protected OsuScreenStack ScreenStack; protected BackButton BackButton; + protected ScreenFooter ScreenFooter; protected SettingsOverlay Settings; @@ -917,7 +922,7 @@ namespace osu.Game }; Container logoContainer; - BackButton.Receptor receptor; + ScreenFooter.BackReceptor backReceptor; dependencies.CacheAs(idleTracker = new GameIdleTracker(6000)); @@ -950,20 +955,28 @@ namespace osu.Game Origin = Anchor.Centre, Children = new Drawable[] { - receptor = new BackButton.Receptor(), + backReceptor = new ScreenFooter.BackReceptor(), ScreenStack = new OsuScreenStack { RelativeSizeAxes = Axes.Both }, - BackButton = new BackButton(receptor) + BackButton = new BackButton(backReceptor) { Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, - Action = () => + Action = () => ScreenFooter.OnBack?.Invoke(), + }, + new PopoverContainer + { + RelativeSizeAxes = Axes.Both, + Child = ScreenFooter = new ScreenFooter(backReceptor) { - if (!(ScreenStack.CurrentScreen is IOsuScreen currentScreen)) - return; + OnBack = () => + { + if (!(ScreenStack.CurrentScreen is IOsuScreen currentScreen)) + return; - if (!((Drawable)currentScreen).IsLoaded || (currentScreen.AllowBackButton && !currentScreen.OnBackButton())) - ScreenStack.Exit(); - } + if (!((Drawable)currentScreen).IsLoaded || (currentScreen.AllowBackButton && !currentScreen.OnBackButton())) + ScreenStack.Exit(); + } + }, }, logoContainer = new Container { RelativeSizeAxes = Axes.Both }, } @@ -985,6 +998,8 @@ namespace osu.Game new ConfineMouseTracker() }); + dependencies.Cache(ScreenFooter); + ScreenStack.ScreenPushed += screenPushed; ScreenStack.ScreenExited += screenExited; @@ -1457,6 +1472,7 @@ namespace osu.Game ScreenOffsetContainer.Padding = new MarginPadding { Top = toolbarOffset }; overlayOffsetContainer.Padding = new MarginPadding { Top = toolbarOffset }; + ScreenStack.Padding = new MarginPadding { Bottom = screenFooterOffset }; float horizontalOffset = 0f; @@ -1529,6 +1545,18 @@ namespace osu.Game BackButton.Show(); else BackButton.Hide(); + + if (newOsuScreen.ShowFooter) + { + BackButton.Hide(); + ScreenFooter.SetButtons(newOsuScreen.CreateFooterButtons()); + ScreenFooter.Show(); + } + else + { + ScreenFooter.SetButtons(Array.Empty()); + ScreenFooter.Hide(); + } } skinEditor.SetTarget((OsuScreen)newScreen); diff --git a/osu.Game/Screens/IOsuScreen.cs b/osu.Game/Screens/IOsuScreen.cs index 5b4e2d75f4..b80c1f87a4 100644 --- a/osu.Game/Screens/IOsuScreen.cs +++ b/osu.Game/Screens/IOsuScreen.cs @@ -1,11 +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.Bindables; using osu.Framework.Screens; using osu.Game.Beatmaps; using osu.Game.Overlays; using osu.Game.Rulesets; +using osu.Game.Screens.Footer; using osu.Game.Users; namespace osu.Game.Screens @@ -19,10 +21,18 @@ namespace osu.Game.Screens bool DisallowExternalBeatmapRulesetChanges { get; } /// - /// Whether the user can exit this this by pressing the back button. + /// Whether the user can exit this by pressing the back button. /// bool AllowBackButton { get; } + /// + /// Whether a footer (and a back button) should be displayed underneath the screen. + /// + /// + /// Temporarily, the back button is shown regardless of whether is true. + /// + bool ShowFooter { get; } + /// /// Whether a top-level component should be allowed to exit the current screen to, for example, /// complete an import. Note that this can be overridden by a user if they specifically request. @@ -63,6 +73,11 @@ namespace osu.Game.Screens Bindable Ruleset { get; } + /// + /// A list of footer buttons to be added to the game footer, or empty to display no buttons. + /// + IReadOnlyList CreateFooterButtons(); + /// /// Whether mod track adjustments should be applied on entering this screen. /// A value means that the parent screen's value of this setting will be used. diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs b/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs index 622ffddba6..a8dfece916 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs @@ -173,9 +173,9 @@ namespace osu.Game.Screens.OnlinePlay IsValidMod = IsValidMod }; - protected override IEnumerable<(FooterButton, OverlayContainer?)> CreateFooterButtons() + protected override IEnumerable<(FooterButton, OverlayContainer?)> CreateSongSelectFooterButtons() { - var baseButtons = base.CreateFooterButtons().ToList(); + var baseButtons = base.CreateSongSelectFooterButtons().ToList(); var freeModsButton = new FooterButtonFreeMods(freeModSelectOverlay) { Current = FreeMods }; baseButtons.Insert(baseButtons.FindIndex(b => b.Item1 is FooterButtonMods) + 1, (freeModsButton, freeModSelectOverlay)); diff --git a/osu.Game/Screens/OsuScreen.cs b/osu.Game/Screens/OsuScreen.cs index 2e8f85423d..695a074907 100644 --- a/osu.Game/Screens/OsuScreen.cs +++ b/osu.Game/Screens/OsuScreen.cs @@ -16,6 +16,7 @@ using osu.Game.Beatmaps; using osu.Game.Overlays; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; +using osu.Game.Screens.Footer; using osu.Game.Screens.Menu; using osu.Game.Users; @@ -38,6 +39,8 @@ namespace osu.Game.Screens public virtual bool AllowBackButton => true; + public virtual bool ShowFooter => false; + public virtual bool AllowExternalScreenChange => false; public virtual bool HideOverlaysOnEnter => false; @@ -141,6 +144,10 @@ namespace osu.Game.Screens [Resolved(canBeNull: true)] private OsuLogo logo { get; set; } + [Resolved(canBeNull: true)] + [CanBeNull] + protected ScreenFooter Footer { get; private set; } + protected OsuScreen() { Anchor = Anchor.Centre; @@ -298,6 +305,8 @@ namespace osu.Game.Screens /// protected virtual BackgroundScreen CreateBackground() => null; + public virtual IReadOnlyList CreateFooterButtons() => Array.Empty(); + public virtual bool OnBackButton() => false; } } diff --git a/osu.Game/Screens/OsuScreenStack.cs b/osu.Game/Screens/OsuScreenStack.cs index 7d1f6419ad..7103fd6466 100644 --- a/osu.Game/Screens/OsuScreenStack.cs +++ b/osu.Game/Screens/OsuScreenStack.cs @@ -17,6 +17,12 @@ namespace osu.Game.Screens protected float ParallaxAmount => parallaxContainer.ParallaxAmount; + public new MarginPadding Padding + { + get => base.Padding; + set => base.Padding = value; + } + public OsuScreenStack() { InternalChild = parallaxContainer = new ParallaxContainer diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index 6225534e95..4b0084c7c5 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -60,19 +60,19 @@ namespace osu.Game.Screens.Select /// protected virtual bool ControlGlobalMusic => true; - protected virtual bool ShowFooter => true; + protected virtual bool ShowSongSelectFooter => true; public override bool? ApplyModTrackAdjustments => true; /// - /// Can be null if is false. + /// Can be null if is false. /// protected BeatmapOptionsOverlay BeatmapOptions { get; private set; } = null!; /// - /// Can be null if is false. + /// Can be null if is false. /// - protected Footer? Footer { get; private set; } + protected Footer? SongSelectFooter { get; private set; } /// /// Contains any panel which is triggered by a footer button. @@ -163,7 +163,7 @@ namespace osu.Game.Screens.Select Origin = Anchor.CentreRight, RelativeSizeAxes = Axes.Both, BleedTop = FilterControl.HEIGHT, - BleedBottom = Footer.HEIGHT, + BleedBottom = Select.Footer.HEIGHT, SelectionChanged = updateSelectedBeatmap, BeatmapSetsChanged = carouselBeatmapsLoaded, FilterApplied = () => Scheduler.AddOnce(updateVisibleBeatmapCount), @@ -210,7 +210,7 @@ namespace osu.Game.Screens.Select Padding = new MarginPadding { Top = FilterControl.HEIGHT, - Bottom = Footer.HEIGHT + Bottom = Select.Footer.HEIGHT }, Child = new LoadingSpinner(true) { State = { Value = Visibility.Visible } } } @@ -297,7 +297,7 @@ namespace osu.Game.Screens.Select RelativeSizeAxes = Axes.Both, Padding = new MarginPadding { - Bottom = Footer.HEIGHT, + Bottom = Select.Footer.HEIGHT, Top = WEDGE_HEIGHT + 70, Left = left_area_padding, Right = left_area_padding * 2, @@ -321,7 +321,7 @@ namespace osu.Game.Screens.Select }, }); - if (ShowFooter) + if (ShowSongSelectFooter) { AddRangeInternal(new Drawable[] { @@ -330,13 +330,13 @@ namespace osu.Game.Screens.Select Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding { Bottom = Footer.HEIGHT }, + Padding = new MarginPadding { Bottom = Select.Footer.HEIGHT }, Children = new Drawable[] { BeatmapOptions = new BeatmapOptionsOverlay(), } }, - Footer = new Footer() + SongSelectFooter = new Footer() }); } @@ -344,10 +344,10 @@ namespace osu.Game.Screens.Select // therein it will be registered at the `OsuGame` level to properly function as a blocking overlay. LoadComponent(ModSelect = CreateModSelectOverlay()); - if (Footer != null) + if (SongSelectFooter != null) { - foreach (var (button, overlay) in CreateFooterButtons()) - Footer.AddButton(button, overlay); + foreach (var (button, overlay) in CreateSongSelectFooterButtons()) + SongSelectFooter.AddButton(button, overlay); BeatmapOptions.AddButton(@"Manage", @"collections", FontAwesome.Solid.Book, colours.Green, () => manageCollectionsDialog?.Show()); BeatmapOptions.AddButton(@"Delete", @"all difficulties", FontAwesome.Solid.Trash, colours.Pink, () => DeleteBeatmap(Beatmap.Value.BeatmapSetInfo)); @@ -381,7 +381,7 @@ namespace osu.Game.Screens.Select /// Creates the buttons to be displayed in the footer. /// /// A set of and an optional which the button opens when pressed. - protected virtual IEnumerable<(FooterButton, OverlayContainer?)> CreateFooterButtons() => new (FooterButton, OverlayContainer?)[] + protected virtual IEnumerable<(FooterButton, OverlayContainer?)> CreateSongSelectFooterButtons() => new (FooterButton, OverlayContainer?)[] { (new FooterButtonMods { Current = Mods }, ModSelect), (new FooterButtonRandom From 2b5818a4d7e7cc4940703885e28633a080e859b9 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 16 May 2024 07:21:32 +0300 Subject: [PATCH 0823/2493] Fix DI crash in beatmap options popover --- .../SelectV2/Footer/BeatmapOptionsPopover.cs | 22 +++++++++---------- .../Footer/ScreenFooterButtonOptions.cs | 6 ++++- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/osu.Game/Screens/SelectV2/Footer/BeatmapOptionsPopover.cs b/osu.Game/Screens/SelectV2/Footer/BeatmapOptionsPopover.cs index f73be15a36..11a83f3438 100644 --- a/osu.Game/Screens/SelectV2/Footer/BeatmapOptionsPopover.cs +++ b/osu.Game/Screens/SelectV2/Footer/BeatmapOptionsPopover.cs @@ -20,7 +20,6 @@ using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Localisation; using osu.Game.Overlays; -using osu.Game.Screens.Select; using osuTK; using osuTK.Graphics; using osuTK.Input; @@ -33,18 +32,22 @@ namespace osu.Game.Screens.SelectV2.Footer private FillFlowContainer buttonFlow = null!; private readonly ScreenFooterButtonOptions footerButton; + [Cached] + private readonly OverlayColourProvider colourProvider; + private WorkingBeatmap beatmapWhenOpening = null!; [Resolved] private IBindable beatmap { get; set; } = null!; - public BeatmapOptionsPopover(ScreenFooterButtonOptions footerButton) + public BeatmapOptionsPopover(ScreenFooterButtonOptions footerButton, OverlayColourProvider colourProvider) { this.footerButton = footerButton; + this.colourProvider = colourProvider; } [BackgroundDependencyLoader] - private void load(ManageCollectionsDialog? manageCollectionsDialog, SongSelect? songSelect, OsuColour colours, BeatmapManager? beatmapManager) + private void load(ManageCollectionsDialog? manageCollectionsDialog, OsuColour colours, BeatmapManager? beatmapManager) { Content.Padding = new MarginPadding(5); @@ -61,15 +64,15 @@ namespace osu.Game.Screens.SelectV2.Footer addButton(SongSelectStrings.ManageCollections, FontAwesome.Solid.Book, () => manageCollectionsDialog?.Show()); addHeader(SongSelectStrings.ForAllDifficulties, beatmapWhenOpening.BeatmapSetInfo.ToString()); - addButton(SongSelectStrings.DeleteBeatmap, FontAwesome.Solid.Trash, () => songSelect?.DeleteBeatmap(beatmapWhenOpening.BeatmapSetInfo), colours.Red1); + addButton(SongSelectStrings.DeleteBeatmap, FontAwesome.Solid.Trash, () => { }, colours.Red1); // songSelect?.DeleteBeatmap(beatmapWhenOpening.BeatmapSetInfo); addHeader(SongSelectStrings.ForSelectedDifficulty, beatmapWhenOpening.BeatmapInfo.DifficultyName); // TODO: make work, and make show "unplayed" or "played" based on status. addButton(SongSelectStrings.MarkAsPlayed, FontAwesome.Regular.TimesCircle, null); - addButton(SongSelectStrings.ClearAllLocalScores, FontAwesome.Solid.Eraser, () => songSelect?.ClearScores(beatmapWhenOpening.BeatmapInfo), colours.Red1); + addButton(SongSelectStrings.ClearAllLocalScores, FontAwesome.Solid.Eraser, () => { }, colours.Red1); // songSelect?.ClearScores(beatmapWhenOpening.BeatmapInfo); - if (songSelect != null && songSelect.AllowEditing) - addButton(SongSelectStrings.EditBeatmap, FontAwesome.Solid.PencilAlt, () => songSelect.Edit(beatmapWhenOpening.BeatmapInfo)); + // if (songSelect != null && songSelect.AllowEditing) + addButton(SongSelectStrings.EditBeatmap, FontAwesome.Solid.PencilAlt, () => { }); // songSelect.Edit(beatmapWhenOpening.BeatmapInfo); addButton(WebCommonStrings.ButtonsHide.ToSentence(), FontAwesome.Solid.Magic, () => beatmapManager?.Hide(beatmapWhenOpening.BeatmapInfo)); } @@ -83,9 +86,6 @@ namespace osu.Game.Screens.SelectV2.Footer beatmap.BindValueChanged(_ => Hide()); } - [Resolved] - private OverlayColourProvider overlayColourProvider { get; set; } = null!; - private void addHeader(LocalisableString text, string? context = null) { var textFlow = new OsuTextFlowContainer @@ -102,7 +102,7 @@ namespace osu.Game.Screens.SelectV2.Footer textFlow.NewLine(); textFlow.AddText(context, t => { - t.Colour = overlayColourProvider.Content2; + t.Colour = colourProvider.Content2; t.Font = t.Font.With(size: 13); }); } diff --git a/osu.Game/Screens/SelectV2/Footer/ScreenFooterButtonOptions.cs b/osu.Game/Screens/SelectV2/Footer/ScreenFooterButtonOptions.cs index 74fe3e3d11..72409b5566 100644 --- a/osu.Game/Screens/SelectV2/Footer/ScreenFooterButtonOptions.cs +++ b/osu.Game/Screens/SelectV2/Footer/ScreenFooterButtonOptions.cs @@ -8,12 +8,16 @@ using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.UserInterface; using osu.Game.Graphics; using osu.Game.Input.Bindings; +using osu.Game.Overlays; using osu.Game.Screens.Footer; namespace osu.Game.Screens.SelectV2.Footer { public partial class ScreenFooterButtonOptions : ScreenFooterButton, IHasPopover { + [Resolved] + private OverlayColourProvider colourProvider { get; set; } = null!; + [BackgroundDependencyLoader] private void load(OsuColour colour) { @@ -25,6 +29,6 @@ namespace osu.Game.Screens.SelectV2.Footer Action = this.ShowPopover; } - public Popover GetPopover() => new BeatmapOptionsPopover(this); + public Popover GetPopover() => new BeatmapOptionsPopover(this, colourProvider); } } From 8b285f5b1e911fe8baec3ce2691e678d2a33d269 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 16 May 2024 07:21:41 +0300 Subject: [PATCH 0824/2493] Add dummy `SongSelectV2` screen to house new footer buttons (and new components going forward) --- .../SongSelect/TestSceneSongSelectV2.cs | 204 ++++++++++++++++++ .../TestSceneSongSelectV2Navigation.cs | 33 +++ osu.Game/Screens/SelectV2/SongSelectV2.cs | 145 +++++++++++++ 3 files changed, 382 insertions(+) create mode 100644 osu.Game.Tests/Visual/SongSelect/TestSceneSongSelectV2.cs create mode 100644 osu.Game.Tests/Visual/SongSelect/TestSceneSongSelectV2Navigation.cs create mode 100644 osu.Game/Screens/SelectV2/SongSelectV2.cs diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneSongSelectV2.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneSongSelectV2.cs new file mode 100644 index 0000000000..674eaa2ff8 --- /dev/null +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneSongSelectV2.cs @@ -0,0 +1,204 @@ +// 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; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; +using osu.Framework.Screens; +using osu.Framework.Testing; +using osu.Game.Overlays.Mods; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Osu.Mods; +using osu.Game.Screens; +using osu.Game.Screens.Footer; +using osu.Game.Screens.Menu; +using osu.Game.Screens.SelectV2; +using osu.Game.Screens.SelectV2.Footer; +using osuTK.Input; + +namespace osu.Game.Tests.Visual.SongSelect +{ + public partial class TestSceneSongSelectV2 : ScreenTestScene + { + [Cached] + private readonly ScreenFooter screenScreenFooter; + + [Cached] + private readonly OsuLogo logo; + + public TestSceneSongSelectV2() + { + Children = new Drawable[] + { + new PopoverContainer + { + RelativeSizeAxes = Axes.Both, + Child = screenScreenFooter = new ScreenFooter + { + OnBack = () => Stack.CurrentScreen.Exit(), + }, + }, + logo = new OsuLogo + { + Alpha = 0f, + }, + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + Stack.ScreenPushed += updateFooter; + Stack.ScreenExited += updateFooter; + } + + [SetUpSteps] + public override void SetUpSteps() + { + base.SetUpSteps(); + + AddStep("load screen", () => Stack.Push(new SongSelectV2())); + AddUntilStep("wait for load", () => Stack.CurrentScreen is SongSelectV2 songSelect && songSelect.IsLoaded); + } + + #region Footer + + [Test] + public void TestMods() + { + AddStep("one mod", () => SelectedMods.Value = new List { new OsuModHidden() }); + AddStep("two mods", () => SelectedMods.Value = new List { new OsuModHidden(), new OsuModHardRock() }); + AddStep("three mods", () => SelectedMods.Value = new List { new OsuModHidden(), new OsuModHardRock(), new OsuModDoubleTime() }); + AddStep("four mods", () => SelectedMods.Value = new List { new OsuModHidden(), new OsuModHardRock(), new OsuModDoubleTime(), new OsuModClassic() }); + AddStep("five mods", () => SelectedMods.Value = new List { new OsuModHidden(), new OsuModHardRock(), new OsuModDoubleTime(), new OsuModClassic(), new OsuModDifficultyAdjust() }); + + AddStep("modified", () => SelectedMods.Value = new List { new OsuModDoubleTime { SpeedChange = { Value = 1.2 } } }); + AddStep("modified + one", () => SelectedMods.Value = new List { new OsuModHidden(), new OsuModDoubleTime { SpeedChange = { Value = 1.2 } } }); + AddStep("modified + two", () => SelectedMods.Value = new List { new OsuModHidden(), new OsuModHardRock(), new OsuModDoubleTime { SpeedChange = { Value = 1.2 } } }); + AddStep("modified + three", () => SelectedMods.Value = new List { new OsuModHidden(), new OsuModHardRock(), new OsuModClassic(), new OsuModDoubleTime { SpeedChange = { Value = 1.2 } } }); + AddStep("modified + four", () => SelectedMods.Value = new List { new OsuModHidden(), new OsuModHardRock(), new OsuModClassic(), new OsuModDifficultyAdjust(), new OsuModDoubleTime { SpeedChange = { Value = 1.2 } } }); + + AddStep("clear mods", () => SelectedMods.Value = Array.Empty()); + AddWaitStep("wait", 3); + AddStep("one mod", () => SelectedMods.Value = new List { new OsuModHidden() }); + + AddStep("clear mods", () => SelectedMods.Value = Array.Empty()); + AddWaitStep("wait", 3); + AddStep("five mods", () => SelectedMods.Value = new List { new OsuModHidden(), new OsuModHardRock(), new OsuModDoubleTime(), new OsuModClassic(), new OsuModDifficultyAdjust() }); + } + + [Test] + public void TestShowOptions() + { + AddStep("enable options", () => + { + var optionsButton = this.ChildrenOfType().Last(); + + optionsButton.Enabled.Value = true; + optionsButton.TriggerClick(); + }); + } + + [Test] + public void TestState() + { + AddToggleStep("set options enabled state", state => this.ChildrenOfType().Last().Enabled.Value = state); + } + + // add these test cases when functionality is implemented. + // [Test] + // public void TestFooterRandom() + // { + // AddStep("press F2", () => InputManager.Key(Key.F2)); + // AddAssert("next random invoked", () => nextRandomCalled && !previousRandomCalled); + // } + // + // [Test] + // public void TestFooterRandomViaMouse() + // { + // AddStep("click button", () => + // { + // InputManager.MoveMouseTo(randomButton); + // InputManager.Click(MouseButton.Left); + // }); + // AddAssert("next random invoked", () => nextRandomCalled && !previousRandomCalled); + // } + // + // [Test] + // public void TestFooterRewind() + // { + // AddStep("press Shift+F2", () => + // { + // InputManager.PressKey(Key.LShift); + // InputManager.PressKey(Key.F2); + // InputManager.ReleaseKey(Key.F2); + // InputManager.ReleaseKey(Key.LShift); + // }); + // AddAssert("previous random invoked", () => previousRandomCalled && !nextRandomCalled); + // } + // + // [Test] + // public void TestFooterRewindViaShiftMouseLeft() + // { + // AddStep("shift + click button", () => + // { + // InputManager.PressKey(Key.LShift); + // InputManager.MoveMouseTo(randomButton); + // InputManager.Click(MouseButton.Left); + // InputManager.ReleaseKey(Key.LShift); + // }); + // AddAssert("previous random invoked", () => previousRandomCalled && !nextRandomCalled); + // } + // + // [Test] + // public void TestFooterRewindViaMouseRight() + // { + // AddStep("right click button", () => + // { + // InputManager.MoveMouseTo(randomButton); + // InputManager.Click(MouseButton.Right); + // }); + // AddAssert("previous random invoked", () => previousRandomCalled && !nextRandomCalled); + // } + + [Test] + public void TestOverlayPresent() + { + AddStep("Press F1", () => + { + InputManager.MoveMouseTo(this.ChildrenOfType().Single()); + InputManager.Click(MouseButton.Left); + }); + AddAssert("Overlay visible", () => this.ChildrenOfType().Single().State.Value == Visibility.Visible); + AddStep("Hide", () => this.ChildrenOfType().Single().Hide()); + } + + #endregion + + protected override void Update() + { + base.Update(); + Stack.Padding = new MarginPadding { Bottom = screenScreenFooter.DrawHeight - screenScreenFooter.Y }; + } + + private void updateFooter(IScreen? _, IScreen? newScreen) + { + if (newScreen is IOsuScreen osuScreen && osuScreen.ShowFooter) + { + screenScreenFooter.Show(); + screenScreenFooter.SetButtons(osuScreen.CreateFooterButtons()); + } + else + { + screenScreenFooter.Hide(); + screenScreenFooter.SetButtons(Array.Empty()); + } + } + } +} diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneSongSelectV2Navigation.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneSongSelectV2Navigation.cs new file mode 100644 index 0000000000..ededb80228 --- /dev/null +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneSongSelectV2Navigation.cs @@ -0,0 +1,33 @@ +// 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.Testing; +using osu.Game.Screens.Menu; +using osu.Game.Screens.SelectV2; +using osuTK.Input; + +namespace osu.Game.Tests.Visual.SongSelect +{ + public partial class TestSceneSongSelectV2Navigation : OsuGameTestScene + { + public override void SetUpSteps() + { + base.SetUpSteps(); + AddStep("press enter", () => InputManager.Key(Key.Enter)); + AddWaitStep("wait", 5); + PushAndConfirm(() => new SongSelectV2()); + } + + [Test] + public void TestClickLogo() + { + AddStep("click", () => + { + InputManager.MoveMouseTo(Game.ChildrenOfType().Single()); + InputManager.Click(MouseButton.Left); + }); + } + } +} diff --git a/osu.Game/Screens/SelectV2/SongSelectV2.cs b/osu.Game/Screens/SelectV2/SongSelectV2.cs new file mode 100644 index 0000000000..10ed7783c4 --- /dev/null +++ b/osu.Game/Screens/SelectV2/SongSelectV2.cs @@ -0,0 +1,145 @@ +// 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 osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; +using osu.Framework.Screens; +using osu.Game.Overlays; +using osu.Game.Overlays.Mods; +using osu.Game.Screens.Footer; +using osu.Game.Screens.Menu; +using osu.Game.Screens.Play; +using osu.Game.Screens.SelectV2.Footer; + +namespace osu.Game.Screens.SelectV2 +{ + /// + /// This screen is intended to house all components introduced in the new song select design to add transitions and examine the overall look. + /// This will be gradually built upon and ultimately replace once everything is in place. + /// + public partial class SongSelectV2 : OsuScreen + { + private readonly ModSelectOverlay modSelectOverlay = new SoloModSelectOverlay(); + + [Cached] + private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine); + + public override bool ShowFooter => true; + + [BackgroundDependencyLoader] + private void load() + { + AddRangeInternal(new Drawable[] + { + new PopoverContainer + { + RelativeSizeAxes = Axes.Both, + }, + modSelectOverlay, + }); + } + + public override IReadOnlyList CreateFooterButtons() => new ScreenFooterButton[] + { + new ScreenFooterButtonMods(modSelectOverlay) { Current = Mods }, + new ScreenFooterButtonRandom(), + new ScreenFooterButtonOptions(), + }; + + public override void OnEntering(ScreenTransitionEvent e) + { + this.FadeIn(); + base.OnEntering(e); + } + + public override void OnResuming(ScreenTransitionEvent e) + { + this.FadeIn(); + base.OnResuming(e); + } + + public override void OnSuspending(ScreenTransitionEvent e) + { + this.Delay(400).FadeOut(); + base.OnSuspending(e); + } + + public override bool OnExiting(ScreenExitEvent e) + { + this.Delay(400).FadeOut(); + return base.OnExiting(e); + } + + public override bool OnBackButton() + { + if (modSelectOverlay.State.Value == Visibility.Visible) + { + modSelectOverlay.Hide(); + return true; + } + + return false; + } + + protected override void LogoArriving(OsuLogo logo, bool resuming) + { + base.LogoArriving(logo, resuming); + + if (logo.Alpha > 0.8f) + Footer?.StartTrackingLogo(logo, 400, Easing.OutQuint); + else + { + logo.Hide(); + logo.ScaleTo(0.2f); + Footer?.StartTrackingLogo(logo); + } + + logo.FadeIn(240, Easing.OutQuint); + logo.ScaleTo(0.4f, 240, Easing.OutQuint); + + logo.Action = () => + { + this.Push(new PlayerLoaderV2(() => new SoloPlayer())); + return false; + }; + } + + protected override void LogoSuspending(OsuLogo logo) + { + base.LogoSuspending(logo); + Footer?.StopTrackingLogo(); + } + + protected override void LogoExiting(OsuLogo logo) + { + base.LogoExiting(logo); + Scheduler.AddDelayed(() => Footer?.StopTrackingLogo(), 120); + logo.ScaleTo(0.2f, 120, Easing.Out); + logo.FadeOut(120, Easing.Out); + } + + private partial class SoloModSelectOverlay : ModSelectOverlay + { + protected override bool ShowPresets => true; + + public SoloModSelectOverlay() + : base(OverlayColourScheme.Aquamarine) + { + } + } + + private partial class PlayerLoaderV2 : PlayerLoader + { + public override bool ShowFooter => true; + + public PlayerLoaderV2(Func createPlayer) + : base(createPlayer) + { + } + } + } +} From 97de73b99c47b2c0787ece0efa8431272df49c8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 16 May 2024 08:21:52 +0200 Subject: [PATCH 0825/2493] Do not change mania column width on mobile platforms - Closes https://github.com/ppy/osu/issues/25852 - Reverts https://github.com/ppy/osu/pull/25336 / https://github.com/ppy/osu/pull/25777 With the columns not being directly touchable anymore after https://github.com/ppy/osu/pull/28173 I see very little point to this continuing to exist. --- osu.Game.Rulesets.Mania/UI/ColumnFlow.cs | 36 ------------------------ 1 file changed, 36 deletions(-) diff --git a/osu.Game.Rulesets.Mania/UI/ColumnFlow.cs b/osu.Game.Rulesets.Mania/UI/ColumnFlow.cs index 1593e8e76f..f444448797 100644 --- a/osu.Game.Rulesets.Mania/UI/ColumnFlow.cs +++ b/osu.Game.Rulesets.Mania/UI/ColumnFlow.cs @@ -3,15 +3,12 @@ #nullable disable -using System; -using osu.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Skinning; using osu.Game.Skinning; -using osuTK; namespace osu.Game.Rulesets.Mania.UI { @@ -62,12 +59,6 @@ namespace osu.Game.Rulesets.Mania.UI onSkinChanged(); } - protected override void LoadComplete() - { - base.LoadComplete(); - updateMobileSizing(); - } - private void onSkinChanged() { for (int i = 0; i < stageDefinition.Columns; i++) @@ -92,8 +83,6 @@ namespace osu.Game.Rulesets.Mania.UI columns[i].Width = width.Value; } - - updateMobileSizing(); } /// @@ -106,31 +95,6 @@ namespace osu.Game.Rulesets.Mania.UI Content[column] = columns[column].Child = content; } - private void updateMobileSizing() - { - if (!IsLoaded || !RuntimeInfo.IsMobile) - return; - - // GridContainer+CellContainer containing this stage (gets split up for dual stages). - Vector2? containingCell = this.FindClosestParent()?.Parent?.DrawSize; - - // Will be null in tests. - if (containingCell == null) - return; - - float aspectRatio = containingCell.Value.X / containingCell.Value.Y; - - // 2.83 is a mostly arbitrary scale-up (170 / 60, based on original implementation for argon) - float mobileAdjust = 2.83f * Math.Min(1, 7f / stageDefinition.Columns); - // 1.92 is a "reference" mobile screen aspect ratio for phones. - // We should scale it back for cases like tablets which aren't so extreme. - mobileAdjust *= aspectRatio / 1.92f; - - // Best effort until we have better mobile support. - for (int i = 0; i < stageDefinition.Columns; i++) - columns[i].Width *= mobileAdjust; - } - protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); From 5dd64a7c86bae98bc991386ba92faff97d6a205d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 16 May 2024 14:49:56 +0200 Subject: [PATCH 0826/2493] Fix duplicated localisation key in `DeleteConfiormationContentStrings` Noticed via `osu-resources` build warnings. There are also a few other warnings about https://github.com/ppy/osu/pull/27472. Seems something in crowdin innards may still be exporting those strings even though they have been fixed already. Not sure how to address that. Probably need these to be detected via static analysis at this point since it's happened again. Might look into the feasibility of making that happen. --- osu.Game/Localisation/DeleteConfirmationContentStrings.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Localisation/DeleteConfirmationContentStrings.cs b/osu.Game/Localisation/DeleteConfirmationContentStrings.cs index 563fbf5654..d781fadbce 100644 --- a/osu.Game/Localisation/DeleteConfirmationContentStrings.cs +++ b/osu.Game/Localisation/DeleteConfirmationContentStrings.cs @@ -32,7 +32,7 @@ namespace osu.Game.Localisation /// /// "Are you sure you want to delete all scores? This cannot be undone!" /// - public static LocalisableString Scores => new TranslatableString(getKey(@"collections"), @"Are you sure you want to delete all scores? This cannot be undone!"); + public static LocalisableString Scores => new TranslatableString(getKey(@"scores"), @"Are you sure you want to delete all scores? This cannot be undone!"); /// /// "Are you sure you want to delete all mod presets?" From f2f03f08cb0dd79de4587a2ec63da97f3cb375da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 16 May 2024 17:36:19 +0200 Subject: [PATCH 0827/2493] Fix xmldoc mismatches in localisation files This is enforced by the localisation analyser after https://github.com/ppy/osu-localisation-analyser/pull/62, but it appears the analyser was never actually bumped game-side after that change and I'm not super sure why, as there does not appear to be a reason to _not_ do that. So this commit does it. --- .../FirstRunOverlayImportFromStableScreenStrings.cs | 2 +- osu.Game/Localisation/NotificationsStrings.cs | 2 +- osu.Game/osu.Game.csproj | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Localisation/FirstRunOverlayImportFromStableScreenStrings.cs b/osu.Game/Localisation/FirstRunOverlayImportFromStableScreenStrings.cs index 04fecab3df..521a77fe20 100644 --- a/osu.Game/Localisation/FirstRunOverlayImportFromStableScreenStrings.cs +++ b/osu.Game/Localisation/FirstRunOverlayImportFromStableScreenStrings.cs @@ -47,7 +47,7 @@ namespace osu.Game.Localisation public static LocalisableString Calculating => new TranslatableString(getKey(@"calculating"), @"calculating..."); /// - /// "{0} items" + /// "{0} item(s)" /// public static LocalisableString Items(int arg0) => new TranslatableString(getKey(@"items"), @"{0} item(s)", arg0); diff --git a/osu.Game/Localisation/NotificationsStrings.cs b/osu.Game/Localisation/NotificationsStrings.cs index 3188ca5533..698fe230b2 100644 --- a/osu.Game/Localisation/NotificationsStrings.cs +++ b/osu.Game/Localisation/NotificationsStrings.cs @@ -114,7 +114,7 @@ Please try changing your audio device to a working setting."); public static LocalisableString MismatchingBeatmapForReplay => new TranslatableString(getKey(@"mismatching_beatmap_for_replay"), @"Your local copy of the beatmap for this replay appears to be different than expected. You may need to update or re-download it."); /// - /// "You are now running osu! {version}. + /// "You are now running osu! {0}. /// Click to see what's new!" /// public static LocalisableString GameVersionAfterUpdate(string version) => new TranslatableString(getKey(@"game_version_after_update"), @"You are now running osu! {0}. diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 5452a648d1..7588b2b3c8 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -30,7 +30,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive From a3dfd99f7d5895fb0bf5f0c8398245a42b6d2848 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 16 May 2024 18:23:19 +0200 Subject: [PATCH 0828/2493] Fix discord arbitrarily refusing to work on "too short" strings Closes https://github.com/ppy/osu/issues/28192. --- osu.Desktop/DiscordRichPresence.cs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/osu.Desktop/DiscordRichPresence.cs b/osu.Desktop/DiscordRichPresence.cs index 74ebd38f2c..3e0a9099cb 100644 --- a/osu.Desktop/DiscordRichPresence.cs +++ b/osu.Desktop/DiscordRichPresence.cs @@ -164,8 +164,8 @@ namespace osu.Desktop // user activity if (activity.Value != null) { - presence.State = truncate(activity.Value.GetStatus(hideIdentifiableInformation)); - presence.Details = truncate(activity.Value.GetDetails(hideIdentifiableInformation) ?? string.Empty); + presence.State = clampLength(activity.Value.GetStatus(hideIdentifiableInformation)); + presence.Details = clampLength(activity.Value.GetDetails(hideIdentifiableInformation) ?? string.Empty); if (getBeatmapID(activity.Value) is int beatmapId && beatmapId > 0) { @@ -271,8 +271,15 @@ namespace osu.Desktop private static readonly int ellipsis_length = Encoding.UTF8.GetByteCount(new[] { '…' }); - private static string truncate(string str) + private static string clampLength(string str) { + // For whatever reason, discord decides that strings shorter than 2 characters cannot possibly be valid input, because... reasons? + // And yes, that is two *characters*, or *codepoints*, not *bytes* as further down below (as determined by empirical testing). + // That seems very questionable, and isn't even documented anywhere. So to *make it* accept such valid input, + // just tack on enough of U+200B ZERO WIDTH SPACEs at the end. + if (str.Length < 2) + return str.PadRight(2, '\u200B'); + if (Encoding.UTF8.GetByteCount(str) <= 128) return str; From 2f9d74286dd5401450f071659323415c3329d3b7 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 17 May 2024 11:15:17 +0900 Subject: [PATCH 0829/2493] Bump once more --- .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 99906f0895..ace7db82f8 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -21,7 +21,7 @@ ] }, "ppy.localisationanalyser.tools": { - "version": "2023.1117.0", + "version": "2024.517.0", "commands": [ "localisation" ] diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 7588b2b3c8..f91995feff 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -30,7 +30,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive From 61a415fed2ae74c50223015d957bcc55b8a11c5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 17 May 2024 10:32:39 +0200 Subject: [PATCH 0830/2493] Add client/server models & operations for "daily challenge" feature --- osu.Game/Online/Metadata/DailyChallengeInfo.cs | 16 ++++++++++++++++ osu.Game/Online/Metadata/IMetadataClient.cs | 6 ++++++ osu.Game/Online/Metadata/IMetadataServer.cs | 7 ++++++- osu.Game/Online/Metadata/MetadataClient.cs | 9 +++++++++ osu.Game/Online/Metadata/OnlineMetadataClient.cs | 11 +++++++++++ osu.Game/Online/Rooms/RoomCategory.cs | 3 +++ .../Tests/Visual/Metadata/TestMetadataClient.cs | 9 +++++++++ 7 files changed, 60 insertions(+), 1 deletion(-) create mode 100644 osu.Game/Online/Metadata/DailyChallengeInfo.cs diff --git a/osu.Game/Online/Metadata/DailyChallengeInfo.cs b/osu.Game/Online/Metadata/DailyChallengeInfo.cs new file mode 100644 index 0000000000..7c49556653 --- /dev/null +++ b/osu.Game/Online/Metadata/DailyChallengeInfo.cs @@ -0,0 +1,16 @@ +// 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 MessagePack; + +namespace osu.Game.Online.Metadata +{ + [MessagePackObject] + [Serializable] + public struct DailyChallengeInfo + { + [Key(0)] + public long RoomID { get; set; } + } +} diff --git a/osu.Game/Online/Metadata/IMetadataClient.cs b/osu.Game/Online/Metadata/IMetadataClient.cs index 7102554ae9..ee7a726bfc 100644 --- a/osu.Game/Online/Metadata/IMetadataClient.cs +++ b/osu.Game/Online/Metadata/IMetadataClient.cs @@ -20,5 +20,11 @@ namespace osu.Game.Online.Metadata /// Delivers an update of the of the user with the supplied . /// Task UserPresenceUpdated(int userId, UserPresence? status); + + /// + /// Delivers an update of the current "daily challenge" status. + /// Null value means there is no "daily challenge" currently active. + /// + Task DailyChallengeUpdated(DailyChallengeInfo? info); } } diff --git a/osu.Game/Online/Metadata/IMetadataServer.cs b/osu.Game/Online/Metadata/IMetadataServer.cs index 9780045333..8bf3f8f56b 100644 --- a/osu.Game/Online/Metadata/IMetadataServer.cs +++ b/osu.Game/Online/Metadata/IMetadataServer.cs @@ -7,7 +7,12 @@ using osu.Game.Users; namespace osu.Game.Online.Metadata { /// - /// Metadata server is responsible for keeping the osu! client up-to-date with any changes. + /// Metadata server is responsible for keeping the osu! client up-to-date with various real-time happenings, such as: + /// + /// beatmap updates via BSS, + /// online user activity/status updates, + /// other real-time happenings, such as current "daily challenge" status. + /// /// public interface IMetadataServer { diff --git a/osu.Game/Online/Metadata/MetadataClient.cs b/osu.Game/Online/Metadata/MetadataClient.cs index 8e99a9b2cb..b619970494 100644 --- a/osu.Game/Online/Metadata/MetadataClient.cs +++ b/osu.Game/Online/Metadata/MetadataClient.cs @@ -59,6 +59,15 @@ namespace osu.Game.Online.Metadata #endregion + #region Daily Challenge + + public abstract IBindable DailyChallengeInfo { get; } + + /// + public abstract Task DailyChallengeUpdated(DailyChallengeInfo? info); + + #endregion + #region Disconnection handling public event Action? Disconnecting; diff --git a/osu.Game/Online/Metadata/OnlineMetadataClient.cs b/osu.Game/Online/Metadata/OnlineMetadataClient.cs index 3805d12688..b94f26a71d 100644 --- a/osu.Game/Online/Metadata/OnlineMetadataClient.cs +++ b/osu.Game/Online/Metadata/OnlineMetadataClient.cs @@ -26,6 +26,9 @@ namespace osu.Game.Online.Metadata public override IBindableDictionary UserStates => userStates; private readonly BindableDictionary userStates = new BindableDictionary(); + public override IBindable DailyChallengeInfo => dailyChallengeInfo; + private readonly Bindable dailyChallengeInfo = new Bindable(); + private readonly string endpoint; private IHubClientConnector? connector; @@ -58,6 +61,7 @@ namespace osu.Game.Online.Metadata // https://github.com/dotnet/aspnetcore/issues/15198 connection.On(nameof(IMetadataClient.BeatmapSetsUpdated), ((IMetadataClient)this).BeatmapSetsUpdated); connection.On(nameof(IMetadataClient.UserPresenceUpdated), ((IMetadataClient)this).UserPresenceUpdated); + connection.On(nameof(IMetadataClient.DailyChallengeUpdated), ((IMetadataClient)this).DailyChallengeUpdated); connection.On(nameof(IStatefulUserHubClient.DisconnectRequested), ((IMetadataClient)this).DisconnectRequested); }; @@ -101,6 +105,7 @@ namespace osu.Game.Online.Metadata { isWatchingUserPresence.Value = false; userStates.Clear(); + dailyChallengeInfo.Value = null; }); return; } @@ -229,6 +234,12 @@ namespace osu.Game.Online.Metadata } } + public override Task DailyChallengeUpdated(DailyChallengeInfo? info) + { + Schedule(() => dailyChallengeInfo.Value = info); + return Task.CompletedTask; + } + public override async Task DisconnectRequested() { await base.DisconnectRequested().ConfigureAwait(false); diff --git a/osu.Game/Online/Rooms/RoomCategory.cs b/osu.Game/Online/Rooms/RoomCategory.cs index 17afb0dc7f..4534e7de59 100644 --- a/osu.Game/Online/Rooms/RoomCategory.cs +++ b/osu.Game/Online/Rooms/RoomCategory.cs @@ -13,5 +13,8 @@ namespace osu.Game.Online.Rooms [Description("Featured Artist")] FeaturedArtist, + + [Description("Daily Challenge")] + DailyChallenge, } } diff --git a/osu.Game/Tests/Visual/Metadata/TestMetadataClient.cs b/osu.Game/Tests/Visual/Metadata/TestMetadataClient.cs index 16cbf879df..b589e66d8b 100644 --- a/osu.Game/Tests/Visual/Metadata/TestMetadataClient.cs +++ b/osu.Game/Tests/Visual/Metadata/TestMetadataClient.cs @@ -21,6 +21,9 @@ namespace osu.Game.Tests.Visual.Metadata public override IBindableDictionary UserStates => userStates; private readonly BindableDictionary userStates = new BindableDictionary(); + public override IBindable DailyChallengeInfo => dailyChallengeInfo; + private readonly Bindable dailyChallengeInfo = new Bindable(); + [Resolved] private IAPIProvider api { get; set; } = null!; @@ -77,5 +80,11 @@ namespace osu.Game.Tests.Visual.Metadata => Task.FromResult(new BeatmapUpdates(Array.Empty(), queueId)); public override Task BeatmapSetsUpdated(BeatmapUpdates updates) => Task.CompletedTask; + + public override Task DailyChallengeUpdated(DailyChallengeInfo? info) + { + dailyChallengeInfo.Value = info; + return Task.CompletedTask; + } } } From a4f8ed2a0ef48d8acf77877606b9d733e4170e58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 17 May 2024 10:39:24 +0200 Subject: [PATCH 0831/2493] Add button to access daily challenge playlist from main menu --- .../UserInterface/TestSceneMainMenuButton.cs | 83 +++++++ .../UpdateableOnlineBeatmapSetCover.cs | 6 +- osu.Game/Graphics/OsuIcon.cs | 4 + osu.Game/Localisation/ButtonSystemStrings.cs | 7 +- osu.Game/Screens/Menu/ButtonSystem.cs | 57 +++-- osu.Game/Screens/Menu/DailyChallengeButton.cs | 209 ++++++++++++++++++ osu.Game/Screens/Menu/MainMenu.cs | 6 + osu.Game/Screens/Menu/MainMenuButton.cs | 183 +++++++++------ .../Screens/OnlinePlay/Playlists/Playlists.cs | 3 + 9 files changed, 476 insertions(+), 82 deletions(-) create mode 100644 osu.Game.Tests/Visual/UserInterface/TestSceneMainMenuButton.cs create mode 100644 osu.Game/Screens/Menu/DailyChallengeButton.cs diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneMainMenuButton.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneMainMenuButton.cs new file mode 100644 index 0000000000..921e28d607 --- /dev/null +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneMainMenuButton.cs @@ -0,0 +1,83 @@ +// 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 NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Game.Graphics; +using osu.Game.Localisation; +using osu.Game.Online.API; +using osu.Game.Online.Metadata; +using osu.Game.Online.Rooms; +using osu.Game.Screens.Menu; +using osuTK.Input; +using Color4 = osuTK.Graphics.Color4; + +namespace osu.Game.Tests.Visual.UserInterface +{ + [TestFixture] + public partial class TestSceneMainMenuButton : OsuTestScene + { + [Resolved] + private MetadataClient metadataClient { get; set; } = null!; + + private DummyAPIAccess dummyAPI => (DummyAPIAccess)API; + + [Test] + public void TestStandardButton() + { + AddStep("add button", () => Child = new MainMenuButton( + ButtonSystemStrings.Solo, @"button-default-select", OsuIcon.Player, new Color4(102, 68, 204, 255), _ => { }, 0, Key.P) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + ButtonSystemState = ButtonSystemState.TopLevel, + }); + } + + [Test] + public void TestDailyChallengeButton() + { + AddStep("add button", () => Child = new DailyChallengeButton(@"button-default-select", new Color4(102, 68, 204, 255), _ => { }, 0, Key.D) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + ButtonSystemState = ButtonSystemState.TopLevel, + }); + + AddStep("set up API", () => dummyAPI.HandleRequest = req => + { + switch (req) + { + case GetRoomRequest getRoomRequest: + if (getRoomRequest.RoomId != 1234) + return false; + + var beatmap = CreateAPIBeatmap(); + beatmap.OnlineID = 1001; + getRoomRequest.TriggerSuccess(new Room + { + RoomID = { Value = 1234 }, + Playlist = + { + new PlaylistItem(beatmap) + }, + EndDate = { Value = DateTimeOffset.Now.AddSeconds(30) } + }); + return true; + + default: + return false; + } + }); + + AddStep("beatmap of the day active", () => metadataClient.DailyChallengeUpdated(new DailyChallengeInfo + { + RoomID = 1234, + })); + + AddStep("beatmap of the day not active", () => metadataClient.DailyChallengeUpdated(null)); + } + } +} diff --git a/osu.Game/Beatmaps/Drawables/UpdateableOnlineBeatmapSetCover.cs b/osu.Game/Beatmaps/Drawables/UpdateableOnlineBeatmapSetCover.cs index 93b0dd5c15..2a6b6f90e3 100644 --- a/osu.Game/Beatmaps/Drawables/UpdateableOnlineBeatmapSetCover.cs +++ b/osu.Game/Beatmaps/Drawables/UpdateableOnlineBeatmapSetCover.cs @@ -43,7 +43,11 @@ namespace osu.Game.Beatmaps.Drawables protected override double TransformDuration => 400; protected override DelayedLoadWrapper CreateDelayedLoadWrapper(Func createContentFunc, double timeBeforeLoad) - => new DelayedLoadUnloadWrapper(createContentFunc, timeBeforeLoad); + => new DelayedLoadUnloadWrapper(createContentFunc, timeBeforeLoad) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }; protected override Drawable CreateDrawable(IBeatmapSetOnlineInfo model) { diff --git a/osu.Game/Graphics/OsuIcon.cs b/osu.Game/Graphics/OsuIcon.cs index 32e780f11c..9879ef5d14 100644 --- a/osu.Game/Graphics/OsuIcon.cs +++ b/osu.Game/Graphics/OsuIcon.cs @@ -120,6 +120,7 @@ namespace osu.Game.Graphics public static IconUsage Cross => get(OsuIconMapping.Cross); public static IconUsage CrossCircle => get(OsuIconMapping.CrossCircle); public static IconUsage Crown => get(OsuIconMapping.Crown); + public static IconUsage DailyChallenge => get(OsuIconMapping.DailyChallenge); public static IconUsage Debug => get(OsuIconMapping.Debug); public static IconUsage Delete => get(OsuIconMapping.Delete); public static IconUsage Details => get(OsuIconMapping.Details); @@ -218,6 +219,9 @@ namespace osu.Game.Graphics [Description(@"crown")] Crown, + [Description(@"daily-challenge")] + DailyChallenge, + [Description(@"debug")] Debug, diff --git a/osu.Game/Localisation/ButtonSystemStrings.cs b/osu.Game/Localisation/ButtonSystemStrings.cs index ba4abf63a6..b0a205eebe 100644 --- a/osu.Game/Localisation/ButtonSystemStrings.cs +++ b/osu.Game/Localisation/ButtonSystemStrings.cs @@ -1,4 +1,4 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. using osu.Framework.Localisation; @@ -54,6 +54,11 @@ namespace osu.Game.Localisation /// public static LocalisableString Exit => new TranslatableString(getKey(@"exit"), @"exit"); + /// + /// "daily challenge" + /// + public static LocalisableString DailyChallenge => new TranslatableString(getKey(@"daily_challenge"), @"daily challenge"); + private static string getKey(string key) => $@"{prefix}:{key}"; } } diff --git a/osu.Game/Screens/Menu/ButtonSystem.cs b/osu.Game/Screens/Menu/ButtonSystem.cs index 15a2740160..33d2a8d348 100644 --- a/osu.Game/Screens/Menu/ButtonSystem.cs +++ b/osu.Game/Screens/Menu/ButtonSystem.cs @@ -24,6 +24,7 @@ using osu.Game.Input; using osu.Game.Input.Bindings; using osu.Game.Localisation; using osu.Game.Online.API; +using osu.Game.Online.Rooms; using osu.Game.Overlays; using osuTK; using osuTK.Graphics; @@ -46,6 +47,7 @@ namespace osu.Game.Screens.Menu public Action? OnSettings; public Action? OnMultiplayer; public Action? OnPlaylists; + public Action? OnDailyChallenge; private readonly IBindable isIdle = new BindableBool(); @@ -102,10 +104,13 @@ namespace osu.Game.Screens.Menu buttonArea.AddRange(new Drawable[] { - new MainMenuButton(ButtonSystemStrings.Settings, string.Empty, OsuIcon.Settings, new Color4(85, 85, 85, 255), () => OnSettings?.Invoke(), -WEDGE_WIDTH, Key.O, Key.S), - backButton = new MainMenuButton(ButtonSystemStrings.Back, @"back-to-top", OsuIcon.PrevCircle, new Color4(51, 58, 94, 255), () => State = ButtonSystemState.TopLevel, - -WEDGE_WIDTH) + new MainMenuButton(ButtonSystemStrings.Settings, string.Empty, OsuIcon.Settings, new Color4(85, 85, 85, 255), _ => OnSettings?.Invoke(), Key.O, Key.S) { + Padding = new MarginPadding { Right = WEDGE_WIDTH }, + }, + backButton = new MainMenuButton(ButtonSystemStrings.Back, @"back-to-top", OsuIcon.PrevCircle, new Color4(51, 58, 94, 255), _ => State = ButtonSystemState.TopLevel) + { + Padding = new MarginPadding { Right = WEDGE_WIDTH }, VisibleStateMin = ButtonSystemState.Play, VisibleStateMax = ButtonSystemState.Edit, }, @@ -127,21 +132,31 @@ namespace osu.Game.Screens.Menu [BackgroundDependencyLoader] private void load(AudioManager audio, IdleTracker? idleTracker, GameHost host) { - buttonsPlay.Add(new MainMenuButton(ButtonSystemStrings.Solo, @"button-default-select", OsuIcon.Player, new Color4(102, 68, 204, 255), () => OnSolo?.Invoke(), WEDGE_WIDTH, Key.P)); - buttonsPlay.Add(new MainMenuButton(ButtonSystemStrings.Multi, @"button-default-select", OsuIcon.Online, new Color4(94, 63, 186, 255), onMultiplayer, 0, Key.M)); - buttonsPlay.Add(new MainMenuButton(ButtonSystemStrings.Playlists, @"button-default-select", OsuIcon.Tournament, new Color4(94, 63, 186, 255), onPlaylists, 0, Key.L)); + buttonsPlay.Add(new MainMenuButton(ButtonSystemStrings.Solo, @"button-default-select", OsuIcon.Player, new Color4(102, 68, 204, 255), _ => OnSolo?.Invoke(), Key.P) + { + Padding = new MarginPadding { Left = WEDGE_WIDTH }, + }); + buttonsPlay.Add(new MainMenuButton(ButtonSystemStrings.Multi, @"button-default-select", OsuIcon.Online, new Color4(94, 63, 186, 255), onMultiplayer, Key.M)); + buttonsPlay.Add(new MainMenuButton(ButtonSystemStrings.Playlists, @"button-default-select", OsuIcon.Tournament, new Color4(94, 63, 186, 255), onPlaylists, Key.L)); + buttonsPlay.Add(new DailyChallengeButton(@"button-default-select", new Color4(94, 63, 186, 255), onDailyChallenge, Key.D)); buttonsPlay.ForEach(b => b.VisibleState = ButtonSystemState.Play); - buttonsEdit.Add(new MainMenuButton(EditorStrings.BeatmapEditor.ToLower(), @"button-default-select", OsuIcon.Beatmap, new Color4(238, 170, 0, 255), () => OnEditBeatmap?.Invoke(), WEDGE_WIDTH, Key.B, Key.E)); - buttonsEdit.Add(new MainMenuButton(SkinEditorStrings.SkinEditor.ToLower(), @"button-default-select", OsuIcon.SkinB, new Color4(220, 160, 0, 255), () => OnEditSkin?.Invoke(), 0, Key.S)); + buttonsEdit.Add(new MainMenuButton(EditorStrings.BeatmapEditor.ToLower(), @"button-default-select", OsuIcon.Beatmap, new Color4(238, 170, 0, 255), _ => OnEditBeatmap?.Invoke(), Key.B, Key.E) + { + Padding = new MarginPadding { Left = WEDGE_WIDTH}, + }); + buttonsEdit.Add(new MainMenuButton(SkinEditorStrings.SkinEditor.ToLower(), @"button-default-select", OsuIcon.SkinB, new Color4(220, 160, 0, 255), _ => OnEditSkin?.Invoke(), Key.S)); buttonsEdit.ForEach(b => b.VisibleState = ButtonSystemState.Edit); - buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Play, @"button-play-select", OsuIcon.Logo, new Color4(102, 68, 204, 255), () => State = ButtonSystemState.Play, WEDGE_WIDTH, Key.P, Key.M, Key.L)); - buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Edit, @"button-play-select", OsuIcon.EditCircle, new Color4(238, 170, 0, 255), () => State = ButtonSystemState.Edit, 0, Key.E)); - buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Browse, @"button-default-select", OsuIcon.Beatmap, new Color4(165, 204, 0, 255), () => OnBeatmapListing?.Invoke(), 0, Key.B, Key.D)); + buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Play, @"button-play-select", OsuIcon.Logo, new Color4(102, 68, 204, 255), _ => State = ButtonSystemState.Play, Key.P, Key.M, Key.L) + { + Padding = new MarginPadding { Left = WEDGE_WIDTH }, + }); + buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Edit, @"button-play-select", OsuIcon.EditCircle, new Color4(238, 170, 0, 255), _ => State = ButtonSystemState.Edit, Key.E)); + buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Browse, @"button-default-select", OsuIcon.Beatmap, new Color4(165, 204, 0, 255), _ => OnBeatmapListing?.Invoke(), Key.B, Key.D)); if (host.CanExit) - buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Exit, string.Empty, OsuIcon.CrossCircle, new Color4(238, 51, 153, 255), () => OnExit?.Invoke(), 0, Key.Q)); + buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Exit, string.Empty, OsuIcon.CrossCircle, new Color4(238, 51, 153, 255), _ => OnExit?.Invoke(), Key.Q)); buttonArea.AddRange(buttonsPlay); buttonArea.AddRange(buttonsEdit); @@ -164,7 +179,7 @@ namespace osu.Game.Screens.Menu sampleLogoSwoosh = audio.Samples.Get(@"Menu/osu-logo-swoosh"); } - private void onMultiplayer() + private void onMultiplayer(MainMenuButton _) { if (api.State.Value != APIState.Online) { @@ -175,7 +190,7 @@ namespace osu.Game.Screens.Menu OnMultiplayer?.Invoke(); } - private void onPlaylists() + private void onPlaylists(MainMenuButton _) { if (api.State.Value != APIState.Online) { @@ -186,6 +201,20 @@ namespace osu.Game.Screens.Menu OnPlaylists?.Invoke(); } + private void onDailyChallenge(MainMenuButton button) + { + if (api.State.Value != APIState.Online) + { + loginOverlay?.Show(); + return; + } + + var dailyChallengeButton = (DailyChallengeButton)button; + + if (dailyChallengeButton.Room != null) + OnDailyChallenge?.Invoke(dailyChallengeButton.Room); + } + private void updateIdleState(bool isIdle) { if (!ReturnToTopOnIdle) diff --git a/osu.Game/Screens/Menu/DailyChallengeButton.cs b/osu.Game/Screens/Menu/DailyChallengeButton.cs new file mode 100644 index 0000000000..907fd04148 --- /dev/null +++ b/osu.Game/Screens/Menu/DailyChallengeButton.cs @@ -0,0 +1,209 @@ +// 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.Bindables; +using osu.Framework.Extensions.ObjectExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Threading; +using osu.Game.Beatmaps.Drawables; +using osu.Game.Beatmaps.Drawables.Cards; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Localisation; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Online.Metadata; +using osu.Game.Online.Rooms; +using osu.Game.Overlays; +using osuTK; +using osuTK.Graphics; +using osuTK.Input; + +namespace osu.Game.Screens.Menu +{ + public partial class DailyChallengeButton : MainMenuButton, IHasCustomTooltip + { + public Room? Room { get; private set; } + + private readonly OsuSpriteText countdown; + private ScheduledDelegate? scheduledCountdownUpdate; + + private UpdateableOnlineBeatmapSetCover cover = null!; + private IBindable info = null!; + private BufferedContainer background = null!; + + [Resolved] + private IAPIProvider api { get; set; } = null!; + + public DailyChallengeButton(string sampleName, Color4 colour, Action? clickAction = null, params Key[] triggerKeys) + : base(ButtonSystemStrings.DailyChallenge, sampleName, OsuIcon.DailyChallenge, colour, clickAction, triggerKeys) + { + BaseSize = new Vector2(ButtonSystem.BUTTON_WIDTH * 1.3f, ButtonArea.BUTTON_AREA_HEIGHT); + + Content.Add(countdown = new OsuSpriteText + { + Shadow = true, + AllowMultiline = false, + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + Margin = new MarginPadding + { + Left = -3, + Bottom = 22, + }, + Font = OsuFont.Default.With(size: 12), + Alpha = 0, + }); + } + + protected override Drawable CreateBackground(Colour4 accentColour) => background = new BufferedContainer + { + Children = new Drawable[] + { + cover = new UpdateableOnlineBeatmapSetCover + { + RelativeSizeAxes = Axes.Y, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativePositionAxes = Axes.X, + X = -0.5f, + }, + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = ColourInfo.GradientVertical(accentColour.Opacity(0), accentColour), + Blending = BlendingParameters.Additive, + }, + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = accentColour.Opacity(0.7f) + }, + }, + }; + + [BackgroundDependencyLoader] + private void load(MetadataClient metadataClient) + { + info = metadataClient.DailyChallengeInfo.GetBoundCopy(); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + info.BindValueChanged(updateDisplay, true); + FinishTransforms(true); + + cover.MoveToX(-0.5f, 10000, Easing.InOutSine) + .Then().MoveToX(0.5f, 10000, Easing.InOutSine) + .Loop(); + } + + protected override void Update() + { + base.Update(); + + cover.Width = 2 * background.DrawWidth; + } + + private void updateDisplay(ValueChangedEvent info) + { + UpdateState(); + + scheduledCountdownUpdate?.Cancel(); + scheduledCountdownUpdate = null; + + if (info.NewValue == null) + { + Room = null; + cover.OnlineInfo = TooltipContent = null; + } + else + { + var roomRequest = new GetRoomRequest(info.NewValue.Value.RoomID); + + roomRequest.Success += room => + { + Room = room; + cover.OnlineInfo = TooltipContent = room.Playlist.FirstOrDefault()?.Beatmap.BeatmapSet as APIBeatmapSet; + + updateCountdown(); + Scheduler.AddDelayed(updateCountdown, 1000, true); + }; + api.Queue(roomRequest); + } + } + + private void updateCountdown() + { + if (Room == null) + return; + + var remaining = (Room.EndDate.Value - DateTimeOffset.Now) ?? TimeSpan.Zero; + + if (remaining <= TimeSpan.Zero) + { + countdown.FadeOut(250, Easing.OutQuint); + } + else + { + if (countdown.Alpha == 0) + countdown.FadeIn(250, Easing.OutQuint); + + countdown.Text = remaining.ToString(@"hh\:mm\:ss"); + } + } + + protected override void UpdateState() + { + if (info.IsNotNull() && info.Value == null) + { + ContractStyle = 0; + State = ButtonState.Contracted; + return; + } + + base.UpdateState(); + } + + public ITooltip GetCustomTooltip() => new DailyChallengeTooltip(); + + public APIBeatmapSet? TooltipContent { get; private set; } + + internal partial class DailyChallengeTooltip : CompositeDrawable, ITooltip + { + [Cached] + private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple); + + private APIBeatmapSet? lastContent; + + [BackgroundDependencyLoader] + private void load() + { + AutoSizeAxes = Axes.Both; + } + + public void Move(Vector2 pos) => Position = pos; + + public void SetContent(APIBeatmapSet? content) + { + if (content == lastContent) + return; + + lastContent = content; + + ClearInternal(); + if (content != null) + AddInternal(new BeatmapCardNano(content)); + } + } + } +} diff --git a/osu.Game/Screens/Menu/MainMenu.cs b/osu.Game/Screens/Menu/MainMenu.cs index 235c5d5c56..722e776e3d 100644 --- a/osu.Game/Screens/Menu/MainMenu.cs +++ b/osu.Game/Screens/Menu/MainMenu.cs @@ -147,6 +147,12 @@ namespace osu.Game.Screens.Menu OnSolo = loadSoloSongSelect, OnMultiplayer = () => this.Push(new Multiplayer()), OnPlaylists = () => this.Push(new Playlists()), + OnDailyChallenge = room => + { + Playlists playlistsScreen; + this.Push(playlistsScreen = new Playlists()); + playlistsScreen.Join(room); + }, OnExit = () => { exitConfirmedViaHoldOrClick = true; diff --git a/osu.Game/Screens/Menu/MainMenuButton.cs b/osu.Game/Screens/Menu/MainMenuButton.cs index 1dc79e9b1a..fe8fb91766 100644 --- a/osu.Game/Screens/Menu/MainMenuButton.cs +++ b/osu.Game/Screens/Menu/MainMenuButton.cs @@ -38,11 +38,8 @@ namespace osu.Game.Screens.Menu public readonly Key[] TriggerKeys; - private readonly Container iconText; - private readonly Container box; - private readonly Box boxHoverLayer; - private readonly SpriteIcon icon; - private readonly string sampleName; + protected override Container Content => content; + private readonly Container content; /// /// The menu state for which we are visible for (assuming only one). @@ -59,7 +56,24 @@ namespace osu.Game.Screens.Menu public ButtonSystemState VisibleStateMin = ButtonSystemState.TopLevel; public ButtonSystemState VisibleStateMax = ButtonSystemState.TopLevel; - private readonly Action? clickAction; + public new MarginPadding Padding + { + get => Content.Padding; + set => Content.Padding = value; + } + + protected Vector2 BaseSize { get; init; } = new Vector2(ButtonSystem.BUTTON_WIDTH, ButtonArea.BUTTON_AREA_HEIGHT); + + private readonly Action? clickAction; + + private readonly Container background; + private readonly Drawable backgroundContent; + private readonly Box boxHoverLayer; + private readonly SpriteIcon icon; + + private Vector2 initialSize => BaseSize + Padding.Total; + + private readonly string sampleName; private Sample? sampleClick; private Sample? sampleHover; private SampleChannel? sampleChannel; @@ -68,9 +82,9 @@ namespace osu.Game.Screens.Menu // Allow keyboard interaction based on state rather than waiting for delayed animations. || state == ButtonState.Expanded; - public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => box.ReceivePositionalInputAt(screenSpacePos); + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => background.ReceivePositionalInputAt(screenSpacePos); - public MainMenuButton(LocalisableString text, string sampleName, IconUsage symbol, Color4 colour, Action? clickAction = null, float extraWidth = 0, params Key[] triggerKeys) + public MainMenuButton(LocalisableString text, string sampleName, IconUsage symbol, Color4 colour, Action? clickAction = null, params Key[] triggerKeys) { this.sampleName = sampleName; this.clickAction = clickAction; @@ -79,11 +93,9 @@ namespace osu.Game.Screens.Menu AutoSizeAxes = Axes.Both; Alpha = 0; - Vector2 boxSize = new Vector2(ButtonSystem.BUTTON_WIDTH + Math.Abs(extraWidth), ButtonArea.BUTTON_AREA_HEIGHT); - - Children = new Drawable[] + AddRangeInternal(new Drawable[] { - box = new Container + background = new Container { // box needs to be always present to ensure the button is always sized correctly for flow AlwaysPresent = true, @@ -98,35 +110,46 @@ namespace osu.Game.Screens.Menu }, Anchor = Anchor.Centre, Origin = Anchor.Centre, - Scale = new Vector2(0, 1), - Size = boxSize, - Shear = new Vector2(ButtonSystem.WEDGE_WIDTH / boxSize.Y, 0), Children = new[] { - new Box + backgroundContent = CreateBackground(colour).With(bg => { - EdgeSmoothness = new Vector2(1.5f, 0), - RelativeSizeAxes = Axes.Both, - Colour = colour, - }, + bg.RelativeSizeAxes = Axes.Y; + bg.X = -ButtonSystem.WEDGE_WIDTH; + bg.Anchor = Anchor.Centre; + bg.Origin = Anchor.Centre; + }), boxHoverLayer = new Box { EdgeSmoothness = new Vector2(1.5f, 0), RelativeSizeAxes = Axes.Both, Blending = BlendingParameters.Additive, Colour = Color4.White, + Depth = float.MinValue, Alpha = 0, }, } }, - iconText = new Container + content = new Container { - AutoSizeAxes = Axes.Both, - Position = new Vector2(extraWidth / 2, 0), + RelativeSizeAxes = Axes.Both, Anchor = Anchor.Centre, Origin = Anchor.Centre, Children = new Drawable[] { + new OsuSpriteText + { + Shadow = true, + AllowMultiline = false, + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + Margin = new MarginPadding + { + Left = -3, + Bottom = 7, + }, + Text = text + }, icon = new SpriteIcon { Shadow = true, @@ -136,20 +159,36 @@ namespace osu.Game.Screens.Menu Position = new Vector2(0, 0), Margin = new MarginPadding { Top = -4 }, Icon = symbol - }, - new OsuSpriteText - { - Shadow = true, - AllowMultiline = false, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Position = new Vector2(0, 35), - Margin = new MarginPadding { Left = -3 }, - Text = text } } } - }; + }); + } + + protected virtual Drawable CreateBackground(Colour4 accentColour) => new Container + { + Child = new Box + { + EdgeSmoothness = new Vector2(1.5f, 0), + RelativeSizeAxes = Axes.Both, + Colour = accentColour, + } + }; + + protected override void LoadComplete() + { + base.LoadComplete(); + + background.Size = initialSize; + background.Shear = new Vector2(ButtonSystem.WEDGE_WIDTH / initialSize.Y, 0); + + // for whatever reason, attempting to size the background "just in time" to cover the visible width + // results in gaps when the width changes are quick (only visible when testing menu at 100% speed, not visible slowed down). + // to ensure there's no missing backdrop, just use a ballpark that should be enough to always cover the width and then some. + // note that while on a code inspections it would seem that `1.5 * initialSize.X` would be enough, elastic usings are used in this button + // (which can exceed the [0;1] range during interpolation). + backgroundContent.Width = 2 * initialSize.X; + backgroundContent.Shear = -background.Shear; } private bool rightward; @@ -179,15 +218,15 @@ namespace osu.Game.Screens.Menu { if (State != ButtonState.Expanded) return true; - sampleHover?.Play(); - - box.ScaleTo(new Vector2(1.5f, 1), 500, Easing.OutElastic); - double duration = TimeUntilNextBeat; icon.ClearTransforms(); icon.RotateTo(rightward ? -BOUNCE_ROTATION : BOUNCE_ROTATION, duration, Easing.InOutSine); icon.ScaleTo(new Vector2(HOVER_SCALE, HOVER_SCALE * BOUNCE_COMPRESSION), duration, Easing.Out); + + sampleHover?.Play(); + background.ResizeTo(Vector2.Multiply(initialSize, new Vector2(1.5f, 1)), 500, Easing.OutElastic); + return true; } @@ -199,7 +238,7 @@ namespace osu.Game.Screens.Menu icon.ScaleTo(Vector2.One, 200, Easing.Out); if (State == ButtonState.Expanded) - box.ScaleTo(new Vector2(1, 1), 500, Easing.OutElastic); + background.ResizeTo(initialSize, 500, Easing.OutElastic); } [BackgroundDependencyLoader] @@ -246,7 +285,7 @@ namespace osu.Game.Screens.Menu sampleChannel = sampleClick?.GetChannel(); sampleChannel?.Play(); - clickAction?.Invoke(); + clickAction?.Invoke(this); boxHoverLayer.ClearTransforms(); boxHoverLayer.Alpha = 0.9f; @@ -254,13 +293,13 @@ namespace osu.Game.Screens.Menu } public override bool HandleNonPositionalInput => state == ButtonState.Expanded; - public override bool HandlePositionalInput => state != ButtonState.Exploded && box.Scale.X >= 0.8f; + public override bool HandlePositionalInput => state != ButtonState.Exploded && background.Width / initialSize.X >= 0.8f; public void StopSamplePlayback() => sampleChannel?.Stop(); protected override void Update() { - iconText.Alpha = Math.Clamp((box.Scale.X - 0.5f) / 0.3f, 0, 1); + content.Alpha = Math.Clamp((background.Width / initialSize.X - 0.5f) / 0.3f, 0, 1); base.Update(); } @@ -285,12 +324,12 @@ namespace osu.Game.Screens.Menu switch (ContractStyle) { default: - box.ScaleTo(new Vector2(0, 1), 500, Easing.OutExpo); + background.ResizeTo(Vector2.Multiply(initialSize, new Vector2(0, 1)), 500, Easing.OutExpo); this.FadeOut(500); break; case 1: - box.ScaleTo(new Vector2(0, 1), 400, Easing.InSine); + background.ResizeTo(Vector2.Multiply(initialSize, new Vector2(0, 1)), 400, Easing.InSine); this.FadeOut(800); break; } @@ -299,13 +338,13 @@ namespace osu.Game.Screens.Menu case ButtonState.Expanded: const int expand_duration = 500; - box.ScaleTo(new Vector2(1, 1), expand_duration, Easing.OutExpo); + background.ResizeTo(initialSize, expand_duration, Easing.OutExpo); this.FadeIn(expand_duration / 6f); break; case ButtonState.Exploded: const int explode_duration = 200; - box.ScaleTo(new Vector2(2, 1), explode_duration, Easing.OutExpo); + background.ResizeTo(Vector2.Multiply(initialSize, new Vector2(2, 1)), explode_duration, Easing.OutExpo); this.FadeOut(explode_duration / 4f * 3); break; } @@ -314,32 +353,44 @@ namespace osu.Game.Screens.Menu } } + private ButtonSystemState buttonSystemState; + public ButtonSystemState ButtonSystemState { + get => buttonSystemState; set { - ContractStyle = 0; + if (buttonSystemState == value) + return; - switch (value) - { - case ButtonSystemState.Initial: + buttonSystemState = value; + UpdateState(); + } + } + + protected virtual void UpdateState() + { + ContractStyle = 0; + + switch (ButtonSystemState) + { + case ButtonSystemState.Initial: + State = ButtonState.Contracted; + break; + + case ButtonSystemState.EnteringMode: + ContractStyle = 1; + State = ButtonState.Contracted; + break; + + default: + if (ButtonSystemState <= VisibleStateMax && ButtonSystemState >= VisibleStateMin) + State = ButtonState.Expanded; + else if (ButtonSystemState < VisibleStateMin) State = ButtonState.Contracted; - break; - - case ButtonSystemState.EnteringMode: - ContractStyle = 1; - State = ButtonState.Contracted; - break; - - default: - if (value <= VisibleStateMax && value >= VisibleStateMin) - State = ButtonState.Expanded; - else if (value < VisibleStateMin) - State = ButtonState.Contracted; - else - State = ButtonState.Exploded; - break; - } + else + State = ButtonState.Exploded; + break; } } } diff --git a/osu.Game/Screens/OnlinePlay/Playlists/Playlists.cs b/osu.Game/Screens/OnlinePlay/Playlists/Playlists.cs index f1d2384c2f..9e615ffa98 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/Playlists.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/Playlists.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.Online.Rooms; using osu.Game.Screens.OnlinePlay.Lounge; namespace osu.Game.Screens.OnlinePlay.Playlists @@ -10,5 +11,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists protected override string ScreenTitle => "Playlists"; protected override LoungeSubScreen CreateLounge() => new PlaylistsLoungeSubScreen(); + + public void Join(Room room) => Schedule(() => Lounge.Join(room, string.Empty)); } } From c9b7aaf442c89fcf4f27cfea3171ed4b4a90d8e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 17 May 2024 11:50:23 +0200 Subject: [PATCH 0832/2493] Fix formatting --- osu.Game/Screens/Menu/ButtonSystem.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Menu/ButtonSystem.cs b/osu.Game/Screens/Menu/ButtonSystem.cs index 33d2a8d348..e9fff9bb07 100644 --- a/osu.Game/Screens/Menu/ButtonSystem.cs +++ b/osu.Game/Screens/Menu/ButtonSystem.cs @@ -143,7 +143,7 @@ namespace osu.Game.Screens.Menu buttonsEdit.Add(new MainMenuButton(EditorStrings.BeatmapEditor.ToLower(), @"button-default-select", OsuIcon.Beatmap, new Color4(238, 170, 0, 255), _ => OnEditBeatmap?.Invoke(), Key.B, Key.E) { - Padding = new MarginPadding { Left = WEDGE_WIDTH}, + Padding = new MarginPadding { Left = WEDGE_WIDTH }, }); buttonsEdit.Add(new MainMenuButton(SkinEditorStrings.SkinEditor.ToLower(), @"button-default-select", OsuIcon.SkinB, new Color4(220, 160, 0, 255), _ => OnEditSkin?.Invoke(), Key.S)); buttonsEdit.ForEach(b => b.VisibleState = ButtonSystemState.Edit); From a4142937e75ec240b91f930614354dae5c63d9fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 17 May 2024 12:53:25 +0200 Subject: [PATCH 0833/2493] 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 f91995feff..821a7f1fab 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -36,7 +36,7 @@ - + From 2027d481eecdd433f91f23a498ca7c7ee81dfa6c Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 18 May 2024 06:38:25 +0300 Subject: [PATCH 0834/2493] Remove `TreatWarningsAsErrors` flags from local builds for developer convenience --- Directory.Build.props | 1 - 1 file changed, 1 deletion(-) diff --git a/Directory.Build.props b/Directory.Build.props index 2d289d0f22..5ba12b845b 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -2,7 +2,6 @@ 12.0 - true enable From 0a391d5c4ebf9bd38d6cb3b6b8f7313169d75ed8 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 18 May 2024 08:39:45 +0300 Subject: [PATCH 0835/2493] Remove duplicate back button class --- osu.Game/Screens/Footer/BackButtonV2.cs | 64 ------------------------- osu.Game/Screens/Footer/ScreenFooter.cs | 4 +- 2 files changed, 2 insertions(+), 66 deletions(-) delete mode 100644 osu.Game/Screens/Footer/BackButtonV2.cs diff --git a/osu.Game/Screens/Footer/BackButtonV2.cs b/osu.Game/Screens/Footer/BackButtonV2.cs deleted file mode 100644 index 08daa339c2..0000000000 --- a/osu.Game/Screens/Footer/BackButtonV2.cs +++ /dev/null @@ -1,64 +0,0 @@ -// 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.Extensions.Color4Extensions; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Sprites; -using osu.Game.Graphics; -using osu.Game.Graphics.Sprites; -using osu.Game.Graphics.UserInterface; -using osu.Game.Localisation; -using osuTK; -using osuTK.Graphics; - -namespace osu.Game.Screens.Footer -{ - public partial class BackButtonV2 : ShearedButton - { - // todo: see https://github.com/ppy/osu-framework/issues/3271 - private const float torus_scale_factor = 1.2f; - - public const float BUTTON_WIDTH = 240; - - public BackButtonV2() - : base(BUTTON_WIDTH, 70) - { - } - - [BackgroundDependencyLoader] - private void load() - { - ButtonContent.Child = new FillFlowContainer - { - X = -10f, - RelativeSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(20f, 0f), - Children = new Drawable[] - { - new SpriteIcon - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Size = new Vector2(20f), - Icon = FontAwesome.Solid.ChevronLeft, - }, - new OsuSpriteText - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Font = OsuFont.TorusAlternate.With(size: 20 * torus_scale_factor), - Text = CommonStrings.Back, - UseFullGlyphHeight = false, - } - } - }; - - DarkerColour = Color4Extensions.FromHex("#DE31AE"); - LighterColour = Color4Extensions.FromHex("#FF86DD"); - TextColour = Color4.White; - } - } -} diff --git a/osu.Game/Screens/Footer/ScreenFooter.cs b/osu.Game/Screens/Footer/ScreenFooter.cs index 69d5a2616c..9addda5deb 100644 --- a/osu.Game/Screens/Footer/ScreenFooter.cs +++ b/osu.Game/Screens/Footer/ScreenFooter.cs @@ -28,7 +28,7 @@ namespace osu.Game.Screens.Footer private readonly List overlays = new List(); - private BackButtonV2 backButton = null!; + private ScreenBackButton backButton = null!; private FillFlowContainer buttonsFlow = null!; private Container removedButtonsContainer = null!; private LogoTrackingContainer logoTrackingContainer = null!; @@ -71,7 +71,7 @@ namespace osu.Game.Screens.Footer Spacing = new Vector2(7, 0), AutoSizeAxes = Axes.Both }, - backButton = new BackButtonV2 + backButton = new ScreenBackButton { Margin = new MarginPadding { Bottom = 10f, Left = 12f }, Anchor = Anchor.BottomLeft, From 0af32c5d4b0092f737bb91c3cac3498aebd40df8 Mon Sep 17 00:00:00 2001 From: Aurelian Date: Sat, 18 May 2024 07:45:01 +0200 Subject: [PATCH 0836/2493] Added a minimum value for the scale calculated by the CS value. --- osu.Game/Rulesets/Objects/Legacy/LegacyRulesetExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Objects/Legacy/LegacyRulesetExtensions.cs b/osu.Game/Rulesets/Objects/Legacy/LegacyRulesetExtensions.cs index 2a5a11161b..1d3416f494 100644 --- a/osu.Game/Rulesets/Objects/Legacy/LegacyRulesetExtensions.cs +++ b/osu.Game/Rulesets/Objects/Legacy/LegacyRulesetExtensions.cs @@ -55,7 +55,7 @@ namespace osu.Game.Rulesets.Objects.Legacy // It works out to under 1 game pixel and is generally not meaningful to gameplay, but is to replay playback accuracy. const float broken_gamefield_rounding_allowance = 1.00041f; - return (float)(1.0f - 0.7f * IBeatmapDifficultyInfo.DifficultyRange(circleSize)) / 2 * (applyFudge ? broken_gamefield_rounding_allowance : 1); + return (float)Math.Max(0.02, (1.0f - 0.7f * IBeatmapDifficultyInfo.DifficultyRange(circleSize)) / 2 * (applyFudge ? broken_gamefield_rounding_allowance : 1)); } public static int CalculateDifficultyPeppyStars(BeatmapDifficulty difficulty, int objectCount, int drainLength) From a912e56ca995db8560785761209f68dcc93a0e43 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 18 May 2024 11:04:40 +0300 Subject: [PATCH 0837/2493] Fix checkboxes applying extra padding --- osu.Game/Graphics/UserInterface/OsuCheckbox.cs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/osu.Game/Graphics/UserInterface/OsuCheckbox.cs b/osu.Game/Graphics/UserInterface/OsuCheckbox.cs index b7b405a7e8..caab3d97f8 100644 --- a/osu.Game/Graphics/UserInterface/OsuCheckbox.cs +++ b/osu.Game/Graphics/UserInterface/OsuCheckbox.cs @@ -52,8 +52,6 @@ namespace osu.Game.Graphics.UserInterface AutoSizeAxes = Axes.Y; RelativeSizeAxes = Axes.X; - const float nub_padding = 5; - Children = new Drawable[] { LabelTextFlowContainer = new OsuTextFlowContainer(ApplyLabelParameters) @@ -69,15 +67,13 @@ 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.DEFAULT_EXPANDED_SIZE + nub_padding * 2 }; + LabelTextFlowContainer.Padding = new MarginPadding { Right = Nub.DEFAULT_EXPANDED_SIZE + 10f }; } else { Nub.Anchor = Anchor.CentreLeft; Nub.Origin = Anchor.CentreLeft; - Nub.Margin = new MarginPadding { Left = nub_padding }; - LabelTextFlowContainer.Padding = new MarginPadding { Left = Nub.DEFAULT_EXPANDED_SIZE + nub_padding * 2 }; + LabelTextFlowContainer.Padding = new MarginPadding { Left = Nub.DEFAULT_EXPANDED_SIZE + 10f }; } Nub.Current.BindTo(Current); From a12a20e8b503bdb2a5647bebefe05de33b0553be Mon Sep 17 00:00:00 2001 From: Fabian van Oeffelt Date: Sat, 18 May 2024 18:37:44 +0200 Subject: [PATCH 0838/2493] Change Inputkeys to Ctrl+Up/Ctrl+Down --- osu.Game/Input/Bindings/GlobalActionContainer.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Input/Bindings/GlobalActionContainer.cs b/osu.Game/Input/Bindings/GlobalActionContainer.cs index 5dacb6db4d..b0a1684512 100644 --- a/osu.Game/Input/Bindings/GlobalActionContainer.cs +++ b/osu.Game/Input/Bindings/GlobalActionContainer.cs @@ -182,8 +182,8 @@ namespace osu.Game.Input.Bindings new KeyBinding(new[] { InputKey.Shift, InputKey.F2 }, GlobalAction.SelectPreviousRandom), new KeyBinding(InputKey.F3, GlobalAction.ToggleBeatmapOptions), new KeyBinding(InputKey.BackSpace, GlobalAction.DeselectAllMods), - new KeyBinding(InputKey.PageUp, GlobalAction.IncreaseSpeed), - new KeyBinding(InputKey.PageDown, GlobalAction.DecreaseSpeed), + new KeyBinding(new[] { InputKey.Control, InputKey.Up }, GlobalAction.IncreaseSpeed), + new KeyBinding(new[] { InputKey.Control, InputKey.Down }, GlobalAction.DecreaseSpeed), }; private static IEnumerable audioControlKeyBindings => new[] From 80064c4b98d955431ac423578b3a9c55b3f4ae7e Mon Sep 17 00:00:00 2001 From: Fabian van Oeffelt Date: Sat, 18 May 2024 18:38:23 +0200 Subject: [PATCH 0839/2493] Speedchange now also works in Modselect --- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index 25293e8e20..572379ea2c 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -27,6 +27,7 @@ using osu.Game.Graphics.UserInterface; using osu.Game.Input.Bindings; using osu.Game.Localisation; using osu.Game.Rulesets.Mods; +using osu.Game.Screens.Select; using osu.Game.Utils; using osuTK; using osuTK.Input; @@ -63,6 +64,9 @@ namespace osu.Game.Overlays.Mods private Func isValidMod = _ => true; + [Resolved] + private SongSelect? songSelect { get; set; } + /// /// A function determining whether each mod in the column should be displayed. /// A return value of means that the mod is not filtered and therefore its corresponding panel should be displayed. @@ -752,6 +756,14 @@ namespace osu.Game.Overlays.Mods return true; } + + case GlobalAction.IncreaseSpeed: + songSelect!.ChangeSpeed(0.05); + return true; + + case GlobalAction.DecreaseSpeed: + songSelect!.ChangeSpeed(-0.05); + return true; } return base.OnPressed(e); From 99f30d92c84789670b40ae3989cd52b2dc3a3dc2 Mon Sep 17 00:00:00 2001 From: Fabian van Oeffelt Date: Sat, 18 May 2024 18:38:31 +0200 Subject: [PATCH 0840/2493] Add Unit Tests --- .../SongSelect/TestScenePlaySongSelect.cs | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs index e03ffd48f1..938b858110 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs @@ -87,6 +87,84 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep("delete all beatmaps", () => manager.Delete()); } + [Test] + public void TestSpeedChange() + { + createSongSelect(); + changeMods(); + + AddStep("decreasing speed without mods", () => songSelect?.ChangeSpeed(-0.05)); + AddAssert("halftime at 0.95", () => songSelect!.Mods.Value.Single() is ModHalfTime mod && mod.SpeedChange.Value == 0.95); + + AddStep("decreasing speed with halftime", () => songSelect?.ChangeSpeed(-0.05)); + AddAssert("halftime at 0.9", () => songSelect!.Mods.Value.Single() is ModHalfTime mod && mod.SpeedChange.Value == 0.9); + + AddStep("increasing speed with halftime", () => songSelect?.ChangeSpeed(+0.05)); + AddAssert("halftime at 0.95", () => songSelect!.Mods.Value.Single() is ModHalfTime mod && mod.SpeedChange.Value == 0.95); + + AddStep("increasing speed with halftime to nomod", () => songSelect?.ChangeSpeed(+0.05)); + AddAssert("no mods selected", () => songSelect!.Mods.Value.Count == 0); + + AddStep("increasing speed without mods", () => songSelect?.ChangeSpeed(+0.05)); + AddAssert("doubletime at 1.05", () => songSelect!.Mods.Value.Single() is ModDoubleTime mod && mod.SpeedChange.Value == 1.05); + + AddStep("increasing speed with doubletime", () => songSelect?.ChangeSpeed(+0.05)); + AddAssert("doubletime at 1.1", () => songSelect!.Mods.Value.Single() is ModDoubleTime mod && mod.SpeedChange.Value == 1.1); + + AddStep("decreasing speed with doubletime", () => songSelect?.ChangeSpeed(-0.05)); + AddAssert("doubletime at 1.05", () => songSelect!.Mods.Value.Single() is ModDoubleTime mod && mod.SpeedChange.Value == 1.05); + + OsuModNightcore nc = new OsuModNightcore + { + SpeedChange = { Value = 1.05 } + }; + changeMods(nc); + AddStep("increasing speed with nightcore", () => songSelect?.ChangeSpeed(+0.05)); + AddAssert("nightcore at 1.1", () => songSelect!.Mods.Value.Single() is ModNightcore mod && mod.SpeedChange.Value == 1.1); + + AddStep("decreasing speed with nightcore", () => songSelect?.ChangeSpeed(-0.05)); + AddAssert("doubletime at 1.05", () => songSelect!.Mods.Value.Single() is ModNightcore mod && mod.SpeedChange.Value == 1.05); + + AddStep("decreasing speed with nightcore to nomod", () => songSelect?.ChangeSpeed(-0.05)); + AddAssert("no mods selected", () => songSelect!.Mods.Value.Count == 0); + + AddStep("decreasing speed nomod, nightcore was selected", () => songSelect?.ChangeSpeed(-0.05)); + AddAssert("daycore at 0.95", () => songSelect!.Mods.Value.Single() is ModDaycore mod && mod.SpeedChange.Value == 0.95); + + AddStep("decreasing speed with daycore", () => songSelect?.ChangeSpeed(-0.05)); + AddAssert("daycore at 0.9", () => songSelect!.Mods.Value.Single() is ModDaycore mod && mod.SpeedChange.Value == 0.9); + + AddStep("increasing speed with daycore", () => songSelect?.ChangeSpeed(0.05)); + AddAssert("daycore at 0.95", () => songSelect!.Mods.Value.Single() is ModDaycore mod && mod.SpeedChange.Value == 0.95); + + OsuModDoubleTime dt = new OsuModDoubleTime + { + SpeedChange = { Value = 1.02 }, + AdjustPitch = { Value = true }, + }; + changeMods(dt); + AddStep("decreasing speed from doubletime 1.02 with adjustpitch enabled", () => songSelect?.ChangeSpeed(-0.05)); + AddAssert("halftime at 0.97 with adjustpitch enabled", () => songSelect!.Mods.Value.Single() is ModHalfTime mod && mod.SpeedChange.Value == 0.97 && mod.AdjustPitch.Value); + + OsuModHalfTime ht = new OsuModHalfTime + { + SpeedChange = { Value = 0.97 }, + AdjustPitch = { Value = true }, + }; + Mod[] modlist = { ht, new OsuModHardRock(), new OsuModHidden() }; + changeMods(modlist); + AddStep("decreasing speed from halftime 0.97 with adjustpitch enabled, HDHR enabled", () => songSelect?.ChangeSpeed(0.05)); + AddAssert("doubletime at 1.02 with adjustpitch enabled, HDHR still enabled", () => songSelect!.Mods.Value.Count(mod => (mod is ModDoubleTime modDt && modDt.AdjustPitch.Value && modDt.SpeedChange.Value == 1.02) || mod is ModHardRock || mod is ModHidden) == 3); + + changeMods(new ModWindUp()); + AddStep("windup active, trying to change speed", () => songSelect?.ChangeSpeed(0.05)); + AddAssert("windup still active", () => songSelect!.Mods.Value.First() is ModWindUp); + + changeMods(new ModAdaptiveSpeed()); + AddStep("adaptivespeed active, trying to change speed", () => songSelect?.ChangeSpeed(0.05)); + AddAssert("adaptivespeed still active", () => songSelect!.Mods.Value.First() is ModAdaptiveSpeed); + } + [Test] public void TestPlaceholderBeatmapPresence() { From 3fdbd735ce063653bb92b7570df716e700a9b529 Mon Sep 17 00:00:00 2001 From: Fabian van Oeffelt Date: Sat, 18 May 2024 18:40:51 +0200 Subject: [PATCH 0841/2493] change to single Function, Nightcore now switches into Daycore, keep Adjustpitch after change --- osu.Game/Screens/Select/SongSelect.cs | 182 +++++++++++++++----------- 1 file changed, 109 insertions(+), 73 deletions(-) diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index de0f24aa90..e1447b284a 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -98,6 +98,9 @@ namespace osu.Game.Screens.Select new OsuMenuItem(@"Select", MenuItemType.Highlighted, () => FinaliseSelection(getBeatmap())) }; + [Resolved] + private OsuGameBase? game { get; set; } + [Resolved] private Bindable> selectedMods { get; set; } = null!; @@ -144,6 +147,10 @@ namespace osu.Game.Screens.Select private Bindable configBackgroundBlur = null!; + private bool lastPitchState; + + private bool usedPitchMods; + [BackgroundDependencyLoader(true)] private void load(AudioManager audio, OsuColour colours, ManageCollectionsDialog? manageCollectionsDialog, DifficultyRecommender? recommender, OsuConfigManager config) { @@ -809,94 +816,123 @@ namespace osu.Game.Screens.Select return false; } - private void increaseSpeed() + public void ChangeSpeed(double delta) { - var rateAdjustStates = ModSelect.AllAvailableMods.Where(pair => pair.Mod is ModRateAdjust); - var stateDoubleTime = ModSelect.AllAvailableMods.First(pair => pair.Mod is ModDoubleTime); - bool rateModActive = ModSelect.AllAvailableMods.Count(pair => pair.Mod is ModRateAdjust && pair.Active.Value) > 0; - const double stepsize = 0.05d; - double newRate = 1d + stepsize; + // Mod Change from 0.95 DC to 1.0 none to 1.05 DT/NC ? - // If no mod rateAdjust mod is currently active activate DoubleTime with speed newRate - if (!rateModActive) - { - stateDoubleTime.Active.Value = true; - ((ModDoubleTime)stateDoubleTime.Mod).SpeedChange.Value = newRate; + if (game == null) return; + + ModNightcore modNc = (ModNightcore)((MultiMod)game.AvailableMods.Value[ModType.DifficultyIncrease].First(mod => mod is MultiMod multiMod && multiMod.Mods.Count(modType => modType is ModNightcore) > 0)).Mods.First(mod => mod is ModNightcore); + ModDoubleTime modDt = (ModDoubleTime)((MultiMod)game.AvailableMods.Value[ModType.DifficultyIncrease].First(mod => mod is MultiMod multiMod && multiMod.Mods.Count(modType => modType is ModDoubleTime) > 0)).Mods.First(mod => mod is ModDoubleTime); + ModDaycore modDc = (ModDaycore)((MultiMod)game.AvailableMods.Value[ModType.DifficultyReduction].First(mod => mod is MultiMod multiMod && multiMod.Mods.Count(modType => modType is ModDaycore) > 0)).Mods.First(mod => mod is ModDaycore); + ModHalfTime modHt = (ModHalfTime)((MultiMod)game.AvailableMods.Value[ModType.DifficultyReduction].First(mod => mod is MultiMod multiMod && multiMod.Mods.Count(modType => modType is ModHalfTime) > 0)).Mods.First(mod => mod is ModHalfTime); + bool rateModActive = selectedMods.Value.Count(mod => mod is ModRateAdjust) > 0; + bool incompatiableModActive = selectedMods.Value.Count(mod => modDt.IncompatibleMods.Count(incompatibleMod => (mod.GetType().IsSubclassOf(incompatibleMod) || mod.GetType() == incompatibleMod) && incompatibleMod != typeof(ModRateAdjust)) > 0) > 0; + double newRate = 1d + delta; + bool isPositive = delta > 0; + + if (incompatiableModActive) return; - } - // Find current active rateAdjust mod and modify speed, enable DoubleTime if necessary - foreach (var state in rateAdjustStates) + if (rateModActive) { - ModRateAdjust mod = (ModRateAdjust)state.Mod; + ModRateAdjust mod = (ModRateAdjust)selectedMods.Value.First(mod => mod is ModRateAdjust); - if (!state.Active.Value) continue; + // Find current active rateAdjust mod and modify speed, enable HalfTime if necessary + newRate = mod.SpeedChange.Value + delta; - newRate = mod.SpeedChange.Value + stepsize; - - if (mod.Acronym == "DT" || mod.Acronym == "NC") - mod.SpeedChange.Value = newRate; - else + if (newRate == 1.0) { - if (newRate == 1.0d) - state.Active.Value = false; + lastPitchState = false; + usedPitchMods = false; - if (newRate > 1d) - { - state.Active.Value = false; - stateDoubleTime.Active.Value = true; - ((ModDoubleTime)stateDoubleTime.Mod).SpeedChange.Value = newRate; - break; - } + if (mod is ModDoubleTime dtmod && dtmod.AdjustPitch.Value) lastPitchState = true; - if (newRate < 1d) - mod.SpeedChange.Value = newRate; + if (mod is ModHalfTime htmod && htmod.AdjustPitch.Value) lastPitchState = true; + + if (mod is ModNightcore || mod is ModDaycore) usedPitchMods = true; + + //Disable RateAdjustMods + selectedMods.Value = selectedMods.Value.Where(search => search is not ModRateAdjust).ToList(); + return; } - } - } - private void decreaseSpeed() - { - var rateAdjustStates = ModSelect.AllAvailableMods.Where(pair => pair.Mod is ModRateAdjust); - var stateHalfTime = ModSelect.AllAvailableMods.First(pair => pair.Mod is ModHalfTime); - bool rateModActive = ModSelect.AllAvailableMods.Count(pair => pair.Mod is ModRateAdjust && pair.Active.Value) > 0; - const double stepsize = 0.05d; - double newRate = 1d - stepsize; - - // If no mod rateAdjust mod is currently active activate HalfTime with speed newRate - if (!rateModActive) - { - stateHalfTime.Active.Value = true; - ((ModHalfTime)stateHalfTime.Mod).SpeedChange.Value = newRate; - return; - } - - // Find current active rateAdjust mod and modify speed, enable HalfTime if necessary - foreach (var state in rateAdjustStates) - { - ModRateAdjust mod = (ModRateAdjust)state.Mod; - - if (!state.Active.Value) continue; - - newRate = mod.SpeedChange.Value - stepsize; - - if (mod.Acronym == "HT" || mod.Acronym == "DC") - mod.SpeedChange.Value = newRate; - else + if (((mod is ModDoubleTime || mod is ModNightcore) && newRate < mod.SpeedChange.MinValue) + || ((mod is ModHalfTime || mod is ModDaycore) && newRate > mod.SpeedChange.MaxValue)) { - if (newRate == 1.0d) - state.Active.Value = false; + bool adjustPitch = (mod is ModDoubleTime dtmod && dtmod.AdjustPitch.Value) || (mod is ModHalfTime htmod && htmod.AdjustPitch.Value); - if (newRate < 1d) + //Disable RateAdjustMods + selectedMods.Value = selectedMods.Value.Where(search => search is not ModRateAdjust).ToList(); + + ModRateAdjust? oppositeMod = null; + + switch (mod) { - state.Active.Value = false; - stateHalfTime.Active.Value = true; - ((ModHalfTime)stateHalfTime.Mod).SpeedChange.Value = newRate; - break; + case ModDoubleTime: + modHt.AdjustPitch.Value = adjustPitch; + oppositeMod = modHt; + break; + + case ModHalfTime: + modDt.AdjustPitch.Value = adjustPitch; + oppositeMod = modDt; + break; + + case ModNightcore: + oppositeMod = modDc; + break; + + case ModDaycore: + oppositeMod = modNc; + break; } - if (newRate > 1d) - mod.SpeedChange.Value = newRate; + if (oppositeMod == null) return; + + oppositeMod.SpeedChange.Value = newRate; + selectedMods.Value = selectedMods.Value.Append(oppositeMod).ToList(); + return; + } + + if (newRate > mod.SpeedChange.MaxValue && (mod is ModDoubleTime || mod is ModNightcore)) + newRate = mod.SpeedChange.MaxValue; + + if (newRate < mod.SpeedChange.MinValue && (mod is ModHalfTime || mod is ModDaycore)) + newRate = mod.SpeedChange.MinValue; + + mod.SpeedChange.Value = newRate; + } + else + { + // If no ModRateAdjust is actived activate one + if (isPositive) + { + if (!usedPitchMods) + { + modDt.SpeedChange.Value = newRate; + modDt.AdjustPitch.Value = lastPitchState; + selectedMods.Value = selectedMods.Value.Append(modDt).ToList(); + } + else + { + modNc.SpeedChange.Value = newRate; + selectedMods.Value = selectedMods.Value.Append(modNc).ToList(); + } + } + else + { + if (!usedPitchMods) + { + modHt.SpeedChange.Value = newRate; + modHt.AdjustPitch.Value = lastPitchState; + selectedMods.Value = selectedMods.Value.Append(modHt).ToList(); + } + else + { + modDc.SpeedChange.Value = newRate; + selectedMods.Value = selectedMods.Value.Append(modDc).ToList(); + } } } } @@ -1111,11 +1147,11 @@ namespace osu.Game.Screens.Select return true; case GlobalAction.IncreaseSpeed: - increaseSpeed(); + ChangeSpeed(0.05); return true; case GlobalAction.DecreaseSpeed: - decreaseSpeed(); + ChangeSpeed(-0.05); return true; } From 614cbdf0a404487ebbeea3d98766781800a9ad0f Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Sat, 18 May 2024 22:51:58 +0300 Subject: [PATCH 0842/2493] Reduce container nesting in PathControlPointPiece --- .../Components/PathControlPointPiece.cs | 50 +++++++------------ 1 file changed, 17 insertions(+), 33 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs index c6e05d3ca3..9d819f6cc0 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs @@ -8,9 +8,9 @@ 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.Cursor; using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; using osu.Framework.Localisation; using osu.Game.Graphics; @@ -40,7 +40,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components public readonly PathControlPoint ControlPoint; private readonly T hitObject; - private readonly Container marker; + private readonly Circle circle; private readonly Drawable markerRing; [Resolved] @@ -60,38 +60,22 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components Origin = Anchor.Centre; AutoSizeAxes = Axes.Both; - InternalChildren = new Drawable[] + InternalChildren = new[] { - marker = new Container + circle = new Circle { Anchor = Anchor.Centre, Origin = Anchor.Centre, - AutoSizeAxes = Axes.Both, - Children = new[] - { - new Circle - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Size = new Vector2(20), - }, - markerRing = new CircularContainer - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Size = new Vector2(28), - Masking = true, - BorderThickness = 2, - BorderColour = Color4.White, - Alpha = 0, - Child = new Box - { - RelativeSizeAxes = Axes.Both, - Alpha = 0, - AlwaysPresent = true - } - } - } + Size = new Vector2(20), + }, + markerRing = new CircularProgress + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(28), + Alpha = 0, + InnerRadius = 0.1f, + Progress = 1 } }; } @@ -115,7 +99,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components } // The connecting path is excluded from positional input - public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => marker.ReceivePositionalInputAt(screenSpacePos); + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => circle.ReceivePositionalInputAt(screenSpacePos); protected override bool OnHover(HoverEvent e) { @@ -209,8 +193,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components if (IsHovered || IsSelected.Value) colour = colour.Lighten(1); - marker.Colour = colour; - marker.Scale = new Vector2(hitObject.Scale); + Colour = colour; + Scale = new Vector2(hitObject.Scale); } private Color4 getColourFromNodeType() From be642c8c428966665fff99b0383a9d5404da801f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 19 May 2024 09:41:08 +0200 Subject: [PATCH 0843/2493] Fix total score without mods migration failing on custom ruleset scores when custom ruleset cannot be loaded Closes https://github.com/ppy/osu/issues/28209. Yes this means that such scores will have a zero total score without mods in DB and thus might up getting their total recalculated to zero when we try a mod multiplier rebalance (unless we skip scores with zero completely I suppose). I also don't really care about that right now. --- osu.Game/Database/RealmAccess.cs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/osu.Game/Database/RealmAccess.cs b/osu.Game/Database/RealmAccess.cs index 63f61228f3..1ece81be50 100644 --- a/osu.Game/Database/RealmAccess.cs +++ b/osu.Game/Database/RealmAccess.cs @@ -1134,7 +1134,17 @@ namespace osu.Game.Database case 41: foreach (var score in migration.NewRealm.All()) - LegacyScoreDecoder.PopulateTotalScoreWithoutMods(score); + { + try + { + // this can fail e.g. if a user has a score set on a ruleset that can no longer be loaded. + LegacyScoreDecoder.PopulateTotalScoreWithoutMods(score); + } + catch (Exception ex) + { + Logger.Log($@"Failed to populate total score without mods for score {score.ID}: {ex}", LoggingTarget.Database); + } + } break; } From e4858a975dda5a07d7b39b3b0b875167ff4cf5d1 Mon Sep 17 00:00:00 2001 From: Susko3 Date: Sun, 19 May 2024 14:07:40 +0200 Subject: [PATCH 0844/2493] Show mouse and joystick settings on mobile --- osu.Game/OsuGameBase.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 5533ee8337..5e4ec5a61d 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -578,17 +578,17 @@ namespace osu.Game { case ITabletHandler th: return new TabletSettings(th); - - case MouseHandler mh: - return new MouseSettings(mh); - - case JoystickHandler jh: - return new JoystickSettings(jh); } } switch (handler) { + case MouseHandler mh: + return new MouseSettings(mh); + + case JoystickHandler jh: + return new JoystickSettings(jh); + case TouchHandler th: return new TouchSettings(th); From 04acc58b7405836a205d05c4d0e1840884385988 Mon Sep 17 00:00:00 2001 From: Susko3 Date: Sun, 19 May 2024 14:12:21 +0200 Subject: [PATCH 0845/2493] Don't show warning on android Unsure about iOS. --- .../Settings/Sections/Input/MouseSettings.cs | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs b/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs index 7805ed5834..6eb512fa35 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs @@ -105,12 +105,17 @@ namespace osu.Game.Overlays.Settings.Sections.Input highPrecisionMouse.Current.BindValueChanged(highPrecision => { - if (RuntimeInfo.OS != RuntimeInfo.Platform.Windows) + switch (RuntimeInfo.OS) { - if (highPrecision.NewValue) - highPrecisionMouse.SetNoticeText(MouseSettingsStrings.HighPrecisionPlatformWarning, true); - else - highPrecisionMouse.ClearNoticeText(); + case RuntimeInfo.Platform.Linux: + case RuntimeInfo.Platform.macOS: + case RuntimeInfo.Platform.iOS: + if (highPrecision.NewValue) + highPrecisionMouse.SetNoticeText(MouseSettingsStrings.HighPrecisionPlatformWarning, true); + else + highPrecisionMouse.ClearNoticeText(); + + break; } }, true); } From 609268786f540e42996b711eab525f1e531044c4 Mon Sep 17 00:00:00 2001 From: James Wilson Date: Sun, 19 May 2024 13:28:46 +0100 Subject: [PATCH 0846/2493] remove unneeded extra `Previous` calls from `RhythmEvaluator` --- .../Difficulty/Evaluators/RhythmEvaluator.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs index 05939bb3ab..f23e8329fa 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs @@ -66,10 +66,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators } else { - if (current.Previous(i - 1).BaseObject is Slider) // bpm change is into slider, this is easy acc window + if (currObj.BaseObject is Slider) // bpm change is into slider, this is easy acc window effectiveRatio *= 0.125; - if (current.Previous(i).BaseObject is Slider) // bpm change was from a slider, this is easier typically than circle -> circle + if (prevObj.BaseObject is Slider) // bpm change was from a slider, this is easier typically than circle -> circle effectiveRatio *= 0.25; if (previousIslandSize == islandSize) // repeated island size (ex: triplet -> triplet) From f31928875bc8510f6bfe38b95902dbb12f5e415d Mon Sep 17 00:00:00 2001 From: James Wilson Date: Sun, 19 May 2024 16:26:51 +0100 Subject: [PATCH 0847/2493] Reduce `Previous` calls in `RhythmEvaluator` by optimising loop logic --- .../Difficulty/Evaluators/RhythmEvaluator.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs index 05939bb3ab..a121b1de0b 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs @@ -36,11 +36,12 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators while (rhythmStart < historicalNoteCount - 2 && current.StartTime - current.Previous(rhythmStart).StartTime < history_time_max) rhythmStart++; + OsuDifficultyHitObject prevObj = (OsuDifficultyHitObject)current.Previous(rhythmStart); + OsuDifficultyHitObject lastObj = (OsuDifficultyHitObject)current.Previous(rhythmStart + 1); + for (int i = rhythmStart; i > 0; i--) { OsuDifficultyHitObject currObj = (OsuDifficultyHitObject)current.Previous(i - 1); - OsuDifficultyHitObject prevObj = (OsuDifficultyHitObject)current.Previous(i); - OsuDifficultyHitObject lastObj = (OsuDifficultyHitObject)current.Previous(i + 1); double currHistoricalDecay = (history_time_max - (current.StartTime - currObj.StartTime)) / history_time_max; // scales note 0 to 1 from history to now @@ -100,6 +101,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators startRatio = effectiveRatio; islandSize = 1; } + + lastObj = prevObj; + prevObj = currObj; } return Math.Sqrt(4 + rhythmComplexitySum * rhythm_multiplier) / 2; //produces multiplier that can be applied to strain. range [1, infinity) (not really though) From c03f68413a11770c6d59f3d363dc7d16f21ebdfe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 20 May 2024 09:43:25 +0200 Subject: [PATCH 0848/2493] Fix skin editor closest origin selection spazzing out on scaled sprites Closes https://github.com/ppy/osu/issues/28215. `drawable.Position` is a location in the parent's coordinate space, and `drawable.OriginPosition` is in the drawable's local space and additionally does not take scale into account. --- osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs b/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs index 75bb77fa73..28b2435346 100644 --- a/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs +++ b/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs @@ -425,9 +425,9 @@ namespace osu.Game.Overlays.SkinEditor { if (origin == drawable.Origin) return; - var previousOrigin = drawable.OriginPosition; + var previousOrigin = drawable.ToParentSpace(drawable.OriginPosition); drawable.Origin = origin; - drawable.Position += drawable.OriginPosition - previousOrigin; + drawable.Position += drawable.ToParentSpace(drawable.OriginPosition) - previousOrigin; } private static void adjustScaleFromAnchor(ref Vector2 scale, Anchor reference) From 3da3b91be51526c1d40e016ddf9002134f9d788c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 20 May 2024 10:14:08 +0200 Subject: [PATCH 0849/2493] Improve closest origin selection to include effects of rotation/flip Closes https://github.com/ppy/osu/issues/28237. Solution as proposed here: https://github.com/ppy/osu/pull/28089#issuecomment-2095372157 For flips and rotations by 90 degrees this does what you would expect it to. For arbitrary rotations it *sort of kind of* attempts to do this but the results are a bit wonky - probably still better than what was there before, though? --- .../SkinEditor/SkinSelectionHandler.cs | 36 +++++++++++++++++-- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs b/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs index 28b2435346..21909cdc10 100644 --- a/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs +++ b/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; +using osu.Framework.Extensions; using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -421,12 +422,41 @@ namespace osu.Game.Overlays.SkinEditor drawable.Position -= drawable.AnchorPosition - previousAnchor; } - private static void applyOrigin(Drawable drawable, Anchor origin) + private static void applyOrigin(Drawable drawable, Anchor screenSpaceOrigin) { - if (origin == drawable.Origin) return; + var boundingBox = drawable.ScreenSpaceDrawQuad.AABBFloat; + + var targetScreenSpacePosition = screenSpaceOrigin.PositionOnQuad(boundingBox); + + Anchor localOrigin = Anchor.TopLeft; + float smallestDistanceFromTargetPosition = float.PositiveInfinity; + + void checkOrigin(Anchor originToTest) + { + Vector2 positionToTest = drawable.ToScreenSpace(originToTest.PositionOnQuad(drawable.DrawRectangle)); + float testedDistance = Vector2.Distance(targetScreenSpacePosition, positionToTest); + + if (testedDistance < smallestDistanceFromTargetPosition) + { + localOrigin = originToTest; + smallestDistanceFromTargetPosition = testedDistance; + } + } + + checkOrigin(Anchor.TopLeft); + checkOrigin(Anchor.TopCentre); + checkOrigin(Anchor.TopRight); + + checkOrigin(Anchor.CentreLeft); + checkOrigin(Anchor.Centre); + checkOrigin(Anchor.CentreRight); + + checkOrigin(Anchor.BottomLeft); + checkOrigin(Anchor.BottomCentre); + checkOrigin(Anchor.BottomRight); var previousOrigin = drawable.ToParentSpace(drawable.OriginPosition); - drawable.Origin = origin; + drawable.Origin = localOrigin; drawable.Position += drawable.ToParentSpace(drawable.OriginPosition) - previousOrigin; } From fe0af7e720cf17fdbf49842c94ba2d05f6055daf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 20 May 2024 11:36:39 +0200 Subject: [PATCH 0850/2493] Fix unnecessary padding of empty strings for discord RPC purposes Closes https://github.com/ppy/osu/issues/28248. Destroy all software. --- osu.Desktop/DiscordRichPresence.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Desktop/DiscordRichPresence.cs b/osu.Desktop/DiscordRichPresence.cs index 3e0a9099cb..780d367900 100644 --- a/osu.Desktop/DiscordRichPresence.cs +++ b/osu.Desktop/DiscordRichPresence.cs @@ -273,7 +273,11 @@ namespace osu.Desktop private static string clampLength(string str) { - // For whatever reason, discord decides that strings shorter than 2 characters cannot possibly be valid input, because... reasons? + // Empty strings are fine to discord even though single-character strings are not. Make it make sense. + if (string.IsNullOrEmpty(str)) + return str; + + // As above, discord decides that *non-empty* strings shorter than 2 characters cannot possibly be valid input, because... reasons? // And yes, that is two *characters*, or *codepoints*, not *bytes* as further down below (as determined by empirical testing). // That seems very questionable, and isn't even documented anywhere. So to *make it* accept such valid input, // just tack on enough of U+200B ZERO WIDTH SPACEs at the end. From 85f85dee9ef28b4b65678d58f6e3f54abf0fe2f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 20 May 2024 14:46:28 +0200 Subject: [PATCH 0851/2493] Enable NRT in `TestScenePresentScore` --- .../Visual/Navigation/TestScenePresentScore.cs | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/osu.Game.Tests/Visual/Navigation/TestScenePresentScore.cs b/osu.Game.Tests/Visual/Navigation/TestScenePresentScore.cs index 212783d047..2d4302c0df 100644 --- a/osu.Game.Tests/Visual/Navigation/TestScenePresentScore.cs +++ b/osu.Game.Tests/Visual/Navigation/TestScenePresentScore.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; @@ -26,7 +24,7 @@ namespace osu.Game.Tests.Visual.Navigation { public partial class TestScenePresentScore : OsuGameTestScene { - private BeatmapSetInfo beatmap; + private BeatmapSetInfo beatmap = null!; [SetUpSteps] public new void SetUpSteps() @@ -64,7 +62,7 @@ namespace osu.Game.Tests.Visual.Navigation Ruleset = new OsuRuleset().RulesetInfo }, } - })?.Value; + })!.Value; }); } @@ -171,9 +169,9 @@ namespace osu.Game.Tests.Visual.Navigation AddUntilStep("wait for menu", () => Game.ScreenStack.CurrentScreen is MainMenu); } - private Func importScore(int i, RulesetInfo ruleset = null) + private Func importScore(int i, RulesetInfo? ruleset = null) { - ScoreInfo imported = null; + ScoreInfo? imported = null; AddStep($"import score {i}", () => { imported = Game.ScoreManager.Import(new ScoreInfo @@ -188,14 +186,14 @@ namespace osu.Game.Tests.Visual.Navigation AddAssert($"import {i} succeeded", () => imported != null); - return () => imported; + return () => imported!; } /// /// Some tests test waiting for a particular screen twice in a row, but expect a new instance each time. /// There's a case where they may succeed incorrectly if we don't compare against the previous instance. /// - private IScreen lastWaitedScreen; + private IScreen lastWaitedScreen = null!; private void presentAndConfirm(Func getImport, ScorePresentType type) { From 3b15c223be4a38c624f68abd30561638d46ea14d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 20 May 2024 14:48:02 +0200 Subject: [PATCH 0852/2493] Add failing test case --- .../Navigation/TestScenePresentScore.cs | 25 +++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Navigation/TestScenePresentScore.cs b/osu.Game.Tests/Visual/Navigation/TestScenePresentScore.cs index 2d4302c0df..2c2335de13 100644 --- a/osu.Game.Tests/Visual/Navigation/TestScenePresentScore.cs +++ b/osu.Game.Tests/Visual/Navigation/TestScenePresentScore.cs @@ -156,6 +156,27 @@ namespace osu.Game.Tests.Visual.Navigation presentAndConfirm(secondImport, type); } + [Test] + public void TestScoreRefetchIgnoresEmptyHash() + { + AddStep("enter song select", () => Game.ChildrenOfType().Single().OnSolo?.Invoke()); + AddUntilStep("song select is current", () => Game.ScreenStack.CurrentScreen is PlaySongSelect songSelect && songSelect.BeatmapSetsLoaded); + + importScore(-1, hash: string.Empty); + importScore(3, hash: @"deadbeef"); + + // oftentimes a `PresentScore()` call will be given a `ScoreInfo` which is converted from an online score, + // in which cases the hash will generally not be available. + AddStep("present score", () => Game.PresentScore(new ScoreInfo { OnlineID = 3, Hash = string.Empty })); + + AddUntilStep("wait for results", () => lastWaitedScreen != Game.ScreenStack.CurrentScreen && Game.ScreenStack.CurrentScreen is ResultsScreen); + AddUntilStep("correct score displayed", () => + { + var score = ((ResultsScreen)Game.ScreenStack.CurrentScreen).Score!; + return score.OnlineID == 3 && score.Hash == "deadbeef"; + }); + } + private void returnToMenu() { // if we don't pause, there's a chance the track may change at the main menu out of our control (due to reaching the end of the track). @@ -169,14 +190,14 @@ namespace osu.Game.Tests.Visual.Navigation AddUntilStep("wait for menu", () => Game.ScreenStack.CurrentScreen is MainMenu); } - private Func importScore(int i, RulesetInfo? ruleset = null) + private Func importScore(int i, RulesetInfo? ruleset = null, string? hash = null) { ScoreInfo? imported = null; AddStep($"import score {i}", () => { imported = Game.ScoreManager.Import(new ScoreInfo { - Hash = Guid.NewGuid().ToString(), + Hash = hash ?? Guid.NewGuid().ToString(), OnlineID = i, BeatmapInfo = beatmap.Beatmaps.First(), Ruleset = ruleset ?? new OsuRuleset().RulesetInfo, From ed498f6eed66a4c801604484dbcbea883b229cbc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 20 May 2024 14:48:36 +0200 Subject: [PATCH 0853/2493] Do not attempt to match score by equality of hash if it's empty Closes https://github.com/ppy/osu/issues/28216. The affected user's database contained six sentakki scores with an empty hash. When an online score is being imported, an online model (which does not have a hash) will be transmogrified into a `ScoreInfo` with an empty hash, which would end up accidentally matching those scores and basically breaking everything at that point. To fix, avoid attempting to match anything on empty hash. This does not break online score matching because for those cases the actual online ID of the score will be used. --- osu.Game/Scoring/ScoreManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Scoring/ScoreManager.cs b/osu.Game/Scoring/ScoreManager.cs index 0c707ffa19..df4735b5e6 100644 --- a/osu.Game/Scoring/ScoreManager.cs +++ b/osu.Game/Scoring/ScoreManager.cs @@ -88,7 +88,7 @@ namespace osu.Game.Scoring { ScoreInfo? databasedScoreInfo = null; - if (originalScoreInfo is ScoreInfo scoreInfo) + if (originalScoreInfo is ScoreInfo scoreInfo && !string.IsNullOrEmpty(scoreInfo.Hash)) databasedScoreInfo = Query(s => s.Hash == scoreInfo.Hash); if (originalScoreInfo.OnlineID > 0) From db8b72eb37464d50ec5092f7180082507e7fc2b0 Mon Sep 17 00:00:00 2001 From: Aurelian Date: Mon, 20 May 2024 16:23:16 +0200 Subject: [PATCH 0854/2493] Clamped Difficulty Ranges to [0,10] --- osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs index 6fa78fa8e6..059451e228 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs @@ -383,21 +383,24 @@ namespace osu.Game.Beatmaps.Formats switch (pair.Key) { case @"HPDrainRate": - difficulty.DrainRate = Parsing.ParseFloat(pair.Value); + difficulty.DrainRate = Math.Clamp(Parsing.ParseFloat(pair.Value), 0, 10); break; case @"CircleSize": difficulty.CircleSize = Parsing.ParseFloat(pair.Value); + //If the mode is not Mania, clamp circle size to [0,10] + if (!beatmap.BeatmapInfo.Ruleset.OnlineID.Equals(3)) + difficulty.CircleSize = Math.Clamp(difficulty.CircleSize, 0, 10); break; case @"OverallDifficulty": - difficulty.OverallDifficulty = Parsing.ParseFloat(pair.Value); + difficulty.OverallDifficulty = Math.Clamp(Parsing.ParseFloat(pair.Value), 0, 10); if (!hasApproachRate) difficulty.ApproachRate = difficulty.OverallDifficulty; break; case @"ApproachRate": - difficulty.ApproachRate = Parsing.ParseFloat(pair.Value); + difficulty.ApproachRate = Math.Clamp(Parsing.ParseFloat(pair.Value), 0, 10); hasApproachRate = true; break; From e740b8bcc358653d12ff528fb33e6c42cf505559 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 21 May 2024 14:36:11 +0800 Subject: [PATCH 0855/2493] Fix single frame glitching in skin editor https://github.com/ppy/osu/pull/28257#discussion_r1606999574 --- osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs b/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs index 21909cdc10..8fd9c1b559 100644 --- a/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs +++ b/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs @@ -455,9 +455,10 @@ namespace osu.Game.Overlays.SkinEditor checkOrigin(Anchor.BottomCentre); checkOrigin(Anchor.BottomRight); - var previousOrigin = drawable.ToParentSpace(drawable.OriginPosition); + Vector2 offset = drawable.ToParentSpace(localOrigin.PositionOnQuad(drawable.DrawRectangle)) - drawable.ToParentSpace(drawable.Origin.PositionOnQuad(drawable.DrawRectangle)); + drawable.Origin = localOrigin; - drawable.Position += drawable.ToParentSpace(drawable.OriginPosition) - previousOrigin; + drawable.Position += offset; } private static void adjustScaleFromAnchor(ref Vector2 scale, Anchor reference) From d7d569cf4e68acdbcc9cec844337f93bdb207a54 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 21 May 2024 14:34:53 +0800 Subject: [PATCH 0856/2493] Temporary rollback of framework / SDL3 --- osu.Android.props | 2 +- osu.Android/AndroidJoystickSettings.cs | 76 +++++++++++++++ osu.Android/AndroidMouseSettings.cs | 97 +++++++++++++++++++ osu.Android/OsuGameAndroid.cs | 22 +++++ osu.Desktop/OsuGameDesktop.cs | 11 +-- osu.Desktop/Program.cs | 33 +++---- .../Components/PathControlPointVisualiser.cs | 2 +- osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs | 3 +- osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs | 3 +- .../Screens/Ladder/LadderDragContainer.cs | 2 +- osu.Game/Database/EmptyRealmSet.cs | 5 - .../UserInterface/ExpandableSlider.cs | 8 +- .../Graphics/UserInterface/OsuSliderBar.cs | 9 +- .../Graphics/UserInterface/OsuTabControl.cs | 24 ++--- .../Graphics/UserInterface/PageTabControl.cs | 14 +-- .../UserInterface/RoundedSliderBar.cs | 5 +- .../UserInterface/ShearedSliderBar.cs | 5 +- .../UserInterfaceV2/LabelledSliderBar.cs | 4 +- .../UserInterfaceV2/SliderWithTextBoxInput.cs | 8 +- osu.Game/OsuGameBase.cs | 12 +-- .../BeatmapListingCardSizeTabControl.cs | 12 +-- ...BeatmapSearchMultipleSelectionFilterRow.cs | 4 - .../Overlays/BeatmapListing/FilterTabItem.cs | 12 +-- .../BeatmapSet/BeatmapRulesetSelector.cs | 2 +- .../OverlayPanelDisplayStyleControl.cs | 14 +-- osu.Game/Overlays/OverlayRulesetTabItem.cs | 14 +-- osu.Game/Overlays/OverlayStreamItem.cs | 12 +-- osu.Game/Overlays/OverlayTabControl.cs | 14 +-- .../Overlays/Settings/Sections/SizeSlider.cs | 3 +- .../Settings/SettingsPercentageSlider.cs | 4 +- osu.Game/Overlays/Settings/SettingsSlider.cs | 6 +- .../Toolbar/ToolbarRulesetSelector.cs | 16 ++- .../Toolbar/ToolbarRulesetTabButton.cs | 12 --- osu.Game/Rulesets/Mods/DifficultyBindable.cs | 2 +- .../Objects/Drawables/DrawableHitObject.cs | 3 +- .../Scoring/LegacyDrainingHealthProcessor.cs | 7 -- .../Rulesets/UI/FrameStabilityContainer.cs | 2 +- .../Timeline/TimelineTickDisplay.cs | 3 +- osu.Game/Screens/Edit/Editor.cs | 14 ++- .../IndeterminateSliderWithTextBoxInput.cs | 8 +- .../Match/Components/MatchTypePicker.cs | 11 +-- .../Play/PlayerSettings/PlayerSliderBar.cs | 4 +- osu.Game/osu.Game.csproj | 4 +- osu.iOS.props | 2 +- 44 files changed, 304 insertions(+), 226 deletions(-) create mode 100644 osu.Android/AndroidJoystickSettings.cs create mode 100644 osu.Android/AndroidMouseSettings.cs diff --git a/osu.Android.props b/osu.Android.props index e20ac2e0b7..2d7a9d2652 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -10,7 +10,7 @@ true - + diff --git a/osu.iOS.props b/osu.iOS.props index 103ef50e0c..b2e3fc0779 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -23,6 +23,6 @@ iossimulator-x64 - + From 1127a69359f6eb9305d74a85dff8579278135997 Mon Sep 17 00:00:00 2001 From: Aurelian Date: Tue, 21 May 2024 10:15:53 +0200 Subject: [PATCH 0857/2493] Moved DIfficulty Clamping to occur after the file has been parsed This is to handle potential issues with the ruleset being parsed after circle size has been parsed. --- .../Beatmaps/Formats/LegacyBeatmapDecoder.cs | 35 ++++++++++++++----- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs index 059451e228..2acabe2518 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs @@ -85,6 +85,8 @@ namespace osu.Game.Beatmaps.Formats base.ParseStreamInto(stream, beatmap); + applyDifficultyRestrictions(beatmap.Difficulty); + flushPendingPoints(); // Objects may be out of order *only* if a user has manually edited an .osu file. @@ -102,6 +104,26 @@ namespace osu.Game.Beatmaps.Formats } } + /// + /// Clamp Difficulty settings to be within the normal range. + /// + private void applyDifficultyRestrictions(BeatmapDifficulty difficulty) + { + difficulty.DrainRate = Math.Clamp(difficulty.DrainRate, 0, 10); + //If the mode is not Mania, clamp circle size to [0,10] + if (!beatmap.BeatmapInfo.Ruleset.OnlineID.Equals(3)) + difficulty.CircleSize = Math.Clamp(difficulty.CircleSize, 0, 10); + //If it is Mania, it must be within [1,20] - dual stages with 10 keys each. + //The lower bound should be 4, but there are ranked maps that are lower than this. + else + difficulty.CircleSize = Math.Clamp(difficulty.CircleSize, 1, 20); + difficulty.OverallDifficulty = Math.Clamp(difficulty.OverallDifficulty, 0, 10); + difficulty.ApproachRate = Math.Clamp(difficulty.ApproachRate, 0, 10); + + difficulty.SliderMultiplier = Math.Clamp(difficulty.SliderMultiplier, 0.4, 3.6); + difficulty.SliderTickRate = Math.Clamp(difficulty.SliderTickRate, 0.5, 8); + } + /// /// Processes the beatmap such that a new combo is started the first hitobject following each break. /// @@ -383,33 +405,30 @@ namespace osu.Game.Beatmaps.Formats switch (pair.Key) { case @"HPDrainRate": - difficulty.DrainRate = Math.Clamp(Parsing.ParseFloat(pair.Value), 0, 10); + difficulty.DrainRate = Parsing.ParseFloat(pair.Value); break; case @"CircleSize": difficulty.CircleSize = Parsing.ParseFloat(pair.Value); - //If the mode is not Mania, clamp circle size to [0,10] - if (!beatmap.BeatmapInfo.Ruleset.OnlineID.Equals(3)) - difficulty.CircleSize = Math.Clamp(difficulty.CircleSize, 0, 10); break; case @"OverallDifficulty": - difficulty.OverallDifficulty = Math.Clamp(Parsing.ParseFloat(pair.Value), 0, 10); + difficulty.OverallDifficulty = Parsing.ParseFloat(pair.Value); if (!hasApproachRate) difficulty.ApproachRate = difficulty.OverallDifficulty; break; case @"ApproachRate": - difficulty.ApproachRate = Math.Clamp(Parsing.ParseFloat(pair.Value), 0, 10); + difficulty.ApproachRate = Parsing.ParseFloat(pair.Value); hasApproachRate = true; break; case @"SliderMultiplier": - difficulty.SliderMultiplier = Math.Clamp(Parsing.ParseDouble(pair.Value), 0.4, 3.6); + difficulty.SliderMultiplier = Parsing.ParseDouble(pair.Value); break; case @"SliderTickRate": - difficulty.SliderTickRate = Math.Clamp(Parsing.ParseDouble(pair.Value), 0.5, 8); + difficulty.SliderTickRate = Parsing.ParseDouble(pair.Value); break; } } From c1efcc054cf1594d3d883fcf66ab67b811d4de81 Mon Sep 17 00:00:00 2001 From: danielthirtle Date: Tue, 21 May 2024 21:03:53 +1200 Subject: [PATCH 0858/2493] Change miss penalty (nerf longer maps) --- osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index 47f6770ce5..36768967ff 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -261,7 +261,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty // Miss penalty assumes that a player will miss on the hardest parts of a map, // so we use the amount of relatively difficult sections to adjust miss penalty // to make it more punishing on maps with lower amount of hard sections. - private double calculateMissPenalty(double missCount, double difficultStrainCount) => 0.94 / ((missCount / (2 * Math.Sqrt(difficultStrainCount))) + 1); + private double calculateMissPenalty(double missCount, double difficultStrainCount) => 0.96 / ((missCount / (4 * Math.Pow(Math.Log(difficultStrainCount), 0.94))) + 1); private double getComboScalingFactor(OsuDifficultyAttributes attributes) => attributes.MaxCombo <= 0 ? 1.0 : Math.Min(Math.Pow(scoreMaxCombo, 0.8) / Math.Pow(attributes.MaxCombo, 0.8), 1.0); private int totalHits => countGreat + countOk + countMeh + countMiss; } From 45fcbea182d1076ae9239984f76ed9b36b458c5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 17 May 2024 14:43:06 +0200 Subject: [PATCH 0859/2493] Compute total score without mods during standardised score conversion This is going to be used by server-side flows. Note that the server-side overload of `UpdateFromLegacy()` was not calling `LegacyScoreDecoder.PopulateTotalScoreWithoutMods()`. Computing the score without mods inline reduces reflection overheads from constructing mod instances, which feels pretty important for server-side flows. There is one weird kink in the treatment of stable scores with score V2 active - they get the *legacy* multipliers unapplied for them because that made the most sense. For all intents and purposes this matters mostly for client-side replays with score V2. I'm not sure whether scores with SV2 ever make it to submission in stable. There may be minute differences in converted score due to rounding shenanigans but I don't think it's worth doing a reverify for this. --- .../StandardisedScoreMigrationTools.cs | 67 ++++++++++--------- 1 file changed, 34 insertions(+), 33 deletions(-) diff --git a/osu.Game/Database/StandardisedScoreMigrationTools.cs b/osu.Game/Database/StandardisedScoreMigrationTools.cs index 7d09ebdb40..db44731bed 100644 --- a/osu.Game/Database/StandardisedScoreMigrationTools.cs +++ b/osu.Game/Database/StandardisedScoreMigrationTools.cs @@ -16,7 +16,6 @@ using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring.Legacy; using osu.Game.Scoring; -using osu.Game.Scoring.Legacy; namespace osu.Game.Database { @@ -248,8 +247,7 @@ namespace osu.Game.Database // warning: ordering is important here - both total score and ranks are dependent on accuracy! score.Accuracy = computeAccuracy(score, scoreProcessor); score.Rank = computeRank(score, scoreProcessor); - score.TotalScore = convertFromLegacyTotalScore(score, ruleset, beatmap); - LegacyScoreDecoder.PopulateTotalScoreWithoutMods(score); + (score.TotalScoreWithoutMods, score.TotalScore) = convertFromLegacyTotalScore(score, ruleset, beatmap); } /// @@ -273,7 +271,7 @@ namespace osu.Game.Database // warning: ordering is important here - both total score and ranks are dependent on accuracy! score.Accuracy = computeAccuracy(score, scoreProcessor); score.Rank = computeRank(score, scoreProcessor); - score.TotalScore = convertFromLegacyTotalScore(score, ruleset, difficulty, attributes); + (score.TotalScoreWithoutMods, score.TotalScore) = convertFromLegacyTotalScore(score, ruleset, difficulty, attributes); } /// @@ -283,17 +281,13 @@ namespace osu.Game.Database /// The in which the score was set. /// The applicable for this score. /// The standardised total score. - private static long convertFromLegacyTotalScore(ScoreInfo score, Ruleset ruleset, WorkingBeatmap beatmap) + private static (long withoutMods, long withMods) convertFromLegacyTotalScore(ScoreInfo score, Ruleset ruleset, WorkingBeatmap beatmap) { if (!score.IsLegacyScore) - return score.TotalScore; + return (score.TotalScoreWithoutMods, score.TotalScore); if (ruleset is not ILegacyRuleset legacyRuleset) - return score.TotalScore; - - var mods = score.Mods; - if (mods.Any(mod => mod is ModScoreV2)) - return score.TotalScore; + return (score.TotalScoreWithoutMods, score.TotalScore); var playableBeatmap = beatmap.GetPlayableBeatmap(ruleset.RulesetInfo, score.Mods); @@ -302,8 +296,13 @@ namespace osu.Game.Database ILegacyScoreSimulator sv1Simulator = legacyRuleset.CreateLegacyScoreSimulator(); LegacyScoreAttributes attributes = sv1Simulator.Simulate(beatmap, playableBeatmap); + var legacyBeatmapConversionDifficultyInfo = LegacyBeatmapConversionDifficultyInfo.FromBeatmap(beatmap.Beatmap); - return convertFromLegacyTotalScore(score, ruleset, LegacyBeatmapConversionDifficultyInfo.FromBeatmap(beatmap.Beatmap), attributes); + var mods = score.Mods; + if (mods.Any(mod => mod is ModScoreV2)) + return ((long)Math.Round(score.TotalScore / sv1Simulator.GetLegacyScoreMultiplier(mods, legacyBeatmapConversionDifficultyInfo)), score.TotalScore); + + return convertFromLegacyTotalScore(score, ruleset, legacyBeatmapConversionDifficultyInfo, attributes); } /// @@ -314,15 +313,15 @@ namespace osu.Game.Database /// The beatmap difficulty. /// The legacy scoring attributes for the beatmap which the score was set on. /// The standardised total score. - private static long convertFromLegacyTotalScore(ScoreInfo score, Ruleset ruleset, LegacyBeatmapConversionDifficultyInfo difficulty, LegacyScoreAttributes attributes) + private static (long withoutMods, long withMods) convertFromLegacyTotalScore(ScoreInfo score, Ruleset ruleset, LegacyBeatmapConversionDifficultyInfo difficulty, LegacyScoreAttributes attributes) { if (!score.IsLegacyScore) - return score.TotalScore; + return (score.TotalScoreWithoutMods, score.TotalScore); Debug.Assert(score.LegacyTotalScore != null); if (ruleset is not ILegacyRuleset legacyRuleset) - return score.TotalScore; + return (score.TotalScoreWithoutMods, score.TotalScore); double legacyModMultiplier = legacyRuleset.CreateLegacyScoreSimulator().GetLegacyScoreMultiplier(score.Mods, difficulty); int maximumLegacyAccuracyScore = attributes.AccuracyScore; @@ -354,17 +353,18 @@ namespace osu.Game.Database double modMultiplier = score.Mods.Select(m => m.ScoreMultiplier).Aggregate(1.0, (c, n) => c * n); - long convertedTotalScore; + long convertedTotalScoreWithoutMods; switch (score.Ruleset.OnlineID) { case 0: if (score.MaxCombo == 0 || score.Accuracy == 0) { - return (long)Math.Round(( + convertedTotalScoreWithoutMods = (long)Math.Round( 0 + 500000 * Math.Pow(score.Accuracy, 5) - + bonusProportion) * modMultiplier); + + bonusProportion); + break; } // see similar check above. @@ -372,10 +372,11 @@ namespace osu.Game.Database // are either pointless or wildly wrong. if (maximumLegacyComboScore + maximumLegacyBonusScore == 0) { - return (long)Math.Round(( + convertedTotalScoreWithoutMods = (long)Math.Round( 500000 * comboProportion // as above, zero if mods result in zero multiplier, one otherwise + 500000 * Math.Pow(score.Accuracy, 5) - + bonusProportion) * modMultiplier); + + bonusProportion); + break; } // Assumptions: @@ -472,17 +473,17 @@ namespace osu.Game.Database double newComboScoreProportion = estimatedComboPortionInStandardisedScore / maximumAchievableComboPortionInStandardisedScore; - convertedTotalScore = (long)Math.Round(( + convertedTotalScoreWithoutMods = (long)Math.Round( 500000 * newComboScoreProportion * score.Accuracy + 500000 * Math.Pow(score.Accuracy, 5) - + bonusProportion) * modMultiplier); + + bonusProportion); break; case 1: - convertedTotalScore = (long)Math.Round(( + convertedTotalScoreWithoutMods = (long)Math.Round( 250000 * comboProportion + 750000 * Math.Pow(score.Accuracy, 3.6) - + bonusProportion) * modMultiplier); + + bonusProportion); break; case 2: @@ -507,28 +508,28 @@ namespace osu.Game.Database ? 0 : (double)score.Statistics.GetValueOrDefault(HitResult.SmallTickHit) / score.MaximumStatistics.GetValueOrDefault(HitResult.SmallTickHit); - convertedTotalScore = (long)Math.Round(( + convertedTotalScoreWithoutMods = (long)Math.Round( comboPortion * estimateComboProportionForCatch(attributes.MaxCombo, score.MaxCombo, score.Statistics.GetValueOrDefault(HitResult.Miss)) + dropletsPortion * dropletsHit - + bonusProportion) * modMultiplier); + + bonusProportion); break; case 3: - convertedTotalScore = (long)Math.Round(( + convertedTotalScoreWithoutMods = (long)Math.Round( 850000 * comboProportion + 150000 * Math.Pow(score.Accuracy, 2 + 2 * score.Accuracy) - + bonusProportion) * modMultiplier); + + bonusProportion); break; default: - convertedTotalScore = score.TotalScore; - break; + return (score.TotalScoreWithoutMods, score.TotalScore); } - if (convertedTotalScore < 0) - throw new InvalidOperationException($"Total score conversion operation returned invalid total of {convertedTotalScore}"); + if (convertedTotalScoreWithoutMods < 0) + throw new InvalidOperationException($"Total score conversion operation returned invalid total of {convertedTotalScoreWithoutMods}"); - return convertedTotalScore; + long convertedTotalScore = (long)Math.Round(convertedTotalScoreWithoutMods * modMultiplier); + return (convertedTotalScoreWithoutMods, convertedTotalScore); } /// From 148afd120127c58655ed35190fb7456ce1f0e973 Mon Sep 17 00:00:00 2001 From: Fabian van Oeffelt Date: Tue, 21 May 2024 14:47:34 +0200 Subject: [PATCH 0860/2493] Change Speedchange behaviour to keep changing while holding key, Add Toast to nofity user what just happend --- osu.Game/Localisation/ToastStrings.cs | 5 +++++ osu.Game/Overlays/OSD/SpeedChangeToast.cs | 17 +++++++++++++++ osu.Game/Screens/Select/SongSelect.cs | 26 ++++++++++++++++------- 3 files changed, 40 insertions(+), 8 deletions(-) create mode 100644 osu.Game/Overlays/OSD/SpeedChangeToast.cs diff --git a/osu.Game/Localisation/ToastStrings.cs b/osu.Game/Localisation/ToastStrings.cs index da798a3937..33027966dd 100644 --- a/osu.Game/Localisation/ToastStrings.cs +++ b/osu.Game/Localisation/ToastStrings.cs @@ -49,6 +49,11 @@ namespace osu.Game.Localisation /// public static LocalisableString UrlCopied => new TranslatableString(getKey(@"url_copied"), @"URL copied"); + /// + /// "Speed Changed" + /// + public static LocalisableString SpeedChanged => new TranslatableString(getKey(@"speed_changed"), @"Speed Changed"); + private static string getKey(string key) => $@"{prefix}:{key}"; } } diff --git a/osu.Game/Overlays/OSD/SpeedChangeToast.cs b/osu.Game/Overlays/OSD/SpeedChangeToast.cs new file mode 100644 index 0000000000..73ba23622b --- /dev/null +++ b/osu.Game/Overlays/OSD/SpeedChangeToast.cs @@ -0,0 +1,17 @@ +// 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.Configuration; +using osu.Game.Input.Bindings; +using osu.Game.Localisation; + +namespace osu.Game.Overlays.OSD +{ + public partial class SpeedChangeToast : Toast + { + public SpeedChangeToast(OsuConfigManager config, double delta) + : base(CommonStrings.Beatmaps, ToastStrings.SpeedChanged, config.LookupKeyBindings(GlobalAction.IncreaseSpeed) + " / " + config.LookupKeyBindings(GlobalAction.DecreaseSpeed)) + { + } + } +} diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index e1447b284a..7eb2be9100 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -30,6 +30,7 @@ using osu.Game.Graphics.UserInterface; using osu.Game.Input.Bindings; using osu.Game.Overlays; using osu.Game.Overlays.Mods; +using osu.Game.Overlays.OSD; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Screens.Backgrounds; @@ -151,6 +152,12 @@ namespace osu.Game.Screens.Select private bool usedPitchMods; + [Resolved] + private OnScreenDisplay? onScreenDisplay { get; set; } + + [Resolved] + private OsuConfigManager? config { get; set; } + [BackgroundDependencyLoader(true)] private void load(AudioManager audio, OsuColour colours, ManageCollectionsDialog? manageCollectionsDialog, DifficultyRecommender? recommender, OsuConfigManager config) { @@ -819,7 +826,7 @@ namespace osu.Game.Screens.Select public void ChangeSpeed(double delta) { // Mod Change from 0.95 DC to 1.0 none to 1.05 DT/NC ? - + onScreenDisplay?.Display(new SpeedChangeToast(config!, delta)); if (game == null) return; ModNightcore modNc = (ModNightcore)((MultiMod)game.AvailableMods.Value[ModType.DifficultyIncrease].First(mod => mod is MultiMod multiMod && multiMod.Mods.Count(modType => modType is ModNightcore) > 0)).Mods.First(mod => mod is ModNightcore); @@ -1135,17 +1142,10 @@ namespace osu.Game.Screens.Select public virtual bool OnPressed(KeyBindingPressEvent e) { - if (e.Repeat) - return false; - if (!this.IsCurrentScreen()) return false; switch (e.Action) { - case GlobalAction.Select: - FinaliseSelection(); - return true; - case GlobalAction.IncreaseSpeed: ChangeSpeed(0.05); return true; @@ -1155,6 +1155,16 @@ namespace osu.Game.Screens.Select return true; } + if (e.Repeat) + return false; + + switch (e.Action) + { + case GlobalAction.Select: + FinaliseSelection(); + return true; + } + return false; } From 3403789c6fef04827679b27715b653778b7d0aed Mon Sep 17 00:00:00 2001 From: Fabian van Oeffelt Date: Tue, 21 May 2024 16:11:20 +0200 Subject: [PATCH 0861/2493] Toast now only shows when speed is actually changed --- osu.Game/Screens/Select/SongSelect.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index 7eb2be9100..b78134392b 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -825,8 +825,6 @@ namespace osu.Game.Screens.Select public void ChangeSpeed(double delta) { - // Mod Change from 0.95 DC to 1.0 none to 1.05 DT/NC ? - onScreenDisplay?.Display(new SpeedChangeToast(config!, delta)); if (game == null) return; ModNightcore modNc = (ModNightcore)((MultiMod)game.AvailableMods.Value[ModType.DifficultyIncrease].First(mod => mod is MultiMod multiMod && multiMod.Mods.Count(modType => modType is ModNightcore) > 0)).Mods.First(mod => mod is ModNightcore); @@ -841,6 +839,8 @@ namespace osu.Game.Screens.Select if (incompatiableModActive) return; + onScreenDisplay?.Display(new SpeedChangeToast(config!, delta)); + if (rateModActive) { ModRateAdjust mod = (ModRateAdjust)selectedMods.Value.First(mod => mod is ModRateAdjust); From 99d99cede03993796aa4f8fe5f1d344a9cfe9472 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 22 May 2024 11:59:34 +0800 Subject: [PATCH 0862/2493] Basic cleanup Before I gave up on attempting to fix the method. --- osu.Game/Screens/Select/SongSelect.cs | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index b78134392b..18d5799bae 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -100,7 +100,7 @@ namespace osu.Game.Screens.Select }; [Resolved] - private OsuGameBase? game { get; set; } + private OsuGameBase game { get; set; } = null!; [Resolved] private Bindable> selectedMods { get; set; } = null!; @@ -156,7 +156,7 @@ namespace osu.Game.Screens.Select private OnScreenDisplay? onScreenDisplay { get; set; } [Resolved] - private OsuConfigManager? config { get; set; } + private OsuConfigManager config { get; set; } = null!; [BackgroundDependencyLoader(true)] private void load(AudioManager audio, OsuColour colours, ManageCollectionsDialog? manageCollectionsDialog, DifficultyRecommender? recommender, OsuConfigManager config) @@ -825,21 +825,19 @@ namespace osu.Game.Screens.Select public void ChangeSpeed(double delta) { - if (game == null) return; - ModNightcore modNc = (ModNightcore)((MultiMod)game.AvailableMods.Value[ModType.DifficultyIncrease].First(mod => mod is MultiMod multiMod && multiMod.Mods.Count(modType => modType is ModNightcore) > 0)).Mods.First(mod => mod is ModNightcore); ModDoubleTime modDt = (ModDoubleTime)((MultiMod)game.AvailableMods.Value[ModType.DifficultyIncrease].First(mod => mod is MultiMod multiMod && multiMod.Mods.Count(modType => modType is ModDoubleTime) > 0)).Mods.First(mod => mod is ModDoubleTime); ModDaycore modDc = (ModDaycore)((MultiMod)game.AvailableMods.Value[ModType.DifficultyReduction].First(mod => mod is MultiMod multiMod && multiMod.Mods.Count(modType => modType is ModDaycore) > 0)).Mods.First(mod => mod is ModDaycore); ModHalfTime modHt = (ModHalfTime)((MultiMod)game.AvailableMods.Value[ModType.DifficultyReduction].First(mod => mod is MultiMod multiMod && multiMod.Mods.Count(modType => modType is ModHalfTime) > 0)).Mods.First(mod => mod is ModHalfTime); bool rateModActive = selectedMods.Value.Count(mod => mod is ModRateAdjust) > 0; - bool incompatiableModActive = selectedMods.Value.Count(mod => modDt.IncompatibleMods.Count(incompatibleMod => (mod.GetType().IsSubclassOf(incompatibleMod) || mod.GetType() == incompatibleMod) && incompatibleMod != typeof(ModRateAdjust)) > 0) > 0; + bool incompatibleModActive = selectedMods.Value.Count(mod => modDt.IncompatibleMods.Count(incompatibleMod => (mod.GetType().IsSubclassOf(incompatibleMod) || mod.GetType() == incompatibleMod) && incompatibleMod != typeof(ModRateAdjust)) > 0) > 0; double newRate = 1d + delta; bool isPositive = delta > 0; - if (incompatiableModActive) + if (incompatibleModActive) return; - onScreenDisplay?.Display(new SpeedChangeToast(config!, delta)); + onScreenDisplay?.Display(new SpeedChangeToast(config, delta)); if (rateModActive) { @@ -912,7 +910,7 @@ namespace osu.Game.Screens.Select } else { - // If no ModRateAdjust is actived activate one + // If no ModRateAdjust is active, activate one if (isPositive) { if (!usedPitchMods) From 02a388cba6493207a170728abd607d80bfdecb3a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 22 May 2024 12:03:48 +0800 Subject: [PATCH 0863/2493] Fix enum not being at end (and adjust naming) --- osu.Game/Input/Bindings/GlobalActionContainer.cs | 16 ++++++++-------- .../GlobalActionKeyBindingStrings.cs | 8 ++++---- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 4 ++-- osu.Game/Overlays/OSD/SpeedChangeToast.cs | 2 +- osu.Game/Screens/Select/SongSelect.cs | 4 ++-- 5 files changed, 17 insertions(+), 17 deletions(-) diff --git a/osu.Game/Input/Bindings/GlobalActionContainer.cs b/osu.Game/Input/Bindings/GlobalActionContainer.cs index b0a1684512..09db7461d6 100644 --- a/osu.Game/Input/Bindings/GlobalActionContainer.cs +++ b/osu.Game/Input/Bindings/GlobalActionContainer.cs @@ -182,8 +182,8 @@ namespace osu.Game.Input.Bindings new KeyBinding(new[] { InputKey.Shift, InputKey.F2 }, GlobalAction.SelectPreviousRandom), new KeyBinding(InputKey.F3, GlobalAction.ToggleBeatmapOptions), new KeyBinding(InputKey.BackSpace, GlobalAction.DeselectAllMods), - new KeyBinding(new[] { InputKey.Control, InputKey.Up }, GlobalAction.IncreaseSpeed), - new KeyBinding(new[] { InputKey.Control, InputKey.Down }, GlobalAction.DecreaseSpeed), + new KeyBinding(new[] { InputKey.Control, InputKey.Up }, GlobalAction.IncreaseModSpeed), + new KeyBinding(new[] { InputKey.Control, InputKey.Down }, GlobalAction.DecreaseModSpeed), }; private static IEnumerable audioControlKeyBindings => new[] @@ -411,12 +411,6 @@ namespace osu.Game.Input.Bindings [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorToggleRotateControl))] EditorToggleRotateControl, - [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.IncreaseSpeed))] - IncreaseSpeed, - - [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.DecreaseSpeed))] - DecreaseSpeed, - [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.IncreaseOffset))] IncreaseOffset, @@ -428,6 +422,12 @@ namespace osu.Game.Input.Bindings [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.StepReplayBackward))] StepReplayBackward, + + [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.IncreaseModSpeed))] + IncreaseModSpeed, + + [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.DecreaseModSpeed))] + DecreaseModSpeed, } public enum GlobalActionCategory diff --git a/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs b/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs index d0cbf52f07..18a1d3e4fe 100644 --- a/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs +++ b/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs @@ -370,14 +370,14 @@ namespace osu.Game.Localisation public static LocalisableString EditorToggleRotateControl => new TranslatableString(getKey(@"editor_toggle_rotate_control"), @"Toggle rotate control"); /// - /// "Increase Speed" + /// "Increase mod speed" /// - public static LocalisableString IncreaseSpeed => new TranslatableString(getKey(@"increase_speed"), @"Increase Speed"); + public static LocalisableString IncreaseModSpeed => new TranslatableString(getKey(@"increase_mod_speed"), @"Increase mod speed"); /// - /// "Decrease Speed" + /// "Decrease mod speed" /// - public static LocalisableString DecreaseSpeed => new TranslatableString(getKey(@"decrease_speed"), @"Decrease Speed"); + public static LocalisableString DecreaseModSpeed => new TranslatableString(getKey(@"decrease_mod_speed"), @"Decrease mod speed"); private static string getKey(string key) => $@"{prefix}:{key}"; } diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index 572379ea2c..3b8090a4b2 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -757,11 +757,11 @@ namespace osu.Game.Overlays.Mods return true; } - case GlobalAction.IncreaseSpeed: + case GlobalAction.IncreaseModSpeed: songSelect!.ChangeSpeed(0.05); return true; - case GlobalAction.DecreaseSpeed: + case GlobalAction.DecreaseModSpeed: songSelect!.ChangeSpeed(-0.05); return true; } diff --git a/osu.Game/Overlays/OSD/SpeedChangeToast.cs b/osu.Game/Overlays/OSD/SpeedChangeToast.cs index 73ba23622b..231ef86526 100644 --- a/osu.Game/Overlays/OSD/SpeedChangeToast.cs +++ b/osu.Game/Overlays/OSD/SpeedChangeToast.cs @@ -10,7 +10,7 @@ namespace osu.Game.Overlays.OSD public partial class SpeedChangeToast : Toast { public SpeedChangeToast(OsuConfigManager config, double delta) - : base(CommonStrings.Beatmaps, ToastStrings.SpeedChanged, config.LookupKeyBindings(GlobalAction.IncreaseSpeed) + " / " + config.LookupKeyBindings(GlobalAction.DecreaseSpeed)) + : base(CommonStrings.Beatmaps, ToastStrings.SpeedChanged, config.LookupKeyBindings(GlobalAction.IncreaseModSpeed) + " / " + config.LookupKeyBindings(GlobalAction.DecreaseModSpeed)) { } } diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index 18d5799bae..257f6583a4 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -1144,11 +1144,11 @@ namespace osu.Game.Screens.Select switch (e.Action) { - case GlobalAction.IncreaseSpeed: + case GlobalAction.IncreaseModSpeed: ChangeSpeed(0.05); return true; - case GlobalAction.DecreaseSpeed: + case GlobalAction.DecreaseModSpeed: ChangeSpeed(-0.05); return true; } From f979200712aa0336de04e30fb2b3bebd714b5920 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 22 May 2024 12:06:51 +0800 Subject: [PATCH 0864/2493] Use null conditional rather than implicit not-null --- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index 3b8090a4b2..ad589e8fa9 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -758,11 +758,11 @@ namespace osu.Game.Overlays.Mods } case GlobalAction.IncreaseModSpeed: - songSelect!.ChangeSpeed(0.05); + songSelect?.ChangeSpeed(0.05); return true; case GlobalAction.DecreaseModSpeed: - songSelect!.ChangeSpeed(-0.05); + songSelect?.ChangeSpeed(-0.05); return true; } From d0b1ebff5a616ca89391a87699df320edbb695a3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 22 May 2024 16:29:39 +0800 Subject: [PATCH 0865/2493] Revert "Temporary rollback of framework / SDL3" This reverts commit d7d569cf4e68acdbcc9cec844337f93bdb207a54. --- osu.Android.props | 2 +- osu.Android/AndroidJoystickSettings.cs | 76 --------------- osu.Android/AndroidMouseSettings.cs | 97 ------------------- osu.Android/OsuGameAndroid.cs | 22 ----- osu.Desktop/OsuGameDesktop.cs | 11 ++- osu.Desktop/Program.cs | 33 ++++--- .../Components/PathControlPointVisualiser.cs | 2 +- osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs | 3 +- osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs | 3 +- .../Screens/Ladder/LadderDragContainer.cs | 2 +- osu.Game/Database/EmptyRealmSet.cs | 5 + .../UserInterface/ExpandableSlider.cs | 8 +- .../Graphics/UserInterface/OsuSliderBar.cs | 9 +- .../Graphics/UserInterface/OsuTabControl.cs | 24 +++-- .../Graphics/UserInterface/PageTabControl.cs | 14 ++- .../UserInterface/RoundedSliderBar.cs | 5 +- .../UserInterface/ShearedSliderBar.cs | 5 +- .../UserInterfaceV2/LabelledSliderBar.cs | 4 +- .../UserInterfaceV2/SliderWithTextBoxInput.cs | 8 +- osu.Game/OsuGameBase.cs | 12 ++- .../BeatmapListingCardSizeTabControl.cs | 12 ++- ...BeatmapSearchMultipleSelectionFilterRow.cs | 4 + .../Overlays/BeatmapListing/FilterTabItem.cs | 12 ++- .../BeatmapSet/BeatmapRulesetSelector.cs | 2 +- .../OverlayPanelDisplayStyleControl.cs | 14 ++- osu.Game/Overlays/OverlayRulesetTabItem.cs | 14 ++- osu.Game/Overlays/OverlayStreamItem.cs | 12 ++- osu.Game/Overlays/OverlayTabControl.cs | 14 ++- .../Overlays/Settings/Sections/SizeSlider.cs | 3 +- .../Settings/SettingsPercentageSlider.cs | 4 +- osu.Game/Overlays/Settings/SettingsSlider.cs | 6 +- .../Toolbar/ToolbarRulesetSelector.cs | 16 +-- .../Toolbar/ToolbarRulesetTabButton.cs | 12 +++ osu.Game/Rulesets/Mods/DifficultyBindable.cs | 2 +- .../Objects/Drawables/DrawableHitObject.cs | 3 +- .../Scoring/LegacyDrainingHealthProcessor.cs | 7 ++ .../Rulesets/UI/FrameStabilityContainer.cs | 2 +- .../Timeline/TimelineTickDisplay.cs | 3 +- osu.Game/Screens/Edit/Editor.cs | 14 +-- .../IndeterminateSliderWithTextBoxInput.cs | 8 +- .../Match/Components/MatchTypePicker.cs | 11 ++- .../Play/PlayerSettings/PlayerSliderBar.cs | 4 +- osu.Game/osu.Game.csproj | 4 +- osu.iOS.props | 2 +- 44 files changed, 226 insertions(+), 304 deletions(-) delete mode 100644 osu.Android/AndroidJoystickSettings.cs delete mode 100644 osu.Android/AndroidMouseSettings.cs diff --git a/osu.Android.props b/osu.Android.props index 2d7a9d2652..e20ac2e0b7 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -10,7 +10,7 @@ true - + diff --git a/osu.iOS.props b/osu.iOS.props index b2e3fc0779..103ef50e0c 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -23,6 +23,6 @@ iossimulator-x64 - + From b25987ffe726cd5815aa8f84919180db47a88ddc Mon Sep 17 00:00:00 2001 From: Aurelian Date: Wed, 22 May 2024 11:37:55 +0200 Subject: [PATCH 0866/2493] Changed allowed mania keys, and reverted 0af32c5 --- osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs | 6 +++--- osu.Game/Rulesets/Objects/Legacy/LegacyRulesetExtensions.cs | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs index 2acabe2518..e5567b2215 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs @@ -111,12 +111,12 @@ namespace osu.Game.Beatmaps.Formats { difficulty.DrainRate = Math.Clamp(difficulty.DrainRate, 0, 10); //If the mode is not Mania, clamp circle size to [0,10] - if (!beatmap.BeatmapInfo.Ruleset.OnlineID.Equals(3)) + if (beatmap.BeatmapInfo.Ruleset.OnlineID != 3) difficulty.CircleSize = Math.Clamp(difficulty.CircleSize, 0, 10); - //If it is Mania, it must be within [1,20] - dual stages with 10 keys each. + //If it is Mania, it must be within [1,18] - copying what stable does https://github.com/ppy/osu/pull/28200#discussion_r1609522988 //The lower bound should be 4, but there are ranked maps that are lower than this. else - difficulty.CircleSize = Math.Clamp(difficulty.CircleSize, 1, 20); + difficulty.CircleSize = Math.Clamp(difficulty.CircleSize, 1, 18); difficulty.OverallDifficulty = Math.Clamp(difficulty.OverallDifficulty, 0, 10); difficulty.ApproachRate = Math.Clamp(difficulty.ApproachRate, 0, 10); diff --git a/osu.Game/Rulesets/Objects/Legacy/LegacyRulesetExtensions.cs b/osu.Game/Rulesets/Objects/Legacy/LegacyRulesetExtensions.cs index 1d3416f494..2a5a11161b 100644 --- a/osu.Game/Rulesets/Objects/Legacy/LegacyRulesetExtensions.cs +++ b/osu.Game/Rulesets/Objects/Legacy/LegacyRulesetExtensions.cs @@ -55,7 +55,7 @@ namespace osu.Game.Rulesets.Objects.Legacy // It works out to under 1 game pixel and is generally not meaningful to gameplay, but is to replay playback accuracy. const float broken_gamefield_rounding_allowance = 1.00041f; - return (float)Math.Max(0.02, (1.0f - 0.7f * IBeatmapDifficultyInfo.DifficultyRange(circleSize)) / 2 * (applyFudge ? broken_gamefield_rounding_allowance : 1)); + return (float)(1.0f - 0.7f * IBeatmapDifficultyInfo.DifficultyRange(circleSize)) / 2 * (applyFudge ? broken_gamefield_rounding_allowance : 1); } public static int CalculateDifficultyPeppyStars(BeatmapDifficulty difficulty, int objectCount, int drainLength) From 97fe59cb24cc2f26ff2d099ef6da09c95d9fd6ba Mon Sep 17 00:00:00 2001 From: tsunyoku Date: Wed, 22 May 2024 10:38:47 +0100 Subject: [PATCH 0867/2493] set `Ranked` to `true` for `OsuModTraceable` --- osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs index 9671f53bea..75ad00e169 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs @@ -19,6 +19,7 @@ namespace osu.Game.Rulesets.Osu.Mods public override ModType Type => ModType.Fun; public override LocalisableString Description => "Put your faith in the approach circles..."; public override double ScoreMultiplier => 1; + public override bool Ranked => true; public override Type[] IncompatibleMods => new[] { typeof(IHidesApproachCircles), typeof(OsuModDepth) }; From f3cae73e2ed892469e1879f834f4c8472e06cf13 Mon Sep 17 00:00:00 2001 From: Aurelian Date: Wed, 22 May 2024 13:26:00 +0200 Subject: [PATCH 0868/2493] Added tests for difficulty clamping --- .../Formats/LegacyBeatmapDecoderTest.cs | 30 +++++++++++++++++++ .../out-of-range-difficulties-mania.osu | 5 ++++ .../Resources/out-of-range-difficulties.osu | 10 +++++++ 3 files changed, 45 insertions(+) create mode 100644 osu.Game.Tests/Resources/out-of-range-difficulties-mania.osu create mode 100644 osu.Game.Tests/Resources/out-of-range-difficulties.osu diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs index 02432a1935..e6daba2016 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs @@ -1188,5 +1188,35 @@ namespace osu.Game.Tests.Beatmaps.Formats Assert.That(beatmap.HitObjects[0].GetEndTime(), Is.EqualTo(3153)); } } + + [Test] + public void TestBeatmapDifficultyIsClamped() + { + var decoder = new LegacyBeatmapDecoder { ApplyOffsets = false }; + + using (var resStream = TestResources.OpenResource("out-of-range-difficulties.osu")) + using (var stream = new LineBufferedReader(resStream)) + { + var decoded = decoder.Decode(stream).Difficulty; + Assert.That(decoded.DrainRate, Is.EqualTo(10)); + Assert.That(decoded.CircleSize, Is.EqualTo(10)); + Assert.That(decoded.OverallDifficulty, Is.EqualTo(10)); + Assert.That(decoded.ApproachRate, Is.EqualTo(10)); + Assert.That(decoded.SliderMultiplier, Is.EqualTo(3.6)); + Assert.That(decoded.SliderTickRate, Is.EqualTo(8)); + } + } + [Test] + public void TestManiaBeatmapDifficultyCircleSizeClamp() + { + var decoder = new LegacyBeatmapDecoder { ApplyOffsets = false }; + + using (var resStream = TestResources.OpenResource("out-of-range-difficulties-mania.osu")) + using (var stream = new LineBufferedReader(resStream)) + { + var decoded = decoder.Decode(stream).Difficulty; + Assert.That(decoded.CircleSize, Is.EqualTo(14)); + } + } } } diff --git a/osu.Game.Tests/Resources/out-of-range-difficulties-mania.osu b/osu.Game.Tests/Resources/out-of-range-difficulties-mania.osu new file mode 100644 index 0000000000..7dc2e51ad9 --- /dev/null +++ b/osu.Game.Tests/Resources/out-of-range-difficulties-mania.osu @@ -0,0 +1,5 @@ +[General] +Mode: 3 + +[Difficulty] +CircleSize:14 \ No newline at end of file diff --git a/osu.Game.Tests/Resources/out-of-range-difficulties.osu b/osu.Game.Tests/Resources/out-of-range-difficulties.osu new file mode 100644 index 0000000000..5029395614 --- /dev/null +++ b/osu.Game.Tests/Resources/out-of-range-difficulties.osu @@ -0,0 +1,10 @@ +[General] +Mode: 0 + +[Difficulty] +HPDrainRate:25 +CircleSize:25 +OverallDifficulty:25 +ApproachRate:30 +SliderMultiplier:30 +SliderTickRate:30 \ No newline at end of file From 57da4229ff621a12a43ae704bbd21884a9039d74 Mon Sep 17 00:00:00 2001 From: Fabian van Oeffelt Date: Wed, 22 May 2024 13:58:59 +0200 Subject: [PATCH 0869/2493] Add speed value to Toast --- osu.Game/Localisation/ToastStrings.cs | 4 ++-- osu.Game/Overlays/OSD/SpeedChangeToast.cs | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/osu.Game/Localisation/ToastStrings.cs b/osu.Game/Localisation/ToastStrings.cs index 33027966dd..25899153f8 100644 --- a/osu.Game/Localisation/ToastStrings.cs +++ b/osu.Game/Localisation/ToastStrings.cs @@ -50,9 +50,9 @@ namespace osu.Game.Localisation public static LocalisableString UrlCopied => new TranslatableString(getKey(@"url_copied"), @"URL copied"); /// - /// "Speed Changed" + /// "Speed changed to" /// - public static LocalisableString SpeedChanged => new TranslatableString(getKey(@"speed_changed"), @"Speed Changed"); + public static LocalisableString SpeedChangedTo => new TranslatableString(getKey(@"speed_changed"), @"Speed changed to"); private static string getKey(string key) => $@"{prefix}:{key}"; } diff --git a/osu.Game/Overlays/OSD/SpeedChangeToast.cs b/osu.Game/Overlays/OSD/SpeedChangeToast.cs index 231ef86526..df4f825541 100644 --- a/osu.Game/Overlays/OSD/SpeedChangeToast.cs +++ b/osu.Game/Overlays/OSD/SpeedChangeToast.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.Threading; using osu.Game.Configuration; using osu.Game.Input.Bindings; using osu.Game.Localisation; @@ -9,8 +10,8 @@ namespace osu.Game.Overlays.OSD { public partial class SpeedChangeToast : Toast { - public SpeedChangeToast(OsuConfigManager config, double delta) - : base(CommonStrings.Beatmaps, ToastStrings.SpeedChanged, config.LookupKeyBindings(GlobalAction.IncreaseModSpeed) + " / " + config.LookupKeyBindings(GlobalAction.DecreaseModSpeed)) + public SpeedChangeToast(OsuConfigManager config, double newSpeed) + : base(CommonStrings.Beatmaps, ToastStrings.SpeedChangedTo + " " + newSpeed.ToString(Thread.CurrentThread.CurrentCulture), config.LookupKeyBindings(GlobalAction.IncreaseModSpeed) + " / " + config.LookupKeyBindings(GlobalAction.DecreaseModSpeed)) { } } From abc67ebbaccfdc3d36ef1853f766829685e1308e Mon Sep 17 00:00:00 2001 From: Fabian van Oeffelt Date: Wed, 22 May 2024 13:59:26 +0200 Subject: [PATCH 0870/2493] Fix test not running due to floating point number inaccuacy --- .../SongSelect/TestScenePlaySongSelect.cs | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs index 938b858110..af8b2a7760 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs @@ -94,25 +94,25 @@ namespace osu.Game.Tests.Visual.SongSelect changeMods(); AddStep("decreasing speed without mods", () => songSelect?.ChangeSpeed(-0.05)); - AddAssert("halftime at 0.95", () => songSelect!.Mods.Value.Single() is ModHalfTime mod && mod.SpeedChange.Value == 0.95); + AddAssert("halftime at 0.95", () => songSelect!.Mods.Value.Single() is ModHalfTime mod && Math.Round(mod.SpeedChange.Value, 2) == 0.95); AddStep("decreasing speed with halftime", () => songSelect?.ChangeSpeed(-0.05)); - AddAssert("halftime at 0.9", () => songSelect!.Mods.Value.Single() is ModHalfTime mod && mod.SpeedChange.Value == 0.9); + AddAssert("halftime at 0.9", () => songSelect!.Mods.Value.Single() is ModHalfTime mod && Math.Round(mod.SpeedChange.Value, 2) == 0.9); AddStep("increasing speed with halftime", () => songSelect?.ChangeSpeed(+0.05)); - AddAssert("halftime at 0.95", () => songSelect!.Mods.Value.Single() is ModHalfTime mod && mod.SpeedChange.Value == 0.95); + AddAssert("halftime at 0.95", () => songSelect!.Mods.Value.Single() is ModHalfTime mod && Math.Round(mod.SpeedChange.Value, 2) == 0.95); AddStep("increasing speed with halftime to nomod", () => songSelect?.ChangeSpeed(+0.05)); AddAssert("no mods selected", () => songSelect!.Mods.Value.Count == 0); AddStep("increasing speed without mods", () => songSelect?.ChangeSpeed(+0.05)); - AddAssert("doubletime at 1.05", () => songSelect!.Mods.Value.Single() is ModDoubleTime mod && mod.SpeedChange.Value == 1.05); + AddAssert("doubletime at 1.05", () => songSelect!.Mods.Value.Single() is ModDoubleTime mod && Math.Round(mod.SpeedChange.Value, 2) == 1.05); AddStep("increasing speed with doubletime", () => songSelect?.ChangeSpeed(+0.05)); - AddAssert("doubletime at 1.1", () => songSelect!.Mods.Value.Single() is ModDoubleTime mod && mod.SpeedChange.Value == 1.1); + AddAssert("doubletime at 1.1", () => songSelect!.Mods.Value.Single() is ModDoubleTime mod && Math.Round(mod.SpeedChange.Value, 2) == 1.1); AddStep("decreasing speed with doubletime", () => songSelect?.ChangeSpeed(-0.05)); - AddAssert("doubletime at 1.05", () => songSelect!.Mods.Value.Single() is ModDoubleTime mod && mod.SpeedChange.Value == 1.05); + AddAssert("doubletime at 1.05", () => songSelect!.Mods.Value.Single() is ModDoubleTime mod && Math.Round(mod.SpeedChange.Value, 2) == 1.05); OsuModNightcore nc = new OsuModNightcore { @@ -120,22 +120,22 @@ namespace osu.Game.Tests.Visual.SongSelect }; changeMods(nc); AddStep("increasing speed with nightcore", () => songSelect?.ChangeSpeed(+0.05)); - AddAssert("nightcore at 1.1", () => songSelect!.Mods.Value.Single() is ModNightcore mod && mod.SpeedChange.Value == 1.1); + AddAssert("nightcore at 1.1", () => songSelect!.Mods.Value.Single() is ModNightcore mod && Math.Round(mod.SpeedChange.Value, 2) == 1.1); AddStep("decreasing speed with nightcore", () => songSelect?.ChangeSpeed(-0.05)); - AddAssert("doubletime at 1.05", () => songSelect!.Mods.Value.Single() is ModNightcore mod && mod.SpeedChange.Value == 1.05); + AddAssert("doubletime at 1.05", () => songSelect!.Mods.Value.Single() is ModNightcore mod && Math.Round(mod.SpeedChange.Value, 2) == 1.05); AddStep("decreasing speed with nightcore to nomod", () => songSelect?.ChangeSpeed(-0.05)); AddAssert("no mods selected", () => songSelect!.Mods.Value.Count == 0); AddStep("decreasing speed nomod, nightcore was selected", () => songSelect?.ChangeSpeed(-0.05)); - AddAssert("daycore at 0.95", () => songSelect!.Mods.Value.Single() is ModDaycore mod && mod.SpeedChange.Value == 0.95); + AddAssert("daycore at 0.95", () => songSelect!.Mods.Value.Single() is ModDaycore mod && Math.Round(mod.SpeedChange.Value, 2) == 0.95); AddStep("decreasing speed with daycore", () => songSelect?.ChangeSpeed(-0.05)); - AddAssert("daycore at 0.9", () => songSelect!.Mods.Value.Single() is ModDaycore mod && mod.SpeedChange.Value == 0.9); + AddAssert("daycore at 0.9", () => songSelect!.Mods.Value.Single() is ModDaycore mod && Math.Round(mod.SpeedChange.Value, 2) == 0.9); AddStep("increasing speed with daycore", () => songSelect?.ChangeSpeed(0.05)); - AddAssert("daycore at 0.95", () => songSelect!.Mods.Value.Single() is ModDaycore mod && mod.SpeedChange.Value == 0.95); + AddAssert("daycore at 0.95", () => songSelect!.Mods.Value.Single() is ModDaycore mod && Math.Round(mod.SpeedChange.Value, 2) == 0.95); OsuModDoubleTime dt = new OsuModDoubleTime { @@ -144,7 +144,7 @@ namespace osu.Game.Tests.Visual.SongSelect }; changeMods(dt); AddStep("decreasing speed from doubletime 1.02 with adjustpitch enabled", () => songSelect?.ChangeSpeed(-0.05)); - AddAssert("halftime at 0.97 with adjustpitch enabled", () => songSelect!.Mods.Value.Single() is ModHalfTime mod && mod.SpeedChange.Value == 0.97 && mod.AdjustPitch.Value); + AddAssert("halftime at 0.97 with adjustpitch enabled", () => songSelect!.Mods.Value.Single() is ModHalfTime mod && Math.Round(mod.SpeedChange.Value, 2) == 0.97 && mod.AdjustPitch.Value); OsuModHalfTime ht = new OsuModHalfTime { @@ -154,7 +154,7 @@ namespace osu.Game.Tests.Visual.SongSelect Mod[] modlist = { ht, new OsuModHardRock(), new OsuModHidden() }; changeMods(modlist); AddStep("decreasing speed from halftime 0.97 with adjustpitch enabled, HDHR enabled", () => songSelect?.ChangeSpeed(0.05)); - AddAssert("doubletime at 1.02 with adjustpitch enabled, HDHR still enabled", () => songSelect!.Mods.Value.Count(mod => (mod is ModDoubleTime modDt && modDt.AdjustPitch.Value && modDt.SpeedChange.Value == 1.02) || mod is ModHardRock || mod is ModHidden) == 3); + AddAssert("doubletime at 1.02 with adjustpitch enabled, HDHR still enabled", () => songSelect!.Mods.Value.Count(mod => (mod is ModDoubleTime modDt && modDt.AdjustPitch.Value && Math.Round(modDt.SpeedChange.Value, 2) == 1.02) || mod is ModHardRock || mod is ModHidden) == 3); changeMods(new ModWindUp()); AddStep("windup active, trying to change speed", () => songSelect?.ChangeSpeed(0.05)); From 0df634574fc5b680e168d59966a2d537a2baa160 Mon Sep 17 00:00:00 2001 From: Fabian van Oeffelt Date: Wed, 22 May 2024 13:59:33 +0200 Subject: [PATCH 0871/2493] Improve readability --- osu.Game/Screens/Select/SongSelect.cs | 223 ++++++++++++++------------ 1 file changed, 119 insertions(+), 104 deletions(-) diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index 257f6583a4..b3823d7a0f 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -823,123 +823,138 @@ namespace osu.Game.Screens.Select return false; } + private Mod getRateMod(ModType modType, Type type) + { + var modList = game.AvailableMods.Value[modType]; + var multiMod = (MultiMod)modList.First(mod => mod is MultiMod multiMod && multiMod.Mods.Count(mod2 => mod2.GetType().IsSubclassOf(type)) > 0); + var mod = multiMod.Mods.First(mod => mod.GetType().IsSubclassOf(type)); + return mod; + } + public void ChangeSpeed(double delta) { - ModNightcore modNc = (ModNightcore)((MultiMod)game.AvailableMods.Value[ModType.DifficultyIncrease].First(mod => mod is MultiMod multiMod && multiMod.Mods.Count(modType => modType is ModNightcore) > 0)).Mods.First(mod => mod is ModNightcore); - ModDoubleTime modDt = (ModDoubleTime)((MultiMod)game.AvailableMods.Value[ModType.DifficultyIncrease].First(mod => mod is MultiMod multiMod && multiMod.Mods.Count(modType => modType is ModDoubleTime) > 0)).Mods.First(mod => mod is ModDoubleTime); - ModDaycore modDc = (ModDaycore)((MultiMod)game.AvailableMods.Value[ModType.DifficultyReduction].First(mod => mod is MultiMod multiMod && multiMod.Mods.Count(modType => modType is ModDaycore) > 0)).Mods.First(mod => mod is ModDaycore); - ModHalfTime modHt = (ModHalfTime)((MultiMod)game.AvailableMods.Value[ModType.DifficultyReduction].First(mod => mod is MultiMod multiMod && multiMod.Mods.Count(modType => modType is ModHalfTime) > 0)).Mods.First(mod => mod is ModHalfTime); + ModNightcore modNc = (ModNightcore)getRateMod(ModType.DifficultyIncrease, typeof(ModNightcore)); + ModDoubleTime modDt = (ModDoubleTime)getRateMod(ModType.DifficultyIncrease, typeof(ModDoubleTime)); + ModDaycore modDc = (ModDaycore)getRateMod(ModType.DifficultyReduction, typeof(ModDaycore)); + ModHalfTime modHt = (ModHalfTime)getRateMod(ModType.DifficultyReduction, typeof(ModHalfTime)); bool rateModActive = selectedMods.Value.Count(mod => mod is ModRateAdjust) > 0; bool incompatibleModActive = selectedMods.Value.Count(mod => modDt.IncompatibleMods.Count(incompatibleMod => (mod.GetType().IsSubclassOf(incompatibleMod) || mod.GetType() == incompatibleMod) && incompatibleMod != typeof(ModRateAdjust)) > 0) > 0; - double newRate = 1d + delta; + double newRate = Math.Round(1d + delta, 2); bool isPositive = delta > 0; if (incompatibleModActive) return; - onScreenDisplay?.Display(new SpeedChangeToast(config, delta)); - - if (rateModActive) + if (!rateModActive) { - ModRateAdjust mod = (ModRateAdjust)selectedMods.Value.First(mod => mod is ModRateAdjust); + onScreenDisplay?.Display(new SpeedChangeToast(config, newRate)); - // Find current active rateAdjust mod and modify speed, enable HalfTime if necessary - newRate = mod.SpeedChange.Value + delta; - - if (newRate == 1.0) - { - lastPitchState = false; - usedPitchMods = false; - - if (mod is ModDoubleTime dtmod && dtmod.AdjustPitch.Value) lastPitchState = true; - - if (mod is ModHalfTime htmod && htmod.AdjustPitch.Value) lastPitchState = true; - - if (mod is ModNightcore || mod is ModDaycore) usedPitchMods = true; - - //Disable RateAdjustMods - selectedMods.Value = selectedMods.Value.Where(search => search is not ModRateAdjust).ToList(); - return; - } - - if (((mod is ModDoubleTime || mod is ModNightcore) && newRate < mod.SpeedChange.MinValue) - || ((mod is ModHalfTime || mod is ModDaycore) && newRate > mod.SpeedChange.MaxValue)) - { - bool adjustPitch = (mod is ModDoubleTime dtmod && dtmod.AdjustPitch.Value) || (mod is ModHalfTime htmod && htmod.AdjustPitch.Value); - - //Disable RateAdjustMods - selectedMods.Value = selectedMods.Value.Where(search => search is not ModRateAdjust).ToList(); - - ModRateAdjust? oppositeMod = null; - - switch (mod) - { - case ModDoubleTime: - modHt.AdjustPitch.Value = adjustPitch; - oppositeMod = modHt; - break; - - case ModHalfTime: - modDt.AdjustPitch.Value = adjustPitch; - oppositeMod = modDt; - break; - - case ModNightcore: - oppositeMod = modDc; - break; - - case ModDaycore: - oppositeMod = modNc; - break; - } - - if (oppositeMod == null) return; - - oppositeMod.SpeedChange.Value = newRate; - selectedMods.Value = selectedMods.Value.Append(oppositeMod).ToList(); - return; - } - - if (newRate > mod.SpeedChange.MaxValue && (mod is ModDoubleTime || mod is ModNightcore)) - newRate = mod.SpeedChange.MaxValue; - - if (newRate < mod.SpeedChange.MinValue && (mod is ModHalfTime || mod is ModDaycore)) - newRate = mod.SpeedChange.MinValue; - - mod.SpeedChange.Value = newRate; - } - else - { // If no ModRateAdjust is active, activate one - if (isPositive) - { - if (!usedPitchMods) - { - modDt.SpeedChange.Value = newRate; - modDt.AdjustPitch.Value = lastPitchState; - selectedMods.Value = selectedMods.Value.Append(modDt).ToList(); - } - else - { - modNc.SpeedChange.Value = newRate; - selectedMods.Value = selectedMods.Value.Append(modNc).ToList(); - } - } - else - { - if (!usedPitchMods) - { - modHt.SpeedChange.Value = newRate; - modHt.AdjustPitch.Value = lastPitchState; - selectedMods.Value = selectedMods.Value.Append(modHt).ToList(); - } - else - { - modDc.SpeedChange.Value = newRate; - selectedMods.Value = selectedMods.Value.Append(modDc).ToList(); - } - } + ModRateAdjust? newMod = null; + + if (isPositive && !usedPitchMods) + newMod = modDt; + + if (isPositive && usedPitchMods) + newMod = modNc; + + if (!isPositive && !usedPitchMods) + newMod = modHt; + + if (!isPositive && usedPitchMods) + newMod = modDc; + + if (!usedPitchMods && newMod is ModDoubleTime newModDt) + newModDt.AdjustPitch.Value = lastPitchState; + + if (!usedPitchMods && newMod is ModHalfTime newModHt) + newModHt.AdjustPitch.Value = lastPitchState; + + newMod!.SpeedChange.Value = newRate; + selectedMods.Value = selectedMods.Value.Append(newMod).ToList(); + return; } + + ModRateAdjust mod = (ModRateAdjust)selectedMods.Value.First(mod => mod is ModRateAdjust); + newRate = Math.Round(mod.SpeedChange.Value + delta, 2); + + // Disable RateAdjustMods if newRate is 1 + if (newRate == 1.0) + { + lastPitchState = false; + usedPitchMods = false; + + if (mod is ModDoubleTime dtmod && dtmod.AdjustPitch.Value) + lastPitchState = true; + + if (mod is ModHalfTime htmod && htmod.AdjustPitch.Value) + lastPitchState = true; + + if (mod is ModNightcore || mod is ModDaycore) + usedPitchMods = true; + + //Disable RateAdjustMods + selectedMods.Value = selectedMods.Value.Where(search => search is not ModRateAdjust).ToList(); + + onScreenDisplay?.Display(new SpeedChangeToast(config, newRate)); + + return; + } + + bool overMaxRateLimit = (mod is ModHalfTime || mod is ModDaycore) && newRate > mod.SpeedChange.MaxValue; + bool underMinRateLimit = (mod is ModDoubleTime || mod is ModNightcore) && newRate < mod.SpeedChange.MinValue; + + // Swap mod to opposite mod if newRate exceeds max/min speed values + if (overMaxRateLimit || underMinRateLimit) + { + bool adjustPitch = (mod is ModDoubleTime dtmod && dtmod.AdjustPitch.Value) || (mod is ModHalfTime htmod && htmod.AdjustPitch.Value); + + //Disable RateAdjustMods + selectedMods.Value = selectedMods.Value.Where(search => search is not ModRateAdjust).ToList(); + + ModRateAdjust? oppositeMod = null; + + switch (mod) + { + case ModDoubleTime: + modHt.AdjustPitch.Value = adjustPitch; + oppositeMod = modHt; + break; + + case ModHalfTime: + modDt.AdjustPitch.Value = adjustPitch; + oppositeMod = modDt; + break; + + case ModNightcore: + oppositeMod = modDc; + break; + + case ModDaycore: + oppositeMod = modNc; + break; + } + + if (oppositeMod == null) return; + + oppositeMod.SpeedChange.Value = newRate; + selectedMods.Value = selectedMods.Value.Append(oppositeMod).ToList(); + + onScreenDisplay?.Display(new SpeedChangeToast(config, newRate)); + + return; + } + + // Cap newRate to max/min values and change rate of current active mod + if (newRate > mod.SpeedChange.MaxValue && (mod is ModDoubleTime || mod is ModNightcore)) + newRate = mod.SpeedChange.MaxValue; + + if (newRate < mod.SpeedChange.MinValue && (mod is ModHalfTime || mod is ModDaycore)) + newRate = mod.SpeedChange.MinValue; + + mod.SpeedChange.Value = newRate; + + onScreenDisplay?.Display(new SpeedChangeToast(config, newRate)); } protected override void Dispose(bool isDisposing) From 8d02ac5e219ad064e553c560d076224683ad2651 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 22 May 2024 21:20:34 +0800 Subject: [PATCH 0872/2493] 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 e20ac2e0b7..8fefce3a60 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -10,7 +10,7 @@ true - + diff --git a/osu.iOS.props b/osu.iOS.props index 103ef50e0c..29a0350fde 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -23,6 +23,6 @@ iossimulator-x64 - + From 66ceda1d674ce57395419b9157daec91128d403f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 22 May 2024 21:27:53 +0800 Subject: [PATCH 0873/2493] Update focus specifications in line with framework changes --- .../Screens/Ladder/Components/LadderEditorSettings.cs | 2 +- osu.Game/Collections/ManageCollectionsDialog.cs | 2 +- osu.Game/Graphics/UserInterface/FocusedTextBox.cs | 2 +- osu.Game/Graphics/UserInterfaceV2/LabelledTextBox.cs | 2 +- osu.Game/Graphics/UserInterfaceV2/SliderWithTextBoxInput.cs | 2 +- osu.Game/Overlays/AccountCreation/ScreenEntry.cs | 2 +- osu.Game/Overlays/Comments/ReplyCommentEditor.cs | 2 +- osu.Game/Overlays/Login/LoginForm.cs | 2 +- osu.Game/Overlays/Login/LoginPanel.cs | 4 ++-- osu.Game/Overlays/Login/SecondFactorAuthForm.cs | 2 +- osu.Game/Overlays/LoginOverlay.cs | 2 +- osu.Game/Overlays/Mods/AddPresetPopover.cs | 2 +- osu.Game/Overlays/Mods/EditPresetPopover.cs | 2 +- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 2 +- osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs | 2 +- .../Settings/Sections/Input/KeyBindingsSubsection.cs | 2 +- osu.Game/Overlays/SettingsPanel.cs | 2 +- .../Screens/Edit/Compose/Components/BeatDivisorControl.cs | 2 +- .../Compose/Components/Timeline/DifficultyPointPiece.cs | 2 +- .../Edit/Compose/Components/Timeline/SamplePointPiece.cs | 2 +- osu.Game/Screens/Edit/Setup/LabelledTextBoxWithPopover.cs | 2 +- osu.Game/Screens/Edit/Setup/MetadataSection.cs | 2 +- .../Edit/Timing/IndeterminateSliderWithTextBoxInput.cs | 2 +- osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs | 6 +++--- osu.Game/Screens/Select/FilterControl.cs | 2 +- osu.Game/Screens/SelectV2/Footer/BeatmapOptionsPopover.cs | 2 +- 26 files changed, 29 insertions(+), 29 deletions(-) diff --git a/osu.Game.Tournament/Screens/Ladder/Components/LadderEditorSettings.cs b/osu.Game.Tournament/Screens/Ladder/Components/LadderEditorSettings.cs index 9f0fa19915..08ed815253 100644 --- a/osu.Game.Tournament/Screens/Ladder/Components/LadderEditorSettings.cs +++ b/osu.Game.Tournament/Screens/Ladder/Components/LadderEditorSettings.cs @@ -58,7 +58,7 @@ namespace osu.Game.Tournament.Screens.Ladder.Components editorInfo.Selected.ValueChanged += selection => { // ensure any ongoing edits are committed out to the *current* selection before changing to a new one. - GetContainingInputManager().TriggerFocusContention(null); + GetContainingFocusManager().TriggerFocusContention(null); // Required to avoid cyclic failure in BindableWithCurrent (TriggerChange called during the Current_Set process). // Arguable a framework issue but since we haven't hit it anywhere else a local workaround seems best. diff --git a/osu.Game/Collections/ManageCollectionsDialog.cs b/osu.Game/Collections/ManageCollectionsDialog.cs index 16645d6796..ea663f45fe 100644 --- a/osu.Game/Collections/ManageCollectionsDialog.cs +++ b/osu.Game/Collections/ManageCollectionsDialog.cs @@ -137,7 +137,7 @@ namespace osu.Game.Collections this.ScaleTo(0.9f, exit_duration); // Ensure that textboxes commit - GetContainingInputManager()?.TriggerFocusContention(this); + GetContainingFocusManager()?.TriggerFocusContention(this); } } } diff --git a/osu.Game/Graphics/UserInterface/FocusedTextBox.cs b/osu.Game/Graphics/UserInterface/FocusedTextBox.cs index 338f32f321..4ec93995a4 100644 --- a/osu.Game/Graphics/UserInterface/FocusedTextBox.cs +++ b/osu.Game/Graphics/UserInterface/FocusedTextBox.cs @@ -31,7 +31,7 @@ namespace osu.Game.Graphics.UserInterface if (!allowImmediateFocus) return; - Scheduler.Add(() => GetContainingInputManager().ChangeFocus(this)); + Scheduler.Add(() => GetContainingFocusManager().ChangeFocus(this)); } public new void KillFocus() => base.KillFocus(); diff --git a/osu.Game/Graphics/UserInterfaceV2/LabelledTextBox.cs b/osu.Game/Graphics/UserInterfaceV2/LabelledTextBox.cs index 8b9d35e343..863ad5a173 100644 --- a/osu.Game/Graphics/UserInterfaceV2/LabelledTextBox.cs +++ b/osu.Game/Graphics/UserInterfaceV2/LabelledTextBox.cs @@ -57,7 +57,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 protected override void OnFocus(FocusEvent e) { base.OnFocus(e); - GetContainingInputManager().ChangeFocus(Component); + GetContainingFocusManager().ChangeFocus(Component); } protected override OsuTextBox CreateComponent() => CreateTextBox().With(t => diff --git a/osu.Game/Graphics/UserInterfaceV2/SliderWithTextBoxInput.cs b/osu.Game/Graphics/UserInterfaceV2/SliderWithTextBoxInput.cs index abd828e98f..4c16cb4951 100644 --- a/osu.Game/Graphics/UserInterfaceV2/SliderWithTextBoxInput.cs +++ b/osu.Game/Graphics/UserInterfaceV2/SliderWithTextBoxInput.cs @@ -85,7 +85,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 Current.BindValueChanged(updateTextBoxFromSlider, true); } - public bool TakeFocus() => GetContainingInputManager().ChangeFocus(textBox); + public bool TakeFocus() => GetContainingFocusManager().ChangeFocus(textBox); private bool updatingFromTextBox; diff --git a/osu.Game/Overlays/AccountCreation/ScreenEntry.cs b/osu.Game/Overlays/AccountCreation/ScreenEntry.cs index f57c7d22a2..53e51e0611 100644 --- a/osu.Game/Overlays/AccountCreation/ScreenEntry.cs +++ b/osu.Game/Overlays/AccountCreation/ScreenEntry.cs @@ -243,7 +243,7 @@ namespace osu.Game.Overlays.AccountCreation if (nextTextBox != null) { - Schedule(() => GetContainingInputManager().ChangeFocus(nextTextBox)); + Schedule(() => GetContainingFocusManager().ChangeFocus(nextTextBox)); return true; } diff --git a/osu.Game/Overlays/Comments/ReplyCommentEditor.cs b/osu.Game/Overlays/Comments/ReplyCommentEditor.cs index 8e9e82507d..caf19829ee 100644 --- a/osu.Game/Overlays/Comments/ReplyCommentEditor.cs +++ b/osu.Game/Overlays/Comments/ReplyCommentEditor.cs @@ -39,7 +39,7 @@ namespace osu.Game.Overlays.Comments base.LoadComplete(); if (!TextBox.ReadOnly) - GetContainingInputManager().ChangeFocus(TextBox); + GetContainingFocusManager().ChangeFocus(TextBox); } protected override void OnCommit(string text) diff --git a/osu.Game/Overlays/Login/LoginForm.cs b/osu.Game/Overlays/Login/LoginForm.cs index 80dfca93d2..418721f371 100644 --- a/osu.Game/Overlays/Login/LoginForm.cs +++ b/osu.Game/Overlays/Login/LoginForm.cs @@ -150,7 +150,7 @@ namespace osu.Game.Overlays.Login protected override void OnFocus(FocusEvent e) { - Schedule(() => { GetContainingInputManager().ChangeFocus(string.IsNullOrEmpty(username.Text) ? username : password); }); + Schedule(() => { GetContainingFocusManager().ChangeFocus(string.IsNullOrEmpty(username.Text) ? username : password); }); } } } diff --git a/osu.Game/Overlays/Login/LoginPanel.cs b/osu.Game/Overlays/Login/LoginPanel.cs index a8adf4ce8c..845d20ccaf 100644 --- a/osu.Game/Overlays/Login/LoginPanel.cs +++ b/osu.Game/Overlays/Login/LoginPanel.cs @@ -186,7 +186,7 @@ namespace osu.Game.Overlays.Login } if (form != null) - ScheduleAfterChildren(() => GetContainingInputManager()?.ChangeFocus(form)); + ScheduleAfterChildren(() => GetContainingFocusManager()?.ChangeFocus(form)); }); private void updateDropdownCurrent(UserStatus? status) @@ -216,7 +216,7 @@ namespace osu.Game.Overlays.Login protected override void OnFocus(FocusEvent e) { - if (form != null) GetContainingInputManager().ChangeFocus(form); + if (form != null) GetContainingFocusManager().ChangeFocus(form); base.OnFocus(e); } } diff --git a/osu.Game/Overlays/Login/SecondFactorAuthForm.cs b/osu.Game/Overlays/Login/SecondFactorAuthForm.cs index dcd3119f33..82e328c036 100644 --- a/osu.Game/Overlays/Login/SecondFactorAuthForm.cs +++ b/osu.Game/Overlays/Login/SecondFactorAuthForm.cs @@ -141,7 +141,7 @@ namespace osu.Game.Overlays.Login protected override void OnFocus(FocusEvent e) { - Schedule(() => { GetContainingInputManager().ChangeFocus(codeTextBox); }); + Schedule(() => { GetContainingFocusManager().ChangeFocus(codeTextBox); }); } } } diff --git a/osu.Game/Overlays/LoginOverlay.cs b/osu.Game/Overlays/LoginOverlay.cs index c0aff6aae9..8dc454c0a0 100644 --- a/osu.Game/Overlays/LoginOverlay.cs +++ b/osu.Game/Overlays/LoginOverlay.cs @@ -78,7 +78,7 @@ namespace osu.Game.Overlays this.FadeIn(transition_time, Easing.OutQuint); FadeEdgeEffectTo(WaveContainer.SHADOW_OPACITY, WaveContainer.APPEAR_DURATION, Easing.Out); - ScheduleAfterChildren(() => GetContainingInputManager().ChangeFocus(panel)); + ScheduleAfterChildren(() => GetContainingFocusManager().ChangeFocus(panel)); } protected override void PopOut() diff --git a/osu.Game/Overlays/Mods/AddPresetPopover.cs b/osu.Game/Overlays/Mods/AddPresetPopover.cs index b782b5d6ba..50aa5a2eb4 100644 --- a/osu.Game/Overlays/Mods/AddPresetPopover.cs +++ b/osu.Game/Overlays/Mods/AddPresetPopover.cs @@ -89,7 +89,7 @@ namespace osu.Game.Overlays.Mods { base.LoadComplete(); - ScheduleAfterChildren(() => GetContainingInputManager().ChangeFocus(nameTextBox)); + ScheduleAfterChildren(() => GetContainingFocusManager().ChangeFocus(nameTextBox)); nameTextBox.Current.BindValueChanged(s => { diff --git a/osu.Game/Overlays/Mods/EditPresetPopover.cs b/osu.Game/Overlays/Mods/EditPresetPopover.cs index 9554ba8ce2..8fa6b35162 100644 --- a/osu.Game/Overlays/Mods/EditPresetPopover.cs +++ b/osu.Game/Overlays/Mods/EditPresetPopover.cs @@ -136,7 +136,7 @@ namespace osu.Game.Overlays.Mods { base.LoadComplete(); - ScheduleAfterChildren(() => GetContainingInputManager().ChangeFocus(nameTextBox)); + ScheduleAfterChildren(() => GetContainingFocusManager().ChangeFocus(nameTextBox)); } public override bool OnPressed(KeyBindingPressEvent e) diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index 25293e8e20..54124e10c7 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -949,7 +949,7 @@ namespace osu.Game.Overlays.Mods RequestScroll?.Invoke(this); // Killing focus is done here because it's the only feasible place on ModSelectOverlay you can click on without triggering any action. - Scheduler.Add(() => GetContainingInputManager().ChangeFocus(null)); + Scheduler.Add(() => GetContainingFocusManager().ChangeFocus(null)); return true; } diff --git a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs index e82cebe9f4..3f6eeca10e 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs @@ -465,7 +465,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input } if (HasFocus) - GetContainingInputManager().ChangeFocus(null); + GetContainingFocusManager().ChangeFocus(null); cancelAndClearButtons.FadeOut(300, Easing.OutQuint); cancelAndClearButtons.BypassAutoSizeAxes |= Axes.Y; diff --git a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingsSubsection.cs b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingsSubsection.cs index dd0a88bfb1..db3b56b9f0 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingsSubsection.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingsSubsection.cs @@ -106,7 +106,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input { var next = Children.SkipWhile(c => c != sender).Skip(1).FirstOrDefault(); if (next != null) - GetContainingInputManager().ChangeFocus(next); + GetContainingFocusManager().ChangeFocus(next); } } } diff --git a/osu.Game/Overlays/SettingsPanel.cs b/osu.Game/Overlays/SettingsPanel.cs index 748673035b..d5c642d24f 100644 --- a/osu.Game/Overlays/SettingsPanel.cs +++ b/osu.Game/Overlays/SettingsPanel.cs @@ -201,7 +201,7 @@ namespace osu.Game.Overlays searchTextBox.HoldFocus = false; if (searchTextBox.HasFocus) - GetContainingInputManager().ChangeFocus(null); + GetContainingFocusManager().ChangeFocus(null); } public override bool AcceptsFocus => true; diff --git a/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs b/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs index 40b97d2137..005b96bfef 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs @@ -580,7 +580,7 @@ namespace osu.Game.Screens.Edit.Compose.Components { base.LoadComplete(); - GetContainingInputManager().ChangeFocus(this); + GetContainingFocusManager().ChangeFocus(this); SelectAll(); } } diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/DifficultyPointPiece.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/DifficultyPointPiece.cs index fc240c570b..d9084a7477 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/DifficultyPointPiece.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/DifficultyPointPiece.cs @@ -138,7 +138,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline protected override void LoadComplete() { base.LoadComplete(); - ScheduleAfterChildren(() => GetContainingInputManager().ChangeFocus(sliderVelocitySlider)); + ScheduleAfterChildren(() => GetContainingFocusManager().ChangeFocus(sliderVelocitySlider)); } } } diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs index 28841fc9e5..5c4a9faaca 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs @@ -142,7 +142,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline protected override void LoadComplete() { base.LoadComplete(); - ScheduleAfterChildren(() => GetContainingInputManager().ChangeFocus(volume)); + ScheduleAfterChildren(() => GetContainingFocusManager().ChangeFocus(volume)); } private static string? getCommonBank(IList[] relevantSamples) => relevantSamples.Select(GetBankValue).Distinct().Count() == 1 ? GetBankValue(relevantSamples.First()) : null; diff --git a/osu.Game/Screens/Edit/Setup/LabelledTextBoxWithPopover.cs b/osu.Game/Screens/Edit/Setup/LabelledTextBoxWithPopover.cs index 79288e2977..5abf40dda7 100644 --- a/osu.Game/Screens/Edit/Setup/LabelledTextBoxWithPopover.cs +++ b/osu.Game/Screens/Edit/Setup/LabelledTextBoxWithPopover.cs @@ -45,7 +45,7 @@ namespace osu.Game.Screens.Edit.Setup OnFocused?.Invoke(); base.OnFocus(e); - GetContainingInputManager().TriggerFocusContention(this); + GetContainingFocusManager().TriggerFocusContention(this); } } } diff --git a/osu.Game/Screens/Edit/Setup/MetadataSection.cs b/osu.Game/Screens/Edit/Setup/MetadataSection.cs index 752f590308..660c470204 100644 --- a/osu.Game/Screens/Edit/Setup/MetadataSection.cs +++ b/osu.Game/Screens/Edit/Setup/MetadataSection.cs @@ -73,7 +73,7 @@ namespace osu.Game.Screens.Edit.Setup base.LoadComplete(); if (string.IsNullOrEmpty(ArtistTextBox.Current.Value)) - ScheduleAfterChildren(() => GetContainingInputManager().ChangeFocus(ArtistTextBox)); + ScheduleAfterChildren(() => GetContainingFocusManager().ChangeFocus(ArtistTextBox)); ArtistTextBox.Current.BindValueChanged(artist => transferIfRomanised(artist.NewValue, RomanisedArtistTextBox)); TitleTextBox.Current.BindValueChanged(title => transferIfRomanised(title.NewValue, RomanisedTitleTextBox)); diff --git a/osu.Game/Screens/Edit/Timing/IndeterminateSliderWithTextBoxInput.cs b/osu.Game/Screens/Edit/Timing/IndeterminateSliderWithTextBoxInput.cs index 26f374ba85..4f7a1bf589 100644 --- a/osu.Game/Screens/Edit/Timing/IndeterminateSliderWithTextBoxInput.cs +++ b/osu.Game/Screens/Edit/Timing/IndeterminateSliderWithTextBoxInput.cs @@ -126,7 +126,7 @@ namespace osu.Game.Screens.Edit.Timing protected override void OnFocus(FocusEvent e) { base.OnFocus(e); - GetContainingInputManager().ChangeFocus(textBox); + GetContainingFocusManager().ChangeFocus(textBox); } private void updateState() diff --git a/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs b/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs index 66bbf92e58..2f6a220c82 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs @@ -248,21 +248,21 @@ namespace osu.Game.Screens.OnlinePlay.Lounge { base.LoadComplete(); - ScheduleAfterChildren(() => GetContainingInputManager().ChangeFocus(passwordTextBox)); + ScheduleAfterChildren(() => GetContainingFocusManager().ChangeFocus(passwordTextBox)); passwordTextBox.OnCommit += (_, _) => performJoin(); } private void performJoin() { lounge?.Join(room, passwordTextBox.Text, null, joinFailed); - GetContainingInputManager().TriggerFocusContention(passwordTextBox); + GetContainingFocusManager().TriggerFocusContention(passwordTextBox); } private void joinFailed(string error) => Schedule(() => { passwordTextBox.Text = string.Empty; - GetContainingInputManager().ChangeFocus(passwordTextBox); + GetContainingFocusManager().ChangeFocus(passwordTextBox); errorText.Text = error; errorText diff --git a/osu.Game/Screens/Select/FilterControl.cs b/osu.Game/Screens/Select/FilterControl.cs index 73c122dda6..30eb4a8491 100644 --- a/osu.Game/Screens/Select/FilterControl.cs +++ b/osu.Game/Screens/Select/FilterControl.cs @@ -245,7 +245,7 @@ namespace osu.Game.Screens.Select searchTextBox.ReadOnly = true; searchTextBox.HoldFocus = false; if (searchTextBox.HasFocus) - GetContainingInputManager().ChangeFocus(searchTextBox); + GetContainingFocusManager().ChangeFocus(searchTextBox); } public void Activate() diff --git a/osu.Game/Screens/SelectV2/Footer/BeatmapOptionsPopover.cs b/osu.Game/Screens/SelectV2/Footer/BeatmapOptionsPopover.cs index f73be15a36..2827a9cb50 100644 --- a/osu.Game/Screens/SelectV2/Footer/BeatmapOptionsPopover.cs +++ b/osu.Game/Screens/SelectV2/Footer/BeatmapOptionsPopover.cs @@ -78,7 +78,7 @@ namespace osu.Game.Screens.SelectV2.Footer { base.LoadComplete(); - ScheduleAfterChildren(() => GetContainingInputManager().ChangeFocus(this)); + ScheduleAfterChildren(() => GetContainingFocusManager().ChangeFocus(this)); beatmap.BindValueChanged(_ => Hide()); } From f7ca18b52ec4e8dbc704d58a7cfef0c301201524 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 22 May 2024 15:52:57 +0200 Subject: [PATCH 0874/2493] Menial cleanups --- .../Beatmaps/Formats/LegacyBeatmapDecoder.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs index e5567b2215..8ea1d55a0d 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs @@ -105,18 +105,18 @@ namespace osu.Game.Beatmaps.Formats } /// - /// Clamp Difficulty settings to be within the normal range. + /// Ensures that all settings are within the allowed ranges. + /// See also: https://github.com/peppy/osu-stable-reference/blob/0e425c0d525ef21353c8293c235cc0621d28338b/osu!/GameplayElements/Beatmaps/Beatmap.cs#L567-L614 /// private void applyDifficultyRestrictions(BeatmapDifficulty difficulty) { difficulty.DrainRate = Math.Clamp(difficulty.DrainRate, 0, 10); - //If the mode is not Mania, clamp circle size to [0,10] - if (beatmap.BeatmapInfo.Ruleset.OnlineID != 3) - difficulty.CircleSize = Math.Clamp(difficulty.CircleSize, 0, 10); - //If it is Mania, it must be within [1,18] - copying what stable does https://github.com/ppy/osu/pull/28200#discussion_r1609522988 - //The lower bound should be 4, but there are ranked maps that are lower than this. - else - difficulty.CircleSize = Math.Clamp(difficulty.CircleSize, 1, 18); + + // mania uses "circle size" for key count, thus different allowable range + difficulty.CircleSize = beatmap.BeatmapInfo.Ruleset.OnlineID != 3 + ? Math.Clamp(difficulty.CircleSize, 0, 10) + : Math.Clamp(difficulty.CircleSize, 1, 18); + difficulty.OverallDifficulty = Math.Clamp(difficulty.OverallDifficulty, 0, 10); difficulty.ApproachRate = Math.Clamp(difficulty.ApproachRate, 0, 10); From 093be3d723ef18bcb3e7eaac1642c0006b62f922 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 22 May 2024 21:55:53 +0800 Subject: [PATCH 0875/2493] Cast remaining test usages to `IFocusManager` to remove obsolete notice --- .../TestSceneHitObjectSampleAdjustments.cs | 3 ++- .../Editing/TestSceneLabelledTimeSignature.cs | 9 +++++---- .../UserInterface/TestSceneModSelectOverlay.cs | 3 ++- .../TestSceneSliderWithTextBoxInput.cs | 17 +++++++++-------- 4 files changed, 18 insertions(+), 14 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneHitObjectSampleAdjustments.cs b/osu.Game.Tests/Visual/Editing/TestSceneHitObjectSampleAdjustments.cs index 1415ff4b0f..0e12ed68e4 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneHitObjectSampleAdjustments.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneHitObjectSampleAdjustments.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Collections.Generic; using Humanizer; using NUnit.Framework; +using osu.Framework.Input; using osu.Framework.Testing; using osu.Game.Audio; using osu.Game.Beatmaps; @@ -396,7 +397,7 @@ namespace osu.Game.Tests.Visual.Editing textBox.Current.Value = bank; // force a commit via keyboard. // this is needed when testing attempting to set empty bank - which should revert to the previous value, but only on commit. - InputManager.ChangeFocus(textBox); + ((IFocusManager)InputManager).ChangeFocus(textBox); InputManager.Key(Key.Enter); }); diff --git a/osu.Game.Tests/Visual/Editing/TestSceneLabelledTimeSignature.cs b/osu.Game.Tests/Visual/Editing/TestSceneLabelledTimeSignature.cs index e91596b872..3d7d0797d4 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneLabelledTimeSignature.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneLabelledTimeSignature.cs @@ -6,6 +6,7 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Graphics; +using osu.Framework.Input; using osu.Framework.Testing; using osu.Game.Beatmaps.Timing; using osu.Game.Graphics.UserInterface; @@ -62,12 +63,12 @@ namespace osu.Game.Tests.Visual.Editing createLabelledTimeSignature(TimeSignature.SimpleQuadruple); AddAssert("current is 4/4", () => timeSignature.Current.Value.Equals(TimeSignature.SimpleQuadruple)); - AddStep("focus text box", () => InputManager.ChangeFocus(numeratorTextBox)); + AddStep("focus text box", () => ((IFocusManager)InputManager).ChangeFocus(numeratorTextBox)); AddStep("set numerator to 7", () => numeratorTextBox.Current.Value = "7"); AddAssert("current is 4/4", () => timeSignature.Current.Value.Equals(TimeSignature.SimpleQuadruple)); - AddStep("drop focus", () => InputManager.ChangeFocus(null)); + AddStep("drop focus", () => ((IFocusManager)InputManager).ChangeFocus(null)); AddAssert("current is 7/4", () => timeSignature.Current.Value.Equals(new TimeSignature(7))); } @@ -77,12 +78,12 @@ namespace osu.Game.Tests.Visual.Editing createLabelledTimeSignature(TimeSignature.SimpleQuadruple); AddAssert("current is 4/4", () => timeSignature.Current.Value.Equals(TimeSignature.SimpleQuadruple)); - AddStep("focus text box", () => InputManager.ChangeFocus(numeratorTextBox)); + AddStep("focus text box", () => ((IFocusManager)InputManager).ChangeFocus(numeratorTextBox)); AddStep("set numerator to 0", () => numeratorTextBox.Current.Value = "0"); AddAssert("current is 4/4", () => timeSignature.Current.Value.Equals(TimeSignature.SimpleQuadruple)); - AddStep("drop focus", () => InputManager.ChangeFocus(null)); + AddStep("drop focus", () => ((IFocusManager)InputManager).ChangeFocus(null)); AddAssert("current is 4/4", () => timeSignature.Current.Value.Equals(TimeSignature.SimpleQuadruple)); AddAssert("numerator is 4", () => numeratorTextBox.Current.Value == "4"); } diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs index 8ddbd84890..a1452ddb31 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs @@ -10,6 +10,7 @@ using osu.Framework.Bindables; using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Input; using osu.Framework.Localisation; using osu.Framework.Testing; using osu.Framework.Utils; @@ -623,7 +624,7 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep("press tab", () => InputManager.Key(Key.Tab)); AddAssert("search text box focused", () => modSelectOverlay.SearchTextBox.HasFocus); - AddStep("unfocus search text box externally", () => InputManager.ChangeFocus(null)); + AddStep("unfocus search text box externally", () => ((IFocusManager)InputManager).ChangeFocus(null)); AddStep("press tab", () => InputManager.Key(Key.Tab)); AddAssert("search text box focused", () => modSelectOverlay.SearchTextBox.HasFocus); diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneSliderWithTextBoxInput.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneSliderWithTextBoxInput.cs index d23fcebae3..06b9623508 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneSliderWithTextBoxInput.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneSliderWithTextBoxInput.cs @@ -5,6 +5,7 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Framework.Input; using osu.Framework.Testing; using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterfaceV2; @@ -42,7 +43,7 @@ namespace osu.Game.Tests.Visual.UserInterface { AddStep("set instantaneous to false", () => sliderWithTextBoxInput.Instantaneous = false); - AddStep("focus textbox", () => InputManager.ChangeFocus(textBox)); + AddStep("focus textbox", () => ((IFocusManager)InputManager).ChangeFocus(textBox)); AddStep("change text", () => textBox.Text = "3"); AddAssert("slider not moved", () => slider.Current.Value, () => Is.Zero); AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.Zero); @@ -61,7 +62,7 @@ namespace osu.Game.Tests.Visual.UserInterface AddAssert("textbox changed", () => textBox.Current.Value, () => Is.EqualTo("-5")); AddAssert("current changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(-5)); - AddStep("focus textbox", () => InputManager.ChangeFocus(textBox)); + AddStep("focus textbox", () => ((IFocusManager)InputManager).ChangeFocus(textBox)); AddStep("set text to invalid", () => textBox.Text = "garbage"); AddAssert("slider not moved", () => slider.Current.Value, () => Is.EqualTo(-5)); AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(-5)); @@ -71,12 +72,12 @@ namespace osu.Game.Tests.Visual.UserInterface AddAssert("slider not moved", () => slider.Current.Value, () => Is.EqualTo(-5)); AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(-5)); - AddStep("focus textbox", () => InputManager.ChangeFocus(textBox)); + AddStep("focus textbox", () => ((IFocusManager)InputManager).ChangeFocus(textBox)); AddStep("set text to invalid", () => textBox.Text = "garbage"); AddAssert("slider not moved", () => slider.Current.Value, () => Is.EqualTo(-5)); AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(-5)); - AddStep("lose focus", () => InputManager.ChangeFocus(null)); + AddStep("lose focus", () => ((IFocusManager)InputManager).ChangeFocus(null)); AddAssert("text restored", () => textBox.Text, () => Is.EqualTo("-5")); AddAssert("slider not moved", () => slider.Current.Value, () => Is.EqualTo(-5)); AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(-5)); @@ -87,7 +88,7 @@ namespace osu.Game.Tests.Visual.UserInterface { AddStep("set instantaneous to true", () => sliderWithTextBoxInput.Instantaneous = true); - AddStep("focus textbox", () => InputManager.ChangeFocus(textBox)); + AddStep("focus textbox", () => ((IFocusManager)InputManager).ChangeFocus(textBox)); AddStep("change text", () => textBox.Text = "3"); AddAssert("slider moved", () => slider.Current.Value, () => Is.EqualTo(3)); AddAssert("current changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(3)); @@ -106,7 +107,7 @@ namespace osu.Game.Tests.Visual.UserInterface AddAssert("textbox not changed", () => textBox.Current.Value, () => Is.EqualTo("-5")); AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(-5)); - AddStep("focus textbox", () => InputManager.ChangeFocus(textBox)); + AddStep("focus textbox", () => ((IFocusManager)InputManager).ChangeFocus(textBox)); AddStep("set text to invalid", () => textBox.Text = "garbage"); AddAssert("slider not moved", () => slider.Current.Value, () => Is.EqualTo(-5)); AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(-5)); @@ -116,12 +117,12 @@ namespace osu.Game.Tests.Visual.UserInterface AddAssert("slider not moved", () => slider.Current.Value, () => Is.EqualTo(-5)); AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(-5)); - AddStep("focus textbox", () => InputManager.ChangeFocus(textBox)); + AddStep("focus textbox", () => ((IFocusManager)InputManager).ChangeFocus(textBox)); AddStep("set text to invalid", () => textBox.Text = "garbage"); AddAssert("slider not moved", () => slider.Current.Value, () => Is.EqualTo(-5)); AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(-5)); - AddStep("lose focus", () => InputManager.ChangeFocus(null)); + AddStep("lose focus", () => ((IFocusManager)InputManager).ChangeFocus(null)); AddAssert("text restored", () => textBox.Text, () => Is.EqualTo("-5")); AddAssert("slider not moved", () => slider.Current.Value, () => Is.EqualTo(-5)); AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(-5)); From 0d13848421de5198fc438fd7f4a2420c265dc31b Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 23 May 2024 00:21:19 +0900 Subject: [PATCH 0876/2493] Add whitespace to appease R# --- osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs index e6daba2016..a4cd888823 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs @@ -1206,6 +1206,7 @@ namespace osu.Game.Tests.Beatmaps.Formats Assert.That(decoded.SliderTickRate, Is.EqualTo(8)); } } + [Test] public void TestManiaBeatmapDifficultyCircleSizeClamp() { From 73cb363eba003fd329477ab778f28610d0b9e596 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 22 May 2024 23:25:59 +0800 Subject: [PATCH 0877/2493] Make some more methods static --- osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs index 8ea1d55a0d..c2f4097889 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs @@ -85,7 +85,7 @@ namespace osu.Game.Beatmaps.Formats base.ParseStreamInto(stream, beatmap); - applyDifficultyRestrictions(beatmap.Difficulty); + applyDifficultyRestrictions(beatmap.Difficulty, beatmap); flushPendingPoints(); @@ -108,7 +108,7 @@ namespace osu.Game.Beatmaps.Formats /// Ensures that all settings are within the allowed ranges. /// See also: https://github.com/peppy/osu-stable-reference/blob/0e425c0d525ef21353c8293c235cc0621d28338b/osu!/GameplayElements/Beatmaps/Beatmap.cs#L567-L614 /// - private void applyDifficultyRestrictions(BeatmapDifficulty difficulty) + private static void applyDifficultyRestrictions(BeatmapDifficulty difficulty, Beatmap beatmap) { difficulty.DrainRate = Math.Clamp(difficulty.DrainRate, 0, 10); @@ -127,7 +127,7 @@ namespace osu.Game.Beatmaps.Formats /// /// Processes the beatmap such that a new combo is started the first hitobject following each break. /// - private void postProcessBreaks(Beatmap beatmap) + private static void postProcessBreaks(Beatmap beatmap) { int currentBreak = 0; bool forceNewCombo = false; @@ -183,7 +183,7 @@ namespace osu.Game.Beatmaps.Formats /// This method's intention is to restore those legacy defaults. /// See also: https://osu.ppy.sh/wiki/en/Client/File_formats/Osu_%28file_format%29 /// - private void applyLegacyDefaults(BeatmapInfo beatmapInfo) + private static void applyLegacyDefaults(BeatmapInfo beatmapInfo) { beatmapInfo.WidescreenStoryboard = false; beatmapInfo.SamplesMatchPlaybackRate = false; From c3a2a1361d045dbfd7c409fcd003ddc33dd54164 Mon Sep 17 00:00:00 2001 From: Aurelian Date: Wed, 22 May 2024 10:39:42 +0200 Subject: [PATCH 0878/2493] SliderBody's Size getter updates size to the body/path's Size --- .../Sliders/Components/SliderBodyPiece.cs | 12 +++++++++++- .../Skinning/Default/ManualSliderBody.cs | 13 ++++++++++--- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderBodyPiece.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderBodyPiece.cs index 075e9e6aa1..14d72a2d36 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderBodyPiece.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderBodyPiece.cs @@ -61,10 +61,20 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components body.SetVertices(vertices); } - Size = body.Size; OriginPosition = body.PathOffset; } + public override Vector2 Size + { + get + { + if (base.Size != body.Size) + Size = body.Size; + return base.Size; + } + set => base.Size = value; + } + public void RecyclePath() => body.RecyclePath(); public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => body.ReceivePositionalInputAt(screenSpacePos); diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/ManualSliderBody.cs b/osu.Game.Rulesets.Osu/Skinning/Default/ManualSliderBody.cs index d171f56f40..99d954059c 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Default/ManualSliderBody.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Default/ManualSliderBody.cs @@ -11,10 +11,17 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default /// public partial class ManualSliderBody : SliderBody { - public new void SetVertices(IReadOnlyList vertices) + public new void SetVertices(IReadOnlyList vertices) => base.SetVertices(vertices); + + public override Vector2 Size { - base.SetVertices(vertices); - Size = Path.Size; + get + { + if (base.Size != Path.Size) + Size = Path.Size; + return base.Size; + } + set => base.Size = value; } } } From fd9f8bd3e098ed85b84c562afd884e1191018d25 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 23 May 2024 01:20:58 +0800 Subject: [PATCH 0879/2493] Update framework --- osu.Android.props | 2 +- osu.Game/Collections/CollectionDropdown.cs | 7 ++++++- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 2 +- 4 files changed, 9 insertions(+), 4 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index 8fefce3a60..1f241c6db5 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -10,7 +10,7 @@ true - + diff --git a/osu.iOS.props b/osu.iOS.props index 29a0350fde..eba9abd3b8 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -23,6 +23,6 @@ iossimulator-x64 - + From f85a1339d9e8b0a185aa48d38912ba971556bd73 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 23 May 2024 14:12:27 +0800 Subject: [PATCH 0880/2493] Unload daily challenge background less aggressively --- .../UpdateableOnlineBeatmapSetCover.cs | 17 +++++++++++------ osu.Game/Screens/Menu/DailyChallengeButton.cs | 2 +- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/osu.Game/Beatmaps/Drawables/UpdateableOnlineBeatmapSetCover.cs b/osu.Game/Beatmaps/Drawables/UpdateableOnlineBeatmapSetCover.cs index 2a6b6f90e3..5bce472613 100644 --- a/osu.Game/Beatmaps/Drawables/UpdateableOnlineBeatmapSetCover.cs +++ b/osu.Game/Beatmaps/Drawables/UpdateableOnlineBeatmapSetCover.cs @@ -27,8 +27,17 @@ namespace osu.Game.Beatmaps.Drawables set => base.Masking = value; } - public UpdateableOnlineBeatmapSetCover(BeatmapSetCoverType coverType = BeatmapSetCoverType.Cover) + protected override double LoadDelay { get; } + + private readonly double timeBeforeUnload; + + protected override double TransformDuration => 400; + + public UpdateableOnlineBeatmapSetCover(BeatmapSetCoverType coverType = BeatmapSetCoverType.Cover, double timeBeforeLoad = 500, double timeBeforeUnload = 1000) { + LoadDelay = timeBeforeLoad; + this.timeBeforeUnload = timeBeforeUnload; + this.coverType = coverType; InternalChild = new Box @@ -38,12 +47,8 @@ namespace osu.Game.Beatmaps.Drawables }; } - protected override double LoadDelay => 500; - - protected override double TransformDuration => 400; - protected override DelayedLoadWrapper CreateDelayedLoadWrapper(Func createContentFunc, double timeBeforeLoad) - => new DelayedLoadUnloadWrapper(createContentFunc, timeBeforeLoad) + => new DelayedLoadUnloadWrapper(createContentFunc, timeBeforeLoad, timeBeforeUnload) { Anchor = Anchor.Centre, Origin = Anchor.Centre, diff --git a/osu.Game/Screens/Menu/DailyChallengeButton.cs b/osu.Game/Screens/Menu/DailyChallengeButton.cs index 907fd04148..28b3747fbf 100644 --- a/osu.Game/Screens/Menu/DailyChallengeButton.cs +++ b/osu.Game/Screens/Menu/DailyChallengeButton.cs @@ -67,7 +67,7 @@ namespace osu.Game.Screens.Menu { Children = new Drawable[] { - cover = new UpdateableOnlineBeatmapSetCover + cover = new UpdateableOnlineBeatmapSetCover(timeBeforeLoad: 0, timeBeforeUnload: 600_000) { RelativeSizeAxes = Axes.Y, Anchor = Anchor.Centre, From 84fe3699f641b3489eda5730a2f94ea2317e53ad Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 23 May 2024 14:31:20 +0800 Subject: [PATCH 0881/2493] Reorder test steps to work better on multiple runs --- .../UserInterface/TestSceneMainMenuButton.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneMainMenuButton.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneMainMenuButton.cs index 921e28d607..5914898cb1 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneMainMenuButton.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneMainMenuButton.cs @@ -39,12 +39,7 @@ namespace osu.Game.Tests.Visual.UserInterface [Test] public void TestDailyChallengeButton() { - AddStep("add button", () => Child = new DailyChallengeButton(@"button-default-select", new Color4(102, 68, 204, 255), _ => { }, 0, Key.D) - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - ButtonSystemState = ButtonSystemState.TopLevel, - }); + AddStep("beatmap of the day not active", () => metadataClient.DailyChallengeUpdated(null)); AddStep("set up API", () => dummyAPI.HandleRequest = req => { @@ -72,12 +67,17 @@ namespace osu.Game.Tests.Visual.UserInterface } }); + AddStep("add button", () => Child = new DailyChallengeButton(@"button-default-select", new Color4(102, 68, 204, 255), _ => { }, 0, Key.D) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + ButtonSystemState = ButtonSystemState.TopLevel, + }); + AddStep("beatmap of the day active", () => metadataClient.DailyChallengeUpdated(new DailyChallengeInfo { RoomID = 1234, })); - - AddStep("beatmap of the day not active", () => metadataClient.DailyChallengeUpdated(null)); } } } From 88a2f74326183605a7130c3a74cdd09ebb6a9b36 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 23 May 2024 16:00:23 +0800 Subject: [PATCH 0882/2493] Adjust animation --- osu.Game/Screens/Menu/DailyChallengeButton.cs | 23 ++++++++++++------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/osu.Game/Screens/Menu/DailyChallengeButton.cs b/osu.Game/Screens/Menu/DailyChallengeButton.cs index 28b3747fbf..3e514d0c1f 100644 --- a/osu.Game/Screens/Menu/DailyChallengeButton.cs +++ b/osu.Game/Screens/Menu/DailyChallengeButton.cs @@ -12,6 +12,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Shapes; using osu.Framework.Threading; +using osu.Framework.Utils; using osu.Game.Beatmaps.Drawables; using osu.Game.Beatmaps.Drawables.Cards; using osu.Game.Graphics; @@ -72,8 +73,7 @@ namespace osu.Game.Screens.Menu RelativeSizeAxes = Axes.Y, Anchor = Anchor.Centre, Origin = Anchor.Centre, - RelativePositionAxes = Axes.X, - X = -0.5f, + RelativePositionAxes = Axes.Both, }, new Box { @@ -100,18 +100,25 @@ namespace osu.Game.Screens.Menu base.LoadComplete(); info.BindValueChanged(updateDisplay, true); - FinishTransforms(true); - - cover.MoveToX(-0.5f, 10000, Easing.InOutSine) - .Then().MoveToX(0.5f, 10000, Easing.InOutSine) - .Loop(); } protected override void Update() { base.Update(); - cover.Width = 2 * background.DrawWidth; + if (cover.LatestTransformEndTime == Time.Current) + { + const double duration = 3000; + + float scale = 1 + RNG.NextSingle(); + + cover.ScaleTo(scale, duration, Easing.InOutSine) + .RotateTo(RNG.NextSingle(-4, 4) * (scale - 1), duration, Easing.InOutSine) + .MoveTo(new Vector2( + RNG.NextSingle(-0.5f, 0.5f) * (scale - 1), + RNG.NextSingle(-0.5f, 0.5f) * (scale - 1) + ), duration, Easing.InOutSine); + } } private void updateDisplay(ValueChangedEvent info) From a3639e0ce3c8cec679432624f571c71443c84978 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 23 May 2024 17:19:11 +0800 Subject: [PATCH 0883/2493] Remove unused field --- osu.Game/Screens/Menu/DailyChallengeButton.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game/Screens/Menu/DailyChallengeButton.cs b/osu.Game/Screens/Menu/DailyChallengeButton.cs index 3e514d0c1f..7dbd90eeba 100644 --- a/osu.Game/Screens/Menu/DailyChallengeButton.cs +++ b/osu.Game/Screens/Menu/DailyChallengeButton.cs @@ -38,7 +38,6 @@ namespace osu.Game.Screens.Menu private UpdateableOnlineBeatmapSetCover cover = null!; private IBindable info = null!; - private BufferedContainer background = null!; [Resolved] private IAPIProvider api { get; set; } = null!; @@ -64,7 +63,7 @@ namespace osu.Game.Screens.Menu }); } - protected override Drawable CreateBackground(Colour4 accentColour) => background = new BufferedContainer + protected override Drawable CreateBackground(Colour4 accentColour) => new BufferedContainer { Children = new Drawable[] { From 357e55ae1f9d4d19fc7e28f508a73d2ca05e8751 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 23 May 2024 17:39:59 +0800 Subject: [PATCH 0884/2493] Make gradient layer a bit more dynamic --- osu.Game/Screens/Menu/DailyChallengeButton.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Menu/DailyChallengeButton.cs b/osu.Game/Screens/Menu/DailyChallengeButton.cs index 7dbd90eeba..c365994736 100644 --- a/osu.Game/Screens/Menu/DailyChallengeButton.cs +++ b/osu.Game/Screens/Menu/DailyChallengeButton.cs @@ -39,6 +39,8 @@ namespace osu.Game.Screens.Menu private UpdateableOnlineBeatmapSetCover cover = null!; private IBindable info = null!; + private Box gradientLayer = null!; + [Resolved] private IAPIProvider api { get; set; } = null!; @@ -74,10 +76,10 @@ namespace osu.Game.Screens.Menu Origin = Anchor.Centre, RelativePositionAxes = Axes.Both, }, - new Box + gradientLayer = new Box { RelativeSizeAxes = Axes.Both, - Colour = ColourInfo.GradientVertical(accentColour.Opacity(0), accentColour), + Colour = ColourInfo.GradientVertical(accentColour.Opacity(0.2f), accentColour), Blending = BlendingParameters.Additive, }, new Box @@ -117,6 +119,10 @@ namespace osu.Game.Screens.Menu RNG.NextSingle(-0.5f, 0.5f) * (scale - 1), RNG.NextSingle(-0.5f, 0.5f) * (scale - 1) ), duration, Easing.InOutSine); + + gradientLayer.FadeIn(duration / 2) + .Then() + .FadeOut(duration / 2); } } From bfa23ec7a47ed41be7ffc40e46b28fd5fff2b648 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 23 May 2024 11:46:48 +0200 Subject: [PATCH 0885/2493] Fix main menu button animation not playing on initial show --- osu.Game/Screens/Menu/MainMenuButton.cs | 69 ++++++++++++++----------- 1 file changed, 38 insertions(+), 31 deletions(-) diff --git a/osu.Game/Screens/Menu/MainMenuButton.cs b/osu.Game/Screens/Menu/MainMenuButton.cs index fe8fb91766..29a661066c 100644 --- a/osu.Game/Screens/Menu/MainMenuButton.cs +++ b/osu.Game/Screens/Menu/MainMenuButton.cs @@ -179,7 +179,6 @@ namespace osu.Game.Screens.Menu { base.LoadComplete(); - background.Size = initialSize; background.Shear = new Vector2(ButtonSystem.WEDGE_WIDTH / initialSize.Y, 0); // for whatever reason, attempting to size the background "just in time" to cover the visible width @@ -189,6 +188,9 @@ namespace osu.Game.Screens.Menu // (which can exceed the [0;1] range during interpolation). backgroundContent.Width = 2 * initialSize.X; backgroundContent.Shear = -background.Shear; + + animateState(); + FinishTransforms(true); } private bool rightward; @@ -318,41 +320,46 @@ namespace osu.Game.Screens.Menu state = value; - switch (state) - { - case ButtonState.Contracted: - switch (ContractStyle) - { - default: - background.ResizeTo(Vector2.Multiply(initialSize, new Vector2(0, 1)), 500, Easing.OutExpo); - this.FadeOut(500); - break; - - case 1: - background.ResizeTo(Vector2.Multiply(initialSize, new Vector2(0, 1)), 400, Easing.InSine); - this.FadeOut(800); - break; - } - - break; - - case ButtonState.Expanded: - const int expand_duration = 500; - background.ResizeTo(initialSize, expand_duration, Easing.OutExpo); - this.FadeIn(expand_duration / 6f); - break; - - case ButtonState.Exploded: - const int explode_duration = 200; - background.ResizeTo(Vector2.Multiply(initialSize, new Vector2(2, 1)), explode_duration, Easing.OutExpo); - this.FadeOut(explode_duration / 4f * 3); - break; - } + animateState(); StateChanged?.Invoke(State); } } + private void animateState() + { + switch (state) + { + case ButtonState.Contracted: + switch (ContractStyle) + { + default: + background.ResizeTo(Vector2.Multiply(initialSize, new Vector2(0, 1)), 500, Easing.OutExpo); + this.FadeOut(500); + break; + + case 1: + background.ResizeTo(Vector2.Multiply(initialSize, new Vector2(0, 1)), 400, Easing.InSine); + this.FadeOut(800); + break; + } + + break; + + case ButtonState.Expanded: + const int expand_duration = 500; + background.ResizeTo(initialSize, expand_duration, Easing.OutExpo); + this.FadeIn(expand_duration / 6f); + break; + + case ButtonState.Exploded: + const int explode_duration = 200; + background.ResizeTo(Vector2.Multiply(initialSize, new Vector2(2, 1)), explode_duration, Easing.OutExpo); + this.FadeOut(explode_duration / 4f * 3); + break; + } + } + private ButtonSystemState buttonSystemState; public ButtonSystemState ButtonSystemState From 3411ebc4af5711c275e21b843b264bc7d96f864b Mon Sep 17 00:00:00 2001 From: Susko3 Date: Thu, 23 May 2024 12:50:06 +0200 Subject: [PATCH 0886/2493] Move `SDL3BatteryInfo` to separate file --- osu.Desktop/OsuGameDesktop.cs | 20 -------------------- osu.Desktop/SDL3BatteryInfo.cs | 27 +++++++++++++++++++++++++++ 2 files changed, 27 insertions(+), 20 deletions(-) create mode 100644 osu.Desktop/SDL3BatteryInfo.cs diff --git a/osu.Desktop/OsuGameDesktop.cs b/osu.Desktop/OsuGameDesktop.cs index e8783c997a..b1e1a8f118 100644 --- a/osu.Desktop/OsuGameDesktop.cs +++ b/osu.Desktop/OsuGameDesktop.cs @@ -22,7 +22,6 @@ using osu.Game.IPC; using osu.Game.Online.Multiplayer; using osu.Game.Performance; using osu.Game.Utils; -using SDL; namespace osu.Desktop { @@ -169,24 +168,5 @@ namespace osu.Desktop osuSchemeLinkIPCChannel?.Dispose(); archiveImportIPCChannel?.Dispose(); } - - private unsafe class SDL3BatteryInfo : BatteryInfo - { - public override double? ChargeLevel - { - get - { - int percentage; - SDL3.SDL_GetPowerInfo(null, &percentage); - - if (percentage == -1) - return null; - - return percentage / 100.0; - } - } - - public override bool OnBattery => SDL3.SDL_GetPowerInfo(null, null) == SDL_PowerState.SDL_POWERSTATE_ON_BATTERY; - } } } diff --git a/osu.Desktop/SDL3BatteryInfo.cs b/osu.Desktop/SDL3BatteryInfo.cs new file mode 100644 index 0000000000..89084b5a15 --- /dev/null +++ b/osu.Desktop/SDL3BatteryInfo.cs @@ -0,0 +1,27 @@ +// 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.Utils; +using SDL; + +namespace osu.Desktop +{ + internal unsafe class SDL3BatteryInfo : BatteryInfo + { + public override double? ChargeLevel + { + get + { + int percentage; + SDL3.SDL_GetPowerInfo(null, &percentage); + + if (percentage == -1) + return null; + + return percentage / 100.0; + } + } + + public override bool OnBattery => SDL3.SDL_GetPowerInfo(null, null) == SDL_PowerState.SDL_POWERSTATE_ON_BATTERY; + } +} From 45ed86f46cdf413d1acaa189f0faea7a10b0ad44 Mon Sep 17 00:00:00 2001 From: Susko3 Date: Thu, 23 May 2024 12:53:33 +0200 Subject: [PATCH 0887/2493] Add back `SDL2BatteryInfo` --- osu.Desktop/OsuGameDesktop.cs | 2 +- osu.Desktop/SDL2BatteryInfo.cs | 25 +++++++++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 osu.Desktop/SDL2BatteryInfo.cs diff --git a/osu.Desktop/OsuGameDesktop.cs b/osu.Desktop/OsuGameDesktop.cs index b1e1a8f118..3e06dad4c5 100644 --- a/osu.Desktop/OsuGameDesktop.cs +++ b/osu.Desktop/OsuGameDesktop.cs @@ -160,7 +160,7 @@ namespace osu.Desktop host.Window.Title = Name; } - protected override BatteryInfo CreateBatteryInfo() => new SDL3BatteryInfo(); + protected override BatteryInfo CreateBatteryInfo() => FrameworkEnvironment.UseSDL3 ? new SDL3BatteryInfo() : new SDL2BatteryInfo(); protected override void Dispose(bool isDisposing) { diff --git a/osu.Desktop/SDL2BatteryInfo.cs b/osu.Desktop/SDL2BatteryInfo.cs new file mode 100644 index 0000000000..9ca2dc3a5c --- /dev/null +++ b/osu.Desktop/SDL2BatteryInfo.cs @@ -0,0 +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 osu.Game.Utils; + +namespace osu.Desktop +{ + internal class SDL2BatteryInfo : BatteryInfo + { + public override double? ChargeLevel + { + get + { + SDL2.SDL.SDL_GetPowerInfo(out _, out int percentage); + + if (percentage == -1) + return null; + + return percentage / 100.0; + } + } + + public override bool OnBattery => SDL2.SDL.SDL_GetPowerInfo(out _, out _) == SDL2.SDL.SDL_PowerState.SDL_POWERSTATE_ON_BATTERY; + } +} From ccf8473aae70b4898c7289c2601a07e418e23257 Mon Sep 17 00:00:00 2001 From: Susko3 Date: Thu, 23 May 2024 13:00:18 +0200 Subject: [PATCH 0888/2493] Use appropriate `SDL_ShowSimpleMessageBox` --- osu.Desktop/Program.cs | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/osu.Desktop/Program.cs b/osu.Desktop/Program.cs index 23e56cdce9..0d8de8dce7 100644 --- a/osu.Desktop/Program.cs +++ b/osu.Desktop/Program.cs @@ -28,6 +28,14 @@ namespace osu.Desktop private static LegacyTcpIpcProvider? legacyIpc; + private static unsafe void showMessageBox(string title, string message) + { + if (FrameworkEnvironment.UseSDL3) + SDL3.SDL_ShowSimpleMessageBox(SDL_MessageBoxFlags.SDL_MESSAGEBOX_ERROR, title, message, null); + else + SDL2.SDL.SDL_ShowSimpleMessageBox(SDL2.SDL.SDL_MessageBoxFlags.SDL_MESSAGEBOX_ERROR, title, message, IntPtr.Zero); + } + [STAThread] public static void Main(string[] args) { @@ -52,19 +60,15 @@ namespace osu.Desktop // See https://www.mongodb.com/docs/realm/sdk/dotnet/compatibility/ if (windowsVersion.Major < 6 || (windowsVersion.Major == 6 && windowsVersion.Minor <= 2)) { - unsafe - { - // If users running in compatibility mode becomes more of a common thing, we may want to provide better guidance or even consider - // disabling it ourselves. - // We could also better detect compatibility mode if required: - // https://stackoverflow.com/questions/10744651/how-i-can-detect-if-my-application-is-running-under-compatibility-mode#comment58183249_10744730 - SDL3.SDL_ShowSimpleMessageBox(SDL_MessageBoxFlags.SDL_MESSAGEBOX_ERROR, - "Your operating system is too old to run osu!"u8, - "This version of osu! requires at least Windows 8.1 to run.\n"u8 - + "Please upgrade your operating system or consider using an older version of osu!.\n\n"u8 - + "If you are running a newer version of windows, please check you don't have \"Compatibility mode\" turned on for osu!"u8, null); - return; - } + // If users running in compatibility mode becomes more of a common thing, we may want to provide better guidance or even consider + // disabling it ourselves. + // We could also better detect compatibility mode if required: + // https://stackoverflow.com/questions/10744651/how-i-can-detect-if-my-application-is-running-under-compatibility-mode#comment58183249_10744730 + showMessageBox("Your operating system is too old to run osu!", + "This version of osu! requires at least Windows 8.1 to run.\n" + + "Please upgrade your operating system or consider using an older version of osu!.\n\n" + + "If you are running a newer version of windows, please check you don't have \"Compatibility mode\" turned on for osu!"); + return; } setupSquirrel(); From 070668c96f4494958b0e1b4464dd4059e9ba0ec5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 23 May 2024 13:55:11 +0200 Subject: [PATCH 0889/2493] Use `ShiftPressed` instead of explicitly checking both physical keys Not only is this simpler, but it also is more correct (for explanation why, try holding both shift keys while dragging, and just releasing one of them - the previous code would briefly turn aspect ratio off). --- .../Edit/Compose/Components/SelectionBoxScaleHandle.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionBoxScaleHandle.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionBoxScaleHandle.cs index c188d23a58..12787a1c55 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionBoxScaleHandle.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionBoxScaleHandle.cs @@ -51,9 +51,9 @@ namespace osu.Game.Screens.Edit.Compose.Components protected override bool OnKeyDown(KeyDownEvent e) { - if (IsDragged && (e.Key == Key.ShiftLeft || e.Key == Key.ShiftRight)) + if (IsDragged) { - applyScale(shouldLockAspectRatio: true); + applyScale(shouldLockAspectRatio: e.ShiftPressed); return true; } @@ -64,8 +64,8 @@ namespace osu.Game.Screens.Edit.Compose.Components { base.OnKeyUp(e); - if (IsDragged && (e.Key == Key.ShiftLeft || e.Key == Key.ShiftRight)) - applyScale(shouldLockAspectRatio: false); + if (IsDragged) + applyScale(shouldLockAspectRatio: e.ShiftPressed); } protected override void OnDragEnd(DragEndEvent e) From 9e86a08405db8a88fec2066975843b13ae831eed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 23 May 2024 14:07:43 +0200 Subject: [PATCH 0890/2493] Simplify scale origin computation --- .../Components/SelectionBoxScaleHandle.cs | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionBoxScaleHandle.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionBoxScaleHandle.cs index 12787a1c55..352a4985d6 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionBoxScaleHandle.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionBoxScaleHandle.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; +using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Input.Events; using osu.Framework.Utils; @@ -102,21 +103,8 @@ namespace osu.Game.Screens.Edit.Compose.Components ? new Vector2((rawScale.X + rawScale.Y) * 0.5f) : rawScale; - scaleHandler!.Update(newScale, getOriginPosition(), getAdjustAxis()); - } - - private Vector2 getOriginPosition() - { - var quad = scaleHandler!.OriginalSurroundingQuad!.Value; - Vector2 origin = quad.TopLeft; - - if ((originalAnchor & Anchor.x0) > 0) - origin.X += quad.Width; - - if ((originalAnchor & Anchor.y0) > 0) - origin.Y += quad.Height; - - return origin; + var scaleOrigin = originalAnchor.Opposite().PositionOnQuad(scaleHandler!.OriginalSurroundingQuad!.Value); + scaleHandler!.Update(newScale, scaleOrigin, getAdjustAxis()); } private Axes getAdjustAxis() From abca62d5f0e545cd167305d292f09944a6397cd1 Mon Sep 17 00:00:00 2001 From: Susko3 Date: Thu, 23 May 2024 14:24:42 +0200 Subject: [PATCH 0891/2493] Revert "Use appropriate `SDL_ShowSimpleMessageBox`" This reverts commit ccf8473aae70b4898c7289c2601a07e418e23257. --- osu.Desktop/Program.cs | 30 +++++++++++++----------------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/osu.Desktop/Program.cs b/osu.Desktop/Program.cs index 0d8de8dce7..23e56cdce9 100644 --- a/osu.Desktop/Program.cs +++ b/osu.Desktop/Program.cs @@ -28,14 +28,6 @@ namespace osu.Desktop private static LegacyTcpIpcProvider? legacyIpc; - private static unsafe void showMessageBox(string title, string message) - { - if (FrameworkEnvironment.UseSDL3) - SDL3.SDL_ShowSimpleMessageBox(SDL_MessageBoxFlags.SDL_MESSAGEBOX_ERROR, title, message, null); - else - SDL2.SDL.SDL_ShowSimpleMessageBox(SDL2.SDL.SDL_MessageBoxFlags.SDL_MESSAGEBOX_ERROR, title, message, IntPtr.Zero); - } - [STAThread] public static void Main(string[] args) { @@ -60,15 +52,19 @@ namespace osu.Desktop // See https://www.mongodb.com/docs/realm/sdk/dotnet/compatibility/ if (windowsVersion.Major < 6 || (windowsVersion.Major == 6 && windowsVersion.Minor <= 2)) { - // If users running in compatibility mode becomes more of a common thing, we may want to provide better guidance or even consider - // disabling it ourselves. - // We could also better detect compatibility mode if required: - // https://stackoverflow.com/questions/10744651/how-i-can-detect-if-my-application-is-running-under-compatibility-mode#comment58183249_10744730 - showMessageBox("Your operating system is too old to run osu!", - "This version of osu! requires at least Windows 8.1 to run.\n" - + "Please upgrade your operating system or consider using an older version of osu!.\n\n" - + "If you are running a newer version of windows, please check you don't have \"Compatibility mode\" turned on for osu!"); - return; + unsafe + { + // If users running in compatibility mode becomes more of a common thing, we may want to provide better guidance or even consider + // disabling it ourselves. + // We could also better detect compatibility mode if required: + // https://stackoverflow.com/questions/10744651/how-i-can-detect-if-my-application-is-running-under-compatibility-mode#comment58183249_10744730 + SDL3.SDL_ShowSimpleMessageBox(SDL_MessageBoxFlags.SDL_MESSAGEBOX_ERROR, + "Your operating system is too old to run osu!"u8, + "This version of osu! requires at least Windows 8.1 to run.\n"u8 + + "Please upgrade your operating system or consider using an older version of osu!.\n\n"u8 + + "If you are running a newer version of windows, please check you don't have \"Compatibility mode\" turned on for osu!"u8, null); + return; + } } setupSquirrel(); From f17f70dca7eeb60690d83435d0bdba399fdd38bd Mon Sep 17 00:00:00 2001 From: Aurelian Date: Thu, 23 May 2024 14:36:49 +0200 Subject: [PATCH 0892/2493] Changed Size to be handled by AutoSizeAxes --- .../Sliders/Components/SliderBodyPiece.cs | 13 ++----------- .../Skinning/Default/ManualSliderBody.cs | 13 +++++-------- 2 files changed, 7 insertions(+), 19 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderBodyPiece.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderBodyPiece.cs index 14d72a2d36..44c754d8f5 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderBodyPiece.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderBodyPiece.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using osu.Framework.Allocation; +using osu.Framework.Graphics; using osu.Game.Graphics; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Skinning.Default; @@ -41,6 +42,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components private void load(OsuColour colours) { body.BorderColour = colours.Yellow; + AutoSizeAxes = Axes.Both; } private int? lastVersion; @@ -64,17 +66,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components OriginPosition = body.PathOffset; } - public override Vector2 Size - { - get - { - if (base.Size != body.Size) - Size = body.Size; - return base.Size; - } - set => base.Size = value; - } - public void RecyclePath() => body.RecyclePath(); public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => body.ReceivePositionalInputAt(screenSpacePos); diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/ManualSliderBody.cs b/osu.Game.Rulesets.Osu/Skinning/Default/ManualSliderBody.cs index 99d954059c..2fc18da254 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Default/ManualSliderBody.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Default/ManualSliderBody.cs @@ -2,6 +2,8 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; +using osu.Framework.Allocation; +using osu.Framework.Graphics; using osuTK; namespace osu.Game.Rulesets.Osu.Skinning.Default @@ -13,15 +15,10 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default { public new void SetVertices(IReadOnlyList vertices) => base.SetVertices(vertices); - public override Vector2 Size + [BackgroundDependencyLoader] + private void load() { - get - { - if (base.Size != Path.Size) - Size = Path.Size; - return base.Size; - } - set => base.Size = value; + AutoSizeAxes = Axes.Both; } } } From ac5c031a3a077cc5072b7f6b331623aa91681d58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 23 May 2024 14:18:29 +0200 Subject: [PATCH 0893/2493] Simplify original state management in skin selection scale handler --- .../SkinEditor/SkinSelectionScaleHandler.cs | 47 ++++++++++--------- 1 file changed, 25 insertions(+), 22 deletions(-) diff --git a/osu.Game/Overlays/SkinEditor/SkinSelectionScaleHandler.cs b/osu.Game/Overlays/SkinEditor/SkinSelectionScaleHandler.cs index 0c2ee6aae3..08df8df7e2 100644 --- a/osu.Game/Overlays/SkinEditor/SkinSelectionScaleHandler.cs +++ b/osu.Game/Overlays/SkinEditor/SkinSelectionScaleHandler.cs @@ -53,13 +53,8 @@ namespace osu.Game.Overlays.SkinEditor private bool allSelectedSupportManualSizing(Axes axis) => selectedItems.All(b => (b as CompositeDrawable)?.AutoSizeAxes.HasFlagFast(axis) == false); - private Drawable[]? objectsInScale; - + private Dictionary? objectsInScale; private Vector2? defaultOrigin; - private Dictionary? originalWidths; - private Dictionary? originalHeights; - private Dictionary? originalScales; - private Dictionary? originalPositions; private bool isFlippedX; private bool isFlippedY; @@ -71,12 +66,8 @@ namespace osu.Game.Overlays.SkinEditor changeHandler?.BeginChange(); - objectsInScale = selectedItems.Cast().ToArray(); - originalWidths = objectsInScale.ToDictionary(d => d, d => d.Width); - originalHeights = objectsInScale.ToDictionary(d => d, d => d.Height); - originalScales = objectsInScale.ToDictionary(d => d, d => d.Scale); - originalPositions = objectsInScale.ToDictionary(d => d, d => d.ToScreenSpace(d.OriginPosition)); - OriginalSurroundingQuad = ToLocalSpace(GeometryUtils.GetSurroundingQuad(objectsInScale.SelectMany(d => d.ScreenSpaceDrawQuad.GetVertices().ToArray()))); + objectsInScale = selectedItems.Cast().ToDictionary(d => d, d => new OriginalDrawableState(d)); + OriginalSurroundingQuad = ToLocalSpace(GeometryUtils.GetSurroundingQuad(objectsInScale.SelectMany(d => d.Key.ScreenSpaceDrawQuad.GetVertices().ToArray()))); defaultOrigin = OriginalSurroundingQuad.Value.Centre; isFlippedX = false; @@ -88,7 +79,7 @@ namespace osu.Game.Overlays.SkinEditor if (objectsInScale == null) throw new InvalidOperationException($"Cannot {nameof(Update)} a scale operation without calling {nameof(Begin)} first!"); - Debug.Assert(originalWidths != null && originalHeights != null && originalScales != null && originalPositions != null && defaultOrigin != null && OriginalSurroundingQuad != null); + Debug.Assert(defaultOrigin != null && OriginalSurroundingQuad != null); var actualOrigin = ToScreenSpace(origin ?? defaultOrigin.Value); @@ -132,9 +123,9 @@ namespace osu.Game.Overlays.SkinEditor return; } - foreach (var b in objectsInScale) + foreach (var (b, originalState) in objectsInScale) { - UpdatePosition(b, GeometryUtils.GetScaledPosition(scale, actualOrigin, originalPositions[b])); + UpdatePosition(b, GeometryUtils.GetScaledPosition(scale, actualOrigin, originalState.ScreenSpaceOriginPosition)); var currentScale = scale; if (Precision.AlmostEquals(MathF.Abs(b.Rotation) % 180, 90)) @@ -143,15 +134,15 @@ namespace osu.Game.Overlays.SkinEditor switch (adjustAxis) { case Axes.X: - b.Width = MathF.Abs(originalWidths[b] * currentScale.X); + b.Width = MathF.Abs(originalState.Width * currentScale.X); break; case Axes.Y: - b.Height = MathF.Abs(originalHeights[b] * currentScale.Y); + b.Height = MathF.Abs(originalState.Height * currentScale.Y); break; case Axes.Both: - b.Scale = originalScales[b] * currentScale; + b.Scale = originalState.Scale * currentScale; break; } } @@ -165,11 +156,23 @@ namespace osu.Game.Overlays.SkinEditor changeHandler?.EndChange(); objectsInScale = null; - originalPositions = null; - originalWidths = null; - originalHeights = null; - originalScales = null; defaultOrigin = null; } + + private struct OriginalDrawableState + { + public float Width { get; } + public float Height { get; } + public Vector2 Scale { get; } + public Vector2 ScreenSpaceOriginPosition { get; } + + public OriginalDrawableState(Drawable drawable) + { + Width = drawable.Width; + Height = drawable.Height; + Scale = drawable.Scale; + ScreenSpaceOriginPosition = drawable.ToScreenSpace(drawable.OriginPosition); + } + } } } From f7bcccacb03358667fa40d0603bb01d3233bf591 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 23 May 2024 14:41:59 +0200 Subject: [PATCH 0894/2493] Simplify original state management in osu! scale handler --- .../Edit/OsuSelectionScaleHandler.cs | 54 ++++++++++--------- 1 file changed, 29 insertions(+), 25 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs index 7d5240fb69..b0299c5668 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs @@ -55,12 +55,8 @@ namespace osu.Game.Rulesets.Osu.Edit CanScaleDiagonally.Value = CanScaleX.Value && CanScaleY.Value; } - private OsuHitObject[]? objectsInScale; - + private Dictionary? objectsInScale; private Vector2? defaultOrigin; - private Dictionary? originalPositions; - private Dictionary? originalPathControlPointPositions; - private Dictionary? originalPathControlPointTypes; public override void Begin() { @@ -69,18 +65,11 @@ namespace osu.Game.Rulesets.Osu.Edit changeHandler?.BeginChange(); - objectsInScale = selectedMovableObjects.ToArray(); - OriginalSurroundingQuad = objectsInScale.Length == 1 && objectsInScale.First() is Slider slider + objectsInScale = selectedMovableObjects.ToDictionary(ho => ho, ho => new OriginalHitObjectState(ho)); + OriginalSurroundingQuad = objectsInScale.Count == 1 && objectsInScale.First().Key is Slider slider ? GeometryUtils.GetSurroundingQuad(slider.Path.ControlPoints.Select(p => p.Position)) - : GeometryUtils.GetSurroundingQuad(objectsInScale); + : GeometryUtils.GetSurroundingQuad(objectsInScale.Keys); defaultOrigin = OriginalSurroundingQuad.Value.Centre; - originalPositions = objectsInScale.ToDictionary(obj => obj, obj => obj.Position); - originalPathControlPointPositions = objectsInScale.OfType().ToDictionary( - obj => obj, - obj => obj.Path.ControlPoints.Select(point => point.Position).ToArray()); - originalPathControlPointTypes = objectsInScale.OfType().ToDictionary( - obj => obj, - obj => obj.Path.ControlPoints.Select(p => p.Type).ToArray()); } public override void Update(Vector2 scale, Vector2? origin = null, Axes adjustAxis = Axes.Both) @@ -88,22 +77,26 @@ namespace osu.Game.Rulesets.Osu.Edit if (objectsInScale == null) throw new InvalidOperationException($"Cannot {nameof(Update)} a scale operation without calling {nameof(Begin)} first!"); - Debug.Assert(originalPositions != null && originalPathControlPointPositions != null && defaultOrigin != null && originalPathControlPointTypes != null && OriginalSurroundingQuad != null); + Debug.Assert(defaultOrigin != null && OriginalSurroundingQuad != null); Vector2 actualOrigin = origin ?? defaultOrigin.Value; // for the time being, allow resizing of slider paths only if the slider is // the only hit object selected. with a group selection, it's likely the user // is not looking to change the duration of the slider but expand the whole pattern. - if (objectsInScale.Length == 1 && objectsInScale.First() is Slider slider) - scaleSlider(slider, scale, originalPathControlPointPositions[slider], originalPathControlPointTypes[slider]); + if (objectsInScale.Count == 1 && objectsInScale.First().Key is Slider slider) + { + var originalInfo = objectsInScale[slider]; + Debug.Assert(originalInfo.PathControlPointPositions != null && originalInfo.PathControlPointTypes != null); + scaleSlider(slider, scale, originalInfo.PathControlPointPositions, originalInfo.PathControlPointTypes); + } else { scale = getClampedScale(OriginalSurroundingQuad.Value, actualOrigin, scale); - foreach (var ho in objectsInScale) + foreach (var (ho, originalState) in objectsInScale) { - ho.Position = GeometryUtils.GetScaledPosition(scale, actualOrigin, originalPositions[ho]); + ho.Position = GeometryUtils.GetScaledPosition(scale, actualOrigin, originalState.Position); } } @@ -119,9 +112,6 @@ namespace osu.Game.Rulesets.Osu.Edit objectsInScale = null; OriginalSurroundingQuad = null; - originalPositions = null; - originalPathControlPointPositions = null; - originalPathControlPointTypes = null; defaultOrigin = null; } @@ -193,7 +183,7 @@ namespace osu.Game.Rulesets.Osu.Edit private void moveSelectionInBounds() { - Quad quad = GeometryUtils.GetSurroundingQuad(objectsInScale!); + Quad quad = GeometryUtils.GetSurroundingQuad(objectsInScale!.Keys); Vector2 delta = Vector2.Zero; @@ -207,8 +197,22 @@ namespace osu.Game.Rulesets.Osu.Edit if (quad.BottomRight.Y > OsuPlayfield.BASE_SIZE.Y) delta.Y -= quad.BottomRight.Y - OsuPlayfield.BASE_SIZE.Y; - foreach (var h in objectsInScale!) + foreach (var (h, _) in objectsInScale!) h.Position += delta; } + + private struct OriginalHitObjectState + { + public Vector2 Position { get; } + public Vector2[]? PathControlPointPositions { get; } + public PathType?[]? PathControlPointTypes { get; } + + public OriginalHitObjectState(OsuHitObject hitObject) + { + Position = hitObject.Position; + PathControlPointPositions = (hitObject as IHasPath)?.Path.ControlPoints.Select(p => p.Position).ToArray(); + PathControlPointTypes = (hitObject as IHasPath)?.Path.ControlPoints.Select(p => p.Type).ToArray(); + } + } } } From 3e34b2d37ed895f9eb707be9921964795c2e750e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 23 May 2024 14:56:08 +0200 Subject: [PATCH 0895/2493] Bring back clamping in osu! scale handler Being able to flip doesn't really feel all that good and `master` was already clamping, so let's just bring that back for now. Flipping can be reconsidered in a follow-up if it actually can be made to behave well. --- osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs index b0299c5668..75b404684f 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs @@ -9,6 +9,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Primitives; +using osu.Framework.Logging; using osu.Framework.Utils; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects; @@ -120,6 +121,8 @@ namespace osu.Game.Rulesets.Osu.Edit private void scaleSlider(Slider slider, Vector2 scale, Vector2[] originalPathPositions, PathType?[] originalPathTypes) { + scale = Vector2.ComponentMax(scale, new Vector2(Precision.FLOAT_EPSILON)); + // Maintain the path types in case they were defaulted to bezier at some point during scaling for (int i = 0; i < slider.Path.ControlPoints.Count; i++) { @@ -178,7 +181,8 @@ namespace osu.Game.Rulesets.Osu.Edit if (!Precision.AlmostEquals(selectionQuad.BottomRight.Y - origin.Y, 0)) scale.Y = selectionQuad.BottomRight.Y - origin.Y < 0 ? MathHelper.Clamp(scale.Y, br2.Y, br1.Y) : MathHelper.Clamp(scale.Y, br1.Y, br2.Y); - return scale; + Logger.Log($"scale = {scale}"); + return Vector2.ComponentMax(scale, new Vector2(Precision.FLOAT_EPSILON)); } private void moveSelectionInBounds() From 128029e2af5bd0d23db7935761807487ded1eb15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 23 May 2024 15:08:43 +0200 Subject: [PATCH 0896/2493] Fix aspect ratio lock applying when shift pressed on a non-corner anchor It doesn't make sense and it wasn't doing the right thing. --- .../Edit/Compose/Components/SelectionBoxScaleHandle.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionBoxScaleHandle.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionBoxScaleHandle.cs index 352a4985d6..eca0c08ba1 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionBoxScaleHandle.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionBoxScaleHandle.cs @@ -3,6 +3,7 @@ using osu.Framework.Allocation; using osu.Framework.Extensions; +using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Graphics; using osu.Framework.Input.Events; using osu.Framework.Utils; @@ -47,14 +48,14 @@ namespace osu.Game.Screens.Edit.Compose.Components rawScale = convertDragEventToScaleMultiplier(e); - applyScale(shouldLockAspectRatio: e.ShiftPressed); + applyScale(shouldLockAspectRatio: isCornerAnchor(originalAnchor) && e.ShiftPressed); } protected override bool OnKeyDown(KeyDownEvent e) { if (IsDragged) { - applyScale(shouldLockAspectRatio: e.ShiftPressed); + applyScale(shouldLockAspectRatio: isCornerAnchor(originalAnchor) && e.ShiftPressed); return true; } @@ -66,7 +67,7 @@ namespace osu.Game.Screens.Edit.Compose.Components base.OnKeyUp(e); if (IsDragged) - applyScale(shouldLockAspectRatio: e.ShiftPressed); + applyScale(shouldLockAspectRatio: isCornerAnchor(originalAnchor) && e.ShiftPressed); } protected override void OnDragEnd(DragEndEvent e) @@ -123,5 +124,7 @@ namespace osu.Game.Screens.Edit.Compose.Components return Axes.Both; } } + + private bool isCornerAnchor(Anchor anchor) => !anchor.HasFlagFast(Anchor.x1) && !anchor.HasFlagFast(Anchor.y1); } } From d8ba95f87712a7e407a8e28c9c41cc8035b9826a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 23 May 2024 15:13:42 +0200 Subject: [PATCH 0897/2493] Remove leftover log whooops. --- osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs index 75b404684f..af03c4d925 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs @@ -9,7 +9,6 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Primitives; -using osu.Framework.Logging; using osu.Framework.Utils; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects; @@ -181,7 +180,6 @@ namespace osu.Game.Rulesets.Osu.Edit if (!Precision.AlmostEquals(selectionQuad.BottomRight.Y - origin.Y, 0)) scale.Y = selectionQuad.BottomRight.Y - origin.Y < 0 ? MathHelper.Clamp(scale.Y, br2.Y, br1.Y) : MathHelper.Clamp(scale.Y, br1.Y, br2.Y); - Logger.Log($"scale = {scale}"); return Vector2.ComponentMax(scale, new Vector2(Precision.FLOAT_EPSILON)); } From b1c7afd75b273c1f7f12738e1a48db7c2c956002 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 23 May 2024 23:45:04 +0900 Subject: [PATCH 0898/2493] Move to ctor --- .../Blueprints/Sliders/Components/SliderBodyPiece.cs | 11 ++++++----- .../Skinning/Default/ManualSliderBody.cs | 8 +++----- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderBodyPiece.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderBodyPiece.cs index 44c754d8f5..12626a77ed 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderBodyPiece.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderBodyPiece.cs @@ -28,21 +28,22 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components public SliderBodyPiece() { - InternalChild = body = new ManualSliderBody - { - AccentColour = Color4.Transparent - }; + AutoSizeAxes = Axes.Both; // SliderSelectionBlueprint relies on calling ReceivePositionalInputAt on this drawable to determine whether selection should occur. // Without AlwaysPresent, a movement in a parent container (ie. the editor composer area resizing) could cause incorrect input handling. AlwaysPresent = true; + + InternalChild = body = new ManualSliderBody + { + AccentColour = Color4.Transparent + }; } [BackgroundDependencyLoader] private void load(OsuColour colours) { body.BorderColour = colours.Yellow; - AutoSizeAxes = Axes.Both; } private int? lastVersion; diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/ManualSliderBody.cs b/osu.Game.Rulesets.Osu/Skinning/Default/ManualSliderBody.cs index 2fc18da254..127d13730a 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Default/ManualSliderBody.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Default/ManualSliderBody.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; -using osu.Framework.Allocation; using osu.Framework.Graphics; using osuTK; @@ -13,12 +12,11 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default /// public partial class ManualSliderBody : SliderBody { - public new void SetVertices(IReadOnlyList vertices) => base.SetVertices(vertices); - - [BackgroundDependencyLoader] - private void load() + public ManualSliderBody() { AutoSizeAxes = Axes.Both; } + + public new void SetVertices(IReadOnlyList vertices) => base.SetVertices(vertices); } } From 20c54ab697eec2bb183a2f7f9ea5022a7b9be66a Mon Sep 17 00:00:00 2001 From: js1086 Date: Thu, 23 May 2024 19:08:32 +0100 Subject: [PATCH 0899/2493] Apply code quality changes --- osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs | 3 +-- .../Difficulty/Skills/OsuStrainSkill.cs | 16 ++++++++-------- osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs | 10 ++++------ 3 files changed, 13 insertions(+), 16 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs index ad0e3fd107..6c17c84c19 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs @@ -34,8 +34,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills { currentStrain *= strainDecay(current.DeltaTime); currentStrain += AimEvaluator.EvaluateDifficultyOf(current, withSliders) * skillMultiplier; - - objectStrains.Add(currentStrain); + ObjectStrains.Add(currentStrain); return currentStrain; } diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs index 39175d55e0..c2e9357e3a 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs @@ -34,8 +34,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills /// protected virtual double DifficultyMultiplier => DEFAULT_DIFFICULTY_MULTIPLIER; - protected List objectStrains = new List(); - protected double difficulty; + protected List ObjectStrains = new List(); + protected double Difficulty; protected OsuStrainSkill(Mod[] mods) : base(mods) @@ -44,7 +44,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills public override double DifficultyValue() { - difficulty = 0; + Difficulty = 0; double weight = 1; // Sections with 0 strain are excluded to avoid worst-case time complexity of the following sort (e.g. /b/2351871). @@ -64,11 +64,11 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills // We're sorting from highest to lowest strain. foreach (double strain in strains.OrderDescending()) { - difficulty += strain * weight; + Difficulty += strain * weight; weight *= DecayWeight; } - return difficulty * DifficultyMultiplier; + return Difficulty * DifficultyMultiplier; } /// @@ -77,9 +77,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills /// public double CountDifficultStrains() { - double consistentTopStrain = difficulty / 10; // What would the top strain be if all strain values were identical + double consistentTopStrain = Difficulty / 10; // What would the top strain be if all strain values were identical // Use a weighted sum of all strains. Constants are arbitrary and give nice values - return objectStrains.Sum(s => 1.1 / (1 + Math.Exp(-10 * (s / consistentTopStrain - 0.88)))); + return ObjectStrains.Sum(s => 1.1 / (1 + Math.Exp(-10 * (s / consistentTopStrain - 0.88)))); } } -} \ No newline at end of file +} diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs index b1cd0b21d1..bf8e09bd53 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs @@ -41,23 +41,21 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills currentRhythm = RhythmEvaluator.EvaluateDifficultyOf(current); double totalStrain = currentStrain * currentRhythm; - - objectStrains.Add(totalStrain); + ObjectStrains.Add(totalStrain); return totalStrain; } public double RelevantNoteCount() { - if (objectStrains.Count == 0) + if (ObjectStrains.Count == 0) return 0; - double maxStrain = objectStrains.Max(); - + double maxStrain = ObjectStrains.Max(); if (maxStrain == 0) return 0; - return objectStrains.Sum(strain => 1.0 / (1.0 + Math.Exp(-(strain / maxStrain * 12.0 - 6.0)))); + return ObjectStrains.Sum(strain => 1.0 / (1.0 + Math.Exp(-(strain / maxStrain * 12.0 - 6.0)))); } } } From d47c4cb47946877f7ba00bfe0d497137638f8230 Mon Sep 17 00:00:00 2001 From: Aurelian Date: Fri, 24 May 2024 06:28:19 +0200 Subject: [PATCH 0900/2493] Test for scaling slider flat --- .../Editor/TestSliderScaling.cs | 113 ++++++++++++++++++ 1 file changed, 113 insertions(+) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSliderScaling.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSliderScaling.cs index 021fdba225..157a08df46 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSliderScaling.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSliderScaling.cs @@ -68,6 +68,119 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor AddAssert("slider length shrunk", () => slider.Path.Distance < distanceBefore); } + [Test] + [Timeout(4000)] //Catches crashes in other threads, but not ideal. Hopefully there is a improvement to this. + public void TestScalingSliderFlat( + [Values(0, 1, 2, 3)] int type_int + ) + { + Slider slider = null; + + switch (type_int) + { + case 0: + AddStep("Add linear slider", () => + { + slider = new Slider { StartTime = EditorClock.CurrentTime, Position = new Vector2(300) }; + + PathControlPoint[] points = + { + new PathControlPoint(new Vector2(0), PathType.LINEAR), + new PathControlPoint(new Vector2(50, 100)), + }; + + slider.Path = new SliderPath(points); + EditorBeatmap.Add(slider); + }); + break; + case 1: + AddStep("Add perfect curve slider", () => + { + slider = new Slider { StartTime = EditorClock.CurrentTime, Position = new Vector2(300) }; + + PathControlPoint[] points = + { + new PathControlPoint(new Vector2(0), PathType.PERFECT_CURVE), + new PathControlPoint(new Vector2(50, 25)), + new PathControlPoint(new Vector2(25, 100)), + }; + + slider.Path = new SliderPath(points); + EditorBeatmap.Add(slider); + }); + break; + case 2: + AddStep("Add bezier slider", () => + { + slider = new Slider { StartTime = EditorClock.CurrentTime, Position = new Vector2(300) }; + + PathControlPoint[] points = + { + new PathControlPoint(new Vector2(0), PathType.BEZIER), + new PathControlPoint(new Vector2(50, 25)), + new PathControlPoint(new Vector2(25, 80)), + new PathControlPoint(new Vector2(40, 100)), + }; + + slider.Path = new SliderPath(points); + EditorBeatmap.Add(slider); + }); + break; + AddStep("Add perfect curve slider", () => + { + slider = new Slider { StartTime = EditorClock.CurrentTime, Position = new Vector2(300) }; + + PathControlPoint[] points = + { + new PathControlPoint(new Vector2(0), PathType.PERFECT_CURVE), + new PathControlPoint(new Vector2(50, 25)), + new PathControlPoint(new Vector2(25, 100)), + }; + + slider.Path = new SliderPath(points); + EditorBeatmap.Add(slider); + }); + break; + case 3: + AddStep("Add catmull slider", () => + { + slider = new Slider { StartTime = EditorClock.CurrentTime, Position = new Vector2(300) }; + + PathControlPoint[] points = + { + new PathControlPoint(new Vector2(0), PathType.CATMULL), + new PathControlPoint(new Vector2(50, 25)), + new PathControlPoint(new Vector2(25, 80)), + new PathControlPoint(new Vector2(40, 100)), + }; + + slider.Path = new SliderPath(points); + EditorBeatmap.Add(slider); + }); + break; + } + + AddAssert("ensure object placed", () => EditorBeatmap.HitObjects.Count == 1); + + moveMouse(new Vector2(300)); + AddStep("select slider", () => InputManager.Click(MouseButton.Left)); + AddStep("slider is valid", () => slider.Path.GetSegmentEnds()); //To run ensureValid(); + + SelectionBoxDragHandle dragHandle = null!; + AddStep("store drag handle", () => dragHandle = Editor.ChildrenOfType().Skip(1).First()); + AddAssert("is dragHandle not null", () => dragHandle != null); + + AddStep("move mouse to handle", () => InputManager.MoveMouseTo(dragHandle)); + AddStep("begin drag", () => InputManager.PressButton(MouseButton.Left)); + moveMouse(new Vector2(0, 300)); + AddStep("end drag", () => InputManager.ReleaseButton(MouseButton.Left)); + + AddStep("move mouse to handle", () => InputManager.MoveMouseTo(dragHandle)); + AddStep("begin drag", () => InputManager.PressButton(MouseButton.Left)); + moveMouse(new Vector2(0, 300)); //Should crash here if broken, although doesn't count as failed... + AddStep("end drag", () => InputManager.ReleaseButton(MouseButton.Left)); + } + private void moveMouse(Vector2 pos) => AddStep($"move mouse to {pos}", () => InputManager.MoveMouseTo(playfield.ToScreenSpace(pos))); } From d948e0fc5c3af124d56b6d8bb80d2d904f73a704 Mon Sep 17 00:00:00 2001 From: Aurelian Date: Fri, 24 May 2024 08:26:17 +0200 Subject: [PATCH 0901/2493] Nearly straight sliders are treated as linear --- osu.Game/Rulesets/Objects/SliderPath.cs | 35 +++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/osu.Game/Rulesets/Objects/SliderPath.cs b/osu.Game/Rulesets/Objects/SliderPath.cs index e8e769e3fa..b0a5d02e71 100644 --- a/osu.Game/Rulesets/Objects/SliderPath.cs +++ b/osu.Game/Rulesets/Objects/SliderPath.cs @@ -269,6 +269,37 @@ namespace osu.Game.Rulesets.Objects pathCache.Validate(); } + /// + /// Checks if the array of vectors is almost straight. + /// + /// + /// The angle is first obtained based on the farthest vector from the first, + /// then we find the angle of each vector from the first, + /// and calculate the distance between the two angle vectors. + /// We than scale this distance to the distance from the first vector + /// (or by 10 if the distance is smaller), + /// and if it is greater than acceptableDifference, we return false. + /// + private static bool isAlmostStraight(Vector2[] vectors, float acceptableDifference = 0.1f) + { + if (vectors.Length <= 2 || vectors.All(x => x == vectors.First())) return true; + + Vector2 first = vectors.First(); + Vector2 farthest = vectors.MaxBy(x => Vector2.Distance(first, x)); + + Vector2 angle = Vector2.Normalize(farthest - first); + foreach (Vector2 vector in vectors) + { + if (vector == first) + continue; + + if (Math.Max(10.0f, Vector2.Distance(vector, first)) * Vector2.Distance(Vector2.Normalize(vector - first), angle) > acceptableDifference) + return false; + } + + return true; + } + private void calculatePath() { calculatedPath.Clear(); @@ -293,6 +324,10 @@ namespace osu.Game.Rulesets.Objects var segmentVertices = vertices.AsSpan().Slice(start, i - start + 1); var segmentType = ControlPoints[start].Type ?? PathType.LINEAR; + //If a segment is almost straight, treat it as linear. + if (segmentType != PathType.LINEAR && isAlmostStraight(segmentVertices.ToArray())) + segmentType = PathType.LINEAR; + // No need to calculate path when there is only 1 vertex if (segmentVertices.Length == 1) calculatedPath.Add(segmentVertices[0]); From 481883801fbdb8df3303d2cbec05fbe52cfdbdc9 Mon Sep 17 00:00:00 2001 From: Aurelian Date: Fri, 24 May 2024 08:55:10 +0200 Subject: [PATCH 0902/2493] Removed duplicate unreachable code --- .../Editor/TestSliderScaling.cs | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSliderScaling.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSliderScaling.cs index 157a08df46..44f85837dc 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSliderScaling.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSliderScaling.cs @@ -126,21 +126,6 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor EditorBeatmap.Add(slider); }); break; - AddStep("Add perfect curve slider", () => - { - slider = new Slider { StartTime = EditorClock.CurrentTime, Position = new Vector2(300) }; - - PathControlPoint[] points = - { - new PathControlPoint(new Vector2(0), PathType.PERFECT_CURVE), - new PathControlPoint(new Vector2(50, 25)), - new PathControlPoint(new Vector2(25, 100)), - }; - - slider.Path = new SliderPath(points); - EditorBeatmap.Add(slider); - }); - break; case 3: AddStep("Add catmull slider", () => { From fff52be59a9bc076aa30d7c9045dcf3acd1aff58 Mon Sep 17 00:00:00 2001 From: Aurelian Date: Fri, 24 May 2024 09:30:24 +0200 Subject: [PATCH 0903/2493] Addressed code quality issues --- .../Editor/TestSliderScaling.cs | 21 +++++++++++-------- osu.Game/Rulesets/Objects/SliderPath.cs | 5 +++-- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSliderScaling.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSliderScaling.cs index 44f85837dc..99694f82d1 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSliderScaling.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSliderScaling.cs @@ -71,12 +71,12 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor [Test] [Timeout(4000)] //Catches crashes in other threads, but not ideal. Hopefully there is a improvement to this. public void TestScalingSliderFlat( - [Values(0, 1, 2, 3)] int type_int + [Values(0, 1, 2, 3)] int typeInt ) { - Slider slider = null; + Slider slider = null!; - switch (type_int) + switch (typeInt) { case 0: AddStep("Add linear slider", () => @@ -93,6 +93,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor EditorBeatmap.Add(slider); }); break; + case 1: AddStep("Add perfect curve slider", () => { @@ -109,14 +110,15 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor EditorBeatmap.Add(slider); }); break; - case 2: - AddStep("Add bezier slider", () => + + case 3: + AddStep("Add catmull slider", () => { slider = new Slider { StartTime = EditorClock.CurrentTime, Position = new Vector2(300) }; PathControlPoint[] points = { - new PathControlPoint(new Vector2(0), PathType.BEZIER), + new PathControlPoint(new Vector2(0), PathType.CATMULL), new PathControlPoint(new Vector2(50, 25)), new PathControlPoint(new Vector2(25, 80)), new PathControlPoint(new Vector2(40, 100)), @@ -126,14 +128,15 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor EditorBeatmap.Add(slider); }); break; - case 3: - AddStep("Add catmull slider", () => + + default: + AddStep("Add bezier slider", () => { slider = new Slider { StartTime = EditorClock.CurrentTime, Position = new Vector2(300) }; PathControlPoint[] points = { - new PathControlPoint(new Vector2(0), PathType.CATMULL), + new PathControlPoint(new Vector2(0), PathType.BEZIER), new PathControlPoint(new Vector2(50, 25)), new PathControlPoint(new Vector2(25, 80)), new PathControlPoint(new Vector2(40, 100)), diff --git a/osu.Game/Rulesets/Objects/SliderPath.cs b/osu.Game/Rulesets/Objects/SliderPath.cs index b0a5d02e71..ddea23034c 100644 --- a/osu.Game/Rulesets/Objects/SliderPath.cs +++ b/osu.Game/Rulesets/Objects/SliderPath.cs @@ -274,9 +274,9 @@ namespace osu.Game.Rulesets.Objects /// /// /// The angle is first obtained based on the farthest vector from the first, - /// then we find the angle of each vector from the first, + /// then we find the angle of each vector from the first, /// and calculate the distance between the two angle vectors. - /// We than scale this distance to the distance from the first vector + /// We than scale this distance to the distance from the first vector /// (or by 10 if the distance is smaller), /// and if it is greater than acceptableDifference, we return false. /// @@ -288,6 +288,7 @@ namespace osu.Game.Rulesets.Objects Vector2 farthest = vectors.MaxBy(x => Vector2.Distance(first, x)); Vector2 angle = Vector2.Normalize(farthest - first); + foreach (Vector2 vector in vectors) { if (vector == first) From a80dbba9d0d4fc836b899b1825692170c1c843d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 24 May 2024 10:21:40 +0200 Subject: [PATCH 0904/2493] Update to not use obsoleted method --- osu.Game.Tests/Visual/Editing/TestSceneMetadataSection.cs | 2 +- osu.Game/Screens/Edit/Editor.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneMetadataSection.cs b/osu.Game.Tests/Visual/Editing/TestSceneMetadataSection.cs index 5930c077a4..8b6f31d599 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneMetadataSection.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneMetadataSection.cs @@ -65,7 +65,7 @@ namespace osu.Game.Tests.Visual.Editing // It's important values are committed immediately on focus loss so the editor exit sequence detects them. AddAssert("value immediately changed on focus loss", () => { - InputManager.TriggerFocusContention(metadataSection); + ((IFocusManager)InputManager).TriggerFocusContention(metadataSection); return editorBeatmap.Metadata.Artist; }, () => Is.EqualTo("Example ArtistExample Artist")); } diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 47b2a53607..55ab03a590 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -723,7 +723,7 @@ namespace osu.Game.Screens.Edit // // This is important to ensure that if the user is still editing a textbox, it will commit // (and potentially block the exit procedure for save). - GetContainingInputManager().TriggerFocusContention(this); + GetContainingFocusManager().TriggerFocusContention(this); if (!ExitConfirmed) { From 807d982a721ec043990d1871f41316763d3b5b15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 24 May 2024 10:24:50 +0200 Subject: [PATCH 0905/2493] Move workaround to subscreen --- osu.Game/Screens/Edit/Editor.cs | 6 +----- osu.Game/Screens/Edit/EditorScreen.cs | 5 +++++ osu.Game/Screens/Edit/Setup/SetupScreen.cs | 12 ++++++++++++ 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 55ab03a590..07c32983f5 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -719,11 +719,7 @@ namespace osu.Game.Screens.Edit public override bool OnExiting(ScreenExitEvent e) { - // Before exiting, trigger a focus loss. - // - // This is important to ensure that if the user is still editing a textbox, it will commit - // (and potentially block the exit procedure for save). - GetContainingFocusManager().TriggerFocusContention(this); + currentScreen?.OnExiting(e); if (!ExitConfirmed) { diff --git a/osu.Game/Screens/Edit/EditorScreen.cs b/osu.Game/Screens/Edit/EditorScreen.cs index 3bc870b898..a795b310a2 100644 --- a/osu.Game/Screens/Edit/EditorScreen.cs +++ b/osu.Game/Screens/Edit/EditorScreen.cs @@ -6,6 +6,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; +using osu.Framework.Screens; namespace osu.Game.Screens.Edit { @@ -37,6 +38,10 @@ namespace osu.Game.Screens.Edit protected override void PopOut() => this.FadeOut(); + public virtual void OnExiting(ScreenExitEvent e) + { + } + #region Clipboard operations public BindableBool CanCut { get; } = new BindableBool(); diff --git a/osu.Game/Screens/Edit/Setup/SetupScreen.cs b/osu.Game/Screens/Edit/Setup/SetupScreen.cs index 266ea1f929..5345db0a4f 100644 --- a/osu.Game/Screens/Edit/Setup/SetupScreen.cs +++ b/osu.Game/Screens/Edit/Setup/SetupScreen.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Shapes; +using osu.Framework.Screens; using osu.Game.Graphics.Containers; using osu.Game.Overlays; @@ -55,6 +56,17 @@ namespace osu.Game.Screens.Edit.Setup })); } + public override void OnExiting(ScreenExitEvent e) + { + base.OnExiting(e); + + // Before exiting, trigger a focus loss. + // + // This is important to ensure that if the user is still editing a textbox, it will commit + // (and potentially block the exit procedure for save). + GetContainingFocusManager().TriggerFocusContention(this); + } + private partial class SetupScreenSectionsContainer : SectionsContainer { protected override UserTrackingScrollContainer CreateScrollContainer() From 7255cc3344e75ec0e0eb1fd99eab94da1f50f784 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 24 May 2024 11:25:29 +0200 Subject: [PATCH 0906/2493] Fix tests dying on a nullref --- osu.Game/Screens/Edit/Setup/SetupScreen.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Setup/SetupScreen.cs b/osu.Game/Screens/Edit/Setup/SetupScreen.cs index 5345db0a4f..7a7907d08a 100644 --- a/osu.Game/Screens/Edit/Setup/SetupScreen.cs +++ b/osu.Game/Screens/Edit/Setup/SetupScreen.cs @@ -64,7 +64,7 @@ namespace osu.Game.Screens.Edit.Setup // // This is important to ensure that if the user is still editing a textbox, it will commit // (and potentially block the exit procedure for save). - GetContainingFocusManager().TriggerFocusContention(this); + GetContainingFocusManager()?.TriggerFocusContention(this); } private partial class SetupScreenSectionsContainer : SectionsContainer From 9045ec24abc5e844da7cc646fcc7fcbb620e75bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 24 May 2024 12:04:09 +0200 Subject: [PATCH 0907/2493] Rewrite test --- .../SongSelect/TestScenePlaySongSelect.cs | 64 ++++++++++--------- 1 file changed, 34 insertions(+), 30 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs index af8b2a7760..7f0c209215 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs @@ -93,49 +93,49 @@ namespace osu.Game.Tests.Visual.SongSelect createSongSelect(); changeMods(); - AddStep("decreasing speed without mods", () => songSelect?.ChangeSpeed(-0.05)); - AddAssert("halftime at 0.95", () => songSelect!.Mods.Value.Single() is ModHalfTime mod && Math.Round(mod.SpeedChange.Value, 2) == 0.95); + AddStep("decrease speed", () => songSelect?.ChangeSpeed(-0.05)); + AddAssert("half time activated at 0.95x", () => songSelect!.Mods.Value.OfType().Single().SpeedChange.Value, () => Is.EqualTo(0.95).Within(0.005)); - AddStep("decreasing speed with halftime", () => songSelect?.ChangeSpeed(-0.05)); - AddAssert("halftime at 0.9", () => songSelect!.Mods.Value.Single() is ModHalfTime mod && Math.Round(mod.SpeedChange.Value, 2) == 0.9); + AddStep("decrease speed", () => songSelect?.ChangeSpeed(-0.05)); + AddAssert("half time speed changed to 0.9x", () => songSelect!.Mods.Value.OfType().Single().SpeedChange.Value, () => Is.EqualTo(0.9).Within(0.005)); - AddStep("increasing speed with halftime", () => songSelect?.ChangeSpeed(+0.05)); - AddAssert("halftime at 0.95", () => songSelect!.Mods.Value.Single() is ModHalfTime mod && Math.Round(mod.SpeedChange.Value, 2) == 0.95); + AddStep("increase speed", () => songSelect?.ChangeSpeed(0.05)); + AddAssert("half time speed changed to 0.95x", () => songSelect!.Mods.Value.OfType().Single().SpeedChange.Value, () => Is.EqualTo(0.95).Within(0.005)); - AddStep("increasing speed with halftime to nomod", () => songSelect?.ChangeSpeed(+0.05)); + AddStep("increase speed", () => songSelect?.ChangeSpeed(0.05)); AddAssert("no mods selected", () => songSelect!.Mods.Value.Count == 0); - AddStep("increasing speed without mods", () => songSelect?.ChangeSpeed(+0.05)); - AddAssert("doubletime at 1.05", () => songSelect!.Mods.Value.Single() is ModDoubleTime mod && Math.Round(mod.SpeedChange.Value, 2) == 1.05); + AddStep("increase speed", () => songSelect?.ChangeSpeed(0.05)); + AddAssert("double time activated at 1.05x", () => songSelect!.Mods.Value.OfType().Single().SpeedChange.Value, () => Is.EqualTo(1.05).Within(0.005)); - AddStep("increasing speed with doubletime", () => songSelect?.ChangeSpeed(+0.05)); - AddAssert("doubletime at 1.1", () => songSelect!.Mods.Value.Single() is ModDoubleTime mod && Math.Round(mod.SpeedChange.Value, 2) == 1.1); + AddStep("increase speed", () => songSelect?.ChangeSpeed(0.05)); + AddAssert("double time speed changed to 1.1x", () => songSelect!.Mods.Value.OfType().Single().SpeedChange.Value, () => Is.EqualTo(1.1).Within(0.005)); - AddStep("decreasing speed with doubletime", () => songSelect?.ChangeSpeed(-0.05)); - AddAssert("doubletime at 1.05", () => songSelect!.Mods.Value.Single() is ModDoubleTime mod && Math.Round(mod.SpeedChange.Value, 2) == 1.05); + AddStep("decrease speed", () => songSelect?.ChangeSpeed(-0.05)); + AddAssert("double time speed changed to 1.05x", () => songSelect!.Mods.Value.OfType().Single().SpeedChange.Value, () => Is.EqualTo(1.05).Within(0.005)); OsuModNightcore nc = new OsuModNightcore { SpeedChange = { Value = 1.05 } }; changeMods(nc); - AddStep("increasing speed with nightcore", () => songSelect?.ChangeSpeed(+0.05)); - AddAssert("nightcore at 1.1", () => songSelect!.Mods.Value.Single() is ModNightcore mod && Math.Round(mod.SpeedChange.Value, 2) == 1.1); + AddStep("increase speed", () => songSelect?.ChangeSpeed(0.05)); + AddAssert("nightcore speed changed to 1.1x", () => songSelect!.Mods.Value.OfType().Single().SpeedChange.Value, () => Is.EqualTo(1.1).Within(0.005)); - AddStep("decreasing speed with nightcore", () => songSelect?.ChangeSpeed(-0.05)); - AddAssert("doubletime at 1.05", () => songSelect!.Mods.Value.Single() is ModNightcore mod && Math.Round(mod.SpeedChange.Value, 2) == 1.05); + AddStep("decrease speed", () => songSelect?.ChangeSpeed(-0.05)); + AddAssert("nightcore speed changed to 1.05x", () => songSelect!.Mods.Value.OfType().Single().SpeedChange.Value, () => Is.EqualTo(1.05).Within(0.005)); - AddStep("decreasing speed with nightcore to nomod", () => songSelect?.ChangeSpeed(-0.05)); + AddStep("decrease speed", () => songSelect?.ChangeSpeed(-0.05)); AddAssert("no mods selected", () => songSelect!.Mods.Value.Count == 0); - AddStep("decreasing speed nomod, nightcore was selected", () => songSelect?.ChangeSpeed(-0.05)); - AddAssert("daycore at 0.95", () => songSelect!.Mods.Value.Single() is ModDaycore mod && Math.Round(mod.SpeedChange.Value, 2) == 0.95); + AddStep("decrease speed", () => songSelect?.ChangeSpeed(-0.05)); + AddAssert("daycore activated at 0.95x", () => songSelect!.Mods.Value.OfType().Single().SpeedChange.Value, () => Is.EqualTo(0.95).Within(0.005)); - AddStep("decreasing speed with daycore", () => songSelect?.ChangeSpeed(-0.05)); - AddAssert("daycore at 0.9", () => songSelect!.Mods.Value.Single() is ModDaycore mod && Math.Round(mod.SpeedChange.Value, 2) == 0.9); + AddStep("decrease speed", () => songSelect?.ChangeSpeed(-0.05)); + AddAssert("daycore activated at 0.95x", () => songSelect!.Mods.Value.OfType().Single().SpeedChange.Value, () => Is.EqualTo(0.9).Within(0.005)); - AddStep("increasing speed with daycore", () => songSelect?.ChangeSpeed(0.05)); - AddAssert("daycore at 0.95", () => songSelect!.Mods.Value.Single() is ModDaycore mod && Math.Round(mod.SpeedChange.Value, 2) == 0.95); + AddStep("increase speed", () => songSelect?.ChangeSpeed(0.05)); + AddAssert("daycore activated at 0.95x", () => songSelect!.Mods.Value.OfType().Single().SpeedChange.Value, () => Is.EqualTo(0.95).Within(0.005)); OsuModDoubleTime dt = new OsuModDoubleTime { @@ -143,8 +143,9 @@ namespace osu.Game.Tests.Visual.SongSelect AdjustPitch = { Value = true }, }; changeMods(dt); - AddStep("decreasing speed from doubletime 1.02 with adjustpitch enabled", () => songSelect?.ChangeSpeed(-0.05)); - AddAssert("halftime at 0.97 with adjustpitch enabled", () => songSelect!.Mods.Value.Single() is ModHalfTime mod && Math.Round(mod.SpeedChange.Value, 2) == 0.97 && mod.AdjustPitch.Value); + AddStep("decrease speed", () => songSelect?.ChangeSpeed(-0.05)); + AddAssert("half time activated at 0.97x", () => songSelect!.Mods.Value.OfType().Single().SpeedChange.Value, () => Is.EqualTo(0.97).Within(0.005)); + AddAssert("adjust pitch preserved", () => songSelect!.Mods.Value.OfType().Single().AdjustPitch.Value, () => Is.True); OsuModHalfTime ht = new OsuModHalfTime { @@ -153,16 +154,19 @@ namespace osu.Game.Tests.Visual.SongSelect }; Mod[] modlist = { ht, new OsuModHardRock(), new OsuModHidden() }; changeMods(modlist); - AddStep("decreasing speed from halftime 0.97 with adjustpitch enabled, HDHR enabled", () => songSelect?.ChangeSpeed(0.05)); - AddAssert("doubletime at 1.02 with adjustpitch enabled, HDHR still enabled", () => songSelect!.Mods.Value.Count(mod => (mod is ModDoubleTime modDt && modDt.AdjustPitch.Value && Math.Round(modDt.SpeedChange.Value, 2) == 1.02) || mod is ModHardRock || mod is ModHidden) == 3); + AddStep("decrease speed", () => songSelect?.ChangeSpeed(0.05)); + AddAssert("double time activated at 1.02x", () => songSelect!.Mods.Value.OfType().Single().SpeedChange.Value, () => Is.EqualTo(1.02).Within(0.005)); + AddAssert("double time activated at 1.02x", () => songSelect!.Mods.Value.OfType().Single().AdjustPitch.Value, () => Is.True); + AddAssert("HD still enabled", () => songSelect!.Mods.Value.OfType().SingleOrDefault(), () => Is.Not.Null); + AddAssert("HR still enabled", () => songSelect!.Mods.Value.OfType().SingleOrDefault(), () => Is.Not.Null); changeMods(new ModWindUp()); AddStep("windup active, trying to change speed", () => songSelect?.ChangeSpeed(0.05)); AddAssert("windup still active", () => songSelect!.Mods.Value.First() is ModWindUp); changeMods(new ModAdaptiveSpeed()); - AddStep("adaptivespeed active, trying to change speed", () => songSelect?.ChangeSpeed(0.05)); - AddAssert("adaptivespeed still active", () => songSelect!.Mods.Value.First() is ModAdaptiveSpeed); + AddStep("adaptive speed active, trying to change speed", () => songSelect?.ChangeSpeed(0.05)); + AddAssert("adaptive speed still active", () => songSelect!.Mods.Value.First() is ModAdaptiveSpeed); } [Test] From 63406b6feb2215111626903bceef923ffb5ca46d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 24 May 2024 12:59:24 +0200 Subject: [PATCH 0908/2493] Rewrite implementation --- .../SongSelect/TestScenePlaySongSelect.cs | 51 ++++-- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 11 +- .../Screens/Select/ModSpeedHotkeyHandler.cs | 105 ++++++++++++ osu.Game/Screens/Select/SongSelect.cs | 152 +----------------- 4 files changed, 149 insertions(+), 170 deletions(-) create mode 100644 osu.Game/Screens/Select/ModSpeedHotkeyHandler.cs diff --git a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs index 7f0c209215..6581ce0323 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs @@ -93,25 +93,25 @@ namespace osu.Game.Tests.Visual.SongSelect createSongSelect(); changeMods(); - AddStep("decrease speed", () => songSelect?.ChangeSpeed(-0.05)); + decreaseModSpeed(); AddAssert("half time activated at 0.95x", () => songSelect!.Mods.Value.OfType().Single().SpeedChange.Value, () => Is.EqualTo(0.95).Within(0.005)); - AddStep("decrease speed", () => songSelect?.ChangeSpeed(-0.05)); + decreaseModSpeed(); AddAssert("half time speed changed to 0.9x", () => songSelect!.Mods.Value.OfType().Single().SpeedChange.Value, () => Is.EqualTo(0.9).Within(0.005)); - AddStep("increase speed", () => songSelect?.ChangeSpeed(0.05)); + increaseModSpeed(); AddAssert("half time speed changed to 0.95x", () => songSelect!.Mods.Value.OfType().Single().SpeedChange.Value, () => Is.EqualTo(0.95).Within(0.005)); - AddStep("increase speed", () => songSelect?.ChangeSpeed(0.05)); + increaseModSpeed(); AddAssert("no mods selected", () => songSelect!.Mods.Value.Count == 0); - AddStep("increase speed", () => songSelect?.ChangeSpeed(0.05)); + increaseModSpeed(); AddAssert("double time activated at 1.05x", () => songSelect!.Mods.Value.OfType().Single().SpeedChange.Value, () => Is.EqualTo(1.05).Within(0.005)); - AddStep("increase speed", () => songSelect?.ChangeSpeed(0.05)); + increaseModSpeed(); AddAssert("double time speed changed to 1.1x", () => songSelect!.Mods.Value.OfType().Single().SpeedChange.Value, () => Is.EqualTo(1.1).Within(0.005)); - AddStep("decrease speed", () => songSelect?.ChangeSpeed(-0.05)); + decreaseModSpeed(); AddAssert("double time speed changed to 1.05x", () => songSelect!.Mods.Value.OfType().Single().SpeedChange.Value, () => Is.EqualTo(1.05).Within(0.005)); OsuModNightcore nc = new OsuModNightcore @@ -119,22 +119,23 @@ namespace osu.Game.Tests.Visual.SongSelect SpeedChange = { Value = 1.05 } }; changeMods(nc); - AddStep("increase speed", () => songSelect?.ChangeSpeed(0.05)); + + increaseModSpeed(); AddAssert("nightcore speed changed to 1.1x", () => songSelect!.Mods.Value.OfType().Single().SpeedChange.Value, () => Is.EqualTo(1.1).Within(0.005)); - AddStep("decrease speed", () => songSelect?.ChangeSpeed(-0.05)); + decreaseModSpeed(); AddAssert("nightcore speed changed to 1.05x", () => songSelect!.Mods.Value.OfType().Single().SpeedChange.Value, () => Is.EqualTo(1.05).Within(0.005)); - AddStep("decrease speed", () => songSelect?.ChangeSpeed(-0.05)); + decreaseModSpeed(); AddAssert("no mods selected", () => songSelect!.Mods.Value.Count == 0); - AddStep("decrease speed", () => songSelect?.ChangeSpeed(-0.05)); + decreaseModSpeed(); AddAssert("daycore activated at 0.95x", () => songSelect!.Mods.Value.OfType().Single().SpeedChange.Value, () => Is.EqualTo(0.95).Within(0.005)); - AddStep("decrease speed", () => songSelect?.ChangeSpeed(-0.05)); + decreaseModSpeed(); AddAssert("daycore activated at 0.95x", () => songSelect!.Mods.Value.OfType().Single().SpeedChange.Value, () => Is.EqualTo(0.9).Within(0.005)); - AddStep("increase speed", () => songSelect?.ChangeSpeed(0.05)); + increaseModSpeed(); AddAssert("daycore activated at 0.95x", () => songSelect!.Mods.Value.OfType().Single().SpeedChange.Value, () => Is.EqualTo(0.95).Within(0.005)); OsuModDoubleTime dt = new OsuModDoubleTime @@ -143,7 +144,8 @@ namespace osu.Game.Tests.Visual.SongSelect AdjustPitch = { Value = true }, }; changeMods(dt); - AddStep("decrease speed", () => songSelect?.ChangeSpeed(-0.05)); + + decreaseModSpeed(); AddAssert("half time activated at 0.97x", () => songSelect!.Mods.Value.OfType().Single().SpeedChange.Value, () => Is.EqualTo(0.97).Within(0.005)); AddAssert("adjust pitch preserved", () => songSelect!.Mods.Value.OfType().Single().AdjustPitch.Value, () => Is.True); @@ -154,19 +156,34 @@ namespace osu.Game.Tests.Visual.SongSelect }; Mod[] modlist = { ht, new OsuModHardRock(), new OsuModHidden() }; changeMods(modlist); - AddStep("decrease speed", () => songSelect?.ChangeSpeed(0.05)); + + increaseModSpeed(); AddAssert("double time activated at 1.02x", () => songSelect!.Mods.Value.OfType().Single().SpeedChange.Value, () => Is.EqualTo(1.02).Within(0.005)); AddAssert("double time activated at 1.02x", () => songSelect!.Mods.Value.OfType().Single().AdjustPitch.Value, () => Is.True); AddAssert("HD still enabled", () => songSelect!.Mods.Value.OfType().SingleOrDefault(), () => Is.Not.Null); AddAssert("HR still enabled", () => songSelect!.Mods.Value.OfType().SingleOrDefault(), () => Is.Not.Null); changeMods(new ModWindUp()); - AddStep("windup active, trying to change speed", () => songSelect?.ChangeSpeed(0.05)); + increaseModSpeed(); AddAssert("windup still active", () => songSelect!.Mods.Value.First() is ModWindUp); changeMods(new ModAdaptiveSpeed()); - AddStep("adaptive speed active, trying to change speed", () => songSelect?.ChangeSpeed(0.05)); + increaseModSpeed(); AddAssert("adaptive speed still active", () => songSelect!.Mods.Value.First() is ModAdaptiveSpeed); + + void increaseModSpeed() => AddStep("increase mod speed", () => + { + InputManager.PressKey(Key.ControlLeft); + InputManager.Key(Key.Up); + InputManager.ReleaseKey(Key.ControlLeft); + }); + + void decreaseModSpeed() => AddStep("decrease mod speed", () => + { + InputManager.PressKey(Key.ControlLeft); + InputManager.Key(Key.Down); + InputManager.ReleaseKey(Key.ControlLeft); + }); } [Test] diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index ad589e8fa9..f8c67f4a10 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -64,9 +64,6 @@ namespace osu.Game.Overlays.Mods private Func isValidMod = _ => true; - [Resolved] - private SongSelect? songSelect { get; set; } - /// /// A function determining whether each mod in the column should be displayed. /// A return value of means that the mod is not filtered and therefore its corresponding panel should be displayed. @@ -138,6 +135,7 @@ namespace osu.Game.Overlays.Mods private FillFlowContainer footerButtonFlow = null!; private FillFlowContainer footerContentFlow = null!; private DeselectAllModsButton deselectAllModsButton = null!; + private ModSpeedHotkeyHandler modSpeedHotkeyHandler = null!; private Container aboveColumnsContent = null!; private RankingInformationDisplay? rankingInformationDisplay; @@ -190,7 +188,8 @@ namespace osu.Game.Overlays.Mods Anchor = Anchor.BottomCentre, Origin = Anchor.BottomCentre, Height = 0 - } + }, + modSpeedHotkeyHandler = new ModSpeedHotkeyHandler(), }); MainAreaContent.AddRange(new Drawable[] @@ -758,11 +757,11 @@ namespace osu.Game.Overlays.Mods } case GlobalAction.IncreaseModSpeed: - songSelect?.ChangeSpeed(0.05); + modSpeedHotkeyHandler.ChangeSpeed(0.05, AllAvailableMods.Where(state => state.ValidForSelection.Value).Select(state => state.Mod)); return true; case GlobalAction.DecreaseModSpeed: - songSelect?.ChangeSpeed(-0.05); + modSpeedHotkeyHandler.ChangeSpeed(-0.05, AllAvailableMods.Where(state => state.ValidForSelection.Value).Select(state => state.Mod)); return true; } diff --git a/osu.Game/Screens/Select/ModSpeedHotkeyHandler.cs b/osu.Game/Screens/Select/ModSpeedHotkeyHandler.cs new file mode 100644 index 0000000000..af64002bcf --- /dev/null +++ b/osu.Game/Screens/Select/ModSpeedHotkeyHandler.cs @@ -0,0 +1,105 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Utils; +using osu.Game.Configuration; +using osu.Game.Overlays; +using osu.Game.Overlays.OSD; +using osu.Game.Rulesets.Mods; +using osu.Game.Utils; + +namespace osu.Game.Screens.Select +{ + public partial class ModSpeedHotkeyHandler : Component + { + [Resolved] + private Bindable> selectedMods { get; set; } = null!; + + [Resolved] + private OsuConfigManager config { get; set; } = null!; + + [Resolved] + private OnScreenDisplay? onScreenDisplay { get; set; } + + private ModRateAdjust? lastActiveRateAdjustMod; + + protected override void LoadComplete() + { + base.LoadComplete(); + + selectedMods.BindValueChanged(val => + { + lastActiveRateAdjustMod = val.NewValue.OfType().SingleOrDefault() ?? lastActiveRateAdjustMod; + }, true); + } + + public bool ChangeSpeed(double delta, IEnumerable availableMods) + { + double targetSpeed = (selectedMods.Value.OfType().SingleOrDefault()?.SpeedChange.Value ?? 1) + delta; + + if (Precision.AlmostEquals(targetSpeed, 1, 0.005)) + { + selectedMods.Value = selectedMods.Value.Where(m => m is not ModRateAdjust).ToList(); + onScreenDisplay?.Display(new SpeedChangeToast(config, targetSpeed)); + return true; + } + + ModRateAdjust? targetMod; + + if (lastActiveRateAdjustMod is ModDaycore || lastActiveRateAdjustMod is ModNightcore) + { + targetMod = targetSpeed < 1 + ? availableMods.OfType().SingleOrDefault() + : availableMods.OfType().SingleOrDefault(); + } + else + { + targetMod = targetSpeed < 1 + ? availableMods.OfType().SingleOrDefault() + : availableMods.OfType().SingleOrDefault(); + } + + if (targetMod == null) + return false; + + // preserve other settings from latest rate adjust mod instance seen + if (lastActiveRateAdjustMod != null) + { + foreach (var (_, sourceProperty) in lastActiveRateAdjustMod.GetSettingsSourceProperties()) + { + if (sourceProperty.Name == nameof(ModRateAdjust.SpeedChange)) + continue; + + var targetProperty = targetMod.GetType().GetProperty(sourceProperty.Name); + + if (targetProperty == null) + continue; + + var targetBindable = (IBindable)targetProperty.GetValue(targetMod)!; + var sourceBindable = (IBindable)sourceProperty.GetValue(lastActiveRateAdjustMod)!; + + if (targetBindable.GetType() != sourceBindable.GetType()) + continue; + + lastActiveRateAdjustMod.CopyAdjustedSetting(targetBindable, sourceBindable); + } + } + + targetMod.SpeedChange.Value = targetSpeed; + + var intendedMods = selectedMods.Value.Where(m => m is not ModRateAdjust).Append(targetMod).ToList(); + + if (!ModUtils.CheckCompatibleSet(intendedMods)) + return false; + + selectedMods.Value = intendedMods; + onScreenDisplay?.Display(new SpeedChangeToast(config, targetMod.SpeedChange.Value)); + return true; + } + } +} diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index b3823d7a0f..14e3931fce 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -30,7 +30,6 @@ using osu.Game.Graphics.UserInterface; using osu.Game.Input.Bindings; using osu.Game.Overlays; using osu.Game.Overlays.Mods; -using osu.Game.Overlays.OSD; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Screens.Backgrounds; @@ -40,6 +39,7 @@ using osu.Game.Screens.Play; using osu.Game.Screens.Select.Details; using osu.Game.Screens.Select.Options; using osu.Game.Skinning; +using osu.Game.Utils; using osuTK; using osuTK.Graphics; using osuTK.Input; @@ -137,6 +137,7 @@ namespace osu.Game.Screens.Select private double audioFeedbackLastPlaybackTime; private IDisposable? modSelectOverlayRegistration; + private ModSpeedHotkeyHandler modSpeedHotkeyHandler = null!; private AdvancedStats advancedStats = null!; @@ -148,16 +149,6 @@ namespace osu.Game.Screens.Select private Bindable configBackgroundBlur = null!; - private bool lastPitchState; - - private bool usedPitchMods; - - [Resolved] - private OnScreenDisplay? onScreenDisplay { get; set; } - - [Resolved] - private OsuConfigManager config { get; set; } = null!; - [BackgroundDependencyLoader(true)] private void load(AudioManager audio, OsuColour colours, ManageCollectionsDialog? manageCollectionsDialog, DifficultyRecommender? recommender, OsuConfigManager config) { @@ -333,6 +324,7 @@ namespace osu.Game.Screens.Select { RelativeSizeAxes = Axes.Both, }, + modSpeedHotkeyHandler = new ModSpeedHotkeyHandler(), }); if (ShowFooter) @@ -823,140 +815,6 @@ namespace osu.Game.Screens.Select return false; } - private Mod getRateMod(ModType modType, Type type) - { - var modList = game.AvailableMods.Value[modType]; - var multiMod = (MultiMod)modList.First(mod => mod is MultiMod multiMod && multiMod.Mods.Count(mod2 => mod2.GetType().IsSubclassOf(type)) > 0); - var mod = multiMod.Mods.First(mod => mod.GetType().IsSubclassOf(type)); - return mod; - } - - public void ChangeSpeed(double delta) - { - ModNightcore modNc = (ModNightcore)getRateMod(ModType.DifficultyIncrease, typeof(ModNightcore)); - ModDoubleTime modDt = (ModDoubleTime)getRateMod(ModType.DifficultyIncrease, typeof(ModDoubleTime)); - ModDaycore modDc = (ModDaycore)getRateMod(ModType.DifficultyReduction, typeof(ModDaycore)); - ModHalfTime modHt = (ModHalfTime)getRateMod(ModType.DifficultyReduction, typeof(ModHalfTime)); - bool rateModActive = selectedMods.Value.Count(mod => mod is ModRateAdjust) > 0; - bool incompatibleModActive = selectedMods.Value.Count(mod => modDt.IncompatibleMods.Count(incompatibleMod => (mod.GetType().IsSubclassOf(incompatibleMod) || mod.GetType() == incompatibleMod) && incompatibleMod != typeof(ModRateAdjust)) > 0) > 0; - double newRate = Math.Round(1d + delta, 2); - bool isPositive = delta > 0; - - if (incompatibleModActive) - return; - - if (!rateModActive) - { - onScreenDisplay?.Display(new SpeedChangeToast(config, newRate)); - - // If no ModRateAdjust is active, activate one - ModRateAdjust? newMod = null; - - if (isPositive && !usedPitchMods) - newMod = modDt; - - if (isPositive && usedPitchMods) - newMod = modNc; - - if (!isPositive && !usedPitchMods) - newMod = modHt; - - if (!isPositive && usedPitchMods) - newMod = modDc; - - if (!usedPitchMods && newMod is ModDoubleTime newModDt) - newModDt.AdjustPitch.Value = lastPitchState; - - if (!usedPitchMods && newMod is ModHalfTime newModHt) - newModHt.AdjustPitch.Value = lastPitchState; - - newMod!.SpeedChange.Value = newRate; - selectedMods.Value = selectedMods.Value.Append(newMod).ToList(); - return; - } - - ModRateAdjust mod = (ModRateAdjust)selectedMods.Value.First(mod => mod is ModRateAdjust); - newRate = Math.Round(mod.SpeedChange.Value + delta, 2); - - // Disable RateAdjustMods if newRate is 1 - if (newRate == 1.0) - { - lastPitchState = false; - usedPitchMods = false; - - if (mod is ModDoubleTime dtmod && dtmod.AdjustPitch.Value) - lastPitchState = true; - - if (mod is ModHalfTime htmod && htmod.AdjustPitch.Value) - lastPitchState = true; - - if (mod is ModNightcore || mod is ModDaycore) - usedPitchMods = true; - - //Disable RateAdjustMods - selectedMods.Value = selectedMods.Value.Where(search => search is not ModRateAdjust).ToList(); - - onScreenDisplay?.Display(new SpeedChangeToast(config, newRate)); - - return; - } - - bool overMaxRateLimit = (mod is ModHalfTime || mod is ModDaycore) && newRate > mod.SpeedChange.MaxValue; - bool underMinRateLimit = (mod is ModDoubleTime || mod is ModNightcore) && newRate < mod.SpeedChange.MinValue; - - // Swap mod to opposite mod if newRate exceeds max/min speed values - if (overMaxRateLimit || underMinRateLimit) - { - bool adjustPitch = (mod is ModDoubleTime dtmod && dtmod.AdjustPitch.Value) || (mod is ModHalfTime htmod && htmod.AdjustPitch.Value); - - //Disable RateAdjustMods - selectedMods.Value = selectedMods.Value.Where(search => search is not ModRateAdjust).ToList(); - - ModRateAdjust? oppositeMod = null; - - switch (mod) - { - case ModDoubleTime: - modHt.AdjustPitch.Value = adjustPitch; - oppositeMod = modHt; - break; - - case ModHalfTime: - modDt.AdjustPitch.Value = adjustPitch; - oppositeMod = modDt; - break; - - case ModNightcore: - oppositeMod = modDc; - break; - - case ModDaycore: - oppositeMod = modNc; - break; - } - - if (oppositeMod == null) return; - - oppositeMod.SpeedChange.Value = newRate; - selectedMods.Value = selectedMods.Value.Append(oppositeMod).ToList(); - - onScreenDisplay?.Display(new SpeedChangeToast(config, newRate)); - - return; - } - - // Cap newRate to max/min values and change rate of current active mod - if (newRate > mod.SpeedChange.MaxValue && (mod is ModDoubleTime || mod is ModNightcore)) - newRate = mod.SpeedChange.MaxValue; - - if (newRate < mod.SpeedChange.MinValue && (mod is ModHalfTime || mod is ModDaycore)) - newRate = mod.SpeedChange.MinValue; - - mod.SpeedChange.Value = newRate; - - onScreenDisplay?.Display(new SpeedChangeToast(config, newRate)); - } - protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); @@ -1160,11 +1018,11 @@ namespace osu.Game.Screens.Select switch (e.Action) { case GlobalAction.IncreaseModSpeed: - ChangeSpeed(0.05); + modSpeedHotkeyHandler.ChangeSpeed(0.05, ModUtils.FlattenMods(game.AvailableMods.Value.SelectMany(kv => kv.Value))); return true; case GlobalAction.DecreaseModSpeed: - ChangeSpeed(-0.05); + modSpeedHotkeyHandler.ChangeSpeed(-0.05, ModUtils.FlattenMods(game.AvailableMods.Value.SelectMany(kv => kv.Value))); return true; } From 345fb60679c3017f75b1b2d9dd54c210fde50a25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 24 May 2024 13:08:17 +0200 Subject: [PATCH 0909/2493] Fix toast strings --- osu.Game/Localisation/ToastStrings.cs | 4 ++-- osu.Game/Overlays/OSD/SpeedChangeToast.cs | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/osu.Game/Localisation/ToastStrings.cs b/osu.Game/Localisation/ToastStrings.cs index 25899153f8..942540cfc5 100644 --- a/osu.Game/Localisation/ToastStrings.cs +++ b/osu.Game/Localisation/ToastStrings.cs @@ -50,9 +50,9 @@ namespace osu.Game.Localisation public static LocalisableString UrlCopied => new TranslatableString(getKey(@"url_copied"), @"URL copied"); /// - /// "Speed changed to" + /// "Speed changed to {0:N2}x" /// - public static LocalisableString SpeedChangedTo => new TranslatableString(getKey(@"speed_changed"), @"Speed changed to"); + public static LocalisableString SpeedChangedTo(double speed) => new TranslatableString(getKey(@"speed_changed"), @"Speed changed to {0:N2}x", speed); private static string getKey(string key) => $@"{prefix}:{key}"; } diff --git a/osu.Game/Overlays/OSD/SpeedChangeToast.cs b/osu.Game/Overlays/OSD/SpeedChangeToast.cs index df4f825541..49d3985b04 100644 --- a/osu.Game/Overlays/OSD/SpeedChangeToast.cs +++ b/osu.Game/Overlays/OSD/SpeedChangeToast.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.Threading; using osu.Game.Configuration; using osu.Game.Input.Bindings; using osu.Game.Localisation; @@ -11,7 +10,7 @@ namespace osu.Game.Overlays.OSD public partial class SpeedChangeToast : Toast { public SpeedChangeToast(OsuConfigManager config, double newSpeed) - : base(CommonStrings.Beatmaps, ToastStrings.SpeedChangedTo + " " + newSpeed.ToString(Thread.CurrentThread.CurrentCulture), config.LookupKeyBindings(GlobalAction.IncreaseModSpeed) + " / " + config.LookupKeyBindings(GlobalAction.DecreaseModSpeed)) + : base(ModSelectOverlayStrings.ModCustomisation, ToastStrings.SpeedChangedTo(newSpeed), config.LookupKeyBindings(GlobalAction.IncreaseModSpeed) + " / " + config.LookupKeyBindings(GlobalAction.DecreaseModSpeed)) { } } From 8cac87e4960253eae950f68c88923ad6dcf622dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 24 May 2024 13:09:07 +0200 Subject: [PATCH 0910/2493] Fix speed controls in mod select overlay not handling repeat --- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index f8c67f4a10..bc87bb4e3d 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -704,6 +704,17 @@ namespace osu.Game.Overlays.Mods public override bool OnPressed(KeyBindingPressEvent e) { + switch (e.Action) + { + case GlobalAction.IncreaseModSpeed: + modSpeedHotkeyHandler.ChangeSpeed(0.05, AllAvailableMods.Where(state => state.ValidForSelection.Value).Select(state => state.Mod)); + return true; + + case GlobalAction.DecreaseModSpeed: + modSpeedHotkeyHandler.ChangeSpeed(-0.05, AllAvailableMods.Where(state => state.ValidForSelection.Value).Select(state => state.Mod)); + return true; + } + if (e.Repeat) return false; @@ -755,14 +766,6 @@ namespace osu.Game.Overlays.Mods return true; } - - case GlobalAction.IncreaseModSpeed: - modSpeedHotkeyHandler.ChangeSpeed(0.05, AllAvailableMods.Where(state => state.ValidForSelection.Value).Select(state => state.Mod)); - return true; - - case GlobalAction.DecreaseModSpeed: - modSpeedHotkeyHandler.ChangeSpeed(-0.05, AllAvailableMods.Where(state => state.ValidForSelection.Value).Select(state => state.Mod)); - return true; } return base.OnPressed(e); From b1b207960a46337b0a64036d575a23b846f638c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 24 May 2024 13:09:44 +0200 Subject: [PATCH 0911/2493] Actually use return value --- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 6 ++---- osu.Game/Screens/Select/SongSelect.cs | 6 ++---- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index bc87bb4e3d..8489b06f47 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -707,12 +707,10 @@ namespace osu.Game.Overlays.Mods switch (e.Action) { case GlobalAction.IncreaseModSpeed: - modSpeedHotkeyHandler.ChangeSpeed(0.05, AllAvailableMods.Where(state => state.ValidForSelection.Value).Select(state => state.Mod)); - return true; + return modSpeedHotkeyHandler.ChangeSpeed(0.05, AllAvailableMods.Where(state => state.ValidForSelection.Value).Select(state => state.Mod)); case GlobalAction.DecreaseModSpeed: - modSpeedHotkeyHandler.ChangeSpeed(-0.05, AllAvailableMods.Where(state => state.ValidForSelection.Value).Select(state => state.Mod)); - return true; + return modSpeedHotkeyHandler.ChangeSpeed(-0.05, AllAvailableMods.Where(state => state.ValidForSelection.Value).Select(state => state.Mod)); } if (e.Repeat) diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index 14e3931fce..269ca37ff5 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -1018,12 +1018,10 @@ namespace osu.Game.Screens.Select switch (e.Action) { case GlobalAction.IncreaseModSpeed: - modSpeedHotkeyHandler.ChangeSpeed(0.05, ModUtils.FlattenMods(game.AvailableMods.Value.SelectMany(kv => kv.Value))); - return true; + return modSpeedHotkeyHandler.ChangeSpeed(0.05, ModUtils.FlattenMods(game.AvailableMods.Value.SelectMany(kv => kv.Value))); case GlobalAction.DecreaseModSpeed: - modSpeedHotkeyHandler.ChangeSpeed(-0.05, ModUtils.FlattenMods(game.AvailableMods.Value.SelectMany(kv => kv.Value))); - return true; + return modSpeedHotkeyHandler.ChangeSpeed(-0.05, ModUtils.FlattenMods(game.AvailableMods.Value.SelectMany(kv => kv.Value))); } if (e.Repeat) From cab8cf741073959dc314516bac3e3fb63dcc262b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 24 May 2024 13:14:06 +0200 Subject: [PATCH 0912/2493] Move mod speed hotkey handler to user mod select overlay The very base class is the wrong place for it because `FreeModSelectOverlay` inherits from it, and that one has different assumptions and rules than "user" selection. In particular, in non-user selection, more than one rate adjust mod may be active, which breaks the mod speed hotkey's basic assumptions. --- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 12 --------- .../Overlays/Mods/UserModSelectOverlay.cs | 26 +++++++++++++++++++ 2 files changed, 26 insertions(+), 12 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index 8489b06f47..d2d7ace936 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -27,7 +27,6 @@ using osu.Game.Graphics.UserInterface; using osu.Game.Input.Bindings; using osu.Game.Localisation; using osu.Game.Rulesets.Mods; -using osu.Game.Screens.Select; using osu.Game.Utils; using osuTK; using osuTK.Input; @@ -135,7 +134,6 @@ namespace osu.Game.Overlays.Mods private FillFlowContainer footerButtonFlow = null!; private FillFlowContainer footerContentFlow = null!; private DeselectAllModsButton deselectAllModsButton = null!; - private ModSpeedHotkeyHandler modSpeedHotkeyHandler = null!; private Container aboveColumnsContent = null!; private RankingInformationDisplay? rankingInformationDisplay; @@ -189,7 +187,6 @@ namespace osu.Game.Overlays.Mods Origin = Anchor.BottomCentre, Height = 0 }, - modSpeedHotkeyHandler = new ModSpeedHotkeyHandler(), }); MainAreaContent.AddRange(new Drawable[] @@ -704,15 +701,6 @@ namespace osu.Game.Overlays.Mods public override bool OnPressed(KeyBindingPressEvent e) { - switch (e.Action) - { - case GlobalAction.IncreaseModSpeed: - return modSpeedHotkeyHandler.ChangeSpeed(0.05, AllAvailableMods.Where(state => state.ValidForSelection.Value).Select(state => state.Mod)); - - case GlobalAction.DecreaseModSpeed: - return modSpeedHotkeyHandler.ChangeSpeed(-0.05, AllAvailableMods.Where(state => state.ValidForSelection.Value).Select(state => state.Mod)); - } - if (e.Repeat) return false; diff --git a/osu.Game/Overlays/Mods/UserModSelectOverlay.cs b/osu.Game/Overlays/Mods/UserModSelectOverlay.cs index 49469b99f3..16d71e557b 100644 --- a/osu.Game/Overlays/Mods/UserModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/UserModSelectOverlay.cs @@ -3,18 +3,30 @@ using System.Collections.Generic; using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Input.Events; +using osu.Game.Input.Bindings; using osu.Game.Rulesets.Mods; +using osu.Game.Screens.Select; using osu.Game.Utils; namespace osu.Game.Overlays.Mods { public partial class UserModSelectOverlay : ModSelectOverlay { + private ModSpeedHotkeyHandler modSpeedHotkeyHandler = null!; + public UserModSelectOverlay(OverlayColourScheme colourScheme = OverlayColourScheme.Green) : base(colourScheme) { } + [BackgroundDependencyLoader] + private void load() + { + Add(modSpeedHotkeyHandler = new ModSpeedHotkeyHandler()); + } + protected override ModColumn CreateModColumn(ModType modType) => new UserModColumn(modType, false); protected override IReadOnlyList ComputeNewModsFromSelection(IReadOnlyList oldSelection, IReadOnlyList newSelection) @@ -38,6 +50,20 @@ namespace osu.Game.Overlays.Mods return modsAfterRemoval.ToList(); } + public override bool OnPressed(KeyBindingPressEvent e) + { + switch (e.Action) + { + case GlobalAction.IncreaseModSpeed: + return modSpeedHotkeyHandler.ChangeSpeed(0.05, AllAvailableMods.Where(state => state.ValidForSelection.Value).Select(state => state.Mod)); + + case GlobalAction.DecreaseModSpeed: + return modSpeedHotkeyHandler.ChangeSpeed(-0.05, AllAvailableMods.Where(state => state.ValidForSelection.Value).Select(state => state.Mod)); + } + + return base.OnPressed(e); + } + private partial class UserModColumn : ModColumn { public UserModColumn(ModType modType, bool allowIncompatibleSelection) From b2c4e0e951bb9fc5e1ba50ffe4429109f11b34cc Mon Sep 17 00:00:00 2001 From: Aurelian Date: Fri, 24 May 2024 14:05:56 +0200 Subject: [PATCH 0913/2493] Reworked linear line check, and optimized scaled flat slider test --- .../Editor/TestSliderScaling.cs | 147 ++++++------------ osu.Game/Rulesets/Objects/SliderPath.cs | 43 +---- 2 files changed, 53 insertions(+), 137 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSliderScaling.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSliderScaling.cs index 99694f82d1..ef3824b5b0 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSliderScaling.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSliderScaling.cs @@ -6,6 +6,7 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Testing; +using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; @@ -68,108 +69,52 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor AddAssert("slider length shrunk", () => slider.Path.Distance < distanceBefore); } - [Test] - [Timeout(4000)] //Catches crashes in other threads, but not ideal. Hopefully there is a improvement to this. - public void TestScalingSliderFlat( - [Values(0, 1, 2, 3)] int typeInt - ) - { - Slider slider = null!; - - switch (typeInt) - { - case 0: - AddStep("Add linear slider", () => - { - slider = new Slider { StartTime = EditorClock.CurrentTime, Position = new Vector2(300) }; - - PathControlPoint[] points = - { - new PathControlPoint(new Vector2(0), PathType.LINEAR), - new PathControlPoint(new Vector2(50, 100)), - }; - - slider.Path = new SliderPath(points); - EditorBeatmap.Add(slider); - }); - break; - - case 1: - AddStep("Add perfect curve slider", () => - { - slider = new Slider { StartTime = EditorClock.CurrentTime, Position = new Vector2(300) }; - - PathControlPoint[] points = - { - new PathControlPoint(new Vector2(0), PathType.PERFECT_CURVE), - new PathControlPoint(new Vector2(50, 25)), - new PathControlPoint(new Vector2(25, 100)), - }; - - slider.Path = new SliderPath(points); - EditorBeatmap.Add(slider); - }); - break; - - case 3: - AddStep("Add catmull slider", () => - { - slider = new Slider { StartTime = EditorClock.CurrentTime, Position = new Vector2(300) }; - - PathControlPoint[] points = - { - new PathControlPoint(new Vector2(0), PathType.CATMULL), - new PathControlPoint(new Vector2(50, 25)), - new PathControlPoint(new Vector2(25, 80)), - new PathControlPoint(new Vector2(40, 100)), - }; - - slider.Path = new SliderPath(points); - EditorBeatmap.Add(slider); - }); - break; - - default: - AddStep("Add bezier slider", () => - { - slider = new Slider { StartTime = EditorClock.CurrentTime, Position = new Vector2(300) }; - - PathControlPoint[] points = - { - new PathControlPoint(new Vector2(0), PathType.BEZIER), - new PathControlPoint(new Vector2(50, 25)), - new PathControlPoint(new Vector2(25, 80)), - new PathControlPoint(new Vector2(40, 100)), - }; - - slider.Path = new SliderPath(points); - EditorBeatmap.Add(slider); - }); - break; - } - - AddAssert("ensure object placed", () => EditorBeatmap.HitObjects.Count == 1); - - moveMouse(new Vector2(300)); - AddStep("select slider", () => InputManager.Click(MouseButton.Left)); - AddStep("slider is valid", () => slider.Path.GetSegmentEnds()); //To run ensureValid(); - - SelectionBoxDragHandle dragHandle = null!; - AddStep("store drag handle", () => dragHandle = Editor.ChildrenOfType().Skip(1).First()); - AddAssert("is dragHandle not null", () => dragHandle != null); - - AddStep("move mouse to handle", () => InputManager.MoveMouseTo(dragHandle)); - AddStep("begin drag", () => InputManager.PressButton(MouseButton.Left)); - moveMouse(new Vector2(0, 300)); - AddStep("end drag", () => InputManager.ReleaseButton(MouseButton.Left)); - - AddStep("move mouse to handle", () => InputManager.MoveMouseTo(dragHandle)); - AddStep("begin drag", () => InputManager.PressButton(MouseButton.Left)); - moveMouse(new Vector2(0, 300)); //Should crash here if broken, although doesn't count as failed... - AddStep("end drag", () => InputManager.ReleaseButton(MouseButton.Left)); - } - private void moveMouse(Vector2 pos) => AddStep($"move mouse to {pos}", () => InputManager.MoveMouseTo(playfield.ToScreenSpace(pos))); } + [TestFixture] + public class TestSliderNearLinearScaling + { + [Test] + public void TestScalingSliderFlat() + { + Slider sliderPerfect = new Slider + { + Position = new Vector2(300), + Path = new SliderPath( + [ + new PathControlPoint(new Vector2(0), PathType.PERFECT_CURVE), + new PathControlPoint(new Vector2(50, 25)), + new PathControlPoint(new Vector2(25, 100)), + ]) + }; + + Slider sliderBezier = new Slider + { + Position = new Vector2(300), + Path = new SliderPath( + [ + new PathControlPoint(new Vector2(0), PathType.BEZIER), + new PathControlPoint(new Vector2(50, 25)), + new PathControlPoint(new Vector2(25, 100)), + ]) + }; + + scaleSlider(sliderPerfect, new Vector2(0.000001f, 1)); + scaleSlider(sliderBezier, new Vector2(0.000001f, 1)); + + for (int i = 0; i < 100; i++) + { + Assert.True(Precision.AlmostEquals(sliderPerfect.Path.PositionAt(i / 100.0f), sliderBezier.Path.PositionAt(i / 100.0f))); + } + } + + private void scaleSlider(Slider slider, Vector2 scale) + { + for (int i = 0; i < slider.Path.ControlPoints.Count; i++) + { + slider.Path.ControlPoints[i].Position *= scale; + } + } + } } diff --git a/osu.Game/Rulesets/Objects/SliderPath.cs b/osu.Game/Rulesets/Objects/SliderPath.cs index ddea23034c..eca14269fe 100644 --- a/osu.Game/Rulesets/Objects/SliderPath.cs +++ b/osu.Game/Rulesets/Objects/SliderPath.cs @@ -269,38 +269,6 @@ namespace osu.Game.Rulesets.Objects pathCache.Validate(); } - /// - /// Checks if the array of vectors is almost straight. - /// - /// - /// The angle is first obtained based on the farthest vector from the first, - /// then we find the angle of each vector from the first, - /// and calculate the distance between the two angle vectors. - /// We than scale this distance to the distance from the first vector - /// (or by 10 if the distance is smaller), - /// and if it is greater than acceptableDifference, we return false. - /// - private static bool isAlmostStraight(Vector2[] vectors, float acceptableDifference = 0.1f) - { - if (vectors.Length <= 2 || vectors.All(x => x == vectors.First())) return true; - - Vector2 first = vectors.First(); - Vector2 farthest = vectors.MaxBy(x => Vector2.Distance(first, x)); - - Vector2 angle = Vector2.Normalize(farthest - first); - - foreach (Vector2 vector in vectors) - { - if (vector == first) - continue; - - if (Math.Max(10.0f, Vector2.Distance(vector, first)) * Vector2.Distance(Vector2.Normalize(vector - first), angle) > acceptableDifference) - return false; - } - - return true; - } - private void calculatePath() { calculatedPath.Clear(); @@ -325,10 +293,6 @@ namespace osu.Game.Rulesets.Objects var segmentVertices = vertices.AsSpan().Slice(start, i - start + 1); var segmentType = ControlPoints[start].Type ?? PathType.LINEAR; - //If a segment is almost straight, treat it as linear. - if (segmentType != PathType.LINEAR && isAlmostStraight(segmentVertices.ToArray())) - segmentType = PathType.LINEAR; - // No need to calculate path when there is only 1 vertex if (segmentVertices.Length == 1) calculatedPath.Add(segmentVertices[0]); @@ -366,6 +330,13 @@ namespace osu.Game.Rulesets.Objects if (subControlPoints.Length != 3) break; + //If a curve's theta range almost equals zero, the radius needed to have more than a + //floating point error difference is very large and results in a nearly straight path. + //Calculate it via a bezier aproximation instead. + //0.0005 corresponds with a radius of 8000 to have a more than 0.001 shift in the X value + if (Math.Abs(new CircularArcProperties(subControlPoints).ThetaRange) <= 0.0005d) + break; + List subPath = PathApproximator.CircularArcToPiecewiseLinear(subControlPoints); // If for some reason a circular arc could not be fit to the 3 given points, fall back to a numerically stable bezier approximation. From 497701950d9582c9792f0da3ec883d69079adeb6 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Fri, 24 May 2024 18:11:28 +0200 Subject: [PATCH 0914/2493] fix nitpick --- .../Screens/Edit/Compose/Components/LinedPositionSnapGrid.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/LinedPositionSnapGrid.cs b/osu.Game/Screens/Edit/Compose/Components/LinedPositionSnapGrid.cs index 8a7f6b5344..79b4fa2841 100644 --- a/osu.Game/Screens/Edit/Compose/Components/LinedPositionSnapGrid.cs +++ b/osu.Game/Screens/Edit/Compose/Components/LinedPositionSnapGrid.cs @@ -28,7 +28,8 @@ namespace osu.Game.Screens.Edit.Compose.Components while (true) { - Vector2 currentPosition = StartPosition.Value + index++ * step; + Vector2 currentPosition = StartPosition.Value + index * step; + index++; if (!lineDefinitelyIntersectsBox(currentPosition, step.PerpendicularLeft, drawSize, out var p1, out var p2)) { From 1f012937833974b9da49e32d21621d3ee6729003 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 25 May 2024 12:36:09 +0300 Subject: [PATCH 0915/2493] Make scores slanted in test scene --- .../Visual/SongSelect/TestSceneLeaderboardScoreV2.cs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneLeaderboardScoreV2.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneLeaderboardScoreV2.cs index c5f96d1568..ffd07be8aa 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneLeaderboardScoreV2.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneLeaderboardScoreV2.cs @@ -53,15 +53,22 @@ namespace osu.Game.Tests.Visual.SongSelect Width = relativeWidth, Anchor = Anchor.Centre, Origin = Anchor.Centre, - Spacing = new Vector2(0, 10), RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, + Spacing = new Vector2(0f, 2f), }, drawWidthText = new OsuSpriteText(), }; + int i = 0; + foreach (var scoreInfo in getTestScores()) - fillFlow.Add(new LeaderboardScoreV2(scoreInfo, scoreInfo.Position, scoreInfo.User.Id == 2)); + { + fillFlow.Add(new LeaderboardScoreV2(scoreInfo, scoreInfo.Position, scoreInfo.User.Id == 2) + { + Margin = new MarginPadding { Right = 10f * i, Left = -10f * i++ }, + }); + } foreach (var score in fillFlow.Children) score.Show(); From 35af518fdb3b0099faa74b09fdbaa43a1892dd46 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 25 May 2024 12:52:42 +0300 Subject: [PATCH 0916/2493] Remove expanded/contracted states and limit to 5 mods Also adjusts right content width to contain those 5 mods. Not sure how to handle the extra space in the score though...to be dealt with later. --- .../SongSelect/TestSceneLeaderboardScoreV2.cs | 9 +++---- .../Online/Leaderboards/LeaderboardScoreV2.cs | 27 ++++++++++--------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneLeaderboardScoreV2.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneLeaderboardScoreV2.cs index ffd07be8aa..86c3c9a7ac 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneLeaderboardScoreV2.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneLeaderboardScoreV2.cs @@ -161,16 +161,13 @@ namespace osu.Game.Tests.Visual.SongSelect } }; - for (int i = 0; i < LeaderboardScoreV2.MAX_MODS_EXPANDED; i++) - scores[0].Mods = scores[0].Mods.Concat(new Mod[] { i % 2 == 0 ? new OsuModHidden() : halfTime }).ToArray(); - - for (int i = 0; i < LeaderboardScoreV2.MAX_MODS_EXPANDED + 1; i++) + for (int i = 0; i < LeaderboardScoreV2.MAX_MODS - 1; i++) scores[1].Mods = scores[1].Mods.Concat(new Mod[] { i % 2 == 0 ? new OsuModHidden() : new OsuModHalfTime() }).ToArray(); - for (int i = 0; i < LeaderboardScoreV2.MAX_MODS_CONTRACTED; i++) + for (int i = 0; i < LeaderboardScoreV2.MAX_MODS; i++) scores[2].Mods = scores[2].Mods.Concat(new Mod[] { i % 2 == 0 ? new OsuModHidden() : halfTime }).ToArray(); - for (int i = 0; i < LeaderboardScoreV2.MAX_MODS_CONTRACTED + 1; i++) + for (int i = 0; i < LeaderboardScoreV2.MAX_MODS + 1; i++) scores[3].Mods = scores[3].Mods.Concat(new Mod[] { i % 2 == 0 ? new OsuModHidden() : new OsuModHalfTime() }).ToArray(); scores[4].Mods = scores[4].BeatmapInfo!.Ruleset.CreateInstance().CreateAllMods().ToArray(); diff --git a/osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs b/osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs index b9ae3bb20e..5db86bd7d0 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs @@ -43,14 +43,9 @@ namespace osu.Game.Online.Leaderboards /// /// The maximum number of mods when contracted until the mods display width exceeds the . /// - public const int MAX_MODS_CONTRACTED = 13; + public const int MAX_MODS = 5; - /// - /// The maximum number of mods when expanded until the mods display width exceeds the . - /// - public const int MAX_MODS_EXPANDED = 4; - - private const float right_content_width = 180; + private const float right_content_width = 210; private const float grade_width = 40; private const float username_min_width = 125; private const float statistics_regular_min_width = 175; @@ -183,8 +178,17 @@ namespace osu.Game.Online.Leaderboards innerAvatar.OnLoadComplete += d => d.FadeInFromZero(200); - modsContainer.Spacing = new Vector2(modsContainer.Children.Count > MAX_MODS_EXPANDED ? -20 : 2, 0); - modsContainer.Padding = new MarginPadding { Top = modsContainer.Children.Count > 0 ? 4 : 0 }; + if (score.Mods.Length > MAX_MODS) + modsCounter.Text = $"{score.Mods.Length} mods"; + else if (score.Mods.Length > 0) + { + modsContainer.ChildrenEnumerable = score.Mods.AsOrdered().Select(mod => new ColouredModSwitchTiny(mod) + { + Scale = new Vector2(0.375f) + }); + } + + modsContainer.Padding = new MarginPadding { Top = score.Mods.Length > 0 ? 4 : 0 }; } private Container createCentreContent(APIUser user) => new Container @@ -439,14 +443,13 @@ namespace osu.Game.Online.Leaderboards Shear = -shear, AutoSizeAxes = Axes.Both, Direction = FillDirection.Horizontal, - ChildrenEnumerable = score.Mods.AsOrdered().Select(mod => new ColouredModSwitchTiny(mod) { Scale = new Vector2(0.375f) }) + Spacing = new Vector2(2f, 0f), }, modsCounter = new OsuSpriteText { Anchor = Anchor.TopRight, Origin = Anchor.TopRight, Shear = -shear, - Text = $"{score.Mods.Length} mods", Alpha = 0, } } @@ -492,7 +495,7 @@ namespace osu.Game.Online.Leaderboards using (BeginDelayedSequence(50)) { - Drawable modsDrawable = score.Mods.Length > MAX_MODS_CONTRACTED ? modsCounter : modsContainer; + Drawable modsDrawable = score.Mods.Length > MAX_MODS ? modsCounter : modsContainer; var drawables = new[] { flagBadgeAndDateContainer, modsDrawable }.Concat(statisticsLabels).ToArray(); for (int i = 0; i < drawables.Length; i++) drawables[i].FadeIn(100 + i * 50); From 1e90e5e38e22e43c7bbcbb6ee5d11d82aac77216 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Sat, 25 May 2024 13:24:25 +0300 Subject: [PATCH 0917/2493] Use sb element path as a name --- osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs | 1 + osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs | 1 + 2 files changed, 2 insertions(+) diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs index fae9ec7f2e..f66f84af7a 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs @@ -83,6 +83,7 @@ namespace osu.Game.Storyboards.Drawables Origin = animation.Origin; Position = animation.InitialPosition; Loop = animation.LoopType == AnimationLoopType.LoopForever; + Name = animation.Path; LifetimeStart = animation.StartTime; LifetimeEnd = animation.EndTimeForDisplay; diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs index ec875219b6..c5d70ddecc 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs @@ -85,6 +85,7 @@ namespace osu.Game.Storyboards.Drawables Sprite = sprite; Origin = sprite.Origin; Position = sprite.InitialPosition; + Name = sprite.Path; LifetimeStart = sprite.StartTime; LifetimeEnd = sprite.EndTimeForDisplay; From 59553780040dbf82c48b7bd675f0c64ba97194bd Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 25 May 2024 16:11:24 +0300 Subject: [PATCH 0918/2493] Replace "X mods" text with a pill indicator --- .../SongSelect/TestSceneLeaderboardScoreV2.cs | 12 +-- .../Online/Leaderboards/LeaderboardScoreV2.cs | 86 +++++++++++++------ osu.Game/Rulesets/UI/ModSwitchTiny.cs | 6 +- 3 files changed, 65 insertions(+), 39 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneLeaderboardScoreV2.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneLeaderboardScoreV2.cs index 86c3c9a7ac..42407ad2b0 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneLeaderboardScoreV2.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneLeaderboardScoreV2.cs @@ -161,15 +161,9 @@ namespace osu.Game.Tests.Visual.SongSelect } }; - for (int i = 0; i < LeaderboardScoreV2.MAX_MODS - 1; i++) - scores[1].Mods = scores[1].Mods.Concat(new Mod[] { i % 2 == 0 ? new OsuModHidden() : new OsuModHalfTime() }).ToArray(); - - for (int i = 0; i < LeaderboardScoreV2.MAX_MODS; i++) - scores[2].Mods = scores[2].Mods.Concat(new Mod[] { i % 2 == 0 ? new OsuModHidden() : halfTime }).ToArray(); - - for (int i = 0; i < LeaderboardScoreV2.MAX_MODS + 1; i++) - scores[3].Mods = scores[3].Mods.Concat(new Mod[] { i % 2 == 0 ? new OsuModHidden() : new OsuModHalfTime() }).ToArray(); - + scores[1].Mods = new Mod[] { new OsuModHidden(), new OsuModDoubleTime(), new OsuModHardRock(), new OsuModFlashlight() }; + scores[2].Mods = new Mod[] { new OsuModHidden(), new OsuModDoubleTime(), new OsuModHardRock(), new OsuModFlashlight(), new OsuModClassic() }; + scores[3].Mods = new Mod[] { new OsuModHidden(), new OsuModDoubleTime(), new OsuModHardRock(), new OsuModFlashlight(), new OsuModClassic(), new OsuModDifficultyAdjust() }; scores[4].Mods = scores[4].BeatmapInfo!.Ruleset.CreateInstance().CreateAllMods().ToArray(); return scores; diff --git a/osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs b/osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs index 5db86bd7d0..d1ec44327a 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs @@ -17,6 +17,7 @@ using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; using osu.Framework.Layout; using osu.Framework.Localisation; +using osu.Framework.Utils; using osu.Game.Extensions; using osu.Game.Graphics; using osu.Game.Graphics.Backgrounds; @@ -93,8 +94,7 @@ namespace osu.Game.Online.Leaderboards protected Container RankContainer { get; private set; } = null!; private FillFlowContainer flagBadgeAndDateContainer = null!; - private FillFlowContainer modsContainer = null!; - private OsuSpriteText modsCounter = null!; + private FillFlowContainer modsContainer = null!; private OsuSpriteText scoreText = null!; private Drawable scoreRank = null!; @@ -178,17 +178,23 @@ namespace osu.Game.Online.Leaderboards innerAvatar.OnLoadComplete += d => d.FadeInFromZero(200); - if (score.Mods.Length > MAX_MODS) - modsCounter.Text = $"{score.Mods.Length} mods"; - else if (score.Mods.Length > 0) + if (score.Mods.Length > 0) { - modsContainer.ChildrenEnumerable = score.Mods.AsOrdered().Select(mod => new ColouredModSwitchTiny(mod) + modsContainer.Padding = new MarginPadding { Top = 4f }; + modsContainer.ChildrenEnumerable = score.Mods.AsOrdered().Take(Math.Min(MAX_MODS, score.Mods.Length)).Select(mod => new ColouredModSwitchTiny(mod) { Scale = new Vector2(0.375f) }); - } - modsContainer.Padding = new MarginPadding { Top = score.Mods.Length > 0 ? 4 : 0 }; + if (score.Mods.Length > MAX_MODS) + { + modsContainer.Remove(modsContainer[^1], true); + modsContainer.Add(new MoreModSwitchTiny(score.Mods.Length - MAX_MODS + 1) + { + Scale = new Vector2(0.375f), + }); + } + } } private Container createCentreContent(APIUser user) => new Container @@ -436,7 +442,7 @@ namespace osu.Game.Online.Leaderboards Current = scoreManager.GetBindableTotalScoreString(score), Font = OsuFont.GetFont(size: 30, weight: FontWeight.Light), }, - modsContainer = new FillFlowContainer + modsContainer = new FillFlowContainer { Anchor = Anchor.TopRight, Origin = Anchor.TopRight, @@ -445,13 +451,6 @@ namespace osu.Game.Online.Leaderboards Direction = FillDirection.Horizontal, Spacing = new Vector2(2f, 0f), }, - modsCounter = new OsuSpriteText - { - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - Shear = -shear, - Alpha = 0, - } } } } @@ -495,8 +494,7 @@ namespace osu.Game.Online.Leaderboards using (BeginDelayedSequence(50)) { - Drawable modsDrawable = score.Mods.Length > MAX_MODS ? modsCounter : modsContainer; - var drawables = new[] { flagBadgeAndDateContainer, modsDrawable }.Concat(statisticsLabels).ToArray(); + var drawables = new Drawable[] { flagBadgeAndDateContainer, modsContainer }.Concat(statisticsLabels).ToArray(); for (int i = 0; i < drawables.Length; i++) drawables[i].FadeIn(100 + i * 50); } @@ -652,20 +650,54 @@ namespace osu.Game.Online.Leaderboards { this.mod = mod; Active.Value = true; - Masking = true; - EdgeEffect = new EdgeEffectParameters - { - Roundness = 15, - Type = EdgeEffectType.Shadow, - Colour = Colour4.Black.Opacity(0.15f), - Radius = 3, - Offset = new Vector2(-2, 0) - }; } public LocalisableString TooltipText => (mod as Mod)?.IconTooltip ?? mod.Name; } + private sealed partial class MoreModSwitchTiny : CompositeDrawable + { + private readonly int count; + + public MoreModSwitchTiny(int count) + { + this.count = count; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + Size = new Vector2(ModSwitchTiny.WIDTH, ModSwitchTiny.DEFAULT_HEIGHT); + + InternalChild = new CircularContainer + { + Masking = true, + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = OsuColour.Gray(0.2f), + }, + new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Shadow = false, + Font = OsuFont.Numeric.With(size: 24, weight: FontWeight.Black), + Text = $"+{count}", + Colour = colours.Yellow, + Margin = new MarginPadding + { + Top = 4 + } + } + } + }; + } + } + #endregion public MenuItem[] ContextMenuItems diff --git a/osu.Game/Rulesets/UI/ModSwitchTiny.cs b/osu.Game/Rulesets/UI/ModSwitchTiny.cs index 4d50e702af..4a3bc9e31b 100644 --- a/osu.Game/Rulesets/UI/ModSwitchTiny.cs +++ b/osu.Game/Rulesets/UI/ModSwitchTiny.cs @@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.UI public BindableBool Active { get; } = new BindableBool(); public const float DEFAULT_HEIGHT = 30; - private const float width = 73; + public const float WIDTH = 73; protected readonly IMod Mod; private readonly bool showExtendedInformation; @@ -56,7 +56,7 @@ namespace osu.Game.Rulesets.UI Width = 100 + DEFAULT_HEIGHT / 2, RelativeSizeAxes = Axes.Y, Masking = true, - X = width, + X = WIDTH, Margin = new MarginPadding { Left = -DEFAULT_HEIGHT }, Children = new Drawable[] { @@ -77,7 +77,7 @@ namespace osu.Game.Rulesets.UI }, new CircularContainer { - Width = width, + Width = WIDTH, RelativeSizeAxes = Axes.Y, Masking = true, Children = new Drawable[] From d395c854187611eb16baa417fc76166ad99fbef9 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 25 May 2024 17:09:02 +0300 Subject: [PATCH 0919/2493] Adjust right content width based on scoring mode --- .../SongSelect/TestSceneLeaderboardScoreV2.cs | 37 +-- .../Online/Leaderboards/LeaderboardScoreV2.cs | 250 ++++++++++-------- 2 files changed, 164 insertions(+), 123 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneLeaderboardScoreV2.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneLeaderboardScoreV2.cs index 42407ad2b0..99554492fc 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneLeaderboardScoreV2.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneLeaderboardScoreV2.cs @@ -7,6 +7,8 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Utils; +using osu.Game.Configuration; using osu.Game.Graphics.Sprites; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Leaderboards; @@ -15,6 +17,7 @@ using osu.Game.Rulesets.Mania; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Mods; +using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; using osu.Game.Tests.Resources; using osu.Game.Users; @@ -27,6 +30,9 @@ namespace osu.Game.Tests.Visual.SongSelect [Cached] private OverlayColourProvider colourProvider { get; set; } = new OverlayColourProvider(OverlayColourScheme.Aquamarine); + [Resolved] + private OsuConfigManager config { get; set; } = null!; + private FillFlowContainer? fillFlow; private OsuSpriteText? drawWidthText; private float relativeWidth; @@ -41,6 +47,7 @@ namespace osu.Game.Tests.Visual.SongSelect relativeWidth = v; if (fillFlow != null) fillFlow.Width = v; }); + AddToggleStep("toggle scoring mode", v => config.SetValue(OsuSetting.ScoreDisplayMode, v ? ScoringMode.Classic : ScoringMode.Standardised)); } [SetUp] @@ -91,7 +98,8 @@ namespace osu.Game.Tests.Visual.SongSelect Rank = ScoreRank.X, Accuracy = 1, MaxCombo = 244, - TotalScore = 1707827, + TotalScore = RNG.Next(1_800_000, 2_000_000), + MaximumStatistics = { { HitResult.Great, 3000 } }, Ruleset = new OsuRuleset().RulesetInfo, User = new APIUser { @@ -108,7 +116,8 @@ namespace osu.Game.Tests.Visual.SongSelect Rank = ScoreRank.S, Accuracy = 0.1f, MaxCombo = 32040, - TotalScore = 1707827, + TotalScore = RNG.Next(1_200_000, 1_500_000), + MaximumStatistics = { { HitResult.Great, 3000 } }, Ruleset = new OsuRuleset().RulesetInfo, User = new APIUser { @@ -119,13 +128,15 @@ namespace osu.Game.Tests.Visual.SongSelect }, Date = DateTimeOffset.Now.AddMonths(-6), }, + TestResources.CreateTestScoreInfo(), new ScoreInfo { Position = 110000, - Rank = ScoreRank.A, + Rank = ScoreRank.B, Accuracy = 1, MaxCombo = 244, - TotalScore = 17078279, + TotalScore = RNG.Next(1_000_000, 1_200_000), + MaximumStatistics = { { HitResult.Great, 3000 } }, Ruleset = new ManiaRuleset().RulesetInfo, User = new APIUser { @@ -137,10 +148,11 @@ namespace osu.Game.Tests.Visual.SongSelect new ScoreInfo { Position = 110000, - Rank = ScoreRank.A, + Rank = ScoreRank.D, Accuracy = 1, MaxCombo = 244, - TotalScore = 1234567890, + TotalScore = RNG.Next(500_000, 1_000_000), + MaximumStatistics = { { HitResult.Great, 3000 } }, Ruleset = new ManiaRuleset().RulesetInfo, User = new APIUser { @@ -150,21 +162,16 @@ namespace osu.Game.Tests.Visual.SongSelect }, Date = DateTimeOffset.Now, }, - TestResources.CreateTestScoreInfo(), }; - var halfTime = new OsuModHalfTime - { - SpeedChange = - { - Value = 0.99 - } - }; + scores[2].Rank = ScoreRank.A; + scores[2].TotalScore = RNG.Next(120_000, 400_000); + scores[2].MaximumStatistics[HitResult.Great] = 3000; scores[1].Mods = new Mod[] { new OsuModHidden(), new OsuModDoubleTime(), new OsuModHardRock(), new OsuModFlashlight() }; scores[2].Mods = new Mod[] { new OsuModHidden(), new OsuModDoubleTime(), new OsuModHardRock(), new OsuModFlashlight(), new OsuModClassic() }; scores[3].Mods = new Mod[] { new OsuModHidden(), new OsuModDoubleTime(), new OsuModHardRock(), new OsuModFlashlight(), new OsuModClassic(), new OsuModDifficultyAdjust() }; - scores[4].Mods = scores[4].BeatmapInfo!.Ruleset.CreateInstance().CreateAllMods().ToArray(); + scores[4].Mods = new ManiaRuleset().CreateAllMods().ToArray(); return scores; } diff --git a/osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs b/osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs index d1ec44327a..a9eb9ae2d1 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs @@ -5,19 +5,19 @@ 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.Extensions.LocalisationExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; -using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; using osu.Framework.Layout; using osu.Framework.Localisation; -using osu.Framework.Utils; +using osu.Game.Configuration; using osu.Game.Extensions; using osu.Game.Graphics; using osu.Game.Graphics.Backgrounds; @@ -28,6 +28,7 @@ using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays; using osu.Game.Resources.Localisation.Web; using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI; using osu.Game.Scoring; using osu.Game.Screens.Select; @@ -41,18 +42,13 @@ namespace osu.Game.Online.Leaderboards { public partial class LeaderboardScoreV2 : OsuClickableContainer, IHasContextMenu, IHasCustomTooltip { - /// - /// The maximum number of mods when contracted until the mods display width exceeds the . - /// - public const int MAX_MODS = 5; - - private const float right_content_width = 210; + private const float expanded_right_content_width = 210; private const float grade_width = 40; private const float username_min_width = 125; private const float statistics_regular_min_width = 175; private const float statistics_compact_min_width = 100; private const float rank_label_width = 65; - private const float rank_label_visibility_width_cutoff = rank_label_width + height + username_min_width + statistics_regular_min_width + right_content_width; + private const float rank_label_visibility_width_cutoff = rank_label_width + height + username_min_width + statistics_regular_min_width + expanded_right_content_width; private readonly ScoreInfo score; @@ -92,6 +88,8 @@ namespace osu.Game.Online.Leaderboards private OsuSpriteText nameLabel = null!; private List statisticsLabels = null!; + private Container rightContent = null!; + protected Container RankContainer { get; private set; } = null!; private FillFlowContainer flagBadgeAndDateContainer = null!; private FillFlowContainer modsContainer = null!; @@ -152,7 +150,7 @@ namespace osu.Game.Online.Leaderboards { new Dimension(GridSizeMode.AutoSize), new Dimension(), - new Dimension(GridSizeMode.Absolute, right_content_width), + new Dimension(GridSizeMode.AutoSize), }, Content = new[] { @@ -177,19 +175,51 @@ namespace osu.Game.Online.Leaderboards }; innerAvatar.OnLoadComplete += d => d.FadeInFromZero(200); + } + + [Resolved] + private OsuConfigManager config { get; set; } = null!; + + private IBindable scoringMode { get; set; } = null!; + + protected override void LoadComplete() + { + base.LoadComplete(); + + scoringMode = config.GetBindable(OsuSetting.ScoreDisplayMode); + scoringMode.BindValueChanged(s => + { + switch (s.NewValue) + { + case ScoringMode.Standardised: + rightContent.Width = 180f; + break; + + case ScoringMode.Classic: + rightContent.Width = expanded_right_content_width; + break; + } + + updateModDisplay(); + }, true); + } + + private void updateModDisplay() + { + int maxMods = scoringMode.Value == ScoringMode.Standardised ? 4 : 5; if (score.Mods.Length > 0) { modsContainer.Padding = new MarginPadding { Top = 4f }; - modsContainer.ChildrenEnumerable = score.Mods.AsOrdered().Take(Math.Min(MAX_MODS, score.Mods.Length)).Select(mod => new ColouredModSwitchTiny(mod) + modsContainer.ChildrenEnumerable = score.Mods.AsOrdered().Take(Math.Min(maxMods, score.Mods.Length)).Select(mod => new ColouredModSwitchTiny(mod) { Scale = new Vector2(0.375f) }); - if (score.Mods.Length > MAX_MODS) + if (score.Mods.Length > maxMods) { modsContainer.Remove(modsContainer[^1], true); - modsContainer.Add(new MoreModSwitchTiny(score.Mods.Length - MAX_MODS + 1) + modsContainer.Add(new MoreModSwitchTiny(score.Mods.Length - maxMods + 1) { Scale = new Vector2(0.375f), }); @@ -342,121 +372,125 @@ namespace osu.Game.Online.Leaderboards }, }; - private Container createRightContent() => new Container + private Container createRightContent() => rightContent = new Container { Name = @"Right content", - AutoSizeAxes = Axes.X, RelativeSizeAxes = Axes.Y, - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - Children = new Drawable[] + Child = new Container { - new Container + AutoSizeAxes = Axes.X, + RelativeSizeAxes = Axes.Y, + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + Children = new Drawable[] { - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding { Right = grade_width }, - Child = new Box + new Container { RelativeSizeAxes = Axes.Both, - Colour = ColourInfo.GradientHorizontal(backgroundColour.Opacity(0), OsuColour.ForRank(score.Rank)), + Padding = new MarginPadding { Right = grade_width }, + Child = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = ColourInfo.GradientHorizontal(backgroundColour.Opacity(0), OsuColour.ForRank(score.Rank)), + }, }, - }, - new Box - { - RelativeSizeAxes = Axes.Y, - Width = grade_width, - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - Colour = OsuColour.ForRank(score.Rank), - }, - new TrianglesV2 - { - RelativeSizeAxes = Axes.Both, - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - SpawnRatio = 2, - Velocity = 0.7f, - Colour = ColourInfo.GradientHorizontal(backgroundColour.Opacity(0), OsuColour.ForRank(score.Rank).Darken(0.2f)), - }, - RankContainer = new Container - { - Shear = -shear, - Anchor = Anchor.CentreRight, - Origin = Anchor.CentreRight, - RelativeSizeAxes = Axes.Y, - Width = grade_width, - Child = scoreRank = new OsuSpriteText + new Box { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Spacing = new Vector2(-2), - Colour = DrawableRank.GetRankNameColour(score.Rank), - Font = OsuFont.Numeric.With(size: 16), - Text = DrawableRank.GetRankName(score.Rank), - ShadowColour = Color4.Black.Opacity(0.3f), - ShadowOffset = new Vector2(0, 0.08f), - Shadow = true, - UseFullGlyphHeight = false, + RelativeSizeAxes = Axes.Y, + Width = grade_width, + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + Colour = OsuColour.ForRank(score.Rank), }, - }, - new Container - { - AutoSizeAxes = Axes.X, - RelativeSizeAxes = Axes.Y, - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Padding = new MarginPadding { Right = grade_width }, - Child = new Container + new TrianglesV2 + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + SpawnRatio = 2, + Velocity = 0.7f, + Colour = ColourInfo.GradientHorizontal(backgroundColour.Opacity(0), OsuColour.ForRank(score.Rank).Darken(0.2f)), + }, + RankContainer = new Container + { + Shear = -shear, + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + RelativeSizeAxes = Axes.Y, + Width = grade_width, + Child = scoreRank = new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Spacing = new Vector2(-2), + Colour = DrawableRank.GetRankNameColour(score.Rank), + Font = OsuFont.Numeric.With(size: 16), + Text = DrawableRank.GetRankName(score.Rank), + ShadowColour = Color4.Black.Opacity(0.3f), + ShadowOffset = new Vector2(0, 0.08f), + Shadow = true, + UseFullGlyphHeight = false, + }, + }, + new Container { AutoSizeAxes = Axes.X, RelativeSizeAxes = Axes.Y, - Masking = true, - CornerRadius = corner_radius, - Children = new Drawable[] + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Padding = new MarginPadding { Right = grade_width }, + Child = new Container { - totalScoreBackground = new Box + AutoSizeAxes = Axes.X, + RelativeSizeAxes = Axes.Y, + Masking = true, + CornerRadius = corner_radius, + Children = new Drawable[] { - RelativeSizeAxes = Axes.Both, - Colour = totalScoreBackgroundGradient, - }, - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = ColourInfo.GradientHorizontal(backgroundColour.Opacity(0), OsuColour.ForRank(score.Rank).Opacity(0.5f)), - }, - new FillFlowContainer - { - AutoSizeAxes = Axes.Both, - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Direction = FillDirection.Vertical, - Padding = new MarginPadding { Horizontal = corner_radius }, - Children = new Drawable[] + totalScoreBackground = new Box { - scoreText = new OsuSpriteText + RelativeSizeAxes = Axes.Both, + Colour = totalScoreBackgroundGradient, + }, + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = ColourInfo.GradientHorizontal(backgroundColour.Opacity(0), OsuColour.ForRank(score.Rank).Opacity(0.5f)), + }, + new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Direction = FillDirection.Vertical, + Padding = new MarginPadding { Horizontal = corner_radius }, + Children = new Drawable[] { - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - UseFullGlyphHeight = false, - Shear = -shear, - Current = scoreManager.GetBindableTotalScoreString(score), - Font = OsuFont.GetFont(size: 30, weight: FontWeight.Light), - }, - modsContainer = new FillFlowContainer - { - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - Shear = -shear, - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(2f, 0f), - }, + scoreText = new OsuSpriteText + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + UseFullGlyphHeight = false, + Shear = -shear, + Current = scoreManager.GetBindableTotalScoreString(score), + Font = OsuFont.GetFont(size: 30, weight: FontWeight.Light), + }, + modsContainer = new FillFlowContainer + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + Shear = -shear, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(2f, 0f), + }, + } } } } } } - } + }, }; protected (CaseTransformableString, LocalisableString DisplayAccuracy)[] GetStatistics(ScoreInfo model) => new[] @@ -542,13 +576,13 @@ namespace osu.Game.Online.Leaderboards else rankLabel.FadeOut(transition_duration, Easing.OutQuint).MoveToX(-rankLabel.DrawWidth, transition_duration, Easing.OutQuint); - if (DrawWidth >= height + username_min_width + statistics_regular_min_width + right_content_width) + if (DrawWidth >= height + username_min_width + statistics_regular_min_width + expanded_right_content_width) { statisticsContainer.FadeIn(transition_duration, Easing.OutQuint).MoveToX(0, transition_duration, Easing.OutQuint); statisticsContainer.Direction = FillDirection.Horizontal; statisticsContainer.ScaleTo(1, transition_duration, Easing.OutQuint); } - else if (DrawWidth >= height + username_min_width + statistics_compact_min_width + right_content_width) + else if (DrawWidth >= height + username_min_width + statistics_compact_min_width + expanded_right_content_width) { statisticsContainer.FadeIn(transition_duration, Easing.OutQuint).MoveToX(0, transition_duration, Easing.OutQuint); statisticsContainer.Direction = FillDirection.Vertical; From 2c18c10ac888f6a590b569e6b4aac2b451d902c3 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 25 May 2024 17:18:51 +0300 Subject: [PATCH 0920/2493] Move to `SelectV2` namespace --- .../Visual/SongSelect/TestSceneLeaderboardScoreV2.cs | 2 +- .../SelectV2}/Leaderboards/LeaderboardScoreV2.cs | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) rename osu.Game/{Online => Screens/SelectV2}/Leaderboards/LeaderboardScoreV2.cs (99%) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneLeaderboardScoreV2.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneLeaderboardScoreV2.cs index 99554492fc..ce0eeca6f4 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneLeaderboardScoreV2.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneLeaderboardScoreV2.cs @@ -11,7 +11,6 @@ using osu.Framework.Utils; using osu.Game.Configuration; using osu.Game.Graphics.Sprites; using osu.Game.Online.API.Requests.Responses; -using osu.Game.Online.Leaderboards; using osu.Game.Overlays; using osu.Game.Rulesets.Mania; using osu.Game.Rulesets.Mods; @@ -19,6 +18,7 @@ using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; +using osu.Game.Screens.SelectV2.Leaderboards; using osu.Game.Tests.Resources; using osu.Game.Users; using osuTK; diff --git a/osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs b/osu.Game/Screens/SelectV2/Leaderboards/LeaderboardScoreV2.cs similarity index 99% rename from osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs rename to osu.Game/Screens/SelectV2/Leaderboards/LeaderboardScoreV2.cs index a9eb9ae2d1..47b5a692bf 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardScoreV2.cs +++ b/osu.Game/Screens/SelectV2/Leaderboards/LeaderboardScoreV2.cs @@ -25,6 +25,7 @@ using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Online.API.Requests.Responses; +using osu.Game.Online.Leaderboards; using osu.Game.Overlays; using osu.Game.Resources.Localisation.Web; using osu.Game.Rulesets.Mods; @@ -38,7 +39,7 @@ using osu.Game.Utils; using osuTK; using osuTK.Graphics; -namespace osu.Game.Online.Leaderboards +namespace osu.Game.Screens.SelectV2.Leaderboards { public partial class LeaderboardScoreV2 : OsuClickableContainer, IHasContextMenu, IHasCustomTooltip { From 91fb5ed74975454f01ced0afaee80e5fe6cf9741 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 25 May 2024 17:28:03 +0300 Subject: [PATCH 0921/2493] Move toggle step to `SetUpSteps` --- .../Visual/SongSelect/TestSceneLeaderboardScoreV2.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneLeaderboardScoreV2.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneLeaderboardScoreV2.cs index ce0eeca6f4..d3d388dba3 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneLeaderboardScoreV2.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneLeaderboardScoreV2.cs @@ -7,6 +7,7 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Testing; using osu.Framework.Utils; using osu.Game.Configuration; using osu.Game.Graphics.Sprites; @@ -47,7 +48,6 @@ namespace osu.Game.Tests.Visual.SongSelect relativeWidth = v; if (fillFlow != null) fillFlow.Width = v; }); - AddToggleStep("toggle scoring mode", v => config.SetValue(OsuSetting.ScoreDisplayMode, v ? ScoringMode.Classic : ScoringMode.Standardised)); } [SetUp] @@ -81,6 +81,12 @@ namespace osu.Game.Tests.Visual.SongSelect score.Show(); }); + [SetUpSteps] + public void SetUpSteps() + { + AddToggleStep("toggle scoring mode", v => config.SetValue(OsuSetting.ScoreDisplayMode, v ? ScoringMode.Classic : ScoringMode.Standardised)); + } + protected override void UpdateAfterChildren() { base.UpdateAfterChildren(); From 6aa92bcc4559ec52a85e8e026e579c03f1d7aca0 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Sat, 25 May 2024 18:31:19 +0200 Subject: [PATCH 0922/2493] Add simple scale tool --- .../Edit/OsuHitObjectComposer.cs | 6 +- .../Edit/PreciseScalePopover.cs | 121 ++++++++++++++++++ .../Edit/TransformToolboxGroup.cs | 33 ++++- .../Input/Bindings/GlobalActionContainer.cs | 4 + .../GlobalActionKeyBindingStrings.cs | 5 + 5 files changed, 167 insertions(+), 2 deletions(-) create mode 100644 osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs index 3ead61f64a..6f3ed9730e 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs @@ -101,7 +101,11 @@ namespace osu.Game.Rulesets.Osu.Edit RightToolbox.AddRange(new EditorToolboxGroup[] { - new TransformToolboxGroup { RotationHandler = BlueprintContainer.SelectionHandler.RotationHandler, }, + new TransformToolboxGroup + { + RotationHandler = BlueprintContainer.SelectionHandler.RotationHandler, + ScaleHandler = BlueprintContainer.SelectionHandler.ScaleHandler, + }, FreehandlSliderToolboxGroup } ); diff --git a/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs b/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs new file mode 100644 index 0000000000..62408e223d --- /dev/null +++ b/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs @@ -0,0 +1,121 @@ +// 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.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osu.Game.Graphics.UserInterfaceV2; +using osu.Game.Rulesets.Osu.UI; +using osu.Game.Screens.Edit.Components.RadioButtons; +using osu.Game.Screens.Edit.Compose.Components; +using osuTK; + +namespace osu.Game.Rulesets.Osu.Edit +{ + public partial class PreciseScalePopover : OsuPopover + { + private readonly SelectionScaleHandler scaleHandler; + + private readonly Bindable scaleInfo = new Bindable(new PreciseScaleInfo(1, ScaleOrigin.PlayfieldCentre, true, true)); + + private SliderWithTextBoxInput scaleInput = null!; + private EditorRadioButtonCollection scaleOrigin = null!; + + private RadioButton selectionCentreButton = null!; + + public PreciseScalePopover(SelectionScaleHandler scaleHandler) + { + this.scaleHandler = scaleHandler; + + AllowableAnchors = new[] { Anchor.CentreLeft, Anchor.CentreRight }; + } + + [BackgroundDependencyLoader] + private void load() + { + Child = new FillFlowContainer + { + Width = 220, + AutoSizeAxes = Axes.Y, + Spacing = new Vector2(20), + Children = new Drawable[] + { + scaleInput = new SliderWithTextBoxInput("Scale:") + { + Current = new BindableNumber + { + MinValue = 0.5f, + MaxValue = 2, + Precision = 0.001f, + Value = 1, + Default = 1, + }, + Instantaneous = true + }, + scaleOrigin = new EditorRadioButtonCollection + { + RelativeSizeAxes = Axes.X, + Items = new[] + { + new RadioButton("Playfield centre", + () => scaleInfo.Value = scaleInfo.Value with { Origin = ScaleOrigin.PlayfieldCentre }, + () => new SpriteIcon { Icon = FontAwesome.Regular.Square }), + selectionCentreButton = new RadioButton("Selection centre", + () => scaleInfo.Value = scaleInfo.Value with { Origin = ScaleOrigin.SelectionCentre }, + () => new SpriteIcon { Icon = FontAwesome.Solid.VectorSquare }) + } + } + } + }; + selectionCentreButton.Selected.DisabledChanged += isDisabled => + { + selectionCentreButton.TooltipText = isDisabled ? "Select more than one object to perform selection-based scaling." : string.Empty; + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + ScheduleAfterChildren(() => scaleInput.TakeFocus()); + scaleInput.Current.BindValueChanged(scale => scaleInfo.Value = scaleInfo.Value with { Scale = scale.NewValue }); + scaleOrigin.Items.First().Select(); + + scaleHandler.CanScaleX.BindValueChanged(e => + { + selectionCentreButton.Selected.Disabled = !e.NewValue; + }, true); + + scaleInfo.BindValueChanged(scale => + { + var newScale = new Vector2(scale.NewValue.XAxis ? scale.NewValue.Scale : 1, scale.NewValue.YAxis ? scale.NewValue.Scale : 1); + scaleHandler.Update(newScale, scale.NewValue.Origin == ScaleOrigin.PlayfieldCentre ? OsuPlayfield.BASE_SIZE / 2 : null); + }); + } + + protected override void PopIn() + { + base.PopIn(); + scaleHandler.Begin(); + } + + protected override void PopOut() + { + base.PopOut(); + + if (IsLoaded) + scaleHandler.Commit(); + } + } + + public enum ScaleOrigin + { + PlayfieldCentre, + SelectionCentre + } + + public record PreciseScaleInfo(float Scale, ScaleOrigin Origin, bool XAxis, bool YAxis); +} diff --git a/osu.Game.Rulesets.Osu/Edit/TransformToolboxGroup.cs b/osu.Game.Rulesets.Osu/Edit/TransformToolboxGroup.cs index 9499bacade..146d771e19 100644 --- a/osu.Game.Rulesets.Osu/Edit/TransformToolboxGroup.cs +++ b/osu.Game.Rulesets.Osu/Edit/TransformToolboxGroup.cs @@ -19,13 +19,20 @@ namespace osu.Game.Rulesets.Osu.Edit public partial class TransformToolboxGroup : EditorToolboxGroup, IKeyBindingHandler { private readonly Bindable canRotate = new BindableBool(); + private readonly Bindable canScale = new BindableBool(); private EditorToolButton rotateButton = null!; + private EditorToolButton scaleButton = null!; private Bindable canRotatePlayfieldOrigin = null!; private Bindable canRotateSelectionOrigin = null!; + private Bindable canScaleX = null!; + private Bindable canScaleY = null!; + private Bindable canScaleDiagonally = null!; + public SelectionRotationHandler RotationHandler { get; init; } = null!; + public SelectionScaleHandler ScaleHandler { get; init; } = null!; public TransformToolboxGroup() : base("transform") @@ -45,7 +52,9 @@ namespace osu.Game.Rulesets.Osu.Edit rotateButton = new EditorToolButton("Rotate", () => new SpriteIcon { Icon = FontAwesome.Solid.Undo }, () => new PreciseRotationPopover(RotationHandler)), - // TODO: scale + scaleButton = new EditorToolButton("Scale", + () => new SpriteIcon { Icon = FontAwesome.Solid.ArrowsAlt }, + () => new PreciseScalePopover(ScaleHandler)) } }; } @@ -66,9 +75,25 @@ namespace osu.Game.Rulesets.Osu.Edit canRotate.Value = RotationHandler.CanRotatePlayfieldOrigin.Value || RotationHandler.CanRotateSelectionOrigin.Value; } + // aggregate three values into canScale + canScaleX = ScaleHandler.CanScaleX.GetBoundCopy(); + canScaleX.BindValueChanged(_ => updateCanScaleAggregate()); + + canScaleY = ScaleHandler.CanScaleY.GetBoundCopy(); + canScaleY.BindValueChanged(_ => updateCanScaleAggregate()); + + canScaleDiagonally = ScaleHandler.CanScaleDiagonally.GetBoundCopy(); + canScaleDiagonally.BindValueChanged(_ => updateCanScaleAggregate()); + + void updateCanScaleAggregate() + { + canScale.Value = ScaleHandler.CanScaleX.Value || ScaleHandler.CanScaleY.Value || ScaleHandler.CanScaleDiagonally.Value; + } + // bindings to `Enabled` on the buttons are decoupled on purpose // due to the weird `OsuButton` behaviour of resetting `Enabled` to `false` when `Action` is set. canRotate.BindValueChanged(_ => rotateButton.Enabled.Value = canRotate.Value, true); + canScale.BindValueChanged(_ => scaleButton.Enabled.Value = canScale.Value, true); } public bool OnPressed(KeyBindingPressEvent e) @@ -82,6 +107,12 @@ namespace osu.Game.Rulesets.Osu.Edit rotateButton.TriggerClick(); return true; } + + case GlobalAction.EditorToggleScaleControl: + { + scaleButton.TriggerClick(); + return true; + } } return false; diff --git a/osu.Game/Input/Bindings/GlobalActionContainer.cs b/osu.Game/Input/Bindings/GlobalActionContainer.cs index 09db7461d6..394cb98089 100644 --- a/osu.Game/Input/Bindings/GlobalActionContainer.cs +++ b/osu.Game/Input/Bindings/GlobalActionContainer.cs @@ -142,6 +142,7 @@ namespace osu.Game.Input.Bindings new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.MouseWheelRight }, GlobalAction.EditorCyclePreviousBeatSnapDivisor), new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.MouseWheelLeft }, GlobalAction.EditorCycleNextBeatSnapDivisor), new KeyBinding(new[] { InputKey.Control, InputKey.R }, GlobalAction.EditorToggleRotateControl), + new KeyBinding(new[] { InputKey.S }, GlobalAction.EditorToggleScaleControl), }; private static IEnumerable inGameKeyBindings => new[] @@ -411,6 +412,9 @@ namespace osu.Game.Input.Bindings [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorToggleRotateControl))] EditorToggleRotateControl, + [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorToggleScaleControl))] + EditorToggleScaleControl, + [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.IncreaseOffset))] IncreaseOffset, diff --git a/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs b/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs index 18a1d3e4fe..2e44b96625 100644 --- a/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs +++ b/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs @@ -369,6 +369,11 @@ namespace osu.Game.Localisation /// public static LocalisableString EditorToggleRotateControl => new TranslatableString(getKey(@"editor_toggle_rotate_control"), @"Toggle rotate control"); + /// + /// "Toggle scale control" + /// + public static LocalisableString EditorToggleScaleControl => new TranslatableString(getKey(@"editor_toggle_scale_control"), @"Toggle scale control"); + /// /// "Increase mod speed" /// From 88314dc584f186cf8a99c631d16047f321135292 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Sat, 25 May 2024 18:41:31 +0200 Subject: [PATCH 0923/2493] select all input text on popup for an easy typing experience --- osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs | 6 +++++- osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs | 6 +++++- osu.Game/Graphics/UserInterfaceV2/LabelledTextBox.cs | 2 ++ osu.Game/Graphics/UserInterfaceV2/SliderWithTextBoxInput.cs | 2 ++ 4 files changed, 14 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs b/osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs index 88c3d7414b..812d622ae5 100644 --- a/osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs +++ b/osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs @@ -78,7 +78,11 @@ namespace osu.Game.Rulesets.Osu.Edit { base.LoadComplete(); - ScheduleAfterChildren(() => angleInput.TakeFocus()); + ScheduleAfterChildren(() => + { + angleInput.TakeFocus(); + angleInput.SelectAll(); + }); angleInput.Current.BindValueChanged(angle => rotationInfo.Value = rotationInfo.Value with { Degrees = angle.NewValue }); rotationOrigin.Items.First().Select(); diff --git a/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs b/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs index 62408e223d..ed52da56d3 100644 --- a/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs +++ b/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs @@ -80,7 +80,11 @@ namespace osu.Game.Rulesets.Osu.Edit { base.LoadComplete(); - ScheduleAfterChildren(() => scaleInput.TakeFocus()); + ScheduleAfterChildren(() => + { + scaleInput.TakeFocus(); + scaleInput.SelectAll(); + }); scaleInput.Current.BindValueChanged(scale => scaleInfo.Value = scaleInfo.Value with { Scale = scale.NewValue }); scaleOrigin.Items.First().Select(); diff --git a/osu.Game/Graphics/UserInterfaceV2/LabelledTextBox.cs b/osu.Game/Graphics/UserInterfaceV2/LabelledTextBox.cs index 863ad5a173..8dfe729ce7 100644 --- a/osu.Game/Graphics/UserInterfaceV2/LabelledTextBox.cs +++ b/osu.Game/Graphics/UserInterfaceV2/LabelledTextBox.cs @@ -50,6 +50,8 @@ namespace osu.Game.Graphics.UserInterfaceV2 Component.BorderColour = colours.Blue; } + public bool SelectAll() => Component.SelectAll(); + protected virtual OsuTextBox CreateTextBox() => new OsuTextBox(); public override bool AcceptsFocus => true; diff --git a/osu.Game/Graphics/UserInterfaceV2/SliderWithTextBoxInput.cs b/osu.Game/Graphics/UserInterfaceV2/SliderWithTextBoxInput.cs index 4c16cb4951..f1f4fe3b46 100644 --- a/osu.Game/Graphics/UserInterfaceV2/SliderWithTextBoxInput.cs +++ b/osu.Game/Graphics/UserInterfaceV2/SliderWithTextBoxInput.cs @@ -87,6 +87,8 @@ namespace osu.Game.Graphics.UserInterfaceV2 public bool TakeFocus() => GetContainingFocusManager().ChangeFocus(textBox); + public bool SelectAll() => textBox.SelectAll(); + private bool updatingFromTextBox; private void textChanged(ValueChangedEvent change) From 4eeebdf60cd70eedcf6692cbad36cc8df6abc8de Mon Sep 17 00:00:00 2001 From: OliBomby Date: Sat, 25 May 2024 20:17:27 +0200 Subject: [PATCH 0924/2493] calculate max scale bounds for scale slider --- .../Edit/OsuSelectionScaleHandler.cs | 47 ++++++++++--------- .../Edit/PreciseScalePopover.cs | 35 ++++++++++++-- .../Components/SelectionScaleHandler.cs | 8 ++++ 3 files changed, 64 insertions(+), 26 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs index af03c4d925..331e8de3f1 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs @@ -67,7 +67,7 @@ namespace osu.Game.Rulesets.Osu.Edit objectsInScale = selectedMovableObjects.ToDictionary(ho => ho, ho => new OriginalHitObjectState(ho)); OriginalSurroundingQuad = objectsInScale.Count == 1 && objectsInScale.First().Key is Slider slider - ? GeometryUtils.GetSurroundingQuad(slider.Path.ControlPoints.Select(p => p.Position)) + ? GeometryUtils.GetSurroundingQuad(slider.Path.ControlPoints.Select(p => slider.Position + p.Position)) : GeometryUtils.GetSurroundingQuad(objectsInScale.Keys); defaultOrigin = OriginalSurroundingQuad.Value.Centre; } @@ -92,7 +92,7 @@ namespace osu.Game.Rulesets.Osu.Edit } else { - scale = getClampedScale(OriginalSurroundingQuad.Value, actualOrigin, scale); + scale = GetClampedScale(scale, actualOrigin); foreach (var (ho, originalState) in objectsInScale) { @@ -155,30 +155,33 @@ namespace osu.Game.Rulesets.Osu.Edit return (xInBounds, yInBounds); } - /// - /// Clamp scale for multi-object-scaling where selection does not exceed playfield bounds or flip. - /// - /// The quad surrounding the hitobjects - /// The origin from which the scale operation is performed - /// The scale to be clamped - /// The clamped scale vector - private Vector2 getClampedScale(Quad selectionQuad, Vector2 origin, Vector2 scale) + public override Vector2 GetClampedScale(Vector2 scale, Vector2? origin = null) { //todo: this is not always correct for selections involving sliders. This approximation assumes each point is scaled independently, but sliderends move with the sliderhead. + if (objectsInScale == null) + return scale; - var tl1 = Vector2.Divide(-origin, selectionQuad.TopLeft - origin); - var tl2 = Vector2.Divide(OsuPlayfield.BASE_SIZE - origin, selectionQuad.TopLeft - origin); - var br1 = Vector2.Divide(-origin, selectionQuad.BottomRight - origin); - var br2 = Vector2.Divide(OsuPlayfield.BASE_SIZE - origin, selectionQuad.BottomRight - origin); + Debug.Assert(defaultOrigin != null && OriginalSurroundingQuad != null); - if (!Precision.AlmostEquals(selectionQuad.TopLeft.X - origin.X, 0)) - scale.X = selectionQuad.TopLeft.X - origin.X < 0 ? MathHelper.Clamp(scale.X, tl2.X, tl1.X) : MathHelper.Clamp(scale.X, tl1.X, tl2.X); - if (!Precision.AlmostEquals(selectionQuad.TopLeft.Y - origin.Y, 0)) - scale.Y = selectionQuad.TopLeft.Y - origin.Y < 0 ? MathHelper.Clamp(scale.Y, tl2.Y, tl1.Y) : MathHelper.Clamp(scale.Y, tl1.Y, tl2.Y); - if (!Precision.AlmostEquals(selectionQuad.BottomRight.X - origin.X, 0)) - scale.X = selectionQuad.BottomRight.X - origin.X < 0 ? MathHelper.Clamp(scale.X, br2.X, br1.X) : MathHelper.Clamp(scale.X, br1.X, br2.X); - if (!Precision.AlmostEquals(selectionQuad.BottomRight.Y - origin.Y, 0)) - scale.Y = selectionQuad.BottomRight.Y - origin.Y < 0 ? MathHelper.Clamp(scale.Y, br2.Y, br1.Y) : MathHelper.Clamp(scale.Y, br1.Y, br2.Y); + if (objectsInScale.Count == 1 && objectsInScale.First().Key is Slider slider) + origin = slider.Position; + + Vector2 actualOrigin = origin ?? defaultOrigin.Value; + var selectionQuad = OriginalSurroundingQuad.Value; + + var tl1 = Vector2.Divide(-actualOrigin, selectionQuad.TopLeft - actualOrigin); + var tl2 = Vector2.Divide(OsuPlayfield.BASE_SIZE - actualOrigin, selectionQuad.TopLeft - actualOrigin); + var br1 = Vector2.Divide(-actualOrigin, selectionQuad.BottomRight - actualOrigin); + var br2 = Vector2.Divide(OsuPlayfield.BASE_SIZE - actualOrigin, selectionQuad.BottomRight - actualOrigin); + + if (!Precision.AlmostEquals(selectionQuad.TopLeft.X - actualOrigin.X, 0)) + scale.X = selectionQuad.TopLeft.X - actualOrigin.X < 0 ? MathHelper.Clamp(scale.X, tl2.X, tl1.X) : MathHelper.Clamp(scale.X, tl1.X, tl2.X); + if (!Precision.AlmostEquals(selectionQuad.TopLeft.Y - actualOrigin.Y, 0)) + scale.Y = selectionQuad.TopLeft.Y - actualOrigin.Y < 0 ? MathHelper.Clamp(scale.Y, tl2.Y, tl1.Y) : MathHelper.Clamp(scale.Y, tl1.Y, tl2.Y); + if (!Precision.AlmostEquals(selectionQuad.BottomRight.X - actualOrigin.X, 0)) + scale.X = selectionQuad.BottomRight.X - actualOrigin.X < 0 ? MathHelper.Clamp(scale.X, br2.X, br1.X) : MathHelper.Clamp(scale.X, br1.X, br2.X); + if (!Precision.AlmostEquals(selectionQuad.BottomRight.Y - actualOrigin.Y, 0)) + scale.Y = selectionQuad.BottomRight.Y - actualOrigin.Y < 0 ? MathHelper.Clamp(scale.Y, br2.Y, br1.Y) : MathHelper.Clamp(scale.Y, br1.Y, br2.Y); return Vector2.ComponentMax(scale, new Vector2(Precision.FLOAT_EPSILON)); } diff --git a/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs b/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs index ed52da56d3..50195ebd1e 100644 --- a/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs +++ b/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.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.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -22,6 +23,7 @@ namespace osu.Game.Rulesets.Osu.Edit private readonly Bindable scaleInfo = new Bindable(new PreciseScaleInfo(1, ScaleOrigin.PlayfieldCentre, true, true)); private SliderWithTextBoxInput scaleInput = null!; + private BindableNumber scaleInputBindable = null!; private EditorRadioButtonCollection scaleOrigin = null!; private RadioButton selectionCentreButton = null!; @@ -45,7 +47,7 @@ namespace osu.Game.Rulesets.Osu.Edit { scaleInput = new SliderWithTextBoxInput("Scale:") { - Current = new BindableNumber + Current = scaleInputBindable = new BindableNumber { MinValue = 0.5f, MaxValue = 2, @@ -61,10 +63,10 @@ namespace osu.Game.Rulesets.Osu.Edit Items = new[] { new RadioButton("Playfield centre", - () => scaleInfo.Value = scaleInfo.Value with { Origin = ScaleOrigin.PlayfieldCentre }, + () => setOrigin(ScaleOrigin.PlayfieldCentre), () => new SpriteIcon { Icon = FontAwesome.Regular.Square }), selectionCentreButton = new RadioButton("Selection centre", - () => scaleInfo.Value = scaleInfo.Value with { Origin = ScaleOrigin.SelectionCentre }, + () => setOrigin(ScaleOrigin.SelectionCentre), () => new SpriteIcon { Icon = FontAwesome.Solid.VectorSquare }) } } @@ -96,14 +98,39 @@ namespace osu.Game.Rulesets.Osu.Edit scaleInfo.BindValueChanged(scale => { var newScale = new Vector2(scale.NewValue.XAxis ? scale.NewValue.Scale : 1, scale.NewValue.YAxis ? scale.NewValue.Scale : 1); - scaleHandler.Update(newScale, scale.NewValue.Origin == ScaleOrigin.PlayfieldCentre ? OsuPlayfield.BASE_SIZE / 2 : null); + scaleHandler.Update(newScale, getOriginPosition(scale.NewValue)); }); } + private void updateMaxScale() + { + if (!scaleHandler.OriginalSurroundingQuad.HasValue) + return; + + const float max_scale = 10; + var scale = scaleHandler.GetClampedScale(new Vector2(max_scale), getOriginPosition(scaleInfo.Value)); + + if (!scaleInfo.Value.XAxis) + scale.X = max_scale; + if (!scaleInfo.Value.YAxis) + scale.Y = max_scale; + + scaleInputBindable.MaxValue = MathF.Max(1, MathF.Min(scale.X, scale.Y)); + } + + private void setOrigin(ScaleOrigin origin) + { + scaleInfo.Value = scaleInfo.Value with { Origin = origin }; + updateMaxScale(); + } + + private Vector2? getOriginPosition(PreciseScaleInfo scale) => scale.Origin == ScaleOrigin.PlayfieldCentre ? OsuPlayfield.BASE_SIZE / 2 : null; + protected override void PopIn() { base.PopIn(); scaleHandler.Begin(); + updateMaxScale(); } protected override void PopOut() diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionScaleHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionScaleHandler.cs index a96f627e56..fb421c2329 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionScaleHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionScaleHandler.cs @@ -34,6 +34,14 @@ namespace osu.Game.Screens.Edit.Compose.Components public Quad? OriginalSurroundingQuad { get; protected set; } + /// + /// Clamp scale where selection does not exceed playfield bounds or flip. + /// + /// The origin from which the scale operation is performed + /// The scale to be clamped + /// The clamped scale vector + public virtual Vector2 GetClampedScale(Vector2 scale, Vector2? origin = null) => scale; + /// /// Performs a single, instant, atomic scale operation. /// From 37530eebccdc6b7bacee3742946830ac61e2e815 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Sat, 25 May 2024 20:35:06 +0200 Subject: [PATCH 0925/2493] Enable scale buttons at the correct times --- osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs | 2 ++ osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs | 2 +- osu.Game.Rulesets.Osu/Edit/TransformToolboxGroup.cs | 8 ++++---- .../Edit/Compose/Components/SelectionScaleHandler.cs | 10 ++++++++++ 4 files changed, 17 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs index 331e8de3f1..e45494977f 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs @@ -53,6 +53,8 @@ namespace osu.Game.Rulesets.Osu.Edit CanScaleX.Value = quad.Width > 0; CanScaleY.Value = quad.Height > 0; CanScaleDiagonally.Value = CanScaleX.Value && CanScaleY.Value; + CanScaleSelectionOrigin.Value = CanScaleX.Value || CanScaleY.Value; + CanScalePlayfieldOrigin.Value = selectedMovableObjects.Any(); } private Dictionary? objectsInScale; diff --git a/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs b/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs index 50195ebd1e..355064f000 100644 --- a/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs +++ b/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs @@ -90,7 +90,7 @@ namespace osu.Game.Rulesets.Osu.Edit scaleInput.Current.BindValueChanged(scale => scaleInfo.Value = scaleInfo.Value with { Scale = scale.NewValue }); scaleOrigin.Items.First().Select(); - scaleHandler.CanScaleX.BindValueChanged(e => + scaleHandler.CanScaleSelectionOrigin.BindValueChanged(e => { selectionCentreButton.Selected.Disabled = !e.NewValue; }, true); diff --git a/osu.Game.Rulesets.Osu/Edit/TransformToolboxGroup.cs b/osu.Game.Rulesets.Osu/Edit/TransformToolboxGroup.cs index 146d771e19..e1f53846dc 100644 --- a/osu.Game.Rulesets.Osu/Edit/TransformToolboxGroup.cs +++ b/osu.Game.Rulesets.Osu/Edit/TransformToolboxGroup.cs @@ -29,7 +29,7 @@ namespace osu.Game.Rulesets.Osu.Edit private Bindable canScaleX = null!; private Bindable canScaleY = null!; - private Bindable canScaleDiagonally = null!; + private Bindable canScalePlayfieldOrigin = null!; public SelectionRotationHandler RotationHandler { get; init; } = null!; public SelectionScaleHandler ScaleHandler { get; init; } = null!; @@ -82,12 +82,12 @@ namespace osu.Game.Rulesets.Osu.Edit canScaleY = ScaleHandler.CanScaleY.GetBoundCopy(); canScaleY.BindValueChanged(_ => updateCanScaleAggregate()); - canScaleDiagonally = ScaleHandler.CanScaleDiagonally.GetBoundCopy(); - canScaleDiagonally.BindValueChanged(_ => updateCanScaleAggregate()); + canScalePlayfieldOrigin = ScaleHandler.CanScalePlayfieldOrigin.GetBoundCopy(); + canScalePlayfieldOrigin.BindValueChanged(_ => updateCanScaleAggregate()); void updateCanScaleAggregate() { - canScale.Value = ScaleHandler.CanScaleX.Value || ScaleHandler.CanScaleY.Value || ScaleHandler.CanScaleDiagonally.Value; + canScale.Value = ScaleHandler.CanScaleX.Value || ScaleHandler.CanScaleY.Value || ScaleHandler.CanScalePlayfieldOrigin.Value; } // bindings to `Enabled` on the buttons are decoupled on purpose diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionScaleHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionScaleHandler.cs index fb421c2329..495cce7ad6 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionScaleHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionScaleHandler.cs @@ -32,6 +32,16 @@ namespace osu.Game.Screens.Edit.Compose.Components /// public Bindable CanScaleDiagonally { get; private set; } = new BindableBool(); + /// + /// Whether scaling anchored by the selection origin can currently be performed. + /// + public Bindable CanScaleSelectionOrigin { get; private set; } = new BindableBool(); + + /// + /// Whether scaling anchored by the center of the playfield can currently be performed. + /// + public Bindable CanScalePlayfieldOrigin { get; private set; } = new BindableBool(); + public Quad? OriginalSurroundingQuad { get; protected set; } /// From d4489545f275fb29eae60ea96fbe4c84d8f82cf2 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Sat, 25 May 2024 21:44:08 +0200 Subject: [PATCH 0926/2493] add axis toggles --- .../Edit/PreciseScalePopover.cs | 36 ++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs b/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs index 355064f000..b76d778b3d 100644 --- a/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs +++ b/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs @@ -8,6 +8,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Rulesets.Osu.UI; using osu.Game.Screens.Edit.Components.RadioButtons; @@ -28,6 +29,9 @@ namespace osu.Game.Rulesets.Osu.Edit private RadioButton selectionCentreButton = null!; + private OsuCheckbox xCheckBox = null!; + private OsuCheckbox yCheckBox = null!; + public PreciseScalePopover(SelectionScaleHandler scaleHandler) { this.scaleHandler = scaleHandler; @@ -69,7 +73,28 @@ namespace osu.Game.Rulesets.Osu.Edit () => setOrigin(ScaleOrigin.SelectionCentre), () => new SpriteIcon { Icon = FontAwesome.Solid.VectorSquare }) } - } + }, + new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Spacing = new Vector2(4), + Children = new Drawable[] + { + xCheckBox = new OsuCheckbox(false) + { + RelativeSizeAxes = Axes.X, + LabelText = "X-axis", + Current = { Value = true }, + }, + yCheckBox = new OsuCheckbox(false) + { + RelativeSizeAxes = Axes.X, + LabelText = "Y-axis", + Current = { Value = true }, + }, + } + }, } }; selectionCentreButton.Selected.DisabledChanged += isDisabled => @@ -90,6 +115,9 @@ namespace osu.Game.Rulesets.Osu.Edit scaleInput.Current.BindValueChanged(scale => scaleInfo.Value = scaleInfo.Value with { Scale = scale.NewValue }); scaleOrigin.Items.First().Select(); + xCheckBox.Current.BindValueChanged(x => setAxis(x.NewValue, yCheckBox.Current.Value)); + yCheckBox.Current.BindValueChanged(y => setAxis(xCheckBox.Current.Value, y.NewValue)); + scaleHandler.CanScaleSelectionOrigin.BindValueChanged(e => { selectionCentreButton.Selected.Disabled = !e.NewValue; @@ -126,6 +154,12 @@ namespace osu.Game.Rulesets.Osu.Edit private Vector2? getOriginPosition(PreciseScaleInfo scale) => scale.Origin == ScaleOrigin.PlayfieldCentre ? OsuPlayfield.BASE_SIZE / 2 : null; + private void setAxis(bool x, bool y) + { + scaleInfo.Value = scaleInfo.Value with { XAxis = x, YAxis = y }; + updateMaxScale(); + } + protected override void PopIn() { base.PopIn(); From 76f13b21da3ed79e9daf3d7421342bdb29762dae Mon Sep 17 00:00:00 2001 From: Joppe27 Date: Sat, 25 May 2024 23:28:51 +0200 Subject: [PATCH 0927/2493] Correct scale of taiko-glow element --- osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyKiaiGlow.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyKiaiGlow.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyKiaiGlow.cs index 623243e9e1..487106d879 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyKiaiGlow.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyKiaiGlow.cs @@ -21,6 +21,8 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy private Sprite sprite = null!; + private const float base_scale = 0.8f; + [BackgroundDependencyLoader(true)] private void load(ISkinSource skin, HealthProcessor? healthProcessor) { @@ -30,7 +32,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy Origin = Anchor.Centre, Anchor = Anchor.Centre, Alpha = 0, - Scale = new Vector2(0.7f), + Scale = new Vector2(base_scale), Colour = new Colour4(255, 228, 0, 255), }; @@ -58,8 +60,8 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy if (!result.IsHit || !isKiaiActive) return; - sprite.ScaleTo(0.85f).Then() - .ScaleTo(0.7f, 80, Easing.OutQuad); + sprite.ScaleTo(base_scale + 0.15f).Then() + .ScaleTo(base_scale, 80, Easing.OutQuad); } } } From 8e14c24ee3c6e3d8641b7af281423735f4a0252c Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Sun, 26 May 2024 00:24:03 -0700 Subject: [PATCH 0928/2493] Follow slanted flow logic precedent in test See `ModSelectOverlay` components. --- .../SongSelect/TestSceneLeaderboardScoreV2.cs | 5 ++-- .../Leaderboards/LeaderboardScoreV2.cs | 23 ++++++++++--------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneLeaderboardScoreV2.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneLeaderboardScoreV2.cs index d3d388dba3..c8725fde08 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneLeaderboardScoreV2.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneLeaderboardScoreV2.cs @@ -63,17 +63,16 @@ namespace osu.Game.Tests.Visual.SongSelect RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Spacing = new Vector2(0f, 2f), + Shear = LeaderboardScoreV2.SHEAR }, drawWidthText = new OsuSpriteText(), }; - int i = 0; - foreach (var scoreInfo in getTestScores()) { fillFlow.Add(new LeaderboardScoreV2(scoreInfo, scoreInfo.Position, scoreInfo.User.Id == 2) { - Margin = new MarginPadding { Right = 10f * i, Left = -10f * i++ }, + Shear = Vector2.Zero, }); } diff --git a/osu.Game/Screens/SelectV2/Leaderboards/LeaderboardScoreV2.cs b/osu.Game/Screens/SelectV2/Leaderboards/LeaderboardScoreV2.cs index 47b5a692bf..0a558186dd 100644 --- a/osu.Game/Screens/SelectV2/Leaderboards/LeaderboardScoreV2.cs +++ b/osu.Game/Screens/SelectV2/Leaderboards/LeaderboardScoreV2.cs @@ -65,7 +65,8 @@ namespace osu.Game.Screens.SelectV2.Leaderboards private Colour4 backgroundColour; private ColourInfo totalScoreBackgroundGradient; - private static readonly Vector2 shear = new Vector2(0.15f, 0); + // TODO: once https://github.com/ppy/osu/pull/28183 is merged, probably use OsuGame.SHEAR + public static readonly Vector2 SHEAR = new Vector2(0.15f, 0); [Resolved] private OverlayColourProvider colourProvider { get; set; } = null!; @@ -112,7 +113,7 @@ namespace osu.Game.Screens.SelectV2.Leaderboards this.rank = rank; this.isPersonalBest = isPersonalBest; - Shear = shear; + Shear = SHEAR; RelativeSizeAxes = Axes.X; Height = height; } @@ -245,7 +246,7 @@ namespace osu.Game.Screens.SelectV2.Leaderboards { RelativeSizeAxes = Axes.Both, User = score.User, - Shear = -shear, + Shear = -SHEAR, Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, Colour = ColourInfo.GradientHorizontal(Colour4.White.Opacity(0.5f), Colour4.FromHex(@"222A27").Opacity(1)), @@ -276,7 +277,7 @@ namespace osu.Game.Screens.SelectV2.Leaderboards Anchor = Anchor.Centre, Origin = Anchor.Centre, Scale = new Vector2(1.1f), - Shear = -shear, + Shear = -SHEAR, RelativeSizeAxes = Axes.Both, }) { @@ -316,7 +317,7 @@ namespace osu.Game.Screens.SelectV2.Leaderboards { flagBadgeAndDateContainer = new FillFlowContainer { - Shear = -shear, + Shear = -SHEAR, Direction = FillDirection.Horizontal, Spacing = new Vector2(5), AutoSizeAxes = Axes.Both, @@ -340,7 +341,7 @@ namespace osu.Game.Screens.SelectV2.Leaderboards nameLabel = new TruncatingSpriteText { RelativeSizeAxes = Axes.X, - Shear = -shear, + Shear = -SHEAR, Text = user.Username, Font = OsuFont.GetFont(size: 20, weight: FontWeight.SemiBold) } @@ -356,7 +357,7 @@ namespace osu.Game.Screens.SelectV2.Leaderboards Name = @"Statistics container", Padding = new MarginPadding { Right = 40 }, Spacing = new Vector2(25, 0), - Shear = -shear, + Shear = -SHEAR, Anchor = Anchor.CentreRight, Origin = Anchor.CentreRight, AutoSizeAxes = Axes.Both, @@ -414,7 +415,7 @@ namespace osu.Game.Screens.SelectV2.Leaderboards }, RankContainer = new Container { - Shear = -shear, + Shear = -SHEAR, Anchor = Anchor.CentreRight, Origin = Anchor.CentreRight, RelativeSizeAxes = Axes.Y, @@ -472,7 +473,7 @@ namespace osu.Game.Screens.SelectV2.Leaderboards Anchor = Anchor.TopRight, Origin = Anchor.TopRight, UseFullGlyphHeight = false, - Shear = -shear, + Shear = -SHEAR, Current = scoreManager.GetBindableTotalScoreString(score), Font = OsuFont.GetFont(size: 30, weight: FontWeight.Light), }, @@ -480,7 +481,7 @@ namespace osu.Game.Screens.SelectV2.Leaderboards { Anchor = Anchor.TopRight, Origin = Anchor.TopRight, - Shear = -shear, + Shear = -SHEAR, AutoSizeAxes = Axes.Both, Direction = FillDirection.Horizontal, Spacing = new Vector2(2f, 0f), @@ -665,7 +666,7 @@ namespace osu.Game.Screens.SelectV2.Leaderboards Child = new OsuSpriteText { - Shear = -shear, + Shear = -SHEAR, Anchor = Anchor.Centre, Origin = Anchor.Centre, Font = OsuFont.GetFont(size: 20, weight: FontWeight.SemiBold, italics: true), From 61afda1089eeee4d42f59ab6185023d699acb788 Mon Sep 17 00:00:00 2001 From: js1086 Date: Sun, 26 May 2024 11:24:06 +0100 Subject: [PATCH 0929/2493] Fix NaN case when difficulty is 0 --- osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs index c2e9357e3a..af97d90e53 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs @@ -77,6 +77,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills /// public double CountDifficultStrains() { + if (double.IsNaN(Difficulty)) + return 0.0; + double consistentTopStrain = Difficulty / 10; // What would the top strain be if all strain values were identical // Use a weighted sum of all strains. Constants are arbitrary and give nice values return ObjectStrains.Sum(s => 1.1 / (1 + Math.Exp(-10 * (s / consistentTopStrain - 0.88)))); From c25e1bdeb586db8a2def47232632be61b4d4242e Mon Sep 17 00:00:00 2001 From: js1086 Date: Sun, 26 May 2024 14:21:47 +0100 Subject: [PATCH 0930/2493] Use correct operation for 0 difficulty case --- osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs index af97d90e53..ec132237db 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs @@ -77,7 +77,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills /// public double CountDifficultStrains() { - if (double.IsNaN(Difficulty)) + if (Difficulty == 0) return 0.0; double consistentTopStrain = Difficulty / 10; // What would the top strain be if all strain values were identical From a62b9fa633437bd31dd375bf6c9b12a8591ff225 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 27 May 2024 11:42:36 +0900 Subject: [PATCH 0931/2493] Revert windows 16px icon to original version This also fixes the 48px version looking uncanny due to smaller paddings. --- osu.Desktop/lazer.ico | Bin 76679 -> 76679 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/osu.Desktop/lazer.ico b/osu.Desktop/lazer.ico index 24c1c29ba269aaeb38a9b9c604a1c364c986a753..d5dbf933c1c6795b54b485b349aa76f914707996 100644 GIT binary patch delta 11340 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 b&=ZDEW}Izh;E>_$Qd7_DezO;xnO^@5U$|tB delta 11404 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 From 0d6adf160bcff5da41e5e2262b7e223163670179 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 27 May 2024 14:20:28 +0900 Subject: [PATCH 0932/2493] Share scale factor with hit target --- .../Skinning/Legacy/LegacyKiaiGlow.cs | 8 +++----- .../Skinning/Legacy/TaikoLegacyHitTarget.cs | 10 ++++++++-- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyKiaiGlow.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyKiaiGlow.cs index 487106d879..9877efa127 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyKiaiGlow.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyKiaiGlow.cs @@ -21,8 +21,6 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy private Sprite sprite = null!; - private const float base_scale = 0.8f; - [BackgroundDependencyLoader(true)] private void load(ISkinSource skin, HealthProcessor? healthProcessor) { @@ -32,7 +30,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy Origin = Anchor.Centre, Anchor = Anchor.Centre, Alpha = 0, - Scale = new Vector2(base_scale), + Scale = new Vector2(TaikoLegacyHitTarget.SCALE), Colour = new Colour4(255, 228, 0, 255), }; @@ -60,8 +58,8 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy if (!result.IsHit || !isKiaiActive) return; - sprite.ScaleTo(base_scale + 0.15f).Then() - .ScaleTo(base_scale, 80, Easing.OutQuad); + sprite.ScaleTo(TaikoLegacyHitTarget.SCALE + 0.15f).Then() + .ScaleTo(TaikoLegacyHitTarget.SCALE, 80, Easing.OutQuad); } } } diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacyHitTarget.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacyHitTarget.cs index 0b43f1c845..2a008d81d9 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacyHitTarget.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacyHitTarget.cs @@ -12,6 +12,12 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy { public partial class TaikoLegacyHitTarget : CompositeDrawable { + /// + /// In stable this is 0.7f (see https://github.com/peppy/osu-stable-reference/blob/7519cafd1823f1879c0d9c991ba0e5c7fd3bfa02/osu!/GameModes/Play/Rulesets/Taiko/RulesetTaiko.cs#L592) + /// but for whatever reason this doesn't match visually. + /// + public const float SCALE = 0.8f; + [BackgroundDependencyLoader] private void load(ISkinSource skin) { @@ -22,7 +28,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy new Sprite { Texture = skin.GetTexture("approachcircle"), - Scale = new Vector2(0.83f), + Scale = new Vector2(SCALE + 0.03f), Alpha = 0.47f, // eyeballed to match stable Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -30,7 +36,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy new Sprite { Texture = skin.GetTexture("taikobigcircle"), - Scale = new Vector2(0.8f), + Scale = new Vector2(SCALE), Alpha = 0.22f, // eyeballed to match stable Anchor = Anchor.Centre, Origin = Anchor.Centre, From 11c3d11db9b66097d61f877cb2e9b602bf90b5e5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 27 May 2024 14:38:43 +0900 Subject: [PATCH 0933/2493] Fix cinema mod not hiding playfield skin layer --- osu.Game/Rulesets/Mods/ModCinema.cs | 2 ++ osu.Game/Screens/Play/HUDOverlay.cs | 15 +++++++++------ 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/osu.Game/Rulesets/Mods/ModCinema.cs b/osu.Game/Rulesets/Mods/ModCinema.cs index 7c88a8a588..0c00eb6ae0 100644 --- a/osu.Game/Rulesets/Mods/ModCinema.cs +++ b/osu.Game/Rulesets/Mods/ModCinema.cs @@ -36,6 +36,8 @@ namespace osu.Game.Rulesets.Mods { overlay.ShowHud.Value = false; overlay.ShowHud.Disabled = true; + + overlay.PlayfieldSkinLayer.Hide(); } public void ApplyToPlayer(Player player) diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index 9d7a05bc90..16dfff8c19 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -109,7 +109,10 @@ namespace osu.Game.Screens.Play private readonly List hideTargets; - private readonly Drawable playfieldComponents; + /// + /// The container for skin components attached to + /// + internal readonly Drawable PlayfieldSkinLayer; public HUDOverlay([CanBeNull] DrawableRuleset drawableRuleset, IReadOnlyList mods, bool alwaysShowLeaderboard = true) { @@ -129,7 +132,7 @@ namespace osu.Game.Screens.Play drawableRuleset != null ? (rulesetComponents = new HUDComponentsContainer(drawableRuleset.Ruleset.RulesetInfo) { AlwaysPresent = true, }) : Empty(), - playfieldComponents = drawableRuleset != null + PlayfieldSkinLayer = drawableRuleset != null ? new SkinComponentsContainer(new SkinComponentsContainerLookup(SkinComponentsContainerLookup.TargetArea.Playfield, drawableRuleset.Ruleset.RulesetInfo)) { AlwaysPresent = true, } : Empty(), topRightElements = new FillFlowContainer @@ -247,10 +250,10 @@ namespace osu.Game.Screens.Play { Quad playfieldScreenSpaceDrawQuad = drawableRuleset.Playfield.SkinnableComponentScreenSpaceDrawQuad; - playfieldComponents.Position = ToLocalSpace(playfieldScreenSpaceDrawQuad.TopLeft); - playfieldComponents.Width = (ToLocalSpace(playfieldScreenSpaceDrawQuad.TopRight) - ToLocalSpace(playfieldScreenSpaceDrawQuad.TopLeft)).Length; - playfieldComponents.Height = (ToLocalSpace(playfieldScreenSpaceDrawQuad.BottomLeft) - ToLocalSpace(playfieldScreenSpaceDrawQuad.TopLeft)).Length; - playfieldComponents.Rotation = drawableRuleset.Playfield.Rotation; + PlayfieldSkinLayer.Position = ToLocalSpace(playfieldScreenSpaceDrawQuad.TopLeft); + PlayfieldSkinLayer.Width = (ToLocalSpace(playfieldScreenSpaceDrawQuad.TopRight) - ToLocalSpace(playfieldScreenSpaceDrawQuad.TopLeft)).Length; + PlayfieldSkinLayer.Height = (ToLocalSpace(playfieldScreenSpaceDrawQuad.BottomLeft) - ToLocalSpace(playfieldScreenSpaceDrawQuad.TopLeft)).Length; + PlayfieldSkinLayer.Rotation = drawableRuleset.Playfield.Rotation; } float? lowestTopScreenSpaceLeft = null; From bdfce4b9dafc90314e6b55f448e899d164ca1d97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 27 May 2024 08:26:00 +0200 Subject: [PATCH 0934/2493] Fix xmldoc reference --- osu.Game/Screens/Play/HUDOverlay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index 16dfff8c19..0c0941573c 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -110,7 +110,7 @@ namespace osu.Game.Screens.Play private readonly List hideTargets; /// - /// The container for skin components attached to + /// The container for skin components attached to /// internal readonly Drawable PlayfieldSkinLayer; From b6471f0b9cd4ebb3dee155990e89b44e1e2a14f8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 27 May 2024 17:09:35 +0900 Subject: [PATCH 0935/2493] Allow previewing audio of playlist items --- .../Drawables/Cards/BeatmapCardThumbnail.cs | 5 +- osu.Game/Overlays/BeatmapSet/BasicStats.cs | 5 +- .../Overlays/BeatmapSet/Buttons/PlayButton.cs | 5 +- .../BeatmapSet/Buttons/PreviewButton.cs | 6 +-- osu.Game/Overlays/BeatmapSet/Details.cs | 1 + .../OnlinePlay/DrawableRoomPlaylistItem.cs | 49 ++++++++++++++++--- 6 files changed, 51 insertions(+), 20 deletions(-) diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardThumbnail.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardThumbnail.cs index cd498c474a..e70d115715 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardThumbnail.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardThumbnail.cs @@ -8,7 +8,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Game.Beatmaps.Drawables.Cards.Buttons; -using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays; using osu.Framework.Graphics.UserInterface; using osuTK; @@ -36,14 +35,14 @@ namespace osu.Game.Beatmaps.Drawables.Cards [Resolved] private OverlayColourProvider colourProvider { get; set; } = null!; - public BeatmapCardThumbnail(APIBeatmapSet beatmapSetInfo) + public BeatmapCardThumbnail(IBeatmapSetInfo beatmapSetInfo) { InternalChildren = new Drawable[] { new UpdateableOnlineBeatmapSetCover(BeatmapSetCoverType.List) { RelativeSizeAxes = Axes.Both, - OnlineInfo = beatmapSetInfo + OnlineInfo = beatmapSetInfo as IBeatmapSetOnlineInfo }, background = new Box { diff --git a/osu.Game/Overlays/BeatmapSet/BasicStats.cs b/osu.Game/Overlays/BeatmapSet/BasicStats.cs index 0b1befe7b9..364874cdf7 100644 --- a/osu.Game/Overlays/BeatmapSet/BasicStats.cs +++ b/osu.Game/Overlays/BeatmapSet/BasicStats.cs @@ -16,7 +16,6 @@ using osu.Game.Beatmaps; using osu.Game.Extensions; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; -using osu.Game.Online.API.Requests.Responses; using osu.Game.Resources.Localisation.Web; using osuTK; @@ -26,9 +25,9 @@ namespace osu.Game.Overlays.BeatmapSet { private readonly Statistic length, bpm, circleCount, sliderCount; - private APIBeatmapSet beatmapSet; + private IBeatmapSetInfo beatmapSet; - public APIBeatmapSet BeatmapSet + public IBeatmapSetInfo BeatmapSet { get => beatmapSet; set diff --git a/osu.Game/Overlays/BeatmapSet/Buttons/PlayButton.cs b/osu.Game/Overlays/BeatmapSet/Buttons/PlayButton.cs index 5f9cdf5065..921f136de9 100644 --- a/osu.Game/Overlays/BeatmapSet/Buttons/PlayButton.cs +++ b/osu.Game/Overlays/BeatmapSet/Buttons/PlayButton.cs @@ -11,6 +11,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Events; using osu.Game.Audio; +using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Graphics.UserInterface; using osu.Game.Online.API.Requests.Responses; @@ -28,9 +29,9 @@ namespace osu.Game.Overlays.BeatmapSet.Buttons [CanBeNull] public PreviewTrack Preview { get; private set; } - private APIBeatmapSet beatmapSet; + private IBeatmapSetInfo beatmapSet; - public APIBeatmapSet BeatmapSet + public IBeatmapSetInfo BeatmapSet { get => beatmapSet; set diff --git a/osu.Game/Overlays/BeatmapSet/Buttons/PreviewButton.cs b/osu.Game/Overlays/BeatmapSet/Buttons/PreviewButton.cs index 2254514a44..1eff4a7c11 100644 --- a/osu.Game/Overlays/BeatmapSet/Buttons/PreviewButton.cs +++ b/osu.Game/Overlays/BeatmapSet/Buttons/PreviewButton.cs @@ -8,9 +8,9 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; using osu.Game.Audio; +using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Graphics.Containers; -using osu.Game.Online.API.Requests.Responses; using osuTK; namespace osu.Game.Overlays.BeatmapSet.Buttons @@ -24,7 +24,7 @@ namespace osu.Game.Overlays.BeatmapSet.Buttons public IBindable Playing => playButton.Playing; - public APIBeatmapSet BeatmapSet + public IBeatmapSetInfo BeatmapSet { get => playButton.BeatmapSet; set => playButton.BeatmapSet = value; @@ -32,8 +32,6 @@ namespace osu.Game.Overlays.BeatmapSet.Buttons public PreviewButton() { - Height = 42; - Children = new Drawable[] { background = new Box diff --git a/osu.Game/Overlays/BeatmapSet/Details.cs b/osu.Game/Overlays/BeatmapSet/Details.cs index d656a6b14b..7d69cb7329 100644 --- a/osu.Game/Overlays/BeatmapSet/Details.cs +++ b/osu.Game/Overlays/BeatmapSet/Details.cs @@ -68,6 +68,7 @@ namespace osu.Game.Overlays.BeatmapSet preview = new PreviewButton { RelativeSizeAxes = Axes.X, + Height = 42, }, new DetailBox { diff --git a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs index 1b8e2d8be6..b28269c6e6 100644 --- a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs +++ b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs @@ -22,6 +22,7 @@ using osu.Framework.Localisation; using osu.Framework.Logging; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Drawables; +using osu.Game.Beatmaps.Drawables.Cards; using osu.Game.Collections; using osu.Game.Database; using osu.Game.Graphics; @@ -32,6 +33,7 @@ using osu.Game.Online.Chat; using osu.Game.Online.Rooms; using osu.Game.Overlays; using osu.Game.Overlays.BeatmapSet; +using osu.Game.Overlays.BeatmapSet.Buttons; using osu.Game.Resources.Localisation.Web; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; @@ -81,7 +83,7 @@ namespace osu.Game.Screens.OnlinePlay private Mod[] requiredMods = Array.Empty(); private Container maskingContainer; - private Container difficultyIconContainer; + private FillFlowContainer difficultyIconContainer; private LinkFlowContainer beatmapText; private LinkFlowContainer authorText; private ExplicitContentBeatmapBadge explicitContent; @@ -93,6 +95,7 @@ namespace osu.Game.Screens.OnlinePlay private Drawable removeButton; private PanelBackground panelBackground; private FillFlowContainer mainFillFlow; + private BeatmapCardThumbnail thumbnail; [Resolved] private RealmAccess realm { get; set; } @@ -282,10 +285,23 @@ namespace osu.Game.Screens.OnlinePlay if (beatmap != null) { - difficultyIconContainer.Child = new DifficultyIcon(beatmap, ruleset, requiredMods) + difficultyIconContainer.Children = new Drawable[] { - Size = new Vector2(icon_height), - TooltipType = DifficultyIconTooltipType.Extended, + thumbnail = new BeatmapCardThumbnail(beatmap.BeatmapSet!) + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Width = 60, + RelativeSizeAxes = Axes.Y, + Dimmed = { Value = IsHovered } + }, + new DifficultyIcon(beatmap, ruleset, requiredMods) + { + Size = new Vector2(icon_height), + TooltipType = DifficultyIconTooltipType.Extended, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + }, }; } else @@ -329,7 +345,7 @@ namespace osu.Game.Screens.OnlinePlay protected override Drawable CreateContent() { - Action fontParameters = s => s.Font = OsuFont.Default.With(weight: FontWeight.SemiBold); + Action fontParameters = s => s.Font = OsuFont.Default.With(size: 14, weight: FontWeight.SemiBold); return maskingContainer = new Container { @@ -364,12 +380,15 @@ namespace osu.Game.Screens.OnlinePlay { new Drawable[] { - difficultyIconContainer = new Container + difficultyIconContainer = new FillFlowContainer { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, - AutoSizeAxes = Axes.Both, - Margin = new MarginPadding { Left = 8, Right = 8 }, + AutoSizeAxes = Axes.X, + RelativeSizeAxes = Axes.Y, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(4), + Margin = new MarginPadding { Right = 8 }, }, mainFillFlow = new MainFlow(() => SelectedItem.Value == Model || !AllowSelection) { @@ -484,6 +503,20 @@ namespace osu.Game.Screens.OnlinePlay }, }; + protected override bool OnHover(HoverEvent e) + { + if (thumbnail != null) + thumbnail.Dimmed.Value = true; + return base.OnHover(e); + } + + protected override void OnHoverLost(HoverLostEvent e) + { + if (thumbnail != null) + thumbnail.Dimmed.Value = false; + base.OnHoverLost(e); + } + protected override bool OnClick(ClickEvent e) { if (AllowSelection && valid.Value) From 1e2cac3e92f3cec02d28639a1314db80ae0bf022 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 27 May 2024 11:44:55 +0200 Subject: [PATCH 0936/2493] Remove unused using directive --- osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs index b28269c6e6..090236d6e2 100644 --- a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs +++ b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs @@ -33,7 +33,6 @@ using osu.Game.Online.Chat; using osu.Game.Online.Rooms; using osu.Game.Overlays; using osu.Game.Overlays.BeatmapSet; -using osu.Game.Overlays.BeatmapSet.Buttons; using osu.Game.Resources.Localisation.Web; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; From 1f41261fc7d5e0c0f5a77cc064a1ba3c1d525ff5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 27 May 2024 12:01:02 +0200 Subject: [PATCH 0937/2493] Fix test failure --- .../Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs index 6446ebd35f..bd62a8b131 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs @@ -16,6 +16,7 @@ using osu.Framework.Platform; using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Drawables; +using osu.Game.Beatmaps.Drawables.Cards; using osu.Game.Database; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Cursor; @@ -317,13 +318,13 @@ namespace osu.Game.Tests.Visual.Multiplayer p.RequestResults = _ => resultsRequested = true; }); + AddUntilStep("wait for load", () => playlist.ChildrenOfType().Any() && playlist.ChildrenOfType().First().DrawWidth > 0); AddStep("move mouse to first item title", () => { var drawQuad = playlist.ChildrenOfType().First().ScreenSpaceDrawQuad; var location = (drawQuad.TopLeft + drawQuad.BottomLeft) / 2 + new Vector2(drawQuad.Width * 0.2f, 0); InputManager.MoveMouseTo(location); }); - AddUntilStep("wait for text load", () => playlist.ChildrenOfType().Any()); AddAssert("first item title not hovered", () => playlist.ChildrenOfType().First().IsHovered, () => Is.False); AddStep("click left mouse", () => InputManager.Click(MouseButton.Left)); AddUntilStep("first item selected", () => playlist.ChildrenOfType().First().IsSelectedItem, () => Is.True); From d97622491297efa4ee4699b06748588b2ea84a07 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 27 May 2024 19:59:25 +0900 Subject: [PATCH 0938/2493] Standardise padding on both sides of difficulty icon --- osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs index 090236d6e2..72866d1694 100644 --- a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs +++ b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs @@ -387,7 +387,7 @@ namespace osu.Game.Screens.OnlinePlay RelativeSizeAxes = Axes.Y, Direction = FillDirection.Horizontal, Spacing = new Vector2(4), - Margin = new MarginPadding { Right = 8 }, + Margin = new MarginPadding { Right = 4 }, }, mainFillFlow = new MainFlow(() => SelectedItem.Value == Model || !AllowSelection) { From 75d961e6f2cc74b631a1980be91f3525e4551b80 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 27 May 2024 20:03:46 +0900 Subject: [PATCH 0939/2493] Pass the same thing in twice for better maybe --- .../Visual/Beatmaps/TestSceneBeatmapCardThumbnail.cs | 2 +- osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtra.cs | 2 +- osu.Game/Beatmaps/Drawables/Cards/BeatmapCardNormal.cs | 2 +- osu.Game/Beatmaps/Drawables/Cards/BeatmapCardThumbnail.cs | 4 ++-- osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapCardThumbnail.cs b/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapCardThumbnail.cs index f44fe2b90c..f5f9d121cc 100644 --- a/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapCardThumbnail.cs +++ b/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapCardThumbnail.cs @@ -34,7 +34,7 @@ namespace osu.Game.Tests.Visual.Beatmaps var beatmapSet = CreateAPIBeatmapSet(Ruleset.Value); beatmapSet.OnlineID = 241526; // ID hardcoded to ensure that the preview track exists online. - Child = thumbnail = new BeatmapCardThumbnail(beatmapSet) + Child = thumbnail = new BeatmapCardThumbnail(beatmapSet, beatmapSet) { Anchor = Anchor.Centre, Origin = Anchor.Centre, diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtra.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtra.cs index 175c15ea7b..2c2761ff0c 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtra.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtra.cs @@ -61,7 +61,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards RelativeSizeAxes = Axes.Both, Children = new Drawable[] { - thumbnail = new BeatmapCardThumbnail(BeatmapSet) + thumbnail = new BeatmapCardThumbnail(BeatmapSet, BeatmapSet) { Name = @"Left (icon) area", Size = new Vector2(height), diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardNormal.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardNormal.cs index 18e1584a98..c6ba4f234a 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardNormal.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardNormal.cs @@ -62,7 +62,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards RelativeSizeAxes = Axes.Both, Children = new Drawable[] { - thumbnail = new BeatmapCardThumbnail(BeatmapSet) + thumbnail = new BeatmapCardThumbnail(BeatmapSet, BeatmapSet) { Name = @"Left (icon) area", Size = new Vector2(height), diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardThumbnail.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardThumbnail.cs index e70d115715..5d2717a787 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardThumbnail.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardThumbnail.cs @@ -35,14 +35,14 @@ namespace osu.Game.Beatmaps.Drawables.Cards [Resolved] private OverlayColourProvider colourProvider { get; set; } = null!; - public BeatmapCardThumbnail(IBeatmapSetInfo beatmapSetInfo) + public BeatmapCardThumbnail(IBeatmapSetInfo beatmapSetInfo, IBeatmapSetOnlineInfo onlineInfo) { InternalChildren = new Drawable[] { new UpdateableOnlineBeatmapSetCover(BeatmapSetCoverType.List) { RelativeSizeAxes = Axes.Both, - OnlineInfo = beatmapSetInfo as IBeatmapSetOnlineInfo + OnlineInfo = onlineInfo }, background = new Box { diff --git a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs index 72866d1694..e9126a1404 100644 --- a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs +++ b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs @@ -286,7 +286,7 @@ namespace osu.Game.Screens.OnlinePlay { difficultyIconContainer.Children = new Drawable[] { - thumbnail = new BeatmapCardThumbnail(beatmap.BeatmapSet!) + thumbnail = new BeatmapCardThumbnail(beatmap.BeatmapSet!, (IBeatmapSetOnlineInfo)beatmap.BeatmapSet!) { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, From aad0982e26ba3fe57caf1df7999d7d582c413097 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 27 May 2024 20:33:24 +0900 Subject: [PATCH 0940/2493] Adjust size of play button / progress to match `BeatmapCardThumbnail` usage --- osu.Game/Beatmaps/Drawables/Cards/BeatmapCardThumbnail.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardThumbnail.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardThumbnail.cs index 5d2717a787..7b668d7dc4 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardThumbnail.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardThumbnail.cs @@ -61,7 +61,6 @@ namespace osu.Game.Beatmaps.Drawables.Cards { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Size = new Vector2(50), InnerRadius = 0.2f }, content = new Container @@ -92,6 +91,9 @@ namespace osu.Game.Beatmaps.Drawables.Cards { base.Update(); progress.Progress = playButton.Progress.Value; + + playButton.Scale = new Vector2(DrawWidth / 100); + progress.Size = new Vector2(50 * DrawWidth / 100); } private void updateState() From 405c72c0d66ef895cbb5d34aa6c32e5d81dd5d2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 27 May 2024 13:36:44 +0200 Subject: [PATCH 0941/2493] Fix mod display not being aligned with mapper text --- osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs index e9126a1404..ab32ca2558 100644 --- a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs +++ b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs @@ -416,6 +416,8 @@ namespace osu.Game.Screens.OnlinePlay new FillFlowContainer { AutoSizeAxes = Axes.Both, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, Direction = FillDirection.Horizontal, Spacing = new Vector2(10f, 0), Children = new Drawable[] @@ -438,7 +440,8 @@ namespace osu.Game.Screens.OnlinePlay Child = modDisplay = new ModDisplay { Scale = new Vector2(0.4f), - ExpansionMode = ExpansionMode.AlwaysExpanded + ExpansionMode = ExpansionMode.AlwaysExpanded, + Margin = new MarginPadding { Vertical = -6 }, } } } From d81be56adf67a46c0302695b273ac7c0f6c8e212 Mon Sep 17 00:00:00 2001 From: Aurelian Date: Mon, 27 May 2024 19:35:33 +0200 Subject: [PATCH 0942/2493] Test for accuracy of perfect curves --- .../Editor/TestSliderScaling.cs | 125 ++++++++++++++---- 1 file changed, 99 insertions(+), 26 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSliderScaling.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSliderScaling.cs index ef3824b5b0..2ffde0d3a3 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSliderScaling.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSliderScaling.cs @@ -3,8 +3,10 @@ #nullable disable +using System; using System.Linq; using NUnit.Framework; +using osu.Framework.Graphics.Primitives; using osu.Framework.Testing; using osu.Framework.Utils; using osu.Game.Beatmaps; @@ -72,48 +74,119 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor private void moveMouse(Vector2 pos) => AddStep($"move mouse to {pos}", () => InputManager.MoveMouseTo(playfield.ToScreenSpace(pos))); } + [TestFixture] public class TestSliderNearLinearScaling { + private readonly Random rng = new Random(1337); + [Test] public void TestScalingSliderFlat() { - Slider sliderPerfect = new Slider - { - Position = new Vector2(300), - Path = new SliderPath( - [ - new PathControlPoint(new Vector2(0), PathType.PERFECT_CURVE), - new PathControlPoint(new Vector2(50, 25)), - new PathControlPoint(new Vector2(25, 100)), - ]) - }; + SliderPath sliderPathPerfect = new SliderPath( + [ + new PathControlPoint(new Vector2(0), PathType.PERFECT_CURVE), + new PathControlPoint(new Vector2(50, 25)), + new PathControlPoint(new Vector2(25, 100)), + ]); - Slider sliderBezier = new Slider - { - Position = new Vector2(300), - Path = new SliderPath( - [ - new PathControlPoint(new Vector2(0), PathType.BEZIER), - new PathControlPoint(new Vector2(50, 25)), - new PathControlPoint(new Vector2(25, 100)), - ]) - }; + SliderPath sliderPathBezier = new SliderPath( + [ + new PathControlPoint(new Vector2(0), PathType.BEZIER), + new PathControlPoint(new Vector2(50, 25)), + new PathControlPoint(new Vector2(25, 100)), + ]); - scaleSlider(sliderPerfect, new Vector2(0.000001f, 1)); - scaleSlider(sliderBezier, new Vector2(0.000001f, 1)); + scaleSlider(sliderPathPerfect, new Vector2(0.000001f, 1)); + scaleSlider(sliderPathBezier, new Vector2(0.000001f, 1)); for (int i = 0; i < 100; i++) { - Assert.True(Precision.AlmostEquals(sliderPerfect.Path.PositionAt(i / 100.0f), sliderBezier.Path.PositionAt(i / 100.0f))); + Assert.True(Precision.AlmostEquals(sliderPathPerfect.PositionAt(i / 100.0f), sliderPathBezier.PositionAt(i / 100.0f))); } } - private void scaleSlider(Slider slider, Vector2 scale) + [Test] + public void TestPerfectCurveMatchesTheoretical() { - for (int i = 0; i < slider.Path.ControlPoints.Count; i++) + for (int i = 0; i < 20000; i++) { - slider.Path.ControlPoints[i].Position *= scale; + //Only test points that are in the screen's bounds + float p1X = 640.0f * (float)rng.NextDouble(); + float p2X = 640.0f * (float)rng.NextDouble(); + + float p1Y = 480.0f * (float)rng.NextDouble(); + float p2Y = 480.0f * (float)rng.NextDouble(); + SliderPath sliderPathPerfect = new SliderPath( + [ + new PathControlPoint(new Vector2(0, 0), PathType.PERFECT_CURVE), + new PathControlPoint(new Vector2(p1X, p1Y)), + new PathControlPoint(new Vector2(p2X, p2Y)), + ]); + + assertMatchesPerfectCircle(sliderPathPerfect); + + scaleSlider(sliderPathPerfect, new Vector2(0.00001f, 1)); + + assertMatchesPerfectCircle(sliderPathPerfect); + } + } + + private void assertMatchesPerfectCircle(SliderPath path) + { + if (path.ControlPoints.Count != 3) + return; + + //Replication of PathApproximator.CircularArcToPiecewiseLinear + CircularArcProperties circularArcProperties = new CircularArcProperties(path.ControlPoints.Select(x => x.Position).ToArray()); + + if (!circularArcProperties.IsValid) + return; + + //Addresses cases where circularArcProperties.ThetaRange>0.5 + //Occurs in code in PathControlPointVisualiser.ensureValidPathType + RectangleF boundingBox = PathApproximator.CircularArcBoundingBox(path.ControlPoints.Select(x => x.Position).ToArray()); + if (boundingBox.Width >= 640 || boundingBox.Height >= 480) + return; + + int subpoints = (2f * circularArcProperties.Radius <= 0.1f) ? 2 : Math.Max(2, (int)Math.Ceiling(circularArcProperties.ThetaRange / (2.0 * Math.Acos(1f - 0.1f / circularArcProperties.Radius)))); + + //ignore cases where subpoints is int.MaxValue, result will be garbage + //as well, having this many subpoints will cause an out of memory error, so can't happen during normal useage + if (subpoints == int.MaxValue) + return; + + for (int i = 0; i < Math.Min(subpoints, 100); i++) + { + float progress = (float)rng.NextDouble(); + + //To avoid errors from interpolating points, ensure we check only positions that would be subpoints. + progress = (float)Math.Ceiling(progress * (subpoints - 1)) / (subpoints - 1); + + //Special case - if few subpoints, ensure checking every single one rather than randomly + if (subpoints < 100) + progress = i / (float)(subpoints - 1); + + //edge points cause issue with interpolation, so ignore the last two points and first + if (progress == 0.0f || progress >= (subpoints - 2) / (float)(subpoints - 1)) + continue; + + double theta = circularArcProperties.ThetaStart + circularArcProperties.Direction * progress * circularArcProperties.ThetaRange; + Vector2 vector = new Vector2((float)Math.Cos(theta), (float)Math.Sin(theta)) * circularArcProperties.Radius; + + Assert.True(Precision.AlmostEquals(circularArcProperties.Centre + vector, path.PositionAt(progress), 0.01f), + "A perfect circle with points " + string.Join(", ", path.ControlPoints.Select(x => x.Position)) + " and radius" + circularArcProperties.Radius + "from SliderPath does not almost equal a theoretical perfect circle with " + subpoints + " subpoints" + + ": " + (circularArcProperties.Centre + vector) + " - " + path.PositionAt(progress) + + " = " + (circularArcProperties.Centre + vector - path.PositionAt(progress)) + ); + } + } + + private void scaleSlider(SliderPath path, Vector2 scale) + { + for (int i = 0; i < path.ControlPoints.Count; i++) + { + path.ControlPoints[i].Position *= scale; } } } From 172cfdf88d4fb6ffb1b4dd35c5183b272407629e Mon Sep 17 00:00:00 2001 From: Aurelian Date: Mon, 27 May 2024 19:41:38 +0200 Subject: [PATCH 0943/2493] Added missing brackets for formulas --- osu.Game.Rulesets.Osu.Tests/Editor/TestSliderScaling.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSliderScaling.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSliderScaling.cs index 2ffde0d3a3..52a170b84e 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSliderScaling.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSliderScaling.cs @@ -149,7 +149,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor if (boundingBox.Width >= 640 || boundingBox.Height >= 480) return; - int subpoints = (2f * circularArcProperties.Radius <= 0.1f) ? 2 : Math.Max(2, (int)Math.Ceiling(circularArcProperties.ThetaRange / (2.0 * Math.Acos(1f - 0.1f / circularArcProperties.Radius)))); + int subpoints = (2f * circularArcProperties.Radius <= 0.1f) ? 2 : Math.Max(2, (int)Math.Ceiling(circularArcProperties.ThetaRange / (2.0 * Math.Acos(1f - (0.1f / circularArcProperties.Radius))))); //ignore cases where subpoints is int.MaxValue, result will be garbage //as well, having this many subpoints will cause an out of memory error, so can't happen during normal useage @@ -171,7 +171,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor if (progress == 0.0f || progress >= (subpoints - 2) / (float)(subpoints - 1)) continue; - double theta = circularArcProperties.ThetaStart + circularArcProperties.Direction * progress * circularArcProperties.ThetaRange; + double theta = circularArcProperties.ThetaStart + (circularArcProperties.Direction * progress * circularArcProperties.ThetaRange); Vector2 vector = new Vector2((float)Math.Cos(theta), (float)Math.Sin(theta)) * circularArcProperties.Radius; Assert.True(Precision.AlmostEquals(circularArcProperties.Centre + vector, path.PositionAt(progress), 0.01f), From 6c4def1c097298abbda31be5338ba791cd12dc1d Mon Sep 17 00:00:00 2001 From: Aurelian Date: Mon, 27 May 2024 20:32:18 +0200 Subject: [PATCH 0944/2493] Added check for infinite subpoints for PerfectCurve --- osu.Game/Rulesets/Objects/SliderPath.cs | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/osu.Game/Rulesets/Objects/SliderPath.cs b/osu.Game/Rulesets/Objects/SliderPath.cs index eca14269fe..aa2570c336 100644 --- a/osu.Game/Rulesets/Objects/SliderPath.cs +++ b/osu.Game/Rulesets/Objects/SliderPath.cs @@ -330,11 +330,18 @@ namespace osu.Game.Rulesets.Objects if (subControlPoints.Length != 3) break; - //If a curve's theta range almost equals zero, the radius needed to have more than a - //floating point error difference is very large and results in a nearly straight path. - //Calculate it via a bezier aproximation instead. - //0.0005 corresponds with a radius of 8000 to have a more than 0.001 shift in the X value - if (Math.Abs(new CircularArcProperties(subControlPoints).ThetaRange) <= 0.0005d) + CircularArcProperties circularArcProperties = new CircularArcProperties(subControlPoints); + + //if false, we'll end up breaking anyways when calculating subPath + if (!circularArcProperties.IsValid) + break; + + //Coppied from PathApproximator.CircularArcToPiecewiseLinear + int subPoints = (2f * circularArcProperties.Radius <= 0.1f) ? 2 : Math.Max(2, (int)Math.Ceiling(circularArcProperties.ThetaRange / (2.0 * Math.Acos(1f - (0.1f / circularArcProperties.Radius))))); + + //if the amount of subpoints is int.MaxValue, causes an out of memory issue, so we default to bezier + //this only occurs for very large radii, so the result should be essentially a straight line anyways + if (subPoints == int.MaxValue) break; List subPath = PathApproximator.CircularArcToPiecewiseLinear(subControlPoints); From 96af0e1ec3c6c84ecd28178b5ae1b8ff6f4d3b43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 28 May 2024 13:07:11 +0200 Subject: [PATCH 0945/2493] Add failing test case for catch conversion Test is an abridged / cropped version of https://osu.ppy.sh/beatmapsets/971028#fruits/2062131 to demonstrate the specific failure case (unfortunately can't use the whole beatmap due to other conversion failures). --- .../CatchBeatmapConversionTest.cs | 1 + ...tiplier-precision-expected-conversion.json | 1 + .../high-speed-multiplier-precision.osu | 238 ++++++++++++++++++ 3 files changed, 240 insertions(+) create mode 100644 osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/high-speed-multiplier-precision-expected-conversion.json create mode 100644 osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/high-speed-multiplier-precision.osu diff --git a/osu.Game.Rulesets.Catch.Tests/CatchBeatmapConversionTest.cs b/osu.Game.Rulesets.Catch.Tests/CatchBeatmapConversionTest.cs index 81e0675aaa..f4c36d5188 100644 --- a/osu.Game.Rulesets.Catch.Tests/CatchBeatmapConversionTest.cs +++ b/osu.Game.Rulesets.Catch.Tests/CatchBeatmapConversionTest.cs @@ -54,6 +54,7 @@ namespace osu.Game.Rulesets.Catch.Tests [TestCase("3949367", new[] { typeof(CatchModDoubleTime), typeof(CatchModEasy) })] [TestCase("112643")] [TestCase("1041052", new[] { typeof(CatchModHardRock) })] + [TestCase("high-speed-multiplier-precision")] public new void Test(string name, params Type[] mods) => base.Test(name, mods); protected override IEnumerable CreateConvertValue(HitObject hitObject) diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/high-speed-multiplier-precision-expected-conversion.json b/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/high-speed-multiplier-precision-expected-conversion.json new file mode 100644 index 0000000000..a562074fe9 --- /dev/null +++ b/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/high-speed-multiplier-precision-expected-conversion.json @@ -0,0 +1 @@ +{"Mappings":[{"StartTime":265568.0,"Objects":[{"StartTime":265568.0,"Position":486.0,"HyperDash":false},{"StartTime":265658.0,"Position":465.1873,"HyperDash":false},{"StartTime":265749.0,"Position":463.208435,"HyperDash":false},{"StartTime":265840.0,"Position":465.146484,"HyperDash":false},{"StartTime":265967.0,"Position":459.5862,"HyperDash":false}]}]} \ No newline at end of file diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/high-speed-multiplier-precision.osu b/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/high-speed-multiplier-precision.osu new file mode 100644 index 0000000000..ff641d9b0a --- /dev/null +++ b/osu.Game.Rulesets.Catch.Tests/Resources/Testing/Beatmaps/high-speed-multiplier-precision.osu @@ -0,0 +1,238 @@ +osu file format v14 + +[General] +AudioFilename: audio.mp3 +AudioLeadIn: 0 +PreviewTime: 226943 +Countdown: 0 +SampleSet: Soft +StackLeniency: 0.7 +Mode: 2 +LetterboxInBreaks: 0 +WidescreenStoryboard: 1 + +[Editor] +Bookmarks: 85568,86768,90968,265568 +DistanceSpacing: 0.9 +BeatDivisor: 12 +GridSize: 16 +TimelineZoom: 1 + +[Metadata] +Title:Snow +TitleUnicode:Snow +Artist:Ricky Montgomery +ArtistUnicode:Ricky Montgomery +Creator:Crowley +Version:Bury Me Six Feet in Snow +Source: +Tags:indie the honeysticks alternative english +BeatmapID:2062131 +BeatmapSetID:971028 + +[Difficulty] +HPDrainRate:6 +CircleSize:4.2 +OverallDifficulty:8.3 +ApproachRate:8.3 +SliderMultiplier:3.59999990463257 +SliderTickRate:1 + +[Events] +//Background and Video events +0,0,"me.jpg",0,0 +//Break Periods +//Storyboard Layer 0 (Background) +//Storyboard Layer 1 (Fail) +//Storyboard Layer 2 (Pass) +//Storyboard Layer 3 (Foreground) +//Storyboard Layer 4 (Overlay) +//Storyboard Sound Samples + +[TimingPoints] +368,1200,2,2,1,30,1,0 +368,-66.6666666666667,2,2,1,30,0,0 +29168,-58.8235294117647,2,2,1,40,0,0 +30368,-58.8235294117647,2,2,2,40,0,0 +30568,-58.8235294117647,2,2,1,40,0,0 +31368,-58.8235294117647,2,2,2,40,0,0 +31568,-58.8235294117647,2,2,1,40,0,0 +32768,-58.8235294117647,2,2,2,40,0,0 +33568,-58.8235294117647,2,2,2,40,0,0 +33968,-58.8235294117647,2,2,1,40,0,0 +35168,-58.8235294117647,2,2,2,40,0,0 +35968,-58.8235294117647,2,2,1,40,0,0 +36168,-58.8235294117647,2,2,2,40,0,0 +36368,-58.8235294117647,2,2,1,40,0,0 +37568,-58.8235294117647,2,2,2,40,0,0 +37968,-58.8235294117647,2,2,1,40,0,0 +38368,-58.8235294117647,2,2,2,40,0,0 +38768,-58.8235294117647,2,2,1,40,0,0 +39968,-58.8235294117647,2,2,2,40,0,0 +40168,-58.8235294117647,2,2,1,40,0,0 +40968,-58.8235294117647,2,2,2,40,0,0 +41168,-58.8235294117647,2,2,1,40,0,0 +42368,-58.8235294117647,2,2,2,40,0,0 +43168,-58.8235294117647,2,2,2,40,0,0 +43568,-58.8235294117647,2,2,1,40,0,0 +44768,-58.8235294117647,2,2,2,40,0,0 +45768,-58.8235294117647,2,2,2,40,0,0 +45968,-58.8235294117647,2,2,1,50,0,0 +47168,-58.8235294117647,2,2,2,50,0,0 +48368,-62.5,2,2,1,50,0,0 +67568,-58.8235294117647,2,2,1,70,0,1 +84668,-58.8235294117647,2,2,1,5,0,1 +84768,-58.8235294117647,2,2,1,70,0,1 +85068,-58.8235294117647,2,2,1,5,0,1 +85168,-58.8235294117647,2,2,1,70,0,1 +85468,-58.8235294117647,2,2,1,5,0,1 +85568,-58.8235294117647,2,2,1,70,0,1 +86768,-58.8235294117647,2,2,1,30,0,0 +91168,-58.8235294117647,2,2,1,50,0,0 +91568,1200,2,2,1,50,1,0 +91568,-58.8235294117647,2,2,1,50,0,1 +91643,-58.8235294117647,2,2,1,50,0,0 +92768,-58.8235294117647,2,2,2,50,0,0 +92968,-58.8235294117647,2,2,1,50,0,0 +95168,-58.8235294117647,2,2,2,50,0,0 +95368,-58.8235294117647,2,2,1,50,0,0 +97568,-58.8235294117647,2,2,2,50,0,0 +97768,-58.8235294117647,2,2,1,50,0,0 +99968,-58.8235294117647,2,2,2,50,0,0 +100168,-58.8235294117647,2,2,1,50,0,0 +100768,-58.8235294117647,2,2,2,50,0,0 +101168,-58.8235294117647,2,2,1,50,0,0 +102368,-58.8235294117647,2,2,2,50,0,0 +102568,-58.8235294117647,2,2,1,50,0,0 +104768,-58.8235294117647,2,2,2,50,0,0 +104968,-58.8235294117647,2,2,1,50,0,0 +107168,-58.8235294117647,2,2,2,50,0,0 +107368,-58.8235294117647,2,2,1,50,0,0 +108968,-58.8235294117647,2,2,2,50,0,0 +109168,-58.8235294117647,2,2,1,50,0,0 +109568,-58.8235294117647,2,2,2,50,0,0 +109968,-58.8235294117647,2,2,1,50,0,0 +110368,-58.8235294117647,2,2,2,50,0,0 +110768,-100,2,2,1,40,0,0 +127568,-62.5,2,2,2,50,0,0 +127968,-62.5,2,2,1,50,0,0 +128168,-62.5,2,2,2,50,0,0 +129968,-58.8235294117647,2,2,1,50,0,0 +131168,-58.8235294117647,2,2,2,50,0,0 +131368,-58.8235294117647,2,2,1,50,0,0 +133568,-58.8235294117647,2,2,2,50,0,0 +133768,-58.8235294117647,2,2,1,50,0,0 +135968,-58.8235294117647,2,2,2,50,0,0 +136168,-58.8235294117647,2,2,1,50,0,0 +138368,-58.8235294117647,2,2,2,50,0,0 +138568,-58.8235294117647,2,2,1,50,0,0 +139168,-58.8235294117647,2,2,2,50,0,0 +139368,-58.8235294117647,2,2,1,50,0,0 +139568,-58.8235294117647,2,2,1,50,0,0 +140768,-58.8235294117647,2,2,2,50,0,0 +140968,-58.8235294117647,2,2,1,50,0,0 +143168,-58.8235294117647,2,2,2,50,0,0 +143368,-58.8235294117647,2,2,1,50,0,0 +145568,-58.8235294117647,2,2,2,50,0,0 +145768,-58.8235294117647,2,2,1,50,0,0 +147368,-58.8235294117647,2,2,2,50,0,0 +147768,-58.8235294117647,2,2,1,50,0,0 +147968,-58.8235294117647,2,2,1,60,0,0 +148768,-58.8235294117647,2,2,2,60,0,0 +149168,-58.8235294117647,2,2,1,70,0,1 +158268,-58.8235294117647,2,2,2,70,0,1 +158568,-58.8235294117647,2,2,1,70,0,1 +166268,-58.8235294117647,2,2,1,5,0,1 +166368,-58.8235294117647,2,2,1,70,0,1 +166668,-58.8235294117647,2,2,1,5,0,1 +166768,-58.8235294117647,2,2,1,70,0,1 +167068,-58.8235294117647,2,2,1,5,0,1 +167168,-58.8235294117647,2,2,1,70,0,1 +168368,-62.5,2,2,1,50,0,0 +172368,-62.5,2,2,1,50,0,1 +173168,-62.5,2,2,1,50,0,0 +185168,-62.5,2,2,1,60,0,0 +185468,-62.5,2,2,1,5,0,0 +185568,-62.5,2,2,1,60,0,0 +185868,-62.5,2,2,1,5,0,0 +185968,-62.5,2,2,1,60,0,0 +186268,-62.5,2,2,1,5,0,0 +186368,-62.5,2,2,1,60,0,0 +186668,-62.5,2,2,1,5,0,0 +186768,-52.6315789473684,2,2,1,60,0,0 +187068,-62.5,2,2,1,5,0,0 +187168,-62.5,2,2,1,60,0,0 +187468,-62.5,2,2,1,5,0,0 +187568,-62.5,2,2,1,20,0,0 +187768,-62.5,2,2,1,24,0,0 +187968,-62.5,2,2,1,28,0,0 +188168,-62.5,2,2,1,32,0,0 +188368,-62.5,2,2,1,36,0,0 +188568,-62.5,2,2,1,40,0,0 +188768,1200,2,2,1,50,1,1 +188768,-58.8235294117647,2,2,1,50,0,1 +188843,-58.8235294117647,2,2,1,50,0,0 +189968,-58.8235294117647,2,2,2,50,0,0 +190168,-58.8235294117647,2,2,1,50,0,0 +192368,-58.8235294117647,2,2,2,50,0,0 +192568,-58.8235294117647,2,2,1,50,0,0 +194768,-58.8235294117647,2,2,2,50,0,0 +194968,-58.8235294117647,2,2,1,50,0,0 +196568,-58.8235294117647,2,2,2,50,0,0 +196768,-58.8235294117647,2,2,1,50,0,0 +197168,-58.8235294117647,2,2,2,50,0,0 +197368,-58.8235294117647,2,2,1,50,0,0 +197568,-58.8235294117647,2,2,2,50,0,0 +197968,-58.8235294117647,2,2,1,50,0,0 +198368,-58.8235294117647,2,2,1,50,0,0 +199568,-58.8235294117647,2,2,2,50,0,0 +199768,-58.8235294117647,2,2,1,50,0,0 +201968,-58.8235294117647,2,2,2,50,0,0 +202168,-58.8235294117647,2,2,1,50,0,0 +204368,-58.8235294117647,2,2,2,50,0,0 +204568,-58.8235294117647,2,2,1,50,0,0 +206768,-58.8235294117647,2,2,1,60,0,0 +207168,-58.8235294117647,2,2,2,60,0,0 +207968,-58.8235294117647,2,2,1,70,0,1 +216968,-58.8235294117647,2,2,2,70,0,1 +217168,-58.8235294117647,2,2,1,70,0,1 +217368,-58.8235294117647,2,2,2,70,0,1 +217568,-58.8235294117647,2,2,1,70,0,1 +225068,-58.8235294117647,2,2,1,5,0,1 +225168,-58.8235294117647,2,2,1,70,0,1 +225468,-58.8235294117647,2,2,1,5,0,1 +225568,-58.8235294117647,2,2,1,70,0,1 +225868,-58.8235294117647,2,2,1,5,0,1 +225968,-58.8235294117647,2,2,1,70,0,1 +227168,-58.8235294117647,2,2,1,30,0,0 +234368,-58.8235294117647,2,2,1,40,0,0 +236768,-58.8235294117647,2,2,1,70,0,1 +255968,-58.8235294117647,2,2,1,70,0,1 +261168,-58.8235294117647,2,2,1,70,0,1 +263068,-58.8235294117647,2,2,1,70,0,0 +263168,-58.8235294117647,2,2,1,60,0,1 +263243,-58.8235294117647,2,2,1,60,0,0 +264368,-58.8235294117647,2,2,1,60,0,1 +264443,-58.8235294117647,2,2,1,60,0,0 +265568,-444.444444444444,2,2,1,50,0,1 +265643,-444.444444444444,2,2,1,50,0,0 +266768,-444.444444444444,2,2,1,40,0,0 +267968,-444.444444444444,2,2,1,30,0,0 +269168,-444.444444444444,2,2,1,20,0,0 +270368,-444.444444444444,2,2,1,10,0,0 +271168,-444.444444444444,2,2,1,9,0,0 +271568,-444.444444444444,2,2,1,8,0,0 +271968,-444.444444444444,2,2,1,7,0,0 +272368,-444.444444444444,2,2,1,6,0,0 +272768,-444.444444444444,2,2,1,5,0,0 +275168,-444.444444444444,2,2,1,5,0,0 + + +[Colours] +Combo1 : 255,128,128 +Combo2 : 72,72,255 +Combo3 : 192,192,192 +Combo4 : 255,136,79 + +[HitObjects] +486,179,265568,6,0,P|461:174|454:174,1,26.999997997284,6|0,1:2|0:0,0:0:0:0: From a3b849375101c5ff4461fb78b92adb9e290af03a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 28 May 2024 13:07:13 +0200 Subject: [PATCH 0946/2493] Remove rounding of slider velocity multiplier on juice streams Compare: https://github.com/ppy/osu/pull/26616 This came up elsewhere, namely in https://github.com/ppy/osu/pull/28277#issuecomment-2133505958. As it turns out, at least one beatmap among those whose scores had unexpected changes in total score, namely https://osu.ppy.sh/beatmapsets/971028#fruits/2062131, was using slider velocity multipliers that were not a multiple of 0.01 (the specific value used was 0.225x). This meant that due to the rounding applied to `SliderVelocityMultiplierBindable` via `Precision`, the raw value was being incorrectly rounded, resulting in incorrect conversion. The "direct" change that revealed this is most likely https://github.com/ppy/osu-framework/pull/6249, by the virtue of shuffling the `BindableNumber` rounding code around and accidentally changing midpoint rounding semantics in the process. But it was not at fault here, as rounding was just as wrong before that change as after in this specific context. --- osu.Game.Rulesets.Catch/Objects/JuiceStream.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs index 671291ef0e..dade129038 100644 --- a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs +++ b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs @@ -29,7 +29,6 @@ namespace osu.Game.Rulesets.Catch.Objects public BindableNumber SliderVelocityMultiplierBindable { get; } = new BindableDouble(1) { - Precision = 0.01, MinValue = 0.1, MaxValue = 10 }; From c2e7cdfdccf1aaa83c5e9ab78cdf778547580951 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 28 May 2024 21:29:29 +0900 Subject: [PATCH 0947/2493] 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 1f241c6db5..3a20dd2fdb 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -10,7 +10,7 @@ true - + diff --git a/osu.iOS.props b/osu.iOS.props index eba9abd3b8..2f64fcefa5 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -23,6 +23,6 @@ iossimulator-x64 - + From bf0040447cfaefb6f5a56a399391ba18242f2549 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 28 May 2024 15:24:37 +0200 Subject: [PATCH 0948/2493] Fix legacy mania note body animation not resetting sometimes Hopefully closes https://github.com/ppy/osu/issues/28284. As far as I can tell this is a somewhat difficult one to reproduce because it relies on a specific set of circumstances (at least the reproduction case that I found does). The reset to frame 0 would previously be called explicitly when `isHitting` changed: https://github.com/ppy/osu/blob/182ca145c78432f4b832c8ea407e107dfeaaa8ad/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyBodyPiece.cs#L144 However, it can be the case that `bodyAnimation` is not loaded at the point of this call. This is significant because `SkinnableTextureAnimation` contains this logic: https://github.com/ppy/osu/blob/182ca145c78432f4b832c8ea407e107dfeaaa8ad/osu.Game/Skinning/LegacySkinExtensions.cs#L192-L211 which cannot be moved any earlier (because any earlier the `Clock` may no longer be correct), and also causes the animation to be seeked forward while it is stopped. I can't figure out a decent way to layer this otherwise (by scheduling or whatever), so this commit is just applying the nuclear option of just seeking back to frame 0 on every update frame in which the body piece is not being hit. --- osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyBodyPiece.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyBodyPiece.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyBodyPiece.cs index a8200e0144..00054f6be2 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyBodyPiece.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyBodyPiece.cs @@ -140,10 +140,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy private void onIsHittingChanged(ValueChangedEvent isHitting) { if (bodySprite is TextureAnimation bodyAnimation) - { - bodyAnimation.GotoFrame(0); bodyAnimation.IsPlaying = isHitting.NewValue; - } if (lightContainer == null) return; @@ -219,6 +216,9 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy { base.Update(); + if (!isHitting.Value) + (bodySprite as TextureAnimation)?.GotoFrame(0); + if (holdNote.Body.HasHoldBreak) missFadeTime.Value = holdNote.Body.Result.TimeAbsolute; From 36453f621513ee8e7ad5971f3bc7c1a39404d700 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Tue, 28 May 2024 15:56:59 +0200 Subject: [PATCH 0949/2493] Change scale hotkey to Ctrl+T --- 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 394cb98089..fd8e6fd6d0 100644 --- a/osu.Game/Input/Bindings/GlobalActionContainer.cs +++ b/osu.Game/Input/Bindings/GlobalActionContainer.cs @@ -142,7 +142,7 @@ namespace osu.Game.Input.Bindings new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.MouseWheelRight }, GlobalAction.EditorCyclePreviousBeatSnapDivisor), new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.MouseWheelLeft }, GlobalAction.EditorCycleNextBeatSnapDivisor), new KeyBinding(new[] { InputKey.Control, InputKey.R }, GlobalAction.EditorToggleRotateControl), - new KeyBinding(new[] { InputKey.S }, GlobalAction.EditorToggleScaleControl), + new KeyBinding(new[] { InputKey.Control, InputKey.T }, GlobalAction.EditorToggleScaleControl), }; private static IEnumerable inGameKeyBindings => new[] From a89ba33b475a4f73167f0c81e53eec8f082c16ec Mon Sep 17 00:00:00 2001 From: OliBomby Date: Tue, 28 May 2024 16:14:16 +0200 Subject: [PATCH 0950/2493] rename CanScaleSelectionOrigin/PlayfieldOrigin to make clear its not the origin being scaled --- .../Edit/OsuSelectionRotationHandler.cs | 4 ++-- osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs | 4 ++-- osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs | 2 +- osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs | 2 +- osu.Game.Rulesets.Osu/Edit/TransformToolboxGroup.cs | 10 +++++----- .../Visual/Editing/TestSceneComposeSelectBox.cs | 2 +- .../SkinEditor/SkinSelectionRotationHandler.cs | 2 +- .../Screens/Edit/Compose/Components/SelectionBox.cs | 2 +- .../Compose/Components/SelectionRotationHandler.cs | 4 ++-- .../Edit/Compose/Components/SelectionScaleHandler.cs | 4 ++-- 10 files changed, 18 insertions(+), 18 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionRotationHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionRotationHandler.cs index d48bc6a90b..b581e3fdea 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionRotationHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionRotationHandler.cs @@ -41,8 +41,8 @@ namespace osu.Game.Rulesets.Osu.Edit private void updateState() { var quad = GeometryUtils.GetSurroundingQuad(selectedMovableObjects); - CanRotateSelectionOrigin.Value = quad.Width > 0 || quad.Height > 0; - CanRotatePlayfieldOrigin.Value = selectedMovableObjects.Any(); + CanRotateFromSelectionOrigin.Value = quad.Width > 0 || quad.Height > 0; + CanRotateFromPlayfieldOrigin.Value = selectedMovableObjects.Any(); } private OsuHitObject[]? objectsInRotation; diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs index e45494977f..a9cbc1b8f1 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs @@ -53,8 +53,8 @@ namespace osu.Game.Rulesets.Osu.Edit CanScaleX.Value = quad.Width > 0; CanScaleY.Value = quad.Height > 0; CanScaleDiagonally.Value = CanScaleX.Value && CanScaleY.Value; - CanScaleSelectionOrigin.Value = CanScaleX.Value || CanScaleY.Value; - CanScalePlayfieldOrigin.Value = selectedMovableObjects.Any(); + CanScaleFromSelectionOrigin.Value = CanScaleX.Value || CanScaleY.Value; + CanScaleFromPlayfieldOrigin.Value = selectedMovableObjects.Any(); } private Dictionary? objectsInScale; diff --git a/osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs b/osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs index 812d622ae5..ea6b28b215 100644 --- a/osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs +++ b/osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs @@ -86,7 +86,7 @@ namespace osu.Game.Rulesets.Osu.Edit angleInput.Current.BindValueChanged(angle => rotationInfo.Value = rotationInfo.Value with { Degrees = angle.NewValue }); rotationOrigin.Items.First().Select(); - rotationHandler.CanRotateSelectionOrigin.BindValueChanged(e => + rotationHandler.CanRotateFromSelectionOrigin.BindValueChanged(e => { selectionCentreButton.Selected.Disabled = !e.NewValue; }, true); diff --git a/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs b/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs index b76d778b3d..124a79390a 100644 --- a/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs +++ b/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs @@ -118,7 +118,7 @@ namespace osu.Game.Rulesets.Osu.Edit xCheckBox.Current.BindValueChanged(x => setAxis(x.NewValue, yCheckBox.Current.Value)); yCheckBox.Current.BindValueChanged(y => setAxis(xCheckBox.Current.Value, y.NewValue)); - scaleHandler.CanScaleSelectionOrigin.BindValueChanged(e => + scaleHandler.CanScaleFromSelectionOrigin.BindValueChanged(e => { selectionCentreButton.Selected.Disabled = !e.NewValue; }, true); diff --git a/osu.Game.Rulesets.Osu/Edit/TransformToolboxGroup.cs b/osu.Game.Rulesets.Osu/Edit/TransformToolboxGroup.cs index e1f53846dc..67baf7d165 100644 --- a/osu.Game.Rulesets.Osu/Edit/TransformToolboxGroup.cs +++ b/osu.Game.Rulesets.Osu/Edit/TransformToolboxGroup.cs @@ -64,15 +64,15 @@ namespace osu.Game.Rulesets.Osu.Edit base.LoadComplete(); // aggregate two values into canRotate - canRotatePlayfieldOrigin = RotationHandler.CanRotatePlayfieldOrigin.GetBoundCopy(); + canRotatePlayfieldOrigin = RotationHandler.CanRotateFromPlayfieldOrigin.GetBoundCopy(); canRotatePlayfieldOrigin.BindValueChanged(_ => updateCanRotateAggregate()); - canRotateSelectionOrigin = RotationHandler.CanRotateSelectionOrigin.GetBoundCopy(); + canRotateSelectionOrigin = RotationHandler.CanRotateFromSelectionOrigin.GetBoundCopy(); canRotateSelectionOrigin.BindValueChanged(_ => updateCanRotateAggregate()); void updateCanRotateAggregate() { - canRotate.Value = RotationHandler.CanRotatePlayfieldOrigin.Value || RotationHandler.CanRotateSelectionOrigin.Value; + canRotate.Value = RotationHandler.CanRotateFromPlayfieldOrigin.Value || RotationHandler.CanRotateFromSelectionOrigin.Value; } // aggregate three values into canScale @@ -82,12 +82,12 @@ namespace osu.Game.Rulesets.Osu.Edit canScaleY = ScaleHandler.CanScaleY.GetBoundCopy(); canScaleY.BindValueChanged(_ => updateCanScaleAggregate()); - canScalePlayfieldOrigin = ScaleHandler.CanScalePlayfieldOrigin.GetBoundCopy(); + canScalePlayfieldOrigin = ScaleHandler.CanScaleFromPlayfieldOrigin.GetBoundCopy(); canScalePlayfieldOrigin.BindValueChanged(_ => updateCanScaleAggregate()); void updateCanScaleAggregate() { - canScale.Value = ScaleHandler.CanScaleX.Value || ScaleHandler.CanScaleY.Value || ScaleHandler.CanScalePlayfieldOrigin.Value; + canScale.Value = ScaleHandler.CanScaleX.Value || ScaleHandler.CanScaleY.Value || ScaleHandler.CanScaleFromPlayfieldOrigin.Value; } // bindings to `Enabled` on the buttons are decoupled on purpose diff --git a/osu.Game.Tests/Visual/Editing/TestSceneComposeSelectBox.cs b/osu.Game.Tests/Visual/Editing/TestSceneComposeSelectBox.cs index 6bd6c4a8c4..d12f7ebde9 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneComposeSelectBox.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneComposeSelectBox.cs @@ -69,7 +69,7 @@ namespace osu.Game.Tests.Visual.Editing { this.getTargetContainer = getTargetContainer; - CanRotateSelectionOrigin.Value = true; + CanRotateFromSelectionOrigin.Value = true; } [CanBeNull] diff --git a/osu.Game/Overlays/SkinEditor/SkinSelectionRotationHandler.cs b/osu.Game/Overlays/SkinEditor/SkinSelectionRotationHandler.cs index 7ecf116b68..3a3eb9457b 100644 --- a/osu.Game/Overlays/SkinEditor/SkinSelectionRotationHandler.cs +++ b/osu.Game/Overlays/SkinEditor/SkinSelectionRotationHandler.cs @@ -41,7 +41,7 @@ namespace osu.Game.Overlays.SkinEditor private void updateState() { - CanRotateSelectionOrigin.Value = selectedItems.Count > 0; + CanRotateFromSelectionOrigin.Value = selectedItems.Count > 0; } private Drawable[]? objectsInRotation; diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs index 31a5c30fff..9f709f8c64 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs @@ -127,7 +127,7 @@ namespace osu.Game.Screens.Edit.Compose.Components private void load() { if (rotationHandler != null) - canRotate.BindTo(rotationHandler.CanRotateSelectionOrigin); + canRotate.BindTo(rotationHandler.CanRotateFromSelectionOrigin); if (scaleHandler != null) { diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionRotationHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionRotationHandler.cs index 459e4b0c41..8c35dc07b7 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionRotationHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionRotationHandler.cs @@ -15,12 +15,12 @@ namespace osu.Game.Screens.Edit.Compose.Components /// /// Whether rotation anchored by the selection origin can currently be performed. /// - public Bindable CanRotateSelectionOrigin { get; private set; } = new BindableBool(); + public Bindable CanRotateFromSelectionOrigin { get; private set; } = new BindableBool(); /// /// Whether rotation anchored by the center of the playfield can currently be performed. /// - public Bindable CanRotatePlayfieldOrigin { get; private set; } = new BindableBool(); + public Bindable CanRotateFromPlayfieldOrigin { get; private set; } = new BindableBool(); /// /// Performs a single, instant, atomic rotation operation. diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionScaleHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionScaleHandler.cs index 495cce7ad6..b72c3406f1 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionScaleHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionScaleHandler.cs @@ -35,12 +35,12 @@ namespace osu.Game.Screens.Edit.Compose.Components /// /// Whether scaling anchored by the selection origin can currently be performed. /// - public Bindable CanScaleSelectionOrigin { get; private set; } = new BindableBool(); + public Bindable CanScaleFromSelectionOrigin { get; private set; } = new BindableBool(); /// /// Whether scaling anchored by the center of the playfield can currently be performed. /// - public Bindable CanScalePlayfieldOrigin { get; private set; } = new BindableBool(); + public Bindable CanScaleFromPlayfieldOrigin { get; private set; } = new BindableBool(); public Quad? OriginalSurroundingQuad { get; protected set; } From 8eb23f8a604818b6ce94dc0810a551c2a5455a4d Mon Sep 17 00:00:00 2001 From: OliBomby Date: Tue, 28 May 2024 16:19:57 +0200 Subject: [PATCH 0951/2493] remove redundant CanScaleFromSelectionOrigin --- .../Edit/OsuSelectionScaleHandler.cs | 1 - .../Edit/PreciseScalePopover.cs | 16 ++++++++++++---- .../Compose/Components/SelectionScaleHandler.cs | 5 ----- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs index a9cbc1b8f1..f120c8bd75 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs @@ -53,7 +53,6 @@ namespace osu.Game.Rulesets.Osu.Edit CanScaleX.Value = quad.Width > 0; CanScaleY.Value = quad.Height > 0; CanScaleDiagonally.Value = CanScaleX.Value && CanScaleY.Value; - CanScaleFromSelectionOrigin.Value = CanScaleX.Value || CanScaleY.Value; CanScaleFromPlayfieldOrigin.Value = selectedMovableObjects.Any(); } diff --git a/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs b/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs index 124a79390a..dca262cf5a 100644 --- a/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs +++ b/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs @@ -32,6 +32,9 @@ namespace osu.Game.Rulesets.Osu.Edit private OsuCheckbox xCheckBox = null!; private OsuCheckbox yCheckBox = null!; + private Bindable canScaleX = null!; + private Bindable canScaleY = null!; + public PreciseScalePopover(SelectionScaleHandler scaleHandler) { this.scaleHandler = scaleHandler; @@ -118,10 +121,15 @@ namespace osu.Game.Rulesets.Osu.Edit xCheckBox.Current.BindValueChanged(x => setAxis(x.NewValue, yCheckBox.Current.Value)); yCheckBox.Current.BindValueChanged(y => setAxis(xCheckBox.Current.Value, y.NewValue)); - scaleHandler.CanScaleFromSelectionOrigin.BindValueChanged(e => - { - selectionCentreButton.Selected.Disabled = !e.NewValue; - }, true); + // aggregate two values into canScaleFromSelectionCentre + canScaleX = scaleHandler.CanScaleX.GetBoundCopy(); + canScaleX.BindValueChanged(_ => updateCanScaleFromSelectionCentre()); + + canScaleY = scaleHandler.CanScaleY.GetBoundCopy(); + canScaleY.BindValueChanged(_ => updateCanScaleFromSelectionCentre(), true); + + void updateCanScaleFromSelectionCentre() => + selectionCentreButton.Selected.Disabled = !(scaleHandler.CanScaleY.Value || scaleHandler.CanScaleFromPlayfieldOrigin.Value); scaleInfo.BindValueChanged(scale => { diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionScaleHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionScaleHandler.cs index b72c3406f1..2c8b413560 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionScaleHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionScaleHandler.cs @@ -32,11 +32,6 @@ namespace osu.Game.Screens.Edit.Compose.Components /// public Bindable CanScaleDiagonally { get; private set; } = new BindableBool(); - /// - /// Whether scaling anchored by the selection origin can currently be performed. - /// - public Bindable CanScaleFromSelectionOrigin { get; private set; } = new BindableBool(); - /// /// Whether scaling anchored by the center of the playfield can currently be performed. /// From 7cdc755c1614ba666d63d3eba5d8062a9313e1a4 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Tue, 28 May 2024 16:57:24 +0200 Subject: [PATCH 0952/2493] Bind axis checkbox disabled state to CanScaleX/Y --- .../Edit/PreciseScalePopover.cs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs b/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs index dca262cf5a..ac6e2849c9 100644 --- a/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs +++ b/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs @@ -35,6 +35,8 @@ namespace osu.Game.Rulesets.Osu.Edit private Bindable canScaleX = null!; private Bindable canScaleY = null!; + private bool scaleInProgress; + public PreciseScalePopover(SelectionScaleHandler scaleHandler) { this.scaleHandler = scaleHandler; @@ -124,15 +126,26 @@ namespace osu.Game.Rulesets.Osu.Edit // aggregate two values into canScaleFromSelectionCentre canScaleX = scaleHandler.CanScaleX.GetBoundCopy(); canScaleX.BindValueChanged(_ => updateCanScaleFromSelectionCentre()); + canScaleX.BindValueChanged(e => updateAxisCheckBoxEnabled(e.NewValue, xCheckBox.Current), true); canScaleY = scaleHandler.CanScaleY.GetBoundCopy(); canScaleY.BindValueChanged(_ => updateCanScaleFromSelectionCentre(), true); + canScaleY.BindValueChanged(e => updateAxisCheckBoxEnabled(e.NewValue, yCheckBox.Current), true); void updateCanScaleFromSelectionCentre() => selectionCentreButton.Selected.Disabled = !(scaleHandler.CanScaleY.Value || scaleHandler.CanScaleFromPlayfieldOrigin.Value); + void updateAxisCheckBoxEnabled(bool enabled, Bindable current) + { + current.Disabled = false; // enable the bindable to allow setting the value + current.Value = enabled; + current.Disabled = !enabled; + } + scaleInfo.BindValueChanged(scale => { + if (!scaleInProgress) return; + var newScale = new Vector2(scale.NewValue.XAxis ? scale.NewValue.Scale : 1, scale.NewValue.YAxis ? scale.NewValue.Scale : 1); scaleHandler.Update(newScale, getOriginPosition(scale.NewValue)); }); @@ -172,6 +185,7 @@ namespace osu.Game.Rulesets.Osu.Edit { base.PopIn(); scaleHandler.Begin(); + scaleInProgress = true; updateMaxScale(); } @@ -180,7 +194,10 @@ namespace osu.Game.Rulesets.Osu.Edit base.PopOut(); if (IsLoaded) + { scaleHandler.Commit(); + scaleInProgress = false; + } } } From d143a697d2efbd7d57fc7d7ff7d3d4e3cfc13a7d Mon Sep 17 00:00:00 2001 From: OliBomby Date: Tue, 28 May 2024 17:12:16 +0200 Subject: [PATCH 0953/2493] refactor CanScaleFromPlayfieldOrigin and GetClampedScale to derived class --- osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs | 2 +- .../Edit/OsuSelectionScaleHandler.cs | 13 ++++++++++++- osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs | 5 ++--- osu.Game.Rulesets.Osu/Edit/TransformToolboxGroup.cs | 2 +- .../Compose/Components/SelectionScaleHandler.cs | 13 ------------- 5 files changed, 16 insertions(+), 19 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs index 6f3ed9730e..cc1d1fe89f 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs @@ -104,7 +104,7 @@ namespace osu.Game.Rulesets.Osu.Edit new TransformToolboxGroup { RotationHandler = BlueprintContainer.SelectionHandler.RotationHandler, - ScaleHandler = BlueprintContainer.SelectionHandler.ScaleHandler, + ScaleHandler = (OsuSelectionScaleHandler)BlueprintContainer.SelectionHandler.ScaleHandler, }, FreehandlSliderToolboxGroup } diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs index f120c8bd75..32057bd7fe 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs @@ -24,6 +24,11 @@ namespace osu.Game.Rulesets.Osu.Edit { public partial class OsuSelectionScaleHandler : SelectionScaleHandler { + /// + /// Whether scaling anchored by the center of the playfield can currently be performed. + /// + public Bindable CanScaleFromPlayfieldOrigin { get; private set; } = new BindableBool(); + [Resolved] private IEditorChangeHandler? changeHandler { get; set; } @@ -156,7 +161,13 @@ namespace osu.Game.Rulesets.Osu.Edit return (xInBounds, yInBounds); } - public override Vector2 GetClampedScale(Vector2 scale, Vector2? origin = null) + /// + /// Clamp scale for multi-object-scaling where selection does not exceed playfield bounds or flip. + /// + /// The origin from which the scale operation is performed + /// The scale to be clamped + /// The clamped scale vector + public Vector2 GetClampedScale(Vector2 scale, Vector2? origin = null) { //todo: this is not always correct for selections involving sliders. This approximation assumes each point is scaled independently, but sliderends move with the sliderhead. if (objectsInScale == null) diff --git a/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs b/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs index ac6e2849c9..f14e166d3b 100644 --- a/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs +++ b/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs @@ -12,14 +12,13 @@ using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Rulesets.Osu.UI; using osu.Game.Screens.Edit.Components.RadioButtons; -using osu.Game.Screens.Edit.Compose.Components; using osuTK; namespace osu.Game.Rulesets.Osu.Edit { public partial class PreciseScalePopover : OsuPopover { - private readonly SelectionScaleHandler scaleHandler; + private readonly OsuSelectionScaleHandler scaleHandler; private readonly Bindable scaleInfo = new Bindable(new PreciseScaleInfo(1, ScaleOrigin.PlayfieldCentre, true, true)); @@ -37,7 +36,7 @@ namespace osu.Game.Rulesets.Osu.Edit private bool scaleInProgress; - public PreciseScalePopover(SelectionScaleHandler scaleHandler) + public PreciseScalePopover(OsuSelectionScaleHandler scaleHandler) { this.scaleHandler = scaleHandler; diff --git a/osu.Game.Rulesets.Osu/Edit/TransformToolboxGroup.cs b/osu.Game.Rulesets.Osu/Edit/TransformToolboxGroup.cs index 67baf7d165..e70be8d93c 100644 --- a/osu.Game.Rulesets.Osu/Edit/TransformToolboxGroup.cs +++ b/osu.Game.Rulesets.Osu/Edit/TransformToolboxGroup.cs @@ -32,7 +32,7 @@ namespace osu.Game.Rulesets.Osu.Edit private Bindable canScalePlayfieldOrigin = null!; public SelectionRotationHandler RotationHandler { get; init; } = null!; - public SelectionScaleHandler ScaleHandler { get; init; } = null!; + public OsuSelectionScaleHandler ScaleHandler { get; init; } = null!; public TransformToolboxGroup() : base("transform") diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionScaleHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionScaleHandler.cs index 2c8b413560..a96f627e56 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionScaleHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionScaleHandler.cs @@ -32,21 +32,8 @@ namespace osu.Game.Screens.Edit.Compose.Components /// public Bindable CanScaleDiagonally { get; private set; } = new BindableBool(); - /// - /// Whether scaling anchored by the center of the playfield can currently be performed. - /// - public Bindable CanScaleFromPlayfieldOrigin { get; private set; } = new BindableBool(); - public Quad? OriginalSurroundingQuad { get; protected set; } - /// - /// Clamp scale where selection does not exceed playfield bounds or flip. - /// - /// The origin from which the scale operation is performed - /// The scale to be clamped - /// The clamped scale vector - public virtual Vector2 GetClampedScale(Vector2 scale, Vector2? origin = null) => scale; - /// /// Performs a single, instant, atomic scale operation. /// From 9548585b15fe4154e7e7da80d21da9779486e898 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Tue, 28 May 2024 17:24:31 +0200 Subject: [PATCH 0954/2493] fix axis checkboxes being disabled in playfield origin scale --- .../Edit/PreciseScalePopover.cs | 35 +++++++++++++------ 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs b/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs index f14e166d3b..d45c4020b9 100644 --- a/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs +++ b/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs @@ -125,21 +125,14 @@ namespace osu.Game.Rulesets.Osu.Edit // aggregate two values into canScaleFromSelectionCentre canScaleX = scaleHandler.CanScaleX.GetBoundCopy(); canScaleX.BindValueChanged(_ => updateCanScaleFromSelectionCentre()); - canScaleX.BindValueChanged(e => updateAxisCheckBoxEnabled(e.NewValue, xCheckBox.Current), true); + canScaleX.BindValueChanged(e => updateAxisCheckBoxesEnabled()); canScaleY = scaleHandler.CanScaleY.GetBoundCopy(); canScaleY.BindValueChanged(_ => updateCanScaleFromSelectionCentre(), true); - canScaleY.BindValueChanged(e => updateAxisCheckBoxEnabled(e.NewValue, yCheckBox.Current), true); + canScaleY.BindValueChanged(e => updateAxisCheckBoxesEnabled(), true); void updateCanScaleFromSelectionCentre() => - selectionCentreButton.Selected.Disabled = !(scaleHandler.CanScaleY.Value || scaleHandler.CanScaleFromPlayfieldOrigin.Value); - - void updateAxisCheckBoxEnabled(bool enabled, Bindable current) - { - current.Disabled = false; // enable the bindable to allow setting the value - current.Value = enabled; - current.Disabled = !enabled; - } + selectionCentreButton.Selected.Disabled = !(scaleHandler.CanScaleX.Value || scaleHandler.CanScaleY.Value); scaleInfo.BindValueChanged(scale => { @@ -150,6 +143,27 @@ namespace osu.Game.Rulesets.Osu.Edit }); } + private void updateAxisCheckBoxesEnabled() + { + if (scaleInfo.Value.Origin == ScaleOrigin.PlayfieldCentre) + { + setBindableEnabled(true, xCheckBox.Current); + setBindableEnabled(true, yCheckBox.Current); + } + else + { + setBindableEnabled(canScaleX.Value, xCheckBox.Current); + setBindableEnabled(canScaleY.Value, yCheckBox.Current); + } + } + + private void setBindableEnabled(bool enabled, Bindable current) + { + current.Disabled = false; // enable the bindable to allow setting the value + current.Value = enabled; + current.Disabled = !enabled; + } + private void updateMaxScale() { if (!scaleHandler.OriginalSurroundingQuad.HasValue) @@ -170,6 +184,7 @@ namespace osu.Game.Rulesets.Osu.Edit { scaleInfo.Value = scaleInfo.Value with { Origin = origin }; updateMaxScale(); + updateAxisCheckBoxesEnabled(); } private Vector2? getOriginPosition(PreciseScaleInfo scale) => scale.Origin == ScaleOrigin.PlayfieldCentre ? OsuPlayfield.BASE_SIZE / 2 : null; From 9a18ba2699883a0278adba889b2141b3e21f044b Mon Sep 17 00:00:00 2001 From: OliBomby Date: Tue, 28 May 2024 18:27:01 +0200 Subject: [PATCH 0955/2493] disable playfield centre origin when scaling slider and simplify logic --- .../Edit/OsuSelectionScaleHandler.cs | 6 +++ .../Edit/PreciseScalePopover.cs | 42 +++++++------------ 2 files changed, 21 insertions(+), 27 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs index 32057bd7fe..9d1c3bc78f 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs @@ -29,6 +29,11 @@ namespace osu.Game.Rulesets.Osu.Edit /// public Bindable CanScaleFromPlayfieldOrigin { get; private set; } = new BindableBool(); + /// + /// Whether a single slider is currently selected, which results in a different scaling behaviour. + /// + public Bindable IsScalingSlider { get; private set; } = new BindableBool(); + [Resolved] private IEditorChangeHandler? changeHandler { get; set; } @@ -59,6 +64,7 @@ namespace osu.Game.Rulesets.Osu.Edit CanScaleY.Value = quad.Height > 0; CanScaleDiagonally.Value = CanScaleX.Value && CanScaleY.Value; CanScaleFromPlayfieldOrigin.Value = selectedMovableObjects.Any(); + IsScalingSlider.Value = selectedMovableObjects.Count() == 1 && selectedMovableObjects.First() is Slider; } private Dictionary? objectsInScale; diff --git a/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs b/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs index d45c4020b9..b792baf428 100644 --- a/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs +++ b/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs @@ -26,16 +26,12 @@ namespace osu.Game.Rulesets.Osu.Edit private BindableNumber scaleInputBindable = null!; private EditorRadioButtonCollection scaleOrigin = null!; + private RadioButton playfieldCentreButton = null!; private RadioButton selectionCentreButton = null!; private OsuCheckbox xCheckBox = null!; private OsuCheckbox yCheckBox = null!; - private Bindable canScaleX = null!; - private Bindable canScaleY = null!; - - private bool scaleInProgress; - public PreciseScalePopover(OsuSelectionScaleHandler scaleHandler) { this.scaleHandler = scaleHandler; @@ -70,7 +66,7 @@ namespace osu.Game.Rulesets.Osu.Edit RelativeSizeAxes = Axes.X, Items = new[] { - new RadioButton("Playfield centre", + playfieldCentreButton = new RadioButton("Playfield centre", () => setOrigin(ScaleOrigin.PlayfieldCentre), () => new SpriteIcon { Icon = FontAwesome.Regular.Square }), selectionCentreButton = new RadioButton("Selection centre", @@ -101,6 +97,10 @@ namespace osu.Game.Rulesets.Osu.Edit }, } }; + playfieldCentreButton.Selected.DisabledChanged += isDisabled => + { + playfieldCentreButton.TooltipText = isDisabled ? "Select something other than a single slider to perform playfield-based scaling." : string.Empty; + }; selectionCentreButton.Selected.DisabledChanged += isDisabled => { selectionCentreButton.TooltipText = isDisabled ? "Select more than one object to perform selection-based scaling." : string.Empty; @@ -117,27 +117,20 @@ namespace osu.Game.Rulesets.Osu.Edit scaleInput.SelectAll(); }); scaleInput.Current.BindValueChanged(scale => scaleInfo.Value = scaleInfo.Value with { Scale = scale.NewValue }); - scaleOrigin.Items.First().Select(); xCheckBox.Current.BindValueChanged(x => setAxis(x.NewValue, yCheckBox.Current.Value)); yCheckBox.Current.BindValueChanged(y => setAxis(xCheckBox.Current.Value, y.NewValue)); - // aggregate two values into canScaleFromSelectionCentre - canScaleX = scaleHandler.CanScaleX.GetBoundCopy(); - canScaleX.BindValueChanged(_ => updateCanScaleFromSelectionCentre()); - canScaleX.BindValueChanged(e => updateAxisCheckBoxesEnabled()); + selectionCentreButton.Selected.Disabled = !(scaleHandler.CanScaleX.Value || scaleHandler.CanScaleY.Value); + playfieldCentreButton.Selected.Disabled = scaleHandler.IsScalingSlider.Value && !selectionCentreButton.Selected.Disabled; - canScaleY = scaleHandler.CanScaleY.GetBoundCopy(); - canScaleY.BindValueChanged(_ => updateCanScaleFromSelectionCentre(), true); - canScaleY.BindValueChanged(e => updateAxisCheckBoxesEnabled(), true); - - void updateCanScaleFromSelectionCentre() => - selectionCentreButton.Selected.Disabled = !(scaleHandler.CanScaleX.Value || scaleHandler.CanScaleY.Value); + if (playfieldCentreButton.Selected.Disabled) + scaleOrigin.Items.Last().Select(); + else + scaleOrigin.Items.First().Select(); scaleInfo.BindValueChanged(scale => { - if (!scaleInProgress) return; - var newScale = new Vector2(scale.NewValue.XAxis ? scale.NewValue.Scale : 1, scale.NewValue.YAxis ? scale.NewValue.Scale : 1); scaleHandler.Update(newScale, getOriginPosition(scale.NewValue)); }); @@ -152,8 +145,8 @@ namespace osu.Game.Rulesets.Osu.Edit } else { - setBindableEnabled(canScaleX.Value, xCheckBox.Current); - setBindableEnabled(canScaleY.Value, yCheckBox.Current); + setBindableEnabled(scaleHandler.CanScaleX.Value, xCheckBox.Current); + setBindableEnabled(scaleHandler.CanScaleY.Value, yCheckBox.Current); } } @@ -199,7 +192,6 @@ namespace osu.Game.Rulesets.Osu.Edit { base.PopIn(); scaleHandler.Begin(); - scaleInProgress = true; updateMaxScale(); } @@ -207,11 +199,7 @@ namespace osu.Game.Rulesets.Osu.Edit { base.PopOut(); - if (IsLoaded) - { - scaleHandler.Commit(); - scaleInProgress = false; - } + if (IsLoaded) scaleHandler.Commit(); } } From 203e9284eb5eb54d464e7a290e106b605b7cf66e Mon Sep 17 00:00:00 2001 From: OliBomby Date: Tue, 28 May 2024 19:01:53 +0200 Subject: [PATCH 0956/2493] End circle only gets brighter once shift is held --- .../Sliders/Components/SliderTailPiece.cs | 29 ++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderTailPiece.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderTailPiece.cs index 5cf9346f2e..dc57d6d7ca 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderTailPiece.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderTailPiece.cs @@ -61,11 +61,38 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components updateCirclePieceColour(); } + protected override bool OnKeyDown(KeyDownEvent e) + { + if (e.Repeat) + return false; + + handleDragToggle(e); + return base.OnKeyDown(e); + } + + protected override void OnKeyUp(KeyUpEvent e) + { + handleDragToggle(e); + base.OnKeyUp(e); + } + + private bool lastShiftPressed; + + private void handleDragToggle(KeyboardEvent key) + { + bool shiftPressed = key.ShiftPressed; + + if (shiftPressed == lastShiftPressed) return; + + lastShiftPressed = shiftPressed; + updateCirclePieceColour(); + } + private void updateCirclePieceColour() { Color4 colour = colours.Yellow; - if (IsHovered) + if (IsHovered && inputManager.CurrentState.Keyboard.ShiftPressed) colour = colour.Lighten(1); CirclePiece.Colour = colour; From b74f66e3351457ef7233c277c88b18db7e174f2a Mon Sep 17 00:00:00 2001 From: Aurelian Date: Tue, 28 May 2024 19:38:33 +0200 Subject: [PATCH 0957/2493] SliderBall's rotation updates based on CurvePositionAt --- .../Objects/Drawables/DrawableSliderBall.cs | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderBall.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderBall.cs index 46f0231981..24c0d0fcf0 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderBall.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderBall.cs @@ -10,9 +10,7 @@ using osu.Framework.Graphics.Containers; 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; namespace osu.Game.Rulesets.Osu.Objects.Drawables { @@ -63,22 +61,20 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables base.ApplyTransformsAt(time, false); } - private Vector2? lastPosition; - public void UpdateProgress(double completionProgress) { - Position = drawableSlider.HitObject.CurvePositionAt(completionProgress); + Slider slider = drawableSlider.HitObject; + Position = slider.CurvePositionAt(completionProgress); - var diff = lastPosition.HasValue ? lastPosition.Value - Position : Position - drawableSlider.HitObject.CurvePositionAt(completionProgress + 0.01f); - - bool rewinding = (Clock as IGameplayClock)?.IsRewinding == true; + //0.1 / slider.Path.Distance is the additional progress needed to ensure the diff length is 0.1 + var diff = slider.CurvePositionAt(completionProgress) - slider.CurvePositionAt(Math.Min(1, completionProgress + 0.1 / slider.Path.Distance)); // Ensure the value is substantially high enough to allow for Atan2 to get a valid angle. + // Needed for when near completion, or in case of a very short slider. if (diff.LengthFast < 0.01f) return; - ball.Rotation = -90 + (float)(-Math.Atan2(diff.X, diff.Y) * 180 / Math.PI) + (rewinding ? 180 : 0); - lastPosition = Position; + ball.Rotation = -90 + (float)(-Math.Atan2(diff.X, diff.Y) * 180 / Math.PI); } } } From 542809a748072f8438b9289027bafd018a33c5f3 Mon Sep 17 00:00:00 2001 From: Aurelian Date: Wed, 29 May 2024 09:39:46 +0200 Subject: [PATCH 0958/2493] Reduced subpoints limit to be a more practical value --- osu.Game/Rulesets/Objects/SliderPath.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/osu.Game/Rulesets/Objects/SliderPath.cs b/osu.Game/Rulesets/Objects/SliderPath.cs index aa2570c336..730a2013b0 100644 --- a/osu.Game/Rulesets/Objects/SliderPath.cs +++ b/osu.Game/Rulesets/Objects/SliderPath.cs @@ -339,9 +339,10 @@ namespace osu.Game.Rulesets.Objects //Coppied from PathApproximator.CircularArcToPiecewiseLinear int subPoints = (2f * circularArcProperties.Radius <= 0.1f) ? 2 : Math.Max(2, (int)Math.Ceiling(circularArcProperties.ThetaRange / (2.0 * Math.Acos(1f - (0.1f / circularArcProperties.Radius))))); - //if the amount of subpoints is int.MaxValue, causes an out of memory issue, so we default to bezier - //this only occurs for very large radii, so the result should be essentially a straight line anyways - if (subPoints == int.MaxValue) + //theoretically can be int.MaxValue, but lets set this to a lower value anyways + //1000 requires an arc length of over 20 thousand to surpass this limit, which should be safe. + //See here for calculations https://www.desmos.com/calculator/210bwswkbb + if (subPoints >= 1000) break; List subPath = PathApproximator.CircularArcToPiecewiseLinear(subControlPoints); From 4c881b56331626feb5272e0b33442ec388765acf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 29 May 2024 09:40:29 +0200 Subject: [PATCH 0959/2493] Use better name if we're renaming --- osu.Game.Rulesets.Osu/Edit/OsuSelectionRotationHandler.cs | 4 ++-- osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs | 2 +- osu.Game.Rulesets.Osu/Edit/TransformToolboxGroup.cs | 6 +++--- osu.Game.Tests/Visual/Editing/TestSceneComposeSelectBox.cs | 2 +- .../Overlays/SkinEditor/SkinSelectionRotationHandler.cs | 2 +- osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs | 2 +- .../Edit/Compose/Components/SelectionRotationHandler.cs | 4 ++-- 7 files changed, 11 insertions(+), 11 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionRotationHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionRotationHandler.cs index b581e3fdea..7624b2f27e 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionRotationHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionRotationHandler.cs @@ -41,8 +41,8 @@ namespace osu.Game.Rulesets.Osu.Edit private void updateState() { var quad = GeometryUtils.GetSurroundingQuad(selectedMovableObjects); - CanRotateFromSelectionOrigin.Value = quad.Width > 0 || quad.Height > 0; - CanRotateFromPlayfieldOrigin.Value = selectedMovableObjects.Any(); + CanRotateAroundSelectionOrigin.Value = quad.Width > 0 || quad.Height > 0; + CanRotateAroundPlayfieldOrigin.Value = selectedMovableObjects.Any(); } private OsuHitObject[]? objectsInRotation; diff --git a/osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs b/osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs index ea6b28b215..3a0d3c4763 100644 --- a/osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs +++ b/osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs @@ -86,7 +86,7 @@ namespace osu.Game.Rulesets.Osu.Edit angleInput.Current.BindValueChanged(angle => rotationInfo.Value = rotationInfo.Value with { Degrees = angle.NewValue }); rotationOrigin.Items.First().Select(); - rotationHandler.CanRotateFromSelectionOrigin.BindValueChanged(e => + rotationHandler.CanRotateAroundSelectionOrigin.BindValueChanged(e => { selectionCentreButton.Selected.Disabled = !e.NewValue; }, true); diff --git a/osu.Game.Rulesets.Osu/Edit/TransformToolboxGroup.cs b/osu.Game.Rulesets.Osu/Edit/TransformToolboxGroup.cs index e70be8d93c..4da1593fb7 100644 --- a/osu.Game.Rulesets.Osu/Edit/TransformToolboxGroup.cs +++ b/osu.Game.Rulesets.Osu/Edit/TransformToolboxGroup.cs @@ -64,15 +64,15 @@ namespace osu.Game.Rulesets.Osu.Edit base.LoadComplete(); // aggregate two values into canRotate - canRotatePlayfieldOrigin = RotationHandler.CanRotateFromPlayfieldOrigin.GetBoundCopy(); + canRotatePlayfieldOrigin = RotationHandler.CanRotateAroundPlayfieldOrigin.GetBoundCopy(); canRotatePlayfieldOrigin.BindValueChanged(_ => updateCanRotateAggregate()); - canRotateSelectionOrigin = RotationHandler.CanRotateFromSelectionOrigin.GetBoundCopy(); + canRotateSelectionOrigin = RotationHandler.CanRotateAroundSelectionOrigin.GetBoundCopy(); canRotateSelectionOrigin.BindValueChanged(_ => updateCanRotateAggregate()); void updateCanRotateAggregate() { - canRotate.Value = RotationHandler.CanRotateFromPlayfieldOrigin.Value || RotationHandler.CanRotateFromSelectionOrigin.Value; + canRotate.Value = RotationHandler.CanRotateAroundPlayfieldOrigin.Value || RotationHandler.CanRotateAroundSelectionOrigin.Value; } // aggregate three values into canScale diff --git a/osu.Game.Tests/Visual/Editing/TestSceneComposeSelectBox.cs b/osu.Game.Tests/Visual/Editing/TestSceneComposeSelectBox.cs index d12f7ebde9..79a808bbd2 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneComposeSelectBox.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneComposeSelectBox.cs @@ -69,7 +69,7 @@ namespace osu.Game.Tests.Visual.Editing { this.getTargetContainer = getTargetContainer; - CanRotateFromSelectionOrigin.Value = true; + CanRotateAroundSelectionOrigin.Value = true; } [CanBeNull] diff --git a/osu.Game/Overlays/SkinEditor/SkinSelectionRotationHandler.cs b/osu.Game/Overlays/SkinEditor/SkinSelectionRotationHandler.cs index 3a3eb9457b..6a118a73a8 100644 --- a/osu.Game/Overlays/SkinEditor/SkinSelectionRotationHandler.cs +++ b/osu.Game/Overlays/SkinEditor/SkinSelectionRotationHandler.cs @@ -41,7 +41,7 @@ namespace osu.Game.Overlays.SkinEditor private void updateState() { - CanRotateFromSelectionOrigin.Value = selectedItems.Count > 0; + CanRotateAroundSelectionOrigin.Value = selectedItems.Count > 0; } private Drawable[]? objectsInRotation; diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs index 9f709f8c64..fec3224fad 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs @@ -127,7 +127,7 @@ namespace osu.Game.Screens.Edit.Compose.Components private void load() { if (rotationHandler != null) - canRotate.BindTo(rotationHandler.CanRotateFromSelectionOrigin); + canRotate.BindTo(rotationHandler.CanRotateAroundSelectionOrigin); if (scaleHandler != null) { diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionRotationHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionRotationHandler.cs index 8c35dc07b7..787716a38c 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionRotationHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionRotationHandler.cs @@ -15,12 +15,12 @@ namespace osu.Game.Screens.Edit.Compose.Components /// /// Whether rotation anchored by the selection origin can currently be performed. /// - public Bindable CanRotateFromSelectionOrigin { get; private set; } = new BindableBool(); + public Bindable CanRotateAroundSelectionOrigin { get; private set; } = new BindableBool(); /// /// Whether rotation anchored by the center of the playfield can currently be performed. /// - public Bindable CanRotateFromPlayfieldOrigin { get; private set; } = new BindableBool(); + public Bindable CanRotateAroundPlayfieldOrigin { get; private set; } = new BindableBool(); /// /// Performs a single, instant, atomic rotation operation. From 4a8273b6ed8defecef61bcf6e77a937372a1de4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 29 May 2024 09:43:09 +0200 Subject: [PATCH 0960/2493] Rename another method --- osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs | 4 ++-- osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs index 9d1c3bc78f..de00aa6ad3 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs @@ -104,7 +104,7 @@ namespace osu.Game.Rulesets.Osu.Edit } else { - scale = GetClampedScale(scale, actualOrigin); + scale = ClampScaleToPlayfieldBounds(scale, actualOrigin); foreach (var (ho, originalState) in objectsInScale) { @@ -173,7 +173,7 @@ namespace osu.Game.Rulesets.Osu.Edit /// The origin from which the scale operation is performed /// The scale to be clamped /// The clamped scale vector - public Vector2 GetClampedScale(Vector2 scale, Vector2? origin = null) + public Vector2 ClampScaleToPlayfieldBounds(Vector2 scale, Vector2? origin = null) { //todo: this is not always correct for selections involving sliders. This approximation assumes each point is scaled independently, but sliderends move with the sliderhead. if (objectsInScale == null) diff --git a/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs b/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs index b792baf428..b7202b9310 100644 --- a/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs +++ b/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs @@ -163,7 +163,7 @@ namespace osu.Game.Rulesets.Osu.Edit return; const float max_scale = 10; - var scale = scaleHandler.GetClampedScale(new Vector2(max_scale), getOriginPosition(scaleInfo.Value)); + var scale = scaleHandler.ClampScaleToPlayfieldBounds(new Vector2(max_scale), getOriginPosition(scaleInfo.Value)); if (!scaleInfo.Value.XAxis) scale.X = max_scale; From bd5060965f4a9e0fcbed9fc6044126e66e5d36d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 29 May 2024 09:49:16 +0200 Subject: [PATCH 0961/2493] Simplify toolbox button enable logic --- .../Edit/TransformToolboxGroup.cs | 38 +++++-------------- 1 file changed, 9 insertions(+), 29 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/TransformToolboxGroup.cs b/osu.Game.Rulesets.Osu/Edit/TransformToolboxGroup.cs index 4da1593fb7..278d38b2d9 100644 --- a/osu.Game.Rulesets.Osu/Edit/TransformToolboxGroup.cs +++ b/osu.Game.Rulesets.Osu/Edit/TransformToolboxGroup.cs @@ -18,8 +18,8 @@ namespace osu.Game.Rulesets.Osu.Edit { public partial class TransformToolboxGroup : EditorToolboxGroup, IKeyBindingHandler { - private readonly Bindable canRotate = new BindableBool(); - private readonly Bindable canScale = new BindableBool(); + private readonly AggregateBindable canRotate = new AggregateBindable((x, y) => x || y); + private readonly AggregateBindable canScale = new AggregateBindable((x, y) => x || y); private EditorToolButton rotateButton = null!; private EditorToolButton scaleButton = null!; @@ -63,37 +63,17 @@ namespace osu.Game.Rulesets.Osu.Edit { base.LoadComplete(); - // aggregate two values into canRotate - canRotatePlayfieldOrigin = RotationHandler.CanRotateAroundPlayfieldOrigin.GetBoundCopy(); - canRotatePlayfieldOrigin.BindValueChanged(_ => updateCanRotateAggregate()); + canRotate.AddSource(RotationHandler.CanRotateAroundPlayfieldOrigin); + canRotate.AddSource(RotationHandler.CanRotateAroundSelectionOrigin); - canRotateSelectionOrigin = RotationHandler.CanRotateAroundSelectionOrigin.GetBoundCopy(); - canRotateSelectionOrigin.BindValueChanged(_ => updateCanRotateAggregate()); - - void updateCanRotateAggregate() - { - canRotate.Value = RotationHandler.CanRotateAroundPlayfieldOrigin.Value || RotationHandler.CanRotateAroundSelectionOrigin.Value; - } - - // aggregate three values into canScale - canScaleX = ScaleHandler.CanScaleX.GetBoundCopy(); - canScaleX.BindValueChanged(_ => updateCanScaleAggregate()); - - canScaleY = ScaleHandler.CanScaleY.GetBoundCopy(); - canScaleY.BindValueChanged(_ => updateCanScaleAggregate()); - - canScalePlayfieldOrigin = ScaleHandler.CanScaleFromPlayfieldOrigin.GetBoundCopy(); - canScalePlayfieldOrigin.BindValueChanged(_ => updateCanScaleAggregate()); - - void updateCanScaleAggregate() - { - canScale.Value = ScaleHandler.CanScaleX.Value || ScaleHandler.CanScaleY.Value || ScaleHandler.CanScaleFromPlayfieldOrigin.Value; - } + canScale.AddSource(ScaleHandler.CanScaleX); + canScale.AddSource(ScaleHandler.CanScaleY); + canScale.AddSource(ScaleHandler.CanScaleFromPlayfieldOrigin); // bindings to `Enabled` on the buttons are decoupled on purpose // due to the weird `OsuButton` behaviour of resetting `Enabled` to `false` when `Action` is set. - canRotate.BindValueChanged(_ => rotateButton.Enabled.Value = canRotate.Value, true); - canScale.BindValueChanged(_ => scaleButton.Enabled.Value = canScale.Value, true); + canRotate.Result.BindValueChanged(rotate => rotateButton.Enabled.Value = rotate.NewValue, true); + canScale.Result.BindValueChanged(scale => scaleButton.Enabled.Value = scale.NewValue, true); } public bool OnPressed(KeyBindingPressEvent e) From 96a8bdf92064f04c00120a3274b451cf9ebd1ea9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 29 May 2024 09:59:19 +0200 Subject: [PATCH 0962/2493] Use more generic tooltip copy --- osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs b/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs index b7202b9310..ac1b40e235 100644 --- a/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs +++ b/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs @@ -99,11 +99,11 @@ namespace osu.Game.Rulesets.Osu.Edit }; playfieldCentreButton.Selected.DisabledChanged += isDisabled => { - playfieldCentreButton.TooltipText = isDisabled ? "Select something other than a single slider to perform playfield-based scaling." : string.Empty; + playfieldCentreButton.TooltipText = isDisabled ? "The current selection cannot be scaled relative to playfield centre." : string.Empty; }; selectionCentreButton.Selected.DisabledChanged += isDisabled => { - selectionCentreButton.TooltipText = isDisabled ? "Select more than one object to perform selection-based scaling." : string.Empty; + selectionCentreButton.TooltipText = isDisabled ? "The current selection cannot be scaled relative to its centre." : string.Empty; }; } From ba4073735649eaf4a581ce3f0ae1f566fab38ddc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 29 May 2024 10:01:04 +0200 Subject: [PATCH 0963/2493] Simplify logic --- osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs b/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs index ac1b40e235..d3e1b491b0 100644 --- a/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs +++ b/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs @@ -124,10 +124,7 @@ namespace osu.Game.Rulesets.Osu.Edit selectionCentreButton.Selected.Disabled = !(scaleHandler.CanScaleX.Value || scaleHandler.CanScaleY.Value); playfieldCentreButton.Selected.Disabled = scaleHandler.IsScalingSlider.Value && !selectionCentreButton.Selected.Disabled; - if (playfieldCentreButton.Selected.Disabled) - scaleOrigin.Items.Last().Select(); - else - scaleOrigin.Items.First().Select(); + scaleOrigin.Items.First(b => !b.Selected.Disabled).Select(); scaleInfo.BindValueChanged(scale => { From 9bd4b0d61303fb0dde3892d51b595174a61a2b4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 29 May 2024 10:04:49 +0200 Subject: [PATCH 0964/2493] Rename method --- .../Edit/PreciseScalePopover.cs | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs b/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs index d3e1b491b0..a299eebbce 100644 --- a/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs +++ b/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs @@ -137,21 +137,23 @@ namespace osu.Game.Rulesets.Osu.Edit { if (scaleInfo.Value.Origin == ScaleOrigin.PlayfieldCentre) { - setBindableEnabled(true, xCheckBox.Current); - setBindableEnabled(true, yCheckBox.Current); + toggleAxisAvailable(xCheckBox.Current, true); + toggleAxisAvailable(yCheckBox.Current, true); } else { - setBindableEnabled(scaleHandler.CanScaleX.Value, xCheckBox.Current); - setBindableEnabled(scaleHandler.CanScaleY.Value, yCheckBox.Current); + toggleAxisAvailable(xCheckBox.Current, scaleHandler.CanScaleX.Value); + toggleAxisAvailable(yCheckBox.Current, scaleHandler.CanScaleY.Value); } } - private void setBindableEnabled(bool enabled, Bindable current) + private void toggleAxisAvailable(Bindable axisBindable, bool available) { - current.Disabled = false; // enable the bindable to allow setting the value - current.Value = enabled; - current.Disabled = !enabled; + // enable the bindable to allow setting the value + axisBindable.Disabled = false; + // restore the presumed default value given the axis's new availability state + axisBindable.Value = available; + axisBindable.Disabled = !available; } private void updateMaxScale() From 9477e3b67de6f33a6866d9761b14a2cda20785b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 29 May 2024 10:14:47 +0200 Subject: [PATCH 0965/2493] Change editor scale hotkey to Ctrl-E Forgot that Ctrl-T was taken by the game-global toolbar already, so it wasn't working. --- 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 fd8e6fd6d0..2af564d8ba 100644 --- a/osu.Game/Input/Bindings/GlobalActionContainer.cs +++ b/osu.Game/Input/Bindings/GlobalActionContainer.cs @@ -142,7 +142,7 @@ namespace osu.Game.Input.Bindings new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.MouseWheelRight }, GlobalAction.EditorCyclePreviousBeatSnapDivisor), new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.MouseWheelLeft }, GlobalAction.EditorCycleNextBeatSnapDivisor), new KeyBinding(new[] { InputKey.Control, InputKey.R }, GlobalAction.EditorToggleRotateControl), - new KeyBinding(new[] { InputKey.Control, InputKey.T }, GlobalAction.EditorToggleScaleControl), + new KeyBinding(new[] { InputKey.Control, InputKey.E }, GlobalAction.EditorToggleScaleControl), }; private static IEnumerable inGameKeyBindings => new[] From 84513343d6789cf38bd2d8e355a233e277382f19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 29 May 2024 10:18:22 +0200 Subject: [PATCH 0966/2493] Remove unused fields --- osu.Game.Rulesets.Osu/Edit/TransformToolboxGroup.cs | 7 ------- 1 file changed, 7 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/TransformToolboxGroup.cs b/osu.Game.Rulesets.Osu/Edit/TransformToolboxGroup.cs index 278d38b2d9..28d0f8320f 100644 --- a/osu.Game.Rulesets.Osu/Edit/TransformToolboxGroup.cs +++ b/osu.Game.Rulesets.Osu/Edit/TransformToolboxGroup.cs @@ -24,13 +24,6 @@ namespace osu.Game.Rulesets.Osu.Edit private EditorToolButton rotateButton = null!; private EditorToolButton scaleButton = null!; - private Bindable canRotatePlayfieldOrigin = null!; - private Bindable canRotateSelectionOrigin = null!; - - private Bindable canScaleX = null!; - private Bindable canScaleY = null!; - private Bindable canScalePlayfieldOrigin = null!; - public SelectionRotationHandler RotationHandler { get; init; } = null!; public OsuSelectionScaleHandler ScaleHandler { get; init; } = null!; From 22a2adb5e6ca899487d0a09f3edbaade37c6338b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 29 May 2024 10:57:30 +0200 Subject: [PATCH 0967/2493] Revert unrelated changes --- osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs | 2 +- osu.Game.Rulesets.Osu/Objects/Slider.cs | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs index 0c7ba180f2..73c061afbd 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs @@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables public const float DEFAULT_TICK_SIZE = 16; - public DrawableSlider DrawableSlider => (DrawableSlider)ParentHitObject; + protected DrawableSlider DrawableSlider => (DrawableSlider)ParentHitObject; private SkinnableDrawable scaleContainer; diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs index 248f40208a..cc3ffd376e 100644 --- a/osu.Game.Rulesets.Osu/Objects/Slider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs @@ -248,8 +248,6 @@ namespace osu.Game.Rulesets.Osu.Objects if (TailCircle != null) TailCircle.Position = EndPosition; - - // Positions of other nested hitobjects are not updated } protected void UpdateNestedSamples() From a6c776dac891203fa591426dc4c086eefcdf499c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 29 May 2024 11:11:43 +0200 Subject: [PATCH 0968/2493] Use hopefully safer implementation of anchoring judgements to objects --- .../TestSceneDrawableJudgement.cs | 2 +- .../Objects/Drawables/DrawableOsuJudgement.cs | 14 +++---------- .../Rulesets/Judgements/DrawableJudgement.cs | 20 ++++++++++++------- 3 files changed, 17 insertions(+), 19 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneDrawableJudgement.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneDrawableJudgement.cs index 5f5596cbb3..a239f671af 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneDrawableJudgement.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneDrawableJudgement.cs @@ -108,7 +108,7 @@ namespace osu.Game.Rulesets.Osu.Tests private partial class TestDrawableOsuJudgement : DrawableOsuJudgement { public new SkinnableSprite Lighting => base.Lighting; - public new SkinnableDrawable JudgementBody => base.JudgementBody; + public new SkinnableDrawable? JudgementBody => base.JudgementBody; } } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs index ffbf45291f..98fb99609a 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.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.Game.Configuration; @@ -14,10 +12,10 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { public partial class DrawableOsuJudgement : DrawableJudgement { - internal SkinnableLighting Lighting { get; private set; } + internal SkinnableLighting Lighting { get; private set; } = null!; [Resolved] - private OsuConfigManager config { get; set; } + private OsuConfigManager config { get; set; } = null!; [BackgroundDependencyLoader] private void load() @@ -38,19 +36,13 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables Lighting.ResetAnimation(); Lighting.SetColourFrom(JudgedObject, Result); - - if (JudgedObject is DrawableOsuHitObject osuObject) - { - Position = osuObject.ToSpaceOfOtherDrawable(osuObject.OriginPosition, Parent!); - Scale = new Vector2(osuObject.HitObject.Scale); - } } protected override void Update() { base.Update(); - if (JudgedObject is DrawableOsuHitObject osuObject && Parent != null && osuObject.HitObject != null) + if (JudgedObject is DrawableOsuHitObject osuObject && JudgedObject.IsInUse) { Position = osuObject.ToSpaceOfOtherDrawable(osuObject.OriginPosition, Parent!); Scale = new Vector2(osuObject.HitObject.Scale); diff --git a/osu.Game/Rulesets/Judgements/DrawableJudgement.cs b/osu.Game/Rulesets/Judgements/DrawableJudgement.cs index b4686c52f3..37a9766b71 100644 --- a/osu.Game/Rulesets/Judgements/DrawableJudgement.cs +++ b/osu.Game/Rulesets/Judgements/DrawableJudgement.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. -#nullable disable - using System; using System.Diagnostics; -using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -24,13 +21,13 @@ namespace osu.Game.Rulesets.Judgements { private const float judgement_size = 128; - public JudgementResult Result { get; private set; } + public JudgementResult? Result { get; private set; } - public DrawableHitObject JudgedObject { get; private set; } + public DrawableHitObject? JudgedObject { get; private set; } public override bool RemoveCompletedTransforms => false; - protected SkinnableDrawable JudgementBody { get; private set; } + protected SkinnableDrawable? JudgementBody { get; private set; } private readonly Container aboveHitObjectsContent; @@ -97,12 +94,19 @@ namespace osu.Game.Rulesets.Judgements /// /// The applicable judgement. /// The drawable object. - public void Apply([NotNull] JudgementResult result, [CanBeNull] DrawableHitObject judgedObject) + public void Apply(JudgementResult result, DrawableHitObject? judgedObject) { Result = result; JudgedObject = judgedObject; } + protected override void FreeAfterUse() + { + base.FreeAfterUse(); + + JudgedObject = null; + } + protected override void PrepareForUse() { base.PrepareForUse(); @@ -121,6 +125,8 @@ namespace osu.Game.Rulesets.Judgements ApplyTransformsAt(double.MinValue, true); ClearTransforms(true); + Debug.Assert(Result != null && JudgementBody != null); + LifetimeStart = Result.TimeAbsolute; using (BeginAbsoluteSequence(Result.TimeAbsolute)) From cc136556175b4c2e854aca51d3aebe35f0e5069f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 29 May 2024 13:31:15 +0200 Subject: [PATCH 0969/2493] Derive API response version from game version (Or local date, in the case of non-deployed builds). Came up when I was looking at https://github.com/ppy/osu-web/pull/11240 and found that we were still hardcoding this. Thankfully, this *should not* cause issues, since there don't seem to be any (documented or undocumented) API response version checks for versions newer than 20220705 in osu-web master. For clarity and possible debugging needs, the API response version is also logged. --- osu.Game/Online/API/APIAccess.cs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/osu.Game/Online/API/APIAccess.cs b/osu.Game/Online/API/APIAccess.cs index d3707fe74d..6f84e98d2c 100644 --- a/osu.Game/Online/API/APIAccess.cs +++ b/osu.Game/Online/API/APIAccess.cs @@ -42,7 +42,7 @@ namespace osu.Game.Online.API public string WebsiteRootUrl { get; } - public int APIVersion => 20220705; // We may want to pull this from the game version eventually. + public int APIVersion { get; } public Exception LastLoginError { get; private set; } @@ -84,12 +84,23 @@ namespace osu.Game.Online.API this.config = config; this.versionHash = versionHash; + if (game.IsDeployedBuild) + APIVersion = game.AssemblyVersion.Major * 10000 + game.AssemblyVersion.Minor; + else + { + var now = DateTimeOffset.Now; + APIVersion = now.Year * 10000 + now.Month * 100 + now.Day; + } + APIEndpointUrl = endpointConfiguration.APIEndpointUrl; WebsiteRootUrl = endpointConfiguration.WebsiteRootUrl; NotificationsClient = setUpNotificationsClient(); authentication = new OAuth(endpointConfiguration.APIClientID, endpointConfiguration.APIClientSecret, APIEndpointUrl); + log = Logger.GetLogger(LoggingTarget.Network); + log.Add($@"API endpoint root: {APIEndpointUrl}"); + log.Add($@"API request version: {APIVersion}"); ProvidedUsername = config.Get(OsuSetting.Username); From ab01fa6d4572af4239527c0090ae438f549f55c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 29 May 2024 13:34:12 +0200 Subject: [PATCH 0970/2493] Add xmldoc to `APIAccess.APIVersion` --- osu.Game/Online/API/APIAccess.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/Online/API/APIAccess.cs b/osu.Game/Online/API/APIAccess.cs index 6f84e98d2c..923f841bd8 100644 --- a/osu.Game/Online/API/APIAccess.cs +++ b/osu.Game/Online/API/APIAccess.cs @@ -42,6 +42,10 @@ namespace osu.Game.Online.API public string WebsiteRootUrl { get; } + /// + /// The API response version. + /// See: https://osu.ppy.sh/docs/index.html#api-versions + /// public int APIVersion { get; } public Exception LastLoginError { get; private set; } From d2c86b0813d41ddd01ce4cbc00906f94271bfbf2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 29 May 2024 21:32:52 +0900 Subject: [PATCH 0971/2493] Avoid passing beatmap in from editor when it's already present --- osu.Game/Screens/Edit/Editor.cs | 2 +- .../Screens/Edit/GameplayTest/EditorPlayer.cs | 32 +++++++++++-------- .../Edit/GameplayTest/EditorPlayerLoader.cs | 5 ++- 3 files changed, 21 insertions(+), 18 deletions(-) diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 2954d7dcaa..07c32983f5 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -452,7 +452,7 @@ namespace osu.Game.Screens.Edit pushEditorPlayer(); } - void pushEditorPlayer() => this.Push(new EditorPlayerLoader(this, playableBeatmap)); + void pushEditorPlayer() => this.Push(new EditorPlayerLoader(this)); } /// diff --git a/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs b/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs index 42eb57c253..c327ae185d 100644 --- a/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs +++ b/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs @@ -21,19 +21,17 @@ namespace osu.Game.Screens.Edit.GameplayTest { private readonly Editor editor; private readonly EditorState editorState; - private readonly IBeatmap playableBeatmap; protected override UserActivity InitialActivity => new UserActivity.TestingBeatmap(Beatmap.Value.BeatmapInfo); [Resolved] private MusicController musicController { get; set; } = null!; - public EditorPlayer(Editor editor, IBeatmap playableBeatmap) + public EditorPlayer(Editor editor) : base(new PlayerConfiguration { ShowResults = false }) { this.editor = editor; editorState = editor.GetState(); - this.playableBeatmap = playableBeatmap; } protected override GameplayClockContainer CreateGameplayClockContainer(WorkingBeatmap beatmap, double gameplayStart) @@ -52,17 +50,7 @@ namespace osu.Game.Screens.Edit.GameplayTest { base.LoadComplete(); - var frame = new ReplayFrame { Header = new FrameHeader(new ScoreInfo(), new ScoreProcessorStatistics()) }; - - foreach (var hitObject in enumerateHitObjects(playableBeatmap.HitObjects.Where(h => h.StartTime < editorState.Time))) - { - var judgement = hitObject.CreateJudgement(); - frame.Header.Statistics.TryAdd(judgement.MaxResult, 0); - frame.Header.Statistics[judgement.MaxResult]++; - } - - HealthProcessor.ResetFromReplayFrame(frame); - ScoreProcessor.ResetFromReplayFrame(frame); + markPreviousObjectsHit(); ScoreProcessor.HasCompleted.BindValueChanged(completed => { @@ -75,6 +63,22 @@ namespace osu.Game.Screens.Edit.GameplayTest }, RESULTS_DISPLAY_DELAY); } }); + } + + private void markPreviousObjectsHit() + { + var frame = new ReplayFrame { Header = new FrameHeader(new ScoreInfo(), new ScoreProcessorStatistics()) }; + + foreach (var hitObject in enumerateHitObjects(DrawableRuleset.Objects.Where(h => h.StartTime < editorState.Time))) + { + var judgement = hitObject.CreateJudgement(); + + frame.Header.Statistics.TryAdd(judgement.MaxResult, 0); + frame.Header.Statistics[judgement.MaxResult]++; + } + + HealthProcessor.ResetFromReplayFrame(frame); + ScoreProcessor.ResetFromReplayFrame(frame); static IEnumerable enumerateHitObjects(IEnumerable hitObjects) { diff --git a/osu.Game/Screens/Edit/GameplayTest/EditorPlayerLoader.cs b/osu.Game/Screens/Edit/GameplayTest/EditorPlayerLoader.cs index c62b8cafb8..bb151e4a45 100644 --- a/osu.Game/Screens/Edit/GameplayTest/EditorPlayerLoader.cs +++ b/osu.Game/Screens/Edit/GameplayTest/EditorPlayerLoader.cs @@ -4,7 +4,6 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Screens; -using osu.Game.Beatmaps; using osu.Game.Screens.Menu; using osu.Game.Screens.Play; @@ -15,8 +14,8 @@ namespace osu.Game.Screens.Edit.GameplayTest [Resolved] private OsuLogo osuLogo { get; set; } = null!; - public EditorPlayerLoader(Editor editor, IBeatmap playableBeatmap) - : base(() => new EditorPlayer(editor, playableBeatmap)) + public EditorPlayerLoader(Editor editor) + : base(() => new EditorPlayer(editor)) { } From 3b5b7b2f880152dcd5d4bc2e99fc61e79830a6f1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 29 May 2024 22:57:17 +0900 Subject: [PATCH 0972/2493] Fix the majority of cases where gameplay stil doesn't end due to judgement count mismatch --- osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs b/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs index c327ae185d..836f718f81 100644 --- a/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs +++ b/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs @@ -69,9 +69,9 @@ namespace osu.Game.Screens.Edit.GameplayTest { var frame = new ReplayFrame { Header = new FrameHeader(new ScoreInfo(), new ScoreProcessorStatistics()) }; - foreach (var hitObject in enumerateHitObjects(DrawableRuleset.Objects.Where(h => h.StartTime < editorState.Time))) + foreach (var hitObject in enumerateHitObjects(DrawableRuleset.Objects, editorState.Time)) { - var judgement = hitObject.CreateJudgement(); + var judgement = hitObject.Judgement; frame.Header.Statistics.TryAdd(judgement.MaxResult, 0); frame.Header.Statistics[judgement.MaxResult]++; @@ -80,11 +80,11 @@ namespace osu.Game.Screens.Edit.GameplayTest HealthProcessor.ResetFromReplayFrame(frame); ScoreProcessor.ResetFromReplayFrame(frame); - static IEnumerable enumerateHitObjects(IEnumerable hitObjects) + static IEnumerable enumerateHitObjects(IEnumerable hitObjects, double cutoffTime) { - foreach (var hitObject in hitObjects) + foreach (var hitObject in hitObjects.Where(h => h.GetEndTime() < cutoffTime)) { - foreach (var nested in enumerateHitObjects(hitObject.NestedHitObjects)) + foreach (var nested in enumerateHitObjects(hitObject.NestedHitObjects, cutoffTime)) yield return nested; yield return hitObject; From 126837fadd02156990f479d7b02f161cb1e053cd Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 29 May 2024 23:28:37 +0900 Subject: [PATCH 0973/2493] Apply results rather than fake a replay frame --- osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs b/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs index 836f718f81..7d637fcb09 100644 --- a/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs +++ b/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs @@ -6,12 +6,9 @@ using System.Linq; using osu.Framework.Allocation; using osu.Framework.Screens; using osu.Game.Beatmaps; -using osu.Game.Online.Spectator; using osu.Game.Overlays; +using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects; -using osu.Game.Rulesets.Replays; -using osu.Game.Rulesets.Scoring; -using osu.Game.Scoring; using osu.Game.Screens.Play; using osu.Game.Users; @@ -67,19 +64,14 @@ namespace osu.Game.Screens.Edit.GameplayTest private void markPreviousObjectsHit() { - var frame = new ReplayFrame { Header = new FrameHeader(new ScoreInfo(), new ScoreProcessorStatistics()) }; - foreach (var hitObject in enumerateHitObjects(DrawableRuleset.Objects, editorState.Time)) { var judgement = hitObject.Judgement; - frame.Header.Statistics.TryAdd(judgement.MaxResult, 0); - frame.Header.Statistics[judgement.MaxResult]++; + HealthProcessor.ApplyResult(new JudgementResult(hitObject, judgement) { Type = judgement.MaxResult }); + ScoreProcessor.ApplyResult(new JudgementResult(hitObject, judgement) { Type = judgement.MaxResult }); } - HealthProcessor.ResetFromReplayFrame(frame); - ScoreProcessor.ResetFromReplayFrame(frame); - static IEnumerable enumerateHitObjects(IEnumerable hitObjects, double cutoffTime) { foreach (var hitObject in hitObjects.Where(h => h.GetEndTime() < cutoffTime)) From fdb47f8dfaa7c7f90624b853839111052b4beb29 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 29 May 2024 23:30:47 +0900 Subject: [PATCH 0974/2493] Fix incorrect handling of nested objects when inside parent object's duration --- osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs b/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs index 7d637fcb09..2028094964 100644 --- a/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs +++ b/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs @@ -74,12 +74,16 @@ namespace osu.Game.Screens.Edit.GameplayTest static IEnumerable enumerateHitObjects(IEnumerable hitObjects, double cutoffTime) { - foreach (var hitObject in hitObjects.Where(h => h.GetEndTime() < cutoffTime)) + foreach (var hitObject in hitObjects) { foreach (var nested in enumerateHitObjects(hitObject.NestedHitObjects, cutoffTime)) - yield return nested; + { + if (nested.GetEndTime() < cutoffTime) + yield return nested; + } - yield return hitObject; + if (hitObject.GetEndTime() < cutoffTime) + yield return hitObject; } } } From f5d2c549cb53b0e432c4515540be05cbd214a747 Mon Sep 17 00:00:00 2001 From: Givikap120 Date: Wed, 29 May 2024 19:25:58 +0300 Subject: [PATCH 0975/2493] Update CatchPerformanceCalculator.cs --- .../Difficulty/CatchPerformanceCalculator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceCalculator.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceCalculator.cs index d07f25ba90..55232a9598 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceCalculator.cs @@ -76,7 +76,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty value *= Math.Pow(accuracy(), 5.5); if (score.Mods.Any(m => m is ModNoFail)) - value *= 0.90; + value *= Math.Max(0.90, 1.0 - 0.02 * numMiss); return new CatchPerformanceAttributes { From 8916f08f8620d38b308f211006e6c75535138342 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 30 May 2024 09:03:02 +0200 Subject: [PATCH 0976/2493] Only take initial judgement position from object instead of following Looks less bad with mods like depth active. Co-authored-by: Dean Herbert --- .../Objects/Drawables/DrawableOsuJudgement.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs index 98fb99609a..6e252a53e2 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs @@ -17,6 +17,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables [Resolved] private OsuConfigManager config { get; set; } = null!; + private bool positionTransferred; + [BackgroundDependencyLoader] private void load() { @@ -36,16 +38,20 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables Lighting.ResetAnimation(); Lighting.SetColourFrom(JudgedObject, Result); + + positionTransferred = false; } protected override void Update() { base.Update(); - if (JudgedObject is DrawableOsuHitObject osuObject && JudgedObject.IsInUse) + if (!positionTransferred && JudgedObject is DrawableOsuHitObject osuObject && JudgedObject.IsInUse) { Position = osuObject.ToSpaceOfOtherDrawable(osuObject.OriginPosition, Parent!); Scale = new Vector2(osuObject.HitObject.Scale); + + positionTransferred = true; } } From 2f2bc8e52eaa6bf2213357f8ca624e902cfcf2f4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 30 May 2024 17:37:55 +0900 Subject: [PATCH 0977/2493] Avoid `ChatAckRequest` failures flooding console in `OsuGameTestScene`s --- osu.Game.Tests/Chat/TestSceneChannelManager.cs | 2 +- osu.Game/Online/API/DummyAPIAccess.cs | 8 ++++++++ osu.Game/Online/API/Requests/Responses/ChatAckResponse.cs | 4 ++-- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Chat/TestSceneChannelManager.cs b/osu.Game.Tests/Chat/TestSceneChannelManager.cs index 95fd2669e5..ef4d4f683a 100644 --- a/osu.Game.Tests/Chat/TestSceneChannelManager.cs +++ b/osu.Game.Tests/Chat/TestSceneChannelManager.cs @@ -59,7 +59,7 @@ namespace osu.Game.Tests.Chat return true; case ChatAckRequest ack: - ack.TriggerSuccess(new ChatAckResponse { Silences = silencedUserIds.Select(u => new ChatSilence { UserId = u }).ToList() }); + ack.TriggerSuccess(new ChatAckResponse { Silences = silencedUserIds.Select(u => new ChatSilence { UserId = u }).ToArray() }); silencedUserIds.Clear(); return true; diff --git a/osu.Game/Online/API/DummyAPIAccess.cs b/osu.Game/Online/API/DummyAPIAccess.cs index 4962838bd9..8d1e986577 100644 --- a/osu.Game/Online/API/DummyAPIAccess.cs +++ b/osu.Game/Online/API/DummyAPIAccess.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using osu.Framework.Bindables; @@ -84,6 +85,13 @@ namespace osu.Game.Online.API { if (HandleRequest?.Invoke(request) != true) { + // Noisy so let's silently allow these to succeed. + if (request is ChatAckRequest ack) + { + ack.TriggerSuccess(new ChatAckResponse()); + return; + } + request.Fail(new InvalidOperationException($@"{nameof(DummyAPIAccess)} cannot process this request.")); } }); diff --git a/osu.Game/Online/API/Requests/Responses/ChatAckResponse.cs b/osu.Game/Online/API/Requests/Responses/ChatAckResponse.cs index 6ed22a19b2..f68735d390 100644 --- a/osu.Game/Online/API/Requests/Responses/ChatAckResponse.cs +++ b/osu.Game/Online/API/Requests/Responses/ChatAckResponse.cs @@ -1,7 +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; using Newtonsoft.Json; namespace osu.Game.Online.API.Requests.Responses @@ -10,6 +10,6 @@ namespace osu.Game.Online.API.Requests.Responses public class ChatAckResponse { [JsonProperty("silences")] - public List Silences { get; set; } = null!; + public ChatSilence[] Silences { get; set; } = Array.Empty(); } } From 36d7775032c08f0983ebd2ea28960bcfb0db4968 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 30 May 2024 17:38:05 +0900 Subject: [PATCH 0978/2493] Fix typo in `IAPIProvider` xmldoc --- osu.Game/Online/API/IAPIProvider.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Online/API/IAPIProvider.cs b/osu.Game/Online/API/IAPIProvider.cs index 66f124f7c3..7b95b68ec3 100644 --- a/osu.Game/Online/API/IAPIProvider.cs +++ b/osu.Game/Online/API/IAPIProvider.cs @@ -61,7 +61,7 @@ namespace osu.Game.Online.API string APIEndpointUrl { get; } /// - /// The root URL of of the website, excluding the trailing slash. + /// The root URL of the website, excluding the trailing slash. /// string WebsiteRootUrl { get; } From 50bd0897f653f64fa394abc490b691ac84170547 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 30 May 2024 10:38:22 +0200 Subject: [PATCH 0979/2493] Fix main menu button backgrounds not covering their entire width sometimes I thought I had fixed this already once but it still looks broken. Basically when hovering over main menu buttons every now and then it will look like their backgrounds are not covering their entire width when they expand. The removed X position set looks wrong to me when inspecting the draw visualiser with the element because the element looks to be off centre horizontally, and removing it fixes that. --- osu.Game/Screens/Menu/MainMenuButton.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Screens/Menu/MainMenuButton.cs b/osu.Game/Screens/Menu/MainMenuButton.cs index 29a661066c..4df5e6d309 100644 --- a/osu.Game/Screens/Menu/MainMenuButton.cs +++ b/osu.Game/Screens/Menu/MainMenuButton.cs @@ -115,7 +115,6 @@ namespace osu.Game.Screens.Menu backgroundContent = CreateBackground(colour).With(bg => { bg.RelativeSizeAxes = Axes.Y; - bg.X = -ButtonSystem.WEDGE_WIDTH; bg.Anchor = Anchor.Centre; bg.Origin = Anchor.Centre; }), From f3bc944ac86c7f5bd37c04cc35c0c2fb71cebfba Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 30 May 2024 17:45:32 +0900 Subject: [PATCH 0980/2493] Remove using statement --- osu.Game/Online/API/DummyAPIAccess.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Online/API/DummyAPIAccess.cs b/osu.Game/Online/API/DummyAPIAccess.cs index 8d1e986577..960941fc05 100644 --- a/osu.Game/Online/API/DummyAPIAccess.cs +++ b/osu.Game/Online/API/DummyAPIAccess.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using osu.Framework.Bindables; From f6a59bee9addebb75dc34b4d1253da84967b35bb Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 30 May 2024 19:17:18 +0900 Subject: [PATCH 0981/2493] Use delayed resume overlay for autopilot --- osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs | 2 -- osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs | 9 ++++++++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs b/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs index efcc728d55..b45b4fea13 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs @@ -67,8 +67,6 @@ namespace osu.Game.Rulesets.Osu.Mods // Generate the replay frames the cursor should follow replayFrames = new OsuAutoGenerator(drawableRuleset.Beatmap, drawableRuleset.Mods).Generate().Frames.Cast().ToList(); - - drawableRuleset.UseResumeOverlay = false; } } } diff --git a/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs b/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs index c3efd48053..f0390ad716 100644 --- a/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs @@ -13,6 +13,7 @@ using osu.Game.Replays; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Configuration; +using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Replays; using osu.Game.Rulesets.UI; @@ -45,7 +46,13 @@ namespace osu.Game.Rulesets.Osu.UI public override PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => new OsuPlayfieldAdjustmentContainer { AlignWithStoryboard = true }; - protected override ResumeOverlay CreateResumeOverlay() => new OsuResumeOverlay(); + protected override ResumeOverlay CreateResumeOverlay() + { + if (Mods.Any(m => m is OsuModAutopilot)) + return new DelayedResumeOverlay { Scale = new Vector2(0.65f) }; + + return new OsuResumeOverlay(); + } protected override ReplayInputHandler CreateReplayInputHandler(Replay replay) => new OsuFramedReplayInputHandler(replay); From 87a331fdde783dbfc3b5f395339d2d09cba1105e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 30 May 2024 16:34:52 +0900 Subject: [PATCH 0982/2493] Improve text on external link warning dialog --- osu.Game/Online/Chat/ExternalLinkOpener.cs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/osu.Game/Online/Chat/ExternalLinkOpener.cs b/osu.Game/Online/Chat/ExternalLinkOpener.cs index 56d24e35bb..08adf965da 100644 --- a/osu.Game/Online/Chat/ExternalLinkOpener.cs +++ b/osu.Game/Online/Chat/ExternalLinkOpener.cs @@ -10,6 +10,7 @@ using osu.Framework.Platform; using osu.Game.Configuration; using osu.Game.Overlays; using osu.Game.Overlays.Dialog; +using osu.Game.Resources.Localisation.Web; namespace osu.Game.Online.Chat { @@ -44,8 +45,8 @@ namespace osu.Game.Online.Chat { public ExternalLinkDialog(string url, Action openExternalLinkAction, Action copyExternalLinkAction) { - HeaderText = "Just checking..."; - BodyText = $"You are about to leave osu! and open the following link in a web browser:\n\n{url}"; + HeaderText = "You are leaving osu!"; + BodyText = $"Are you sure you want to open the following link in a web browser:\n\n{url}"; Icon = FontAwesome.Solid.ExclamationTriangle; @@ -53,17 +54,17 @@ namespace osu.Game.Online.Chat { new PopupDialogOkButton { - Text = @"Yes. Go for it.", + Text = @"Open in browser", Action = openExternalLinkAction }, new PopupDialogCancelButton { - Text = @"Copy URL to the clipboard instead.", + Text = @"Copy URL to the clipboard", Action = copyExternalLinkAction }, new PopupDialogCancelButton { - Text = @"No! Abort mission!" + Text = CommonStrings.ButtonsCancel, }, }; } From ed64bfff8d9c13c54b27b955a706e28d3cefb134 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 30 May 2024 16:39:53 +0900 Subject: [PATCH 0983/2493] Bypass external link dialog for links on the trusted osu! domain --- osu.Game/OsuGame.cs | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index af01a1b1ac..29c040c597 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -485,10 +485,19 @@ namespace osu.Game } }); - public void OpenUrlExternally(string url, bool bypassExternalUrlWarning = false) => waitForReady(() => externalLinkOpener, _ => + public void OpenUrlExternally(string url, bool forceBypassExternalUrlWarning = false) => waitForReady(() => externalLinkOpener, _ => { + bool isTrustedDomain; + if (url.StartsWith('/')) - url = $"{API.APIEndpointUrl}{url}"; + { + url = $"{API.WebsiteRootUrl}{url}"; + isTrustedDomain = true; + } + else + { + isTrustedDomain = url.StartsWith(API.APIEndpointUrl, StringComparison.Ordinal); + } if (!url.CheckIsValidUrl()) { @@ -500,7 +509,7 @@ namespace osu.Game return; } - externalLinkOpener.OpenUrlExternally(url, bypassExternalUrlWarning); + externalLinkOpener.OpenUrlExternally(url, forceBypassExternalUrlWarning || isTrustedDomain); }); /// From 53b7c29488f6dd28ae30877e0df148d4c6be6d33 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 30 May 2024 17:35:42 +0900 Subject: [PATCH 0984/2493] Add test coverage of menu banner link opening --- .../Visual/Menus/TestSceneMainMenu.cs | 49 ++++++++++++++++++- osu.Game/Screens/Menu/OnlineMenuBanner.cs | 5 ++ 2 files changed, 52 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Menus/TestSceneMainMenu.cs b/osu.Game.Tests/Visual/Menus/TestSceneMainMenu.cs index e2a841d79a..240421b360 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneMainMenu.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneMainMenu.cs @@ -6,6 +6,7 @@ using NUnit.Framework; using osu.Framework.Graphics.Containers; using osu.Framework.Testing; using osu.Game.Online.API.Requests.Responses; +using osu.Game.Overlays; using osu.Game.Screens.Menu; using osuTK.Input; @@ -15,8 +16,14 @@ namespace osu.Game.Tests.Visual.Menus { private OnlineMenuBanner onlineMenuBanner => Game.ChildrenOfType().Single(); + public override void SetUpSteps() + { + base.SetUpSteps(); + AddStep("don't fetch online content", () => onlineMenuBanner.FetchOnlineContent = false); + } + [Test] - public void TestOnlineMenuBanner() + public void TestOnlineMenuBannerTrusted() { AddStep("set online content", () => onlineMenuBanner.Current.Value = new APIMenuContent { @@ -25,13 +32,51 @@ namespace osu.Game.Tests.Visual.Menus new APIMenuImage { Image = @"https://assets.ppy.sh/main-menu/project-loved-2@2x.png", - Url = @"https://osu.ppy.sh/home/news/2023-12-21-project-loved-december-2023", + Url = $@"{API.WebsiteRootUrl}/home/news/2023-12-21-project-loved-december-2023", } } }); AddAssert("system title not visible", () => onlineMenuBanner.State.Value, () => Is.EqualTo(Visibility.Hidden)); AddStep("enter menu", () => InputManager.Key(Key.Enter)); AddUntilStep("system title visible", () => onlineMenuBanner.State.Value, () => Is.EqualTo(Visibility.Visible)); + AddUntilStep("image loaded", () => onlineMenuBanner.ChildrenOfType().FirstOrDefault()?.IsLoaded, () => Is.True); + + AddStep("click banner", () => + { + InputManager.MoveMouseTo(onlineMenuBanner); + InputManager.Click(MouseButton.Left); + }); + + // Might not catch every occurrence due to async nature, but works in manual testing and saves annoying test setup. + AddAssert("no dialog", () => Game.ChildrenOfType().SingleOrDefault()?.CurrentDialog == null); + } + + [Test] + public void TestOnlineMenuBannerUntrustedDomain() + { + AddStep("set online content", () => onlineMenuBanner.Current.Value = new APIMenuContent + { + Images = new[] + { + new APIMenuImage + { + Image = @"https://assets.ppy.sh/main-menu/project-loved-2@2x.png", + Url = @"https://google.com", + } + } + }); + AddAssert("system title not visible", () => onlineMenuBanner.State.Value, () => Is.EqualTo(Visibility.Hidden)); + AddStep("enter menu", () => InputManager.Key(Key.Enter)); + AddUntilStep("system title visible", () => onlineMenuBanner.State.Value, () => Is.EqualTo(Visibility.Visible)); + AddUntilStep("image loaded", () => onlineMenuBanner.ChildrenOfType().FirstOrDefault()?.IsLoaded, () => Is.True); + + AddStep("click banner", () => + { + InputManager.MoveMouseTo(onlineMenuBanner); + InputManager.Click(MouseButton.Left); + }); + + AddUntilStep("wait for dialog", () => Game.ChildrenOfType().SingleOrDefault()?.CurrentDialog != null); } } } diff --git a/osu.Game/Screens/Menu/OnlineMenuBanner.cs b/osu.Game/Screens/Menu/OnlineMenuBanner.cs index b9d269c82a..edd34d0bfb 100644 --- a/osu.Game/Screens/Menu/OnlineMenuBanner.cs +++ b/osu.Game/Screens/Menu/OnlineMenuBanner.cs @@ -73,6 +73,9 @@ namespace osu.Game.Screens.Menu Task.Run(() => request.Perform()) .ContinueWith(r => { + if (!FetchOnlineContent) + return; + if (r.IsCompletedSuccessfully) Schedule(() => Current.Value = request.ResponseObject); @@ -170,6 +173,8 @@ namespace osu.Game.Screens.Menu private Sprite flash = null!; + public override bool IsPresent => base.IsPresent || Scheduler.HasPendingTasks; + private ScheduledDelegate? openUrlAction; public MenuImage(APIMenuImage image) From 474ff5b99d089cd5db38d26541598735cb65b9cb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 31 May 2024 10:46:30 +0900 Subject: [PATCH 0985/2493] Use question mark for more grammatical correctness Co-authored-by: Joseph Madamba --- osu.Game/Online/Chat/ExternalLinkOpener.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Online/Chat/ExternalLinkOpener.cs b/osu.Game/Online/Chat/ExternalLinkOpener.cs index 08adf965da..513ccd43af 100644 --- a/osu.Game/Online/Chat/ExternalLinkOpener.cs +++ b/osu.Game/Online/Chat/ExternalLinkOpener.cs @@ -46,7 +46,7 @@ namespace osu.Game.Online.Chat public ExternalLinkDialog(string url, Action openExternalLinkAction, Action copyExternalLinkAction) { HeaderText = "You are leaving osu!"; - BodyText = $"Are you sure you want to open the following link in a web browser:\n\n{url}"; + BodyText = $"Are you sure you want to open the following link in a web browser?\n\n{url}"; Icon = FontAwesome.Solid.ExclamationTriangle; From e52f524ea2ce15b6ef891900a3c7cee236f6d7cb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 31 May 2024 11:44:53 +0900 Subject: [PATCH 0986/2493] Use common header text --- osu.Game/Online/Chat/ExternalLinkOpener.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game/Online/Chat/ExternalLinkOpener.cs b/osu.Game/Online/Chat/ExternalLinkOpener.cs index 513ccd43af..2f046d8fd4 100644 --- a/osu.Game/Online/Chat/ExternalLinkOpener.cs +++ b/osu.Game/Online/Chat/ExternalLinkOpener.cs @@ -8,9 +8,10 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Framework.Platform; using osu.Game.Configuration; +using osu.Game.Localisation; using osu.Game.Overlays; using osu.Game.Overlays.Dialog; -using osu.Game.Resources.Localisation.Web; +using CommonStrings = osu.Game.Resources.Localisation.Web.CommonStrings; namespace osu.Game.Online.Chat { @@ -45,7 +46,7 @@ namespace osu.Game.Online.Chat { public ExternalLinkDialog(string url, Action openExternalLinkAction, Action copyExternalLinkAction) { - HeaderText = "You are leaving osu!"; + HeaderText = DeleteConfirmationDialogStrings.HeaderText; BodyText = $"Are you sure you want to open the following link in a web browser?\n\n{url}"; Icon = FontAwesome.Solid.ExclamationTriangle; From 5dfeaa3c4afa75dbfebffbd1c7a61af0dd93b6d4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 31 May 2024 11:46:32 +0900 Subject: [PATCH 0987/2493] Move dialog strings to more common class name --- ...{DeleteConfirmationDialogStrings.cs => DialogStrings.cs} | 6 +++--- osu.Game/Online/Chat/ExternalLinkOpener.cs | 2 +- osu.Game/Overlays/Dialog/DangerousActionDialog.cs | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) rename osu.Game/Localisation/{DeleteConfirmationDialogStrings.cs => DialogStrings.cs} (80%) diff --git a/osu.Game/Localisation/DeleteConfirmationDialogStrings.cs b/osu.Game/Localisation/DialogStrings.cs similarity index 80% rename from osu.Game/Localisation/DeleteConfirmationDialogStrings.cs rename to osu.Game/Localisation/DialogStrings.cs index 25997eadd3..cea543eb9f 100644 --- a/osu.Game/Localisation/DeleteConfirmationDialogStrings.cs +++ b/osu.Game/Localisation/DialogStrings.cs @@ -5,14 +5,14 @@ using osu.Framework.Localisation; namespace osu.Game.Localisation { - public static class DeleteConfirmationDialogStrings + public static class DialogStrings { - private const string prefix = @"osu.Game.Resources.Localisation.DeleteConfirmationDialog"; + private const string prefix = @"osu.Game.Resources.Localisation.DialogStrings"; /// /// "Caution" /// - public static LocalisableString HeaderText => new TranslatableString(getKey(@"header_text"), @"Caution"); + public static LocalisableString Caution => new TranslatableString(getKey(@"header_text"), @"Caution"); /// /// "Yes. Go for it." diff --git a/osu.Game/Online/Chat/ExternalLinkOpener.cs b/osu.Game/Online/Chat/ExternalLinkOpener.cs index 2f046d8fd4..82ad4215c2 100644 --- a/osu.Game/Online/Chat/ExternalLinkOpener.cs +++ b/osu.Game/Online/Chat/ExternalLinkOpener.cs @@ -46,7 +46,7 @@ namespace osu.Game.Online.Chat { public ExternalLinkDialog(string url, Action openExternalLinkAction, Action copyExternalLinkAction) { - HeaderText = DeleteConfirmationDialogStrings.HeaderText; + HeaderText = DialogStrings.Caution; BodyText = $"Are you sure you want to open the following link in a web browser?\n\n{url}"; Icon = FontAwesome.Solid.ExclamationTriangle; diff --git a/osu.Game/Overlays/Dialog/DangerousActionDialog.cs b/osu.Game/Overlays/Dialog/DangerousActionDialog.cs index 42a3ff827c..60f51e7e06 100644 --- a/osu.Game/Overlays/Dialog/DangerousActionDialog.cs +++ b/osu.Game/Overlays/Dialog/DangerousActionDialog.cs @@ -30,7 +30,7 @@ namespace osu.Game.Overlays.Dialog protected DangerousActionDialog() { - HeaderText = DeleteConfirmationDialogStrings.HeaderText; + HeaderText = DialogStrings.DialogCautionHeader; Icon = FontAwesome.Regular.TrashAlt; @@ -38,12 +38,12 @@ namespace osu.Game.Overlays.Dialog { new PopupDialogDangerousButton { - Text = DeleteConfirmationDialogStrings.Confirm, + Text = DialogStrings.DialogConfirm, Action = () => DangerousAction?.Invoke() }, new PopupDialogCancelButton { - Text = DeleteConfirmationDialogStrings.Cancel, + Text = DialogStrings.Cancel, Action = () => CancelAction?.Invoke() } }; From cb72630ce1894a3ed73f607112095c4845413349 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 31 May 2024 08:09:06 +0200 Subject: [PATCH 0988/2493] Fix compile failures --- osu.Game/Overlays/Dialog/DangerousActionDialog.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/Dialog/DangerousActionDialog.cs b/osu.Game/Overlays/Dialog/DangerousActionDialog.cs index 60f51e7e06..31160d1832 100644 --- a/osu.Game/Overlays/Dialog/DangerousActionDialog.cs +++ b/osu.Game/Overlays/Dialog/DangerousActionDialog.cs @@ -30,7 +30,7 @@ namespace osu.Game.Overlays.Dialog protected DangerousActionDialog() { - HeaderText = DialogStrings.DialogCautionHeader; + HeaderText = DialogStrings.Caution; Icon = FontAwesome.Regular.TrashAlt; @@ -38,7 +38,7 @@ namespace osu.Game.Overlays.Dialog { new PopupDialogDangerousButton { - Text = DialogStrings.DialogConfirm, + Text = DialogStrings.Confirm, Action = () => DangerousAction?.Invoke() }, new PopupDialogCancelButton From 1a26a5dbdaf77256564930e69e5f20d3e9a2cd17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 31 May 2024 08:09:25 +0200 Subject: [PATCH 0989/2493] Fix mismatching localisation key prefix The `Strings` suffix is not supposed to be in here, judging by other localisation classes. --- osu.Game/Localisation/DialogStrings.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Localisation/DialogStrings.cs b/osu.Game/Localisation/DialogStrings.cs index cea543eb9f..043a3f5b4c 100644 --- a/osu.Game/Localisation/DialogStrings.cs +++ b/osu.Game/Localisation/DialogStrings.cs @@ -7,7 +7,7 @@ namespace osu.Game.Localisation { public static class DialogStrings { - private const string prefix = @"osu.Game.Resources.Localisation.DialogStrings"; + private const string prefix = @"osu.Game.Resources.Localisation.Dialog"; /// /// "Caution" From 9111da81d22c2a9e35533df964cb9e5c0e691807 Mon Sep 17 00:00:00 2001 From: Aurelian Date: Fri, 31 May 2024 01:30:26 +0200 Subject: [PATCH 0990/2493] Updated comments --- osu.Game/Rulesets/Objects/SliderPath.cs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/osu.Game/Rulesets/Objects/SliderPath.cs b/osu.Game/Rulesets/Objects/SliderPath.cs index 730a2013b0..5550815370 100644 --- a/osu.Game/Rulesets/Objects/SliderPath.cs +++ b/osu.Game/Rulesets/Objects/SliderPath.cs @@ -332,16 +332,15 @@ namespace osu.Game.Rulesets.Objects CircularArcProperties circularArcProperties = new CircularArcProperties(subControlPoints); - //if false, we'll end up breaking anyways when calculating subPath + // `PathApproximator` will already internally revert to B-spline if the arc isn't valid. if (!circularArcProperties.IsValid) break; - //Coppied from PathApproximator.CircularArcToPiecewiseLinear + // taken from https://github.com/ppy/osu-framework/blob/1201e641699a1d50d2f6f9295192dad6263d5820/osu.Framework/Utils/PathApproximator.cs#L181-L186 int subPoints = (2f * circularArcProperties.Radius <= 0.1f) ? 2 : Math.Max(2, (int)Math.Ceiling(circularArcProperties.ThetaRange / (2.0 * Math.Acos(1f - (0.1f / circularArcProperties.Radius))))); - //theoretically can be int.MaxValue, but lets set this to a lower value anyways - //1000 requires an arc length of over 20 thousand to surpass this limit, which should be safe. - //See here for calculations https://www.desmos.com/calculator/210bwswkbb + // 1000 subpoints requires an arc length of at least ~120 thousand to occur + // See here for calculations https://www.desmos.com/calculator/umj6jvmcz7 if (subPoints >= 1000) break; From c6a7082034da26c6399633ad18665a85ccc58f30 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 31 May 2024 15:38:26 +0900 Subject: [PATCH 0991/2493] Fix incorrect prefix check --- osu.Game/OsuGame.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 29c040c597..0833f52b1e 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -496,7 +496,7 @@ namespace osu.Game } else { - isTrustedDomain = url.StartsWith(API.APIEndpointUrl, StringComparison.Ordinal); + isTrustedDomain = url.StartsWith(API.WebsiteRootUrl, StringComparison.Ordinal); } if (!url.CheckIsValidUrl()) From 69990c35cb638d57e735cedd6785fe2a06bd4341 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 31 May 2024 08:47:19 +0200 Subject: [PATCH 0992/2493] Add commentary on presence of `IsPresent` override --- osu.Game/Screens/Menu/OnlineMenuBanner.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Screens/Menu/OnlineMenuBanner.cs b/osu.Game/Screens/Menu/OnlineMenuBanner.cs index edd34d0bfb..49fc89c171 100644 --- a/osu.Game/Screens/Menu/OnlineMenuBanner.cs +++ b/osu.Game/Screens/Menu/OnlineMenuBanner.cs @@ -173,6 +173,9 @@ namespace osu.Game.Screens.Menu private Sprite flash = null!; + /// + /// Overridden as a safety for functioning correctly. + /// public override bool IsPresent => base.IsPresent || Scheduler.HasPendingTasks; private ScheduledDelegate? openUrlAction; From e3205fce4769afd2ac3cce60f6e1321f530ec029 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Sat, 1 Jun 2024 14:29:23 +0900 Subject: [PATCH 0993/2493] Fix unable to drag-scroll on collections right-click menu --- osu.Game/Graphics/UserInterface/DrawableStatefulMenuItem.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game/Graphics/UserInterface/DrawableStatefulMenuItem.cs b/osu.Game/Graphics/UserInterface/DrawableStatefulMenuItem.cs index 686c490930..b9e81e1bf2 100644 --- a/osu.Game/Graphics/UserInterface/DrawableStatefulMenuItem.cs +++ b/osu.Game/Graphics/UserInterface/DrawableStatefulMenuItem.cs @@ -26,9 +26,12 @@ namespace osu.Game.Graphics.UserInterface // Right mouse button is a special case where we allow actioning without dismissing the menu. // This is achieved by not calling `Clicked` (as done by the base implementation in OnClick). if (IsActionable && e.Button == MouseButton.Right) + { Item.Action.Value?.Invoke(); + return true; + } - return true; + return false; } private partial class ToggleTextContainer : TextContainer From 7254096c9011132e68f9e1ef109a6f13c076b986 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Sat, 1 Jun 2024 20:28:39 +0200 Subject: [PATCH 0994/2493] fix isDragging being always true --- .../Edit/Blueprints/Sliders/Components/SliderTailPiece.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderTailPiece.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderTailPiece.cs index dc57d6d7ca..b5894eb84f 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderTailPiece.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderTailPiece.cs @@ -129,10 +129,10 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components protected override void OnDragEnd(DragEndEvent e) { - if (isDragging) - { - editorBeatmap?.EndChange(); - } + if (!isDragging) return; + + isDragging = false; + editorBeatmap?.EndChange(); } /// From ca41c84ba21ab50aea0e9c1fef406c7bc26293a5 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Sat, 1 Jun 2024 21:15:54 +0200 Subject: [PATCH 0995/2493] trim excess control points on drag end --- .../Sliders/Components/SliderTailPiece.cs | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderTailPiece.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderTailPiece.cs index b5894eb84f..a0f4401ca7 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderTailPiece.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderTailPiece.cs @@ -131,10 +131,39 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components { if (!isDragging) return; + trimExcessControlPoints(Slider.Path); + isDragging = false; editorBeatmap?.EndChange(); } + /// + /// Trims control points from the end of the slider path which are not required to reach the expected end of the slider. + /// + /// The slider path to trim control points of. + private void trimExcessControlPoints(SliderPath sliderPath) + { + if (!sliderPath.ExpectedDistance.Value.HasValue) + return; + + double[] segmentEnds = sliderPath.GetSegmentEnds().ToArray(); + int segmentIndex = 0; + + for (int i = 1; i < sliderPath.ControlPoints.Count - 1; i++) + { + if (!sliderPath.ControlPoints[i].Type.HasValue) continue; + + if (Precision.AlmostBigger(segmentEnds[segmentIndex], 1, 1E-3)) + { + sliderPath.ControlPoints.RemoveRange(i + 1, sliderPath.ControlPoints.Count - i - 1); + sliderPath.ControlPoints[^1].Type = null; + break; + } + + segmentIndex++; + } + } + /// /// Finds the expected distance value for which the slider end is closest to the mouse position. /// From 091104764e677d4efecbe8958ca71b7f9cac03b8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 2 Jun 2024 17:33:06 +0900 Subject: [PATCH 0996/2493] Fix `AssemblyRulesetStore` not marking rulesets as available --- osu.Game/Rulesets/AssemblyRulesetStore.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/AssemblyRulesetStore.cs b/osu.Game/Rulesets/AssemblyRulesetStore.cs index 03554ef2db..935ef241dc 100644 --- a/osu.Game/Rulesets/AssemblyRulesetStore.cs +++ b/osu.Game/Rulesets/AssemblyRulesetStore.cs @@ -43,7 +43,12 @@ namespace osu.Game.Rulesets // add all legacy rulesets first to ensure they have exclusive choice of primary key. foreach (var r in instances.Where(r => r is ILegacyRuleset)) - availableRulesets.Add(new RulesetInfo(r.RulesetInfo.ShortName, r.RulesetInfo.Name, r.RulesetInfo.InstantiationInfo, r.RulesetInfo.OnlineID)); + { + availableRulesets.Add(new RulesetInfo(r.RulesetInfo.ShortName, r.RulesetInfo.Name, r.RulesetInfo.InstantiationInfo, r.RulesetInfo.OnlineID) + { + Available = true + }); + } } } } From 96514132c1ecc5e3efabf9427741148b7e283fd1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 3 Jun 2024 12:24:50 +0900 Subject: [PATCH 0997/2493] Fix occasional test failures on new menu content tests Scheduled data transfer could still overwrite test data. --- osu.Game/Screens/Menu/OnlineMenuBanner.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Menu/OnlineMenuBanner.cs b/osu.Game/Screens/Menu/OnlineMenuBanner.cs index 49fc89c171..aa73ce2136 100644 --- a/osu.Game/Screens/Menu/OnlineMenuBanner.cs +++ b/osu.Game/Screens/Menu/OnlineMenuBanner.cs @@ -77,7 +77,15 @@ namespace osu.Game.Screens.Menu return; if (r.IsCompletedSuccessfully) - Schedule(() => Current.Value = request.ResponseObject); + { + Schedule(() => + { + if (!FetchOnlineContent) + return; + + Current.Value = request.ResponseObject; + }); + } // if the request failed, "observe" the exception. // it isn't very important why this failed, as it's only for display. From bb38cb4137b6bbfa87f152bcc43b5b470736c806 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Mon, 3 Jun 2024 13:18:36 +0200 Subject: [PATCH 0998/2493] simplify tracking changes in shift key status --- .../Sliders/Components/SliderTailPiece.cs | 21 +++++-------------- 1 file changed, 5 insertions(+), 16 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderTailPiece.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderTailPiece.cs index a0f4401ca7..f28a8fafda 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderTailPiece.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderTailPiece.cs @@ -63,31 +63,20 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components protected override bool OnKeyDown(KeyDownEvent e) { - if (e.Repeat) - return false; + if (!e.Repeat && (e.Key == Key.ShiftLeft || e.Key == Key.ShiftRight)) + updateCirclePieceColour(); - handleDragToggle(e); return base.OnKeyDown(e); } protected override void OnKeyUp(KeyUpEvent e) { - handleDragToggle(e); + if (e.Key == Key.ShiftLeft || e.Key == Key.ShiftRight) + updateCirclePieceColour(); + base.OnKeyUp(e); } - private bool lastShiftPressed; - - private void handleDragToggle(KeyboardEvent key) - { - bool shiftPressed = key.ShiftPressed; - - if (shiftPressed == lastShiftPressed) return; - - lastShiftPressed = shiftPressed; - updateCirclePieceColour(); - } - private void updateCirclePieceColour() { Color4 colour = colours.Yellow; From 77b47ad2b4a3ea402017c48cb04b095dc1bd1b43 Mon Sep 17 00:00:00 2001 From: Olivier Schipper Date: Mon, 3 Jun 2024 13:23:39 +0200 Subject: [PATCH 0999/2493] simplify nullability annotation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bartłomiej Dach --- .../Edit/Blueprints/Sliders/Components/SliderTailPiece.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderTailPiece.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderTailPiece.cs index a0f4401ca7..23ebd92482 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderTailPiece.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderTailPiece.cs @@ -29,7 +29,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components private readonly Cached fullPathCache = new Cached(); - [Resolved(CanBeNull = true)] + [Resolved] private EditorBeatmap? editorBeatmap { get; set; } [Resolved] From 34c4ee7de8ea1b2228221c46aea2acb2fac56afd Mon Sep 17 00:00:00 2001 From: OliBomby Date: Mon, 3 Jun 2024 13:38:42 +0200 Subject: [PATCH 1000/2493] add CanBeNull attribute to LastRepeat --- .../Edit/Blueprints/Sliders/SliderCircleOverlay.cs | 2 +- osu.Game.Rulesets.Osu/Objects/Slider.cs | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderCircleOverlay.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderCircleOverlay.cs index 2bf5118039..55ea131dab 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderCircleOverlay.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderCircleOverlay.cs @@ -33,7 +33,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders base.Update(); var circle = position == SliderPosition.Start ? (HitCircle)Slider.HeadCircle : - Slider.RepeatCount % 2 == 0 ? Slider.TailCircle : Slider.LastRepeat; + Slider.RepeatCount % 2 == 0 ? Slider.TailCircle : Slider.LastRepeat!; CirclePiece.UpdateFrom(circle); marker.UpdateFrom(circle); diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs index 041907d790..57f277e1ce 100644 --- a/osu.Game.Rulesets.Osu/Objects/Slider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs @@ -9,6 +9,7 @@ using System.Collections.Generic; using osu.Game.Rulesets.Objects; using System.Linq; using System.Threading; +using JetBrains.Annotations; using Newtonsoft.Json; using osu.Framework.Bindables; using osu.Framework.Caching; @@ -163,6 +164,7 @@ namespace osu.Game.Rulesets.Osu.Objects public SliderTailCircle TailCircle { get; protected set; } [JsonIgnore] + [CanBeNull] public SliderRepeat LastRepeat { get; protected set; } public Slider() From f13ca28d5eae95792bb6cdac1e75f05f56d26c2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 4 Jun 2024 10:25:08 +0200 Subject: [PATCH 1001/2493] Fix performance overhead from ternary state bindable callbacks when selection is changing Closes https://github.com/ppy/osu/issues/28369. The reporter of the issue was incorrect; it's not the beat snap grid that is causing the problem, it's something far stupider than that. When the current selection changes, `EditorSelectionHandler.UpdateTernaryStates()` is supposed to update the state of ternary bindables to reflect the reality of the current selection. This in turn will fire bindable change callbacks for said ternary toggles, which heavily use `EditorBeatmap.PerformOnSelection()`. The thing about that method is that it will attempt to check whether any changes were actually made to avoid producing empty undo states, *but* to do this, it must *serialise out the entire beatmap to a stream* and then *binary equality check that* to determine whether any changes were actually made: https://github.com/ppy/osu/blob/7b14c77e43e4ee96775a9fcb6843324170fa70bb/osu.Game/Screens/Edit/EditorChangeHandler.cs#L65-L69 As goes without saying, this is very expensive and unnecessary, which leads to stuff like keeping a selection box active while a taiko beatmap is playing under it dog slow. So to attempt to mitigate that, add precondition checks to every single ternary callback of this sort to avoid this serialisation overhead. And yes, those precondition checks use linq, and that is *still* faster than not having them. --- .../Edit/TaikoSelectionHandler.cs | 6 ++++++ .../Compose/Components/EditorSelectionHandler.cs | 12 ++++++++++++ 2 files changed, 18 insertions(+) diff --git a/osu.Game.Rulesets.Taiko/Edit/TaikoSelectionHandler.cs b/osu.Game.Rulesets.Taiko/Edit/TaikoSelectionHandler.cs index 7ab8a54b02..ae6dced9aa 100644 --- a/osu.Game.Rulesets.Taiko/Edit/TaikoSelectionHandler.cs +++ b/osu.Game.Rulesets.Taiko/Edit/TaikoSelectionHandler.cs @@ -53,6 +53,9 @@ namespace osu.Game.Rulesets.Taiko.Edit public void SetStrongState(bool state) { + if (SelectedItems.OfType().All(h => h.IsStrong == state)) + return; + EditorBeatmap.PerformOnSelection(h => { if (!(h is Hit taikoHit)) return; @@ -67,6 +70,9 @@ namespace osu.Game.Rulesets.Taiko.Edit public void SetRimState(bool state) { + if (SelectedItems.OfType().All(h => h.Type == (state ? HitType.Rim : HitType.Centre))) + return; + EditorBeatmap.PerformOnSelection(h => { if (h is Hit taikoHit) diff --git a/osu.Game/Screens/Edit/Compose/Components/EditorSelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/EditorSelectionHandler.cs index a73278a61e..7420362e19 100644 --- a/osu.Game/Screens/Edit/Compose/Components/EditorSelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/EditorSelectionHandler.cs @@ -198,6 +198,9 @@ namespace osu.Game.Screens.Edit.Compose.Components /// The name of the sample bank. public void AddSampleBank(string bankName) { + if (SelectedItems.All(h => h.Samples.All(s => s.Bank == bankName))) + return; + EditorBeatmap.PerformOnSelection(h => { if (h.Samples.All(s => s.Bank == bankName)) @@ -214,6 +217,9 @@ namespace osu.Game.Screens.Edit.Compose.Components /// The name of the hit sample. public void AddHitSample(string sampleName) { + if (SelectedItems.All(h => h.Samples.Any(s => s.Name == sampleName))) + return; + EditorBeatmap.PerformOnSelection(h => { // Make sure there isn't already an existing sample @@ -231,6 +237,9 @@ namespace osu.Game.Screens.Edit.Compose.Components /// The name of the hit sample. public void RemoveHitSample(string sampleName) { + if (SelectedItems.All(h => h.Samples.All(s => s.Name != sampleName))) + return; + EditorBeatmap.PerformOnSelection(h => { h.SamplesBindable.RemoveAll(s => s.Name == sampleName); @@ -245,6 +254,9 @@ namespace osu.Game.Screens.Edit.Compose.Components /// Throws if any selected object doesn't implement public void SetNewCombo(bool state) { + if (SelectedItems.OfType().All(h => h.NewCombo == state)) + return; + EditorBeatmap.PerformOnSelection(h => { var comboInfo = h as IHasComboInformation; From ecfcf7a2c047a589858a532e0c764b98d9fd3550 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 4 Jun 2024 10:36:30 +0200 Subject: [PATCH 1002/2493] Add xmldoc mention about performance overhead of `PerformOnSelection()` --- osu.Game/Screens/Edit/EditorBeatmap.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/osu.Game/Screens/Edit/EditorBeatmap.cs b/osu.Game/Screens/Edit/EditorBeatmap.cs index 7a3ea474fb..6363ed2854 100644 --- a/osu.Game/Screens/Edit/EditorBeatmap.cs +++ b/osu.Game/Screens/Edit/EditorBeatmap.cs @@ -198,6 +198,11 @@ namespace osu.Game.Screens.Edit /// Perform the provided action on every selected hitobject. /// Changes will be grouped as one history action. /// + /// + /// Note that this incurs a full state save, and as such requires the entire beatmap to be encoded, etc. + /// Very frequent use of this method (e.g. once a frame) is most discouraged. + /// If there is need to do so, use local precondition checks to eliminate changes that are known to be no-ops. + /// /// The action to perform. public void PerformOnSelection(Action action) { From 442b61da849dbca8983475803c3d4d5d1f9702a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 4 Jun 2024 15:13:50 +0200 Subject: [PATCH 1003/2493] Disable primary constructor related inspections I'm not actually sure whether the editorconfig incantation does anything but it's the one official sources give: https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/ide0290 --- .editorconfig | 3 +++ osu.sln.DotSettings | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.editorconfig b/.editorconfig index c249e5e9b3..7aecde95ee 100644 --- a/.editorconfig +++ b/.editorconfig @@ -196,6 +196,9 @@ csharp_style_prefer_switch_expression = false:none csharp_style_namespace_declarations = block_scoped:warning +#Style - C# 12 features +csharp_style_prefer_primary_constructors = false + [*.{yaml,yml}] insert_final_newline = true indent_style = space diff --git a/osu.sln.DotSettings b/osu.sln.DotSettings index 08eb264aab..04633a9348 100644 --- a/osu.sln.DotSettings +++ b/osu.sln.DotSettings @@ -82,7 +82,7 @@ WARNING WARNING HINT - HINT + DO_NOT_SHOW WARNING HINT DO_NOT_SHOW From 82919998dac6d38de78a4c2041e068e2a09461c0 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Tue, 4 Jun 2024 18:26:32 +0200 Subject: [PATCH 1004/2493] dont light up tail piece when hovering anchor --- .../Sliders/Components/SliderTailPiece.cs | 28 +++---------------- 1 file changed, 4 insertions(+), 24 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderTailPiece.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderTailPiece.cs index cf8f909ff6..7d39f04596 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderTailPiece.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderTailPiece.cs @@ -50,38 +50,18 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => CirclePiece.ReceivePositionalInputAt(screenSpacePos); - protected override bool OnHover(HoverEvent e) + protected override void Update() { updateCirclePieceColour(); - return false; - } - - protected override void OnHoverLost(HoverLostEvent e) - { - updateCirclePieceColour(); - } - - protected override bool OnKeyDown(KeyDownEvent e) - { - if (!e.Repeat && (e.Key == Key.ShiftLeft || e.Key == Key.ShiftRight)) - updateCirclePieceColour(); - - return base.OnKeyDown(e); - } - - protected override void OnKeyUp(KeyUpEvent e) - { - if (e.Key == Key.ShiftLeft || e.Key == Key.ShiftRight) - updateCirclePieceColour(); - - base.OnKeyUp(e); + base.Update(); } private void updateCirclePieceColour() { Color4 colour = colours.Yellow; - if (IsHovered && inputManager.CurrentState.Keyboard.ShiftPressed) + if (IsHovered && inputManager.CurrentState.Keyboard.ShiftPressed + && !inputManager.HoveredDrawables.Any(o => o is PathControlPointPiece)) colour = colour.Lighten(1); CirclePiece.Colour = colour; From 7dd18a84f6f6a29df941c5aa2e2d7da6d7a29020 Mon Sep 17 00:00:00 2001 From: Xesquim Date: Wed, 5 Jun 2024 12:25:33 -0300 Subject: [PATCH 1005/2493] Fixing the GetTotalDuration in PlaylistExtesions --- osu.Game/Online/Rooms/PlaylistExtensions.cs | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/osu.Game/Online/Rooms/PlaylistExtensions.cs b/osu.Game/Online/Rooms/PlaylistExtensions.cs index cd52a3c6e6..3171716992 100644 --- a/osu.Game/Online/Rooms/PlaylistExtensions.cs +++ b/osu.Game/Online/Rooms/PlaylistExtensions.cs @@ -1,11 +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; using System.Collections.Generic; using System.Linq; using Humanizer; using Humanizer.Localisation; using osu.Framework.Bindables; +using osu.Game.Rulesets.Mods; namespace osu.Game.Online.Rooms { @@ -39,6 +41,17 @@ namespace osu.Game.Online.Rooms } public static string GetTotalDuration(this BindableList playlist) => - playlist.Select(p => p.Beatmap.Length).Sum().Milliseconds().Humanize(minUnit: TimeUnit.Second, maxUnit: TimeUnit.Hour, precision: 2); + playlist.Select(p => + { + var ruleset = p.Beatmap.Ruleset.CreateInstance(); + double rate = 1; + if (p.RequiredMods.Count() > 0) + { + List mods = p.RequiredMods.Select(mod => mod.ToMod(ruleset)).ToList(); + foreach (var mod in mods.OfType()) + rate = mod.ApplyToRate(0, rate); + } + return p.Beatmap.Length / rate; + }).Sum().Milliseconds().Humanize(minUnit: TimeUnit.Second, maxUnit: TimeUnit.Hour, precision: 2); } } From fe738a09517d7638781efffbd1efde61e05cb861 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Wed, 5 Jun 2024 18:12:02 +0200 Subject: [PATCH 1006/2493] Fix missing container inject --- osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs b/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs index b7bc533296..76e735449a 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs @@ -10,6 +10,7 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; +using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osu.Game.Input.Bindings; using osu.Game.Rulesets.Edit; @@ -26,6 +27,9 @@ namespace osu.Game.Rulesets.Osu.Edit [Resolved] private EditorBeatmap editorBeatmap { get; set; } = null!; + [Resolved] + private IExpandingContainer? expandingContainer { get; set; } + /// /// X position of the grid's origin. /// From 4f8c167cf96bb9732abd50788f5f003aa5ec38c9 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Wed, 5 Jun 2024 18:56:18 +0200 Subject: [PATCH 1007/2493] clean up to match logic in CircularDistanceSnapGrid --- .../Components/CircularPositionSnapGrid.cs | 40 ++++++++----------- 1 file changed, 16 insertions(+), 24 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/CircularPositionSnapGrid.cs b/osu.Game/Screens/Edit/Compose/Components/CircularPositionSnapGrid.cs index 791cb33439..8e63d6bcc0 100644 --- a/osu.Game/Screens/Edit/Compose/Components/CircularPositionSnapGrid.cs +++ b/osu.Game/Screens/Edit/Compose/Components/CircularPositionSnapGrid.cs @@ -7,7 +7,7 @@ using System.Linq; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.UserInterface; using osu.Framework.Utils; using osuTK; @@ -32,14 +32,14 @@ namespace osu.Game.Screens.Edit.Compose.Components { var drawSize = DrawSize; - // Calculate the maximum distance from the origin to the edge of the grid. - float maxDist = MathF.Max( - MathF.Max(StartPosition.Value.Length, (StartPosition.Value - drawSize).Length), - MathF.Max((StartPosition.Value - new Vector2(drawSize.X, 0)).Length, (StartPosition.Value - new Vector2(0, drawSize.Y)).Length) - ); - - generateCircles((int)(maxDist / Spacing.Value) + 1); + // Calculate the required number of circles based on the maximum distance from the origin to the edge of the grid. + float dx = Math.Max(StartPosition.Value.X, DrawWidth - StartPosition.Value.X); + float dy = Math.Max(StartPosition.Value.Y, DrawHeight - StartPosition.Value.Y); + float maxDistance = new Vector2(dx, dy).Length; + // We need to add one because the first circle starts at zero radius. + int requiredCircles = (int)(maxDistance / Spacing.Value) + 1; + generateCircles(requiredCircles); GenerateOutline(drawSize); } @@ -48,30 +48,22 @@ namespace osu.Game.Screens.Edit.Compose.Components // Make lines the same width independent of display resolution. float lineWidth = 2 * DrawWidth / ScreenSpaceDrawQuad.Width; - List generatedCircles = new List(); + List generatedCircles = new List(); for (int i = 0; i < count; i++) { // Add a minimum diameter so the center circle is clearly visible. float diameter = MathF.Max(lineWidth * 1.5f, i * Spacing.Value * 2); - var gridCircle = new CircularContainer + var gridCircle = new CircularProgress { - BorderColour = Colour4.White, - BorderThickness = lineWidth, - Alpha = 0.2f, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.None, - Width = diameter, - Height = diameter, Position = StartPosition.Value, - Masking = true, - Child = new Box - { - RelativeSizeAxes = Axes.Both, - AlwaysPresent = true, - Alpha = 0f, - } + Origin = Anchor.Centre, + Size = new Vector2(diameter), + InnerRadius = lineWidth * 1f / diameter, + Colour = Colour4.White, + Alpha = 0.2f, + Progress = 1, }; generatedCircles.Add(gridCircle); From 7a8a37dae66e4851c9a63e9fc00fe6ca3e050eef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 6 Jun 2024 13:39:32 +0200 Subject: [PATCH 1008/2493] Use established constants --- .../Edit/Compose/Components/Timeline/SamplePointPiece.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs index 5e3d6c5239..d90f62f79d 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs @@ -63,9 +63,9 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline { return bank switch { - "normal" => "N", - "soft" => "S", - "drum" => "D", + HitSampleInfo.BANK_NORMAL => @"N", + HitSampleInfo.BANK_SOFT => @"S", + HitSampleInfo.BANK_DRUM => @"D", _ => bank }; } From dd9c77d248ddb5c61f0b90f613f39f512bb24274 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 6 Jun 2024 13:50:31 +0200 Subject: [PATCH 1009/2493] Fix obsoletion warning --- .../Visual/Editing/TestSceneHitObjectSampleAdjustments.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneHitObjectSampleAdjustments.cs b/osu.Game.Tests/Visual/Editing/TestSceneHitObjectSampleAdjustments.cs index 425eddbcdb..f02d2a1bb1 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneHitObjectSampleAdjustments.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneHitObjectSampleAdjustments.cs @@ -502,7 +502,7 @@ namespace osu.Game.Tests.Visual.Editing textBox.Current.Value = bank; // force a commit via keyboard. // this is needed when testing attempting to set empty bank - which should revert to the previous value, but only on commit. - InputManager.ChangeFocus(textBox); + ((IFocusManager)InputManager).ChangeFocus(textBox); InputManager.Key(Key.Enter); }); From 71ce400359dc69c1f9af1980e802cd630380eadd Mon Sep 17 00:00:00 2001 From: OliBomby Date: Thu, 6 Jun 2024 14:48:17 +0200 Subject: [PATCH 1010/2493] Fix wasteful recreating of container --- .../Components/Timeline/TimelineHitObjectBlueprint.cs | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs index ab9ccf6278..753856199a 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs @@ -33,7 +33,6 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline private const float circle_size = 38; private Container? repeatsContainer; - private Container? nodeSamplesContainer; public Action? OnDragHandled = null!; @@ -246,16 +245,11 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline } // Add node sample pieces - nodeSamplesContainer?.Expire(); - - sampleComponents.Add(nodeSamplesContainer = new Container - { - RelativeSizeAxes = Axes.Both, - }); + sampleComponents.Clear(); for (int i = 0; i < repeats.RepeatCount + 2; i++) { - nodeSamplesContainer.Add(new NodeSamplePointPiece(Item, i) + sampleComponents.Add(new NodeSamplePointPiece(Item, i) { X = (float)i / (repeats.RepeatCount + 1), RelativePositionAxes = Axes.X, From fcc8671cbd5dcb844ff58f50920c3b45ca488e06 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Thu, 6 Jun 2024 14:50:24 +0200 Subject: [PATCH 1011/2493] undo useless change --- .../Screens/Edit/Compose/Components/EditorSelectionHandler.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/EditorSelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/EditorSelectionHandler.cs index c284ee2ebb..7c30b73122 100644 --- a/osu.Game/Screens/Edit/Compose/Components/EditorSelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/EditorSelectionHandler.cs @@ -226,9 +226,7 @@ namespace osu.Game.Screens.Edit.Compose.Components if (h.Samples.Any(s => s.Name == sampleName)) return; - var sampleToAdd = h.CreateHitSampleInfo(sampleName); - - h.Samples.Add(sampleToAdd); + h.Samples.Add(h.CreateHitSampleInfo(sampleName)); EditorBeatmap.Update(h); }); From e87369822182b75f8ee683547ffd3827d224303d Mon Sep 17 00:00:00 2001 From: OliBomby Date: Thu, 6 Jun 2024 14:57:25 +0200 Subject: [PATCH 1012/2493] Add some in-depth xmldoc to GetSamples --- .../Edit/Compose/Components/Timeline/SamplePointPiece.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs index d90f62f79d..64ad840591 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs @@ -85,6 +85,11 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline return samples.Count == 0 ? 0 : samples.Max(o => o.Volume); } + /// + /// Gets the samples to be edited by this sample point piece. + /// This could be the samples of the hit object itself, or of one of the nested hit objects. For example a slider repeat. + /// + /// The samples to be edited. protected virtual IList GetSamples() => HitObject.Samples; public virtual Popover GetPopover() => new SampleEditPopover(HitObject); From 860afb812367c21a88f667ad1d9431f75dab26ab Mon Sep 17 00:00:00 2001 From: Xesquim Date: Thu, 6 Jun 2024 10:06:07 -0300 Subject: [PATCH 1013/2493] Creating method in ModUtils to calculate the rate for the song --- .../Beatmaps/Drawables/DifficultyIconTooltip.cs | 9 ++------- osu.Game/Online/Rooms/PlaylistExtensions.cs | 12 +++++------- .../Overlays/Mods/BeatmapAttributesDisplay.cs | 4 +--- osu.Game/Screens/Select/BeatmapInfoWedge.cs | 4 +--- osu.Game/Screens/Select/Details/AdvancedStats.cs | 5 ++--- osu.Game/Utils/ModUtils.cs | 16 ++++++++++++++++ 6 files changed, 27 insertions(+), 23 deletions(-) diff --git a/osu.Game/Beatmaps/Drawables/DifficultyIconTooltip.cs b/osu.Game/Beatmaps/Drawables/DifficultyIconTooltip.cs index 1f3dcfee8c..a5cac69afd 100644 --- a/osu.Game/Beatmaps/Drawables/DifficultyIconTooltip.cs +++ b/osu.Game/Beatmaps/Drawables/DifficultyIconTooltip.cs @@ -13,6 +13,7 @@ using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; +using osu.Game.Utils; using osuTK; namespace osu.Game.Beatmaps.Drawables @@ -123,13 +124,7 @@ namespace osu.Game.Beatmaps.Drawables difficultyFillFlowContainer.Show(); miscFillFlowContainer.Show(); - double rate = 1; - - if (displayedContent.Mods != null) - { - foreach (var mod in displayedContent.Mods.OfType()) - rate = mod.ApplyToRate(0, rate); - } + double rate = ModUtils.CalculateRateWithMods(displayedContent.Mods); double bpmAdjusted = displayedContent.BeatmapInfo.BPM * rate; diff --git a/osu.Game/Online/Rooms/PlaylistExtensions.cs b/osu.Game/Online/Rooms/PlaylistExtensions.cs index 3171716992..4bcaaa8131 100644 --- a/osu.Game/Online/Rooms/PlaylistExtensions.cs +++ b/osu.Game/Online/Rooms/PlaylistExtensions.cs @@ -8,6 +8,7 @@ using Humanizer; using Humanizer.Localisation; using osu.Framework.Bindables; using osu.Game.Rulesets.Mods; +using osu.Game.Utils; namespace osu.Game.Online.Rooms { @@ -40,17 +41,14 @@ namespace osu.Game.Online.Rooms : GetUpcomingItems(playlist).First(); } + /// + /// Returns the total duration from the in playlist order from the supplied , + /// public static string GetTotalDuration(this BindableList playlist) => playlist.Select(p => { var ruleset = p.Beatmap.Ruleset.CreateInstance(); - double rate = 1; - if (p.RequiredMods.Count() > 0) - { - List mods = p.RequiredMods.Select(mod => mod.ToMod(ruleset)).ToList(); - foreach (var mod in mods.OfType()) - rate = mod.ApplyToRate(0, rate); - } + double rate = ModUtils.CalculateRateWithMods(p.RequiredMods.Select(mod => mod.ToMod(ruleset)).ToList()); return p.Beatmap.Length / rate; }).Sum().Milliseconds().Humanize(minUnit: TimeUnit.Second, maxUnit: TimeUnit.Hour, precision: 2); } diff --git a/osu.Game/Overlays/Mods/BeatmapAttributesDisplay.cs b/osu.Game/Overlays/Mods/BeatmapAttributesDisplay.cs index 5b10a2844e..b625de27f8 100644 --- a/osu.Game/Overlays/Mods/BeatmapAttributesDisplay.cs +++ b/osu.Game/Overlays/Mods/BeatmapAttributesDisplay.cs @@ -165,9 +165,7 @@ namespace osu.Game.Overlays.Mods starRatingDisplay.FinishTransforms(true); }); - double rate = 1; - foreach (var mod in Mods.Value.OfType()) - rate = mod.ApplyToRate(0, rate); + double rate = ModUtils.CalculateRateWithMods(Mods.Value.ToList()); bpmDisplay.Current.Value = FormatUtils.RoundBPM(BeatmapInfo.Value.BPM, rate); diff --git a/osu.Game/Screens/Select/BeatmapInfoWedge.cs b/osu.Game/Screens/Select/BeatmapInfoWedge.cs index 3cab4b67b6..0e2d1db6b7 100644 --- a/osu.Game/Screens/Select/BeatmapInfoWedge.cs +++ b/osu.Game/Screens/Select/BeatmapInfoWedge.cs @@ -402,9 +402,7 @@ namespace osu.Game.Screens.Select return; // this doesn't consider mods which apply variable rates, yet. - double rate = 1; - foreach (var mod in mods.Value.OfType()) - rate = mod.ApplyToRate(0, rate); + double rate = ModUtils.CalculateRateWithMods(mods.Value.ToList()); int bpmMax = FormatUtils.RoundBPM(beatmap.ControlPointInfo.BPMMaximum, rate); int bpmMin = FormatUtils.RoundBPM(beatmap.ControlPointInfo.BPMMinimum, rate); diff --git a/osu.Game/Screens/Select/Details/AdvancedStats.cs b/osu.Game/Screens/Select/Details/AdvancedStats.cs index cb820f4da9..1da890100e 100644 --- a/osu.Game/Screens/Select/Details/AdvancedStats.cs +++ b/osu.Game/Screens/Select/Details/AdvancedStats.cs @@ -27,6 +27,7 @@ using osu.Game.Configuration; using osu.Game.Resources.Localisation.Web; using osu.Game.Rulesets; using osu.Game.Overlays.Mods; +using osu.Game.Utils; namespace osu.Game.Screens.Select.Details { @@ -179,9 +180,7 @@ namespace osu.Game.Screens.Select.Details if (Ruleset.Value != null) { - double rate = 1; - foreach (var mod in mods.Value.OfType()) - rate = mod.ApplyToRate(0, rate); + double rate = ModUtils.CalculateRateWithMods(mods.Value); adjustedDifficulty = Ruleset.Value.CreateInstance().GetRateAdjustedDisplayDifficulty(originalDifficulty, rate); diff --git a/osu.Game/Utils/ModUtils.cs b/osu.Game/Utils/ModUtils.cs index 2c9eef41e3..3378e94ec0 100644 --- a/osu.Game/Utils/ModUtils.cs +++ b/osu.Game/Utils/ModUtils.cs @@ -276,5 +276,21 @@ namespace osu.Game.Utils return scoreMultiplier.ToLocalisableString("0.00x"); } + + /// + /// Calculate the rate for the song with the selected mods. + /// + /// The list of selected mods. + /// The rate with mods. + public static double CalculateRateWithMods(IEnumerable mods) + { + double rate = 1; + if (mods != null) + { + foreach (var mod in mods.OfType()) + rate = mod.ApplyToRate(0, rate); + } + return rate; + } } } From dd3f4bcdab623c3c7563d7cc191dc060cc518d37 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 6 Jun 2024 23:59:15 +0800 Subject: [PATCH 1014/2493] Fix code quality and null handling --- osu.Game/Beatmaps/Drawables/DifficultyIconTooltip.cs | 4 +++- osu.Game/Online/Rooms/PlaylistExtensions.cs | 4 +--- osu.Game/Overlays/Mods/BeatmapAttributesDisplay.cs | 2 +- osu.Game/Screens/Select/BeatmapInfoWedge.cs | 2 +- osu.Game/Utils/ModUtils.cs | 9 ++++----- 5 files changed, 10 insertions(+), 11 deletions(-) diff --git a/osu.Game/Beatmaps/Drawables/DifficultyIconTooltip.cs b/osu.Game/Beatmaps/Drawables/DifficultyIconTooltip.cs index a5cac69afd..36ddb6030e 100644 --- a/osu.Game/Beatmaps/Drawables/DifficultyIconTooltip.cs +++ b/osu.Game/Beatmaps/Drawables/DifficultyIconTooltip.cs @@ -124,7 +124,9 @@ namespace osu.Game.Beatmaps.Drawables difficultyFillFlowContainer.Show(); miscFillFlowContainer.Show(); - double rate = ModUtils.CalculateRateWithMods(displayedContent.Mods); + double rate = 1; + if (displayedContent.Mods != null) + rate = ModUtils.CalculateRateWithMods(displayedContent.Mods); double bpmAdjusted = displayedContent.BeatmapInfo.BPM * rate; diff --git a/osu.Game/Online/Rooms/PlaylistExtensions.cs b/osu.Game/Online/Rooms/PlaylistExtensions.cs index 4bcaaa8131..003fd23d40 100644 --- a/osu.Game/Online/Rooms/PlaylistExtensions.cs +++ b/osu.Game/Online/Rooms/PlaylistExtensions.cs @@ -1,13 +1,11 @@ // 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 Humanizer; using Humanizer.Localisation; using osu.Framework.Bindables; -using osu.Game.Rulesets.Mods; using osu.Game.Utils; namespace osu.Game.Online.Rooms @@ -48,7 +46,7 @@ namespace osu.Game.Online.Rooms playlist.Select(p => { var ruleset = p.Beatmap.Ruleset.CreateInstance(); - double rate = ModUtils.CalculateRateWithMods(p.RequiredMods.Select(mod => mod.ToMod(ruleset)).ToList()); + double rate = ModUtils.CalculateRateWithMods(p.RequiredMods.Select(mod => mod.ToMod(ruleset))); return p.Beatmap.Length / rate; }).Sum().Milliseconds().Humanize(minUnit: TimeUnit.Second, maxUnit: TimeUnit.Hour, precision: 2); } diff --git a/osu.Game/Overlays/Mods/BeatmapAttributesDisplay.cs b/osu.Game/Overlays/Mods/BeatmapAttributesDisplay.cs index b625de27f8..1f4e007f47 100644 --- a/osu.Game/Overlays/Mods/BeatmapAttributesDisplay.cs +++ b/osu.Game/Overlays/Mods/BeatmapAttributesDisplay.cs @@ -165,7 +165,7 @@ namespace osu.Game.Overlays.Mods starRatingDisplay.FinishTransforms(true); }); - double rate = ModUtils.CalculateRateWithMods(Mods.Value.ToList()); + double rate = ModUtils.CalculateRateWithMods(Mods.Value); bpmDisplay.Current.Value = FormatUtils.RoundBPM(BeatmapInfo.Value.BPM, rate); diff --git a/osu.Game/Screens/Select/BeatmapInfoWedge.cs b/osu.Game/Screens/Select/BeatmapInfoWedge.cs index 0e2d1db6b7..02682c1851 100644 --- a/osu.Game/Screens/Select/BeatmapInfoWedge.cs +++ b/osu.Game/Screens/Select/BeatmapInfoWedge.cs @@ -402,7 +402,7 @@ namespace osu.Game.Screens.Select return; // this doesn't consider mods which apply variable rates, yet. - double rate = ModUtils.CalculateRateWithMods(mods.Value.ToList()); + double rate = ModUtils.CalculateRateWithMods(mods.Value); int bpmMax = FormatUtils.RoundBPM(beatmap.ControlPointInfo.BPMMaximum, rate); int bpmMin = FormatUtils.RoundBPM(beatmap.ControlPointInfo.BPMMinimum, rate); diff --git a/osu.Game/Utils/ModUtils.cs b/osu.Game/Utils/ModUtils.cs index 3378e94ec0..f901f15388 100644 --- a/osu.Game/Utils/ModUtils.cs +++ b/osu.Game/Utils/ModUtils.cs @@ -285,11 +285,10 @@ namespace osu.Game.Utils public static double CalculateRateWithMods(IEnumerable mods) { double rate = 1; - if (mods != null) - { - foreach (var mod in mods.OfType()) - rate = mod.ApplyToRate(0, rate); - } + + foreach (var mod in mods.OfType()) + rate = mod.ApplyToRate(0, rate); + return rate; } } From 6e3bea938e1ecfe255bc4dcfe558870007e8cb6e Mon Sep 17 00:00:00 2001 From: Xesquim Date: Thu, 6 Jun 2024 13:26:52 -0300 Subject: [PATCH 1015/2493] Instancing a Ruleset only when it's necessary to --- osu.Game/Online/Rooms/PlaylistExtensions.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/osu.Game/Online/Rooms/PlaylistExtensions.cs b/osu.Game/Online/Rooms/PlaylistExtensions.cs index 003fd23d40..5a5950333b 100644 --- a/osu.Game/Online/Rooms/PlaylistExtensions.cs +++ b/osu.Game/Online/Rooms/PlaylistExtensions.cs @@ -6,6 +6,7 @@ using System.Linq; using Humanizer; using Humanizer.Localisation; using osu.Framework.Bindables; +using osu.Game.Rulesets.Mods; using osu.Game.Utils; namespace osu.Game.Online.Rooms @@ -45,8 +46,13 @@ namespace osu.Game.Online.Rooms public static string GetTotalDuration(this BindableList playlist) => playlist.Select(p => { - var ruleset = p.Beatmap.Ruleset.CreateInstance(); - double rate = ModUtils.CalculateRateWithMods(p.RequiredMods.Select(mod => mod.ToMod(ruleset))); + IEnumerable modList = []; + if (p.RequiredMods.Length > 0) + { + var ruleset = p.Beatmap.Ruleset.CreateInstance(); + modList = p.RequiredMods.Select(mod => mod.ToMod(ruleset)); + } + double rate = ModUtils.CalculateRateWithMods(modList); return p.Beatmap.Length / rate; }).Sum().Milliseconds().Humanize(minUnit: TimeUnit.Second, maxUnit: TimeUnit.Hour, precision: 2); } From 7cbe93efc3dcef9389922f034b12a92c6777aca7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 7 Jun 2024 10:37:27 +0800 Subject: [PATCH 1016/2493] Refactor latest changes to avoid unnecessary call when mods not present --- osu.Game/Online/Rooms/PlaylistExtensions.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Online/Rooms/PlaylistExtensions.cs b/osu.Game/Online/Rooms/PlaylistExtensions.cs index 5a5950333b..e9a0519f3d 100644 --- a/osu.Game/Online/Rooms/PlaylistExtensions.cs +++ b/osu.Game/Online/Rooms/PlaylistExtensions.cs @@ -6,7 +6,6 @@ using System.Linq; using Humanizer; using Humanizer.Localisation; using osu.Framework.Bindables; -using osu.Game.Rulesets.Mods; using osu.Game.Utils; namespace osu.Game.Online.Rooms @@ -46,13 +45,14 @@ namespace osu.Game.Online.Rooms public static string GetTotalDuration(this BindableList playlist) => playlist.Select(p => { - IEnumerable modList = []; + double rate = 1; + if (p.RequiredMods.Length > 0) { var ruleset = p.Beatmap.Ruleset.CreateInstance(); - modList = p.RequiredMods.Select(mod => mod.ToMod(ruleset)); + rate = ModUtils.CalculateRateWithMods(p.RequiredMods.Select(mod => mod.ToMod(ruleset))); } - double rate = ModUtils.CalculateRateWithMods(modList); + return p.Beatmap.Length / rate; }).Sum().Milliseconds().Humanize(minUnit: TimeUnit.Second, maxUnit: TimeUnit.Hour, precision: 2); } From e13e9abda9c051dfa2c61a45de4098f6e8349bf8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 7 Jun 2024 08:09:57 +0200 Subject: [PATCH 1017/2493] Disallow running save-related operations concurrently Closes https://github.com/ppy/osu/issues/25426. Different approach to prior ones, this just disables the relevant actions when something related to save/export is going on. Still ends up being convoluted because many things you wouldn't expect to touch save do touch save, so it's not just a concern between export and save specifically. --- osu.Game/Screens/Edit/BottomBar.cs | 12 +++ osu.Game/Screens/Edit/Editor.cs | 118 ++++++++++++++++++++++------- 2 files changed, 102 insertions(+), 28 deletions(-) diff --git a/osu.Game/Screens/Edit/BottomBar.cs b/osu.Game/Screens/Edit/BottomBar.cs index bc7dfaab88..612aa26c84 100644 --- a/osu.Game/Screens/Edit/BottomBar.cs +++ b/osu.Game/Screens/Edit/BottomBar.cs @@ -4,6 +4,7 @@ #nullable disable using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -22,6 +23,8 @@ namespace osu.Game.Screens.Edit { public TestGameplayButton TestGameplayButton { get; private set; } + private IBindable saveInProgress; + [BackgroundDependencyLoader] private void load(OverlayColourProvider colourProvider, Editor editor) { @@ -74,6 +77,15 @@ namespace osu.Game.Screens.Edit } } }; + + saveInProgress = editor.SaveTracker.InProgress.GetBoundCopy(); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + saveInProgress.BindValueChanged(_ => TestGameplayButton.Enabled.Value = !saveInProgress.Value, true); } } } diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 07c32983f5..991b92cca6 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -49,6 +49,7 @@ using osu.Game.Screens.Edit.GameplayTest; using osu.Game.Screens.Edit.Setup; using osu.Game.Screens.Edit.Timing; using osu.Game.Screens.Edit.Verify; +using osu.Game.Screens.OnlinePlay; using osu.Game.Screens.Play; using osu.Game.Users; using osuTK.Input; @@ -142,6 +143,8 @@ namespace osu.Game.Screens.Edit private readonly Bindable samplePlaybackDisabled = new Bindable(); private bool canSave; + private readonly List saveRelatedMenuItems = new List(); + public OngoingOperationTracker SaveTracker { get; private set; } = new OngoingOperationTracker(); protected bool ExitConfirmed { get; private set; } @@ -328,7 +331,7 @@ namespace osu.Game.Screens.Edit { new MenuItem(CommonStrings.MenuBarFile) { - Items = createFileMenuItems() + Items = createFileMenuItems().ToList() }, new MenuItem(CommonStrings.MenuBarEdit) { @@ -382,6 +385,7 @@ namespace osu.Game.Screens.Edit }, }, bottomBar = new BottomBar(), + SaveTracker, } }); changeHandler?.CanUndo.BindValueChanged(v => undoMenuItem.Action.Disabled = !v.NewValue, true); @@ -402,6 +406,12 @@ namespace osu.Game.Screens.Edit Mode.BindValueChanged(onModeChanged, true); musicController.TrackChanged += onTrackChanged; + + SaveTracker.InProgress.BindValueChanged(_ => + { + foreach (var item in saveRelatedMenuItems) + item.Action.Disabled = SaveTracker.InProgress.Value; + }, true); } protected override void Dispose(bool isDisposing) @@ -442,9 +452,14 @@ namespace osu.Game.Screens.Edit { dialogOverlay.Push(new SaveRequiredPopupDialog("The beatmap will be saved in order to test it.", () => { - if (!Save()) return; + if (SaveTracker.InProgress.Value) return; - pushEditorPlayer(); + using (SaveTracker.BeginOperation()) + { + if (!Save()) return; + + pushEditorPlayer(); + } })); } else @@ -520,7 +535,11 @@ namespace osu.Game.Screens.Edit if (e.Repeat) return false; - Save(); + if (SaveTracker.InProgress.Value) + return false; + + using (SaveTracker.BeginOperation()) + Save(); return true; } @@ -787,7 +806,13 @@ namespace osu.Game.Screens.Edit private void confirmExitWithSave() { - if (!Save()) return; + if (SaveTracker.InProgress.Value) return; + + using (SaveTracker.BeginOperation()) + { + if (!Save()) + return; + } ExitConfirmed = true; this.Exit(); @@ -1020,25 +1045,41 @@ namespace osu.Game.Screens.Edit lastSavedHash = changeHandler?.CurrentStateHash; } - private List createFileMenuItems() => new List + private IEnumerable createFileMenuItems() { - createDifficultyCreationMenu(), - createDifficultySwitchMenu(), - new OsuMenuItemSpacer(), - new EditorMenuItem(EditorStrings.DeleteDifficulty, MenuItemType.Standard, deleteDifficulty) { Action = { Disabled = Beatmap.Value.BeatmapSetInfo.Beatmaps.Count < 2 } }, - new OsuMenuItemSpacer(), - new EditorMenuItem(WebCommonStrings.ButtonsSave, MenuItemType.Standard, () => Save()), - createExportMenu(), - new OsuMenuItemSpacer(), - new EditorMenuItem(CommonStrings.Exit, MenuItemType.Standard, this.Exit) - }; + yield return createDifficultyCreationMenu(); + yield return createDifficultySwitchMenu(); + yield return new OsuMenuItemSpacer(); + yield return new EditorMenuItem(EditorStrings.DeleteDifficulty, MenuItemType.Standard, deleteDifficulty) { Action = { Disabled = Beatmap.Value.BeatmapSetInfo.Beatmaps.Count < 2 } }; + yield return new OsuMenuItemSpacer(); + + var save = new EditorMenuItem(WebCommonStrings.ButtonsSave, MenuItemType.Standard, () => + { + if (SaveTracker.InProgress.Value) return; + + using (SaveTracker.BeginOperation()) + Save(); + }); + saveRelatedMenuItems.Add(save); + yield return save; + + if (RuntimeInfo.IsDesktop) + { + var export = createExportMenu(); + saveRelatedMenuItems.AddRange(export.Items); + yield return export; + } + + yield return new OsuMenuItemSpacer(); + yield return new EditorMenuItem(CommonStrings.Exit, MenuItemType.Standard, this.Exit); + } private EditorMenuItem createExportMenu() { var exportItems = new List { - new EditorMenuItem(EditorStrings.ExportForEditing, MenuItemType.Standard, () => exportBeatmap(false)) { Action = { Disabled = !RuntimeInfo.IsDesktop } }, - new EditorMenuItem(EditorStrings.ExportForCompatibility, MenuItemType.Standard, () => exportBeatmap(true)) { Action = { Disabled = !RuntimeInfo.IsDesktop } }, + new EditorMenuItem(EditorStrings.ExportForEditing, MenuItemType.Standard, () => exportBeatmap(false)), + new EditorMenuItem(EditorStrings.ExportForCompatibility, MenuItemType.Standard, () => exportBeatmap(true)), }; return new EditorMenuItem(CommonStrings.Export) { Items = exportItems }; @@ -1050,22 +1091,35 @@ namespace osu.Game.Screens.Edit { dialogOverlay.Push(new SaveRequiredPopupDialog("The beatmap will be saved in order to export it.", () => { - if (!Save()) return; + if (SaveTracker.InProgress.Value) + return; - runExport(); + var operation = SaveTracker.BeginOperation(); + + if (!Save()) + { + operation.Dispose(); + return; + } + + runExport(operation); })); } else { - runExport(); + if (SaveTracker.InProgress.Value) + return; + + runExport(SaveTracker.BeginOperation()); } - void runExport() + void runExport(IDisposable operationInProgress) { - if (legacy) - beatmapManager.ExportLegacy(Beatmap.Value.BeatmapSetInfo); - else - beatmapManager.Export(Beatmap.Value.BeatmapSetInfo); + var task = legacy + ? beatmapManager.ExportLegacy(Beatmap.Value.BeatmapSetInfo) + : beatmapManager.Export(Beatmap.Value.BeatmapSetInfo); + + task.ContinueWith(_ => operationInProgress.Dispose()); } } @@ -1116,6 +1170,8 @@ namespace osu.Game.Screens.Edit foreach (var ruleset in rulesets.AvailableRulesets) rulesetItems.Add(new EditorMenuItem(ruleset.Name, MenuItemType.Standard, () => CreateNewDifficulty(ruleset))); + saveRelatedMenuItems.AddRange(rulesetItems); + return new EditorMenuItem(EditorStrings.CreateNewDifficulty) { Items = rulesetItems }; } @@ -1125,10 +1181,16 @@ namespace osu.Game.Screens.Edit { dialogOverlay.Push(new SaveRequiredPopupDialog("This beatmap will be saved in order to create another difficulty.", () => { - if (!Save()) + if (SaveTracker.InProgress.Value) return; - CreateNewDifficulty(rulesetInfo); + using (SaveTracker.BeginOperation()) + { + if (!Save()) + return; + + CreateNewDifficulty(rulesetInfo); + } })); return; From 629e7652c0e57639d64ade90ad10b12d64a650d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 7 Jun 2024 09:01:41 +0200 Subject: [PATCH 1018/2493] Implement flip operations in mania editor --- .../Edit/ManiaSelectionHandler.cs | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs b/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs index 8fdbada04f..9ae2112b30 100644 --- a/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs +++ b/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs @@ -4,6 +4,7 @@ using System; using System.Linq; using osu.Framework.Allocation; +using osu.Framework.Graphics; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Objects; @@ -16,6 +17,16 @@ namespace osu.Game.Rulesets.Mania.Edit [Resolved] private HitObjectComposer composer { get; set; } = null!; + protected override void OnSelectionChanged() + { + base.OnSelectionChanged(); + + var selectedObjects = SelectedItems.OfType().ToArray(); + + SelectionBox.CanFlipX = canFlipX(selectedObjects); + SelectionBox.CanFlipY = canFlipY(selectedObjects); + } + public override bool HandleMovement(MoveSelectionEvent moveEvent) { var hitObjectBlueprint = (HitObjectSelectionBlueprint)moveEvent.Blueprint; @@ -26,6 +37,58 @@ namespace osu.Game.Rulesets.Mania.Edit return true; } + public override bool HandleFlip(Direction direction, bool flipOverOrigin) + { + var selectedObjects = SelectedItems.OfType().ToArray(); + var maniaPlayfield = ((ManiaHitObjectComposer)composer).Playfield; + + if (selectedObjects.Length == 0) + return false; + + switch (direction) + { + case Direction.Horizontal: + if (!canFlipX(selectedObjects)) + return false; + + int firstColumn = flipOverOrigin ? 0 : selectedObjects.Min(ho => ho.Column); + int lastColumn = flipOverOrigin ? (int)EditorBeatmap.BeatmapInfo.Difficulty.CircleSize - 1 : selectedObjects.Max(ho => ho.Column); + + EditorBeatmap.PerformOnSelection(hitObject => + { + var maniaObject = (ManiaHitObject)hitObject; + maniaPlayfield.Remove(maniaObject); + maniaObject.Column = firstColumn + (lastColumn - maniaObject.Column); + maniaPlayfield.Add(maniaObject); + }); + + return true; + + case Direction.Vertical: + if (!canFlipY(selectedObjects)) + return false; + + double selectionStartTime = selectedObjects.Min(ho => ho.StartTime); + double selectionEndTime = selectedObjects.Max(ho => ho.GetEndTime()); + + EditorBeatmap.PerformOnSelection(hitObject => + { + hitObject.StartTime = selectionStartTime + (selectionEndTime - hitObject.GetEndTime()); + }); + + return true; + + default: + throw new ArgumentOutOfRangeException(nameof(direction), direction, "Cannot flip over the supplied direction."); + } + } + + private static bool canFlipX(ManiaHitObject[] selectedObjects) + => selectedObjects.Select(ho => ho.Column).Distinct().Count() > 1; + + private static bool canFlipY(ManiaHitObject[] selectedObjects) + => selectedObjects.Length > 1 && selectedObjects.Min(ho => ho.StartTime) < selectedObjects.Max(ho => ho.GetEndTime()); + private void performColumnMovement(int lastColumn, MoveSelectionEvent moveEvent) { var maniaPlayfield = ((ManiaHitObjectComposer)composer).Playfield; From f787a29f49ae8ed8bde13ef0663f3604921c1b92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 7 Jun 2024 09:19:17 +0200 Subject: [PATCH 1019/2493] Add test coverage --- .../Editor/TestSceneManiaSelectionHandler.cs | 96 +++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaSelectionHandler.cs diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaSelectionHandler.cs b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaSelectionHandler.cs new file mode 100644 index 0000000000..b48f579ec0 --- /dev/null +++ b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaSelectionHandler.cs @@ -0,0 +1,96 @@ +// 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.Testing; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Mania.Objects; +using osu.Game.Screens.Edit.Compose.Components; +using osu.Game.Tests.Beatmaps; +using osu.Game.Tests.Visual; +using osuTK.Input; + +namespace osu.Game.Rulesets.Mania.Tests.Editor +{ + public partial class TestSceneManiaSelectionHandler : EditorTestScene + { + protected override Ruleset CreateEditorRuleset() => new ManiaRuleset(); + + protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(ruleset, false); + + [Test] + public void TestHorizontalFlipOverSelection() + { + ManiaHitObject first = null!, second = null!, third = null!; + + AddStep("create objects", () => + { + EditorBeatmap.Add(first = new Note { StartTime = 250, Column = 2 }); + EditorBeatmap.Add(second = new HoldNote { StartTime = 750, Duration = 1500, Column = 1 }); + EditorBeatmap.Add(third = new Note { StartTime = 1250, Column = 3 }); + }); + + AddStep("select everything", () => EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects)); + AddStep("flip horizontally over selection", () => + { + InputManager.MoveMouseTo(this.ChildrenOfType().First()); + InputManager.Click(MouseButton.Left); + }); + + AddAssert("first object stayed in place", () => first.Column, () => Is.EqualTo(2)); + AddAssert("second object flipped", () => second.Column, () => Is.EqualTo(3)); + AddAssert("third object flipped", () => third.Column, () => Is.EqualTo(1)); + } + + [Test] + public void TestHorizontalFlipOverPlayfield() + { + ManiaHitObject first = null!, second = null!, third = null!; + + AddStep("create objects", () => + { + EditorBeatmap.Add(first = new Note { StartTime = 250, Column = 2 }); + EditorBeatmap.Add(second = new HoldNote { StartTime = 750, Duration = 1500, Column = 1 }); + EditorBeatmap.Add(third = new Note { StartTime = 1250, Column = 3 }); + }); + + AddStep("select everything", () => EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects)); + AddStep("flip horizontally", () => + { + InputManager.PressKey(Key.ControlLeft); + InputManager.Key(Key.H); + InputManager.ReleaseKey(Key.ControlLeft); + }); + + AddAssert("first object flipped", () => first.Column, () => Is.EqualTo(1)); + AddAssert("second object flipped", () => second.Column, () => Is.EqualTo(2)); + AddAssert("third object flipped", () => third.Column, () => Is.EqualTo(0)); + } + + [Test] + public void TestVerticalFlip() + { + ManiaHitObject first = null!, second = null!, third = null!; + + AddStep("create objects", () => + { + EditorBeatmap.Add(first = new Note { StartTime = 250, Column = 2 }); + EditorBeatmap.Add(second = new HoldNote { StartTime = 750, Duration = 1500, Column = 1 }); + EditorBeatmap.Add(third = new Note { StartTime = 1250, Column = 3 }); + }); + + AddStep("select everything", () => EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects)); + AddStep("flip vertically", () => + { + InputManager.PressKey(Key.ControlLeft); + InputManager.Key(Key.J); + InputManager.ReleaseKey(Key.ControlLeft); + }); + + AddAssert("first object flipped", () => first.StartTime, () => Is.EqualTo(2250)); + AddAssert("second object flipped", () => second.StartTime, () => Is.EqualTo(250)); + AddAssert("third object flipped", () => third.StartTime, () => Is.EqualTo(1250)); + } + } +} From 199d31c0f4f68b1049cd0692d172273f57689bda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 7 Jun 2024 09:54:00 +0200 Subject: [PATCH 1020/2493] Fix test not compiling A little ugly but maybe it'll do... --- .../Gameplay/TestSceneSkinnableRankDisplay.cs | 24 +++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableRankDisplay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableRankDisplay.cs index dc8b3d994b..d442e69c61 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableRankDisplay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableRankDisplay.cs @@ -3,9 +3,11 @@ using NUnit.Framework; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Scoring; +using osu.Game.Scoring; using osu.Game.Screens.Play.HUD; using osu.Game.Skinning; @@ -16,6 +18,8 @@ namespace osu.Game.Tests.Visual.Gameplay [Cached] private ScoreProcessor scoreProcessor = new ScoreProcessor(new OsuRuleset()); + private Bindable rank => (Bindable)scoreProcessor.Rank; + protected override Drawable CreateDefaultImplementation() => new DefaultRankDisplay(); protected override Drawable CreateLegacyImplementation() => new LegacyRankDisplay(); @@ -23,15 +27,15 @@ namespace osu.Game.Tests.Visual.Gameplay [Test] public void TestChangingRank() { - AddStep("Set rank to SS Hidden", () => scoreProcessor.Rank.Value = Scoring.ScoreRank.XH); - AddStep("Set rank to SS", () => scoreProcessor.Rank.Value = Scoring.ScoreRank.X); - AddStep("Set rank to S Hidden", () => scoreProcessor.Rank.Value = Scoring.ScoreRank.SH); - AddStep("Set rank to S", () => scoreProcessor.Rank.Value = Scoring.ScoreRank.S); - AddStep("Set rank to A", () => scoreProcessor.Rank.Value = Scoring.ScoreRank.A); - AddStep("Set rank to B", () => scoreProcessor.Rank.Value = Scoring.ScoreRank.B); - AddStep("Set rank to C", () => scoreProcessor.Rank.Value = Scoring.ScoreRank.C); - AddStep("Set rank to D", () => scoreProcessor.Rank.Value = Scoring.ScoreRank.D); - AddStep("Set rank to F", () => scoreProcessor.Rank.Value = Scoring.ScoreRank.F); + AddStep("Set rank to SS Hidden", () => rank.Value = ScoreRank.XH); + AddStep("Set rank to SS", () => rank.Value = ScoreRank.X); + AddStep("Set rank to S Hidden", () => rank.Value = ScoreRank.SH); + AddStep("Set rank to S", () => rank.Value = ScoreRank.S); + AddStep("Set rank to A", () => rank.Value = ScoreRank.A); + AddStep("Set rank to B", () => rank.Value = ScoreRank.B); + AddStep("Set rank to C", () => rank.Value = ScoreRank.C); + AddStep("Set rank to D", () => rank.Value = ScoreRank.D); + AddStep("Set rank to F", () => rank.Value = ScoreRank.F); } } -} \ No newline at end of file +} From 72890bb9acf9ab6b3733dd0d3ede0ad6961a45a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 7 Jun 2024 09:54:27 +0200 Subject: [PATCH 1021/2493] Add stable-like animation legacy rank display Just substituting the sprite felt pretty terrible. --- osu.Game/Skinning/LegacyRankDisplay.cs | 33 +++++++++++++++++++++++--- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/osu.Game/Skinning/LegacyRankDisplay.cs b/osu.Game/Skinning/LegacyRankDisplay.cs index 38ece4e5e4..71d487eade 100644 --- a/osu.Game/Skinning/LegacyRankDisplay.cs +++ b/osu.Game/Skinning/LegacyRankDisplay.cs @@ -6,6 +6,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; using osu.Game.Rulesets.Scoring; +using osuTK; namespace osu.Game.Skinning { @@ -25,12 +26,38 @@ namespace osu.Game.Skinning { AutoSizeAxes = Axes.Both; - AddInternal(rank = new Sprite()); + AddInternal(rank = new Sprite + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }); } protected override void LoadComplete() { - scoreProcessor.Rank.BindValueChanged(v => rank.Texture = source.GetTexture($"ranking-{v.NewValue}-small"), true); + scoreProcessor.Rank.BindValueChanged(v => + { + var texture = source.GetTexture($"ranking-{v.NewValue}-small"); + + rank.Texture = texture; + + if (texture != null) + { + var transientRank = new Sprite + { + Texture = texture, + Blending = BlendingParameters.Additive, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + BypassAutoSizeAxes = Axes.Both, + }; + AddInternal(transientRank); + transientRank.FadeOutFromOne(1200, Easing.Out) + .ScaleTo(new Vector2(1.625f), 1200, Easing.Out) + .Expire(); + } + }, true); + FinishTransforms(true); } } -} \ No newline at end of file +} From 366ef64a2c12e1f7f8a1ff7c975fe0fe5ed7ac90 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 7 Jun 2024 16:54:12 +0800 Subject: [PATCH 1022/2493] Apply NRT to `UpdateableRank` --- osu.Game/Online/Leaderboards/UpdateableRank.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/osu.Game/Online/Leaderboards/UpdateableRank.cs b/osu.Game/Online/Leaderboards/UpdateableRank.cs index 46cfe8ec65..717adee79d 100644 --- a/osu.Game/Online/Leaderboards/UpdateableRank.cs +++ b/osu.Game/Online/Leaderboards/UpdateableRank.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.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Scoring; @@ -22,7 +20,7 @@ namespace osu.Game.Online.Leaderboards Rank = rank; } - protected override Drawable CreateDrawable(ScoreRank? rank) + protected override Drawable? CreateDrawable(ScoreRank? rank) { if (rank.HasValue) { From 9c6e707f00454f9370a8ee3b0424903c9c9b917c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 7 Jun 2024 17:04:16 +0800 Subject: [PATCH 1023/2493] Adjust transitions --- .../Online/Leaderboards/UpdateableRank.cs | 28 +++++++++++++++++++ osu.Game/Skinning/LegacyRankDisplay.cs | 4 +-- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/osu.Game/Online/Leaderboards/UpdateableRank.cs b/osu.Game/Online/Leaderboards/UpdateableRank.cs index 717adee79d..b64fab6861 100644 --- a/osu.Game/Online/Leaderboards/UpdateableRank.cs +++ b/osu.Game/Online/Leaderboards/UpdateableRank.cs @@ -1,14 +1,19 @@ // 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.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Transforms; using osu.Game.Scoring; namespace osu.Game.Online.Leaderboards { public partial class UpdateableRank : ModelBackedDrawable { + protected override double TransformDuration => 600; + protected override bool TransformImmediately => true; + public ScoreRank? Rank { get => Model; @@ -20,6 +25,16 @@ namespace osu.Game.Online.Leaderboards Rank = rank; } + protected override DelayedLoadWrapper CreateDelayedLoadWrapper(Func createContentFunc, double timeBeforeLoad) + { + return base.CreateDelayedLoadWrapper(createContentFunc, timeBeforeLoad) + .With(w => + { + w.Anchor = Anchor.Centre; + w.Origin = Anchor.Centre; + }); + } + protected override Drawable? CreateDrawable(ScoreRank? rank) { if (rank.HasValue) @@ -33,5 +48,18 @@ namespace osu.Game.Online.Leaderboards return null; } + + protected override TransformSequence ApplyShowTransforms(Drawable drawable) + { + drawable.ScaleTo(1); + return base.ApplyShowTransforms(drawable); + } + + protected override TransformSequence ApplyHideTransforms(Drawable drawable) + { + drawable.ScaleTo(1.8f, TransformDuration, Easing.Out); + + return base.ApplyHideTransforms(drawable); + } } } diff --git a/osu.Game/Skinning/LegacyRankDisplay.cs b/osu.Game/Skinning/LegacyRankDisplay.cs index 71d487eade..70b5ed0278 100644 --- a/osu.Game/Skinning/LegacyRankDisplay.cs +++ b/osu.Game/Skinning/LegacyRankDisplay.cs @@ -52,8 +52,8 @@ namespace osu.Game.Skinning BypassAutoSizeAxes = Axes.Both, }; AddInternal(transientRank); - transientRank.FadeOutFromOne(1200, Easing.Out) - .ScaleTo(new Vector2(1.625f), 1200, Easing.Out) + transientRank.FadeOutFromOne(500, Easing.Out) + .ScaleTo(new Vector2(1.625f), 500, Easing.Out) .Expire(); } }, true); From f59d94bba4171a0dff65be3b10f3c9576f00795b Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 7 Jun 2024 22:07:37 +0300 Subject: [PATCH 1024/2493] Move transitions inside `ScreenFooterButton` and re-use `Content` from base implementation instead The point is to apply the transitions against a container that's inside of `ScreenFooterButton`, because the `ScreenFooterButton` drawable's position is being controlled by the flow container it's contained within, and we cannot apply the transitions on it directly. --- osu.Game/Screens/Footer/ScreenFooter.cs | 54 ++---- osu.Game/Screens/Footer/ScreenFooterButton.cs | 173 +++++++++++------- .../SelectV2/Footer/ScreenFooterButtonMods.cs | 2 +- 3 files changed, 115 insertions(+), 114 deletions(-) diff --git a/osu.Game/Screens/Footer/ScreenFooter.cs b/osu.Game/Screens/Footer/ScreenFooter.cs index d299bf7362..9e0f657e8b 100644 --- a/osu.Game/Screens/Footer/ScreenFooter.cs +++ b/osu.Game/Screens/Footer/ScreenFooter.cs @@ -97,11 +97,9 @@ namespace osu.Game.Screens.Footer removedButtonsContainer.Add(oldButton); if (buttons.Count > 0) - fadeButtonToLeft(oldButton, i, oldButtons.Length); + makeButtonDisappearToRightAndExpire(oldButton, i, oldButtons.Length); else - fadeButtonToBottom(oldButton, i, oldButtons.Length); - - Scheduler.AddDelayed(() => oldButton.Expire(), oldButton.TopLevelContent.LatestTransformEndTime - Time.Current); + makeButtonDisappearToBottomAndExpire(oldButton, i, oldButtons.Length); } for (int i = 0; i < buttons.Count; i++) @@ -123,52 +121,24 @@ namespace osu.Game.Screens.Footer newButton.OnLoadComplete += _ => { if (oldButtons.Length > 0) - fadeButtonFromRight(newButton, index, buttons.Count, 240); + makeButtonAppearFromLeft(newButton, index, buttons.Count, 240); else - fadeButtonFromBottom(newButton, index); + makeButtonAppearFromBottom(newButton, index); }; } } - private void fadeButtonFromRight(ScreenFooterButton button, int index, int count, float startDelay) - { - button.TopLevelContent - .MoveToX(-300f) - .FadeOut(); + private void makeButtonAppearFromLeft(ScreenFooterButton button, int index, int count, float startDelay) + => button.AppearFromLeft(startDelay + (count - index) * delay_per_button); - button.TopLevelContent - .Delay(startDelay + (count - index) * delay_per_button) - .MoveToX(0f, 240, Easing.OutCubic) - .FadeIn(240, Easing.OutCubic); - } + private void makeButtonAppearFromBottom(ScreenFooterButton button, int index) + => button.AppearFromBottom(index * delay_per_button); - private void fadeButtonFromBottom(ScreenFooterButton button, int index) - { - button.TopLevelContent - .MoveToY(100f) - .FadeOut(); + private void makeButtonDisappearToRightAndExpire(ScreenFooterButton button, int index, int count) + => button.DisappearToRightAndExpire((count - index) * delay_per_button); - button.TopLevelContent - .Delay(index * delay_per_button) - .MoveToY(0f, 240, Easing.OutCubic) - .FadeIn(240, Easing.OutCubic); - } - - private void fadeButtonToLeft(ScreenFooterButton button, int index, int count) - { - button.TopLevelContent - .Delay((count - index) * delay_per_button) - .FadeOut(240, Easing.InOutCubic) - .MoveToX(300f, 360, Easing.InOutCubic); - } - - private void fadeButtonToBottom(ScreenFooterButton button, int index, int count) - { - button.TopLevelContent - .Delay((count - index) * delay_per_button) - .FadeOut(240, Easing.InOutCubic) - .MoveToY(100f, 240, Easing.InOutCubic); - } + private void makeButtonDisappearToBottomAndExpire(ScreenFooterButton button, int index, int count) + => button.DisappearToBottomAndExpire((count - index) * delay_per_button); private void showOverlay(OverlayContainer overlay) { diff --git a/osu.Game/Screens/Footer/ScreenFooterButton.cs b/osu.Game/Screens/Footer/ScreenFooterButton.cs index dda95d1d4c..1e5576e47a 100644 --- a/osu.Game/Screens/Footer/ScreenFooterButton.cs +++ b/osu.Game/Screens/Footer/ScreenFooterButton.cs @@ -69,7 +69,6 @@ namespace osu.Game.Screens.Footer private readonly Box glowBox; private readonly Box flashLayer; - public readonly Container TopLevelContent; public readonly OverlayContainer? Overlay; public ScreenFooterButton(OverlayContainer? overlay = null) @@ -78,89 +77,85 @@ namespace osu.Game.Screens.Footer Size = new Vector2(BUTTON_WIDTH, BUTTON_HEIGHT); - Child = TopLevelContent = new Container + Children = new Drawable[] { - RelativeSizeAxes = Axes.Both, - Children = new Drawable[] + new Container { - new Container + EdgeEffect = new EdgeEffectParameters { - EdgeEffect = new EdgeEffectParameters + Type = EdgeEffectType.Shadow, + Radius = 4, + // Figma says 50% opacity, but it does not match up visually if taken at face value, and looks bad. + Colour = Colour4.Black.Opacity(0.25f), + Offset = new Vector2(0, 2), + }, + Shear = BUTTON_SHEAR, + Masking = true, + CornerRadius = CORNER_RADIUS, + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + backgroundBox = new Box { - Type = EdgeEffectType.Shadow, - Radius = 4, - // Figma says 50% opacity, but it does not match up visually if taken at face value, and looks bad. - Colour = Colour4.Black.Opacity(0.25f), - Offset = new Vector2(0, 2), + RelativeSizeAxes = Axes.Both }, - Shear = BUTTON_SHEAR, - Masking = true, - CornerRadius = CORNER_RADIUS, - RelativeSizeAxes = Axes.Both, - Children = new Drawable[] + glowBox = new Box { - backgroundBox = new Box + RelativeSizeAxes = Axes.Both + }, + // For elements that should not be sheared. + new Container + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Shear = -BUTTON_SHEAR, + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] { - RelativeSizeAxes = Axes.Both - }, - glowBox = new Box - { - RelativeSizeAxes = Axes.Both - }, - // For elements that should not be sheared. - new Container - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Shear = -BUTTON_SHEAR, - RelativeSizeAxes = Axes.Both, - Children = new Drawable[] + TextContainer = new Container { - TextContainer = new Container + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Y = 42, + AutoSizeAxes = Axes.Both, + Child = text = new OsuSpriteText { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - Y = 42, - AutoSizeAxes = Axes.Both, - Child = text = new OsuSpriteText - { - // figma design says the size is 16, but due to the issues with font sizes 19 matches better - Font = OsuFont.TorusAlternate.With(size: 19), - AlwaysPresent = true - } - }, - icon = new SpriteIcon - { - Y = 12, - Size = new Vector2(20), - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre - }, - } - }, - new Container - { - Shear = -BUTTON_SHEAR, - Anchor = Anchor.BottomCentre, - Origin = Anchor.Centre, - Y = -CORNER_RADIUS, - Size = new Vector2(120, 6), - Masking = true, - CornerRadius = 3, - Child = bar = new Box + // figma design says the size is 16, but due to the issues with font sizes 19 matches better + Font = OsuFont.TorusAlternate.With(size: 19), + AlwaysPresent = true + } + }, + icon = new SpriteIcon { - RelativeSizeAxes = Axes.Both, - } - }, - flashLayer = new Box + Y = 12, + Size = new Vector2(20), + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre + }, + } + }, + new Container + { + Shear = -BUTTON_SHEAR, + Anchor = Anchor.BottomCentre, + Origin = Anchor.Centre, + Y = -CORNER_RADIUS, + Size = new Vector2(120, 6), + Masking = true, + CornerRadius = 3, + Child = bar = new Box { RelativeSizeAxes = Axes.Both, - Colour = Colour4.White.Opacity(0.9f), - Blending = BlendingParameters.Additive, - Alpha = 0, - }, + } }, - } + flashLayer = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Colour4.White.Opacity(0.9f), + Blending = BlendingParameters.Additive, + Alpha = 0, + }, + }, } }; } @@ -230,5 +225,41 @@ namespace osu.Game.Screens.Footer glowBox.FadeColour(ColourInfo.GradientVertical(buttonAccentColour.Opacity(0f), buttonAccentColour.Opacity(0.2f)), 150, Easing.OutQuint); } + + public void AppearFromLeft(double delay) + { + Content.MoveToX(-300f) + .FadeOut() + .Delay(delay) + .MoveToX(0f, 240, Easing.OutCubic) + .FadeIn(240, Easing.OutCubic); + } + + public void AppearFromBottom(double delay) + { + Content.MoveToY(100f) + .FadeOut() + .Delay(delay) + .MoveToY(0f, 240, Easing.OutCubic) + .FadeIn(240, Easing.OutCubic); + } + + public void DisappearToRightAndExpire(double delay) + { + Content.Delay(delay) + .FadeOut(240, Easing.InOutCubic) + .MoveToX(300f, 360, Easing.InOutCubic); + + this.Delay(Content.LatestTransformEndTime - Time.Current).Expire(); + } + + public void DisappearToBottomAndExpire(double delay) + { + Content.Delay(delay) + .FadeOut(240, Easing.InOutCubic) + .MoveToY(100f, 240, Easing.InOutCubic); + + this.Delay(Content.LatestTransformEndTime - Time.Current).Expire(); + } } } diff --git a/osu.Game/Screens/SelectV2/Footer/ScreenFooterButtonMods.cs b/osu.Game/Screens/SelectV2/Footer/ScreenFooterButtonMods.cs index 4df4116de1..841f0297e8 100644 --- a/osu.Game/Screens/SelectV2/Footer/ScreenFooterButtonMods.cs +++ b/osu.Game/Screens/SelectV2/Footer/ScreenFooterButtonMods.cs @@ -72,7 +72,7 @@ namespace osu.Game.Screens.SelectV2.Footer Icon = FontAwesome.Solid.ExchangeAlt; AccentColour = colours.Lime1; - TopLevelContent.AddRange(new[] + AddRange(new[] { unrankedBadge = new UnrankedBadge(), modDisplayBar = new Container From 5f8f6caedd0907a1b9cc734c2163c441b2fe2265 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 7 Jun 2024 22:45:22 +0300 Subject: [PATCH 1025/2493] Use `OsuGame.SHEAR` --- .../SongSelect/TestSceneLeaderboardScoreV2.cs | 2 +- .../Leaderboards/LeaderboardScoreV2.cs | 23 ++++++++----------- 2 files changed, 11 insertions(+), 14 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneLeaderboardScoreV2.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneLeaderboardScoreV2.cs index c8725fde08..0f5eb06df7 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneLeaderboardScoreV2.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneLeaderboardScoreV2.cs @@ -63,7 +63,7 @@ namespace osu.Game.Tests.Visual.SongSelect RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Spacing = new Vector2(0f, 2f), - Shear = LeaderboardScoreV2.SHEAR + Shear = new Vector2(OsuGame.SHEAR, 0) }, drawWidthText = new OsuSpriteText(), }; diff --git a/osu.Game/Screens/SelectV2/Leaderboards/LeaderboardScoreV2.cs b/osu.Game/Screens/SelectV2/Leaderboards/LeaderboardScoreV2.cs index 0a558186dd..804a9d24b7 100644 --- a/osu.Game/Screens/SelectV2/Leaderboards/LeaderboardScoreV2.cs +++ b/osu.Game/Screens/SelectV2/Leaderboards/LeaderboardScoreV2.cs @@ -65,9 +65,6 @@ namespace osu.Game.Screens.SelectV2.Leaderboards private Colour4 backgroundColour; private ColourInfo totalScoreBackgroundGradient; - // TODO: once https://github.com/ppy/osu/pull/28183 is merged, probably use OsuGame.SHEAR - public static readonly Vector2 SHEAR = new Vector2(0.15f, 0); - [Resolved] private OverlayColourProvider colourProvider { get; set; } = null!; @@ -113,7 +110,7 @@ namespace osu.Game.Screens.SelectV2.Leaderboards this.rank = rank; this.isPersonalBest = isPersonalBest; - Shear = SHEAR; + Shear = new Vector2(OsuGame.SHEAR, 0); RelativeSizeAxes = Axes.X; Height = height; } @@ -246,7 +243,7 @@ namespace osu.Game.Screens.SelectV2.Leaderboards { RelativeSizeAxes = Axes.Both, User = score.User, - Shear = -SHEAR, + Shear = new Vector2(-OsuGame.SHEAR, 0), Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, Colour = ColourInfo.GradientHorizontal(Colour4.White.Opacity(0.5f), Colour4.FromHex(@"222A27").Opacity(1)), @@ -277,7 +274,7 @@ namespace osu.Game.Screens.SelectV2.Leaderboards Anchor = Anchor.Centre, Origin = Anchor.Centre, Scale = new Vector2(1.1f), - Shear = -SHEAR, + Shear = new Vector2(-OsuGame.SHEAR, 0), RelativeSizeAxes = Axes.Both, }) { @@ -317,7 +314,7 @@ namespace osu.Game.Screens.SelectV2.Leaderboards { flagBadgeAndDateContainer = new FillFlowContainer { - Shear = -SHEAR, + Shear = new Vector2(-OsuGame.SHEAR, 0), Direction = FillDirection.Horizontal, Spacing = new Vector2(5), AutoSizeAxes = Axes.Both, @@ -341,7 +338,7 @@ namespace osu.Game.Screens.SelectV2.Leaderboards nameLabel = new TruncatingSpriteText { RelativeSizeAxes = Axes.X, - Shear = -SHEAR, + Shear = new Vector2(-OsuGame.SHEAR, 0), Text = user.Username, Font = OsuFont.GetFont(size: 20, weight: FontWeight.SemiBold) } @@ -357,7 +354,7 @@ namespace osu.Game.Screens.SelectV2.Leaderboards Name = @"Statistics container", Padding = new MarginPadding { Right = 40 }, Spacing = new Vector2(25, 0), - Shear = -SHEAR, + Shear = new Vector2(-OsuGame.SHEAR, 0), Anchor = Anchor.CentreRight, Origin = Anchor.CentreRight, AutoSizeAxes = Axes.Both, @@ -415,7 +412,7 @@ namespace osu.Game.Screens.SelectV2.Leaderboards }, RankContainer = new Container { - Shear = -SHEAR, + Shear = new Vector2(-OsuGame.SHEAR, 0), Anchor = Anchor.CentreRight, Origin = Anchor.CentreRight, RelativeSizeAxes = Axes.Y, @@ -473,7 +470,7 @@ namespace osu.Game.Screens.SelectV2.Leaderboards Anchor = Anchor.TopRight, Origin = Anchor.TopRight, UseFullGlyphHeight = false, - Shear = -SHEAR, + Shear = new Vector2(-OsuGame.SHEAR, 0), Current = scoreManager.GetBindableTotalScoreString(score), Font = OsuFont.GetFont(size: 30, weight: FontWeight.Light), }, @@ -481,7 +478,7 @@ namespace osu.Game.Screens.SelectV2.Leaderboards { Anchor = Anchor.TopRight, Origin = Anchor.TopRight, - Shear = -SHEAR, + Shear = new Vector2(-OsuGame.SHEAR, 0), AutoSizeAxes = Axes.Both, Direction = FillDirection.Horizontal, Spacing = new Vector2(2f, 0f), @@ -666,7 +663,7 @@ namespace osu.Game.Screens.SelectV2.Leaderboards Child = new OsuSpriteText { - Shear = -SHEAR, + Shear = new Vector2(-OsuGame.SHEAR, 0), Anchor = Anchor.Centre, Origin = Anchor.Centre, Font = OsuFont.GetFont(size: 20, weight: FontWeight.SemiBold, italics: true), From 642095b07b812d3ea90acb334218ea8b08f773ea Mon Sep 17 00:00:00 2001 From: Olle Kelderman Date: Sun, 9 Jun 2024 21:42:37 +0200 Subject: [PATCH 1026/2493] On the mappool screen the auto-pick map logic on map change still assumed 1 ban per team. Now it listens to the BanCount value from the round --- osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs b/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs index 665d3c131a..e8b6bdad9f 100644 --- a/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs +++ b/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs @@ -123,7 +123,12 @@ namespace osu.Game.Tournament.Screens.MapPool private void beatmapChanged(ValueChangedEvent beatmap) { - if (CurrentMatch.Value == null || CurrentMatch.Value.PicksBans.Count(p => p.Type == ChoiceType.Ban) < 2) + if (CurrentMatch.Value?.Round.Value == null) + return; + + int totalBansRequired = CurrentMatch.Value.Round.Value.BanCount.Value * 2; + + if (CurrentMatch.Value.PicksBans.Count(p => p.Type == ChoiceType.Ban) < totalBansRequired) return; // if bans have already been placed, beatmap changes result in a selection being made automatically From 1d6b7e9c9b860a4a7b8efd518b6724146e0d92ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 10 Jun 2024 10:28:10 +0200 Subject: [PATCH 1027/2493] Refactor further to address code quality complaints --- osu.Game/Screens/Edit/BottomBar.cs | 2 +- osu.Game/Screens/Edit/Editor.cs | 115 ++++++++++++++--------------- 2 files changed, 55 insertions(+), 62 deletions(-) diff --git a/osu.Game/Screens/Edit/BottomBar.cs b/osu.Game/Screens/Edit/BottomBar.cs index 612aa26c84..d43e675296 100644 --- a/osu.Game/Screens/Edit/BottomBar.cs +++ b/osu.Game/Screens/Edit/BottomBar.cs @@ -78,7 +78,7 @@ namespace osu.Game.Screens.Edit } }; - saveInProgress = editor.SaveTracker.InProgress.GetBoundCopy(); + saveInProgress = editor.MutationTracker.InProgress.GetBoundCopy(); } protected override void LoadComplete() diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 991b92cca6..3e3e772810 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -6,6 +6,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; using JetBrains.Annotations; using osu.Framework; using osu.Framework.Allocation; @@ -35,6 +36,7 @@ using osu.Game.Graphics.UserInterface; using osu.Game.Input.Bindings; using osu.Game.Localisation; using osu.Game.Online.API; +using osu.Game.Online.Multiplayer; using osu.Game.Overlays; using osu.Game.Overlays.Notifications; using osu.Game.Overlays.OSD; @@ -144,7 +146,12 @@ namespace osu.Game.Screens.Edit private bool canSave; private readonly List saveRelatedMenuItems = new List(); - public OngoingOperationTracker SaveTracker { get; private set; } = new OngoingOperationTracker(); + + /// + /// Tracks ongoing mutually-exclusive operations related to changing the beatmap + /// (e.g. save, export). + /// + public OngoingOperationTracker MutationTracker { get; } = new OngoingOperationTracker(); protected bool ExitConfirmed { get; private set; } @@ -385,7 +392,7 @@ namespace osu.Game.Screens.Edit }, }, bottomBar = new BottomBar(), - SaveTracker, + MutationTracker, } }); changeHandler?.CanUndo.BindValueChanged(v => undoMenuItem.Action.Disabled = !v.NewValue, true); @@ -407,10 +414,10 @@ namespace osu.Game.Screens.Edit musicController.TrackChanged += onTrackChanged; - SaveTracker.InProgress.BindValueChanged(_ => + MutationTracker.InProgress.BindValueChanged(_ => { foreach (var item in saveRelatedMenuItems) - item.Action.Disabled = SaveTracker.InProgress.Value; + item.Action.Disabled = MutationTracker.InProgress.Value; }, true); } @@ -450,17 +457,13 @@ namespace osu.Game.Screens.Edit { if (HasUnsavedChanges) { - dialogOverlay.Push(new SaveRequiredPopupDialog("The beatmap will be saved in order to test it.", () => + dialogOverlay.Push(new SaveRequiredPopupDialog("The beatmap will be saved in order to test it.", () => attemptMutationOperation(() => { - if (SaveTracker.InProgress.Value) return; + if (!Save()) return false; - using (SaveTracker.BeginOperation()) - { - if (!Save()) return; - - pushEditorPlayer(); - } - })); + pushEditorPlayer(); + return true; + }))); } else { @@ -470,6 +473,26 @@ namespace osu.Game.Screens.Edit void pushEditorPlayer() => this.Push(new EditorPlayerLoader(this)); } + private bool attemptMutationOperation(Func mutationOperation) + { + if (MutationTracker.InProgress.Value) + return false; + + using (MutationTracker.BeginOperation()) + return mutationOperation.Invoke(); + } + + private bool attemptAsyncMutationOperation(Func mutationTask) + { + if (MutationTracker.InProgress.Value) + return false; + + var operation = MutationTracker.BeginOperation(); + var task = mutationTask.Invoke(); + task.FireAndForget(operation.Dispose, _ => operation.Dispose()); + return true; + } + /// /// Saves the currently edited beatmap. /// @@ -535,12 +558,7 @@ namespace osu.Game.Screens.Edit if (e.Repeat) return false; - if (SaveTracker.InProgress.Value) - return false; - - using (SaveTracker.BeginOperation()) - Save(); - return true; + return attemptMutationOperation(Save); } return false; @@ -806,13 +824,8 @@ namespace osu.Game.Screens.Edit private void confirmExitWithSave() { - if (SaveTracker.InProgress.Value) return; - - using (SaveTracker.BeginOperation()) - { - if (!Save()) - return; - } + if (!attemptMutationOperation(Save)) + return; ExitConfirmed = true; this.Exit(); @@ -1053,13 +1066,7 @@ namespace osu.Game.Screens.Edit yield return new EditorMenuItem(EditorStrings.DeleteDifficulty, MenuItemType.Standard, deleteDifficulty) { Action = { Disabled = Beatmap.Value.BeatmapSetInfo.Beatmaps.Count < 2 } }; yield return new OsuMenuItemSpacer(); - var save = new EditorMenuItem(WebCommonStrings.ButtonsSave, MenuItemType.Standard, () => - { - if (SaveTracker.InProgress.Value) return; - - using (SaveTracker.BeginOperation()) - Save(); - }); + var save = new EditorMenuItem(WebCommonStrings.ButtonsSave, MenuItemType.Standard, () => attemptMutationOperation(Save)); saveRelatedMenuItems.Add(save); yield return save; @@ -1089,37 +1096,25 @@ namespace osu.Game.Screens.Edit { if (HasUnsavedChanges) { - dialogOverlay.Push(new SaveRequiredPopupDialog("The beatmap will be saved in order to export it.", () => + dialogOverlay.Push(new SaveRequiredPopupDialog("The beatmap will be saved in order to export it.", () => attemptAsyncMutationOperation(() => { - if (SaveTracker.InProgress.Value) - return; - - var operation = SaveTracker.BeginOperation(); - if (!Save()) - { - operation.Dispose(); - return; - } + return Task.CompletedTask; - runExport(operation); - })); + return runExport(); + }))); } else { - if (SaveTracker.InProgress.Value) - return; - - runExport(SaveTracker.BeginOperation()); + attemptAsyncMutationOperation(runExport); } - void runExport(IDisposable operationInProgress) + Task runExport() { - var task = legacy - ? beatmapManager.ExportLegacy(Beatmap.Value.BeatmapSetInfo) - : beatmapManager.Export(Beatmap.Value.BeatmapSetInfo); - - task.ContinueWith(_ => operationInProgress.Dispose()); + if (legacy) + return beatmapManager.ExportLegacy(Beatmap.Value.BeatmapSetInfo); + else + return beatmapManager.Export(Beatmap.Value.BeatmapSetInfo); } } @@ -1181,16 +1176,14 @@ namespace osu.Game.Screens.Edit { dialogOverlay.Push(new SaveRequiredPopupDialog("This beatmap will be saved in order to create another difficulty.", () => { - if (SaveTracker.InProgress.Value) - return; - - using (SaveTracker.BeginOperation()) + attemptMutationOperation(() => { if (!Save()) - return; + return false; CreateNewDifficulty(rulesetInfo); - } + return true; + }); })); return; From 0efa028e0a82dc192303e2b99bead7ce1db66280 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 10 Jun 2024 11:51:14 +0200 Subject: [PATCH 1028/2493] Restructure popover updates to be more centralised --- .../Components/Timeline/SamplePointPiece.cs | 117 ++++++++---------- 1 file changed, 54 insertions(+), 63 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs index 64ad840591..5f83937986 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs @@ -180,26 +180,35 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline if (commonVolume != null) volume.Current.Value = commonVolume.Value; - updateBankPlaceholderText(); + updatePrimaryBankState(); bank.Current.BindValueChanged(val => { - updateBank(val.NewValue); - updateBankPlaceholderText(); + if (string.IsNullOrEmpty(val.NewValue)) + return; + + setBank(val.NewValue); + updatePrimaryBankState(); }); // on commit, ensure that the value is correct by sourcing it from the objects' samples again. // this ensures that committing empty text causes a revert to the previous value. - bank.OnCommit += (_, _) => updateBankText(); + bank.OnCommit += (_, _) => updatePrimaryBankState(); - updateAdditionBankText(); - updateAdditionBankVisual(); + updateAdditionBankState(); additionBank.Current.BindValueChanged(val => { - updateAdditionBank(val.NewValue); - updateAdditionBankVisual(); - }); - additionBank.OnCommit += (_, _) => updateAdditionBankText(); + if (string.IsNullOrEmpty(val.NewValue)) + return; - volume.Current.BindValueChanged(val => updateVolume(val.NewValue)); + setAdditionBank(val.NewValue); + updateAdditionBankState(); + }); + additionBank.OnCommit += (_, _) => updateAdditionBankState(); + + volume.Current.BindValueChanged(val => + { + if (val.NewValue != null) + setVolume(val.NewValue.Value); + }); createStateBindables(); updateTernaryStates(); @@ -210,6 +219,26 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline private string? getCommonAdditionBank() => allRelevantSamples.Select(GetAdditionBankValue).Distinct().Count() == 1 ? GetAdditionBankValue(allRelevantSamples.First()) : null; private int? getCommonVolume() => allRelevantSamples.Select(GetVolumeValue).Distinct().Count() == 1 ? GetVolumeValue(allRelevantSamples.First()) : null; + private void updatePrimaryBankState() + { + string? commonBank = getCommonBank(); + bank.Current.Value = commonBank; + bank.PlaceholderText = string.IsNullOrEmpty(commonBank) ? "(multiple)" : string.Empty; + } + + private void updateAdditionBankState() + { + string? commonAdditionBank = getCommonAdditionBank(); + additionBank.PlaceholderText = string.IsNullOrEmpty(commonAdditionBank) ? "(multiple)" : string.Empty; + additionBank.Current.Value = commonAdditionBank; + + bool anyAdditions = allRelevantSamples.Any(o => o.Any(s => s.Name != HitSampleInfo.HIT_NORMAL)); + if (anyAdditions) + additionBank.Show(); + else + additionBank.Hide(); + } + /// /// Applies the given update action on all samples of /// and invokes the necessary update notifiers for the beatmap and hit objects. @@ -229,11 +258,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline beatmap.EndChange(); } - private void updateBank(string? newBank) + private void setBank(string newBank) { - if (string.IsNullOrEmpty(newBank)) - return; - updateAllRelevantSamples((_, relevantSamples) => { for (int i = 0; i < relevantSamples.Count; i++) @@ -245,11 +271,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline }); } - private void updateAdditionBank(string? newBank) + private void setAdditionBank(string newBank) { - if (string.IsNullOrEmpty(newBank)) - return; - updateAllRelevantSamples((_, relevantSamples) => { for (int i = 0; i < relevantSamples.Count; i++) @@ -261,47 +284,13 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline }); } - private void updateBankText() + private void setVolume(int newVolume) { - bank.Current.Value = getCommonBank(); - } - - private void updateBankPlaceholderText() - { - string? commonBank = getCommonBank(); - bank.PlaceholderText = string.IsNullOrEmpty(commonBank) ? "(multiple)" : string.Empty; - } - - private void updateAdditionBankVisual() - { - string? commonAdditionBank = getCommonAdditionBank(); - additionBank.PlaceholderText = string.IsNullOrEmpty(commonAdditionBank) ? "(multiple)" : string.Empty; - - bool anyAdditions = allRelevantSamples.Any(o => o.Any(s => s.Name != HitSampleInfo.HIT_NORMAL)); - if (anyAdditions) - additionBank.Show(); - else - additionBank.Hide(); - } - - private void updateAdditionBankText() - { - string? commonAdditionBank = getCommonAdditionBank(); - if (string.IsNullOrEmpty(commonAdditionBank)) return; - - additionBank.Current.Value = commonAdditionBank; - } - - private void updateVolume(int? newVolume) - { - if (newVolume == null) - return; - updateAllRelevantSamples((_, relevantSamples) => { for (int i = 0; i < relevantSamples.Count; i++) { - relevantSamples[i] = relevantSamples[i].With(newVolume: newVolume.Value); + relevantSamples[i] = relevantSamples[i].With(newVolume: newVolume); } }); } @@ -371,8 +360,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline relevantSamples.Add(relevantSample?.With(sampleName) ?? h.CreateHitSampleInfo(sampleName)); }); - updateAdditionBankVisual(); - updateAdditionBankText(); + updateAdditionBankState(); } private void removeHitSample(string sampleName) @@ -389,8 +377,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline } }); - updateAdditionBankText(); - updateAdditionBankVisual(); + updateAdditionBankState(); } protected override bool OnKeyDown(KeyDownEvent e) @@ -401,10 +388,14 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline if (e.ShiftPressed) { string? newBank = banks.ElementAtOrDefault(rightIndex); - updateBank(newBank); - updateBankText(); - updateAdditionBank(newBank); - updateAdditionBankText(); + + if (string.IsNullOrEmpty(newBank)) + return true; + + setBank(newBank); + updatePrimaryBankState(); + setAdditionBank(newBank); + updateAdditionBankState(); } else { From 7d5dc750e53938d60fa6cffedc1879d32c546118 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 10 Jun 2024 12:04:52 +0200 Subject: [PATCH 1029/2493] Use slightly lighter shade of pink for alternative colour --- .../Edit/Compose/Components/Timeline/SamplePointPiece.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs index 5f83937986..f318a52b28 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs @@ -39,7 +39,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline public bool AlternativeColor { get; init; } - protected override Color4 GetRepresentingColour(OsuColour colours) => AlternativeColor ? colours.PinkDarker : colours.Pink; + protected override Color4 GetRepresentingColour(OsuColour colours) => AlternativeColor ? colours.Pink2 : colours.Pink1; [BackgroundDependencyLoader] private void load() From 19f39ca1b681c8ce0cd8742814458a1faefb9d6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 30 May 2024 11:51:09 +0200 Subject: [PATCH 1030/2493] Extract `OnlinePlayScreenWaveContainer` from `OnlinePlayScreen` --- .../Screens/OnlinePlay/OnlinePlayScreen.cs | 18 ++------------- .../OnlinePlayScreenWaveContainer.cs | 22 +++++++++++++++++++ 2 files changed, 24 insertions(+), 16 deletions(-) create mode 100644 osu.Game/Screens/OnlinePlay/OnlinePlayScreenWaveContainer.cs diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs b/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs index 9de458b5c6..9b6284fb89 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs @@ -6,7 +6,6 @@ using System.Diagnostics; using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Logging; using osu.Framework.Screens; @@ -36,7 +35,7 @@ namespace osu.Game.Screens.OnlinePlay protected LoungeSubScreen Lounge { get; private set; } - private MultiplayerWaveContainer waves; + private OnlinePlayScreenWaveContainer waves; private ScreenStack screenStack; [Cached(Type = typeof(IRoomManager))] @@ -63,7 +62,7 @@ namespace osu.Game.Screens.OnlinePlay [BackgroundDependencyLoader] private void load() { - InternalChild = waves = new MultiplayerWaveContainer + InternalChild = waves = new OnlinePlayScreenWaveContainer { RelativeSizeAxes = Axes.Both, Children = new Drawable[] @@ -230,19 +229,6 @@ namespace osu.Game.Screens.OnlinePlay protected abstract LoungeSubScreen CreateLounge(); - private partial class MultiplayerWaveContainer : WaveContainer - { - protected override bool StartHidden => true; - - public MultiplayerWaveContainer() - { - FirstWaveColour = Color4Extensions.FromHex(@"654d8c"); - SecondWaveColour = Color4Extensions.FromHex(@"554075"); - ThirdWaveColour = Color4Extensions.FromHex(@"44325e"); - FourthWaveColour = Color4Extensions.FromHex(@"392850"); - } - } - ScreenStack IHasSubScreenStack.SubScreenStack => screenStack; } } diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlayScreenWaveContainer.cs b/osu.Game/Screens/OnlinePlay/OnlinePlayScreenWaveContainer.cs new file mode 100644 index 0000000000..bfa68d82cd --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/OnlinePlayScreenWaveContainer.cs @@ -0,0 +1,22 @@ +// 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.Extensions.Color4Extensions; +using osu.Game.Graphics.Containers; + +namespace osu.Game.Screens.OnlinePlay +{ + public partial class OnlinePlayScreenWaveContainer : WaveContainer + { + protected override bool StartHidden => true; + + public OnlinePlayScreenWaveContainer() + { + FirstWaveColour = Color4Extensions.FromHex(@"654d8c"); + SecondWaveColour = Color4Extensions.FromHex(@"554075"); + ThirdWaveColour = Color4Extensions.FromHex(@"44325e"); + FourthWaveColour = Color4Extensions.FromHex(@"392850"); + } + } +} From d80f09e0c0f90b163c3f271e75cdcfc97abd8e85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 30 May 2024 12:02:16 +0200 Subject: [PATCH 1031/2493] Adjust online play header to be reusable for new daily challenge screen --- osu.Game/Screens/OnlinePlay/Header.cs | 34 +++++++++++++++------------ 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Header.cs b/osu.Game/Screens/OnlinePlay/Header.cs index 4c4851c3ac..860042fd37 100644 --- a/osu.Game/Screens/OnlinePlay/Header.cs +++ b/osu.Game/Screens/OnlinePlay/Header.cs @@ -1,13 +1,11 @@ // 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 Humanizer; -using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Localisation; using osu.Framework.Screens; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; @@ -20,10 +18,10 @@ namespace osu.Game.Screens.OnlinePlay { public const float HEIGHT = 80; - private readonly ScreenStack stack; + private readonly ScreenStack? stack; private readonly MultiHeaderTitle title; - public Header(string mainTitle, ScreenStack stack) + public Header(LocalisableString mainTitle, ScreenStack? stack) { this.stack = stack; @@ -37,12 +35,15 @@ namespace osu.Game.Screens.OnlinePlay Origin = Anchor.CentreLeft, }; - // unnecessary to unbind these as this header has the same lifetime as the screen stack we are attaching to. - stack.ScreenPushed += (_, _) => updateSubScreenTitle(); - stack.ScreenExited += (_, _) => updateSubScreenTitle(); + if (stack != null) + { + // unnecessary to unbind these as this header has the same lifetime as the screen stack we are attaching to. + stack.ScreenPushed += (_, _) => updateSubScreenTitle(); + stack.ScreenExited += (_, _) => updateSubScreenTitle(); + } } - private void updateSubScreenTitle() => title.Screen = stack.CurrentScreen as IOnlinePlaySubScreen; + private void updateSubScreenTitle() => title.Screen = stack?.CurrentScreen as IOnlinePlaySubScreen; private partial class MultiHeaderTitle : CompositeDrawable { @@ -51,13 +52,16 @@ namespace osu.Game.Screens.OnlinePlay private readonly OsuSpriteText dot; private readonly OsuSpriteText pageTitle; - [CanBeNull] - public IOnlinePlaySubScreen Screen + public IOnlinePlaySubScreen? Screen { - set => pageTitle.Text = value?.ShortTitle.Titleize() ?? string.Empty; + set + { + pageTitle.Text = value?.ShortTitle.Titleize() ?? default(LocalisableString); + dot.Alpha = pageTitle.Text == default ? 0 : 1; + } } - public MultiHeaderTitle(string mainTitle) + public MultiHeaderTitle(LocalisableString mainTitle) { AutoSizeAxes = Axes.Both; @@ -82,14 +86,14 @@ namespace osu.Game.Screens.OnlinePlay Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, Font = OsuFont.TorusAlternate.With(size: 24), - Text = "·" + Text = "·", + Alpha = 0, }, pageTitle = new OsuSpriteText { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, Font = OsuFont.TorusAlternate.With(size: 24), - Text = "Lounge" } } }, From f135a9a923ce2128c9fd467e85711c39ada22afb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 30 May 2024 13:49:47 +0200 Subject: [PATCH 1032/2493] Make `SelectedItem` externally mutable Not being able to externally mutate this was making reuse in new daily challenge screen unnecessarily arduous. --- osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs b/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs index ceb8e53778..45f52f3cd8 100644 --- a/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs +++ b/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs @@ -29,7 +29,7 @@ namespace osu.Game.Online.Rooms /// public partial class OnlinePlayBeatmapAvailabilityTracker : CompositeComponent { - public readonly IBindable SelectedItem = new Bindable(); + public readonly Bindable SelectedItem = new Bindable(); [Resolved] private RealmAccess realm { get; set; } = null!; From dd6e9308b3c85bfd9dc6e9269035ce9340d65145 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 30 May 2024 13:50:56 +0200 Subject: [PATCH 1033/2493] Extract user mod select button for reuse --- .../Multiplayer/TestSceneMultiplayer.cs | 2 +- .../TestSceneMultiplayerMatchSubScreen.cs | 4 +-- .../Screens/OnlinePlay/Match/RoomSubScreen.cs | 19 ------------- .../OnlinePlay/Match/UserModSelectButton.cs | 27 +++++++++++++++++++ 4 files changed, 30 insertions(+), 22 deletions(-) create mode 100644 osu.Game/Screens/OnlinePlay/Match/UserModSelectButton.cs diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs index 8c7576ff52..3306b6624e 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs @@ -644,7 +644,7 @@ namespace osu.Game.Tests.Visual.Multiplayer } }); - AddStep("open mod overlay", () => this.ChildrenOfType().Single().TriggerClick()); + AddStep("open mod overlay", () => this.ChildrenOfType().Single().TriggerClick()); AddStep("invoke on back button", () => multiplayerComponents.OnBackButton()); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs index bdfe01ba09..f9ef085838 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs @@ -197,7 +197,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("wait for join", () => RoomJoined); - ClickButtonWhenEnabled(); + ClickButtonWhenEnabled(); AddUntilStep("mod select contents loaded", () => this.ChildrenOfType().Any() && this.ChildrenOfType().All(col => col.IsLoaded && col.ItemsLoaded)); @@ -311,7 +311,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("wait for join", () => RoomJoined); - ClickButtonWhenEnabled(); + ClickButtonWhenEnabled(); AddAssert("mod select shows unranked", () => screen.UserModsSelectOverlay.ChildrenOfType().Single().Ranked.Value == false); AddAssert("score multiplier = 1.20", () => screen.UserModsSelectOverlay.ChildrenOfType().Single().ModMultiplier.Value, () => Is.EqualTo(1.2).Within(0.01)); diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs index 97fbb83992..a694faebac 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs @@ -17,12 +17,9 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Shapes; -using osu.Framework.Input.Bindings; -using osu.Framework.Input.Events; 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; @@ -531,22 +528,6 @@ namespace osu.Game.Screens.OnlinePlay.Match /// The room to change the settings of. protected abstract RoomSettingsOverlay CreateRoomSettingsOverlay(Room room); - public partial class UserModSelectButton : PurpleRoundedButton, IKeyBindingHandler - { - public bool OnPressed(KeyBindingPressEvent e) - { - if (e.Action == GlobalAction.ToggleModSelection && !e.Repeat) - { - TriggerClick(); - return true; - } - - return false; - } - - public void OnReleased(KeyBindingReleaseEvent e) { } - } - protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); diff --git a/osu.Game/Screens/OnlinePlay/Match/UserModSelectButton.cs b/osu.Game/Screens/OnlinePlay/Match/UserModSelectButton.cs new file mode 100644 index 0000000000..f3ea82be99 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Match/UserModSelectButton.cs @@ -0,0 +1,27 @@ +// 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.Input.Bindings; +using osu.Framework.Input.Events; +using osu.Game.Input.Bindings; +using osu.Game.Screens.OnlinePlay.Match.Components; + +namespace osu.Game.Screens.OnlinePlay.Match +{ + public partial class UserModSelectButton : PurpleRoundedButton, IKeyBindingHandler + { + public bool OnPressed(KeyBindingPressEvent e) + { + if (e.Action == GlobalAction.ToggleModSelection && !e.Repeat) + { + TriggerClick(); + return true; + } + + return false; + } + + public void OnReleased(KeyBindingReleaseEvent e) { } + } +} From ffd788c65cbb0794d0dcdba88bc1ff7c2595c5c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 30 May 2024 14:15:23 +0200 Subject: [PATCH 1034/2493] Use room mod select overlay rely on explicit binding rather than DI resolution --- osu.Game/Screens/OnlinePlay/Match/RoomModSelectOverlay.cs | 5 ++--- osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs | 1 + 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomModSelectOverlay.cs b/osu.Game/Screens/OnlinePlay/Match/RoomModSelectOverlay.cs index 55e077df0f..6a856d8d72 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomModSelectOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomModSelectOverlay.cs @@ -16,8 +16,7 @@ namespace osu.Game.Screens.OnlinePlay.Match { public partial class RoomModSelectOverlay : UserModSelectOverlay { - [Resolved] - private IBindable selectedItem { get; set; } = null!; + public Bindable SelectedItem { get; } = new Bindable(); [Resolved] private RulesetStore rulesets { get; set; } = null!; @@ -33,7 +32,7 @@ namespace osu.Game.Screens.OnlinePlay.Match { base.LoadComplete(); - selectedItem.BindValueChanged(v => + SelectedItem.BindValueChanged(v => { roomRequiredMods.Clear(); diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs index a694faebac..4eb092d08b 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs @@ -240,6 +240,7 @@ namespace osu.Game.Screens.OnlinePlay.Match LoadComponent(UserModsSelectOverlay = new RoomModSelectOverlay { + SelectedItem = { BindTarget = SelectedItem }, SelectedMods = { BindTarget = UserMods }, IsValidMod = _ => false }); From e6da17d24865ecfe35d2f7959c491a9a274f3ae2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 30 May 2024 14:15:44 +0200 Subject: [PATCH 1035/2493] Add minimal viable variant of new daily challenge screen --- osu.Game/Screens/Menu/MainMenu.cs | 5 +- .../DailyChallenge/DailyChallenge.cs | 376 ++++++++++++++++++ 2 files changed, 378 insertions(+), 3 deletions(-) create mode 100644 osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs diff --git a/osu.Game/Screens/Menu/MainMenu.cs b/osu.Game/Screens/Menu/MainMenu.cs index 722e776e3d..00b9d909a1 100644 --- a/osu.Game/Screens/Menu/MainMenu.cs +++ b/osu.Game/Screens/Menu/MainMenu.cs @@ -31,6 +31,7 @@ using osu.Game.Overlays.SkinEditor; using osu.Game.Rulesets; using osu.Game.Screens.Backgrounds; using osu.Game.Screens.Edit; +using osu.Game.Screens.OnlinePlay.DailyChallenge; using osu.Game.Screens.OnlinePlay.Multiplayer; using osu.Game.Screens.OnlinePlay.Playlists; using osu.Game.Screens.Select; @@ -149,9 +150,7 @@ namespace osu.Game.Screens.Menu OnPlaylists = () => this.Push(new Playlists()), OnDailyChallenge = room => { - Playlists playlistsScreen; - this.Push(playlistsScreen = new Playlists()); - playlistsScreen.Join(room); + this.Push(new DailyChallenge(room)); }, OnExit = () => { diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs new file mode 100644 index 0000000000..bff58dbb58 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs @@ -0,0 +1,376 @@ +// 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.Audio; +using osu.Framework.Audio.Sample; +using osu.Framework.Bindables; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Extensions.LocalisationExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Screens; +using osu.Game.Beatmaps; +using osu.Game.Graphics.Containers; +using osu.Game.Localisation; +using osu.Game.Online.Rooms; +using osu.Game.Overlays; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Mods; +using osu.Game.Screens.OnlinePlay.Components; +using osu.Game.Screens.OnlinePlay.Match; +using osu.Game.Screens.OnlinePlay.Match.Components; +using osu.Game.Screens.OnlinePlay.Playlists; +using osu.Game.Screens.Play; +using osuTK; + +namespace osu.Game.Screens.OnlinePlay.DailyChallenge +{ + public partial class DailyChallenge : OsuScreen + { + private readonly Room room; + private readonly PlaylistItem playlistItem; + + /// + /// Any mods applied by/to the local user. + /// + private readonly Bindable> userMods = new Bindable>(Array.Empty()); + + private OnlinePlayScreenWaveContainer waves = null!; + private MatchLeaderboard leaderboard = null!; + private RoomModSelectOverlay userModsSelectOverlay = null!; + private Sample? sampleStart; + private IDisposable? userModsSelectOverlayRegistration; + + [Cached] + private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Plum); + + [Cached(Type = typeof(IRoomManager))] + private RoomManager roomManager { get; set; } + + [Cached] + private readonly OnlinePlayBeatmapAvailabilityTracker beatmapAvailabilityTracker = new OnlinePlayBeatmapAvailabilityTracker(); + + [Resolved] + private BeatmapManager beatmapManager { get; set; } = null!; + + [Resolved] + private RulesetStore rulesets { get; set; } = null!; + + [Resolved] + private MusicController musicController { get; set; } = null!; + + [Resolved] + private IOverlayManager? overlayManager { get; set; } + + public override bool DisallowExternalBeatmapRulesetChanges => true; + + public DailyChallenge(Room room) + { + this.room = room; + playlistItem = room.Playlist.Single(); + roomManager = new RoomManager(); + } + + protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) + { + return new CachedModelDependencyContainer(base.CreateChildDependencies(parent)) + { + Model = { Value = room } + }; + } + + [BackgroundDependencyLoader] + private void load(AudioManager audio) + { + sampleStart = audio.Samples.Get(@"SongSelect/confirm-selection"); + + FillFlowContainer footerButtons; + + InternalChild = waves = new OnlinePlayScreenWaveContainer + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + roomManager, + beatmapAvailabilityTracker, + new ScreenStack(new RoomBackgroundScreen(playlistItem)) + { + RelativeSizeAxes = Axes.Both, + }, + new Header(ButtonSystemStrings.DailyChallenge.ToSentence(), null), + new GridContainer + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding + { + Horizontal = WaveOverlayContainer.WIDTH_PADDING, + Top = Header.HEIGHT, + }, + RowDimensions = + [ + new Dimension(GridSizeMode.AutoSize), + new Dimension(GridSizeMode.Absolute, 10), + new Dimension(), + new Dimension(GridSizeMode.Absolute, 30), + new Dimension(GridSizeMode.Absolute, 50) + ], + Content = new[] + { + new Drawable[] + { + new DrawableRoomPlaylistItem(playlistItem) + { + RelativeSizeAxes = Axes.X, + AllowReordering = false, + Scale = new Vector2(1.4f), + Width = 1 / 1.4f, + } + }, + null, + [ + new Container + { + RelativeSizeAxes = Axes.Both, + Masking = true, + CornerRadius = 10, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4Extensions.FromHex(@"3e3a44") // Temporary. + }, + new GridContainer + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding(10), + ColumnDimensions = + [ + new Dimension(), + new Dimension(GridSizeMode.Absolute, 10), + new Dimension(), + new Dimension(GridSizeMode.Absolute, 10), + new Dimension() + ], + Content = new[] + { + new Drawable?[] + { + null, + null, + // Middle column (leaderboard) + new GridContainer + { + RelativeSizeAxes = Axes.Both, + Content = new[] + { + new Drawable[] + { + new OverlinedHeader("Leaderboard") + }, + [leaderboard = new MatchLeaderboard { RelativeSizeAxes = Axes.Both }], + }, + RowDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize), + new Dimension(), + } + }, + // Spacer + null, + // Main right column + new GridContainer + { + RelativeSizeAxes = Axes.Both, + Content = new[] + { + new Drawable[] + { + new OverlinedHeader("Chat") + }, + [new MatchChatDisplay(room) { RelativeSizeAxes = Axes.Both }] + }, + RowDimensions = + [ + new Dimension(GridSizeMode.AutoSize), + new Dimension() + ] + }, + } + } + } + } + } + ], + null, + [ + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding + { + Horizontal = -WaveOverlayContainer.WIDTH_PADDING, + }, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4Extensions.FromHex(@"28242d") // Temporary. + }, + footerButtons = new FillFlowContainer + { + RelativeSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Padding = new MarginPadding(5), + Spacing = new Vector2(10), + Children = new Drawable[] + { + new PlaylistsReadyButton + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Y, + Size = new Vector2(250, 1), + Action = startPlay + } + } + }, + } + } + ], + } + } + } + }; + + LoadComponent(userModsSelectOverlay = new RoomModSelectOverlay + { + SelectedMods = { BindTarget = userMods }, + IsValidMod = _ => false + }); + + if (playlistItem.AllowedMods.Any()) + { + footerButtons.Insert(0, new UserModSelectButton + { + Text = "Free mods", + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Y, + Size = new Vector2(250, 1), + Action = () => userModsSelectOverlay.Show(), + }); + + var rulesetInstance = rulesets.GetRuleset(playlistItem.RulesetID)!.CreateInstance(); + var allowedMods = playlistItem.AllowedMods.Select(m => m.ToMod(rulesetInstance)); + userModsSelectOverlay.IsValidMod = m => allowedMods.Any(a => a.GetType() == m.GetType()); + } + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + beatmapAvailabilityTracker.SelectedItem.Value = playlistItem; + beatmapAvailabilityTracker.Availability.BindValueChanged(_ => trySetDailyChallengeBeatmap(), true); + + userModsSelectOverlayRegistration = overlayManager?.RegisterBlockingOverlay(userModsSelectOverlay); + userModsSelectOverlay.SelectedItem.Value = playlistItem; + userMods.BindValueChanged(_ => Scheduler.AddOnce(updateMods), true); + } + + private void trySetDailyChallengeBeatmap() + { + var beatmap = beatmapManager.QueryBeatmap(b => b.OnlineID == playlistItem.Beatmap.OnlineID); + Beatmap.Value = beatmapManager.GetWorkingBeatmap(beatmap); // this will gracefully fall back to dummy beatmap if missing locally. + Ruleset.Value = rulesets.GetRuleset(playlistItem.RulesetID); + } + + public override void OnEntering(ScreenTransitionEvent e) + { + base.OnEntering(e); + + waves.Show(); + roomManager.JoinRoom(room); + applyLoopingToTrack(); + } + + public override void OnResuming(ScreenTransitionEvent e) + { + base.OnResuming(e); + applyLoopingToTrack(); + } + + public override void OnSuspending(ScreenTransitionEvent e) + { + base.OnSuspending(e); + + userModsSelectOverlay.Hide(); + cancelTrackLooping(); + } + + public override bool OnExiting(ScreenExitEvent e) + { + waves.Hide(); + userModsSelectOverlay.Hide(); + cancelTrackLooping(); + this.Delay(WaveContainer.DISAPPEAR_DURATION).FadeOut(); + + roomManager.PartRoom(); + + return base.OnExiting(e); + } + + private void applyLoopingToTrack() + { + if (!this.IsCurrentScreen()) + return; + + var track = Beatmap.Value?.Track; + + if (track != null) + { + Beatmap.Value?.PrepareTrackForPreview(true); + musicController.EnsurePlayingSomething(); + } + } + + private void cancelTrackLooping() + { + var track = Beatmap.Value?.Track; + + if (track != null) + track.Looping = false; + } + + private void updateMods() + { + if (!this.IsCurrentScreen()) + return; + + Mods.Value = userMods.Value.Concat(playlistItem.RequiredMods.Select(m => m.ToMod(Ruleset.Value.CreateInstance()))).ToList(); + } + + private void startPlay() + { + sampleStart?.Play(); + this.Push(new PlayerLoader(() => new PlaylistsPlayer(room, playlistItem) + { + Exited = () => leaderboard.RefetchScores() + })); + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + userModsSelectOverlayRegistration?.Dispose(); + } + } +} From 2321e408cb017d65be8e4dea890ad407f7cbb1a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 4 Jun 2024 15:36:22 +0200 Subject: [PATCH 1036/2493] Add test scene for daily challenge screen --- .../DailyChallenge/TestSceneDailyChallenge.cs | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallenge.cs diff --git a/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallenge.cs b/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallenge.cs new file mode 100644 index 0000000000..cd09a1d20f --- /dev/null +++ b/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallenge.cs @@ -0,0 +1,40 @@ +// 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 NUnit.Framework; +using osu.Game.Online.API; +using osu.Game.Online.Rooms; +using osu.Game.Rulesets.Osu.Mods; +using osu.Game.Tests.Resources; +using osu.Game.Tests.Visual.OnlinePlay; + +namespace osu.Game.Tests.Visual.DailyChallenge +{ + public partial class TestSceneDailyChallenge : OnlinePlayTestScene + { + [Test] + public void TestDailyChallenge() + { + var room = new Room + { + RoomID = { Value = 1234 }, + Name = { Value = "Daily Challenge: June 4, 2024" }, + Playlist = + { + new PlaylistItem(TestResources.CreateTestBeatmapSetInfo().Beatmaps.First()) + { + RequiredMods = [new APIMod(new OsuModTraceable())], + AllowedMods = [new APIMod(new OsuModDoubleTime())] + } + }, + EndDate = { Value = DateTimeOffset.Now.AddHours(12) }, + Category = { Value = RoomCategory.DailyChallenge } + }; + + AddStep("add room", () => API.Perform(new CreateRoomRequest(room))); + AddStep("push screen", () => LoadScreen(new Screens.OnlinePlay.DailyChallenge.DailyChallenge(room))); + } + } +} From 073ddcebe4da0a447bfe2aaeb9ea5d8514a746c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 30 May 2024 14:20:16 +0200 Subject: [PATCH 1037/2493] Hide daily challenge from playlists listing --- .../Screens/OnlinePlay/Components/ListingPollingComponent.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Screens/OnlinePlay/Components/ListingPollingComponent.cs b/osu.Game/Screens/OnlinePlay/Components/ListingPollingComponent.cs index c296e2a86b..4b38ea68b3 100644 --- a/osu.Game/Screens/OnlinePlay/Components/ListingPollingComponent.cs +++ b/osu.Game/Screens/OnlinePlay/Components/ListingPollingComponent.cs @@ -53,6 +53,8 @@ namespace osu.Game.Screens.OnlinePlay.Components req.Success += result => { + result = result.Where(r => r.Category.Value != RoomCategory.DailyChallenge).ToList(); + foreach (var existing in RoomManager.Rooms.ToArray()) { if (result.All(r => r.RoomID.Value != existing.RoomID.Value)) From 25b2dfa601247427c1e5e656412701bafe2cd370 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 10 Jun 2024 14:34:04 +0200 Subject: [PATCH 1038/2493] Fix stack leniency not applying immediately after change --- .../Beatmaps/OsuBeatmapProcessor.cs | 57 ++++++++++--------- .../Edit/Setup/OsuSetupSection.cs | 1 + osu.Game/Screens/Edit/EditorBeatmap.cs | 2 +- 3 files changed, 31 insertions(+), 29 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapProcessor.cs b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapProcessor.cs index 9cc0a8c414..d335913586 100644 --- a/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapProcessor.cs +++ b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapProcessor.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; using System.Linq; using osu.Framework.Graphics; using osu.Game.Beatmaps; @@ -41,22 +42,22 @@ namespace osu.Game.Rulesets.Osu.Beatmaps { base.PostProcess(); - var osuBeatmap = (Beatmap)Beatmap; + var hitObjects = Beatmap.HitObjects as List ?? Beatmap.HitObjects.OfType().ToList(); - if (osuBeatmap.HitObjects.Count > 0) + if (hitObjects.Count > 0) { // Reset stacking - foreach (var h in osuBeatmap.HitObjects) + foreach (var h in hitObjects) h.StackHeight = 0; if (Beatmap.BeatmapInfo.BeatmapVersion >= 6) - applyStacking(osuBeatmap, 0, osuBeatmap.HitObjects.Count - 1); + applyStacking(Beatmap.BeatmapInfo, hitObjects, 0, hitObjects.Count - 1); else - applyStackingOld(osuBeatmap); + applyStackingOld(Beatmap.BeatmapInfo, hitObjects); } } - private void applyStacking(Beatmap beatmap, int startIndex, int endIndex) + private void applyStacking(BeatmapInfo beatmapInfo, List hitObjects, int startIndex, int endIndex) { ArgumentOutOfRangeException.ThrowIfGreaterThan(startIndex, endIndex); ArgumentOutOfRangeException.ThrowIfNegative(startIndex); @@ -64,24 +65,24 @@ namespace osu.Game.Rulesets.Osu.Beatmaps int extendedEndIndex = endIndex; - if (endIndex < beatmap.HitObjects.Count - 1) + if (endIndex < hitObjects.Count - 1) { // Extend the end index to include objects they are stacked on for (int i = endIndex; i >= startIndex; i--) { int stackBaseIndex = i; - for (int n = stackBaseIndex + 1; n < beatmap.HitObjects.Count; n++) + for (int n = stackBaseIndex + 1; n < hitObjects.Count; n++) { - OsuHitObject stackBaseObject = beatmap.HitObjects[stackBaseIndex]; + OsuHitObject stackBaseObject = hitObjects[stackBaseIndex]; if (stackBaseObject is Spinner) break; - OsuHitObject objectN = beatmap.HitObjects[n]; + OsuHitObject objectN = hitObjects[n]; if (objectN is Spinner) continue; double endTime = stackBaseObject.GetEndTime(); - double stackThreshold = objectN.TimePreempt * beatmap.BeatmapInfo.StackLeniency; + double stackThreshold = objectN.TimePreempt * beatmapInfo.StackLeniency; if (objectN.StartTime - endTime > stackThreshold) // We are no longer within stacking range of the next object. @@ -100,7 +101,7 @@ namespace osu.Game.Rulesets.Osu.Beatmaps if (stackBaseIndex > extendedEndIndex) { extendedEndIndex = stackBaseIndex; - if (extendedEndIndex == beatmap.HitObjects.Count - 1) + if (extendedEndIndex == hitObjects.Count - 1) break; } } @@ -123,10 +124,10 @@ namespace osu.Game.Rulesets.Osu.Beatmaps * 2 and 1 will be ignored in the i loop because they already have a stack value. */ - OsuHitObject objectI = beatmap.HitObjects[i]; + OsuHitObject objectI = hitObjects[i]; if (objectI.StackHeight != 0 || objectI is Spinner) continue; - double stackThreshold = objectI.TimePreempt * beatmap.BeatmapInfo.StackLeniency; + double stackThreshold = objectI.TimePreempt * beatmapInfo.StackLeniency; /* If this object is a hitcircle, then we enter this "special" case. * It either ends with a stack of hitcircles only, or a stack of hitcircles that are underneath a slider. @@ -136,7 +137,7 @@ namespace osu.Game.Rulesets.Osu.Beatmaps { while (--n >= 0) { - OsuHitObject objectN = beatmap.HitObjects[n]; + OsuHitObject objectN = hitObjects[n]; if (objectN is Spinner) continue; double endTime = objectN.GetEndTime(); @@ -164,7 +165,7 @@ namespace osu.Game.Rulesets.Osu.Beatmaps for (int j = n + 1; j <= i; j++) { // For each object which was declared under this slider, we will offset it to appear *below* the slider end (rather than above). - OsuHitObject objectJ = beatmap.HitObjects[j]; + OsuHitObject objectJ = hitObjects[j]; if (Vector2Extensions.Distance(objectN.EndPosition, objectJ.Position) < stack_distance) objectJ.StackHeight -= offset; } @@ -191,7 +192,7 @@ namespace osu.Game.Rulesets.Osu.Beatmaps */ while (--n >= startIndex) { - OsuHitObject objectN = beatmap.HitObjects[n]; + OsuHitObject objectN = hitObjects[n]; if (objectN is Spinner) continue; if (objectI.StartTime - objectN.StartTime > stackThreshold) @@ -208,11 +209,11 @@ namespace osu.Game.Rulesets.Osu.Beatmaps } } - private void applyStackingOld(Beatmap beatmap) + private void applyStackingOld(BeatmapInfo beatmapInfo, List hitObjects) { - for (int i = 0; i < beatmap.HitObjects.Count; i++) + for (int i = 0; i < hitObjects.Count; i++) { - OsuHitObject currHitObject = beatmap.HitObjects[i]; + OsuHitObject currHitObject = hitObjects[i]; if (currHitObject.StackHeight != 0 && !(currHitObject is Slider)) continue; @@ -220,11 +221,11 @@ namespace osu.Game.Rulesets.Osu.Beatmaps double startTime = currHitObject.GetEndTime(); int sliderStack = 0; - for (int j = i + 1; j < beatmap.HitObjects.Count; j++) + for (int j = i + 1; j < hitObjects.Count; j++) { - double stackThreshold = beatmap.HitObjects[i].TimePreempt * beatmap.BeatmapInfo.StackLeniency; + double stackThreshold = hitObjects[i].TimePreempt * beatmapInfo.StackLeniency; - if (beatmap.HitObjects[j].StartTime - stackThreshold > startTime) + if (hitObjects[j].StartTime - stackThreshold > startTime) break; // The start position of the hitobject, or the position at the end of the path if the hitobject is a slider @@ -239,17 +240,17 @@ namespace osu.Game.Rulesets.Osu.Beatmaps // 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) + if (Vector2Extensions.Distance(hitObjects[j].Position, currHitObject.Position) < stack_distance) { currHitObject.StackHeight++; - startTime = beatmap.HitObjects[j].StartTime; + startTime = hitObjects[j].StartTime; } - else if (Vector2Extensions.Distance(beatmap.HitObjects[j].Position, position2) < stack_distance) + else if (Vector2Extensions.Distance(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].StartTime; + hitObjects[j].StackHeight -= sliderStack; + startTime = hitObjects[j].StartTime; } } } diff --git a/osu.Game.Rulesets.Osu/Edit/Setup/OsuSetupSection.cs b/osu.Game.Rulesets.Osu/Edit/Setup/OsuSetupSection.cs index ac567559b8..552b887081 100644 --- a/osu.Game.Rulesets.Osu/Edit/Setup/OsuSetupSection.cs +++ b/osu.Game.Rulesets.Osu/Edit/Setup/OsuSetupSection.cs @@ -49,6 +49,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Setup private void updateBeatmap() { Beatmap.BeatmapInfo.StackLeniency = stackLeniency.Current.Value; + Beatmap.UpdateAllHitObjects(); Beatmap.SaveState(); } } diff --git a/osu.Game/Screens/Edit/EditorBeatmap.cs b/osu.Game/Screens/Edit/EditorBeatmap.cs index 6363ed2854..5be1d27805 100644 --- a/osu.Game/Screens/Edit/EditorBeatmap.cs +++ b/osu.Game/Screens/Edit/EditorBeatmap.cs @@ -105,7 +105,7 @@ namespace osu.Game.Screens.Edit BeatmapSkin.BeatmapSkinChanged += SaveState; } - beatmapProcessor = playableBeatmap.BeatmapInfo.Ruleset.CreateInstance().CreateBeatmapProcessor(PlayableBeatmap); + beatmapProcessor = playableBeatmap.BeatmapInfo.Ruleset.CreateInstance().CreateBeatmapProcessor(this); foreach (var obj in HitObjects) trackStartTime(obj); From b8e07045545fbfe3b46818b01fed515f12977fc8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 11 Jun 2024 09:14:46 +0200 Subject: [PATCH 1039/2493] Implement singular change events for `ControlPoint` --- .../Beatmaps/ControlPoints/ControlPoint.cs | 22 ++++++++++++++++++- .../ControlPoints/DifficultyControlPoint.cs | 5 +++++ .../ControlPoints/EffectControlPoint.cs | 6 +++++ .../ControlPoints/SampleControlPoint.cs | 6 +++++ .../ControlPoints/TimingControlPoint.cs | 7 ++++++ 5 files changed, 45 insertions(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/ControlPoints/ControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/ControlPoint.cs index f46e4af332..f08a3d3f87 100644 --- a/osu.Game/Beatmaps/ControlPoints/ControlPoint.cs +++ b/osu.Game/Beatmaps/ControlPoints/ControlPoint.cs @@ -11,8 +11,28 @@ namespace osu.Game.Beatmaps.ControlPoints { public abstract class ControlPoint : IComparable, IDeepCloneable, IEquatable, IControlPoint { + /// + /// Invoked when any of this 's properties have changed. + /// + public event Action? Changed; + + protected void RaiseChanged() => Changed?.Invoke(this); + + private double time; + [JsonIgnore] - public double Time { get; set; } + public double Time + { + get => time; + set + { + if (time == value) + return; + + time = value; + RaiseChanged(); + } + } public void AttachGroup(ControlPointGroup pointGroup) => Time = pointGroup.Time; diff --git a/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs index 05230c85f4..9f8ed1b396 100644 --- a/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs +++ b/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs @@ -44,6 +44,11 @@ namespace osu.Game.Beatmaps.ControlPoints set => SliderVelocityBindable.Value = value; } + public DifficultyControlPoint() + { + SliderVelocityBindable.BindValueChanged(_ => RaiseChanged()); + } + public override bool IsRedundant(ControlPoint? existing) => existing is DifficultyControlPoint existingDifficulty && GenerateTicks == existingDifficulty.GenerateTicks diff --git a/osu.Game/Beatmaps/ControlPoints/EffectControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/EffectControlPoint.cs index 0138ac7569..d48ed957ee 100644 --- a/osu.Game/Beatmaps/ControlPoints/EffectControlPoint.cs +++ b/osu.Game/Beatmaps/ControlPoints/EffectControlPoint.cs @@ -50,6 +50,12 @@ namespace osu.Game.Beatmaps.ControlPoints set => KiaiModeBindable.Value = value; } + public EffectControlPoint() + { + KiaiModeBindable.BindValueChanged(_ => RaiseChanged()); + ScrollSpeedBindable.BindValueChanged(_ => RaiseChanged()); + } + public override bool IsRedundant(ControlPoint? existing) => existing is EffectControlPoint existingEffect && KiaiMode == existingEffect.KiaiMode diff --git a/osu.Game/Beatmaps/ControlPoints/SampleControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/SampleControlPoint.cs index ae4bdafd6f..800d9f9abc 100644 --- a/osu.Game/Beatmaps/ControlPoints/SampleControlPoint.cs +++ b/osu.Game/Beatmaps/ControlPoints/SampleControlPoint.cs @@ -56,6 +56,12 @@ namespace osu.Game.Beatmaps.ControlPoints set => SampleVolumeBindable.Value = value; } + public SampleControlPoint() + { + SampleBankBindable.BindValueChanged(_ => RaiseChanged()); + SampleVolumeBindable.BindValueChanged(_ => RaiseChanged()); + } + /// /// Create a SampleInfo based on the sample settings in this control point. /// diff --git a/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs index 4e69486e2d..9ac361cffe 100644 --- a/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs +++ b/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs @@ -82,6 +82,13 @@ namespace osu.Game.Beatmaps.ControlPoints /// public double BPM => 60000 / BeatLength; + public TimingControlPoint() + { + TimeSignatureBindable.BindValueChanged(_ => RaiseChanged()); + OmitFirstBarLineBindable.BindValueChanged(_ => RaiseChanged()); + BeatLengthBindable.BindValueChanged(_ => RaiseChanged()); + } + // Timing points are never redundant as they can change the time signature. public override bool IsRedundant(ControlPoint? existing) => false; From 694cfd0e259d2e5357a5692b4e05c8e680ddb49c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 11 Jun 2024 09:15:14 +0200 Subject: [PATCH 1040/2493] Expose singular coarse-grained change event on `ControlPointInfo` --- .../Beatmaps/ControlPoints/ControlPointGroup.cs | 5 +++++ .../Beatmaps/ControlPoints/ControlPointInfo.cs | 14 ++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/osu.Game/Beatmaps/ControlPoints/ControlPointGroup.cs b/osu.Game/Beatmaps/ControlPoints/ControlPointGroup.cs index 1f34f3777d..c9c87dc85d 100644 --- a/osu.Game/Beatmaps/ControlPoints/ControlPointGroup.cs +++ b/osu.Game/Beatmaps/ControlPoints/ControlPointGroup.cs @@ -10,8 +10,11 @@ namespace osu.Game.Beatmaps.ControlPoints public class ControlPointGroup : IComparable, IEquatable { public event Action? ItemAdded; + public event Action? ItemChanged; public event Action? ItemRemoved; + private void raiseItemChanged(ControlPoint controlPoint) => ItemChanged?.Invoke(controlPoint); + /// /// The time at which the control point takes effect. /// @@ -39,12 +42,14 @@ namespace osu.Game.Beatmaps.ControlPoints controlPoints.Add(point); ItemAdded?.Invoke(point); + point.Changed += raiseItemChanged; } public void Remove(ControlPoint point) { controlPoints.Remove(point); ItemRemoved?.Invoke(point); + point.Changed -= raiseItemChanged; } public sealed override bool Equals(object? obj) diff --git a/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs b/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs index 1a15db98e4..cb7515b66c 100644 --- a/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs +++ b/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs @@ -19,6 +19,14 @@ namespace osu.Game.Beatmaps.ControlPoints [Serializable] public class ControlPointInfo : IDeepCloneable { + /// + /// Invoked on any change to the set of control points. + /// + [CanBeNull] + public event Action ControlPointsChanged; + + private void raiseControlPointsChanged([CanBeNull] ControlPoint _ = null) => ControlPointsChanged?.Invoke(); + /// /// All control points grouped by time. /// @@ -116,6 +124,7 @@ namespace osu.Game.Beatmaps.ControlPoints if (addIfNotExisting) { newGroup.ItemAdded += GroupItemAdded; + newGroup.ItemChanged += raiseControlPointsChanged; newGroup.ItemRemoved += GroupItemRemoved; groups.Insert(~i, newGroup); @@ -131,6 +140,7 @@ namespace osu.Game.Beatmaps.ControlPoints group.Remove(item); group.ItemAdded -= GroupItemAdded; + group.ItemChanged -= raiseControlPointsChanged; group.ItemRemoved -= GroupItemRemoved; groups.Remove(group); @@ -287,6 +297,8 @@ namespace osu.Game.Beatmaps.ControlPoints default: throw new ArgumentException($"A control point of unexpected type {controlPoint.GetType()} was added to this {nameof(ControlPointInfo)}"); } + + raiseControlPointsChanged(); } protected virtual void GroupItemRemoved(ControlPoint controlPoint) @@ -301,6 +313,8 @@ namespace osu.Game.Beatmaps.ControlPoints effectPoints.Remove(typed); break; } + + raiseControlPointsChanged(); } public ControlPointInfo DeepClone() From 4048a4bdfbfb1efc6bc4b8e5b4368dd045610934 Mon Sep 17 00:00:00 2001 From: ColdVolcano <16726733+ColdVolcano@users.noreply.github.com> Date: Tue, 11 Jun 2024 02:20:14 -0600 Subject: [PATCH 1041/2493] fix accuracy counter separating whole and fraction parts with wireframes off --- osu.Game/Screens/Play/HUD/ArgonCounterTextComponent.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Screens/Play/HUD/ArgonCounterTextComponent.cs b/osu.Game/Screens/Play/HUD/ArgonCounterTextComponent.cs index efb4d2108e..bd8f17185b 100644 --- a/osu.Game/Screens/Play/HUD/ArgonCounterTextComponent.cs +++ b/osu.Game/Screens/Play/HUD/ArgonCounterTextComponent.cs @@ -58,6 +58,7 @@ namespace osu.Game.Screens.Play.HUD labelText = new OsuSpriteText { Alpha = 0, + BypassAutoSizeAxes = Axes.X, Text = label.GetValueOrDefault(), Font = OsuFont.Torus.With(size: 12, weight: FontWeight.Bold), Margin = new MarginPadding { Left = 2.5f }, @@ -65,6 +66,8 @@ namespace osu.Game.Screens.Play.HUD NumberContainer = new Container { AutoSizeAxes = Axes.Both, + Anchor = anchor, + Origin = anchor, Children = new[] { wireframesPart = new ArgonCounterSpriteText(wireframesLookup) From 10af64234275b47fb26543519eee0fba220db68b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 11 Jun 2024 11:30:20 +0200 Subject: [PATCH 1042/2493] Split mania difficulty section implementation off completely from base - "Circle size" / key count needs completely different handling. - Approach rate does not exist in mania. --- .../Edit/Setup/ManiaDifficultySection.cs | 120 +++++++++++++++++- osu.Game.Rulesets.Mania/ManiaRuleset.cs | 2 +- osu.Game/Rulesets/Ruleset.cs | 2 +- 3 files changed, 117 insertions(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Edit/Setup/ManiaDifficultySection.cs b/osu.Game.Rulesets.Mania/Edit/Setup/ManiaDifficultySection.cs index 4f983debea..0df6f3a1f7 100644 --- a/osu.Game.Rulesets.Mania/Edit/Setup/ManiaDifficultySection.cs +++ b/osu.Game.Rulesets.Mania/Edit/Setup/ManiaDifficultySection.cs @@ -3,20 +3,130 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Extensions; +using osu.Framework.Graphics; +using osu.Framework.Localisation; +using osu.Game.Beatmaps; +using osu.Game.Graphics.UserInterfaceV2; +using osu.Game.Localisation; using osu.Game.Resources.Localisation.Web; +using osu.Game.Screens.Edit; using osu.Game.Screens.Edit.Setup; namespace osu.Game.Rulesets.Mania.Edit.Setup { - public partial class ManiaDifficultySection : DifficultySection + public partial class ManiaDifficultySection : SetupSection { + public override LocalisableString Title => EditorSetupStrings.DifficultyHeader; + + private LabelledSliderBar keyCountSlider { get; set; } = null!; + private LabelledSliderBar healthDrainSlider { get; set; } = null!; + private LabelledSliderBar overallDifficultySlider { get; set; } = null!; + private LabelledSliderBar baseVelocitySlider { get; set; } = null!; + private LabelledSliderBar tickRateSlider { get; set; } = null!; + + [Resolved] + private Editor editor { get; set; } = null!; + + [Resolved] + private IEditorChangeHandler changeHandler { get; set; } = null!; + [BackgroundDependencyLoader] private void load() { - CircleSizeSlider.Label = BeatmapsetsStrings.ShowStatsCsMania; - CircleSizeSlider.Description = "The number of columns in the beatmap"; - if (CircleSizeSlider.Current is BindableNumber circleSizeFloat) - circleSizeFloat.Precision = 1; + Children = new Drawable[] + { + keyCountSlider = new LabelledSliderBar + { + Label = BeatmapsetsStrings.ShowStatsCsMania, + FixedLabelWidth = LABEL_WIDTH, + Description = "The number of columns in the beatmap", + Current = new BindableFloat(Beatmap.Difficulty.CircleSize) + { + Default = BeatmapDifficulty.DEFAULT_DIFFICULTY, + MinValue = 0, + MaxValue = 10, + Precision = 1, + } + }, + healthDrainSlider = new LabelledSliderBar + { + Label = BeatmapsetsStrings.ShowStatsDrain, + FixedLabelWidth = LABEL_WIDTH, + Description = EditorSetupStrings.DrainRateDescription, + Current = new BindableFloat(Beatmap.Difficulty.DrainRate) + { + Default = BeatmapDifficulty.DEFAULT_DIFFICULTY, + MinValue = 0, + MaxValue = 10, + Precision = 0.1f, + } + }, + overallDifficultySlider = new LabelledSliderBar + { + Label = BeatmapsetsStrings.ShowStatsAccuracy, + FixedLabelWidth = LABEL_WIDTH, + Description = EditorSetupStrings.OverallDifficultyDescription, + Current = new BindableFloat(Beatmap.Difficulty.OverallDifficulty) + { + Default = BeatmapDifficulty.DEFAULT_DIFFICULTY, + MinValue = 0, + MaxValue = 10, + Precision = 0.1f, + } + }, + baseVelocitySlider = new LabelledSliderBar + { + Label = EditorSetupStrings.BaseVelocity, + FixedLabelWidth = LABEL_WIDTH, + Description = EditorSetupStrings.BaseVelocityDescription, + Current = new BindableDouble(Beatmap.Difficulty.SliderMultiplier) + { + Default = 1.4, + MinValue = 0.4, + MaxValue = 3.6, + Precision = 0.01f, + } + }, + tickRateSlider = new LabelledSliderBar + { + Label = EditorSetupStrings.TickRate, + FixedLabelWidth = LABEL_WIDTH, + Description = EditorSetupStrings.TickRateDescription, + Current = new BindableDouble(Beatmap.Difficulty.SliderTickRate) + { + Default = 1, + MinValue = 1, + MaxValue = 4, + Precision = 1, + } + }, + }; + + keyCountSlider.Current.BindValueChanged(updateKeyCount); + healthDrainSlider.Current.BindValueChanged(_ => updateValues()); + overallDifficultySlider.Current.BindValueChanged(_ => updateValues()); + baseVelocitySlider.Current.BindValueChanged(_ => updateValues()); + tickRateSlider.Current.BindValueChanged(_ => updateValues()); + } + + private void updateKeyCount(ValueChangedEvent keyCount) + { + updateValues(); + } + + private void updateValues() + { + // for now, update these on commit rather than making BeatmapMetadata bindables. + // after switching database engines we can reconsider if switching to bindables is a good direction. + Beatmap.Difficulty.CircleSize = keyCountSlider.Current.Value; + Beatmap.Difficulty.DrainRate = healthDrainSlider.Current.Value; + Beatmap.Difficulty.OverallDifficulty = overallDifficultySlider.Current.Value; + Beatmap.Difficulty.SliderMultiplier = baseVelocitySlider.Current.Value; + Beatmap.Difficulty.SliderTickRate = tickRateSlider.Current.Value; + + Beatmap.UpdateAllHitObjects(); + Beatmap.SaveState(); } } } diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs index b5614e2b56..40eb44944c 100644 --- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs @@ -421,7 +421,7 @@ namespace osu.Game.Rulesets.Mania public override RulesetSetupSection CreateEditorSetupSection() => new ManiaSetupSection(); - public override DifficultySection CreateEditorDifficultySection() => new ManiaDifficultySection(); + public override SetupSection CreateEditorDifficultySection() => new ManiaDifficultySection(); public int GetKeyCount(IBeatmapInfo beatmapInfo, IReadOnlyList? mods = null) => ManiaBeatmapConverter.GetColumnCount(LegacyBeatmapConversionDifficultyInfo.FromBeatmapInfo(beatmapInfo), mods); diff --git a/osu.Game/Rulesets/Ruleset.cs b/osu.Game/Rulesets/Ruleset.cs index 37a35fd3ae..cae2ce610e 100644 --- a/osu.Game/Rulesets/Ruleset.cs +++ b/osu.Game/Rulesets/Ruleset.cs @@ -401,6 +401,6 @@ namespace osu.Game.Rulesets /// /// Can be overridden to alter the difficulty section to the editor beatmap setup screen. /// - public virtual DifficultySection? CreateEditorDifficultySection() => null; + public virtual SetupSection? CreateEditorDifficultySection() => null; } } From 3afe98612c4020fe833d410ad4de2d85d5036cd5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 11 Jun 2024 11:31:30 +0200 Subject: [PATCH 1043/2493] Add `RestoreState()` to `IEditorChangeHandler` --- osu.Game/Overlays/SkinEditor/SkinEditor.cs | 2 ++ osu.Game/Screens/Edit/EditorChangeHandler.cs | 4 ---- osu.Game/Screens/Edit/IEditorChangeHandler.cs | 6 ++++++ 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/osu.Game/Overlays/SkinEditor/SkinEditor.cs b/osu.Game/Overlays/SkinEditor/SkinEditor.cs index 67fd6a9550..41fd701a09 100644 --- a/osu.Game/Overlays/SkinEditor/SkinEditor.cs +++ b/osu.Game/Overlays/SkinEditor/SkinEditor.cs @@ -540,6 +540,8 @@ namespace osu.Game.Overlays.SkinEditor protected void Redo() => changeHandler?.RestoreState(1); + void IEditorChangeHandler.RestoreState(int direction) => changeHandler?.RestoreState(direction); + public void Save(bool userTriggered = true) => save(currentSkin.Value, userTriggered); private void save(Skin skin, bool userTriggered = true) diff --git a/osu.Game/Screens/Edit/EditorChangeHandler.cs b/osu.Game/Screens/Edit/EditorChangeHandler.cs index 0bb17e4c5d..f8ef133549 100644 --- a/osu.Game/Screens/Edit/EditorChangeHandler.cs +++ b/osu.Game/Screens/Edit/EditorChangeHandler.cs @@ -83,10 +83,6 @@ namespace osu.Game.Screens.Edit } } - /// - /// Restores an older or newer state. - /// - /// The direction to restore in. If less than 0, an older state will be used. If greater than 0, a newer state will be used. public void RestoreState(int direction) { if (TransactionActive) diff --git a/osu.Game/Screens/Edit/IEditorChangeHandler.cs b/osu.Game/Screens/Edit/IEditorChangeHandler.cs index 9fe40ba1b1..2259b52ea8 100644 --- a/osu.Game/Screens/Edit/IEditorChangeHandler.cs +++ b/osu.Game/Screens/Edit/IEditorChangeHandler.cs @@ -43,5 +43,11 @@ namespace osu.Game.Screens.Edit /// Note that this will be a no-op if there is a change in progress via . /// void SaveState(); + + /// + /// Restores an older or newer state. + /// + /// The direction to restore in. If less than 0, an older state will be used. If greater than 0, a newer state will be used. + void RestoreState(int direction); } } From da53a11d3ca009670b2f0d04aec887b0e8f70d14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 11 Jun 2024 11:31:49 +0200 Subject: [PATCH 1044/2493] Attempt full editor reload on key count change --- .../Edit/Setup/ManiaDifficultySection.cs | 22 ++++++++++++ osu.Game/Localisation/EditorDialogsStrings.cs | 5 +++ osu.Game/Screens/Edit/Editor.cs | 21 ++++++++++++ osu.Game/Screens/Edit/ReloadEditorDialog.cs | 34 +++++++++++++++++++ 4 files changed, 82 insertions(+) create mode 100644 osu.Game/Screens/Edit/ReloadEditorDialog.cs diff --git a/osu.Game.Rulesets.Mania/Edit/Setup/ManiaDifficultySection.cs b/osu.Game.Rulesets.Mania/Edit/Setup/ManiaDifficultySection.cs index 0df6f3a1f7..f62c63bf8e 100644 --- a/osu.Game.Rulesets.Mania/Edit/Setup/ManiaDifficultySection.cs +++ b/osu.Game.Rulesets.Mania/Edit/Setup/ManiaDifficultySection.cs @@ -110,9 +110,31 @@ namespace osu.Game.Rulesets.Mania.Edit.Setup tickRateSlider.Current.BindValueChanged(_ => updateValues()); } + private bool updatingKeyCount; + private void updateKeyCount(ValueChangedEvent keyCount) { + if (updatingKeyCount) return; + + updatingKeyCount = true; + updateValues(); + editor.Reload().ContinueWith(t => + { + if (!t.GetResultSafely()) + { + Schedule(() => + { + changeHandler.RestoreState(-1); + Beatmap.Difficulty.CircleSize = keyCountSlider.Current.Value = keyCount.OldValue; + updatingKeyCount = false; + }); + } + else + { + updatingKeyCount = false; + } + }); } private void updateValues() diff --git a/osu.Game/Localisation/EditorDialogsStrings.cs b/osu.Game/Localisation/EditorDialogsStrings.cs index fc4c2b7f2a..94f28c617c 100644 --- a/osu.Game/Localisation/EditorDialogsStrings.cs +++ b/osu.Game/Localisation/EditorDialogsStrings.cs @@ -49,6 +49,11 @@ namespace osu.Game.Localisation /// public static LocalisableString ContinueEditing => new TranslatableString(getKey(@"continue_editing"), @"Oops, continue editing"); + /// + /// "The editor must be reloaded to apply this change. The beatmap will be saved." + /// + public static LocalisableString EditorReloadDialogHeader => new TranslatableString(getKey(@"editor_reload_dialog_header"), @"The editor must be reloaded to apply this change. The beatmap will be saved."); + private static string getKey(string key) => $@"{prefix}:{key}"; } } diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 3e3e772810..a630a5df41 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -1231,6 +1231,27 @@ namespace osu.Game.Screens.Edit loader?.CancelPendingDifficultySwitch(); } + public Task Reload() + { + var tcs = new TaskCompletionSource(); + + dialogOverlay.Push(new ReloadEditorDialog( + reload: () => + { + bool reloadedSuccessfully = attemptMutationOperation(() => + { + if (!Save()) + return false; + + SwitchToDifficulty(editorBeatmap.BeatmapInfo); + return true; + }); + tcs.SetResult(reloadedSuccessfully); + }, + cancel: () => tcs.SetResult(false))); + return tcs.Task; + } + public void HandleTimestamp(string timestamp) { if (!EditorTimestampParser.TryParse(timestamp, out var timeSpan, out string selection)) diff --git a/osu.Game/Screens/Edit/ReloadEditorDialog.cs b/osu.Game/Screens/Edit/ReloadEditorDialog.cs new file mode 100644 index 0000000000..72a9f81347 --- /dev/null +++ b/osu.Game/Screens/Edit/ReloadEditorDialog.cs @@ -0,0 +1,34 @@ +// 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.Graphics.Sprites; +using osu.Game.Overlays.Dialog; +using osu.Game.Localisation; + +namespace osu.Game.Screens.Edit +{ + public partial class ReloadEditorDialog : PopupDialog + { + public ReloadEditorDialog(Action reload, Action cancel) + { + HeaderText = EditorDialogsStrings.EditorReloadDialogHeader; + + Icon = FontAwesome.Solid.Sync; + + Buttons = new PopupDialogButton[] + { + new PopupDialogOkButton + { + Text = DialogStrings.Confirm, + Action = reload + }, + new PopupDialogCancelButton + { + Text = DialogStrings.Cancel, + Action = cancel + } + }; + } + } +} From 922837dd3a6c71bd04163f48270ecb25682b6551 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 11 Jun 2024 09:33:25 +0200 Subject: [PATCH 1045/2493] Reload scrolling composer on control point changes --- .../Rulesets/Edit/ScrollingHitObjectComposer.cs | 17 +++++++++++++++++ osu.Game/Screens/Edit/Editor.cs | 9 +++++++++ 2 files changed, 26 insertions(+) diff --git a/osu.Game/Rulesets/Edit/ScrollingHitObjectComposer.cs b/osu.Game/Rulesets/Edit/ScrollingHitObjectComposer.cs index eb73cef01a..223b770b48 100644 --- a/osu.Game/Rulesets/Edit/ScrollingHitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/ScrollingHitObjectComposer.cs @@ -4,6 +4,7 @@ using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; @@ -12,6 +13,7 @@ using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets.Edit.Tools; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.UI.Scrolling; +using osu.Game.Screens.Edit; using osu.Game.Screens.Edit.Components.TernaryButtons; using osu.Game.Screens.Edit.Compose.Components; using osuTK; @@ -21,6 +23,9 @@ namespace osu.Game.Rulesets.Edit public abstract partial class ScrollingHitObjectComposer : HitObjectComposer where TObject : HitObject { + [Resolved] + private Editor? editor { get; set; } + private readonly Bindable showSpeedChanges = new Bindable(); private Bindable configShowSpeedChanges = null!; @@ -72,6 +77,8 @@ namespace osu.Game.Rulesets.Edit if (beatSnapGrid != null) AddInternal(beatSnapGrid); + + EditorBeatmap.ControlPointInfo.ControlPointsChanged += expireComposeScreenOnControlPointChange; } protected override void UpdateAfterChildren() @@ -104,5 +111,15 @@ namespace osu.Game.Rulesets.Edit beatSnapGrid.SelectionTimeRange = null; } } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (EditorBeatmap.IsNotNull()) + EditorBeatmap.ControlPointInfo.ControlPointsChanged -= expireComposeScreenOnControlPointChange; + } + + private void expireComposeScreenOnControlPointChange() => editor?.ReloadComposeScreen(); } } diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 3e3e772810..9703785856 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -996,6 +996,15 @@ namespace osu.Game.Screens.Edit } } + /// + /// Forces a reload of the compose screen after significant configuration changes. + /// + /// + /// This can be necessary for scrolling rulesets, as they do not easily support control points changing under them. + /// The reason that this works is that will re-instantiate the screen whenever it is requested next. + /// + public void ReloadComposeScreen() => screenContainer.SingleOrDefault(s => s.Type == EditorScreenMode.Compose)?.RemoveAndDisposeImmediately(); + [CanBeNull] private ScheduledDelegate playbackDisabledDebounce; From e67d73be7dd1ffa6eb7fd8089c801206c889536a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 11 Jun 2024 12:00:02 +0200 Subject: [PATCH 1046/2493] Add test coverage --- .../Editor/TestSceneManiaEditorSaving.cs | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaEditorSaving.cs diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaEditorSaving.cs b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaEditorSaving.cs new file mode 100644 index 0000000000..9765648f44 --- /dev/null +++ b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaEditorSaving.cs @@ -0,0 +1,45 @@ +// 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.Testing; +using osu.Game.Graphics.UserInterfaceV2; +using osu.Game.Overlays; +using osu.Game.Screens.Edit; +using osu.Game.Screens.Edit.Setup; +using osu.Game.Tests.Visual; +using osuTK.Input; + +namespace osu.Game.Rulesets.Mania.Tests.Editor +{ + public partial class TestSceneManiaEditorSaving : EditorSavingTestScene + { + protected override Ruleset CreateRuleset() => new ManiaRuleset(); + + [Test] + public void TestKeyCountChange() + { + LabelledSliderBar keyCount = null!; + + AddStep("go to setup screen", () => InputManager.Key(Key.F4)); + AddUntilStep("retrieve key count slider", () => keyCount = Editor.ChildrenOfType().Single().ChildrenOfType>().First(), () => Is.Not.Null); + AddAssert("key count is 5", () => keyCount.Current.Value, () => Is.EqualTo(5)); + AddStep("change key count to 8", () => + { + keyCount.Current.Value = 8; + }); + AddUntilStep("dialog visible", () => Game.ChildrenOfType().SingleOrDefault()?.CurrentDialog, Is.InstanceOf); + AddStep("refuse", () => InputManager.Key(Key.Number2)); + AddAssert("key count is 5", () => keyCount.Current.Value, () => Is.EqualTo(5)); + + AddStep("change key count to 8 again", () => + { + keyCount.Current.Value = 8; + }); + AddUntilStep("dialog visible", () => Game.ChildrenOfType().Single().CurrentDialog, Is.InstanceOf); + AddStep("acquiesce", () => InputManager.Key(Key.Number1)); + AddUntilStep("beatmap became 8K", () => Game.Beatmap.Value.BeatmapInfo.Difficulty.CircleSize, () => Is.EqualTo(8)); + } + } +} From 12dd60736a37d5c173eb9a7d04e207c4a623d3d5 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Tue, 11 Jun 2024 21:20:42 +0200 Subject: [PATCH 1047/2493] remove code already covered by updatePrimaryBankState --- .../Edit/Compose/Components/Timeline/SamplePointPiece.cs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs index f318a52b28..0d392e9a99 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs @@ -172,10 +172,6 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline allRelevantSamples = relevantObjects.Select(GetRelevantSamples).ToArray(); // even if there are multiple objects selected, we can still display sample volume or bank if they all have the same value. - string? commonBank = getCommonBank(); - if (!string.IsNullOrEmpty(commonBank)) - bank.Current.Value = commonBank; - int? commonVolume = getCommonVolume(); if (commonVolume != null) volume.Current.Value = commonVolume.Value; From 869cd40195348a8056968bd9ba2f9927a1dc49ea Mon Sep 17 00:00:00 2001 From: OliBomby Date: Tue, 11 Jun 2024 21:31:18 +0200 Subject: [PATCH 1048/2493] Fixed samples without additions contributing to common addition bank while not having an editable addition bank --- .../Edit/Compose/Components/Timeline/SamplePointPiece.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs index 0d392e9a99..930b78b468 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs @@ -77,7 +77,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline public static string? GetAdditionBankValue(IEnumerable samples) { - return samples.FirstOrDefault(o => o.Name != HitSampleInfo.HIT_NORMAL)?.Bank ?? GetBankValue(samples); + return samples.FirstOrDefault(o => o.Name != HitSampleInfo.HIT_NORMAL)?.Bank; } public static int GetVolumeValue(ICollection samples) @@ -212,7 +212,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline } private string? getCommonBank() => allRelevantSamples.Select(GetBankValue).Distinct().Count() == 1 ? GetBankValue(allRelevantSamples.First()) : null; - private string? getCommonAdditionBank() => allRelevantSamples.Select(GetAdditionBankValue).Distinct().Count() == 1 ? GetAdditionBankValue(allRelevantSamples.First()) : null; + private string? getCommonAdditionBank() => allRelevantSamples.Select(GetAdditionBankValue).Where(o => o is not null).Distinct().Count() == 1 ? GetAdditionBankValue(allRelevantSamples.First()) : null; private int? getCommonVolume() => allRelevantSamples.Select(GetVolumeValue).Distinct().Count() == 1 ? GetVolumeValue(allRelevantSamples.First()) : null; private void updatePrimaryBankState() From 2a8bd8d9686fe3cd6474b8a939eb6123a7858f75 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 12 Jun 2024 11:23:56 +0800 Subject: [PATCH 1049/2493] Fix failing tests due to missing DI pieces --- .../Edit/Setup/ManiaDifficultySection.cs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Edit/Setup/ManiaDifficultySection.cs b/osu.Game.Rulesets.Mania/Edit/Setup/ManiaDifficultySection.cs index f62c63bf8e..62b54a7215 100644 --- a/osu.Game.Rulesets.Mania/Edit/Setup/ManiaDifficultySection.cs +++ b/osu.Game.Rulesets.Mania/Edit/Setup/ManiaDifficultySection.cs @@ -26,10 +26,10 @@ namespace osu.Game.Rulesets.Mania.Edit.Setup private LabelledSliderBar tickRateSlider { get; set; } = null!; [Resolved] - private Editor editor { get; set; } = null!; + private Editor? editor { get; set; } [Resolved] - private IEditorChangeHandler changeHandler { get; set; } = null!; + private IEditorChangeHandler? changeHandler { get; set; } [BackgroundDependencyLoader] private void load() @@ -116,16 +116,19 @@ namespace osu.Game.Rulesets.Mania.Edit.Setup { if (updatingKeyCount) return; + updateValues(); + + if (editor == null) return; + updatingKeyCount = true; - updateValues(); editor.Reload().ContinueWith(t => { if (!t.GetResultSafely()) { Schedule(() => { - changeHandler.RestoreState(-1); + changeHandler!.RestoreState(-1); Beatmap.Difficulty.CircleSize = keyCountSlider.Current.Value = keyCount.OldValue; updatingKeyCount = false; }); From 5e002fbf9b8af739189bad1a12e4ab288375e640 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 12 Jun 2024 08:59:50 +0200 Subject: [PATCH 1050/2493] Fix user mod select button being inserted in incorrect place --- osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs index bff58dbb58..5740825ca2 100644 --- a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs @@ -257,7 +257,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge if (playlistItem.AllowedMods.Any()) { - footerButtons.Insert(0, new UserModSelectButton + footerButtons.Insert(-1, new UserModSelectButton { Text = "Free mods", Anchor = Anchor.Centre, From 6fb0cabf360f7d0347a2efb758b2d17092ad5f97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 31 May 2024 11:49:56 +0200 Subject: [PATCH 1051/2493] Add start date to `Room` --- osu.Game/Online/Rooms/Room.cs | 5 +++++ osu.Game/Screens/OnlinePlay/OnlinePlayComposite.cs | 3 +++ 2 files changed, 8 insertions(+) diff --git a/osu.Game/Online/Rooms/Room.cs b/osu.Game/Online/Rooms/Room.cs index 5abf5034d9..c39932c3bf 100644 --- a/osu.Game/Online/Rooms/Room.cs +++ b/osu.Game/Online/Rooms/Room.cs @@ -146,6 +146,11 @@ namespace osu.Game.Online.Rooms #endregion + // Only supports retrieval for now + [Cached] + [JsonProperty("starts_at")] + public readonly Bindable StartDate = new Bindable(); + // Only supports retrieval for now [Cached] [JsonProperty("ends_at")] diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlayComposite.cs b/osu.Game/Screens/OnlinePlay/OnlinePlayComposite.cs index ff536a65c4..5be5c4b4f4 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlayComposite.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlayComposite.cs @@ -68,6 +68,9 @@ namespace osu.Game.Screens.OnlinePlay [Resolved(typeof(Room))] public Bindable UserScore { get; private set; } + [Resolved(typeof(Room))] + protected Bindable StartDate { get; private set; } + [Resolved(typeof(Room))] protected Bindable EndDate { get; private set; } From 2be6b29f21ba571d9d7783f714eadddaa584ba42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 31 May 2024 11:50:04 +0200 Subject: [PATCH 1052/2493] Implement time remaining display for daily challenge screen --- ...estSceneDailyChallengeTimeRemainingRing.cs | 86 +++++++++++ .../DailyChallenge/DailyChallenge.cs | 10 +- .../DailyChallengeTimeRemainingRing.cs | 136 ++++++++++++++++++ 3 files changed, 229 insertions(+), 3 deletions(-) create mode 100644 osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeTimeRemainingRing.cs create mode 100644 osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeTimeRemainingRing.cs diff --git a/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeTimeRemainingRing.cs b/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeTimeRemainingRing.cs new file mode 100644 index 0000000000..9e21214c11 --- /dev/null +++ b/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeTimeRemainingRing.cs @@ -0,0 +1,86 @@ +// 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 NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Extensions.ObjectExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Shapes; +using osu.Game.Online.Rooms; +using osu.Game.Overlays; +using osu.Game.Screens.OnlinePlay.DailyChallenge; + +namespace osu.Game.Tests.Visual.DailyChallenge +{ + public partial class TestSceneDailyChallengeTimeRemainingRing : OsuTestScene + { + private readonly Bindable room = new Bindable(new Room()); + + protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) => new CachedModelDependencyContainer(base.CreateChildDependencies(parent)) + { + Model = { BindTarget = room } + }; + + [Cached] + private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Plum); + + [Test] + public void TestBasicAppearance() + { + DailyChallengeTimeRemainingRing ring = null!; + + AddStep("create content", () => Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background4, + }, + ring = new DailyChallengeTimeRemainingRing + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + } + }); + AddSliderStep("adjust width", 0.1f, 1, 1, width => + { + if (ring.IsNotNull()) + ring.Width = width; + }); + AddSliderStep("adjust height", 0.1f, 1, 1, height => + { + if (ring.IsNotNull()) + ring.Height = height; + }); + AddStep("just started", () => + { + room.Value.StartDate.Value = DateTimeOffset.Now.AddMinutes(-1); + room.Value.EndDate.Value = room.Value.StartDate.Value.Value.AddDays(1); + }); + AddStep("midway through", () => + { + room.Value.StartDate.Value = DateTimeOffset.Now.AddHours(-12); + room.Value.EndDate.Value = room.Value.StartDate.Value.Value.AddDays(1); + }); + AddStep("nearing end", () => + { + room.Value.StartDate.Value = DateTimeOffset.Now.AddDays(-1).AddMinutes(8); + room.Value.EndDate.Value = room.Value.StartDate.Value.Value.AddDays(1); + }); + AddStep("already ended", () => + { + room.Value.StartDate.Value = DateTimeOffset.Now.AddDays(-2); + room.Value.EndDate.Value = room.Value.StartDate.Value.Value.AddDays(1); + }); + AddSliderStep("manual progress", 0f, 1f, 0f, progress => + { + var startedTimeAgo = TimeSpan.FromHours(24) * progress; + room.Value.StartDate.Value = DateTimeOffset.Now - startedTimeAgo; + room.Value.EndDate.Value = room.Value.StartDate.Value.Value.AddDays(1); + }); + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs index 5740825ca2..e2927617f8 100644 --- a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs @@ -16,6 +16,7 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Screens; using osu.Game.Beatmaps; using osu.Game.Graphics.Containers; +using osu.Game.Graphics.UserInterface; using osu.Game.Localisation; using osu.Game.Online.Rooms; using osu.Game.Overlays; @@ -161,7 +162,10 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge { new Drawable?[] { - null, + new DailyChallengeTimeRemainingRing + { + RelativeSizeAxes = Axes.Both, + }, null, // Middle column (leaderboard) new GridContainer @@ -171,7 +175,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge { new Drawable[] { - new OverlinedHeader("Leaderboard") + new SectionHeader("Leaderboard") }, [leaderboard = new MatchLeaderboard { RelativeSizeAxes = Axes.Both }], }, @@ -191,7 +195,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge { new Drawable[] { - new OverlinedHeader("Chat") + new SectionHeader("Chat") }, [new MatchChatDisplay(room) { RelativeSizeAxes = Axes.Both }] }, diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeTimeRemainingRing.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeTimeRemainingRing.cs new file mode 100644 index 0000000000..ccd073331e --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeTimeRemainingRing.cs @@ -0,0 +1,136 @@ +// 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.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.UserInterface; +using osu.Framework.Threading; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Overlays; +using osuTK; + +namespace osu.Game.Screens.OnlinePlay.DailyChallenge +{ + public partial class DailyChallengeTimeRemainingRing : OnlinePlayComposite + { + private CircularProgress progress = null!; + private OsuSpriteText timeText = null!; + + [Resolved] + private OverlayColourProvider colourProvider { get; set; } = null!; + + [Resolved] + private OsuColour colours { get; set; } = null!; + + [BackgroundDependencyLoader] + private void load() + { + InternalChild = new DrawSizePreservingFillContainer + { + RelativeSizeAxes = Axes.Both, + TargetDrawSize = new Vector2(200), + Strategy = DrawSizePreservationStrategy.Minimum, + Children = new Drawable[] + { + new CircularProgress + { + Size = new Vector2(180), + InnerRadius = 0.1f, + Progress = 1, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Colour = colourProvider.Background5, + }, + progress = new CircularProgress + { + Size = new Vector2(180), + InnerRadius = 0.1f, + Progress = 1, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }, + new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Direction = FillDirection.Vertical, + Children = new[] + { + timeText = new OsuSpriteText + { + Text = "00:00:00", + Font = OsuFont.TorusAlternate.With(size: 40), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }, + new OsuSpriteText + { + Text = "remaining", + Font = OsuFont.Default.With(size: 20), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + } + } + } + } + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + StartDate.BindValueChanged(_ => Scheduler.AddOnce(updateState)); + EndDate.BindValueChanged(_ => Scheduler.AddOnce(updateState)); + updateState(); + FinishTransforms(true); + } + + private ScheduledDelegate? scheduledUpdate; + + private void updateState() + { + scheduledUpdate?.Cancel(); + scheduledUpdate = null; + + const float transition_duration = 300; + + if (StartDate.Value == null || EndDate.Value == null || EndDate.Value < DateTimeOffset.Now) + { + timeText.Text = TimeSpan.Zero.ToString(@"hh\:mm\:ss"); + progress.Progress = 0; + timeText.FadeColour(colours.Red2, transition_duration, Easing.OutQuint); + progress.FadeColour(colours.Red2, transition_duration, Easing.OutQuint); + return; + } + + var roomDuration = EndDate.Value.Value - StartDate.Value.Value; + var remaining = EndDate.Value.Value - DateTimeOffset.Now; + + timeText.Text = remaining.ToString(@"hh\:mm\:ss"); + progress.Progress = remaining.TotalSeconds / roomDuration.TotalSeconds; + + if (remaining < TimeSpan.FromMinutes(15)) + { + timeText.Colour = progress.Colour = colours.Red1; + timeText + .FadeColour(colours.Red1) + .Then().FlashColour(colours.Red0, transition_duration, Easing.OutQuint); + progress + .FadeColour(colours.Red1) + .Then().FlashColour(colours.Red0, transition_duration, Easing.OutQuint); + } + else + { + timeText.FadeColour(Colour4.White, transition_duration, Easing.OutQuint); + progress.FadeColour(colourProvider.Highlight1, transition_duration, Easing.OutQuint); + } + + scheduledUpdate = Scheduler.AddDelayed(updateState, 1000); + } + } +} From 51c598627ab24694336d6ff97c8bdcef831e8709 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 4 Jun 2024 13:53:42 +0200 Subject: [PATCH 1053/2493] Move out section header component from editor This sort of thing has been showing up on flyte designs more and more so I want to start using it more over that rather ugly "overlined" text that's there on multiplayer screens right now. --- .../Graphics/UserInterface/SectionHeader.cs | 55 +++++++++++++++++++ .../Edit/Components/EditorSidebarSection.cs | 48 +--------------- 2 files changed, 56 insertions(+), 47 deletions(-) create mode 100644 osu.Game/Graphics/UserInterface/SectionHeader.cs diff --git a/osu.Game/Graphics/UserInterface/SectionHeader.cs b/osu.Game/Graphics/UserInterface/SectionHeader.cs new file mode 100644 index 0000000000..0ee430c501 --- /dev/null +++ b/osu.Game/Graphics/UserInterface/SectionHeader.cs @@ -0,0 +1,55 @@ +// 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.Shapes; +using osu.Framework.Localisation; +using osu.Game.Graphics.Containers; +using osu.Game.Overlays; +using osuTK; + +namespace osu.Game.Graphics.UserInterface +{ + public partial class SectionHeader : CompositeDrawable + { + private readonly LocalisableString text; + + public SectionHeader(LocalisableString text) + { + this.text = text; + + Margin = new MarginPadding { Vertical = 10, Horizontal = 5 }; + + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + } + + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider) + { + InternalChild = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(2), + Children = new Drawable[] + { + new OsuTextFlowContainer(cp => cp.Font = OsuFont.Default.With(size: 16, weight: FontWeight.SemiBold)) + { + Text = text, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + }, + new Circle + { + Colour = colourProvider.Highlight1, + Size = new Vector2(28, 2), + } + } + }; + } + } +} diff --git a/osu.Game/Screens/Edit/Components/EditorSidebarSection.cs b/osu.Game/Screens/Edit/Components/EditorSidebarSection.cs index 279793c0a1..5dd8f78f6d 100644 --- a/osu.Game/Screens/Edit/Components/EditorSidebarSection.cs +++ b/osu.Game/Screens/Edit/Components/EditorSidebarSection.cs @@ -1,15 +1,10 @@ // 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.Shapes; using osu.Framework.Localisation; -using osu.Game.Graphics; -using osu.Game.Graphics.Containers; -using osu.Game.Overlays; -using osuTK; +using osu.Game.Graphics.UserInterface; namespace osu.Game.Screens.Edit.Components { @@ -38,46 +33,5 @@ namespace osu.Game.Screens.Edit.Components } }; } - - public partial class SectionHeader : CompositeDrawable - { - private readonly LocalisableString text; - - public SectionHeader(LocalisableString text) - { - this.text = text; - - Margin = new MarginPadding { Vertical = 10, Horizontal = 5 }; - - RelativeSizeAxes = Axes.X; - AutoSizeAxes = Axes.Y; - } - - [BackgroundDependencyLoader] - private void load(OverlayColourProvider colourProvider) - { - InternalChild = new FillFlowContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Direction = FillDirection.Vertical, - Spacing = new Vector2(2), - Children = new Drawable[] - { - new OsuTextFlowContainer(cp => cp.Font = OsuFont.Default.With(size: 16, weight: FontWeight.SemiBold)) - { - Text = text, - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - }, - new Circle - { - Colour = colourProvider.Highlight1, - Size = new Vector2(28, 2), - } - } - }; - } - } } } From ae6dd9d053fca849644e1bb63c7644a6d7c9b5ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 4 Jun 2024 14:04:33 +0200 Subject: [PATCH 1054/2493] Use extracted headings on daily challenge screen --- .../DailyChallengeTimeRemainingRing.cs | 86 ++++++++++--------- 1 file changed, 46 insertions(+), 40 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeTimeRemainingRing.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeTimeRemainingRing.cs index ccd073331e..e86f26ad6b 100644 --- a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeTimeRemainingRing.cs +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeTimeRemainingRing.cs @@ -9,6 +9,7 @@ using osu.Framework.Graphics.UserInterface; using osu.Framework.Threading; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; using osu.Game.Overlays; using osuTK; @@ -28,51 +29,56 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge [BackgroundDependencyLoader] private void load() { - InternalChild = new DrawSizePreservingFillContainer + InternalChildren = new Drawable[] { - RelativeSizeAxes = Axes.Both, - TargetDrawSize = new Vector2(200), - Strategy = DrawSizePreservationStrategy.Minimum, - Children = new Drawable[] + new SectionHeader("Time remaining"), + new DrawSizePreservingFillContainer { - new CircularProgress + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Top = 35 }, + TargetDrawSize = new Vector2(200), + Strategy = DrawSizePreservationStrategy.Minimum, + Children = new Drawable[] { - Size = new Vector2(180), - InnerRadius = 0.1f, - Progress = 1, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Colour = colourProvider.Background5, - }, - progress = new CircularProgress - { - Size = new Vector2(180), - InnerRadius = 0.1f, - Progress = 1, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - }, - new FillFlowContainer - { - AutoSizeAxes = Axes.Both, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Direction = FillDirection.Vertical, - Children = new[] + new CircularProgress { - timeText = new OsuSpriteText + Size = new Vector2(180), + InnerRadius = 0.1f, + Progress = 1, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Colour = colourProvider.Background5, + }, + progress = new CircularProgress + { + Size = new Vector2(180), + InnerRadius = 0.1f, + Progress = 1, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }, + new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Direction = FillDirection.Vertical, + Children = new[] { - Text = "00:00:00", - Font = OsuFont.TorusAlternate.With(size: 40), - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - }, - new OsuSpriteText - { - Text = "remaining", - Font = OsuFont.Default.With(size: 20), - Anchor = Anchor.Centre, - Origin = Anchor.Centre, + timeText = new OsuSpriteText + { + Text = "00:00:00", + Font = OsuFont.TorusAlternate.With(size: 40), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }, + new OsuSpriteText + { + Text = "remaining", + Font = OsuFont.Default.With(size: 20), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + } } } } From feadf7a56e0ace3f727e43212312ea0d47c022ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 12 Jun 2024 15:28:14 +0200 Subject: [PATCH 1055/2493] Allow modifying hold note start/end time via mania composer playfield --- .../Editor/TestSceneManiaHitObjectComposer.cs | 4 +- .../Components/EditHoldNoteEndPiece.cs | 81 +++++++++++++++++++ .../Blueprints/HoldNoteSelectionBlueprint.cs | 58 +++++++++++-- 3 files changed, 134 insertions(+), 9 deletions(-) create mode 100644 osu.Game.Rulesets.Mania/Edit/Blueprints/Components/EditHoldNoteEndPiece.cs diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaHitObjectComposer.cs b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaHitObjectComposer.cs index 8e0b51dcf8..cb0fc72a34 100644 --- a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaHitObjectComposer.cs +++ b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaHitObjectComposer.cs @@ -186,8 +186,8 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor AddAssert("head note positioned correctly", () => Precision.AlmostEquals(holdNote.ScreenSpaceDrawQuad.BottomLeft, holdNote.Head.ScreenSpaceDrawQuad.BottomLeft)); AddAssert("tail note positioned correctly", () => Precision.AlmostEquals(holdNote.ScreenSpaceDrawQuad.TopLeft, holdNote.Tail.ScreenSpaceDrawQuad.BottomLeft)); - AddAssert("head blueprint positioned correctly", () => this.ChildrenOfType().ElementAt(0).DrawPosition == holdNote.Head.DrawPosition); - AddAssert("tail blueprint positioned correctly", () => this.ChildrenOfType().ElementAt(1).DrawPosition == holdNote.Tail.DrawPosition); + AddAssert("head blueprint positioned correctly", () => this.ChildrenOfType().ElementAt(0).DrawPosition == holdNote.Head.DrawPosition); + AddAssert("tail blueprint positioned correctly", () => this.ChildrenOfType().ElementAt(1).DrawPosition == holdNote.Tail.DrawPosition); } private void setScrollStep(ScrollingDirection direction) diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/Components/EditHoldNoteEndPiece.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/Components/EditHoldNoteEndPiece.cs new file mode 100644 index 0000000000..0aa72c28b8 --- /dev/null +++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/Components/EditHoldNoteEndPiece.cs @@ -0,0 +1,81 @@ +// 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.Extensions.Color4Extensions; +using osu.Framework.Graphics.Containers; +using osu.Framework.Input.Events; +using osu.Game.Graphics; +using osu.Game.Rulesets.Mania.Skinning.Default; +using osuTK; + +namespace osu.Game.Rulesets.Mania.Edit.Blueprints.Components +{ + public partial class EditHoldNoteEndPiece : CompositeDrawable + { + public Action? DragStarted { get; init; } + public Action? Dragging { get; init; } + public Action? DragEnded { get; init; } + + [Resolved] + private OsuColour colours { get; set; } = null!; + + [BackgroundDependencyLoader] + private void load() + { + Height = DefaultNotePiece.NOTE_HEIGHT; + + CornerRadius = 5; + Masking = true; + + InternalChild = new DefaultNotePiece(); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + updateState(); + } + + protected override bool OnHover(HoverEvent e) + { + updateState(); + return true; + } + + protected override void OnHoverLost(HoverLostEvent e) + { + updateState(); + base.OnHoverLost(e); + } + + protected override bool OnDragStart(DragStartEvent e) + { + DragStarted?.Invoke(); + return true; + } + + protected override void OnDrag(DragEvent e) + { + base.OnDrag(e); + Dragging?.Invoke(e.ScreenSpaceMousePosition); + } + + protected override void OnDragEnd(DragEndEvent e) + { + base.OnDragEnd(e); + DragEnded?.Invoke(); + } + + private void updateState() + { + var colour = colours.Yellow; + + if (IsHovered) + colour = colour.Lighten(1); + + Colour = colour; + } + } +} diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNoteSelectionBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNoteSelectionBlueprint.cs index 8ec5213d5f..b8e6aa26a0 100644 --- a/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNoteSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNoteSelectionBlueprint.cs @@ -1,16 +1,16 @@ // 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; using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.Shapes; using osu.Game.Graphics; +using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Mania.Edit.Blueprints.Components; using osu.Game.Rulesets.Mania.Objects; +using osu.Game.Screens.Edit; using osuTK; namespace osu.Game.Rulesets.Mania.Edit.Blueprints @@ -18,10 +18,19 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints public partial class HoldNoteSelectionBlueprint : ManiaSelectionBlueprint { [Resolved] - private OsuColour colours { get; set; } + private OsuColour colours { get; set; } = null!; - private EditNotePiece head; - private EditNotePiece tail; + [Resolved] + private IEditorChangeHandler? changeHandler { get; set; } + + [Resolved] + private EditorBeatmap? editorBeatmap { get; set; } + + [Resolved] + private IPositionSnapProvider? positionSnapProvider { get; set; } + + private EditHoldNoteEndPiece head = null!; + private EditHoldNoteEndPiece tail = null!; public HoldNoteSelectionBlueprint(HoldNote hold) : base(hold) @@ -33,8 +42,43 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints { InternalChildren = new Drawable[] { - head = new EditNotePiece { RelativeSizeAxes = Axes.X }, - tail = new EditNotePiece { RelativeSizeAxes = Axes.X }, + head = new EditHoldNoteEndPiece + { + RelativeSizeAxes = Axes.X, + DragStarted = () => changeHandler?.BeginChange(), + Dragging = pos => + { + double endTimeBeforeDrag = HitObject.EndTime; + double proposedStartTime = positionSnapProvider?.FindSnappedPositionAndTime(pos).Time ?? HitObjectContainer.TimeAtScreenSpacePosition(pos); + double proposedEndTime = endTimeBeforeDrag; + + if (proposedStartTime >= proposedEndTime) + return; + + HitObject.StartTime = proposedStartTime; + HitObject.EndTime = proposedEndTime; + editorBeatmap?.Update(HitObject); + }, + DragEnded = () => changeHandler?.EndChange(), + }, + tail = new EditHoldNoteEndPiece + { + RelativeSizeAxes = Axes.X, + DragStarted = () => changeHandler?.BeginChange(), + Dragging = pos => + { + double proposedStartTime = HitObject.StartTime; + double proposedEndTime = positionSnapProvider?.FindSnappedPositionAndTime(pos).Time ?? HitObjectContainer.TimeAtScreenSpacePosition(pos); + + if (proposedStartTime >= proposedEndTime) + return; + + HitObject.StartTime = proposedStartTime; + HitObject.EndTime = proposedEndTime; + editorBeatmap?.Update(HitObject); + }, + DragEnded = () => changeHandler?.EndChange(), + }, new Container { RelativeSizeAxes = Axes.Both, From 6d2f9108132b45c07fcdb48aa7bc1a37f879c32b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 12 Jun 2024 15:43:33 +0200 Subject: [PATCH 1056/2493] Add test coverage --- .../Editor/TestSceneManiaHitObjectComposer.cs | 98 +++++++++++++++++++ 1 file changed, 98 insertions(+) diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaHitObjectComposer.cs b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaHitObjectComposer.cs index cb0fc72a34..d88f488582 100644 --- a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaHitObjectComposer.cs +++ b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaHitObjectComposer.cs @@ -190,6 +190,104 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor AddAssert("tail blueprint positioned correctly", () => this.ChildrenOfType().ElementAt(1).DrawPosition == holdNote.Tail.DrawPosition); } + [Test] + public void TestDragHoldNoteHead() + { + setScrollStep(ScrollingDirection.Down); + + HoldNote holdNote = null; + AddStep("setup beatmap", () => + { + composer.EditorBeatmap.Clear(); + composer.EditorBeatmap.Add(holdNote = new HoldNote + { + Column = 1, + StartTime = 250, + EndTime = 750, + }); + }); + + DrawableHoldNote drawableHoldNote = null; + EditHoldNoteEndPiece headPiece = null; + + AddStep("select blueprint", () => + { + drawableHoldNote = this.ChildrenOfType().Single(); + InputManager.MoveMouseTo(drawableHoldNote); + InputManager.Click(MouseButton.Left); + }); + AddStep("grab hold note head", () => + { + headPiece = this.ChildrenOfType().First(); + InputManager.MoveMouseTo(headPiece); + InputManager.PressButton(MouseButton.Left); + }); + + AddStep("drag head downwards", () => + { + InputManager.MoveMouseTo(headPiece, new Vector2(0, 100)); + InputManager.ReleaseButton(MouseButton.Left); + }); + + AddAssert("start time moved back", () => holdNote!.StartTime, () => Is.LessThan(250)); + AddAssert("end time unchanged", () => holdNote.EndTime, () => Is.EqualTo(750)); + + AddAssert("head note positioned correctly", () => Precision.AlmostEquals(drawableHoldNote.ScreenSpaceDrawQuad.BottomLeft, drawableHoldNote.Head.ScreenSpaceDrawQuad.BottomLeft)); + AddAssert("tail note positioned correctly", () => Precision.AlmostEquals(drawableHoldNote.ScreenSpaceDrawQuad.TopLeft, drawableHoldNote.Tail.ScreenSpaceDrawQuad.BottomLeft)); + + AddAssert("head blueprint positioned correctly", () => this.ChildrenOfType().ElementAt(0).DrawPosition == drawableHoldNote.Head.DrawPosition); + AddAssert("tail blueprint positioned correctly", () => this.ChildrenOfType().ElementAt(1).DrawPosition == drawableHoldNote.Tail.DrawPosition); + } + + [Test] + public void TestDragHoldNoteTail() + { + setScrollStep(ScrollingDirection.Down); + + HoldNote holdNote = null; + AddStep("setup beatmap", () => + { + composer.EditorBeatmap.Clear(); + composer.EditorBeatmap.Add(holdNote = new HoldNote + { + Column = 1, + StartTime = 250, + EndTime = 750, + }); + }); + + DrawableHoldNote drawableHoldNote = null; + EditHoldNoteEndPiece tailPiece = null; + + AddStep("select blueprint", () => + { + drawableHoldNote = this.ChildrenOfType().Single(); + InputManager.MoveMouseTo(drawableHoldNote); + InputManager.Click(MouseButton.Left); + }); + AddStep("grab hold note tail", () => + { + tailPiece = this.ChildrenOfType().Last(); + InputManager.MoveMouseTo(tailPiece); + InputManager.PressButton(MouseButton.Left); + }); + + AddStep("drag tail upwards", () => + { + InputManager.MoveMouseTo(tailPiece, new Vector2(0, -100)); + InputManager.ReleaseButton(MouseButton.Left); + }); + + AddAssert("start time unchanged", () => holdNote!.StartTime, () => Is.EqualTo(250)); + AddAssert("end time moved forward", () => holdNote.EndTime, () => Is.GreaterThan(750)); + + AddAssert("head note positioned correctly", () => Precision.AlmostEquals(drawableHoldNote.ScreenSpaceDrawQuad.BottomLeft, drawableHoldNote.Head.ScreenSpaceDrawQuad.BottomLeft)); + AddAssert("tail note positioned correctly", () => Precision.AlmostEquals(drawableHoldNote.ScreenSpaceDrawQuad.TopLeft, drawableHoldNote.Tail.ScreenSpaceDrawQuad.BottomLeft)); + + AddAssert("head blueprint positioned correctly", () => this.ChildrenOfType().ElementAt(0).DrawPosition == drawableHoldNote.Head.DrawPosition); + AddAssert("tail blueprint positioned correctly", () => this.ChildrenOfType().ElementAt(1).DrawPosition == drawableHoldNote.Tail.DrawPosition); + } + private void setScrollStep(ScrollingDirection direction) => AddStep($"set scroll direction = {direction}", () => ((Bindable)composer.Composer.ScrollingInfo.Direction).Value = direction); From 91f2cf8cc30e97acc8ee1a198f40e87766e1c451 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 13 Jun 2024 15:18:39 +0900 Subject: [PATCH 1057/2493] Use more descriptive HitObject names for debugger displays --- osu.Game/Rulesets/Objects/HitObject.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Rulesets/Objects/HitObject.cs b/osu.Game/Rulesets/Objects/HitObject.cs index 04bdc35941..f71fa95aa7 100644 --- a/osu.Game/Rulesets/Objects/HitObject.cs +++ b/osu.Game/Rulesets/Objects/HitObject.cs @@ -12,6 +12,7 @@ using JetBrains.Annotations; using Newtonsoft.Json; using osu.Framework.Bindables; using osu.Framework.Extensions.ListExtensions; +using osu.Framework.Extensions.TypeExtensions; using osu.Framework.Lists; using osu.Game.Audio; using osu.Game.Beatmaps; @@ -228,6 +229,8 @@ namespace osu.Game.Rulesets.Objects return new HitSampleInfo(sampleName); } + + public override string ToString() => $"{GetType().ReadableName()} @ {StartTime}"; } public static class HitObjectExtensions From 1ff20cc13d76e7681767e248ca0437c64f9703b3 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 13 Jun 2024 15:19:38 +0900 Subject: [PATCH 1058/2493] Fix missing texture on extremely long hold notes --- osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyBodyPiece.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyBodyPiece.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyBodyPiece.cs index 00054f6be2..1cba5b8cb3 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyBodyPiece.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyBodyPiece.cs @@ -245,7 +245,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy // i dunno this looks about right?? // the guard against zero draw height is intended for zero-length hold notes. yes, such cases have been spotted in the wild. if (sprite.DrawHeight > 0) - bodySprite.Scale = new Vector2(1, scaleDirection * 32800 / sprite.DrawHeight); + bodySprite.Scale = new Vector2(1, MathF.Max(1, scaleDirection * 32800 / sprite.DrawHeight)); } break; From 8c4aa84037fc420c6179cb65d778e01268445c5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 31 May 2024 13:20:21 +0200 Subject: [PATCH 1059/2493] Implement event feed view for daily challenge screen --- .../TestSceneDailyChallengeEventFeed.cs | 76 ++++++++++ .../DailyChallenge/DailyChallengeEventFeed.cs | 136 ++++++++++++++++++ 2 files changed, 212 insertions(+) create mode 100644 osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeEventFeed.cs create mode 100644 osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeEventFeed.cs diff --git a/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeEventFeed.cs b/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeEventFeed.cs new file mode 100644 index 0000000000..85499f0588 --- /dev/null +++ b/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeEventFeed.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 NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Extensions.ObjectExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Utils; +using osu.Game.Overlays; +using osu.Game.Screens.OnlinePlay.DailyChallenge; +using osu.Game.Tests.Resources; + +namespace osu.Game.Tests.Visual.DailyChallenge +{ + public partial class TestSceneDailyChallengeEventFeed : OsuTestScene + { + [Cached] + private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Plum); + + [Test] + public void TestBasicAppearance() + { + DailyChallengeEventFeed feed = null!; + + AddStep("create content", () => Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background4, + }, + feed = new DailyChallengeEventFeed + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + } + }); + AddSliderStep("adjust width", 0.1f, 1, 1, width => + { + if (feed.IsNotNull()) + feed.Width = width; + }); + AddSliderStep("adjust height", 0.1f, 1, 1, height => + { + if (feed.IsNotNull()) + feed.Height = height; + }); + + AddStep("add normal score", () => + { + var testScore = TestResources.CreateTestScoreInfo(); + testScore.TotalScore = RNG.Next(1_000_000); + + feed.AddNewScore(new DailyChallengeEventFeed.NewScoreEvent(testScore, null)); + }); + + AddStep("add new user best", () => + { + var testScore = TestResources.CreateTestScoreInfo(); + testScore.TotalScore = RNG.Next(1_000_000); + + feed.AddNewScore(new DailyChallengeEventFeed.NewScoreEvent(testScore, RNG.Next(1, 1000))); + }); + + AddStep("add top 10 score", () => + { + var testScore = TestResources.CreateTestScoreInfo(); + testScore.TotalScore = RNG.Next(1_000_000); + + feed.AddNewScore(new DailyChallengeEventFeed.NewScoreEvent(testScore, RNG.Next(1, 10))); + }); + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeEventFeed.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeEventFeed.cs new file mode 100644 index 0000000000..10f4f2cf78 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeEventFeed.cs @@ -0,0 +1,136 @@ +// 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.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.UserInterface; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Scoring; +using osu.Game.Users.Drawables; +using osuTK; + +namespace osu.Game.Screens.OnlinePlay.DailyChallenge +{ + public partial class DailyChallengeEventFeed : CompositeDrawable + { + private DailyChallengeEventFeedFlow flow = null!; + + [BackgroundDependencyLoader] + private void load() + { + InternalChildren = new Drawable[] + { + new SectionHeader("Events"), + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Top = 35 }, + Child = flow = new DailyChallengeEventFeedFlow + { + RelativeSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Origin = Anchor.BottomCentre, + Anchor = Anchor.BottomCentre, + Spacing = new Vector2(5), + Masking = true, + } + } + }; + } + + public void AddNewScore(NewScoreEvent newScoreEvent) + { + var row = new NewScoreEventRow(newScoreEvent) + { + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + }; + flow.Add(row); + row.Delay(15000).Then().FadeOut(300, Easing.OutQuint).Expire(); + } + + protected override void Update() + { + base.Update(); + + for (int i = 0; i < flow.Count; ++i) + { + var row = flow[i]; + + if (row.Y < -flow.DrawHeight) + { + row.RemoveAndDisposeImmediately(); + i -= 1; + } + } + } + + public record NewScoreEvent( + IScoreInfo Score, + int? NewRank); + + private partial class DailyChallengeEventFeedFlow : FillFlowContainer + { + public override IEnumerable FlowingChildren => base.FlowingChildren.Reverse(); + } + + private partial class NewScoreEventRow : CompositeDrawable + { + public Action? PresentScore { get; set; } + + private readonly NewScoreEvent newScore; + + public NewScoreEventRow(NewScoreEvent newScore) + { + this.newScore = newScore; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + LinkFlowContainer text; + + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + AutoSizeDuration = 300; + AutoSizeEasing = Easing.OutQuint; + + InternalChildren = new Drawable[] + { + // TODO: cast is temporary, will be removed later + new ClickableAvatar((APIUser)newScore.Score.User) + { + Size = new Vector2(16), + Masking = true, + CornerRadius = 8, + }, + text = new LinkFlowContainer(t => + { + t.Font = OsuFont.Default.With(weight: newScore.NewRank == null ? FontWeight.Medium : FontWeight.Bold); + t.Colour = newScore.NewRank < 10 ? colours.Orange1 : Colour4.White; + }) + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding { Left = 21 }, + } + }; + + text.AddUserLink(newScore.Score.User); + text.AddText(" got "); + text.AddLink($"{newScore.Score.TotalScore:N0} points", () => PresentScore?.Invoke(newScore.Score)); + + if (newScore.NewRank != null) + text.AddText($" and achieved rank #{newScore.NewRank.Value:N0}"); + + text.AddText("!"); + } + } + } +} From 253b7b046b663cee98177ac15c60e56029ab252b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 13 Jun 2024 15:03:38 +0200 Subject: [PATCH 1060/2493] Add test scene for taiko relax mod --- .../Mods/TestSceneTaikoModRelax.cs | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 osu.Game.Rulesets.Taiko.Tests/Mods/TestSceneTaikoModRelax.cs diff --git a/osu.Game.Rulesets.Taiko.Tests/Mods/TestSceneTaikoModRelax.cs b/osu.Game.Rulesets.Taiko.Tests/Mods/TestSceneTaikoModRelax.cs new file mode 100644 index 0000000000..caf8aa8e76 --- /dev/null +++ b/osu.Game.Rulesets.Taiko.Tests/Mods/TestSceneTaikoModRelax.cs @@ -0,0 +1,46 @@ +// 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.Rulesets.Taiko.Beatmaps; +using osu.Game.Rulesets.Taiko.Mods; +using osu.Game.Rulesets.Taiko.Objects; +using osu.Game.Rulesets.Taiko.Replays; + +namespace osu.Game.Rulesets.Taiko.Tests.Mods +{ + public partial class TestSceneTaikoModRelax : TaikoModTestScene + { + [Test] + public void TestRelax() + { + var beatmap = new TaikoBeatmap + { + HitObjects = + { + new Hit { StartTime = 0, Type = HitType.Centre, }, + new Hit { StartTime = 250, Type = HitType.Rim, }, + new DrumRoll { StartTime = 500, Duration = 500, }, + new Swell { StartTime = 1250, Duration = 500 }, + } + }; + foreach (var ho in beatmap.HitObjects) + ho.ApplyDefaults(beatmap.ControlPointInfo, beatmap.Difficulty); + + var replay = new TaikoAutoGenerator(beatmap).Generate(); + + foreach (var frame in replay.Frames.OfType().Where(r => r.Actions.Any())) + frame.Actions = [TaikoAction.LeftCentre]; + + CreateModTest(new ModTestData + { + Mod = new TaikoModRelax(), + Beatmap = beatmap, + ReplayFrames = replay.Frames, + Autoplay = false, + PassCondition = () => Player.ScoreProcessor.HasCompleted.Value && Player.ScoreProcessor.Accuracy.Value == 1, + }); + } + } +} From 173f1958343efe9d4ccdfea256b951512ef3b7c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 13 Jun 2024 15:06:31 +0200 Subject: [PATCH 1061/2493] Add precautionary test coverage for alternating still being required by default for swells --- .../Judgements/TestSceneSwellJudgements.cs | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/osu.Game.Rulesets.Taiko.Tests/Judgements/TestSceneSwellJudgements.cs b/osu.Game.Rulesets.Taiko.Tests/Judgements/TestSceneSwellJudgements.cs index 6e42ae7eb5..04661fe2cf 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Judgements/TestSceneSwellJudgements.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Judgements/TestSceneSwellJudgements.cs @@ -85,6 +85,42 @@ namespace osu.Game.Rulesets.Taiko.Tests.Judgements AssertResult(0, HitResult.IgnoreMiss); } + [Test] + public void TestAlternatingIsRequired() + { + const double hit_time = 1000; + + Swell swell = new Swell + { + StartTime = hit_time, + Duration = 1000, + RequiredHits = 10 + }; + + List frames = new List + { + new TaikoReplayFrame(0), + new TaikoReplayFrame(2001), + }; + + for (int i = 0; i < swell.RequiredHits; i++) + { + double frameTime = 1000 + i * 50; + frames.Add(new TaikoReplayFrame(frameTime, TaikoAction.LeftCentre)); + frames.Add(new TaikoReplayFrame(frameTime + 10)); + } + + PerformTest(frames, CreateBeatmap(swell)); + + AssertJudgementCount(11); + + AssertResult(0, HitResult.IgnoreHit); + for (int i = 1; i < swell.RequiredHits; i++) + AssertResult(i, HitResult.IgnoreMiss); + + AssertResult(0, HitResult.IgnoreMiss); + } + [Test] public void TestHitNoneSwell() { From d7997cc93c42c83a42bf01b4140154bcff21900f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 13 Jun 2024 14:49:56 +0200 Subject: [PATCH 1062/2493] Implement taiko relax mod --- osu.Game.Rulesets.Taiko/Mods/TaikoModRelax.cs | 25 +++++++++++++++++-- .../Objects/Drawables/DrawableHit.cs | 2 +- .../Objects/Drawables/DrawableSwell.cs | 7 +++++- 3 files changed, 30 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModRelax.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModRelax.cs index e90ab589fc..ed09a85ebb 100644 --- a/osu.Game.Rulesets.Taiko/Mods/TaikoModRelax.cs +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModRelax.cs @@ -5,13 +5,34 @@ using System; using System.Linq; using osu.Framework.Localisation; using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Taiko.Objects.Drawables; namespace osu.Game.Rulesets.Taiko.Mods { - public class TaikoModRelax : ModRelax + public class TaikoModRelax : ModRelax, IApplicableToDrawableHitObject { - public override LocalisableString Description => @"No ninja-like spinners, demanding drumrolls or unexpected katus."; + public override LocalisableString Description => @"No need to remember which key is correct anymore!"; public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(TaikoModSingleTap) }).ToArray(); + + public void ApplyToDrawableHitObject(DrawableHitObject drawable) + { + var allActions = Enum.GetValues(); + + drawable.HitObjectApplied += dho => + { + switch (dho) + { + case DrawableHit hit: + hit.HitActions = allActions; + break; + + case DrawableSwell swell: + swell.MustAlternate = false; + break; + } + }; + } } } diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs index 4fb69056da..a5e63c373f 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs @@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables /// /// A list of keys which can result in hits for this HitObject. /// - public TaikoAction[] HitActions { get; private set; } + public TaikoAction[] HitActions { get; internal set; } /// /// The action that caused this to be hit. diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs index e1fc28fe16..f2fcd185dd 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs @@ -43,6 +43,11 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables public override bool DisplayResult => false; + /// + /// Whether the player must alternate centre and rim hits. + /// + public bool MustAlternate { get; internal set; } = true; + public DrawableSwell() : this(null) { @@ -292,7 +297,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables bool isCentre = e.Action == TaikoAction.LeftCentre || e.Action == TaikoAction.RightCentre; // Ensure alternating centre and rim hits - if (lastWasCentre == isCentre) + if (lastWasCentre == isCentre && MustAlternate) return false; // If we've already successfully judged a tick this frame, do not judge more. From 47f89b89698e0167ada9b176ed7719076749ac60 Mon Sep 17 00:00:00 2001 From: Joppe27 Date: Thu, 13 Jun 2024 18:06:19 +0200 Subject: [PATCH 1063/2493] Clamp X value to avoid excessive balance shift --- osu.Game/Screens/Ranking/ScorePanel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Ranking/ScorePanel.cs b/osu.Game/Screens/Ranking/ScorePanel.cs index e283749e32..85da1afe7b 100644 --- a/osu.Game/Screens/Ranking/ScorePanel.cs +++ b/osu.Game/Screens/Ranking/ScorePanel.cs @@ -225,7 +225,7 @@ namespace osu.Game.Screens.Ranking protected override void Update() { base.Update(); - audioContent.Balance.Value = ((ScreenSpaceDrawQuad.Centre.X / game.ScreenSpaceDrawQuad.Width) * 2 - 1) * OsuGameBase.SFX_STEREO_STRENGTH; + audioContent.Balance.Value = (Math.Clamp(ScreenSpaceDrawQuad.Centre.X / game.ScreenSpaceDrawQuad.Width, -1, 1) * 2 - 1) * OsuGameBase.SFX_STEREO_STRENGTH; } private void playAppearSample() From 8b6385f7d0e9ac575b1895b8118aeeeac13d6026 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 14 Jun 2024 09:08:25 +0200 Subject: [PATCH 1064/2493] Add failing test case demonstrating crash --- .../Editing/TestSceneTimelineSelection.cs | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneTimelineSelection.cs b/osu.Game.Tests/Visual/Editing/TestSceneTimelineSelection.cs index d8219ff36e..9e147f5ff1 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneTimelineSelection.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneTimelineSelection.cs @@ -14,6 +14,7 @@ using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.Osu.Edit; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Screens.Edit.Compose.Components.Timeline; using osu.Game.Tests.Beatmaps; @@ -357,6 +358,51 @@ namespace osu.Game.Tests.Visual.Editing AddAssert("all blueprints are present", () => blueprintContainer.SelectionBlueprints.Count == EditorBeatmap.SelectedHitObjects.Count); } + [Test] + public void TestDragSelectionDuringPlacement() + { + var addedObjects = new[] + { + new Slider + { + StartTime = 300, + Path = new SliderPath([ + new PathControlPoint(), + new PathControlPoint(new Vector2(200)), + ]) + }, + }; + AddStep("add hitobjects", () => EditorBeatmap.AddRange(addedObjects)); + + AddStep("seek to 700", () => EditorClock.Seek(700)); + AddStep("select spinner placement tool", () => + { + InputManager.Key(Key.Number4); + InputManager.MoveMouseTo(this.ChildrenOfType().Single()); + }); + AddStep("begin spinner placement", () => InputManager.Click(MouseButton.Left)); + AddStep("seek to 1500", () => EditorClock.Seek(1500)); + + AddStep("start dragging", () => + { + var blueprintQuad = blueprintContainer.SelectionBlueprints[1].ScreenSpaceDrawQuad; + var dragStartPos = (blueprintQuad.TopLeft + blueprintQuad.BottomLeft) / 2 - new Vector2(30, 0); + InputManager.MoveMouseTo(dragStartPos); + InputManager.PressButton(MouseButton.Left); + }); + + AddStep("select entire object", () => + { + var blueprintQuad = blueprintContainer.SelectionBlueprints[1].ScreenSpaceDrawQuad; + var dragStartPos = (blueprintQuad.TopRight + blueprintQuad.BottomRight) / 2 + new Vector2(30, 0); + InputManager.MoveMouseTo(dragStartPos); + }); + AddStep("end drag", () => InputManager.ReleaseButton(MouseButton.Left)); + + AddUntilStep("hitobject selected", () => EditorBeatmap.SelectedHitObjects, () => NUnit.Framework.Contains.Item(addedObjects[0])); + AddAssert("placement committed", () => EditorBeatmap.HitObjects, () => Has.Count.EqualTo(2)); + } + private void assertSelectionIs(IEnumerable hitObjects) => AddAssert("correct hitobjects selected", () => EditorBeatmap.SelectedHitObjects.OrderBy(h => h.StartTime).SequenceEqual(hitObjects)); } From bdeea37a447c305c7c538e2965469c5c059ca1a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 14 Jun 2024 09:14:07 +0200 Subject: [PATCH 1065/2493] Commit active placement when starting drag selection via timeline This was reported in https://github.com/ppy/osu/pull/28474, albeit the code changes proposed there did not fix the issue at all. See 8b6385f7d0e9ac575b1895b8118aeeeac13d6026 for demonstration of the crash scenario. Basically what is happening there is: - The starting premise is that there is a spinner placement active. - At this time, a drag selection is started via the timeline. - Once the drag selection finds at least one suitable object to select, it mutates `SelectedItems`. - When selection changes for any reason, the `HitObjectComposer` decides to switch to the "select" tool, regardless of why the selection changed. - Changing the active tool causes the current placement - if any - to be committed, which mutates the beatmap. - Back at the drag box selection code, this causes a "collection modified when enumerating" exception. The proposed fix here is to eagerly commit active placement - if any - when drag selection is initiated via the timeline, which avoids this issue. This also appears to vaguely match stable behaviour and is sort of consistent with the logic of committing any outstanding changes upon switching to the selection tool. --- .../Editor/TestSceneManiaBeatSnapGrid.cs | 3 +++ osu.Game/Rulesets/Edit/HitObjectComposer.cs | 7 +++++-- .../Edit/Compose/Components/ComposeBlueprintContainer.cs | 4 ++-- .../Components/Timeline/TimelineBlueprintContainer.cs | 2 ++ 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaBeatSnapGrid.cs b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaBeatSnapGrid.cs index fbc0ed1785..0df6b78bd1 100644 --- a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaBeatSnapGrid.cs +++ b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaBeatSnapGrid.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 osu.Framework.Allocation; @@ -17,6 +18,7 @@ using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Screens.Edit; +using osu.Game.Screens.Edit.Compose.Components; using osu.Game.Tests.Visual; using osuTK; @@ -84,6 +86,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor public partial class TestHitObjectComposer : HitObjectComposer { public override Playfield Playfield { get; } + public override ComposeBlueprintContainer BlueprintContainer => throw new NotImplementedException(); public override IEnumerable HitObjects => Enumerable.Empty(); public override bool CursorInPlacementArea => false; diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs index 4d92a08bed..55295fa47b 100644 --- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs @@ -66,7 +66,8 @@ namespace osu.Game.Rulesets.Edit [Resolved] private OverlayColourProvider colourProvider { get; set; } - protected ComposeBlueprintContainer BlueprintContainer { get; private set; } + public override ComposeBlueprintContainer BlueprintContainer => blueprintContainer; + private ComposeBlueprintContainer blueprintContainer; protected ExpandingToolboxContainer LeftToolbox { get; private set; } @@ -137,7 +138,7 @@ namespace osu.Game.Rulesets.Edit drawableRulesetWrapper, // layers above playfield drawableRulesetWrapper.CreatePlayfieldAdjustmentContainer() - .WithChild(BlueprintContainer = CreateBlueprintContainer()) + .WithChild(blueprintContainer = CreateBlueprintContainer()) } }, new Container @@ -532,6 +533,8 @@ namespace osu.Game.Rulesets.Edit /// public abstract Playfield Playfield { get; } + public abstract ComposeBlueprintContainer BlueprintContainer { get; } + /// /// All s in currently loaded beatmap. /// diff --git a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs index 4fba798a26..ecfaf1f72f 100644 --- a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs @@ -372,7 +372,7 @@ namespace osu.Game.Screens.Edit.Compose.Components } } - private void commitIfPlacementActive() + public void CommitIfPlacementActive() { CurrentPlacement?.EndPlacement(CurrentPlacement.PlacementActive == PlacementBlueprint.PlacementState.Active); removePlacement(); @@ -402,7 +402,7 @@ namespace osu.Game.Screens.Edit.Compose.Components currentTool = value; // As per stable editor, when changing tools, we should forcefully commit any pending placement. - commitIfPlacementActive(); + CommitIfPlacementActive(); } } } diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs index 6ebd1961a2..9a8fdc3dac 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs @@ -170,6 +170,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline protected override void UpdateSelectionFromDragBox() { + Composer.BlueprintContainer.CommitIfPlacementActive(); + var dragBox = (TimelineDragBox)DragBox; double minTime = dragBox.MinTime; double maxTime = dragBox.MaxTime; From bfca64a98ebcb15e84bd22ff4b10653b3ddb00d2 Mon Sep 17 00:00:00 2001 From: Shiumano Date: Fri, 14 Jun 2024 20:07:27 +0900 Subject: [PATCH 1066/2493] Add failing test --- .../Gameplay/TestSceneBeatmapOffsetControl.cs | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapOffsetControl.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapOffsetControl.cs index 83fc5c2013..1de55fce8c 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapOffsetControl.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapOffsetControl.cs @@ -137,5 +137,30 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("Remove reference score", () => offsetControl.ReferenceScore.Value = null); AddAssert("No calibration button", () => !offsetControl.ChildrenOfType().Any()); } + + [Test] + public void TestCalibrationNoChange() + { + const double average_error = 0; + + AddAssert("Offset is neutral", () => offsetControl.Current.Value == 0); + AddAssert("No calibration button", () => !offsetControl.ChildrenOfType().Any()); + AddStep("Set reference score", () => + { + offsetControl.ReferenceScore.Value = new ScoreInfo + { + HitEvents = TestSceneHitEventTimingDistributionGraph.CreateDistributedHitEvents(average_error), + BeatmapInfo = Beatmap.Value.BeatmapInfo, + }; + }); + + AddUntilStep("Has calibration button", () => offsetControl.ChildrenOfType().Any()); + AddStep("Press button", () => offsetControl.ChildrenOfType().Single().TriggerClick()); + AddAssert("Offset is adjusted", () => offsetControl.Current.Value == -average_error); + + AddUntilStep("Button is disabled", () => !offsetControl.ChildrenOfType().Single().Enabled.Value); + AddStep("Remove reference score", () => offsetControl.ReferenceScore.Value = null); + AddAssert("No calibration button", () => !offsetControl.ChildrenOfType().Any()); + } } } From 67ca7e4135f5c3d3578da8db5b060b263ede984a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 14 Jun 2024 13:36:40 +0200 Subject: [PATCH 1067/2493] Implement toggling visibility of pass and fail storyboard layers Closes https://github.com/ppy/osu/issues/6842. This is a rather barebones implementation, just to get this in place somehow at least. The logic is simple - 50% health or above shows pass layer, anything below shows fail layer. This does not match stable logic all across the board because I have no idea how to package that. Stable defines "passing" in like fifty ways: - in mania it's >80% HP (https://github.com/peppy/osu-stable-reference/blob/bb57924c1552adbed11ee3d96cdcde47cf96f2b6/osu!/GameModes/Play/Rulesets/Mania/RulesetMania.cs#L333-L336) - in taiko it's >80% *accuracy* (https://github.com/peppy/osu-stable-reference/blob/bb57924c1552adbed11ee3d96cdcde47cf96f2b6/osu!/GameModes/Play/Rulesets/Taiko/RulesetTaiko.cs#L486-L492) - there's also the part where "geki additions" will unconditionally set passing state (https://github.com/peppy/osu-stable-reference/blob/bb57924c1552adbed11ee3d96cdcde47cf96f2b6/osu!/GameModes/Play/Player.cs#L3561-L3564) - and also the part where at the end of the map, the final passing state is determined by checking whether the user passed more sections than failed (https://github.com/peppy/osu-stable-reference/blob/bb57924c1552adbed11ee3d96cdcde47cf96f2b6/osu!/GameModes/Play/Player.cs#L3320) The biggest issues of these are probably the first two, and they can *probably* be fixed, but would require a new member on `Ruleset` and I'm not sure how to make one look, so I'm not doing that at this time pending collection of ideas on how to do that. --- .../Visual/Gameplay/TestSceneStoryboard.cs | 12 ++++--- osu.Game/Screens/Play/GameplayState.cs | 11 ++++++- osu.Game/Screens/Play/Player.cs | 2 +- .../Drawables/DrawableStoryboard.cs | 33 +++++++++---------- osu.Game/Tests/Gameplay/TestGameplayState.cs | 4 ++- 5 files changed, 37 insertions(+), 25 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboard.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboard.cs index 893b9f11f4..e918a93cbc 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboard.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboard.cs @@ -14,8 +14,11 @@ using osu.Game.Beatmaps; using osu.Game.Beatmaps.Formats; using osu.Game.IO; using osu.Game.Overlays; +using osu.Game.Rulesets.Osu; +using osu.Game.Screens.Play; using osu.Game.Storyboards; using osu.Game.Storyboards.Drawables; +using osu.Game.Tests.Gameplay; using osu.Game.Tests.Resources; using osuTK.Graphics; @@ -28,14 +31,14 @@ namespace osu.Game.Tests.Visual.Gameplay private DrawableStoryboard? storyboard; + [Cached] + private GameplayState testGameplayState = TestGameplayState.Create(new OsuRuleset()); + [Test] public void TestStoryboard() { AddStep("Restart", restart); - AddToggleStep("Passing", passing => - { - if (storyboard != null) storyboard.Passing = passing; - }); + AddToggleStep("Toggle passing state", passing => testGameplayState.HealthProcessor.Health.Value = passing ? 1 : 0); } [Test] @@ -109,7 +112,6 @@ namespace osu.Game.Tests.Visual.Gameplay storyboardContainer.Clock = new FramedClock(Beatmap.Value.Track); storyboard = toLoad.CreateDrawable(SelectedMods.Value); - storyboard.Passing = false; storyboardContainer.Add(storyboard); } diff --git a/osu.Game/Screens/Play/GameplayState.cs b/osu.Game/Screens/Play/GameplayState.cs index 8b0207a340..478acd7229 100644 --- a/osu.Game/Screens/Play/GameplayState.cs +++ b/osu.Game/Screens/Play/GameplayState.cs @@ -40,6 +40,7 @@ namespace osu.Game.Screens.Play public readonly Score Score; public readonly ScoreProcessor ScoreProcessor; + public readonly HealthProcessor HealthProcessor; /// /// The storyboard associated with the beatmap. @@ -68,7 +69,14 @@ namespace osu.Game.Screens.Play private readonly Bindable lastJudgementResult = new Bindable(); - public GameplayState(IBeatmap beatmap, Ruleset ruleset, IReadOnlyList? mods = null, Score? score = null, ScoreProcessor? scoreProcessor = null, Storyboard? storyboard = null) + public GameplayState( + IBeatmap beatmap, + Ruleset ruleset, + IReadOnlyList? mods = null, + Score? score = null, + ScoreProcessor? scoreProcessor = null, + HealthProcessor? healthProcessor = null, + Storyboard? storyboard = null) { Beatmap = beatmap; Ruleset = ruleset; @@ -82,6 +90,7 @@ namespace osu.Game.Screens.Play }; Mods = mods ?? Array.Empty(); ScoreProcessor = scoreProcessor ?? ruleset.CreateScoreProcessor(); + HealthProcessor = healthProcessor ?? ruleset.CreateHealthProcessor(beatmap.HitObjects[0].StartTime); Storyboard = storyboard ?? new Storyboard(); } diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 42ff1d74f3..3a08d3be24 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -260,7 +260,7 @@ namespace osu.Game.Screens.Play Score.ScoreInfo.Ruleset = ruleset.RulesetInfo; Score.ScoreInfo.Mods = gameplayMods; - dependencies.CacheAs(GameplayState = new GameplayState(playableBeatmap, ruleset, gameplayMods, Score, ScoreProcessor, Beatmap.Value.Storyboard)); + dependencies.CacheAs(GameplayState = new GameplayState(playableBeatmap, ruleset, gameplayMods, Score, ScoreProcessor, HealthProcessor, Beatmap.Value.Storyboard)); var rulesetSkinProvider = new RulesetSkinProvidingContainer(ruleset, playableBeatmap, Beatmap.Value.Skin); diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs index fc5ef12fb8..8e7b3feacf 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs @@ -37,20 +37,6 @@ namespace osu.Game.Storyboards.Drawables protected override Vector2 DrawScale => new Vector2(Parent!.DrawHeight / 480); - private bool passing = true; - - public bool Passing - { - get => passing; - set - { - if (passing == value) return; - - passing = value; - updateLayerVisibility(); - } - } - public override bool RemoveCompletedTransforms => false; private double? lastEventEndTime; @@ -66,6 +52,9 @@ namespace osu.Game.Storyboards.Drawables private DependencyContainer dependencies = null!; + private BindableNumber health = null!; + private readonly BindableBool passing = new BindableBool(true); + protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) => dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); @@ -91,8 +80,8 @@ namespace osu.Game.Storyboards.Drawables }); } - [BackgroundDependencyLoader(true)] - private void load(IGameplayClock? clock, CancellationToken? cancellationToken) + [BackgroundDependencyLoader] + private void load(IGameplayClock? clock, CancellationToken? cancellationToken, GameplayState? gameplayState) { if (clock != null) Clock = clock; @@ -110,6 +99,16 @@ namespace osu.Game.Storyboards.Drawables } lastEventEndTime = Storyboard.LatestEventTime; + + health = gameplayState?.HealthProcessor.Health.GetBoundCopy() ?? new BindableDouble(1); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + health.BindValueChanged(val => passing.Value = val.NewValue >= 0.5, true); + passing.BindValueChanged(_ => updateLayerVisibility(), true); } protected virtual IResourceStore CreateResourceLookupStore() => new StoryboardResourceLookupStore(Storyboard, realm, host); @@ -125,7 +124,7 @@ namespace osu.Game.Storyboards.Drawables private void updateLayerVisibility() { foreach (var layer in Children) - layer.Enabled = passing ? layer.Layer.VisibleWhenPassing : layer.Layer.VisibleWhenFailing; + layer.Enabled = passing.Value ? layer.Layer.VisibleWhenPassing : layer.Layer.VisibleWhenFailing; } private class StoryboardResourceLookupStore : IResourceStore diff --git a/osu.Game/Tests/Gameplay/TestGameplayState.cs b/osu.Game/Tests/Gameplay/TestGameplayState.cs index bb82335543..8fad6d1e23 100644 --- a/osu.Game/Tests/Gameplay/TestGameplayState.cs +++ b/osu.Game/Tests/Gameplay/TestGameplayState.cs @@ -27,7 +27,9 @@ namespace osu.Game.Tests.Gameplay var scoreProcessor = ruleset.CreateScoreProcessor(); scoreProcessor.ApplyBeatmap(playableBeatmap); - return new GameplayState(playableBeatmap, ruleset, mods, score, scoreProcessor); + var healthProcessor = ruleset.CreateHealthProcessor(beatmap.HitObjects[0].StartTime); + + return new GameplayState(playableBeatmap, ruleset, mods, score, scoreProcessor, healthProcessor); } } } From 31a8bc75532a8a11da972e3a595246cd5baf43d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 14 Jun 2024 14:12:55 +0200 Subject: [PATCH 1068/2493] Remove redundant qualifier --- .../Editor/TestSceneManiaBeatSnapGrid.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaBeatSnapGrid.cs b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaBeatSnapGrid.cs index 0df6b78bd1..127beed83e 100644 --- a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaBeatSnapGrid.cs +++ b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaBeatSnapGrid.cs @@ -103,7 +103,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All) { - throw new System.NotImplementedException(); + throw new NotImplementedException(); } } } From 6c82f1de9b12700e3654b3eb305e284930f2ea97 Mon Sep 17 00:00:00 2001 From: Shiumano Date: Fri, 14 Jun 2024 21:43:17 +0900 Subject: [PATCH 1069/2493] Set to enable or disable at load time --- osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs index 9039604471..7c0a2e73c0 100644 --- a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs +++ b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs @@ -237,6 +237,8 @@ namespace osu.Game.Screens.Play.PlayerSettings } }); + useAverageButton.Enabled.Value = !Precision.AlmostEquals(lastPlayAverage, 0, Current.Precision / 2); + if (settings != null) { globalOffsetText.AddText("You can also "); From 6bd633f8ce5fa2c608700075a4969c8954fe3121 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 15 Jun 2024 17:26:57 +0800 Subject: [PATCH 1070/2493] Apply NRT to test scene --- .../Visual/Gameplay/TestSceneBeatmapOffsetControl.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapOffsetControl.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapOffsetControl.cs index 1de55fce8c..3b88750013 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapOffsetControl.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapOffsetControl.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.Linq; using NUnit.Framework; using osu.Framework.Graphics; @@ -19,7 +17,7 @@ namespace osu.Game.Tests.Visual.Gameplay { public partial class TestSceneBeatmapOffsetControl : OsuTestScene { - private BeatmapOffsetControl offsetControl; + private BeatmapOffsetControl offsetControl = null!; [SetUpSteps] public void SetUpSteps() From ff2d721029809c8e7cadd6620a6634c898e9004d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 15 Jun 2024 17:34:04 +0800 Subject: [PATCH 1071/2493] Inline enabled setting --- osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs index 7c0a2e73c0..7668d3e635 100644 --- a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs +++ b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs @@ -228,7 +228,8 @@ namespace osu.Game.Screens.Play.PlayerSettings useAverageButton = new SettingsButton { Text = BeatmapOffsetControlStrings.CalibrateUsingLastPlay, - Action = () => Current.Value = lastPlayBeatmapOffset - lastPlayAverage + Action = () => Current.Value = lastPlayBeatmapOffset - lastPlayAverage, + Enabled = { Value = !Precision.AlmostEquals(lastPlayAverage, 0, Current.Precision / 2) } }, globalOffsetText = new LinkFlowContainer { @@ -237,8 +238,6 @@ namespace osu.Game.Screens.Play.PlayerSettings } }); - useAverageButton.Enabled.Value = !Precision.AlmostEquals(lastPlayAverage, 0, Current.Precision / 2); - if (settings != null) { globalOffsetText.AddText("You can also "); From 97003b3679389869d7247044f0964f863fb9e965 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 17 Jun 2024 09:08:43 +0200 Subject: [PATCH 1072/2493] Account for osu! circle radius when drawing playfield border Addresses https://github.com/ppy/osu/discussions/13167. --- osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs index 93c3450904..3a04f92ec0 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs @@ -12,6 +12,7 @@ using osu.Game.Beatmaps; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Objects.Legacy; using osu.Game.Rulesets.Osu.Beatmaps; using osu.Game.Rulesets.Osu.Configuration; using osu.Game.Rulesets.Osu.Objects; @@ -27,6 +28,7 @@ namespace osu.Game.Rulesets.Osu.UI [Cached] public partial class OsuPlayfield : Playfield { + private readonly Container borderContainer; private readonly PlayfieldBorder playfieldBorder; private readonly ProxyContainer approachCircles; private readonly ProxyContainer spinnerProxies; @@ -54,7 +56,11 @@ namespace osu.Game.Rulesets.Osu.UI InternalChildren = new Drawable[] { - playfieldBorder = new PlayfieldBorder { RelativeSizeAxes = Axes.Both }, + borderContainer = new Container + { + RelativeSizeAxes = Axes.Both, + Child = playfieldBorder = new PlayfieldBorder { RelativeSizeAxes = Axes.Both }, + }, Smoke = new SmokeContainer { RelativeSizeAxes = Axes.Both }, spinnerProxies = new ProxyContainer { RelativeSizeAxes = Axes.Both }, FollowPoints = new FollowPointRenderer { RelativeSizeAxes = Axes.Both }, @@ -151,6 +157,9 @@ namespace osu.Game.Rulesets.Osu.UI RegisterPool(2, 20); RegisterPool(10, 200); RegisterPool(10, 200); + + if (beatmap != null) + borderContainer.Padding = new MarginPadding(OsuHitObject.OBJECT_RADIUS * -LegacyRulesetExtensions.CalculateScaleFromCircleSize(beatmap.Difficulty.CircleSize, true)); } protected override HitObjectLifetimeEntry CreateLifetimeEntry(HitObject hitObject) => new OsuHitObjectLifetimeEntry(hitObject); From 41446a08b678e585324f94b3e5a1b6b7238c4cdd Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 17 Jun 2024 16:19:33 +0900 Subject: [PATCH 1073/2493] Annotate ControlPoint and Mod for AOT trimming support --- osu.Game/Beatmaps/ControlPoints/ControlPoint.cs | 2 ++ osu.Game/Rulesets/Mods/Mod.cs | 2 ++ 2 files changed, 4 insertions(+) diff --git a/osu.Game/Beatmaps/ControlPoints/ControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/ControlPoint.cs index f46e4af332..c90557b5f1 100644 --- a/osu.Game/Beatmaps/ControlPoints/ControlPoint.cs +++ b/osu.Game/Beatmaps/ControlPoints/ControlPoint.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Diagnostics.CodeAnalysis; using Newtonsoft.Json; using osu.Game.Graphics; using osu.Game.Utils; @@ -9,6 +10,7 @@ using osuTK.Graphics; namespace osu.Game.Beatmaps.ControlPoints { + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] public abstract class ControlPoint : IComparable, IDeepCloneable, IEquatable, IControlPoint { [JsonIgnore] diff --git a/osu.Game/Rulesets/Mods/Mod.cs b/osu.Game/Rulesets/Mods/Mod.cs index 50c867f41b..b9a937b1a2 100644 --- a/osu.Game/Rulesets/Mods/Mod.cs +++ b/osu.Game/Rulesets/Mods/Mod.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Linq; using System.Reflection; @@ -21,6 +22,7 @@ namespace osu.Game.Rulesets.Mods /// /// The base class for gameplay modifiers. /// + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] public abstract class Mod : IMod, IEquatable, IDeepCloneable { [JsonIgnore] From 1b00d0181a6b5bf073e2cb7a55188e7fa5b2e733 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 17 Jun 2024 09:36:01 +0200 Subject: [PATCH 1074/2493] Fix playfield border size not updating in editor on circle size change --- .../Edit/DrawableOsuEditorRuleset.cs | 23 +++++++++++++++++++ osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs | 7 +++++- 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditorRuleset.cs b/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditorRuleset.cs index 68c565af4d..4dd718597a 100644 --- a/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditorRuleset.cs +++ b/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditorRuleset.cs @@ -2,10 +2,13 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; +using osu.Framework.Allocation; +using osu.Framework.Extensions.ObjectExtensions; using osu.Game.Beatmaps; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.UI; using osu.Game.Rulesets.UI; +using osu.Game.Screens.Edit; using osuTK; namespace osu.Game.Rulesets.Osu.Edit @@ -23,12 +26,32 @@ namespace osu.Game.Rulesets.Osu.Edit private partial class OsuEditorPlayfield : OsuPlayfield { + [Resolved] + private EditorBeatmap editorBeatmap { get; set; } = null!; + protected override GameplayCursorContainer? CreateCursor() => null; public OsuEditorPlayfield() { HitPolicy = new AnyOrderHitPolicy(); } + + protected override void LoadComplete() + { + base.LoadComplete(); + + editorBeatmap.BeatmapReprocessed += onBeatmapReprocessed; + } + + private void onBeatmapReprocessed() => ApplyCircleSizeToPlayfieldBorder(editorBeatmap); + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (editorBeatmap.IsNotNull()) + editorBeatmap.BeatmapReprocessed -= onBeatmapReprocessed; + } } } } diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs index 3a04f92ec0..b39fc34d5d 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs @@ -159,7 +159,12 @@ namespace osu.Game.Rulesets.Osu.UI RegisterPool(10, 200); if (beatmap != null) - borderContainer.Padding = new MarginPadding(OsuHitObject.OBJECT_RADIUS * -LegacyRulesetExtensions.CalculateScaleFromCircleSize(beatmap.Difficulty.CircleSize, true)); + ApplyCircleSizeToPlayfieldBorder(beatmap); + } + + protected void ApplyCircleSizeToPlayfieldBorder(IBeatmap beatmap) + { + borderContainer.Padding = new MarginPadding(OsuHitObject.OBJECT_RADIUS * -LegacyRulesetExtensions.CalculateScaleFromCircleSize(beatmap.Difficulty.CircleSize, true)); } protected override HitObjectLifetimeEntry CreateLifetimeEntry(HitObject hitObject) => new OsuHitObjectLifetimeEntry(hitObject); From f86e9c9a4a5eb2c7b16ddd5820766266dac07b14 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 17 Jun 2024 17:13:44 +0900 Subject: [PATCH 1075/2493] Also annotate ControlPointInfo Same deal with this class. Fully qualifying the type names because this has `#nullable disable` and makes use of `NotNull` which is also present in the `System.Diagnostics.CodeAnalysis` namespace and AAAAAAARGH NAMESPACE CONFLICTS. --- osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs b/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs index 1a15db98e4..c695112990 100644 --- a/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs +++ b/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs @@ -17,6 +17,7 @@ using osu.Game.Utils; namespace osu.Game.Beatmaps.ControlPoints { [Serializable] + [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] public class ControlPointInfo : IDeepCloneable { /// From b42752c9f06d10da5985ff4ab10e8e728a1a5be0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 17 Jun 2024 10:16:40 +0200 Subject: [PATCH 1076/2493] Move timeline toggle controls to "view" menu --- osu.Game/Configuration/OsuConfigManager.cs | 5 ++ osu.Game/Localisation/EditorStrings.cs | 25 ++++++---- .../Compose/Components/Timeline/Timeline.cs | 17 +++---- .../Components/Timeline/TimelineArea.cs | 50 ------------------- osu.Game/Screens/Edit/Editor.cs | 20 +++++++- .../Screens/Edit/WaveformOpacityMenuItem.cs | 1 + 6 files changed, 47 insertions(+), 71 deletions(-) diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index affcaffe02..bef1cf2899 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -208,6 +208,9 @@ namespace osu.Game.Configuration SetDefault(OsuSetting.ComboColourNormalisationAmount, 0.2f, 0f, 1f, 0.01f); SetDefault(OsuSetting.UserOnlineStatus, null); + + SetDefault(OsuSetting.EditorTimelineShowTimingChanges, true); + SetDefault(OsuSetting.EditorTimelineShowTicks, true); } protected override bool CheckLookupContainsPrivateInformation(OsuSetting lookup) @@ -439,5 +442,7 @@ namespace osu.Game.Configuration UserOnlineStatus, MultiplayerRoomFilter, HideCountryFlags, + EditorTimelineShowTimingChanges, + EditorTimelineShowTicks, } } diff --git a/osu.Game/Localisation/EditorStrings.cs b/osu.Game/Localisation/EditorStrings.cs index 6ad12f54df..bcffc18d4d 100644 --- a/osu.Game/Localisation/EditorStrings.cs +++ b/osu.Game/Localisation/EditorStrings.cs @@ -99,16 +99,6 @@ namespace osu.Game.Localisation /// public static LocalisableString TestBeatmap => new TranslatableString(getKey(@"test_beatmap"), @"Test!"); - /// - /// "Waveform" - /// - public static LocalisableString TimelineWaveform => new TranslatableString(getKey(@"timeline_waveform"), @"Waveform"); - - /// - /// "Ticks" - /// - public static LocalisableString TimelineTicks => new TranslatableString(getKey(@"timeline_ticks"), @"Ticks"); - /// /// "{0:0}°" /// @@ -134,6 +124,21 @@ namespace osu.Game.Localisation /// public static LocalisableString FailedToParseEditorLink => new TranslatableString(getKey(@"failed_to_parse_edtior_link"), @"Failed to parse editor link"); + /// + /// "Timeline" + /// + public static LocalisableString Timeline => new TranslatableString(getKey(@"timeline"), @"Timeline"); + + /// + /// "Show timing changes" + /// + public static LocalisableString TimelineShowTimingChanges => new TranslatableString(getKey(@"timeline_show_timing_changes"), @"Show timing changes"); + + /// + /// "Show ticks" + /// + public static LocalisableString TimelineShowTicks => new TranslatableString(getKey(@"timeline_show_ticks"), @"Show ticks"); + private static string getKey(string key) => $@"{prefix}:{key}"; } } diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs index a2704e550c..fa9964b104 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs @@ -30,12 +30,6 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline private readonly Drawable userContent; - public readonly Bindable WaveformVisible = new Bindable(); - - public readonly Bindable ControlPointsVisible = new Bindable(); - - public readonly Bindable TicksVisible = new Bindable(); - [Resolved] private EditorClock editorClock { get; set; } @@ -88,6 +82,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline private Container mainContent; private Bindable waveformOpacity; + private Bindable controlPointsVisible; + private Bindable ticksVisible; private double trackLengthForZoom; @@ -139,6 +135,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline }); waveformOpacity = config.GetBindable(OsuSetting.EditorWaveformOpacity); + controlPointsVisible = config.GetBindable(OsuSetting.EditorTimelineShowTimingChanges); + ticksVisible = config.GetBindable(OsuSetting.EditorTimelineShowTicks); track.BindTo(editorClock.Track); track.BindValueChanged(_ => @@ -168,12 +166,11 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline { base.LoadComplete(); - WaveformVisible.BindValueChanged(_ => updateWaveformOpacity()); waveformOpacity.BindValueChanged(_ => updateWaveformOpacity(), true); - TicksVisible.BindValueChanged(visible => ticks.FadeTo(visible.NewValue ? 1 : 0, 200, Easing.OutQuint), true); + ticksVisible.BindValueChanged(visible => ticks.FadeTo(visible.NewValue ? 1 : 0, 200, Easing.OutQuint), true); - ControlPointsVisible.BindValueChanged(visible => + controlPointsVisible.BindValueChanged(visible => { if (visible.NewValue) { @@ -195,7 +192,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline } private void updateWaveformOpacity() => - waveform.FadeTo(WaveformVisible.Value ? waveformOpacity.Value : 0, 200, Easing.OutQuint); + waveform.FadeTo(waveformOpacity.Value, 200, Easing.OutQuint); protected override void Update() { diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineArea.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineArea.cs index b973ac3731..5631adf8bd 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineArea.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineArea.cs @@ -7,10 +7,7 @@ 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; -using osu.Game.Resources.Localisation.Web; using osu.Game.Rulesets.Edit; using osuTK; @@ -33,10 +30,6 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline [BackgroundDependencyLoader] private void load(OverlayColourProvider colourProvider, OsuColour colours) { - OsuCheckbox waveformCheckbox; - OsuCheckbox controlPointsCheckbox; - OsuCheckbox ticksCheckbox; - const float padding = 10; InternalChildren = new Drawable[] @@ -51,7 +44,6 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline }, ColumnDimensions = new[] { - new Dimension(GridSizeMode.Absolute, 135), new Dimension(), new Dimension(GridSizeMode.Absolute, 35), new Dimension(GridSizeMode.Absolute, HitObjectComposer.TOOLBOX_CONTRACTED_SIZE_RIGHT - padding * 2), @@ -60,44 +52,6 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline { new Drawable[] { - new Container - { - RelativeSizeAxes = Axes.Both, - Name = @"Toggle controls", - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = colourProvider.Background2, - }, - new FillFlowContainer - { - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding(padding), - Direction = FillDirection.Vertical, - Spacing = new Vector2(0, 4), - Children = new[] - { - waveformCheckbox = new OsuCheckbox(nubSize: 30f) - { - LabelText = EditorStrings.TimelineWaveform, - Current = { Value = true }, - }, - ticksCheckbox = new OsuCheckbox(nubSize: 30f) - { - LabelText = EditorStrings.TimelineTicks, - Current = { Value = true }, - }, - controlPointsCheckbox = new OsuCheckbox(nubSize: 30f) - { - LabelText = BeatmapsetsStrings.ShowStatsBpm, - Current = { Value = true }, - }, - } - } - } - }, new Container { RelativeSizeAxes = Axes.X, @@ -167,10 +121,6 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline }, } }; - - Timeline.WaveformVisible.BindTo(waveformCheckbox.Current); - Timeline.ControlPointsVisible.BindTo(controlPointsCheckbox.Current); - Timeline.TicksVisible.BindTo(ticksCheckbox.Current); } } } diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index a630a5df41..316772da6e 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -211,6 +211,8 @@ namespace osu.Game.Screens.Edit private Bindable editorHitMarkers; private Bindable editorAutoSeekOnPlacement; private Bindable editorLimitedDistanceSnap; + private Bindable editorTimelineShowTimingChanges; + private Bindable editorTimelineShowTicks; public Editor(EditorLoader loader = null) { @@ -305,6 +307,8 @@ namespace osu.Game.Screens.Edit editorHitMarkers = config.GetBindable(OsuSetting.EditorShowHitMarkers); editorAutoSeekOnPlacement = config.GetBindable(OsuSetting.EditorAutoSeekOnPlacement); editorLimitedDistanceSnap = config.GetBindable(OsuSetting.EditorLimitedDistanceSnap); + editorTimelineShowTimingChanges = config.GetBindable(OsuSetting.EditorTimelineShowTimingChanges); + editorTimelineShowTicks = config.GetBindable(OsuSetting.EditorTimelineShowTicks); AddInternal(new OsuContextMenuContainer { @@ -357,7 +361,21 @@ namespace osu.Game.Screens.Edit { Items = new MenuItem[] { - new WaveformOpacityMenuItem(config.GetBindable(OsuSetting.EditorWaveformOpacity)), + new MenuItem(EditorStrings.Timeline) + { + Items = + [ + new WaveformOpacityMenuItem(config.GetBindable(OsuSetting.EditorWaveformOpacity)), + new ToggleMenuItem(EditorStrings.TimelineShowTimingChanges) + { + State = { BindTarget = editorTimelineShowTimingChanges } + }, + new ToggleMenuItem(EditorStrings.TimelineShowTicks) + { + State = { BindTarget = editorTimelineShowTicks } + }, + ] + }, new BackgroundDimMenuItem(editorBackgroundDim), new ToggleMenuItem(EditorStrings.ShowHitMarkers) { diff --git a/osu.Game/Screens/Edit/WaveformOpacityMenuItem.cs b/osu.Game/Screens/Edit/WaveformOpacityMenuItem.cs index 9dc0ea0d07..c379e56940 100644 --- a/osu.Game/Screens/Edit/WaveformOpacityMenuItem.cs +++ b/osu.Game/Screens/Edit/WaveformOpacityMenuItem.cs @@ -20,6 +20,7 @@ namespace osu.Game.Screens.Edit { Items = new[] { + createMenuItem(0f), createMenuItem(0.25f), createMenuItem(0.5f), createMenuItem(0.75f), From 03049d45bb136826c70d632825150492ba36cbc1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 17 Jun 2024 10:23:00 +0200 Subject: [PATCH 1077/2493] Remove stuff that looks bad after moving timeline toggle controls --- .../Edit/Compose/Components/Timeline/TimelineArea.cs | 10 ---------- osu.Game/Screens/Edit/EditorScreenWithTimeline.cs | 1 - 2 files changed, 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 5631adf8bd..7bc884073a 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineArea.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineArea.cs @@ -58,16 +58,6 @@ 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, diff --git a/osu.Game/Screens/Edit/EditorScreenWithTimeline.cs b/osu.Game/Screens/Edit/EditorScreenWithTimeline.cs index 2b97d363f1..1b37223bb3 100644 --- a/osu.Game/Screens/Edit/EditorScreenWithTimeline.cs +++ b/osu.Game/Screens/Edit/EditorScreenWithTimeline.cs @@ -62,7 +62,6 @@ namespace osu.Game.Screens.Edit Name = "Timeline content", RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Padding = new MarginPadding { Horizontal = PADDING, Top = PADDING }, Content = new[] { new Drawable[] From f7910f774d8f8ed934209edb0e83b4bc08aff641 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 17 Jun 2024 10:54:52 +0200 Subject: [PATCH 1078/2493] Remove redundant type spec --- osu.Game/Screens/Edit/Editor.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 316772da6e..c18acf8bb8 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -359,7 +359,7 @@ namespace osu.Game.Screens.Edit }, new MenuItem(CommonStrings.MenuBarView) { - Items = new MenuItem[] + Items = new[] { new MenuItem(EditorStrings.Timeline) { From 3884bce2393c583e4b99a143a8f52269a9e159cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 17 Jun 2024 11:09:04 +0200 Subject: [PATCH 1079/2493] Remove unused delegate for now To silence inspections. --- .../OnlinePlay/DailyChallenge/DailyChallengeEventFeed.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeEventFeed.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeEventFeed.cs index 10f4f2cf78..6ddd2f1278 100644 --- a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeEventFeed.cs +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeEventFeed.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 osu.Framework.Allocation; @@ -82,8 +81,6 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge private partial class NewScoreEventRow : CompositeDrawable { - public Action? PresentScore { get; set; } - private readonly NewScoreEvent newScore; public NewScoreEventRow(NewScoreEvent newScore) @@ -124,7 +121,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge text.AddUserLink(newScore.Score.User); text.AddText(" got "); - text.AddLink($"{newScore.Score.TotalScore:N0} points", () => PresentScore?.Invoke(newScore.Score)); + text.AddLink($"{newScore.Score.TotalScore:N0} points", () => { }); // TODO: present the score here if (newScore.NewRank != null) text.AddText($" and achieved rank #{newScore.NewRank.Value:N0}"); From 07f1994a13e5602f85ff6537e00056ed571d2f8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 17 Jun 2024 11:47:37 +0200 Subject: [PATCH 1080/2493] Align beat snap control width with right toolbox --- .../Screens/Edit/Compose/Components/Timeline/TimelineArea.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineArea.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineArea.cs index 7bc884073a..9af57ba9f6 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineArea.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineArea.cs @@ -46,7 +46,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline { new Dimension(), new Dimension(GridSizeMode.Absolute, 35), - new Dimension(GridSizeMode.Absolute, HitObjectComposer.TOOLBOX_CONTRACTED_SIZE_RIGHT - padding * 2), + new Dimension(GridSizeMode.Absolute, HitObjectComposer.TOOLBOX_CONTRACTED_SIZE_RIGHT), }, Content = new[] { From 7cfe8d8df2a7397b0b46fe103274546f693beee9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 17 Jun 2024 12:11:19 +0200 Subject: [PATCH 1081/2493] Reduce editor opacity of several editor components when hovering over composer Addresses https://github.com/ppy/osu/discussions/24384. --- osu.Game/Rulesets/Edit/HitObjectComposer.cs | 52 ++++++++++++++++--- osu.Game/Screens/Edit/BottomBar.cs | 25 ++++----- .../Edit/Components/BottomBarContainer.cs | 2 +- .../Edit/Components/PlaybackControl.cs | 4 +- .../Components/Timeline/TimelineArea.cs | 26 +++++++++- osu.Game/Screens/Edit/Editor.cs | 7 ++- .../Screens/Edit/EditorScreenWithTimeline.cs | 6 --- 7 files changed, 92 insertions(+), 30 deletions(-) diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs index 4d92a08bed..d0c6078c9d 100644 --- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs @@ -7,6 +7,7 @@ using System; using System.Collections.Generic; using System.Collections.Specialized; using System.Linq; +using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.EnumExtensions; @@ -78,14 +79,16 @@ namespace osu.Game.Rulesets.Edit protected InputManager InputManager { get; private set; } + private Box leftToolboxBackground; + private Box rightToolboxBackground; + private EditorRadioButtonCollection toolboxCollection; - private FillFlowContainer togglesCollection; - private FillFlowContainer sampleBankTogglesCollection; private IBindable hasTiming; private Bindable autoSeekOnPlacement; + private readonly Bindable composerFocusMode = new Bindable(); protected DrawableRuleset DrawableRuleset { get; private set; } @@ -97,11 +100,14 @@ namespace osu.Game.Rulesets.Edit protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) => dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); - [BackgroundDependencyLoader] - private void load(OsuConfigManager config) + [BackgroundDependencyLoader(true)] + private void load(OsuConfigManager config, [CanBeNull] Editor editor) { autoSeekOnPlacement = config.GetBindable(OsuSetting.EditorAutoSeekOnPlacement); + if (editor != null) + composerFocusMode.BindTo(editor.ComposerFocusMode); + Config = Dependencies.Get().GetConfigFor(Ruleset); try @@ -126,7 +132,7 @@ namespace osu.Game.Rulesets.Edit InternalChildren = new[] { - PlayfieldContentContainer = new Container + PlayfieldContentContainer = new ContentContainer { Name = "Playfield content", RelativeSizeAxes = Axes.Y, @@ -146,7 +152,7 @@ namespace osu.Game.Rulesets.Edit AutoSizeAxes = Axes.X, Children = new Drawable[] { - new Box + leftToolboxBackground = new Box { Colour = colourProvider.Background5, RelativeSizeAxes = Axes.Both, @@ -191,7 +197,7 @@ namespace osu.Game.Rulesets.Edit AutoSizeAxes = Axes.X, Children = new Drawable[] { - new Box + rightToolboxBackground = new Box { Colour = colourProvider.Background5, RelativeSizeAxes = Axes.Both, @@ -260,6 +266,13 @@ namespace osu.Game.Rulesets.Edit item.Selected.Disabled = !hasTiming.NewValue; } }, true); + + composerFocusMode.BindValueChanged(_ => + { + float targetAlpha = composerFocusMode.Value ? 0.5f : 1; + leftToolboxBackground.FadeTo(targetAlpha, 400, Easing.OutQuint); + rightToolboxBackground.FadeTo(targetAlpha, 400, Easing.OutQuint); + }, true); } protected override void Update() @@ -507,6 +520,31 @@ namespace osu.Game.Rulesets.Edit } #endregion + + private partial class ContentContainer : Container + { + public override bool HandlePositionalInput => true; + + private readonly Bindable composerFocusMode = new Bindable(); + + [BackgroundDependencyLoader(true)] + private void load([CanBeNull] Editor editor) + { + if (editor != null) + composerFocusMode.BindTo(editor.ComposerFocusMode); + } + + protected override bool OnHover(HoverEvent e) + { + composerFocusMode.Value = true; + return false; + } + + protected override void OnHoverLost(HoverLostEvent e) + { + composerFocusMode.Value = false; + } + } } /// diff --git a/osu.Game/Screens/Edit/BottomBar.cs b/osu.Game/Screens/Edit/BottomBar.cs index d43e675296..6118adc0d7 100644 --- a/osu.Game/Screens/Edit/BottomBar.cs +++ b/osu.Game/Screens/Edit/BottomBar.cs @@ -1,16 +1,13 @@ // 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.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Effects; -using osu.Framework.Graphics.Shapes; -using osu.Game.Overlays; +using osu.Framework.Testing; using osu.Game.Rulesets.Edit; using osu.Game.Screens.Edit.Components; using osu.Game.Screens.Edit.Components.Timelines.Summary; @@ -21,12 +18,13 @@ namespace osu.Game.Screens.Edit { internal partial class BottomBar : CompositeDrawable { - public TestGameplayButton TestGameplayButton { get; private set; } + public TestGameplayButton TestGameplayButton { get; private set; } = null!; - private IBindable saveInProgress; + private IBindable saveInProgress = null!; + private Bindable composerFocusMode = null!; [BackgroundDependencyLoader] - private void load(OverlayColourProvider colourProvider, Editor editor) + private void load(Editor editor) { Anchor = Anchor.BottomLeft; Origin = Anchor.BottomLeft; @@ -45,11 +43,6 @@ namespace osu.Game.Screens.Edit InternalChildren = new Drawable[] { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = colourProvider.Background4, - }, new GridContainer { RelativeSizeAxes = Axes.Both, @@ -79,6 +72,7 @@ namespace osu.Game.Screens.Edit }; saveInProgress = editor.MutationTracker.InProgress.GetBoundCopy(); + composerFocusMode = editor.ComposerFocusMode.GetBoundCopy(); } protected override void LoadComplete() @@ -86,6 +80,13 @@ namespace osu.Game.Screens.Edit base.LoadComplete(); saveInProgress.BindValueChanged(_ => TestGameplayButton.Enabled.Value = !saveInProgress.Value, true); + composerFocusMode.BindValueChanged(_ => + { + float targetAlpha = composerFocusMode.Value ? 0.5f : 1; + + foreach (var c in this.ChildrenOfType()) + c.Background.FadeTo(targetAlpha, 400, Easing.OutQuint); + }, true); } } } diff --git a/osu.Game/Screens/Edit/Components/BottomBarContainer.cs b/osu.Game/Screens/Edit/Components/BottomBarContainer.cs index 0ba1ab9258..da71457004 100644 --- a/osu.Game/Screens/Edit/Components/BottomBarContainer.cs +++ b/osu.Game/Screens/Edit/Components/BottomBarContainer.cs @@ -20,7 +20,7 @@ namespace osu.Game.Screens.Edit.Components protected readonly IBindable Track = new Bindable(); - protected readonly Drawable Background; + public readonly Drawable Background; private readonly Container content; protected override Container Content => content; diff --git a/osu.Game/Screens/Edit/Components/PlaybackControl.cs b/osu.Game/Screens/Edit/Components/PlaybackControl.cs index a5ed0d680f..9e27f0e57d 100644 --- a/osu.Game/Screens/Edit/Components/PlaybackControl.cs +++ b/osu.Game/Screens/Edit/Components/PlaybackControl.cs @@ -32,8 +32,10 @@ namespace osu.Game.Screens.Edit.Components private readonly BindableNumber freqAdjust = new BindableDouble(1); [BackgroundDependencyLoader] - private void load() + private void load(OverlayColourProvider colourProvider) { + Background.Colour = colourProvider.Background4; + Children = new Drawable[] { playButton = new IconButton diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineArea.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineArea.cs index 9af57ba9f6..cee7212a5d 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineArea.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineArea.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; @@ -19,6 +20,9 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline private readonly Drawable userContent; + private Box timelineBackground = null!; + private readonly Bindable composerFocusMode = new Bindable(); + public TimelineArea(Drawable? content = null) { RelativeSizeAxes = Axes.X; @@ -28,12 +32,20 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline } [BackgroundDependencyLoader] - private void load(OverlayColourProvider colourProvider, OsuColour colours) + private void load(OverlayColourProvider colourProvider, OsuColour colours, Editor? editor) { const float padding = 10; InternalChildren = new Drawable[] { + new Box + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + Width = 35 + HitObjectComposer.TOOLBOX_CONTRACTED_SIZE_RIGHT, + RelativeSizeAxes = Axes.Y, + Colour = colourProvider.Background4 + }, new GridContainer { RelativeSizeAxes = Axes.X, @@ -58,7 +70,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline AutoSizeAxes = Axes.Y, Children = new Drawable[] { - new Box + timelineBackground = new Box { RelativeSizeAxes = Axes.Both, Depth = float.MaxValue, @@ -111,6 +123,16 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline }, } }; + + if (editor != null) + composerFocusMode.BindTo(editor.ComposerFocusMode); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + composerFocusMode.BindValueChanged(_ => timelineBackground.FadeTo(composerFocusMode.Value ? 0.5f : 1, 400, Easing.OutQuint), true); } } } diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index c18acf8bb8..02dcad46f7 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -214,6 +214,12 @@ namespace osu.Game.Screens.Edit private Bindable editorTimelineShowTimingChanges; private Bindable editorTimelineShowTicks; + /// + /// This controls the opacity of components like the timelines, sidebars, etc. + /// In "composer focus" mode the opacity of the aforementioned components is reduced so that the user can focus on the composer better. + /// + public Bindable ComposerFocusMode { get; } = new Bindable(); + public Editor(EditorLoader loader = null) { this.loader = loader; @@ -323,7 +329,6 @@ namespace osu.Game.Screens.Edit Child = screenContainer = new Container { RelativeSizeAxes = Axes.Both, - Masking = true } }, new Container diff --git a/osu.Game/Screens/Edit/EditorScreenWithTimeline.cs b/osu.Game/Screens/Edit/EditorScreenWithTimeline.cs index 1b37223bb3..cdc8a26c35 100644 --- a/osu.Game/Screens/Edit/EditorScreenWithTimeline.cs +++ b/osu.Game/Screens/Edit/EditorScreenWithTimeline.cs @@ -4,7 +4,6 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; 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.Timeline; @@ -52,11 +51,6 @@ namespace osu.Game.Screens.Edit AutoSizeAxes = Axes.Y, Children = new Drawable[] { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = colourProvider.Background4 - }, new GridContainer { Name = "Timeline content", From 05596a391d481524582772f64d5a299efbdbecbb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 17 Jun 2024 19:11:08 +0800 Subject: [PATCH 1082/2493] Disable collection expression inspections completely --- osu.sln.DotSettings | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.sln.DotSettings b/osu.sln.DotSettings index 04633a9348..25bbc4beb5 100644 --- a/osu.sln.DotSettings +++ b/osu.sln.DotSettings @@ -254,7 +254,7 @@ HINT DO_NOT_SHOW WARNING - HINT + DO_NOT_SHOW WARNING WARNING WARNING From d3d325c46c1c688ce2999cfa2d9480be3df45fbb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 17 Jun 2024 19:16:23 +0800 Subject: [PATCH 1083/2493] Record on single line --- .../OnlinePlay/DailyChallenge/DailyChallengeEventFeed.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeEventFeed.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeEventFeed.cs index 6ddd2f1278..b415b15b65 100644 --- a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeEventFeed.cs +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeEventFeed.cs @@ -70,9 +70,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge } } - public record NewScoreEvent( - IScoreInfo Score, - int? NewRank); + public record NewScoreEvent(IScoreInfo Score, int? NewRank); private partial class DailyChallengeEventFeedFlow : FillFlowContainer { From b5f0e585245b1ec4993a1cb208e09bbeabccdf1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 17 Jun 2024 15:06:17 +0200 Subject: [PATCH 1084/2493] Add ability to better control slider control point type during placement via `Tab` --- .../Sliders/SliderPlacementBlueprint.cs | 83 +++++++++++++++---- 1 file changed, 69 insertions(+), 14 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs index 0fa84c91fc..f21a1279e5 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs @@ -40,6 +40,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders private PathControlPoint segmentStart; private PathControlPoint cursor; private int currentSegmentLength; + private bool usingCustomSegmentType; [Resolved(CanBeNull = true)] [CanBeNull] @@ -149,21 +150,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders case SliderPlacementState.ControlPoints: if (canPlaceNewControlPoint(out var lastPoint)) - { - // Place a new point by detatching the current cursor. - updateCursor(); - cursor = null; - } + placeNewControlPoint(); else - { - // Transform the last point into a new segment. - Debug.Assert(lastPoint != null); - - segmentStart = lastPoint; - segmentStart.Type = PathType.LINEAR; - - currentSegmentLength = 1; - } + beginNewSegment(lastPoint); break; } @@ -171,6 +160,18 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders return true; } + private void beginNewSegment(PathControlPoint lastPoint) + { + // Transform the last point into a new segment. + Debug.Assert(lastPoint != null); + + segmentStart = lastPoint; + segmentStart.Type = PathType.LINEAR; + + currentSegmentLength = 1; + usingCustomSegmentType = false; + } + protected override bool OnDragStart(DragStartEvent e) { if (e.Button != MouseButton.Left) @@ -223,6 +224,47 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders base.OnMouseUp(e); } + private static readonly PathType[] path_types = + [ + PathType.LINEAR, + PathType.BEZIER, + PathType.PERFECT_CURVE, + PathType.BSpline(4), + ]; + + protected override bool OnKeyDown(KeyDownEvent e) + { + if (e.Repeat) + return false; + + if (state != SliderPlacementState.ControlPoints) + return false; + + switch (e.Key) + { + case Key.Tab: + { + usingCustomSegmentType = true; + + int currentTypeIndex = segmentStart.Type.HasValue ? Array.IndexOf(path_types, segmentStart.Type.Value) : -1; + + if (currentTypeIndex < 0 && e.ShiftPressed) + currentTypeIndex = 0; + + do + { + currentTypeIndex = (path_types.Length + currentTypeIndex + (e.ShiftPressed ? -1 : 1)) % path_types.Length; + segmentStart.Type = path_types[currentTypeIndex]; + controlPointVisualiser.EnsureValidPathTypes(); + } while (segmentStart.Type != path_types[currentTypeIndex]); + + return true; + } + } + + return true; + } + protected override void Update() { base.Update(); @@ -246,6 +288,12 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders private void updatePathType() { + if (usingCustomSegmentType) + { + controlPointVisualiser.EnsureValidPathTypes(); + return; + } + if (state == SliderPlacementState.Drawing) { segmentStart.Type = PathType.BSpline(4); @@ -316,6 +364,13 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders return lastPiece.IsHovered != true; } + private void placeNewControlPoint() + { + // Place a new point by detatching the current cursor. + updateCursor(); + cursor = null; + } + private void updateSlider() { if (state == SliderPlacementState.Drawing) From 16ea8f67b00b88fd714c3aa87b42987836a8a5cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 17 Jun 2024 15:06:27 +0200 Subject: [PATCH 1085/2493] Add ability to start a new segment during placement via `S` key --- .../Blueprints/Sliders/SliderPlacementBlueprint.cs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs index f21a1279e5..7fac95ab91 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs @@ -242,6 +242,17 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders switch (e.Key) { + case Key.S: + { + if (!canPlaceNewControlPoint(out _)) + return false; + + placeNewControlPoint(); + var last = HitObject.Path.ControlPoints.Last(p => p != cursor); + beginNewSegment(last); + return true; + } + case Key.Tab: { usingCustomSegmentType = true; From 88bdc12022b3bb90e4b70877450c8dc5a0b57d5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 17 Jun 2024 15:28:52 +0200 Subject: [PATCH 1086/2493] Add ability to cycle through available types when selecting single control point on a slider --- .../Components/PathControlPointVisualiser.cs | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs index 47af16ffa6..2bdef4afe8 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs @@ -245,6 +245,43 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components { } + protected override bool OnKeyDown(KeyDownEvent e) + { + if (e.Repeat || e.Key != Key.Tab) + return false; + + var selectedPieces = Pieces.Where(p => p.IsSelected.Value).ToArray(); + if (selectedPieces.Length != 1) + return false; + + var selectedPoint = selectedPieces.Single().ControlPoint; + var validTypes = getValidPathTypes(selectedPoint).ToArray(); + int currentTypeIndex = Array.IndexOf(validTypes, selectedPoint.Type); + + if (currentTypeIndex < 0 && e.ShiftPressed) + currentTypeIndex = 0; + + do + { + currentTypeIndex = (validTypes.Length + currentTypeIndex + (e.ShiftPressed ? -1 : 1)) % validTypes.Length; + selectedPoint.Type = validTypes[currentTypeIndex]; + EnsureValidPathTypes(); + } while (selectedPoint.Type != validTypes[currentTypeIndex]); + + return true; + + IEnumerable getValidPathTypes(PathControlPoint pathControlPoint) + { + if (pathControlPoint != controlPoints[0]) + yield return null; + + yield return PathType.LINEAR; + yield return PathType.BEZIER; + yield return PathType.PERFECT_CURVE; + yield return PathType.BSpline(4); + } + } + private void selectionRequested(PathControlPointPiece piece, MouseButtonEvent e) { if (e.Button == MouseButton.Left && inputManager.CurrentState.Keyboard.ControlPressed) From 1e137271abcb020276ec12020301fdd5a487b4cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 17 Jun 2024 15:47:22 +0200 Subject: [PATCH 1087/2493] Add testing for keyboard control of path during placement --- .../TestSceneSliderPlacementBlueprint.cs | 106 +++++++++++++----- 1 file changed, 75 insertions(+), 31 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs index bbded55732..bc1e4f9864 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs @@ -2,13 +2,16 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Linq; using NUnit.Framework; +using osu.Framework.Testing; using osu.Framework.Utils; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders; +using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Tests.Visual; @@ -57,7 +60,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor assertPlaced(true); assertLength(200); assertControlPointCount(2); - assertControlPointType(0, PathType.LINEAR); + assertFinalControlPointType(0, PathType.LINEAR); } [Test] @@ -71,7 +74,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor assertPlaced(true); assertControlPointCount(2); - assertControlPointType(0, PathType.LINEAR); + assertFinalControlPointType(0, PathType.LINEAR); } [Test] @@ -89,7 +92,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor assertPlaced(true); assertControlPointCount(3); assertControlPointPosition(1, new Vector2(100, 0)); - assertControlPointType(0, PathType.PERFECT_CURVE); + assertFinalControlPointType(0, PathType.PERFECT_CURVE); } [Test] @@ -111,7 +114,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor assertControlPointCount(4); assertControlPointPosition(1, new Vector2(100, 0)); assertControlPointPosition(2, new Vector2(100, 100)); - assertControlPointType(0, PathType.BEZIER); + assertFinalControlPointType(0, PathType.BEZIER); } [Test] @@ -130,8 +133,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor assertPlaced(true); assertControlPointCount(3); assertControlPointPosition(1, new Vector2(100, 0)); - assertControlPointType(0, PathType.LINEAR); - assertControlPointType(1, PathType.LINEAR); + assertFinalControlPointType(0, PathType.LINEAR); + assertFinalControlPointType(1, PathType.LINEAR); } [Test] @@ -149,7 +152,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor assertPlaced(true); assertControlPointCount(2); - assertControlPointType(0, PathType.LINEAR); + assertFinalControlPointType(0, PathType.LINEAR); assertLength(100); } @@ -171,7 +174,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor assertPlaced(true); assertControlPointCount(3); - assertControlPointType(0, PathType.PERFECT_CURVE); + assertFinalControlPointType(0, PathType.PERFECT_CURVE); } [Test] @@ -195,7 +198,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor assertPlaced(true); assertControlPointCount(4); - assertControlPointType(0, PathType.BEZIER); + assertFinalControlPointType(0, PathType.BEZIER); } [Test] @@ -215,8 +218,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor assertControlPointCount(3); assertControlPointPosition(1, new Vector2(100, 0)); assertControlPointPosition(2, new Vector2(100)); - assertControlPointType(0, PathType.LINEAR); - assertControlPointType(1, PathType.LINEAR); + assertFinalControlPointType(0, PathType.LINEAR); + assertFinalControlPointType(1, PathType.LINEAR); } [Test] @@ -239,8 +242,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor assertControlPointCount(4); assertControlPointPosition(1, new Vector2(100, 0)); assertControlPointPosition(2, new Vector2(100)); - assertControlPointType(0, PathType.LINEAR); - assertControlPointType(1, PathType.PERFECT_CURVE); + assertFinalControlPointType(0, PathType.LINEAR); + assertFinalControlPointType(1, PathType.PERFECT_CURVE); } [Test] @@ -268,8 +271,46 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor assertControlPointPosition(2, new Vector2(100)); assertControlPointPosition(3, new Vector2(200, 100)); assertControlPointPosition(4, new Vector2(200)); - assertControlPointType(0, PathType.PERFECT_CURVE); - assertControlPointType(2, PathType.PERFECT_CURVE); + assertFinalControlPointType(0, PathType.PERFECT_CURVE); + assertFinalControlPointType(2, PathType.PERFECT_CURVE); + } + + [Test] + public void TestManualPathTypeControlViaKeyboard() + { + addMovementStep(new Vector2(200)); + addClickStep(MouseButton.Left); + + addMovementStep(new Vector2(300, 200)); + addClickStep(MouseButton.Left); + + addMovementStep(new Vector2(300)); + + assertControlPointTypeDuringPlacement(0, PathType.PERFECT_CURVE); + + AddRepeatStep("press tab", () => InputManager.Key(Key.Tab), 2); + assertControlPointTypeDuringPlacement(0, PathType.LINEAR); + + AddStep("press shift-tab", () => + { + InputManager.PressKey(Key.ShiftLeft); + InputManager.Key(Key.Tab); + InputManager.ReleaseKey(Key.ShiftLeft); + }); + assertControlPointTypeDuringPlacement(0, PathType.BSpline(4)); + + AddStep("start new segment via S", () => InputManager.Key(Key.S)); + assertControlPointTypeDuringPlacement(2, PathType.LINEAR); + + addMovementStep(new Vector2(400, 300)); + addClickStep(MouseButton.Left); + + addMovementStep(new Vector2(400)); + addClickStep(MouseButton.Right); + + assertPlaced(true); + assertFinalControlPointType(0, PathType.BSpline(4)); + assertFinalControlPointType(2, PathType.PERFECT_CURVE); } [Test] @@ -293,7 +334,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor addClickStep(MouseButton.Right); assertPlaced(true); - assertControlPointType(0, PathType.BEZIER); + assertFinalControlPointType(0, PathType.BEZIER); } [Test] @@ -312,11 +353,11 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor assertPlaced(true); assertLength(808, tolerance: 10); assertControlPointCount(5); - assertControlPointType(0, PathType.BSpline(4)); - assertControlPointType(1, null); - assertControlPointType(2, null); - assertControlPointType(3, null); - assertControlPointType(4, null); + assertFinalControlPointType(0, PathType.BSpline(4)); + assertFinalControlPointType(1, null); + assertFinalControlPointType(2, null); + assertFinalControlPointType(3, null); + assertFinalControlPointType(4, null); } [Test] @@ -337,10 +378,10 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor assertPlaced(true); assertLength(600, tolerance: 10); assertControlPointCount(4); - assertControlPointType(0, PathType.BSpline(4)); - assertControlPointType(1, PathType.BSpline(4)); - assertControlPointType(2, PathType.BSpline(4)); - assertControlPointType(3, null); + assertFinalControlPointType(0, PathType.BSpline(4)); + assertFinalControlPointType(1, PathType.BSpline(4)); + assertFinalControlPointType(2, PathType.BSpline(4)); + assertFinalControlPointType(3, null); } [Test] @@ -359,7 +400,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor assertPlaced(true); assertControlPointCount(3); - assertControlPointType(0, PathType.BEZIER); + assertFinalControlPointType(0, PathType.BEZIER); } [Test] @@ -379,7 +420,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor assertPlaced(true); assertControlPointCount(3); - assertControlPointType(0, PathType.PERFECT_CURVE); + assertFinalControlPointType(0, PathType.PERFECT_CURVE); } [Test] @@ -400,7 +441,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor assertPlaced(true); assertControlPointCount(3); - assertControlPointType(0, PathType.PERFECT_CURVE); + assertFinalControlPointType(0, PathType.PERFECT_CURVE); } [Test] @@ -421,7 +462,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor assertPlaced(true); assertControlPointCount(3); - assertControlPointType(0, PathType.BEZIER); + assertFinalControlPointType(0, PathType.BEZIER); } [Test] @@ -438,7 +479,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor assertPlaced(true); assertControlPointCount(3); - assertControlPointType(0, PathType.PERFECT_CURVE); + assertFinalControlPointType(0, PathType.PERFECT_CURVE); } private void addMovementStep(Vector2 position) => AddStep($"move mouse to {position}", () => InputManager.MoveMouseTo(InputManager.ToScreenSpace(position))); @@ -454,7 +495,10 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor private void assertControlPointCount(int expected) => AddAssert($"has {expected} control points", () => getSlider()!.Path.ControlPoints.Count, () => Is.EqualTo(expected)); - private void assertControlPointType(int index, PathType? type) => AddAssert($"control point {index} is {type?.ToString() ?? "inherit"}", () => getSlider()!.Path.ControlPoints[index].Type, () => Is.EqualTo(type)); + private void assertControlPointTypeDuringPlacement(int index, PathType? type) => AddAssert($"control point {index} is {type?.ToString() ?? "inherit"}", + () => this.ChildrenOfType>().ElementAt(index).ControlPoint.Type, () => Is.EqualTo(type)); + + private void assertFinalControlPointType(int index, PathType? type) => AddAssert($"control point {index} is {type?.ToString() ?? "inherit"}", () => getSlider()!.Path.ControlPoints[index].Type, () => Is.EqualTo(type)); private void assertControlPointPosition(int index, Vector2 position) => AddAssert($"control point {index} at {position}", () => Precision.AlmostEquals(position, getSlider()!.Path.ControlPoints[index].Position, 1)); From 789810069858456380b52403a3de6fde6d065b28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 17 Jun 2024 15:54:45 +0200 Subject: [PATCH 1088/2493] Add testing for keyboard control of path during selection --- .../TestScenePathControlPointVisualiser.cs | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePathControlPointVisualiser.cs index 9af028fd8c..4813cc089c 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePathControlPointVisualiser.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePathControlPointVisualiser.cs @@ -15,6 +15,7 @@ using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Tests.Visual; using osuTK; +using osuTK.Input; namespace osu.Game.Rulesets.Osu.Tests.Editor { @@ -177,6 +178,60 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor addAssertPointPositionChanged(points, i); } + [Test] + public void TestChangingControlPointTypeViaTab() + { + createVisualiser(true); + + addControlPointStep(new Vector2(200), PathType.LINEAR); + addControlPointStep(new Vector2(300)); + addControlPointStep(new Vector2(500, 300)); + addControlPointStep(new Vector2(700, 200)); + addControlPointStep(new Vector2(500, 100)); + + AddStep("select first control point", () => visualiser.Pieces[0].IsSelected.Value = true); + AddStep("press tab", () => InputManager.Key(Key.Tab)); + assertControlPointPathType(0, PathType.BEZIER); + + AddStep("press shift-tab", () => + { + InputManager.PressKey(Key.LShift); + InputManager.Key(Key.Tab); + InputManager.ReleaseKey(Key.LShift); + }); + assertControlPointPathType(0, PathType.LINEAR); + + AddStep("press shift-tab", () => + { + InputManager.PressKey(Key.LShift); + InputManager.Key(Key.Tab); + InputManager.ReleaseKey(Key.LShift); + }); + assertControlPointPathType(0, PathType.BSpline(4)); + + AddStep("press shift-tab", () => + { + InputManager.PressKey(Key.LShift); + InputManager.Key(Key.Tab); + InputManager.ReleaseKey(Key.LShift); + }); + assertControlPointPathType(0, PathType.BEZIER); + + AddStep("select third last control point", () => + { + visualiser.Pieces[0].IsSelected.Value = false; + visualiser.Pieces[2].IsSelected.Value = true; + }); + AddRepeatStep("press tab", () => InputManager.Key(Key.Tab), 3); + assertControlPointPathType(2, PathType.PERFECT_CURVE); + + AddStep("press tab", () => InputManager.Key(Key.Tab)); + assertControlPointPathType(2, PathType.BSpline(4)); + + AddStep("press tab", () => InputManager.Key(Key.Tab)); + assertControlPointPathType(2, null); + } + private void addAssertPointPositionChanged(Vector2[] points, int index) { AddAssert($"Point at {points.ElementAt(index)} changed", From 5652a558f98d2eca210901303bcb190f7e1eccd4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 17 Jun 2024 17:07:47 +0200 Subject: [PATCH 1089/2493] Allow to jump to a specific timestamp via bottom bar in editor Apparently this is a stable feature and is helpful for modding. --- .../Edit/Components/TimeInfoContainer.cs | 116 +++++++++++++++--- 1 file changed, 100 insertions(+), 16 deletions(-) diff --git a/osu.Game/Screens/Edit/Components/TimeInfoContainer.cs b/osu.Game/Screens/Edit/Components/TimeInfoContainer.cs index 4747828bca..b0e0d95132 100644 --- a/osu.Game/Screens/Edit/Components/TimeInfoContainer.cs +++ b/osu.Game/Screens/Edit/Components/TimeInfoContainer.cs @@ -1,11 +1,17 @@ // 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.Globalization; using osu.Framework.Graphics; using osu.Game.Graphics.Sprites; using osu.Framework.Allocation; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; using osu.Game.Extensions; using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.UserInterface; using osu.Game.Overlays; using osuTK; @@ -13,7 +19,6 @@ namespace osu.Game.Screens.Edit.Components { public partial class TimeInfoContainer : BottomBarContainer { - private OsuSpriteText trackTimer = null!; private OsuSpriteText bpm = null!; [Resolved] @@ -29,14 +34,7 @@ namespace osu.Game.Screens.Edit.Components Children = new Drawable[] { - trackTimer = new OsuSpriteText - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Spacing = new Vector2(-2, 0), - Font = OsuFont.Torus.With(size: 36, fixedWidth: true, weight: FontWeight.Light), - Y = -10, - }, + new TimestampControl(), bpm = new OsuSpriteText { Colour = colours.Orange1, @@ -47,19 +45,12 @@ namespace osu.Game.Screens.Edit.Components }; } - private double? lastTime; private double? lastBPM; protected override void Update() { base.Update(); - if (lastTime != editorClock.CurrentTime) - { - lastTime = editorClock.CurrentTime; - trackTimer.Text = editorClock.CurrentTime.ToEditorFormattedString(); - } - double newBPM = editorBeatmap.ControlPointInfo.TimingPointAt(editorClock.CurrentTime).BPM; if (lastBPM != newBPM) @@ -68,5 +59,98 @@ namespace osu.Game.Screens.Edit.Components bpm.Text = @$"{newBPM:0} BPM"; } } + + private partial class TimestampControl : OsuClickableContainer + { + private Container hoverLayer = null!; + private OsuSpriteText trackTimer = null!; + private OsuTextBox inputTextBox = null!; + + [Resolved] + private EditorClock editorClock { get; set; } = null!; + + public TimestampControl() + : base(HoverSampleSet.Button) + { + } + + [BackgroundDependencyLoader] + private void load() + { + AutoSizeAxes = Axes.Both; + + AddRangeInternal(new Drawable[] + { + hoverLayer = new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding + { + Top = 5, + Horizontal = -5 + }, + Child = new Box { RelativeSizeAxes = Axes.Both, }, + Alpha = 0, + }, + trackTimer = new OsuSpriteText + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Spacing = new Vector2(-2, 0), + Font = OsuFont.Torus.With(size: 36, fixedWidth: true, weight: FontWeight.Light), + }, + inputTextBox = new OsuTextBox + { + Width = 150, + Height = 36, + Alpha = 0, + CommitOnFocusLost = true, + }, + }); + + Action = () => + { + trackTimer.Alpha = 0; + inputTextBox.Alpha = 1; + inputTextBox.Text = editorClock.CurrentTime.ToEditorFormattedString(); + Schedule(() => + { + GetContainingFocusManager().ChangeFocus(inputTextBox); + inputTextBox.SelectAll(); + }); + }; + + inputTextBox.OnCommit += (_, __) => + { + if (TimeSpan.TryParseExact(inputTextBox.Text, @"mm\:ss\:fff", CultureInfo.InvariantCulture, out var timestamp)) + editorClock.SeekSmoothlyTo(timestamp.TotalMilliseconds); + + trackTimer.Alpha = 1; + inputTextBox.Alpha = 0; + }; + } + + private double? lastTime; + private bool showingHoverLayer; + + protected override void Update() + { + base.Update(); + + if (lastTime != editorClock.CurrentTime) + { + lastTime = editorClock.CurrentTime; + trackTimer.Text = editorClock.CurrentTime.ToEditorFormattedString(); + } + + bool shouldShowHoverLayer = IsHovered && inputTextBox.Alpha == 0; + + if (shouldShowHoverLayer != showingHoverLayer) + { + hoverLayer.FadeTo(shouldShowHoverLayer ? 0.2f : 0, 400, Easing.OutQuint); + showingHoverLayer = shouldShowHoverLayer; + } + } + } } } From a631d245daa18d0cc2e6cf239ad9e0f36e9a8864 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 17 Jun 2024 18:14:33 +0200 Subject: [PATCH 1090/2493] Fix test failure --- .../Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs index 7fac95ab91..fdfb52008c 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs @@ -273,7 +273,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders } } - return true; + return false; } protected override void Update() From 683d5310b14c6b366b8b29ceaf20dffd1bcc775b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 17 Jun 2024 18:33:36 +0200 Subject: [PATCH 1091/2493] Implement direct choice of slider control point path type via `Alt`-number --- .../Components/PathControlPointVisualiser.cs | 123 +++++++++++------- .../Sliders/SliderPlacementBlueprint.cs | 14 ++ 2 files changed, 91 insertions(+), 46 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs index 2bdef4afe8..3d6e529afa 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs @@ -245,40 +245,73 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components { } + // ReSharper disable once StaticMemberInGenericType + private static readonly PathType?[] path_types = + [ + null, + PathType.LINEAR, + PathType.BEZIER, + PathType.PERFECT_CURVE, + PathType.BSpline(4), + ]; + protected override bool OnKeyDown(KeyDownEvent e) { - if (e.Repeat || e.Key != Key.Tab) + if (e.Repeat) return false; var selectedPieces = Pieces.Where(p => p.IsSelected.Value).ToArray(); if (selectedPieces.Length != 1) return false; - var selectedPoint = selectedPieces.Single().ControlPoint; - var validTypes = getValidPathTypes(selectedPoint).ToArray(); - int currentTypeIndex = Array.IndexOf(validTypes, selectedPoint.Type); + var selectedPiece = selectedPieces.Single(); + var selectedPoint = selectedPiece.ControlPoint; - if (currentTypeIndex < 0 && e.ShiftPressed) - currentTypeIndex = 0; - - do + switch (e.Key) { - currentTypeIndex = (validTypes.Length + currentTypeIndex + (e.ShiftPressed ? -1 : 1)) % validTypes.Length; - selectedPoint.Type = validTypes[currentTypeIndex]; - EnsureValidPathTypes(); - } while (selectedPoint.Type != validTypes[currentTypeIndex]); + case Key.Tab: + { + var validTypes = path_types; - return true; + if (selectedPoint == controlPoints[0]) + validTypes = validTypes.Where(t => t != null).ToArray(); - IEnumerable getValidPathTypes(PathControlPoint pathControlPoint) - { - if (pathControlPoint != controlPoints[0]) - yield return null; + int currentTypeIndex = Array.IndexOf(validTypes, selectedPoint.Type); - yield return PathType.LINEAR; - yield return PathType.BEZIER; - yield return PathType.PERFECT_CURVE; - yield return PathType.BSpline(4); + if (currentTypeIndex < 0 && e.ShiftPressed) + currentTypeIndex = 0; + + changeHandler?.BeginChange(); + + do + { + currentTypeIndex = (validTypes.Length + currentTypeIndex + (e.ShiftPressed ? -1 : 1)) % validTypes.Length; + + updatePathTypeOfSelectedPieces(validTypes[currentTypeIndex]); + } while (selectedPoint.Type != validTypes[currentTypeIndex]); + + changeHandler?.EndChange(); + + return true; + } + + case Key.Number0: + case Key.Number1: + case Key.Number2: + case Key.Number3: + case Key.Number4: + { + var type = path_types[e.Key - Key.Number0]; + + if (selectedPoint == controlPoints[0] && type == null) + return false; + + updatePathTypeOfSelectedPieces(type); + return true; + } + + default: + return false; } } @@ -291,30 +324,38 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components } /// - /// Attempts to set the given control point piece to the given path type. + /// Attempts to set all selected control point pieces to the given path type. /// If that would fail, try to change the path such that it instead succeeds /// in a UX-friendly way. /// - /// The control point piece that we want to change the path type of. /// The path type we want to assign to the given control point piece. - private void updatePathType(PathControlPointPiece piece, PathType? type) + private void updatePathTypeOfSelectedPieces(PathType? type) { - var pointsInSegment = hitObject.Path.PointsInSegment(piece.ControlPoint); - int indexInSegment = pointsInSegment.IndexOf(piece.ControlPoint); + changeHandler?.BeginChange(); - if (type?.Type == SplineType.PerfectCurve) + foreach (var p in Pieces.Where(p => p.IsSelected.Value)) { - // Can't always create a circular arc out of 4 or more points, - // so we split the segment into one 3-point circular arc segment - // and one segment of the previous type. - int thirdPointIndex = indexInSegment + 2; + var pointsInSegment = hitObject.Path.PointsInSegment(p.ControlPoint); + int indexInSegment = pointsInSegment.IndexOf(p.ControlPoint); - if (pointsInSegment.Count > thirdPointIndex + 1) - pointsInSegment[thirdPointIndex].Type = pointsInSegment[0].Type; + if (type?.Type == SplineType.PerfectCurve) + { + // Can't always create a circular arc out of 4 or more points, + // so we split the segment into one 3-point circular arc segment + // and one segment of the previous type. + int thirdPointIndex = indexInSegment + 2; + + if (pointsInSegment.Count > thirdPointIndex + 1) + pointsInSegment[thirdPointIndex].Type = pointsInSegment[0].Type; + } + + hitObject.Path.ExpectedDistance.Value = null; + p.ControlPoint.Type = type; } - hitObject.Path.ExpectedDistance.Value = null; - piece.ControlPoint.Type = type; + EnsureValidPathTypes(); + + changeHandler?.EndChange(); } [Resolved(CanBeNull = true)] @@ -470,17 +511,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components int totalCount = Pieces.Count(p => p.IsSelected.Value); int countOfState = Pieces.Where(p => p.IsSelected.Value).Count(p => p.ControlPoint.Type == type); - var item = new TernaryStateRadioMenuItem(type?.Description ?? "Inherit", MenuItemType.Standard, _ => - { - changeHandler?.BeginChange(); - - foreach (var p in Pieces.Where(p => p.IsSelected.Value)) - updatePathType(p, type); - - EnsureValidPathTypes(); - - changeHandler?.EndChange(); - }); + var item = new TernaryStateRadioMenuItem(type?.Description ?? "Inherit", MenuItemType.Standard, _ => updatePathTypeOfSelectedPieces(type)); if (countOfState == totalCount) item.State.Value = TernaryState.True; diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs index fdfb52008c..91cd270af6 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs @@ -253,6 +253,20 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders return true; } + case Key.Number1: + case Key.Number2: + case Key.Number3: + case Key.Number4: + { + if (!e.AltPressed) + return false; + + usingCustomSegmentType = true; + segmentStart.Type = path_types[e.Key - Key.Number1]; + controlPointVisualiser.EnsureValidPathTypes(); + return true; + } + case Key.Tab: { usingCustomSegmentType = true; From da4160439e0113d2e5e4df6eed2d30f8f73ef794 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 18 Jun 2024 07:26:20 +0200 Subject: [PATCH 1092/2493] 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 3a20dd2fdb..3c115d1371 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -10,7 +10,7 @@ true - + diff --git a/osu.iOS.props b/osu.iOS.props index 2f64fcefa5..449e4b0032 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -23,6 +23,6 @@ iossimulator-x64 - + From 7f080080597094e9c8a49619d677369174381b24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 18 Jun 2024 07:27:54 +0200 Subject: [PATCH 1093/2493] Adjust `AudioFilter` to framework-side changes Co-authored-by: Dan Balasescu --- osu.Game/Audio/Effects/AudioFilter.cs | 44 ++++++--------------------- 1 file changed, 10 insertions(+), 34 deletions(-) diff --git a/osu.Game/Audio/Effects/AudioFilter.cs b/osu.Game/Audio/Effects/AudioFilter.cs index bfa9b31242..8db457ae67 100644 --- a/osu.Game/Audio/Effects/AudioFilter.cs +++ b/osu.Game/Audio/Effects/AudioFilter.cs @@ -1,10 +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.Diagnostics; using ManagedBass.Fx; using osu.Framework.Audio.Mixing; -using osu.Framework.Caching; using osu.Framework.Graphics; namespace osu.Game.Audio.Effects @@ -26,8 +24,6 @@ namespace osu.Game.Audio.Effects private readonly BQFParameters filter; private readonly BQFType type; - private readonly Cached filterApplication = new Cached(); - private int cutoff; /// @@ -42,7 +38,7 @@ namespace osu.Game.Audio.Effects return; cutoff = value; - filterApplication.Invalidate(); + updateFilter(); } } @@ -64,18 +60,9 @@ namespace osu.Game.Audio.Effects fQ = 0.7f }; - Cutoff = getInitialCutoff(type); - } + cutoff = getInitialCutoff(type); - protected override void Update() - { - base.Update(); - - if (!filterApplication.IsValid) - { - updateFilter(cutoff); - filterApplication.Validate(); - } + updateFilter(); } private int getInitialCutoff(BQFType type) @@ -93,13 +80,13 @@ namespace osu.Game.Audio.Effects } } - private void updateFilter(int newValue) + private void updateFilter() { switch (type) { case BQFType.LowPass: // Workaround for weird behaviour when rapidly setting fCenter of a low-pass filter to nyquist - 1hz. - if (newValue >= MAX_LOWPASS_CUTOFF) + if (Cutoff >= MAX_LOWPASS_CUTOFF) { ensureDetached(); return; @@ -109,7 +96,7 @@ namespace osu.Game.Audio.Effects // Workaround for weird behaviour when rapidly setting fCenter of a high-pass filter to 1hz. case BQFType.HighPass: - if (newValue <= 1) + if (Cutoff <= 1) { ensureDetached(); return; @@ -120,17 +107,8 @@ namespace osu.Game.Audio.Effects ensureAttached(); - int filterIndex = mixer.Effects.IndexOf(filter); - - if (filterIndex < 0) return; - - if (mixer.Effects[filterIndex] is BQFParameters existingFilter) - { - existingFilter.fCenter = newValue; - - // required to update effect with new parameters. - mixer.Effects[filterIndex] = existingFilter; - } + filter.fCenter = Cutoff; + mixer.UpdateEffect(filter); } private void ensureAttached() @@ -138,8 +116,7 @@ namespace osu.Game.Audio.Effects if (IsAttached) return; - Debug.Assert(!mixer.Effects.Contains(filter)); - mixer.Effects.Add(filter); + mixer.AddEffect(filter); IsAttached = true; } @@ -148,8 +125,7 @@ namespace osu.Game.Audio.Effects if (!IsAttached) return; - Debug.Assert(mixer.Effects.Contains(filter)); - mixer.Effects.Remove(filter); + mixer.RemoveEffect(filter); IsAttached = false; } From 8a4ae5d23d902c52d5a1e0075849cc295e7e565a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 27 May 2024 11:02:51 +0200 Subject: [PATCH 1094/2493] Null-propagate all calls to `GetContainingFocusManager()` --- .../Screens/Ladder/Components/LadderEditorSettings.cs | 2 +- osu.Game/Graphics/UserInterface/FocusedTextBox.cs | 2 +- osu.Game/Graphics/UserInterfaceV2/LabelledTextBox.cs | 2 +- osu.Game/Graphics/UserInterfaceV2/SliderWithTextBoxInput.cs | 2 +- osu.Game/Overlays/AccountCreation/ScreenEntry.cs | 2 +- osu.Game/Overlays/Comments/ReplyCommentEditor.cs | 2 +- osu.Game/Overlays/Login/LoginForm.cs | 2 +- osu.Game/Overlays/Login/LoginPanel.cs | 2 +- osu.Game/Overlays/Login/SecondFactorAuthForm.cs | 2 +- osu.Game/Overlays/LoginOverlay.cs | 2 +- osu.Game/Overlays/Mods/AddPresetPopover.cs | 2 +- osu.Game/Overlays/Mods/EditPresetPopover.cs | 2 +- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 2 +- osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs | 2 +- .../Settings/Sections/Input/KeyBindingsSubsection.cs | 2 +- osu.Game/Overlays/SettingsPanel.cs | 2 +- .../Screens/Edit/Compose/Components/BeatDivisorControl.cs | 2 +- .../Compose/Components/Timeline/DifficultyPointPiece.cs | 2 +- osu.Game/Screens/Edit/Setup/LabelledTextBoxWithPopover.cs | 2 +- osu.Game/Screens/Edit/Setup/MetadataSection.cs | 2 +- .../Edit/Timing/IndeterminateSliderWithTextBoxInput.cs | 2 +- osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs | 6 +++--- osu.Game/Screens/Select/FilterControl.cs | 2 +- osu.Game/Screens/SelectV2/Footer/BeatmapOptionsPopover.cs | 2 +- 24 files changed, 26 insertions(+), 26 deletions(-) diff --git a/osu.Game.Tournament/Screens/Ladder/Components/LadderEditorSettings.cs b/osu.Game.Tournament/Screens/Ladder/Components/LadderEditorSettings.cs index 08ed815253..775fd4fdf2 100644 --- a/osu.Game.Tournament/Screens/Ladder/Components/LadderEditorSettings.cs +++ b/osu.Game.Tournament/Screens/Ladder/Components/LadderEditorSettings.cs @@ -58,7 +58,7 @@ namespace osu.Game.Tournament.Screens.Ladder.Components editorInfo.Selected.ValueChanged += selection => { // ensure any ongoing edits are committed out to the *current* selection before changing to a new one. - GetContainingFocusManager().TriggerFocusContention(null); + GetContainingFocusManager()?.TriggerFocusContention(null); // Required to avoid cyclic failure in BindableWithCurrent (TriggerChange called during the Current_Set process). // Arguable a framework issue but since we haven't hit it anywhere else a local workaround seems best. diff --git a/osu.Game/Graphics/UserInterface/FocusedTextBox.cs b/osu.Game/Graphics/UserInterface/FocusedTextBox.cs index 4ec93995a4..928865ffb0 100644 --- a/osu.Game/Graphics/UserInterface/FocusedTextBox.cs +++ b/osu.Game/Graphics/UserInterface/FocusedTextBox.cs @@ -31,7 +31,7 @@ namespace osu.Game.Graphics.UserInterface if (!allowImmediateFocus) return; - Scheduler.Add(() => GetContainingFocusManager().ChangeFocus(this)); + Scheduler.Add(() => GetContainingFocusManager()?.ChangeFocus(this)); } public new void KillFocus() => base.KillFocus(); diff --git a/osu.Game/Graphics/UserInterfaceV2/LabelledTextBox.cs b/osu.Game/Graphics/UserInterfaceV2/LabelledTextBox.cs index 8dfe729ce7..42a72744d6 100644 --- a/osu.Game/Graphics/UserInterfaceV2/LabelledTextBox.cs +++ b/osu.Game/Graphics/UserInterfaceV2/LabelledTextBox.cs @@ -59,7 +59,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 protected override void OnFocus(FocusEvent e) { base.OnFocus(e); - GetContainingFocusManager().ChangeFocus(Component); + GetContainingFocusManager()?.ChangeFocus(Component); } protected override OsuTextBox CreateComponent() => CreateTextBox().With(t => diff --git a/osu.Game/Graphics/UserInterfaceV2/SliderWithTextBoxInput.cs b/osu.Game/Graphics/UserInterfaceV2/SliderWithTextBoxInput.cs index f1f4fe3b46..50d8d763e1 100644 --- a/osu.Game/Graphics/UserInterfaceV2/SliderWithTextBoxInput.cs +++ b/osu.Game/Graphics/UserInterfaceV2/SliderWithTextBoxInput.cs @@ -85,7 +85,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 Current.BindValueChanged(updateTextBoxFromSlider, true); } - public bool TakeFocus() => GetContainingFocusManager().ChangeFocus(textBox); + public bool TakeFocus() => GetContainingFocusManager()?.ChangeFocus(textBox) == true; public bool SelectAll() => textBox.SelectAll(); diff --git a/osu.Game/Overlays/AccountCreation/ScreenEntry.cs b/osu.Game/Overlays/AccountCreation/ScreenEntry.cs index 53e51e0611..e34e5c9521 100644 --- a/osu.Game/Overlays/AccountCreation/ScreenEntry.cs +++ b/osu.Game/Overlays/AccountCreation/ScreenEntry.cs @@ -243,7 +243,7 @@ namespace osu.Game.Overlays.AccountCreation if (nextTextBox != null) { - Schedule(() => GetContainingFocusManager().ChangeFocus(nextTextBox)); + Schedule(() => GetContainingFocusManager()?.ChangeFocus(nextTextBox)); return true; } diff --git a/osu.Game/Overlays/Comments/ReplyCommentEditor.cs b/osu.Game/Overlays/Comments/ReplyCommentEditor.cs index caf19829ee..0b4daea0c1 100644 --- a/osu.Game/Overlays/Comments/ReplyCommentEditor.cs +++ b/osu.Game/Overlays/Comments/ReplyCommentEditor.cs @@ -39,7 +39,7 @@ namespace osu.Game.Overlays.Comments base.LoadComplete(); if (!TextBox.ReadOnly) - GetContainingFocusManager().ChangeFocus(TextBox); + GetContainingFocusManager()?.ChangeFocus(TextBox); } protected override void OnCommit(string text) diff --git a/osu.Game/Overlays/Login/LoginForm.cs b/osu.Game/Overlays/Login/LoginForm.cs index 418721f371..cde97d45c1 100644 --- a/osu.Game/Overlays/Login/LoginForm.cs +++ b/osu.Game/Overlays/Login/LoginForm.cs @@ -150,7 +150,7 @@ namespace osu.Game.Overlays.Login protected override void OnFocus(FocusEvent e) { - Schedule(() => { GetContainingFocusManager().ChangeFocus(string.IsNullOrEmpty(username.Text) ? username : password); }); + Schedule(() => { GetContainingFocusManager()?.ChangeFocus(string.IsNullOrEmpty(username.Text) ? username : password); }); } } } diff --git a/osu.Game/Overlays/Login/LoginPanel.cs b/osu.Game/Overlays/Login/LoginPanel.cs index 845d20ccaf..9afaed335d 100644 --- a/osu.Game/Overlays/Login/LoginPanel.cs +++ b/osu.Game/Overlays/Login/LoginPanel.cs @@ -216,7 +216,7 @@ namespace osu.Game.Overlays.Login protected override void OnFocus(FocusEvent e) { - if (form != null) GetContainingFocusManager().ChangeFocus(form); + if (form != null) GetContainingFocusManager()?.ChangeFocus(form); base.OnFocus(e); } } diff --git a/osu.Game/Overlays/Login/SecondFactorAuthForm.cs b/osu.Game/Overlays/Login/SecondFactorAuthForm.cs index 82e328c036..c8e8b316fa 100644 --- a/osu.Game/Overlays/Login/SecondFactorAuthForm.cs +++ b/osu.Game/Overlays/Login/SecondFactorAuthForm.cs @@ -141,7 +141,7 @@ namespace osu.Game.Overlays.Login protected override void OnFocus(FocusEvent e) { - Schedule(() => { GetContainingFocusManager().ChangeFocus(codeTextBox); }); + Schedule(() => { GetContainingFocusManager()?.ChangeFocus(codeTextBox); }); } } } diff --git a/osu.Game/Overlays/LoginOverlay.cs b/osu.Game/Overlays/LoginOverlay.cs index 8dc454c0a0..576c66ff23 100644 --- a/osu.Game/Overlays/LoginOverlay.cs +++ b/osu.Game/Overlays/LoginOverlay.cs @@ -78,7 +78,7 @@ namespace osu.Game.Overlays this.FadeIn(transition_time, Easing.OutQuint); FadeEdgeEffectTo(WaveContainer.SHADOW_OPACITY, WaveContainer.APPEAR_DURATION, Easing.Out); - ScheduleAfterChildren(() => GetContainingFocusManager().ChangeFocus(panel)); + ScheduleAfterChildren(() => GetContainingFocusManager()?.ChangeFocus(panel)); } protected override void PopOut() diff --git a/osu.Game/Overlays/Mods/AddPresetPopover.cs b/osu.Game/Overlays/Mods/AddPresetPopover.cs index 50aa5a2eb4..e59b60a1f1 100644 --- a/osu.Game/Overlays/Mods/AddPresetPopover.cs +++ b/osu.Game/Overlays/Mods/AddPresetPopover.cs @@ -89,7 +89,7 @@ namespace osu.Game.Overlays.Mods { base.LoadComplete(); - ScheduleAfterChildren(() => GetContainingFocusManager().ChangeFocus(nameTextBox)); + ScheduleAfterChildren(() => GetContainingFocusManager()?.ChangeFocus(nameTextBox)); nameTextBox.Current.BindValueChanged(s => { diff --git a/osu.Game/Overlays/Mods/EditPresetPopover.cs b/osu.Game/Overlays/Mods/EditPresetPopover.cs index 8fa6b35162..88119f57b3 100644 --- a/osu.Game/Overlays/Mods/EditPresetPopover.cs +++ b/osu.Game/Overlays/Mods/EditPresetPopover.cs @@ -136,7 +136,7 @@ namespace osu.Game.Overlays.Mods { base.LoadComplete(); - ScheduleAfterChildren(() => GetContainingFocusManager().ChangeFocus(nameTextBox)); + ScheduleAfterChildren(() => GetContainingFocusManager()?.ChangeFocus(nameTextBox)); } public override bool OnPressed(KeyBindingPressEvent e) diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index 0dccc88ea0..13970e718a 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -949,7 +949,7 @@ namespace osu.Game.Overlays.Mods RequestScroll?.Invoke(this); // Killing focus is done here because it's the only feasible place on ModSelectOverlay you can click on without triggering any action. - Scheduler.Add(() => GetContainingFocusManager().ChangeFocus(null)); + Scheduler.Add(() => GetContainingFocusManager()?.ChangeFocus(null)); return true; } diff --git a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs index 3f6eeca10e..36339c484e 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs @@ -465,7 +465,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input } if (HasFocus) - GetContainingFocusManager().ChangeFocus(null); + GetContainingFocusManager()?.ChangeFocus(null); cancelAndClearButtons.FadeOut(300, Easing.OutQuint); cancelAndClearButtons.BypassAutoSizeAxes |= Axes.Y; diff --git a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingsSubsection.cs b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingsSubsection.cs index db3b56b9f0..cde9f10549 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingsSubsection.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingsSubsection.cs @@ -106,7 +106,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input { var next = Children.SkipWhile(c => c != sender).Skip(1).FirstOrDefault(); if (next != null) - GetContainingFocusManager().ChangeFocus(next); + GetContainingFocusManager()?.ChangeFocus(next); } } } diff --git a/osu.Game/Overlays/SettingsPanel.cs b/osu.Game/Overlays/SettingsPanel.cs index d5c642d24f..a5a56d1e8b 100644 --- a/osu.Game/Overlays/SettingsPanel.cs +++ b/osu.Game/Overlays/SettingsPanel.cs @@ -201,7 +201,7 @@ namespace osu.Game.Overlays searchTextBox.HoldFocus = false; if (searchTextBox.HasFocus) - GetContainingFocusManager().ChangeFocus(null); + GetContainingFocusManager()?.ChangeFocus(null); } public override bool AcceptsFocus => true; diff --git a/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs b/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs index 005b96bfef..1751d01fb7 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs @@ -580,7 +580,7 @@ namespace osu.Game.Screens.Edit.Compose.Components { base.LoadComplete(); - GetContainingFocusManager().ChangeFocus(this); + GetContainingFocusManager()?.ChangeFocus(this); SelectAll(); } } diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/DifficultyPointPiece.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/DifficultyPointPiece.cs index d9084a7477..2c9da0446d 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/DifficultyPointPiece.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/DifficultyPointPiece.cs @@ -138,7 +138,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline protected override void LoadComplete() { base.LoadComplete(); - ScheduleAfterChildren(() => GetContainingFocusManager().ChangeFocus(sliderVelocitySlider)); + ScheduleAfterChildren(() => GetContainingFocusManager()?.ChangeFocus(sliderVelocitySlider)); } } } diff --git a/osu.Game/Screens/Edit/Setup/LabelledTextBoxWithPopover.cs b/osu.Game/Screens/Edit/Setup/LabelledTextBoxWithPopover.cs index 5abf40dda7..00cc07413f 100644 --- a/osu.Game/Screens/Edit/Setup/LabelledTextBoxWithPopover.cs +++ b/osu.Game/Screens/Edit/Setup/LabelledTextBoxWithPopover.cs @@ -45,7 +45,7 @@ namespace osu.Game.Screens.Edit.Setup OnFocused?.Invoke(); base.OnFocus(e); - GetContainingFocusManager().TriggerFocusContention(this); + GetContainingFocusManager()?.TriggerFocusContention(this); } } } diff --git a/osu.Game/Screens/Edit/Setup/MetadataSection.cs b/osu.Game/Screens/Edit/Setup/MetadataSection.cs index b575472a18..dd880891ba 100644 --- a/osu.Game/Screens/Edit/Setup/MetadataSection.cs +++ b/osu.Game/Screens/Edit/Setup/MetadataSection.cs @@ -70,7 +70,7 @@ namespace osu.Game.Screens.Edit.Setup base.LoadComplete(); if (string.IsNullOrEmpty(ArtistTextBox.Current.Value)) - ScheduleAfterChildren(() => GetContainingFocusManager().ChangeFocus(ArtistTextBox)); + ScheduleAfterChildren(() => GetContainingFocusManager()?.ChangeFocus(ArtistTextBox)); ArtistTextBox.Current.BindValueChanged(artist => transferIfRomanised(artist.NewValue, RomanisedArtistTextBox)); TitleTextBox.Current.BindValueChanged(title => transferIfRomanised(title.NewValue, RomanisedTitleTextBox)); diff --git a/osu.Game/Screens/Edit/Timing/IndeterminateSliderWithTextBoxInput.cs b/osu.Game/Screens/Edit/Timing/IndeterminateSliderWithTextBoxInput.cs index 4f7a1bf589..187aa3e897 100644 --- a/osu.Game/Screens/Edit/Timing/IndeterminateSliderWithTextBoxInput.cs +++ b/osu.Game/Screens/Edit/Timing/IndeterminateSliderWithTextBoxInput.cs @@ -126,7 +126,7 @@ namespace osu.Game.Screens.Edit.Timing protected override void OnFocus(FocusEvent e) { base.OnFocus(e); - GetContainingFocusManager().ChangeFocus(textBox); + GetContainingFocusManager()?.ChangeFocus(textBox); } private void updateState() diff --git a/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs b/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs index 2f6a220c82..6f06b8686c 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs @@ -248,21 +248,21 @@ namespace osu.Game.Screens.OnlinePlay.Lounge { base.LoadComplete(); - ScheduleAfterChildren(() => GetContainingFocusManager().ChangeFocus(passwordTextBox)); + ScheduleAfterChildren(() => GetContainingFocusManager()?.ChangeFocus(passwordTextBox)); passwordTextBox.OnCommit += (_, _) => performJoin(); } private void performJoin() { lounge?.Join(room, passwordTextBox.Text, null, joinFailed); - GetContainingFocusManager().TriggerFocusContention(passwordTextBox); + GetContainingFocusManager()?.TriggerFocusContention(passwordTextBox); } private void joinFailed(string error) => Schedule(() => { passwordTextBox.Text = string.Empty; - GetContainingFocusManager().ChangeFocus(passwordTextBox); + GetContainingFocusManager()?.ChangeFocus(passwordTextBox); errorText.Text = error; errorText diff --git a/osu.Game/Screens/Select/FilterControl.cs b/osu.Game/Screens/Select/FilterControl.cs index 30eb4a8491..497c1d4c9f 100644 --- a/osu.Game/Screens/Select/FilterControl.cs +++ b/osu.Game/Screens/Select/FilterControl.cs @@ -245,7 +245,7 @@ namespace osu.Game.Screens.Select searchTextBox.ReadOnly = true; searchTextBox.HoldFocus = false; if (searchTextBox.HasFocus) - GetContainingFocusManager().ChangeFocus(searchTextBox); + GetContainingFocusManager()?.ChangeFocus(searchTextBox); } public void Activate() diff --git a/osu.Game/Screens/SelectV2/Footer/BeatmapOptionsPopover.cs b/osu.Game/Screens/SelectV2/Footer/BeatmapOptionsPopover.cs index 4b5c0ee798..e8b8b49785 100644 --- a/osu.Game/Screens/SelectV2/Footer/BeatmapOptionsPopover.cs +++ b/osu.Game/Screens/SelectV2/Footer/BeatmapOptionsPopover.cs @@ -81,7 +81,7 @@ namespace osu.Game.Screens.SelectV2.Footer { base.LoadComplete(); - ScheduleAfterChildren(() => GetContainingFocusManager().ChangeFocus(this)); + ScheduleAfterChildren(() => GetContainingFocusManager()?.ChangeFocus(this)); beatmap.BindValueChanged(_ => Hide()); } From 659505f7115a8ba84ad88e0fead266cba8ef8021 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 27 May 2024 11:23:32 +0200 Subject: [PATCH 1095/2493] Adjust calls to `GetContainingInputManager()` --- .../Edit/Blueprints/JuiceStreamPlacementBlueprint.cs | 2 +- osu.Game.Rulesets.Catch/UI/CatcherArea.cs | 2 +- .../TestSceneDrawableManiaHitObject.cs | 2 +- osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs | 4 ++-- osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleArea.cs | 2 +- osu.Game.Tests/Visual/Editing/TestScenePositionSnapGrid.cs | 2 +- osu.Game/Graphics/Cursor/GlobalCursorDisplay.cs | 2 +- osu.Game/Graphics/UserInterface/FocusedTextBox.cs | 2 +- osu.Game/Graphics/UserInterfaceV2/LabelledTextBox.cs | 2 +- osu.Game/Overlays/AccountCreation/ScreenEntry.cs | 2 +- osu.Game/Overlays/Comments/ReplyCommentEditor.cs | 2 +- osu.Game/Overlays/Login/LoginForm.cs | 2 +- osu.Game/Overlays/Login/LoginPanel.cs | 2 +- osu.Game/Overlays/Login/SecondFactorAuthForm.cs | 2 +- osu.Game/Overlays/LoginOverlay.cs | 2 +- osu.Game/Overlays/Mods/AddPresetPopover.cs | 2 +- osu.Game/Overlays/Mods/EditPresetPopover.cs | 2 +- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 2 +- osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs | 2 +- osu.Game/Overlays/SettingsPanel.cs | 2 +- osu.Game/Overlays/SkinEditor/SkinEditor.cs | 2 +- .../Screens/Edit/Compose/Components/BeatDivisorControl.cs | 2 +- .../Edit/Compose/Components/Timeline/DifficultyPointPiece.cs | 2 +- osu.Game/Screens/Edit/Setup/LabelledTextBoxWithPopover.cs | 2 +- osu.Game/Screens/Edit/Setup/MetadataSection.cs | 2 +- .../Edit/Timing/IndeterminateSliderWithTextBoxInput.cs | 2 +- osu.Game/Screens/Edit/Verify/IssueTable.cs | 2 +- osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs | 4 ++-- osu.Game/Screens/Play/PlayerLoader.cs | 2 +- osu.Game/Screens/Select/BeatmapCarousel.cs | 2 +- osu.Game/Screens/Select/FilterControl.cs | 2 +- osu.Game/Screens/Select/PlaySongSelect.cs | 2 +- osu.Game/Screens/SelectV2/Footer/BeatmapOptionsPopover.cs | 2 +- .../Utility/SampleComponents/LatencySampleComponent.cs | 2 +- 34 files changed, 36 insertions(+), 36 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/JuiceStreamPlacementBlueprint.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/JuiceStreamPlacementBlueprint.cs index c8c8db1ebd..7b57dac36e 100644 --- a/osu.Game.Rulesets.Catch/Edit/Blueprints/JuiceStreamPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/JuiceStreamPlacementBlueprint.cs @@ -49,7 +49,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints { base.LoadComplete(); - inputManager = GetContainingInputManager(); + inputManager = GetContainingInputManager()!; BeginPlacement(); } diff --git a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs index 567c288b47..21faec56de 100644 --- a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs +++ b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs @@ -84,7 +84,7 @@ namespace osu.Game.Rulesets.Catch.UI { base.Update(); - var replayState = (GetContainingInputManager().CurrentState as RulesetInputManagerInputState)?.LastReplayState as CatchFramedReplayInputHandler.CatchReplayState; + var replayState = (GetContainingInputManager()!.CurrentState as RulesetInputManagerInputState)?.LastReplayState as CatchFramedReplayInputHandler.CatchReplayState; SetCatcherPosition( replayState?.CatcherX ?? diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneDrawableManiaHitObject.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneDrawableManiaHitObject.cs index 51c2bac6d1..7a0abb9e64 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneDrawableManiaHitObject.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneDrawableManiaHitObject.cs @@ -66,7 +66,7 @@ namespace osu.Game.Rulesets.Mania.Tests AddStep("Hold key", () => { clock.CurrentTime = 0; - note.OnPressed(new KeyBindingPressEvent(GetContainingInputManager().CurrentState, ManiaAction.Key1)); + note.OnPressed(new KeyBindingPressEvent(GetContainingInputManager()!.CurrentState, ManiaAction.Key1)); }); AddStep("progress time", () => clock.CurrentTime = 500); AddAssert("head is visible", () => note.Head.Alpha == 1); diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs index e6696032ae..98113a6513 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs @@ -161,9 +161,9 @@ namespace osu.Game.Rulesets.Osu.Tests pressed = value; if (value) - OnPressed(new KeyBindingPressEvent(GetContainingInputManager().CurrentState, OsuAction.LeftButton)); + OnPressed(new KeyBindingPressEvent(GetContainingInputManager()!.CurrentState, OsuAction.LeftButton)); else - OnReleased(new KeyBindingReleaseEvent(GetContainingInputManager().CurrentState, OsuAction.LeftButton)); + OnReleased(new KeyBindingReleaseEvent(GetContainingInputManager()!.CurrentState, OsuAction.LeftButton)); } } diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleArea.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleArea.cs index 71174e3295..5cac9843b8 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleArea.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleArea.cs @@ -100,7 +100,7 @@ namespace osu.Game.Rulesets.Osu.Tests private void scheduleHit() => AddStep("schedule action", () => { double delay = hitCircle.StartTime - hitCircle.HitWindows.WindowFor(HitResult.Great) - Time.Current; - Scheduler.AddDelayed(() => hitAreaReceptor.OnPressed(new KeyBindingPressEvent(GetContainingInputManager().CurrentState, OsuAction.LeftButton)), delay); + Scheduler.AddDelayed(() => hitAreaReceptor.OnPressed(new KeyBindingPressEvent(GetContainingInputManager()!.CurrentState, OsuAction.LeftButton)), delay); }); } } diff --git a/osu.Game.Tests/Visual/Editing/TestScenePositionSnapGrid.cs b/osu.Game.Tests/Visual/Editing/TestScenePositionSnapGrid.cs index 2721bc3602..bc2dee8534 100644 --- a/osu.Game.Tests/Visual/Editing/TestScenePositionSnapGrid.cs +++ b/osu.Game.Tests/Visual/Editing/TestScenePositionSnapGrid.cs @@ -92,7 +92,7 @@ namespace osu.Game.Tests.Visual.Editing { base.LoadComplete(); - updatePosition(GetContainingInputManager().CurrentState.Mouse.Position); + updatePosition(GetContainingInputManager()!.CurrentState.Mouse.Position); } protected override bool OnMouseMove(MouseMoveEvent e) diff --git a/osu.Game/Graphics/Cursor/GlobalCursorDisplay.cs b/osu.Game/Graphics/Cursor/GlobalCursorDisplay.cs index 85a2d68e55..d32544fc42 100644 --- a/osu.Game/Graphics/Cursor/GlobalCursorDisplay.cs +++ b/osu.Game/Graphics/Cursor/GlobalCursorDisplay.cs @@ -53,7 +53,7 @@ namespace osu.Game.Graphics.Cursor { base.LoadComplete(); - inputManager = GetContainingInputManager(); + inputManager = GetContainingInputManager()!; showDuringTouch = config.GetBindable(OsuSetting.GameplayCursorDuringTouch); } diff --git a/osu.Game/Graphics/UserInterface/FocusedTextBox.cs b/osu.Game/Graphics/UserInterface/FocusedTextBox.cs index 928865ffb0..f4ca00b7d0 100644 --- a/osu.Game/Graphics/UserInterface/FocusedTextBox.cs +++ b/osu.Game/Graphics/UserInterface/FocusedTextBox.cs @@ -31,7 +31,7 @@ namespace osu.Game.Graphics.UserInterface if (!allowImmediateFocus) return; - Scheduler.Add(() => GetContainingFocusManager()?.ChangeFocus(this)); + Scheduler.Add(() => GetContainingFocusManager()!.ChangeFocus(this)); } public new void KillFocus() => base.KillFocus(); diff --git a/osu.Game/Graphics/UserInterfaceV2/LabelledTextBox.cs b/osu.Game/Graphics/UserInterfaceV2/LabelledTextBox.cs index 42a72744d6..fabfde4333 100644 --- a/osu.Game/Graphics/UserInterfaceV2/LabelledTextBox.cs +++ b/osu.Game/Graphics/UserInterfaceV2/LabelledTextBox.cs @@ -59,7 +59,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 protected override void OnFocus(FocusEvent e) { base.OnFocus(e); - GetContainingFocusManager()?.ChangeFocus(Component); + GetContainingFocusManager()!.ChangeFocus(Component); } protected override OsuTextBox CreateComponent() => CreateTextBox().With(t => diff --git a/osu.Game/Overlays/AccountCreation/ScreenEntry.cs b/osu.Game/Overlays/AccountCreation/ScreenEntry.cs index e34e5c9521..fb6a5796a1 100644 --- a/osu.Game/Overlays/AccountCreation/ScreenEntry.cs +++ b/osu.Game/Overlays/AccountCreation/ScreenEntry.cs @@ -243,7 +243,7 @@ namespace osu.Game.Overlays.AccountCreation if (nextTextBox != null) { - Schedule(() => GetContainingFocusManager()?.ChangeFocus(nextTextBox)); + Schedule(() => GetContainingFocusManager()!.ChangeFocus(nextTextBox)); return true; } diff --git a/osu.Game/Overlays/Comments/ReplyCommentEditor.cs b/osu.Game/Overlays/Comments/ReplyCommentEditor.cs index 0b4daea0c1..d5ae4f92ab 100644 --- a/osu.Game/Overlays/Comments/ReplyCommentEditor.cs +++ b/osu.Game/Overlays/Comments/ReplyCommentEditor.cs @@ -39,7 +39,7 @@ namespace osu.Game.Overlays.Comments base.LoadComplete(); if (!TextBox.ReadOnly) - GetContainingFocusManager()?.ChangeFocus(TextBox); + GetContainingFocusManager()!.ChangeFocus(TextBox); } protected override void OnCommit(string text) diff --git a/osu.Game/Overlays/Login/LoginForm.cs b/osu.Game/Overlays/Login/LoginForm.cs index cde97d45c1..13e528ff8f 100644 --- a/osu.Game/Overlays/Login/LoginForm.cs +++ b/osu.Game/Overlays/Login/LoginForm.cs @@ -150,7 +150,7 @@ namespace osu.Game.Overlays.Login protected override void OnFocus(FocusEvent e) { - Schedule(() => { GetContainingFocusManager()?.ChangeFocus(string.IsNullOrEmpty(username.Text) ? username : password); }); + Schedule(() => { GetContainingFocusManager()!.ChangeFocus(string.IsNullOrEmpty(username.Text) ? username : password); }); } } } diff --git a/osu.Game/Overlays/Login/LoginPanel.cs b/osu.Game/Overlays/Login/LoginPanel.cs index 9afaed335d..cb642f9b72 100644 --- a/osu.Game/Overlays/Login/LoginPanel.cs +++ b/osu.Game/Overlays/Login/LoginPanel.cs @@ -216,7 +216,7 @@ namespace osu.Game.Overlays.Login protected override void OnFocus(FocusEvent e) { - if (form != null) GetContainingFocusManager()?.ChangeFocus(form); + if (form != null) GetContainingFocusManager()!.ChangeFocus(form); base.OnFocus(e); } } diff --git a/osu.Game/Overlays/Login/SecondFactorAuthForm.cs b/osu.Game/Overlays/Login/SecondFactorAuthForm.cs index c8e8b316fa..77835b1f09 100644 --- a/osu.Game/Overlays/Login/SecondFactorAuthForm.cs +++ b/osu.Game/Overlays/Login/SecondFactorAuthForm.cs @@ -141,7 +141,7 @@ namespace osu.Game.Overlays.Login protected override void OnFocus(FocusEvent e) { - Schedule(() => { GetContainingFocusManager()?.ChangeFocus(codeTextBox); }); + Schedule(() => { GetContainingFocusManager()!.ChangeFocus(codeTextBox); }); } } } diff --git a/osu.Game/Overlays/LoginOverlay.cs b/osu.Game/Overlays/LoginOverlay.cs index 576c66ff23..d570983f98 100644 --- a/osu.Game/Overlays/LoginOverlay.cs +++ b/osu.Game/Overlays/LoginOverlay.cs @@ -78,7 +78,7 @@ namespace osu.Game.Overlays this.FadeIn(transition_time, Easing.OutQuint); FadeEdgeEffectTo(WaveContainer.SHADOW_OPACITY, WaveContainer.APPEAR_DURATION, Easing.Out); - ScheduleAfterChildren(() => GetContainingFocusManager()?.ChangeFocus(panel)); + ScheduleAfterChildren(() => GetContainingFocusManager()!.ChangeFocus(panel)); } protected override void PopOut() diff --git a/osu.Game/Overlays/Mods/AddPresetPopover.cs b/osu.Game/Overlays/Mods/AddPresetPopover.cs index e59b60a1f1..7df7d6339c 100644 --- a/osu.Game/Overlays/Mods/AddPresetPopover.cs +++ b/osu.Game/Overlays/Mods/AddPresetPopover.cs @@ -89,7 +89,7 @@ namespace osu.Game.Overlays.Mods { base.LoadComplete(); - ScheduleAfterChildren(() => GetContainingFocusManager()?.ChangeFocus(nameTextBox)); + ScheduleAfterChildren(() => GetContainingFocusManager()!.ChangeFocus(nameTextBox)); nameTextBox.Current.BindValueChanged(s => { diff --git a/osu.Game/Overlays/Mods/EditPresetPopover.cs b/osu.Game/Overlays/Mods/EditPresetPopover.cs index 88119f57b3..526ab6fc63 100644 --- a/osu.Game/Overlays/Mods/EditPresetPopover.cs +++ b/osu.Game/Overlays/Mods/EditPresetPopover.cs @@ -136,7 +136,7 @@ namespace osu.Game.Overlays.Mods { base.LoadComplete(); - ScheduleAfterChildren(() => GetContainingFocusManager()?.ChangeFocus(nameTextBox)); + ScheduleAfterChildren(() => GetContainingFocusManager()!.ChangeFocus(nameTextBox)); } public override bool OnPressed(KeyBindingPressEvent e) diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index 13970e718a..145f58fb55 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -949,7 +949,7 @@ namespace osu.Game.Overlays.Mods RequestScroll?.Invoke(this); // Killing focus is done here because it's the only feasible place on ModSelectOverlay you can click on without triggering any action. - Scheduler.Add(() => GetContainingFocusManager()?.ChangeFocus(null)); + Scheduler.Add(() => GetContainingFocusManager()!.ChangeFocus(null)); return true; } diff --git a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs index 36339c484e..ddf831c23e 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs @@ -465,7 +465,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input } if (HasFocus) - GetContainingFocusManager()?.ChangeFocus(null); + GetContainingFocusManager()!.ChangeFocus(null); cancelAndClearButtons.FadeOut(300, Easing.OutQuint); cancelAndClearButtons.BypassAutoSizeAxes |= Axes.Y; diff --git a/osu.Game/Overlays/SettingsPanel.cs b/osu.Game/Overlays/SettingsPanel.cs index a5a56d1e8b..df50e0f339 100644 --- a/osu.Game/Overlays/SettingsPanel.cs +++ b/osu.Game/Overlays/SettingsPanel.cs @@ -201,7 +201,7 @@ namespace osu.Game.Overlays searchTextBox.HoldFocus = false; if (searchTextBox.HasFocus) - GetContainingFocusManager()?.ChangeFocus(null); + GetContainingFocusManager()!.ChangeFocus(null); } public override bool AcceptsFocus => true; diff --git a/osu.Game/Overlays/SkinEditor/SkinEditor.cs b/osu.Game/Overlays/SkinEditor/SkinEditor.cs index 41fd701a09..484af34603 100644 --- a/osu.Game/Overlays/SkinEditor/SkinEditor.cs +++ b/osu.Game/Overlays/SkinEditor/SkinEditor.cs @@ -669,7 +669,7 @@ namespace osu.Game.Overlays.SkinEditor { SpriteName = { Value = file.Name }, Origin = Anchor.Centre, - Position = skinnableTarget.ToLocalSpace(GetContainingInputManager().CurrentState.Mouse.Position), + Position = skinnableTarget.ToLocalSpace(GetContainingInputManager()!.CurrentState.Mouse.Position), }; SelectedComponents.Clear(); diff --git a/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs b/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs index 1751d01fb7..faab5e7f78 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs @@ -580,7 +580,7 @@ namespace osu.Game.Screens.Edit.Compose.Components { base.LoadComplete(); - GetContainingFocusManager()?.ChangeFocus(this); + GetContainingFocusManager()!.ChangeFocus(this); SelectAll(); } } diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/DifficultyPointPiece.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/DifficultyPointPiece.cs index 2c9da0446d..3ad6095965 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/DifficultyPointPiece.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/DifficultyPointPiece.cs @@ -138,7 +138,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline protected override void LoadComplete() { base.LoadComplete(); - ScheduleAfterChildren(() => GetContainingFocusManager()?.ChangeFocus(sliderVelocitySlider)); + ScheduleAfterChildren(() => GetContainingFocusManager()!.ChangeFocus(sliderVelocitySlider)); } } } diff --git a/osu.Game/Screens/Edit/Setup/LabelledTextBoxWithPopover.cs b/osu.Game/Screens/Edit/Setup/LabelledTextBoxWithPopover.cs index 00cc07413f..f9e93e7b0e 100644 --- a/osu.Game/Screens/Edit/Setup/LabelledTextBoxWithPopover.cs +++ b/osu.Game/Screens/Edit/Setup/LabelledTextBoxWithPopover.cs @@ -45,7 +45,7 @@ namespace osu.Game.Screens.Edit.Setup OnFocused?.Invoke(); base.OnFocus(e); - GetContainingFocusManager()?.TriggerFocusContention(this); + GetContainingFocusManager()!.TriggerFocusContention(this); } } } diff --git a/osu.Game/Screens/Edit/Setup/MetadataSection.cs b/osu.Game/Screens/Edit/Setup/MetadataSection.cs index dd880891ba..19071dc806 100644 --- a/osu.Game/Screens/Edit/Setup/MetadataSection.cs +++ b/osu.Game/Screens/Edit/Setup/MetadataSection.cs @@ -70,7 +70,7 @@ namespace osu.Game.Screens.Edit.Setup base.LoadComplete(); if (string.IsNullOrEmpty(ArtistTextBox.Current.Value)) - ScheduleAfterChildren(() => GetContainingFocusManager()?.ChangeFocus(ArtistTextBox)); + ScheduleAfterChildren(() => GetContainingFocusManager()!.ChangeFocus(ArtistTextBox)); ArtistTextBox.Current.BindValueChanged(artist => transferIfRomanised(artist.NewValue, RomanisedArtistTextBox)); TitleTextBox.Current.BindValueChanged(title => transferIfRomanised(title.NewValue, RomanisedTitleTextBox)); diff --git a/osu.Game/Screens/Edit/Timing/IndeterminateSliderWithTextBoxInput.cs b/osu.Game/Screens/Edit/Timing/IndeterminateSliderWithTextBoxInput.cs index 187aa3e897..01e1856e6c 100644 --- a/osu.Game/Screens/Edit/Timing/IndeterminateSliderWithTextBoxInput.cs +++ b/osu.Game/Screens/Edit/Timing/IndeterminateSliderWithTextBoxInput.cs @@ -126,7 +126,7 @@ namespace osu.Game.Screens.Edit.Timing protected override void OnFocus(FocusEvent e) { base.OnFocus(e); - GetContainingFocusManager()?.ChangeFocus(textBox); + GetContainingFocusManager()!.ChangeFocus(textBox); } private void updateState() diff --git a/osu.Game/Screens/Edit/Verify/IssueTable.cs b/osu.Game/Screens/Edit/Verify/IssueTable.cs index ba5f98a772..8fb30fb726 100644 --- a/osu.Game/Screens/Edit/Verify/IssueTable.cs +++ b/osu.Game/Screens/Edit/Verify/IssueTable.cs @@ -53,7 +53,7 @@ namespace osu.Game.Screens.Edit.Verify if (issue.Time != null) { clock.Seek(issue.Time.Value); - editor.OnPressed(new KeyBindingPressEvent(GetContainingInputManager().CurrentState, GlobalAction.EditorComposeMode)); + editor.OnPressed(new KeyBindingPressEvent(GetContainingInputManager()!.CurrentState, GlobalAction.EditorComposeMode)); } if (!issue.HitObjects.Any()) diff --git a/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs b/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs index 6f06b8686c..fed47e847a 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs @@ -248,7 +248,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge { base.LoadComplete(); - ScheduleAfterChildren(() => GetContainingFocusManager()?.ChangeFocus(passwordTextBox)); + ScheduleAfterChildren(() => GetContainingFocusManager()!.ChangeFocus(passwordTextBox)); passwordTextBox.OnCommit += (_, _) => performJoin(); } @@ -262,7 +262,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge { passwordTextBox.Text = string.Empty; - GetContainingFocusManager()?.ChangeFocus(passwordTextBox); + GetContainingFocusManager()!.ChangeFocus(passwordTextBox); errorText.Text = error; errorText diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs index 51a0c94ff0..12048ecbbe 100644 --- a/osu.Game/Screens/Play/PlayerLoader.cs +++ b/osu.Game/Screens/Play/PlayerLoader.cs @@ -249,7 +249,7 @@ namespace osu.Game.Screens.Play { base.LoadComplete(); - inputManager = GetContainingInputManager(); + inputManager = GetContainingInputManager()!; showStoryboards.BindValueChanged(val => epilepsyWarning?.FadeTo(val.NewValue ? 1 : 0, 250, Easing.OutQuint), true); epilepsyWarning?.FinishTransforms(true); diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 32a1b5cb58..49c23bdbbf 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -1279,7 +1279,7 @@ namespace osu.Game.Screens.Select { // we need to block right click absolute scrolling when hovering a carousel item so context menus can display. // this can be reconsidered when we have an alternative to right click scrolling. - if (GetContainingInputManager().HoveredDrawables.OfType().Any()) + if (GetContainingInputManager()!.HoveredDrawables.OfType().Any()) { rightMouseScrollBlocked = true; return false; diff --git a/osu.Game/Screens/Select/FilterControl.cs b/osu.Game/Screens/Select/FilterControl.cs index 497c1d4c9f..877db75317 100644 --- a/osu.Game/Screens/Select/FilterControl.cs +++ b/osu.Game/Screens/Select/FilterControl.cs @@ -245,7 +245,7 @@ namespace osu.Game.Screens.Select searchTextBox.ReadOnly = true; searchTextBox.HoldFocus = false; if (searchTextBox.HasFocus) - GetContainingFocusManager()?.ChangeFocus(searchTextBox); + GetContainingFocusManager()!.ChangeFocus(searchTextBox); } public void Activate() diff --git a/osu.Game/Screens/Select/PlaySongSelect.cs b/osu.Game/Screens/Select/PlaySongSelect.cs index 52f49ba56a..7b1479f392 100644 --- a/osu.Game/Screens/Select/PlaySongSelect.cs +++ b/osu.Game/Screens/Select/PlaySongSelect.cs @@ -95,7 +95,7 @@ namespace osu.Game.Screens.Select modsAtGameplayStart = Mods.Value; // Ctrl+Enter should start map with autoplay enabled. - if (GetContainingInputManager().CurrentState?.Keyboard.ControlPressed == true) + if (GetContainingInputManager()?.CurrentState?.Keyboard.ControlPressed == true) { var autoInstance = getAutoplayMod(); diff --git a/osu.Game/Screens/SelectV2/Footer/BeatmapOptionsPopover.cs b/osu.Game/Screens/SelectV2/Footer/BeatmapOptionsPopover.cs index e8b8b49785..fb2e32dfdc 100644 --- a/osu.Game/Screens/SelectV2/Footer/BeatmapOptionsPopover.cs +++ b/osu.Game/Screens/SelectV2/Footer/BeatmapOptionsPopover.cs @@ -81,7 +81,7 @@ namespace osu.Game.Screens.SelectV2.Footer { base.LoadComplete(); - ScheduleAfterChildren(() => GetContainingFocusManager()?.ChangeFocus(this)); + ScheduleAfterChildren(() => GetContainingFocusManager()!.ChangeFocus(this)); beatmap.BindValueChanged(_ => Hide()); } diff --git a/osu.Game/Screens/Utility/SampleComponents/LatencySampleComponent.cs b/osu.Game/Screens/Utility/SampleComponents/LatencySampleComponent.cs index 690376cf52..922935f520 100644 --- a/osu.Game/Screens/Utility/SampleComponents/LatencySampleComponent.cs +++ b/osu.Game/Screens/Utility/SampleComponents/LatencySampleComponent.cs @@ -38,7 +38,7 @@ namespace osu.Game.Screens.Utility.SampleComponents { base.LoadComplete(); - inputManager = GetContainingInputManager(); + inputManager = GetContainingInputManager()!; IsActive.BindTo(latencyArea.IsActiveArea); } From 366ba26cb6d0dea7fa3f76b397863140bb056dce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 18 Jun 2024 07:30:41 +0200 Subject: [PATCH 1096/2493] Adjust calls to `DrawableExtensions.With()` --- .../Skinning/Legacy/LegacyBodyPiece.cs | 10 ++-------- .../Skinning/Legacy/LegacyHitExplosion.cs | 5 +---- 2 files changed, 3 insertions(+), 12 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyBodyPiece.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyBodyPiece.cs index 1cba5b8cb3..087b428801 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyBodyPiece.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyBodyPiece.cs @@ -65,11 +65,8 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy if (tmp is IFramedAnimation tmpAnimation && tmpAnimation.FrameCount > 0) frameLength = Math.Max(1000 / 60.0, 170.0 / tmpAnimation.FrameCount); - light = skin.GetAnimation(lightImage, true, true, frameLength: frameLength).With(d => + light = skin.GetAnimation(lightImage, true, true, frameLength: frameLength)?.With(d => { - if (d == null) - return; - d.Origin = Anchor.Centre; d.Blending = BlendingParameters.Additive; d.Scale = new Vector2(lightScale); @@ -91,11 +88,8 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy direction.BindTo(scrollingInfo.Direction); isHitting.BindTo(holdNote.IsHitting); - bodySprite = skin.GetAnimation(imageName, wrapMode, wrapMode, true, true, frameLength: 30).With(d => + bodySprite = skin.GetAnimation(imageName, wrapMode, wrapMode, true, true, frameLength: 30)?.With(d => { - if (d == null) - return; - if (d is TextureAnimation animation) animation.IsPlaying = false; diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyHitExplosion.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyHitExplosion.cs index 1ec218644c..95b00e32ea 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyHitExplosion.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyHitExplosion.cs @@ -43,11 +43,8 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy if (tmp is IFramedAnimation tmpAnimation && tmpAnimation.FrameCount > 0) frameLength = Math.Max(1000 / 60.0, 170.0 / tmpAnimation.FrameCount); - explosion = skin.GetAnimation(imageName, true, false, frameLength: frameLength).With(d => + explosion = skin.GetAnimation(imageName, true, false, frameLength: frameLength)?.With(d => { - if (d == null) - return; - d.Origin = Anchor.Centre; d.Blending = BlendingParameters.Additive; d.Scale = new Vector2(explosionScale); From 310265c43fa95458e61899d60b72d32366c1dcbe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 18 Jun 2024 08:16:22 +0200 Subject: [PATCH 1097/2493] Add slider placement binding description in tooltip --- .../Edit/SliderCompositionTool.cs | 7 ++++++ osu.Game/Rulesets/Edit/HitObjectComposer.cs | 4 ++-- .../Edit/HitObjectCompositionToolButton.cs | 22 +++++++++++++++++++ .../Edit/Tools/HitObjectCompositionTool.cs | 7 +++--- .../Components/RadioButtons/RadioButton.cs | 4 ++-- 5 files changed, 37 insertions(+), 7 deletions(-) create mode 100644 osu.Game/Rulesets/Edit/HitObjectCompositionToolButton.cs diff --git a/osu.Game.Rulesets.Osu/Edit/SliderCompositionTool.cs b/osu.Game.Rulesets.Osu/Edit/SliderCompositionTool.cs index 676205c8d7..617cc1c19b 100644 --- a/osu.Game.Rulesets.Osu/Edit/SliderCompositionTool.cs +++ b/osu.Game.Rulesets.Osu/Edit/SliderCompositionTool.cs @@ -15,6 +15,13 @@ namespace osu.Game.Rulesets.Osu.Edit public SliderCompositionTool() : base(nameof(Slider)) { + TooltipText = """ + Left click for new point. + Left click twice or S key for new segment. + Tab, Shift-Tab, or Alt-1~4 to change current segment type. + Right click to finish. + Click and drag for drawing mode. + """; } public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders); diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs index d0c6078c9d..a34717e7ae 100644 --- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs @@ -215,14 +215,14 @@ namespace osu.Game.Rulesets.Edit toolboxCollection.Items = CompositionTools .Prepend(new SelectTool()) - .Select(t => new RadioButton(t.Name, () => toolSelected(t), t.CreateIcon)) + .Select(t => new HitObjectCompositionToolButton(t, () => toolSelected(t))) .ToList(); foreach (var item in toolboxCollection.Items) { item.Selected.DisabledChanged += isDisabled => { - item.TooltipText = isDisabled ? "Add at least one timing point first!" : string.Empty; + item.TooltipText = isDisabled ? "Add at least one timing point first!" : ((HitObjectCompositionToolButton)item).TooltipText; }; } diff --git a/osu.Game/Rulesets/Edit/HitObjectCompositionToolButton.cs b/osu.Game/Rulesets/Edit/HitObjectCompositionToolButton.cs new file mode 100644 index 0000000000..ba566ff5c0 --- /dev/null +++ b/osu.Game/Rulesets/Edit/HitObjectCompositionToolButton.cs @@ -0,0 +1,22 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Game.Rulesets.Edit.Tools; +using osu.Game.Screens.Edit.Components.RadioButtons; + +namespace osu.Game.Rulesets.Edit +{ + public class HitObjectCompositionToolButton : RadioButton + { + public HitObjectCompositionTool Tool { get; } + + public HitObjectCompositionToolButton(HitObjectCompositionTool tool, Action? action) + : base(tool.Name, action, tool.CreateIcon) + { + Tool = tool; + + TooltipText = tool.TooltipText; + } + } +} diff --git a/osu.Game/Rulesets/Edit/Tools/HitObjectCompositionTool.cs b/osu.Game/Rulesets/Edit/Tools/HitObjectCompositionTool.cs index 707645edeb..26e88aa530 100644 --- a/osu.Game/Rulesets/Edit/Tools/HitObjectCompositionTool.cs +++ b/osu.Game/Rulesets/Edit/Tools/HitObjectCompositionTool.cs @@ -1,9 +1,8 @@ // 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.Graphics; +using osu.Framework.Localisation; namespace osu.Game.Rulesets.Edit.Tools { @@ -11,6 +10,8 @@ namespace osu.Game.Rulesets.Edit.Tools { public readonly string Name; + public LocalisableString TooltipText { get; init; } = default; + protected HitObjectCompositionTool(string name) { Name = name; @@ -18,7 +19,7 @@ namespace osu.Game.Rulesets.Edit.Tools public abstract PlacementBlueprint CreatePlacementBlueprint(); - public virtual Drawable CreateIcon() => null; + public virtual Drawable? CreateIcon() => null; public override string ToString() => Name; } diff --git a/osu.Game/Screens/Edit/Components/RadioButtons/RadioButton.cs b/osu.Game/Screens/Edit/Components/RadioButtons/RadioButton.cs index f49fc6f6ab..26022aa746 100644 --- a/osu.Game/Screens/Edit/Components/RadioButtons/RadioButton.cs +++ b/osu.Game/Screens/Edit/Components/RadioButtons/RadioButton.cs @@ -24,11 +24,11 @@ namespace osu.Game.Screens.Edit.Components.RadioButtons /// /// A function which creates a drawable icon to represent this item. If null, a sane default should be used. /// - public readonly Func? CreateIcon; + public readonly Func? CreateIcon; private readonly Action? action; - public RadioButton(string label, Action? action, Func? createIcon = null) + public RadioButton(string label, Action? action, Func? createIcon = null) { Label = label; CreateIcon = createIcon; From a3326086f79513cdfe613e3304b1ca55c6558f49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 18 Jun 2024 08:25:05 +0200 Subject: [PATCH 1098/2493] Adjust hotkeys to address feedback --- .../Components/PathControlPointVisualiser.cs | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs index 3d6e529afa..775604174b 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs @@ -248,11 +248,11 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components // ReSharper disable once StaticMemberInGenericType private static readonly PathType?[] path_types = [ - null, PathType.LINEAR, PathType.BEZIER, PathType.PERFECT_CURVE, PathType.BSpline(4), + null, ]; protected override bool OnKeyDown(KeyDownEvent e) @@ -260,17 +260,17 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components if (e.Repeat) return false; - var selectedPieces = Pieces.Where(p => p.IsSelected.Value).ToArray(); - if (selectedPieces.Length != 1) - return false; - - var selectedPiece = selectedPieces.Single(); - var selectedPoint = selectedPiece.ControlPoint; - switch (e.Key) { case Key.Tab: { + var selectedPieces = Pieces.Where(p => p.IsSelected.Value).ToArray(); + if (selectedPieces.Length != 1) + return false; + + var selectedPiece = selectedPieces.Single(); + var selectedPoint = selectedPiece.ControlPoint; + var validTypes = path_types; if (selectedPoint == controlPoints[0]) @@ -295,15 +295,15 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components return true; } - case Key.Number0: case Key.Number1: case Key.Number2: case Key.Number3: case Key.Number4: + case Key.Number5: { - var type = path_types[e.Key - Key.Number0]; + var type = path_types[e.Key - Key.Number1]; - if (selectedPoint == controlPoints[0] && type == null) + if (Pieces[0].IsSelected.Value && type == null) return false; updatePathTypeOfSelectedPieces(type); From 73786a6f9f1039b7c64587db8bb22a402ebe7c61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 18 Jun 2024 08:32:12 +0200 Subject: [PATCH 1099/2493] Adjust & expand test coverage --- .../TestScenePathControlPointVisualiser.cs | 31 +++++++++++++++---- 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePathControlPointVisualiser.cs index 4813cc089c..93eb76aba6 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePathControlPointVisualiser.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePathControlPointVisualiser.cs @@ -215,21 +215,40 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor InputManager.Key(Key.Tab); InputManager.ReleaseKey(Key.LShift); }); - assertControlPointPathType(0, PathType.BEZIER); + assertControlPointPathType(0, PathType.PERFECT_CURVE); + assertControlPointPathType(2, PathType.BSpline(4)); AddStep("select third last control point", () => { visualiser.Pieces[0].IsSelected.Value = false; visualiser.Pieces[2].IsSelected.Value = true; }); - AddRepeatStep("press tab", () => InputManager.Key(Key.Tab), 3); + + AddStep("press shift-tab", () => + { + InputManager.PressKey(Key.LShift); + InputManager.Key(Key.Tab); + InputManager.ReleaseKey(Key.LShift); + }); assertControlPointPathType(2, PathType.PERFECT_CURVE); - AddStep("press tab", () => InputManager.Key(Key.Tab)); - assertControlPointPathType(2, PathType.BSpline(4)); - - AddStep("press tab", () => InputManager.Key(Key.Tab)); + AddRepeatStep("press tab", () => InputManager.Key(Key.Tab), 2); + assertControlPointPathType(0, PathType.BEZIER); assertControlPointPathType(2, null); + + AddStep("select first and third control points", () => + { + visualiser.Pieces[0].IsSelected.Value = true; + visualiser.Pieces[2].IsSelected.Value = true; + }); + AddStep("press alt-1", () => + { + InputManager.PressKey(Key.AltLeft); + InputManager.Key(Key.Number1); + InputManager.ReleaseKey(Key.AltLeft); + }); + assertControlPointPathType(0, PathType.LINEAR); + assertControlPointPathType(2, PathType.LINEAR); } private void addAssertPointPositionChanged(Vector2[] points, int index) From 24217514192a8ef58384c9a93e2a8b74cf91444a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 18 Jun 2024 08:33:02 +0200 Subject: [PATCH 1100/2493] Fix code quality inspections --- osu.Game/Rulesets/Edit/Tools/HitObjectCompositionTool.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/Edit/Tools/HitObjectCompositionTool.cs b/osu.Game/Rulesets/Edit/Tools/HitObjectCompositionTool.cs index 26e88aa530..ba1dc817bb 100644 --- a/osu.Game/Rulesets/Edit/Tools/HitObjectCompositionTool.cs +++ b/osu.Game/Rulesets/Edit/Tools/HitObjectCompositionTool.cs @@ -10,14 +10,14 @@ namespace osu.Game.Rulesets.Edit.Tools { public readonly string Name; - public LocalisableString TooltipText { get; init; } = default; + public LocalisableString TooltipText { get; init; } protected HitObjectCompositionTool(string name) { Name = name; } - public abstract PlacementBlueprint CreatePlacementBlueprint(); + public abstract PlacementBlueprint? CreatePlacementBlueprint(); public virtual Drawable? CreateIcon() => null; From e6187ebec09a28aa13c41a6daf26846a092c51b8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 18 Jun 2024 15:13:52 +0800 Subject: [PATCH 1101/2493] Fix nullability insepction --- .../Skinning/Legacy/LegacyStageForeground.cs | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyStageForeground.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyStageForeground.cs index 1a47fe5076..680198c1a6 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyStageForeground.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyStageForeground.cs @@ -28,13 +28,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy string bottomImage = skin.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.BottomStageImage)?.Value ?? "mania-stage-bottom"; - sprite = skin.GetAnimation(bottomImage, true, true)?.With(d => - { - if (d == null) - return; - - d.Scale = new Vector2(1.6f); - }); + sprite = skin.GetAnimation(bottomImage, true, true)?.With(d => d.Scale = new Vector2(1.6f)); if (sprite != null) InternalChild = sprite; From 1b4a3b0e2ebcba788fccdc353613f170c883551c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 18 Jun 2024 09:37:43 +0200 Subject: [PATCH 1102/2493] Change editor speed adjustment back to adjusting tempo - Partially reverts https://github.com/ppy/osu/pull/12080 - Addresses https://github.com/ppy/osu/discussions/27830, https://github.com/ppy/osu/discussions/23789, https://github.com/ppy/osu/discussions/15368, et al. The important distinction here is that to prevent misuse when timing, the control will revert to 1.0x speed and disable when moving to timing screen, with a tooltip explaining why. --- .../Edit/Components/PlaybackControl.cs | 79 ++++++++++++++----- 1 file changed, 59 insertions(+), 20 deletions(-) diff --git a/osu.Game/Screens/Edit/Components/PlaybackControl.cs b/osu.Game/Screens/Edit/Components/PlaybackControl.cs index 9e27f0e57d..0546878788 100644 --- a/osu.Game/Screens/Edit/Components/PlaybackControl.cs +++ b/osu.Game/Screens/Edit/Components/PlaybackControl.cs @@ -10,9 +10,11 @@ using osu.Framework.Bindables; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; 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.Sprites; using osu.Game.Graphics.UserInterface; @@ -25,14 +27,16 @@ namespace osu.Game.Screens.Edit.Components public partial class PlaybackControl : BottomBarContainer { private IconButton playButton = null!; + private PlaybackSpeedControl playbackSpeedControl = null!; [Resolved] private EditorClock editorClock { get; set; } = null!; - private readonly BindableNumber freqAdjust = new BindableDouble(1); + private readonly Bindable currentScreenMode = new Bindable(); + private readonly BindableNumber tempoAdjustment = new BindableDouble(1); [BackgroundDependencyLoader] - private void load(OverlayColourProvider colourProvider) + private void load(OverlayColourProvider colourProvider, Editor? editor) { Background.Colour = colourProvider.Background4; @@ -47,31 +51,61 @@ namespace osu.Game.Screens.Edit.Components Icon = FontAwesome.Regular.PlayCircle, Action = togglePause, }, - new OsuSpriteText + playbackSpeedControl = new PlaybackSpeedControl { - Origin = Anchor.BottomLeft, - Text = EditorStrings.PlaybackSpeed, - RelativePositionAxes = Axes.Y, - Y = 0.5f, - Padding = new MarginPadding { Left = 45 } - }, - new Container - { - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - RelativeSizeAxes = Axes.Both, - Height = 0.5f, - Padding = new MarginPadding { Left = 45 }, - Child = new PlaybackTabControl { Current = freqAdjust }, + AutoSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.X, + Padding = new MarginPadding { Left = 45, }, + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + Direction = FillDirection.Vertical, + Children = new Drawable[] + { + new OsuSpriteText + { + Text = EditorStrings.PlaybackSpeed, + }, + new PlaybackTabControl + { + Current = tempoAdjustment, + RelativeSizeAxes = Axes.X, + Height = 16, + }, + } } }; - Track.BindValueChanged(tr => tr.NewValue?.AddAdjustment(AdjustableProperty.Frequency, freqAdjust), true); + Track.BindValueChanged(tr => tr.NewValue?.AddAdjustment(AdjustableProperty.Tempo, tempoAdjustment), true); + + if (editor != null) + currentScreenMode.BindTo(editor.Mode); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + currentScreenMode.BindValueChanged(_ => + { + if (currentScreenMode.Value == EditorScreenMode.Timing) + { + tempoAdjustment.Value = 1; + tempoAdjustment.Disabled = true; + playbackSpeedControl.FadeTo(0.5f, 400, Easing.OutQuint); + playbackSpeedControl.TooltipText = "Speed adjustment is unavailable in timing mode. Timing at slower speeds is inaccurate due to resampling artifacts."; + } + else + { + tempoAdjustment.Disabled = false; + playbackSpeedControl.FadeTo(1, 400, Easing.OutQuint); + playbackSpeedControl.TooltipText = default; + } + }); } protected override void Dispose(bool isDisposing) { - Track.Value?.RemoveAdjustment(AdjustableProperty.Frequency, freqAdjust); + Track.Value?.RemoveAdjustment(AdjustableProperty.Frequency, tempoAdjustment); base.Dispose(isDisposing); } @@ -109,6 +143,11 @@ namespace osu.Game.Screens.Edit.Components playButton.Icon = editorClock.IsRunning ? pause_icon : play_icon; } + private partial class PlaybackSpeedControl : FillFlowContainer, IHasTooltip + { + public LocalisableString TooltipText { get; set; } + } + private partial class PlaybackTabControl : OsuTabControl { private static readonly double[] tempo_values = { 0.25, 0.5, 0.75, 1 }; @@ -174,7 +213,7 @@ namespace osu.Game.Screens.Edit.Components protected override bool OnHover(HoverEvent e) { updateState(); - return true; + return false; } protected override void OnHoverLost(HoverLostEvent e) => updateState(); From 87888ff0bbca43e94a5383939f37e01aec1d7419 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 18 Jun 2024 10:28:36 +0200 Subject: [PATCH 1103/2493] Extend slider selection box bounds to contain all control points inside Previously, the selection box was only guaranteed to contain the actual body of the slider itself, the control point nodes were allowed to exit it. This lead to a lot of weird interactions with the selection box controls (rotation/drag handles, also the buttons under/over it) as the slider anchors could overlap with them. To bypass this issue entirely just ensure that the selection box's size does include the control point nodes at all times. --- .../Sliders/SliderSelectionBlueprint.cs | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs index 49fdf12d60..2f1e2a9fdd 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs @@ -54,7 +54,21 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders [Resolved(CanBeNull = true)] private BindableBeatDivisor beatDivisor { get; set; } - public override Quad SelectionQuad => BodyPiece.ScreenSpaceDrawQuad; + public override Quad SelectionQuad + { + get + { + var result = BodyPiece.ScreenSpaceDrawQuad.AABBFloat; + + if (ControlPointVisualiser != null) + { + foreach (var piece in ControlPointVisualiser.Pieces) + result = RectangleF.Union(result, piece.ScreenSpaceDrawQuad.AABBFloat); + } + + return result; + } + } private readonly BindableList controlPoints = new BindableList(); private readonly IBindable pathVersion = new Bindable(); From 5fe21f16b994d0c7e040d9e012b92ff977099487 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 18 Jun 2024 10:41:26 +0200 Subject: [PATCH 1104/2493] Fix test failures --- .../Sliders/Components/PathControlPointVisualiser.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs index 775604174b..ddf6cd0f57 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs @@ -301,6 +301,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components case Key.Number4: case Key.Number5: { + if (!e.AltPressed) + return false; + var type = path_types[e.Key - Key.Number1]; if (Pieces[0].IsSelected.Value && type == null) From e1827ac28d7c344deea1764a7ab8f1cb796fb0d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 18 Jun 2024 12:33:12 +0200 Subject: [PATCH 1105/2493] Address review feedback --- osu.Game/OsuGame.cs | 2 +- .../Edit/Components/TimeInfoContainer.cs | 10 +++++----- osu.Game/Screens/Edit/Editor.cs | 19 ++++++++++++------- 3 files changed, 18 insertions(+), 13 deletions(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 667c3ecb99..63aa4564bf 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -595,7 +595,7 @@ namespace osu.Game return; } - editor.HandleTimestamp(timestamp); + editor.HandleTimestamp(timestamp, notifyOnError: true); } /// diff --git a/osu.Game/Screens/Edit/Components/TimeInfoContainer.cs b/osu.Game/Screens/Edit/Components/TimeInfoContainer.cs index b0e0d95132..9e14ec851b 100644 --- a/osu.Game/Screens/Edit/Components/TimeInfoContainer.cs +++ b/osu.Game/Screens/Edit/Components/TimeInfoContainer.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. -using System; -using System.Globalization; using osu.Framework.Graphics; using osu.Game.Graphics.Sprites; using osu.Framework.Allocation; @@ -66,6 +64,9 @@ namespace osu.Game.Screens.Edit.Components private OsuSpriteText trackTimer = null!; private OsuTextBox inputTextBox = null!; + [Resolved] + private Editor? editor { get; set; } + [Resolved] private EditorClock editorClock { get; set; } = null!; @@ -120,11 +121,10 @@ namespace osu.Game.Screens.Edit.Components }); }; + inputTextBox.Current.BindValueChanged(val => editor?.HandleTimestamp(val.NewValue)); + inputTextBox.OnCommit += (_, __) => { - if (TimeSpan.TryParseExact(inputTextBox.Text, @"mm\:ss\:fff", CultureInfo.InvariantCulture, out var timestamp)) - editorClock.SeekSmoothlyTo(timestamp.TotalMilliseconds); - trackTimer.Alpha = 1; inputTextBox.Alpha = 0; }; diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 02dcad46f7..a37d3763a5 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -1275,16 +1275,20 @@ namespace osu.Game.Screens.Edit return tcs.Task; } - public void HandleTimestamp(string timestamp) + public bool HandleTimestamp(string timestamp, bool notifyOnError = false) { if (!EditorTimestampParser.TryParse(timestamp, out var timeSpan, out string selection)) { - Schedule(() => notifications?.Post(new SimpleErrorNotification + if (notifyOnError) { - Icon = FontAwesome.Solid.ExclamationTriangle, - Text = EditorStrings.FailedToParseEditorLink - })); - return; + Schedule(() => notifications?.Post(new SimpleErrorNotification + { + Icon = FontAwesome.Solid.ExclamationTriangle, + Text = EditorStrings.FailedToParseEditorLink + })); + } + + return false; } editorBeatmap.SelectedHitObjects.Clear(); @@ -1297,7 +1301,7 @@ namespace osu.Game.Screens.Edit if (string.IsNullOrEmpty(selection)) { clock.SeekSmoothlyTo(position); - return; + return true; } // Seek to the next closest HitObject instead @@ -1312,6 +1316,7 @@ namespace osu.Game.Screens.Edit // Delegate handling the selection to the ruleset. currentScreen.Dependencies.Get().SelectFromTimestamp(position, selection); + return true; } public double SnapTime(double time, double? referenceTime) => editorBeatmap.SnapTime(time, referenceTime); From 44b9a066393d8468ae5728140ce592caaca0d565 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 18 Jun 2024 13:00:43 +0200 Subject: [PATCH 1106/2493] Allow more lenient parsing of incoming timestamps --- .../Editing/EditorTimestampParserTest.cs | 43 ++++++++++++++++++ osu.Game/Online/Chat/MessageFormatter.cs | 2 +- .../Rulesets/Edit/EditorTimestampParser.cs | 44 +++++++++++++------ 3 files changed, 75 insertions(+), 14 deletions(-) create mode 100644 osu.Game.Tests/Editing/EditorTimestampParserTest.cs diff --git a/osu.Game.Tests/Editing/EditorTimestampParserTest.cs b/osu.Game.Tests/Editing/EditorTimestampParserTest.cs new file mode 100644 index 0000000000..24ac8e32a4 --- /dev/null +++ b/osu.Game.Tests/Editing/EditorTimestampParserTest.cs @@ -0,0 +1,43 @@ +// 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 NUnit.Framework; +using osu.Game.Rulesets.Edit; + +namespace osu.Game.Tests.Editing +{ + [TestFixture] + public class EditorTimestampParserTest + { + public static readonly object?[][] test_cases = + { + new object?[] { ":", false, null, null }, + new object?[] { "1", true, new TimeSpan(0, 0, 1, 0), null }, + new object?[] { "99", true, new TimeSpan(0, 0, 99, 0), null }, + new object?[] { "300", false, null, null }, + new object?[] { "1:2", true, new TimeSpan(0, 0, 1, 2), null }, + new object?[] { "1:02", true, new TimeSpan(0, 0, 1, 2), null }, + new object?[] { "1:92", false, null, null }, + new object?[] { "1:002", false, null, null }, + new object?[] { "1:02:3", true, new TimeSpan(0, 0, 1, 2, 3), null }, + new object?[] { "1:02:300", true, new TimeSpan(0, 0, 1, 2, 300), null }, + new object?[] { "1:02:3000", false, null, null }, + new object?[] { "1:02:300 ()", false, null, null }, + new object?[] { "1:02:300 (1,2,3)", true, new TimeSpan(0, 0, 1, 2, 300), "1,2,3" }, + }; + + [TestCaseSource(nameof(test_cases))] + public void TestTryParse(string timestamp, bool expectedSuccess, TimeSpan? expectedParsedTime, string? expectedSelection) + { + bool actualSuccess = EditorTimestampParser.TryParse(timestamp, out var actualParsedTime, out string? actualSelection); + + Assert.Multiple(() => + { + Assert.That(actualSuccess, Is.EqualTo(expectedSuccess)); + Assert.That(actualParsedTime, Is.EqualTo(expectedParsedTime)); + Assert.That(actualSelection, Is.EqualTo(expectedSelection)); + }); + } + } +} diff --git a/osu.Game/Online/Chat/MessageFormatter.cs b/osu.Game/Online/Chat/MessageFormatter.cs index f055633d64..77454c4775 100644 --- a/osu.Game/Online/Chat/MessageFormatter.cs +++ b/osu.Game/Online/Chat/MessageFormatter.cs @@ -271,7 +271,7 @@ namespace osu.Game.Online.Chat handleAdvanced(advanced_link_regex, result, startIndex); // handle editor times - handleMatches(EditorTimestampParser.TIME_REGEX, "{0}", $@"{OsuGameBase.OSU_PROTOCOL}edit/{{0}}", result, startIndex, LinkAction.OpenEditorTimestamp); + handleMatches(EditorTimestampParser.TIME_REGEX_STRICT, "{0}", $@"{OsuGameBase.OSU_PROTOCOL}edit/{{0}}", result, startIndex, LinkAction.OpenEditorTimestamp); // handle channels handleMatches(channel_regex, "{0}", $@"{OsuGameBase.OSU_PROTOCOL}chan/{{0}}", result, startIndex, LinkAction.OpenChannel); diff --git a/osu.Game/Rulesets/Edit/EditorTimestampParser.cs b/osu.Game/Rulesets/Edit/EditorTimestampParser.cs index bdfdce432e..9c3119d8f4 100644 --- a/osu.Game/Rulesets/Edit/EditorTimestampParser.cs +++ b/osu.Game/Rulesets/Edit/EditorTimestampParser.cs @@ -9,13 +9,34 @@ namespace osu.Game.Rulesets.Edit { public static class EditorTimestampParser { - // 00:00:000 (...) - test - // original osu-web regex: https://github.com/ppy/osu-web/blob/3b1698639244cfdaf0b41c68bfd651ea729ec2e3/resources/js/utils/beatmapset-discussion-helper.ts#L78 - public static readonly Regex TIME_REGEX = new Regex(@"\b(((?\d{2,}):(?[0-5]\d)[:.](?\d{3}))(?\s\([^)]+\))?)", RegexOptions.Compiled); + /// + /// Used for parsing in contexts where we don't want e.g. normal times of day to be parsed as timestamps (e.g. chat) + /// Original osu-web regex: https://github.com/ppy/osu-web/blob/3b1698639244cfdaf0b41c68bfd651ea729ec2e3/resources/js/utils/beatmapset-discussion-helper.ts#L78 + /// + /// + /// 00:00:000 (...) - test + /// + public static readonly Regex TIME_REGEX_STRICT = new Regex(@"\b(((?\d{2,}):(?[0-5]\d)[:.](?\d{3}))(?\s\([^)]+\))?)", RegexOptions.Compiled); + + /// + /// Used for editor-specific context wherein we want to try as hard as we can to process user input as a timestamp. + /// + /// + /// + /// 1 - parses to 01:00:000 + /// 1:2 - parses to 01:02:000 + /// 1:02 - parses to 01:02:000 + /// 1:92 - does not parse + /// 1:02:3 - parses to 01:02:003 + /// 1:02:300 - parses to 01:02:300 + /// 1:02:300 (1,2,3) - parses to 01:02:300 with selection + /// + /// + private static readonly Regex time_regex_lenient = new Regex(@"^(((?\d{1,3})(:(?([0-5]?\d))([:.](?\d{0,3}))?)?)(?\s\([^)]+\))?)$", RegexOptions.Compiled); public static bool TryParse(string timestamp, [NotNullWhen(true)] out TimeSpan? parsedTime, out string? parsedSelection) { - Match match = TIME_REGEX.Match(timestamp); + Match match = time_regex_lenient.Match(timestamp); if (!match.Success) { @@ -24,16 +45,14 @@ namespace osu.Game.Rulesets.Edit return false; } - bool result = true; + int timeMin, timeSec, timeMsec; - result &= int.TryParse(match.Groups[@"minutes"].Value, out int timeMin); - result &= int.TryParse(match.Groups[@"seconds"].Value, out int timeSec); - result &= int.TryParse(match.Groups[@"milliseconds"].Value, out int timeMsec); + int.TryParse(match.Groups[@"minutes"].Value, out timeMin); + int.TryParse(match.Groups[@"seconds"].Value, out timeSec); + int.TryParse(match.Groups[@"milliseconds"].Value, out timeMsec); // somewhat sane limit for timestamp duration (10 hours). - result &= timeMin < 600; - - if (!result) + if (timeMin >= 600) { parsedTime = null; parsedSelection = null; @@ -42,8 +61,7 @@ namespace osu.Game.Rulesets.Edit parsedTime = TimeSpan.FromMinutes(timeMin) + TimeSpan.FromSeconds(timeSec) + TimeSpan.FromMilliseconds(timeMsec); parsedSelection = match.Groups[@"selection"].Value.Trim(); - if (!string.IsNullOrEmpty(parsedSelection)) - parsedSelection = parsedSelection[1..^1]; + parsedSelection = !string.IsNullOrEmpty(parsedSelection) ? parsedSelection[1..^1] : null; return true; } } From ec2b8f3bc3f9ab05afed177e21fbbd3147b7cfb3 Mon Sep 17 00:00:00 2001 From: Givikap120 Date: Sun, 16 Jun 2024 17:12:35 +0300 Subject: [PATCH 1107/2493] Changed timed difficulty attributes to be "per-HitObject" instead of "per-DifficultyHitObject" --- .../Difficulty/DifficultyCalculator.cs | 29 +++++++++---------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs index d37cfc28b9..4644a457bf 100644 --- a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs +++ b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs @@ -109,26 +109,23 @@ namespace osu.Game.Rulesets.Difficulty var progressiveBeatmap = new ProgressiveCalculationBeatmap(Beatmap); var difficultyObjects = getDifficultyHitObjects().ToArray(); - foreach (var obj in difficultyObjects) + int currentIndex = 0; + + foreach (var obj in Beatmap.HitObjects) { - // Implementations expect the progressive beatmap to only contain top-level objects from the original beatmap. - // At the same time, we also need to consider the possibility DHOs may not be generated for any given object, - // so we'll add all remaining objects up to the current point in time to the progressive beatmap. - for (int i = progressiveBeatmap.HitObjects.Count; i < Beatmap.HitObjects.Count; i++) - { - if (obj != difficultyObjects[^1] && Beatmap.HitObjects[i].StartTime > obj.BaseObject.StartTime) - break; + progressiveBeatmap.HitObjects.Add(obj); - progressiveBeatmap.HitObjects.Add(Beatmap.HitObjects[i]); + while (currentIndex < difficultyObjects.Length && difficultyObjects[currentIndex].BaseObject.GetEndTime() <= obj.GetEndTime()) + { + foreach (var skill in skills) + { + cancellationToken.ThrowIfCancellationRequested(); + skill.Process(difficultyObjects[currentIndex]); + } + currentIndex++; } - foreach (var skill in skills) - { - cancellationToken.ThrowIfCancellationRequested(); - skill.Process(obj); - } - - attribs.Add(new TimedDifficultyAttributes(obj.EndTime * clockRate, CreateDifficultyAttributes(progressiveBeatmap, playableMods, skills, clockRate))); + attribs.Add(new TimedDifficultyAttributes(obj.GetEndTime(), CreateDifficultyAttributes(progressiveBeatmap, playableMods, skills, clockRate))); } return attribs; From 1ddfc8f0110e374063f2b0f8e5b9d062f1714e9b Mon Sep 17 00:00:00 2001 From: Givikap120 Date: Tue, 18 Jun 2024 15:48:21 +0300 Subject: [PATCH 1108/2493] Updated the tests according to new logic and fixed one minor CI code quality thing --- .../TestSceneTimedDifficultyCalculation.cs | 45 ++++++------------- .../Difficulty/DifficultyCalculator.cs | 1 + 2 files changed, 14 insertions(+), 32 deletions(-) diff --git a/osu.Game.Tests/NonVisual/TestSceneTimedDifficultyCalculation.cs b/osu.Game.Tests/NonVisual/TestSceneTimedDifficultyCalculation.cs index 1a75f735ef..f860cd097a 100644 --- a/osu.Game.Tests/NonVisual/TestSceneTimedDifficultyCalculation.cs +++ b/osu.Game.Tests/NonVisual/TestSceneTimedDifficultyCalculation.cs @@ -22,7 +22,7 @@ namespace osu.Game.Tests.NonVisual public class TestSceneTimedDifficultyCalculation { [Test] - public void TestAttributesGeneratedForAllNonSkippedObjects() + public void TestAttributesGeneratedForEachObjectOnce() { var beatmap = new Beatmap { @@ -40,15 +40,14 @@ namespace osu.Game.Tests.NonVisual List attribs = new TestDifficultyCalculator(new TestWorkingBeatmap(beatmap)).CalculateTimed(); - Assert.That(attribs.Count, Is.EqualTo(4)); + Assert.That(attribs.Count, Is.EqualTo(3)); assertEquals(attribs[0], beatmap.HitObjects[0]); assertEquals(attribs[1], beatmap.HitObjects[0], beatmap.HitObjects[1]); - assertEquals(attribs[2], beatmap.HitObjects[0], beatmap.HitObjects[1]); // From the nested object. - assertEquals(attribs[3], beatmap.HitObjects[0], beatmap.HitObjects[1], beatmap.HitObjects[2]); + assertEquals(attribs[2], beatmap.HitObjects[0], beatmap.HitObjects[1], beatmap.HitObjects[2]); } [Test] - public void TestAttributesNotGeneratedForSkippedObjects() + public void TestAttributesGeneratedForSkippedObjects() { var beatmap = new Beatmap { @@ -72,35 +71,14 @@ namespace osu.Game.Tests.NonVisual List attribs = new TestDifficultyCalculator(new TestWorkingBeatmap(beatmap)).CalculateTimed(); - Assert.That(attribs.Count, Is.EqualTo(1)); - assertEquals(attribs[0], beatmap.HitObjects[0], beatmap.HitObjects[1], beatmap.HitObjects[2]); - } - - [Test] - public void TestNestedObjectOnlyAddsParentOnce() - { - var beatmap = new Beatmap - { - HitObjects = - { - new TestHitObject - { - StartTime = 1, - Skip = true, - Nested = 2 - }, - } - }; - - List attribs = new TestDifficultyCalculator(new TestWorkingBeatmap(beatmap)).CalculateTimed(); - - Assert.That(attribs.Count, Is.EqualTo(2)); + Assert.That(attribs.Count, Is.EqualTo(3)); assertEquals(attribs[0], beatmap.HitObjects[0]); - assertEquals(attribs[1], beatmap.HitObjects[0]); + assertEquals(attribs[1], beatmap.HitObjects[0], beatmap.HitObjects[1]); + assertEquals(attribs[2], beatmap.HitObjects[0], beatmap.HitObjects[1], beatmap.HitObjects[2]); } [Test] - public void TestSkippedLastObjectAddedInLastIteration() + public void TestAttributesGeneratedOnceForSkippedObjects() { var beatmap = new Beatmap { @@ -110,6 +88,7 @@ namespace osu.Game.Tests.NonVisual new TestHitObject { StartTime = 2, + Nested = 5, Skip = true }, new TestHitObject @@ -122,8 +101,10 @@ namespace osu.Game.Tests.NonVisual List attribs = new TestDifficultyCalculator(new TestWorkingBeatmap(beatmap)).CalculateTimed(); - Assert.That(attribs.Count, Is.EqualTo(1)); - assertEquals(attribs[0], beatmap.HitObjects[0], beatmap.HitObjects[1], beatmap.HitObjects[2]); + Assert.That(attribs.Count, Is.EqualTo(3)); + assertEquals(attribs[0], beatmap.HitObjects[0]); + assertEquals(attribs[1], beatmap.HitObjects[0], beatmap.HitObjects[1]); + assertEquals(attribs[2], beatmap.HitObjects[0], beatmap.HitObjects[1], beatmap.HitObjects[2]); } private void assertEquals(TimedDifficultyAttributes attribs, params HitObject[] expected) diff --git a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs index 4644a457bf..8884166255 100644 --- a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs +++ b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs @@ -122,6 +122,7 @@ namespace osu.Game.Rulesets.Difficulty cancellationToken.ThrowIfCancellationRequested(); skill.Process(difficultyObjects[currentIndex]); } + currentIndex++; } From a9e662a2b6d593681acb0e54058cec9635f013ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 18 Jun 2024 14:55:59 +0200 Subject: [PATCH 1109/2493] Add break display to editor timeline --- .../Components/Timeline/TimelineBreak.cs | 87 +++++++++++++++++ .../Timeline/TimelineBreakDisplay.cs | 94 +++++++++++++++++++ .../Screens/Edit/Compose/ComposeScreen.cs | 10 +- 3 files changed, 190 insertions(+), 1 deletion(-) create mode 100644 osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBreak.cs create mode 100644 osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBreakDisplay.cs diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBreak.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBreak.cs new file mode 100644 index 0000000000..dc54661644 --- /dev/null +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBreak.cs @@ -0,0 +1,87 @@ +// 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.Shapes; +using osu.Game.Beatmaps.Timing; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; + +namespace osu.Game.Screens.Edit.Compose.Components.Timeline +{ + public partial class TimelineBreak : CompositeDrawable + { + public BreakPeriod Break { get; } + + public TimelineBreak(BreakPeriod b) + { + Break = b; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + RelativePositionAxes = Axes.X; + RelativeSizeAxes = Axes.Both; + Origin = Anchor.TopLeft; + X = (float)Break.StartTime; + Width = (float)Break.Duration; + CornerRadius = 10; + Masking = true; + + InternalChildren = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colours.GreyCarmineLight, + Alpha = 0.4f, + }, + new Circle + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + RelativeSizeAxes = Axes.Y, + Width = 10, + CornerRadius = 5, + Colour = colours.GreyCarmineLighter, + }, + new OsuSpriteText + { + Anchor = Anchor.TopLeft, + Origin = Anchor.TopLeft, + Text = "Break", + Margin = new MarginPadding + { + Left = 16, + Top = 3, + }, + Colour = colours.GreyCarmineLighter, + }, + new Circle + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + RelativeSizeAxes = Axes.Y, + Width = 10, + CornerRadius = 5, + Colour = colours.GreyCarmineLighter, + }, + new OsuSpriteText + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + Text = "Break", + Margin = new MarginPadding + { + Right = 16, + Top = 3, + }, + Colour = colours.GreyCarmineLighter, + }, + }; + } + } +} diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBreakDisplay.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBreakDisplay.cs new file mode 100644 index 0000000000..587db23e9a --- /dev/null +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBreakDisplay.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 osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Caching; +using osu.Game.Beatmaps.Timing; +using osu.Game.Screens.Edit.Components.Timelines.Summary.Parts; + +namespace osu.Game.Screens.Edit.Compose.Components.Timeline +{ + public partial class TimelineBreakDisplay : TimelinePart + { + [Resolved] + private Timeline timeline { get; set; } = null!; + + /// + /// The visible time/position range of the timeline. + /// + private (float min, float max) visibleRange = (float.MinValue, float.MaxValue); + + private readonly Cached breakCache = new Cached(); + + private readonly BindableList breaks = new BindableList(); + + protected override void LoadBeatmap(EditorBeatmap beatmap) + { + base.LoadBeatmap(beatmap); + + // TODO: this will have to be mutable soon enough + breaks.AddRange(beatmap.Breaks); + } + + protected override void Update() + { + base.Update(); + + if (DrawWidth <= 0) return; + + (float, float) newRange = ( + (ToLocalSpace(timeline.ScreenSpaceDrawQuad.TopLeft).X) / DrawWidth * Content.RelativeChildSize.X, + (ToLocalSpace(timeline.ScreenSpaceDrawQuad.TopRight).X) / DrawWidth * Content.RelativeChildSize.X); + + if (visibleRange != newRange) + { + visibleRange = newRange; + breakCache.Invalidate(); + } + + if (!breakCache.IsValid) + { + recreateBreaks(); + breakCache.Validate(); + } + } + + private void recreateBreaks() + { + // Remove groups outside the visible range + foreach (TimelineBreak drawableBreak in this) + { + if (!shouldBeVisible(drawableBreak.Break)) + drawableBreak.Expire(); + } + + // Add remaining ones + for (int i = 0; i < breaks.Count; i++) + { + var breakPeriod = breaks[i]; + + if (!shouldBeVisible(breakPeriod)) + continue; + + bool alreadyVisible = false; + + foreach (var b in this) + { + if (ReferenceEquals(b.Break, breakPeriod)) + { + alreadyVisible = true; + break; + } + } + + if (alreadyVisible) + continue; + + Add(new TimelineBreak(breakPeriod)); + } + } + + private bool shouldBeVisible(BreakPeriod breakPeriod) => breakPeriod.EndTime >= visibleRange.min && breakPeriod.StartTime <= visibleRange.max; + } +} diff --git a/osu.Game/Screens/Edit/Compose/ComposeScreen.cs b/osu.Game/Screens/Edit/Compose/ComposeScreen.cs index 0a58b34da6..ed4ef896f5 100644 --- a/osu.Game/Screens/Edit/Compose/ComposeScreen.cs +++ b/osu.Game/Screens/Edit/Compose/ComposeScreen.cs @@ -69,7 +69,15 @@ namespace osu.Game.Screens.Edit.Compose if (ruleset == null || composer == null) return base.CreateTimelineContent(); - return wrapSkinnableContent(new TimelineBlueprintContainer(composer)); + return wrapSkinnableContent(new Container + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + new TimelineBreakDisplay { RelativeSizeAxes = Axes.Both, }, + new TimelineBlueprintContainer(composer) + } + }); } private Drawable wrapSkinnableContent(Drawable content) From 814f1e552fdfafddad2868e8cccbf4c721bd6c3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 18 Jun 2024 15:41:43 +0200 Subject: [PATCH 1110/2493] Implement ability to manually adjust breaks --- .../Components/Timeline/TimelineBreak.cs | 211 ++++++++++++++---- .../Screens/Edit/Compose/ComposeScreen.cs | 2 +- 2 files changed, 171 insertions(+), 42 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBreak.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBreak.cs index dc54661644..785eba2042 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBreak.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBreak.cs @@ -1,13 +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 System; +using System.Diagnostics; +using System.Linq; using osu.Framework.Allocation; +using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Framework.Input.Events; using osu.Game.Beatmaps.Timing; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; +using osu.Game.Rulesets.Objects; +using osuTK; namespace osu.Game.Screens.Edit.Compose.Components.Timeline { @@ -26,62 +33,184 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline RelativePositionAxes = Axes.X; RelativeSizeAxes = Axes.Both; Origin = Anchor.TopLeft; - X = (float)Break.StartTime; - Width = (float)Break.Duration; - CornerRadius = 10; - Masking = true; + Padding = new MarginPadding { Horizontal = -5 }; InternalChildren = new Drawable[] { - new Box + new Container { RelativeSizeAxes = Axes.Both, - Colour = colours.GreyCarmineLight, - Alpha = 0.4f, + Padding = new MarginPadding { Horizontal = 5 }, + Child = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colours.GreyCarmineLight, + Alpha = 0.4f, + }, }, - new Circle - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - RelativeSizeAxes = Axes.Y, - Width = 10, - CornerRadius = 5, - Colour = colours.GreyCarmineLighter, - }, - new OsuSpriteText + new DragHandle(Break, isStartHandle: true) { Anchor = Anchor.TopLeft, Origin = Anchor.TopLeft, - Text = "Break", - Margin = new MarginPadding - { - Left = 16, - Top = 3, - }, - Colour = colours.GreyCarmineLighter, + Action = (time, breakPeriod) => breakPeriod.StartTime = time, }, - new Circle - { - Anchor = Anchor.CentreRight, - Origin = Anchor.CentreRight, - RelativeSizeAxes = Axes.Y, - Width = 10, - CornerRadius = 5, - Colour = colours.GreyCarmineLighter, - }, - new OsuSpriteText + new DragHandle(Break, isStartHandle: false) { Anchor = Anchor.TopRight, Origin = Anchor.TopRight, - Text = "Break", - Margin = new MarginPadding - { - Right = 16, - Top = 3, - }, - Colour = colours.GreyCarmineLighter, + Action = (time, breakPeriod) => breakPeriod.EndTime = time, }, }; } + + protected override void Update() + { + base.Update(); + + X = (float)Break.StartTime; + Width = (float)Break.Duration; + } + + private partial class DragHandle : FillFlowContainer + { + public new Anchor Anchor + { + get => base.Anchor; + init => base.Anchor = value; + } + + public Action? Action { get; init; } + + private readonly BreakPeriod breakPeriod; + private readonly bool isStartHandle; + + private Container handle = null!; + private (double min, double max)? allowedDragRange; + + [Resolved] + private EditorBeatmap beatmap { get; set; } = null!; + + [Resolved] + private Timeline timeline { get; set; } = null!; + + [Resolved] + private IEditorChangeHandler? changeHandler { get; set; } + + [Resolved] + private OsuColour colours { get; set; } = null!; + + public DragHandle(BreakPeriod breakPeriod, bool isStartHandle) + { + this.breakPeriod = breakPeriod; + this.isStartHandle = isStartHandle; + } + + [BackgroundDependencyLoader] + private void load() + { + AutoSizeAxes = Axes.X; + RelativeSizeAxes = Axes.Y; + Direction = FillDirection.Horizontal; + Spacing = new Vector2(5); + + Children = new Drawable[] + { + handle = new Container + { + Anchor = Anchor, + Origin = Anchor, + RelativeSizeAxes = Axes.Y, + CornerRadius = 5, + Masking = true, + Child = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Colour4.White, + }, + }, + new OsuSpriteText + { + BypassAutoSizeAxes = Axes.X, + Anchor = Anchor, + Origin = Anchor, + Text = "Break", + Margin = new MarginPadding { Top = 2, }, + }, + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + updateState(); + FinishTransforms(true); + } + + protected override bool OnHover(HoverEvent e) + { + updateState(); + return true; + } + + protected override void OnHoverLost(HoverLostEvent e) + { + updateState(); + base.OnHoverLost(e); + } + + protected override bool OnDragStart(DragStartEvent e) + { + changeHandler?.BeginChange(); + updateState(); + + double min = beatmap.HitObjects.Last(ho => ho.GetEndTime() <= breakPeriod.StartTime).GetEndTime(); + double max = beatmap.HitObjects.First(ho => ho.StartTime >= breakPeriod.EndTime).StartTime; + + if (isStartHandle) + max = Math.Min(max, breakPeriod.EndTime - BreakPeriod.MIN_BREAK_DURATION); + else + min = Math.Max(min, breakPeriod.StartTime + BreakPeriod.MIN_BREAK_DURATION); + + allowedDragRange = (min, max); + + return true; + } + + protected override void OnDrag(DragEvent e) + { + base.OnDrag(e); + + Debug.Assert(allowedDragRange != null); + + if (timeline.FindSnappedPositionAndTime(e.ScreenSpaceMousePosition).Time is double time + && time > allowedDragRange.Value.min + && time < allowedDragRange.Value.max) + { + Action?.Invoke(time, breakPeriod); + } + + updateState(); + } + + protected override void OnDragEnd(DragEndEvent e) + { + changeHandler?.EndChange(); + updateState(); + base.OnDragEnd(e); + } + + private void updateState() + { + bool active = IsHovered || IsDragged; + + var colour = colours.GreyCarmineLighter; + if (active) + colour = colour.Lighten(0.3f); + + this.FadeColour(colour, 400, Easing.OutQuint); + handle.ResizeWidthTo(active ? 20 : 10, 400, Easing.OutElastic); + } + } } } diff --git a/osu.Game/Screens/Edit/Compose/ComposeScreen.cs b/osu.Game/Screens/Edit/Compose/ComposeScreen.cs index ed4ef896f5..9b945e1d6d 100644 --- a/osu.Game/Screens/Edit/Compose/ComposeScreen.cs +++ b/osu.Game/Screens/Edit/Compose/ComposeScreen.cs @@ -74,8 +74,8 @@ namespace osu.Game.Screens.Edit.Compose RelativeSizeAxes = Axes.Both, Children = new Drawable[] { + new TimelineBlueprintContainer(composer), new TimelineBreakDisplay { RelativeSizeAxes = Axes.Both, }, - new TimelineBlueprintContainer(composer) } }); } From f88f05717a9d147da880720e8b3bf9df8f3d9a36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 18 Jun 2024 15:54:34 +0200 Subject: [PATCH 1111/2493] Fix bottom timeline break visualisations not updating --- .../Timelines/Summary/Parts/BreakPart.cs | 20 +++++++++++++--- .../Visualisations/DurationVisualisation.cs | 23 ------------------- 2 files changed, 17 insertions(+), 26 deletions(-) delete mode 100644 osu.Game/Screens/Edit/Components/Timelines/Summary/Visualisations/DurationVisualisation.cs diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/BreakPart.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/BreakPart.cs index e502dd951b..41ecb44d9d 100644 --- a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/BreakPart.cs +++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/BreakPart.cs @@ -2,9 +2,10 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Shapes; using osu.Game.Beatmaps.Timing; using osu.Game.Graphics; -using osu.Game.Screens.Edit.Components.Timelines.Summary.Visualisations; namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts { @@ -20,11 +21,24 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts Add(new BreakVisualisation(breakPeriod)); } - private partial class BreakVisualisation : DurationVisualisation + private partial class BreakVisualisation : Circle { + private readonly BreakPeriod breakPeriod; + public BreakVisualisation(BreakPeriod breakPeriod) - : base(breakPeriod.StartTime, breakPeriod.EndTime) { + this.breakPeriod = breakPeriod; + + RelativePositionAxes = Axes.X; + RelativeSizeAxes = Axes.Both; + } + + protected override void Update() + { + base.Update(); + + X = (float)breakPeriod.StartTime; + Width = (float)breakPeriod.Duration; } [BackgroundDependencyLoader] diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/Visualisations/DurationVisualisation.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/Visualisations/DurationVisualisation.cs deleted file mode 100644 index bfb50a05ea..0000000000 --- a/osu.Game/Screens/Edit/Components/Timelines/Summary/Visualisations/DurationVisualisation.cs +++ /dev/null @@ -1,23 +0,0 @@ -// 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.Graphics; -using osu.Framework.Graphics.Shapes; - -namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Visualisations -{ - /// - /// Represents a spanning point on a timeline part. - /// - public partial class DurationVisualisation : Circle - { - protected DurationVisualisation(double startTime, double endTime) - { - RelativePositionAxes = Axes.X; - RelativeSizeAxes = Axes.Both; - - X = (float)startTime; - Width = (float)(endTime - startTime); - } - } -} From 623055b60a027d55643d8a12eef9a660111c5f8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 18 Jun 2024 16:41:13 +0200 Subject: [PATCH 1112/2493] Fix tests --- osu.Game.Tests/Editing/EditorTimestampParserTest.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Editing/EditorTimestampParserTest.cs b/osu.Game.Tests/Editing/EditorTimestampParserTest.cs index 24ac8e32a4..5b9663bcfe 100644 --- a/osu.Game.Tests/Editing/EditorTimestampParserTest.cs +++ b/osu.Game.Tests/Editing/EditorTimestampParserTest.cs @@ -10,12 +10,12 @@ namespace osu.Game.Tests.Editing [TestFixture] public class EditorTimestampParserTest { - public static readonly object?[][] test_cases = + private static readonly object?[][] test_cases = { new object?[] { ":", false, null, null }, new object?[] { "1", true, new TimeSpan(0, 0, 1, 0), null }, new object?[] { "99", true, new TimeSpan(0, 0, 99, 0), null }, - new object?[] { "300", false, null, null }, + new object?[] { "3000", false, null, null }, new object?[] { "1:2", true, new TimeSpan(0, 0, 1, 2), null }, new object?[] { "1:02", true, new TimeSpan(0, 0, 1, 2), null }, new object?[] { "1:92", false, null, null }, From 7ee29667db991e1ff37a860283f2eff6ecd9aa47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 18 Jun 2024 16:46:47 +0200 Subject: [PATCH 1113/2493] Parse plain numbers as millisecond count when parsing timestamp --- osu.Game.Tests/Editing/EditorTimestampParserTest.cs | 6 +++--- osu.Game/Rulesets/Edit/EditorTimestampParser.cs | 9 ++++++++- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Editing/EditorTimestampParserTest.cs b/osu.Game.Tests/Editing/EditorTimestampParserTest.cs index 5b9663bcfe..9c7fae0eaf 100644 --- a/osu.Game.Tests/Editing/EditorTimestampParserTest.cs +++ b/osu.Game.Tests/Editing/EditorTimestampParserTest.cs @@ -13,9 +13,9 @@ namespace osu.Game.Tests.Editing private static readonly object?[][] test_cases = { new object?[] { ":", false, null, null }, - new object?[] { "1", true, new TimeSpan(0, 0, 1, 0), null }, - new object?[] { "99", true, new TimeSpan(0, 0, 99, 0), null }, - new object?[] { "3000", false, null, null }, + new object?[] { "1", true, TimeSpan.FromMilliseconds(1), null }, + new object?[] { "99", true, TimeSpan.FromMilliseconds(99), null }, + new object?[] { "320000", true, TimeSpan.FromMilliseconds(320000), null }, new object?[] { "1:2", true, new TimeSpan(0, 0, 1, 2), null }, new object?[] { "1:02", true, new TimeSpan(0, 0, 1, 2), null }, new object?[] { "1:92", false, null, null }, diff --git a/osu.Game/Rulesets/Edit/EditorTimestampParser.cs b/osu.Game/Rulesets/Edit/EditorTimestampParser.cs index 9c3119d8f4..e6bce12170 100644 --- a/osu.Game/Rulesets/Edit/EditorTimestampParser.cs +++ b/osu.Game/Rulesets/Edit/EditorTimestampParser.cs @@ -32,10 +32,17 @@ namespace osu.Game.Rulesets.Edit /// 1:02:300 (1,2,3) - parses to 01:02:300 with selection /// /// - private static readonly Regex time_regex_lenient = new Regex(@"^(((?\d{1,3})(:(?([0-5]?\d))([:.](?\d{0,3}))?)?)(?\s\([^)]+\))?)$", RegexOptions.Compiled); + private static readonly Regex time_regex_lenient = new Regex(@"^(((?\d{1,3}):(?([0-5]?\d))([:.](?\d{0,3}))?)(?\s\([^)]+\))?)$", RegexOptions.Compiled); public static bool TryParse(string timestamp, [NotNullWhen(true)] out TimeSpan? parsedTime, out string? parsedSelection) { + if (double.TryParse(timestamp, out double msec)) + { + parsedTime = TimeSpan.FromMilliseconds(msec); + parsedSelection = null; + return true; + } + Match match = time_regex_lenient.Match(timestamp); if (!match.Success) From f764ec24cd543cc450f609a7243045ec0ac42316 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 18 Jun 2024 18:32:24 +0200 Subject: [PATCH 1114/2493] Correct xmldoc --- osu.Game/Rulesets/Edit/EditorTimestampParser.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Edit/EditorTimestampParser.cs b/osu.Game/Rulesets/Edit/EditorTimestampParser.cs index e6bce12170..92a692b94e 100644 --- a/osu.Game/Rulesets/Edit/EditorTimestampParser.cs +++ b/osu.Game/Rulesets/Edit/EditorTimestampParser.cs @@ -23,7 +23,7 @@ namespace osu.Game.Rulesets.Edit /// /// /// - /// 1 - parses to 01:00:000 + /// 1 - parses to 00:00:001 (bare numbers are treated as milliseconds) /// 1:2 - parses to 01:02:000 /// 1:02 - parses to 01:02:000 /// 1:92 - does not parse From 8836b98070cda3691ad432bc07097732ef7df00a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 18 Jun 2024 18:32:58 +0200 Subject: [PATCH 1115/2493] Fix new inspection after framework bump --- osu.Game/Screens/Edit/Components/TimeInfoContainer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Components/TimeInfoContainer.cs b/osu.Game/Screens/Edit/Components/TimeInfoContainer.cs index 9e14ec851b..9365402c1c 100644 --- a/osu.Game/Screens/Edit/Components/TimeInfoContainer.cs +++ b/osu.Game/Screens/Edit/Components/TimeInfoContainer.cs @@ -116,7 +116,7 @@ namespace osu.Game.Screens.Edit.Components inputTextBox.Text = editorClock.CurrentTime.ToEditorFormattedString(); Schedule(() => { - GetContainingFocusManager().ChangeFocus(inputTextBox); + GetContainingFocusManager()!.ChangeFocus(inputTextBox); inputTextBox.SelectAll(); }); }; From 6a6ccbc09fce5e23857afaf5feabaa77673dec65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 19 Jun 2024 07:31:53 +0200 Subject: [PATCH 1116/2493] Make list of breaks bindable --- .../Mods/TestSceneCatchModNoScope.cs | 2 +- .../Mods/TestSceneOsuModAlternate.cs | 2 +- .../Mods/TestSceneOsuModNoScope.cs | 2 +- .../Mods/TestSceneOsuModSingleTap.cs | 2 +- .../Mods/TestSceneTaikoModSingleTap.cs | 2 +- osu.Game.Tests/Editing/Checks/CheckBreaksTest.cs | 15 +++++++-------- .../Editing/Checks/CheckDrainLengthTest.cs | 2 +- osu.Game/Beatmaps/Beatmap.cs | 3 ++- osu.Game/Beatmaps/IBeatmap.cs | 3 ++- .../Rulesets/Difficulty/DifficultyCalculator.cs | 3 ++- .../Components/Timeline/TimelineBreakDisplay.cs | 5 +++-- osu.Game/Screens/Edit/EditorBeatmap.cs | 2 +- 12 files changed, 23 insertions(+), 20 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModNoScope.cs b/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModNoScope.cs index c48bf7adc9..c8f7da1aae 100644 --- a/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModNoScope.cs +++ b/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModNoScope.cs @@ -74,7 +74,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Mods StartTime = 5000, } }, - Breaks = new List + Breaks = { new BreakPeriod(2000, 4000), } diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAlternate.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAlternate.cs index 88c81c7a39..7375617aa8 100644 --- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAlternate.cs +++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAlternate.cs @@ -133,7 +133,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods Autoplay = false, Beatmap = new Beatmap { - Breaks = new List + Breaks = { new BreakPeriod(500, 2000), }, diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModNoScope.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModNoScope.cs index 9dfa76fc8e..d3996ebc3b 100644 --- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModNoScope.cs +++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModNoScope.cs @@ -46,7 +46,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods StartTime = 5000, } }, - Breaks = new List + Breaks = { new BreakPeriod(2000, 4000), } diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSingleTap.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSingleTap.cs index 402c680b46..bd2b205ac8 100644 --- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSingleTap.cs +++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSingleTap.cs @@ -132,7 +132,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods Autoplay = false, Beatmap = new Beatmap { - Breaks = new List + Breaks = { new BreakPeriod(500, 2000), }, diff --git a/osu.Game.Rulesets.Taiko.Tests/Mods/TestSceneTaikoModSingleTap.cs b/osu.Game.Rulesets.Taiko.Tests/Mods/TestSceneTaikoModSingleTap.cs index 0cd3b85f8e..3a11a91f82 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Mods/TestSceneTaikoModSingleTap.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Mods/TestSceneTaikoModSingleTap.cs @@ -177,7 +177,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Mods Autoplay = false, Beatmap = new Beatmap { - Breaks = new List + Breaks = { new BreakPeriod(100, 1600), }, diff --git a/osu.Game.Tests/Editing/Checks/CheckBreaksTest.cs b/osu.Game.Tests/Editing/Checks/CheckBreaksTest.cs index 28556566ba..f53dd9a62a 100644 --- a/osu.Game.Tests/Editing/Checks/CheckBreaksTest.cs +++ b/osu.Game.Tests/Editing/Checks/CheckBreaksTest.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Collections.Generic; using System.Linq; using NUnit.Framework; using osu.Game.Beatmaps; @@ -29,7 +28,7 @@ namespace osu.Game.Tests.Editing.Checks { var beatmap = new Beatmap { - Breaks = new List + Breaks = { new BreakPeriod(0, 649) } @@ -52,7 +51,7 @@ namespace osu.Game.Tests.Editing.Checks new HitCircle { StartTime = 0 }, new HitCircle { StartTime = 1_200 } }, - Breaks = new List + Breaks = { new BreakPeriod(100, 751) } @@ -75,7 +74,7 @@ namespace osu.Game.Tests.Editing.Checks new HitCircle { StartTime = 0 }, new HitCircle { StartTime = 1_298 } }, - Breaks = new List + Breaks = { new BreakPeriod(200, 850) } @@ -98,7 +97,7 @@ namespace osu.Game.Tests.Editing.Checks new HitCircle { StartTime = 0 }, new HitCircle { StartTime = 1200 } }, - Breaks = new List + Breaks = { new BreakPeriod(1398, 2300) } @@ -121,7 +120,7 @@ namespace osu.Game.Tests.Editing.Checks new HitCircle { StartTime = 1100 }, new HitCircle { StartTime = 1500 } }, - Breaks = new List + Breaks = { new BreakPeriod(0, 652) } @@ -145,7 +144,7 @@ namespace osu.Game.Tests.Editing.Checks new HitCircle { StartTime = 1_297 }, new HitCircle { StartTime = 1_298 } }, - Breaks = new List + Breaks = { new BreakPeriod(200, 850) } @@ -168,7 +167,7 @@ namespace osu.Game.Tests.Editing.Checks new HitCircle { StartTime = 0 }, new HitCircle { StartTime = 1_300 } }, - Breaks = new List + Breaks = { new BreakPeriod(200, 850) } diff --git a/osu.Game.Tests/Editing/Checks/CheckDrainLengthTest.cs b/osu.Game.Tests/Editing/Checks/CheckDrainLengthTest.cs index 1b5c5c398f..be9aa711cb 100644 --- a/osu.Game.Tests/Editing/Checks/CheckDrainLengthTest.cs +++ b/osu.Game.Tests/Editing/Checks/CheckDrainLengthTest.cs @@ -53,7 +53,7 @@ namespace osu.Game.Tests.Editing.Checks new HitCircle { StartTime = 0 }, new HitCircle { StartTime = 40_000 } }, - Breaks = new List + Breaks = { new BreakPeriod(10_000, 21_000) } diff --git a/osu.Game/Beatmaps/Beatmap.cs b/osu.Game/Beatmaps/Beatmap.cs index ae77e4adcf..510410bc09 100644 --- a/osu.Game/Beatmaps/Beatmap.cs +++ b/osu.Game/Beatmaps/Beatmap.cs @@ -8,6 +8,7 @@ using System.Collections.Generic; using System.Linq; using osu.Game.Beatmaps.ControlPoints; using Newtonsoft.Json; +using osu.Framework.Bindables; using osu.Game.IO.Serialization.Converters; namespace osu.Game.Beatmaps @@ -61,7 +62,7 @@ namespace osu.Game.Beatmaps public ControlPointInfo ControlPointInfo { get; set; } = new ControlPointInfo(); - public List Breaks { get; set; } = new List(); + public BindableList Breaks { get; set; } = new BindableList(); public List UnhandledEventLines { get; set; } = new List(); diff --git a/osu.Game/Beatmaps/IBeatmap.cs b/osu.Game/Beatmaps/IBeatmap.cs index 5cc38e5b84..072e246a36 100644 --- a/osu.Game/Beatmaps/IBeatmap.cs +++ b/osu.Game/Beatmaps/IBeatmap.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; +using osu.Framework.Bindables; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.Timing; using osu.Game.Rulesets.Objects; @@ -40,7 +41,7 @@ namespace osu.Game.Beatmaps /// /// The breaks in this beatmap. /// - List Breaks { get; } + BindableList Breaks { get; } /// /// All lines from the [Events] section which aren't handled in the encoding process yet. diff --git a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs index d37cfc28b9..97e8e15975 100644 --- a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs +++ b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs @@ -9,6 +9,7 @@ using System.Linq; using System.Threading; using JetBrains.Annotations; using osu.Framework.Audio.Track; +using osu.Framework.Bindables; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; @@ -329,7 +330,7 @@ namespace osu.Game.Rulesets.Difficulty set => baseBeatmap.Difficulty = value; } - public List Breaks => baseBeatmap.Breaks; + public BindableList Breaks => baseBeatmap.Breaks; public List UnhandledEventLines => baseBeatmap.UnhandledEventLines; public double TotalBreakTime => baseBeatmap.TotalBreakTime; diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBreakDisplay.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBreakDisplay.cs index 587db23e9a..5fdfda25e5 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBreakDisplay.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBreakDisplay.cs @@ -27,8 +27,9 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline { base.LoadBeatmap(beatmap); - // TODO: this will have to be mutable soon enough - breaks.AddRange(beatmap.Breaks); + breaks.UnbindAll(); + breaks.BindTo(beatmap.Breaks); + breaks.BindCollectionChanged((_, _) => breakCache.Invalidate()); } protected override void Update() diff --git a/osu.Game/Screens/Edit/EditorBeatmap.cs b/osu.Game/Screens/Edit/EditorBeatmap.cs index 5be1d27805..f4be987547 100644 --- a/osu.Game/Screens/Edit/EditorBeatmap.cs +++ b/osu.Game/Screens/Edit/EditorBeatmap.cs @@ -172,7 +172,7 @@ namespace osu.Game.Screens.Edit set => PlayableBeatmap.ControlPointInfo = value; } - public List Breaks => PlayableBeatmap.Breaks; + public BindableList Breaks => PlayableBeatmap.Breaks; public List UnhandledEventLines => PlayableBeatmap.UnhandledEventLines; From 1f692f5fc7f450aa7336a235ee60affcaa0d2fb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 19 Jun 2024 09:01:33 +0200 Subject: [PATCH 1117/2493] Make `BreakPeriod` a struct --- osu.Game/Beatmaps/Timing/BreakPeriod.cs | 11 +++-- .../Components/Timeline/TimelineBreak.cs | 48 +++++++++++-------- .../Timeline/TimelineBreakDisplay.cs | 29 +++-------- 3 files changed, 43 insertions(+), 45 deletions(-) diff --git a/osu.Game/Beatmaps/Timing/BreakPeriod.cs b/osu.Game/Beatmaps/Timing/BreakPeriod.cs index 4c90b16745..f16a3c27a1 100644 --- a/osu.Game/Beatmaps/Timing/BreakPeriod.cs +++ b/osu.Game/Beatmaps/Timing/BreakPeriod.cs @@ -1,11 +1,12 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using osu.Game.Screens.Play; namespace osu.Game.Beatmaps.Timing { - public class BreakPeriod + public readonly struct BreakPeriod : IEquatable { /// /// The minimum duration required for a break to have any effect. @@ -15,12 +16,12 @@ namespace osu.Game.Beatmaps.Timing /// /// The break start time. /// - public double StartTime; + public double StartTime { get; init; } /// /// The break end time. /// - public double EndTime; + public double EndTime { get; init; } /// /// The break duration. @@ -49,5 +50,9 @@ namespace osu.Game.Beatmaps.Timing /// The time to check in milliseconds. /// Whether the time falls within this . public bool Contains(double time) => time >= StartTime && time <= EndTime - BreakOverlay.BREAK_FADE_DURATION; + + public bool Equals(BreakPeriod other) => StartTime.Equals(other.StartTime) && EndTime.Equals(other.EndTime); + public override bool Equals(object? obj) => obj is BreakPeriod other && Equals(other); + public override int GetHashCode() => HashCode.Combine(StartTime, EndTime); } } diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBreak.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBreak.cs index 785eba2042..cec4b9b659 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBreak.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBreak.cs @@ -5,6 +5,7 @@ using System; using System.Diagnostics; 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; @@ -20,11 +21,11 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline { public partial class TimelineBreak : CompositeDrawable { - public BreakPeriod Break { get; } + public Bindable Break { get; } = new Bindable(); public TimelineBreak(BreakPeriod b) { - Break = b; + Break.Value = b; } [BackgroundDependencyLoader] @@ -48,40 +49,46 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline Alpha = 0.4f, }, }, - new DragHandle(Break, isStartHandle: true) + new DragHandle(isStartHandle: true) { + Break = { BindTarget = Break }, Anchor = Anchor.TopLeft, Origin = Anchor.TopLeft, - Action = (time, breakPeriod) => breakPeriod.StartTime = time, + Action = (time, breakPeriod) => breakPeriod with { StartTime = time }, }, - new DragHandle(Break, isStartHandle: false) + new DragHandle(isStartHandle: false) { + Break = { BindTarget = Break }, Anchor = Anchor.TopRight, Origin = Anchor.TopRight, - Action = (time, breakPeriod) => breakPeriod.EndTime = time, + Action = (time, breakPeriod) => breakPeriod with { EndTime = time }, }, }; } - protected override void Update() + protected override void LoadComplete() { - base.Update(); + base.LoadComplete(); - X = (float)Break.StartTime; - Width = (float)Break.Duration; + Break.BindValueChanged(_ => + { + X = (float)Break.Value.StartTime; + Width = (float)Break.Value.Duration; + }, true); } private partial class DragHandle : FillFlowContainer { + public Bindable Break { get; } = new Bindable(); + public new Anchor Anchor { get => base.Anchor; init => base.Anchor = value; } - public Action? Action { get; init; } + public Func? Action { get; init; } - private readonly BreakPeriod breakPeriod; private readonly bool isStartHandle; private Container handle = null!; @@ -99,9 +106,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline [Resolved] private OsuColour colours { get; set; } = null!; - public DragHandle(BreakPeriod breakPeriod, bool isStartHandle) + public DragHandle(bool isStartHandle) { - this.breakPeriod = breakPeriod; this.isStartHandle = isStartHandle; } @@ -164,13 +170,13 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline changeHandler?.BeginChange(); updateState(); - double min = beatmap.HitObjects.Last(ho => ho.GetEndTime() <= breakPeriod.StartTime).GetEndTime(); - double max = beatmap.HitObjects.First(ho => ho.StartTime >= breakPeriod.EndTime).StartTime; + double min = beatmap.HitObjects.Last(ho => ho.GetEndTime() <= Break.Value.StartTime).GetEndTime(); + double max = beatmap.HitObjects.First(ho => ho.StartTime >= Break.Value.EndTime).StartTime; if (isStartHandle) - max = Math.Min(max, breakPeriod.EndTime - BreakPeriod.MIN_BREAK_DURATION); + max = Math.Min(max, Break.Value.EndTime - BreakPeriod.MIN_BREAK_DURATION); else - min = Math.Max(min, breakPeriod.StartTime + BreakPeriod.MIN_BREAK_DURATION); + min = Math.Max(min, Break.Value.StartTime + BreakPeriod.MIN_BREAK_DURATION); allowedDragRange = (min, max); @@ -183,11 +189,13 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline Debug.Assert(allowedDragRange != null); - if (timeline.FindSnappedPositionAndTime(e.ScreenSpaceMousePosition).Time is double time + if (Action != null + && timeline.FindSnappedPositionAndTime(e.ScreenSpaceMousePosition).Time is double time && time > allowedDragRange.Value.min && time < allowedDragRange.Value.max) { - Action?.Invoke(time, breakPeriod); + int index = beatmap.Breaks.IndexOf(Break.Value); + beatmap.Breaks[index] = Break.Value = Action.Invoke(time, Break.Value); } updateState(); diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBreakDisplay.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBreakDisplay.cs index 5fdfda25e5..eaa31aea1e 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBreakDisplay.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBreakDisplay.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.Specialized; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Caching; @@ -29,7 +30,11 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline breaks.UnbindAll(); breaks.BindTo(beatmap.Breaks); - breaks.BindCollectionChanged((_, _) => breakCache.Invalidate()); + breaks.BindCollectionChanged((_, e) => + { + if (e.Action != NotifyCollectionChangedAction.Replace) + breakCache.Invalidate(); + }); } protected override void Update() @@ -57,14 +62,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline private void recreateBreaks() { - // Remove groups outside the visible range - foreach (TimelineBreak drawableBreak in this) - { - if (!shouldBeVisible(drawableBreak.Break)) - drawableBreak.Expire(); - } + Clear(); - // Add remaining ones for (int i = 0; i < breaks.Count; i++) { var breakPeriod = breaks[i]; @@ -72,20 +71,6 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline if (!shouldBeVisible(breakPeriod)) continue; - bool alreadyVisible = false; - - foreach (var b in this) - { - if (ReferenceEquals(b.Break, breakPeriod)) - { - alreadyVisible = true; - break; - } - } - - if (alreadyVisible) - continue; - Add(new TimelineBreak(breakPeriod)); } } From 4022a8b06cc056b210047f55bff4b3a59d7c24e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 19 Jun 2024 10:11:04 +0200 Subject: [PATCH 1118/2493] Implement automatic break period generation --- osu.Game/Beatmaps/Timing/BreakPeriod.cs | 32 ++++++++-- osu.Game/Rulesets/Edit/Checks/CheckBreaks.cs | 14 ++-- .../Components/Timeline/TimelineBreak.cs | 4 +- osu.Game/Screens/Edit/EditorBeatmap.cs | 2 +- .../Screens/Edit/EditorBeatmapProcessor.cs | 64 +++++++++++++++++++ osu.Game/Screens/Edit/ManualBreakPeriod.cs | 15 +++++ 6 files changed, 113 insertions(+), 18 deletions(-) create mode 100644 osu.Game/Screens/Edit/EditorBeatmapProcessor.cs create mode 100644 osu.Game/Screens/Edit/ManualBreakPeriod.cs diff --git a/osu.Game/Beatmaps/Timing/BreakPeriod.cs b/osu.Game/Beatmaps/Timing/BreakPeriod.cs index f16a3c27a1..89f0fd6a55 100644 --- a/osu.Game/Beatmaps/Timing/BreakPeriod.cs +++ b/osu.Game/Beatmaps/Timing/BreakPeriod.cs @@ -6,22 +6,39 @@ using osu.Game.Screens.Play; namespace osu.Game.Beatmaps.Timing { - public readonly struct BreakPeriod : IEquatable + public record BreakPeriod { + /// + /// The minimum gap between the start of the break and the previous object. + /// + public const double GAP_BEFORE_BREAK = 200; + + /// + /// The minimum gap between the end of the break and the next object. + /// Based on osu! preempt time at AR=10. + /// See also: https://github.com/ppy/osu/issues/14330#issuecomment-1002158551 + /// + public const double GAP_AFTER_BREAK = 450; + /// /// The minimum duration required for a break to have any effect. /// public const double MIN_BREAK_DURATION = 650; + /// + /// The minimum required duration of a gap between two objects such that a break can be placed between them. + /// + public const double MIN_GAP_DURATION = GAP_BEFORE_BREAK + MIN_BREAK_DURATION + GAP_AFTER_BREAK; + /// /// The break start time. /// - public double StartTime { get; init; } + public double StartTime { get; } /// /// The break end time. /// - public double EndTime { get; init; } + public double EndTime { get; } /// /// The break duration. @@ -51,8 +68,13 @@ namespace osu.Game.Beatmaps.Timing /// Whether the time falls within this . public bool Contains(double time) => time >= StartTime && time <= EndTime - BreakOverlay.BREAK_FADE_DURATION; - public bool Equals(BreakPeriod other) => StartTime.Equals(other.StartTime) && EndTime.Equals(other.EndTime); - public override bool Equals(object? obj) => obj is BreakPeriod other && Equals(other); + public bool Intersects(BreakPeriod other) => StartTime <= other.EndTime && EndTime >= other.StartTime; + + public virtual bool Equals(BreakPeriod? other) => + other != null + && StartTime == other.StartTime + && EndTime == other.EndTime; + public override int GetHashCode() => HashCode.Combine(StartTime, EndTime); } } diff --git a/osu.Game/Rulesets/Edit/Checks/CheckBreaks.cs b/osu.Game/Rulesets/Edit/Checks/CheckBreaks.cs index 0842ff5453..f7be36beab 100644 --- a/osu.Game/Rulesets/Edit/Checks/CheckBreaks.cs +++ b/osu.Game/Rulesets/Edit/Checks/CheckBreaks.cs @@ -13,13 +13,7 @@ namespace osu.Game.Rulesets.Edit.Checks { // Breaks may be off by 1 ms. private const int leniency_threshold = 1; - 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 - // 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[] @@ -45,8 +39,8 @@ namespace osu.Game.Rulesets.Edit.Checks if (previousObjectEndTimeIndex >= 0) { double gapBeforeBreak = breakPeriod.StartTime - endTimes[previousObjectEndTimeIndex]; - if (gapBeforeBreak < minimum_gap_before_break - leniency_threshold) - yield return new IssueTemplateEarlyStart(this).Create(breakPeriod.StartTime, minimum_gap_before_break - gapBeforeBreak); + if (gapBeforeBreak < BreakPeriod.GAP_BEFORE_BREAK - leniency_threshold) + yield return new IssueTemplateEarlyStart(this).Create(breakPeriod.StartTime, BreakPeriod.GAP_BEFORE_BREAK - gapBeforeBreak); } int nextObjectStartTimeIndex = startTimes.BinarySearch(breakPeriod.EndTime); @@ -55,8 +49,8 @@ namespace osu.Game.Rulesets.Edit.Checks if (nextObjectStartTimeIndex < startTimes.Count) { double gapAfterBreak = startTimes[nextObjectStartTimeIndex] - breakPeriod.EndTime; - if (gapAfterBreak < min_end_threshold - leniency_threshold) - yield return new IssueTemplateLateEnd(this).Create(breakPeriod.StartTime, min_end_threshold - gapAfterBreak); + if (gapAfterBreak < BreakPeriod.GAP_AFTER_BREAK - leniency_threshold) + yield return new IssueTemplateLateEnd(this).Create(breakPeriod.StartTime, BreakPeriod.GAP_AFTER_BREAK - gapAfterBreak); } } } diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBreak.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBreak.cs index cec4b9b659..b9651ccd81 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBreak.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBreak.cs @@ -54,14 +54,14 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline Break = { BindTarget = Break }, Anchor = Anchor.TopLeft, Origin = Anchor.TopLeft, - Action = (time, breakPeriod) => breakPeriod with { StartTime = time }, + Action = (time, breakPeriod) => new ManualBreakPeriod(time, breakPeriod.EndTime), }, new DragHandle(isStartHandle: false) { Break = { BindTarget = Break }, Anchor = Anchor.TopRight, Origin = Anchor.TopRight, - Action = (time, breakPeriod) => breakPeriod with { EndTime = time }, + Action = (time, breakPeriod) => new ManualBreakPeriod(breakPeriod.StartTime, time), }, }; } diff --git a/osu.Game/Screens/Edit/EditorBeatmap.cs b/osu.Game/Screens/Edit/EditorBeatmap.cs index f4be987547..80586a923d 100644 --- a/osu.Game/Screens/Edit/EditorBeatmap.cs +++ b/osu.Game/Screens/Edit/EditorBeatmap.cs @@ -105,7 +105,7 @@ namespace osu.Game.Screens.Edit BeatmapSkin.BeatmapSkinChanged += SaveState; } - beatmapProcessor = playableBeatmap.BeatmapInfo.Ruleset.CreateInstance().CreateBeatmapProcessor(this); + beatmapProcessor = new EditorBeatmapProcessor(this, playableBeatmap.BeatmapInfo.Ruleset.CreateInstance()); foreach (var obj in HitObjects) trackStartTime(obj); diff --git a/osu.Game/Screens/Edit/EditorBeatmapProcessor.cs b/osu.Game/Screens/Edit/EditorBeatmapProcessor.cs new file mode 100644 index 0000000000..5b1cf281bb --- /dev/null +++ b/osu.Game/Screens/Edit/EditorBeatmapProcessor.cs @@ -0,0 +1,64 @@ +// 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.Game.Beatmaps; +using osu.Game.Beatmaps.Timing; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Objects; + +namespace osu.Game.Screens.Edit +{ + public class EditorBeatmapProcessor : IBeatmapProcessor + { + public IBeatmap Beatmap { get; } + + private readonly IBeatmapProcessor? rulesetBeatmapProcessor; + + public EditorBeatmapProcessor(IBeatmap beatmap, Ruleset ruleset) + { + Beatmap = beatmap; + rulesetBeatmapProcessor = ruleset.CreateBeatmapProcessor(beatmap); + } + + public void PreProcess() + { + rulesetBeatmapProcessor?.PreProcess(); + } + + public void PostProcess() + { + rulesetBeatmapProcessor?.PostProcess(); + + autoGenerateBreaks(); + } + + private void autoGenerateBreaks() + { + Beatmap.Breaks.RemoveAll(b => b is not ManualBreakPeriod); + + for (int i = 1; i < Beatmap.HitObjects.Count; ++i) + { + double previousObjectEndTime = Beatmap.HitObjects[i - 1].GetEndTime(); + double nextObjectStartTime = Beatmap.HitObjects[i].StartTime; + + if (nextObjectStartTime - previousObjectEndTime < BreakPeriod.MIN_GAP_DURATION) + continue; + + double breakStartTime = previousObjectEndTime + BreakPeriod.GAP_BEFORE_BREAK; + double breakEndTime = nextObjectStartTime - Math.Max(BreakPeriod.GAP_AFTER_BREAK, Beatmap.ControlPointInfo.TimingPointAt(nextObjectStartTime).BeatLength * 2); + + if (breakEndTime - breakStartTime < BreakPeriod.MIN_BREAK_DURATION) + continue; + + var breakPeriod = new BreakPeriod(breakStartTime, breakEndTime); + + if (Beatmap.Breaks.Any(b => b.Intersects(breakPeriod))) + continue; + + Beatmap.Breaks.Add(breakPeriod); + } + } + } +} diff --git a/osu.Game/Screens/Edit/ManualBreakPeriod.cs b/osu.Game/Screens/Edit/ManualBreakPeriod.cs new file mode 100644 index 0000000000..719784b500 --- /dev/null +++ b/osu.Game/Screens/Edit/ManualBreakPeriod.cs @@ -0,0 +1,15 @@ +// 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.Beatmaps.Timing; + +namespace osu.Game.Screens.Edit +{ + public record ManualBreakPeriod : BreakPeriod + { + public ManualBreakPeriod(double startTime, double endTime) + : base(startTime, endTime) + { + } + } +} From 58701b17f842b64c6824ed3c8806cef6565905ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 19 Jun 2024 10:22:14 +0200 Subject: [PATCH 1119/2493] Add patcher support for breaks --- .../Edit/LegacyEditorBeatmapPatcher.cs | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/osu.Game/Screens/Edit/LegacyEditorBeatmapPatcher.cs b/osu.Game/Screens/Edit/LegacyEditorBeatmapPatcher.cs index bb9f702cb5..a1ee41fc48 100644 --- a/osu.Game/Screens/Edit/LegacyEditorBeatmapPatcher.cs +++ b/osu.Game/Screens/Edit/LegacyEditorBeatmapPatcher.cs @@ -45,6 +45,7 @@ namespace osu.Game.Screens.Edit editorBeatmap.BeginChange(); processHitObjects(result, () => newBeatmap ??= readBeatmap(newState)); processTimingPoints(() => newBeatmap ??= readBeatmap(newState)); + processBreaks(() => newBeatmap ??= readBeatmap(newState)); processHitObjectLocalData(() => newBeatmap ??= readBeatmap(newState)); editorBeatmap.EndChange(); } @@ -75,6 +76,27 @@ namespace osu.Game.Screens.Edit } } + private void processBreaks(Func getNewBeatmap) + { + var newBreaks = getNewBeatmap().Breaks.ToArray(); + + foreach (var oldBreak in editorBeatmap.Breaks.ToArray()) + { + if (newBreaks.Any(b => b.Equals(oldBreak))) + continue; + + editorBeatmap.Breaks.Remove(oldBreak); + } + + foreach (var newBreak in newBreaks) + { + if (editorBeatmap.Breaks.Any(b => b.Equals(newBreak))) + continue; + + editorBeatmap.Breaks.Add(newBreak); + } + } + private void processHitObjects(DiffResult result, Func getNewBeatmap) { findChangedIndices(result, LegacyDecoder.Section.HitObjects, out var removedIndices, out var addedIndices); From 7ed587b783ce5c69de2eceb36f37f36cbd8dae9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 19 Jun 2024 10:26:01 +0200 Subject: [PATCH 1120/2493] Fix summary timeline not reloading properly on break addition/removal --- .../Timelines/Summary/Parts/BreakPart.cs | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/BreakPart.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/BreakPart.cs index 41ecb44d9d..1ba552d646 100644 --- a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/BreakPart.cs +++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/BreakPart.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Shapes; using osu.Game.Beatmaps.Timing; @@ -14,11 +15,19 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts /// public partial class BreakPart : TimelinePart { + private readonly BindableList breaks = new BindableList(); + protected override void LoadBeatmap(EditorBeatmap beatmap) { base.LoadBeatmap(beatmap); - foreach (var breakPeriod in beatmap.Breaks) - Add(new BreakVisualisation(breakPeriod)); + + breaks.UnbindAll(); + breaks.BindTo(beatmap.Breaks); + breaks.BindCollectionChanged((_, _) => + { + foreach (var breakPeriod in beatmap.Breaks) + Add(new BreakVisualisation(breakPeriod)); + }, true); } private partial class BreakVisualisation : Circle @@ -31,12 +40,6 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts RelativePositionAxes = Axes.X; RelativeSizeAxes = Axes.Both; - } - - protected override void Update() - { - base.Update(); - X = (float)breakPeriod.StartTime; Width = (float)breakPeriod.Duration; } From 7311a7ffd711c46fd10b693c2ef960697fa8f8b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 19 Jun 2024 10:51:37 +0200 Subject: [PATCH 1121/2493] Purge manual breaks if they intersect with an actual hitobject --- osu.Game/Screens/Edit/EditorBeatmapProcessor.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/osu.Game/Screens/Edit/EditorBeatmapProcessor.cs b/osu.Game/Screens/Edit/EditorBeatmapProcessor.cs index 5b1cf281bb..37bf915cec 100644 --- a/osu.Game/Screens/Edit/EditorBeatmapProcessor.cs +++ b/osu.Game/Screens/Edit/EditorBeatmapProcessor.cs @@ -38,6 +38,12 @@ namespace osu.Game.Screens.Edit { Beatmap.Breaks.RemoveAll(b => b is not ManualBreakPeriod); + foreach (var manualBreak in Beatmap.Breaks.ToList()) + { + if (Beatmap.HitObjects.Any(ho => ho.StartTime <= manualBreak.EndTime && ho.GetEndTime() >= manualBreak.StartTime)) + Beatmap.Breaks.Remove(manualBreak); + } + for (int i = 1; i < Beatmap.HitObjects.Count; ++i) { double previousObjectEndTime = Beatmap.HitObjects[i - 1].GetEndTime(); From 439079876157020272063e39d957590b0d5a6651 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 19 Jun 2024 11:14:38 +0200 Subject: [PATCH 1122/2493] Add test coverage for automatic break generation --- .../TestSceneEditorBeatmapProcessor.cs | 300 ++++++++++++++++++ 1 file changed, 300 insertions(+) create mode 100644 osu.Game.Tests/Editing/TestSceneEditorBeatmapProcessor.cs diff --git a/osu.Game.Tests/Editing/TestSceneEditorBeatmapProcessor.cs b/osu.Game.Tests/Editing/TestSceneEditorBeatmapProcessor.cs new file mode 100644 index 0000000000..02ce3815ec --- /dev/null +++ b/osu.Game.Tests/Editing/TestSceneEditorBeatmapProcessor.cs @@ -0,0 +1,300 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Beatmaps.Timing; +using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Screens.Edit; + +namespace osu.Game.Tests.Editing +{ + [TestFixture] + public class TestSceneEditorBeatmapProcessor + { + [Test] + public void TestEmptyBeatmap() + { + var controlPoints = new ControlPointInfo(); + controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 }); + var beatmap = new Beatmap + { + ControlPointInfo = controlPoints, + }; + + var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new OsuRuleset()); + beatmapProcessor.PreProcess(); + beatmapProcessor.PostProcess(); + + Assert.That(beatmap.Breaks, Is.Empty); + } + + [Test] + public void TestSingleObjectBeatmap() + { + var controlPoints = new ControlPointInfo(); + controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 }); + var beatmap = new Beatmap + { + ControlPointInfo = controlPoints, + HitObjects = + { + new HitCircle { StartTime = 1000 }, + } + }; + + var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new OsuRuleset()); + beatmapProcessor.PreProcess(); + beatmapProcessor.PostProcess(); + + Assert.That(beatmap.Breaks, Is.Empty); + } + + [Test] + public void TestTwoObjectsCloseTogether() + { + var controlPoints = new ControlPointInfo(); + controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 }); + var beatmap = new Beatmap + { + ControlPointInfo = controlPoints, + HitObjects = + { + new HitCircle { StartTime = 1000 }, + new HitCircle { StartTime = 2000 }, + } + }; + + var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new OsuRuleset()); + beatmapProcessor.PreProcess(); + beatmapProcessor.PostProcess(); + + Assert.That(beatmap.Breaks, Is.Empty); + } + + [Test] + public void TestTwoObjectsFarApart() + { + var controlPoints = new ControlPointInfo(); + controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 }); + var beatmap = new Beatmap + { + ControlPointInfo = controlPoints, + HitObjects = + { + new HitCircle { StartTime = 1000 }, + new HitCircle { StartTime = 5000 }, + } + }; + + var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new OsuRuleset()); + beatmapProcessor.PreProcess(); + beatmapProcessor.PostProcess(); + + Assert.Multiple(() => + { + Assert.That(beatmap.Breaks, Has.Count.EqualTo(1)); + Assert.That(beatmap.Breaks[0].StartTime, Is.EqualTo(1200)); + Assert.That(beatmap.Breaks[0].EndTime, Is.EqualTo(4000)); + }); + } + + [Test] + public void TestBreaksAreFused() + { + var controlPoints = new ControlPointInfo(); + controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 }); + var beatmap = new Beatmap + { + ControlPointInfo = controlPoints, + HitObjects = + { + new HitCircle { StartTime = 1000 }, + new HitCircle { StartTime = 9000 }, + }, + Breaks = + { + new BreakPeriod(1200, 4000), + new BreakPeriod(5200, 8000), + } + }; + + var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new OsuRuleset()); + beatmapProcessor.PreProcess(); + beatmapProcessor.PostProcess(); + + Assert.Multiple(() => + { + Assert.That(beatmap.Breaks, Has.Count.EqualTo(1)); + Assert.That(beatmap.Breaks[0].StartTime, Is.EqualTo(1200)); + Assert.That(beatmap.Breaks[0].EndTime, Is.EqualTo(8000)); + }); + } + + [Test] + public void TestBreaksAreSplit() + { + var controlPoints = new ControlPointInfo(); + controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 }); + var beatmap = new Beatmap + { + ControlPointInfo = controlPoints, + HitObjects = + { + new HitCircle { StartTime = 1000 }, + new HitCircle { StartTime = 5000 }, + new HitCircle { StartTime = 9000 }, + }, + Breaks = + { + new BreakPeriod(1200, 8000), + } + }; + + var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new OsuRuleset()); + beatmapProcessor.PreProcess(); + beatmapProcessor.PostProcess(); + + Assert.Multiple(() => + { + Assert.That(beatmap.Breaks, Has.Count.EqualTo(2)); + Assert.That(beatmap.Breaks[0].StartTime, Is.EqualTo(1200)); + Assert.That(beatmap.Breaks[0].EndTime, Is.EqualTo(4000)); + Assert.That(beatmap.Breaks[1].StartTime, Is.EqualTo(5200)); + Assert.That(beatmap.Breaks[1].EndTime, Is.EqualTo(8000)); + }); + } + + [Test] + public void TestBreaksAreNudged() + { + var controlPoints = new ControlPointInfo(); + controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 }); + var beatmap = new Beatmap + { + ControlPointInfo = controlPoints, + HitObjects = + { + new HitCircle { StartTime = 1100 }, + new HitCircle { StartTime = 9000 }, + }, + Breaks = + { + new BreakPeriod(1200, 8000), + } + }; + + var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new OsuRuleset()); + beatmapProcessor.PreProcess(); + beatmapProcessor.PostProcess(); + + Assert.Multiple(() => + { + Assert.That(beatmap.Breaks, Has.Count.EqualTo(1)); + Assert.That(beatmap.Breaks[0].StartTime, Is.EqualTo(1300)); + Assert.That(beatmap.Breaks[0].EndTime, Is.EqualTo(8000)); + }); + } + + [Test] + public void TestManualBreaksAreNotFused() + { + var controlPoints = new ControlPointInfo(); + controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 }); + var beatmap = new Beatmap + { + ControlPointInfo = controlPoints, + HitObjects = + { + new HitCircle { StartTime = 1000 }, + new HitCircle { StartTime = 9000 }, + }, + Breaks = + { + new ManualBreakPeriod(1200, 4000), + new ManualBreakPeriod(5200, 8000), + } + }; + + var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new OsuRuleset()); + beatmapProcessor.PreProcess(); + beatmapProcessor.PostProcess(); + + Assert.Multiple(() => + { + Assert.That(beatmap.Breaks, Has.Count.EqualTo(2)); + Assert.That(beatmap.Breaks[0].StartTime, Is.EqualTo(1200)); + Assert.That(beatmap.Breaks[0].EndTime, Is.EqualTo(4000)); + Assert.That(beatmap.Breaks[1].StartTime, Is.EqualTo(5200)); + Assert.That(beatmap.Breaks[1].EndTime, Is.EqualTo(8000)); + }); + } + + [Test] + public void TestManualBreaksAreSplit() + { + var controlPoints = new ControlPointInfo(); + controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 }); + var beatmap = new Beatmap + { + ControlPointInfo = controlPoints, + HitObjects = + { + new HitCircle { StartTime = 1000 }, + new HitCircle { StartTime = 5000 }, + new HitCircle { StartTime = 9000 }, + }, + Breaks = + { + new ManualBreakPeriod(1200, 8000), + } + }; + + var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new OsuRuleset()); + beatmapProcessor.PreProcess(); + beatmapProcessor.PostProcess(); + + Assert.Multiple(() => + { + Assert.That(beatmap.Breaks, Has.Count.EqualTo(2)); + Assert.That(beatmap.Breaks[0].StartTime, Is.EqualTo(1200)); + Assert.That(beatmap.Breaks[0].EndTime, Is.EqualTo(4000)); + Assert.That(beatmap.Breaks[1].StartTime, Is.EqualTo(5200)); + Assert.That(beatmap.Breaks[1].EndTime, Is.EqualTo(8000)); + }); + } + + [Test] + public void TestManualBreaksAreNotNudged() + { + var controlPoints = new ControlPointInfo(); + controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 }); + var beatmap = new Beatmap + { + ControlPointInfo = controlPoints, + HitObjects = + { + new HitCircle { StartTime = 1000 }, + new HitCircle { StartTime = 9000 }, + }, + Breaks = + { + new ManualBreakPeriod(1200, 8800), + } + }; + + var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new OsuRuleset()); + beatmapProcessor.PreProcess(); + beatmapProcessor.PostProcess(); + + Assert.Multiple(() => + { + Assert.That(beatmap.Breaks, Has.Count.EqualTo(1)); + Assert.That(beatmap.Breaks[0].StartTime, Is.EqualTo(1200)); + Assert.That(beatmap.Breaks[0].EndTime, Is.EqualTo(8800)); + }); + } + } +} From 2d9c3fbed24aed45f044db674b8ed6273ca9e9f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 19 Jun 2024 11:21:57 +0200 Subject: [PATCH 1123/2493] Remove no-longer-necessary null propagation --- osu.Game/Screens/Edit/EditorBeatmap.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Edit/EditorBeatmap.cs b/osu.Game/Screens/Edit/EditorBeatmap.cs index 80586a923d..ae0fd9130f 100644 --- a/osu.Game/Screens/Edit/EditorBeatmap.cs +++ b/osu.Game/Screens/Edit/EditorBeatmap.cs @@ -349,13 +349,13 @@ namespace osu.Game.Screens.Edit if (batchPendingUpdates.Count == 0 && batchPendingDeletes.Count == 0 && batchPendingInserts.Count == 0) return; - beatmapProcessor?.PreProcess(); + beatmapProcessor.PreProcess(); foreach (var h in batchPendingDeletes) processHitObject(h); foreach (var h in batchPendingInserts) processHitObject(h); foreach (var h in batchPendingUpdates) processHitObject(h); - beatmapProcessor?.PostProcess(); + beatmapProcessor.PostProcess(); BeatmapReprocessed?.Invoke(); From 8757e08c2c98e23566af8dd60d7486142a9c8bd5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 19 Jun 2024 11:32:08 +0200 Subject: [PATCH 1124/2493] Fix test failures due to automatic break generation kicking in --- osu.Game.Tests/Visual/Editing/TestSceneEditorChangeStates.cs | 4 ++++ osu.Game/Tests/Beatmaps/TestBeatmap.cs | 4 +++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorChangeStates.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorChangeStates.cs index 278b6e9626..7827347b1f 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorChangeStates.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorChangeStates.cs @@ -4,10 +4,12 @@ #nullable disable using NUnit.Framework; +using osu.Game.Beatmaps; using osu.Game.Rulesets; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Tests.Beatmaps; namespace osu.Game.Tests.Visual.Editing { @@ -15,6 +17,8 @@ namespace osu.Game.Tests.Visual.Editing { protected override Ruleset CreateEditorRuleset() => new OsuRuleset(); + protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(ruleset, false); + [Test] public void TestSelectedObjects() { diff --git a/osu.Game/Tests/Beatmaps/TestBeatmap.cs b/osu.Game/Tests/Beatmaps/TestBeatmap.cs index de7bcfcfaa..31ad2de62e 100644 --- a/osu.Game/Tests/Beatmaps/TestBeatmap.cs +++ b/osu.Game/Tests/Beatmaps/TestBeatmap.cs @@ -26,11 +26,13 @@ namespace osu.Game.Tests.Beatmaps BeatmapInfo = baseBeatmap.BeatmapInfo; ControlPointInfo = baseBeatmap.ControlPointInfo; - Breaks = baseBeatmap.Breaks; UnhandledEventLines = baseBeatmap.UnhandledEventLines; if (withHitObjects) + { HitObjects = baseBeatmap.HitObjects; + Breaks = baseBeatmap.Breaks; + } BeatmapInfo.Ruleset = ruleset; BeatmapInfo.Length = 75000; From 00a866b699403367fdf8de66e1a7e0e8e5f55af3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 19 Jun 2024 20:30:43 +0800 Subject: [PATCH 1125/2493] Change colour to match bottom timeline (and adjust tween sligthly) --- .../Edit/Compose/Components/Timeline/TimelineBreak.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBreak.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBreak.cs index 785eba2042..ec963d08c9 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBreak.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBreak.cs @@ -44,7 +44,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline Child = new Box { RelativeSizeAxes = Axes.Both, - Colour = colours.GreyCarmineLight, + Colour = colours.PurpleLight, Alpha = 0.4f, }, }, @@ -204,12 +204,12 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline { bool active = IsHovered || IsDragged; - var colour = colours.GreyCarmineLighter; + var colour = colours.PurpleLighter; if (active) colour = colour.Lighten(0.3f); this.FadeColour(colour, 400, Easing.OutQuint); - handle.ResizeWidthTo(active ? 20 : 10, 400, Easing.OutElastic); + handle.ResizeWidthTo(active ? 20 : 10, 400, Easing.OutElasticHalf); } } } From 617c1341d722224862e6b0af8b375b61d290158e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 19 Jun 2024 14:53:09 +0200 Subject: [PATCH 1126/2493] Make `(Manual)BreakPeriod` a class again CodeFileSanity doesn't like records and it being a record wasn't doing much anymore anyway. --- osu.Game/Beatmaps/Timing/BreakPeriod.cs | 2 +- osu.Game/Screens/Edit/ManualBreakPeriod.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Beatmaps/Timing/BreakPeriod.cs b/osu.Game/Beatmaps/Timing/BreakPeriod.cs index 89f0fd6a55..d8b500227a 100644 --- a/osu.Game/Beatmaps/Timing/BreakPeriod.cs +++ b/osu.Game/Beatmaps/Timing/BreakPeriod.cs @@ -6,7 +6,7 @@ using osu.Game.Screens.Play; namespace osu.Game.Beatmaps.Timing { - public record BreakPeriod + public class BreakPeriod : IEquatable { /// /// The minimum gap between the start of the break and the previous object. diff --git a/osu.Game/Screens/Edit/ManualBreakPeriod.cs b/osu.Game/Screens/Edit/ManualBreakPeriod.cs index 719784b500..3ab77d84ce 100644 --- a/osu.Game/Screens/Edit/ManualBreakPeriod.cs +++ b/osu.Game/Screens/Edit/ManualBreakPeriod.cs @@ -5,7 +5,7 @@ using osu.Game.Beatmaps.Timing; namespace osu.Game.Screens.Edit { - public record ManualBreakPeriod : BreakPeriod + public class ManualBreakPeriod : BreakPeriod { public ManualBreakPeriod(double startTime, double endTime) : base(startTime, endTime) From a718af8af5eb8878d3237c728df66525fa72c2ac Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 19 Jun 2024 22:02:10 +0800 Subject: [PATCH 1127/2493] Adjust break colours to match closer to stable --- .../Edit/Components/Timelines/Summary/Parts/BreakPart.cs | 6 +----- .../Edit/Compose/Components/Timeline/TimelineBreak.cs | 6 +++--- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/BreakPart.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/BreakPart.cs index 1ba552d646..50062e8465 100644 --- a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/BreakPart.cs +++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/BreakPart.cs @@ -32,12 +32,8 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts private partial class BreakVisualisation : Circle { - private readonly BreakPeriod breakPeriod; - public BreakVisualisation(BreakPeriod breakPeriod) { - this.breakPeriod = breakPeriod; - RelativePositionAxes = Axes.X; RelativeSizeAxes = Axes.Both; X = (float)breakPeriod.StartTime; @@ -45,7 +41,7 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts } [BackgroundDependencyLoader] - private void load(OsuColour colours) => Colour = colours.GreyCarmineLight; + private void load(OsuColour colours) => Colour = colours.Gray7; } } } diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBreak.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBreak.cs index b1a26bbe14..608c2bdab1 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBreak.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBreak.cs @@ -45,8 +45,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline Child = new Box { RelativeSizeAxes = Axes.Both, - Colour = colours.PurpleLight, - Alpha = 0.4f, + Colour = colours.Gray5, + Alpha = 0.7f, }, }, new DragHandle(isStartHandle: true) @@ -212,7 +212,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline { bool active = IsHovered || IsDragged; - var colour = colours.PurpleLighter; + var colour = colours.Gray8; if (active) colour = colour.Lighten(0.3f); From ce4567f87b3abd3e436212a6177ee5c960d2bb56 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Wed, 19 Jun 2024 20:46:55 +0200 Subject: [PATCH 1128/2493] adjust rotation bounds based on grid type --- .../Edit/OsuGridToolboxGroup.cs | 24 +++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs b/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs index 76e735449a..8cffdfbe1d 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs @@ -65,8 +65,8 @@ namespace osu.Game.Rulesets.Osu.Edit /// public BindableFloat GridLinesRotation { get; } = new BindableFloat(0f) { - MinValue = -45f, - MaxValue = 45f, + MinValue = -180f, + MaxValue = 180f, Precision = 1f }; @@ -191,6 +191,26 @@ namespace osu.Game.Rulesets.Osu.Edit gridTypeButtons.FadeTo(v.NewValue ? 1f : 0f, 500, Easing.OutQuint); gridTypeButtons.BypassAutoSizeAxes = !v.NewValue ? Axes.Y : Axes.None; }, true); + + GridType.BindValueChanged(v => + { + GridLinesRotation.Disabled = v.NewValue == PositionSnapGridType.Circle; + + switch (v.NewValue) + { + case PositionSnapGridType.Square: + GridLinesRotation.Value = (GridLinesRotation.Value + 405) % 90 - 45; + GridLinesRotation.MinValue = -45; + GridLinesRotation.MaxValue = 45; + break; + + case PositionSnapGridType.Triangle: + GridLinesRotation.Value = (GridLinesRotation.Value + 390) % 60 - 30; + GridLinesRotation.MinValue = -30; + GridLinesRotation.MaxValue = 30; + break; + } + }, true); } private void nextGridSize() From d5397a213974688344a43e593ce514441955724d Mon Sep 17 00:00:00 2001 From: OliBomby Date: Wed, 19 Jun 2024 20:59:14 +0200 Subject: [PATCH 1129/2493] fix alpha value in disabled state --- osu.Game/Graphics/UserInterface/ExpandableSlider.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/osu.Game/Graphics/UserInterface/ExpandableSlider.cs b/osu.Game/Graphics/UserInterface/ExpandableSlider.cs index a7a8561b94..4cc77e218f 100644 --- a/osu.Game/Graphics/UserInterface/ExpandableSlider.cs +++ b/osu.Game/Graphics/UserInterface/ExpandableSlider.cs @@ -119,9 +119,14 @@ namespace osu.Game.Graphics.UserInterface Expanded.BindValueChanged(v => { label.Text = v.NewValue ? expandedLabelText : contractedLabelText; - slider.FadeTo(v.NewValue ? 1f : 0f, 500, Easing.OutQuint); + slider.FadeTo(v.NewValue ? Current.Disabled ? 0.3f : 1f : 0f, 500, Easing.OutQuint); slider.BypassAutoSizeAxes = !v.NewValue ? Axes.Y : Axes.None; }, true); + + Current.BindDisabledChanged(disabled => + { + slider.Alpha = Expanded.Value ? disabled ? 0.3f : 1 : 0f; + }); } } From f2bd6fac47481b71650af5e2a2822dd7b8286412 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Wed, 19 Jun 2024 21:10:30 +0200 Subject: [PATCH 1130/2493] fix codefactor --- osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs b/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs index 8cffdfbe1d..73ecb2fe7c 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs @@ -199,13 +199,13 @@ namespace osu.Game.Rulesets.Osu.Edit switch (v.NewValue) { case PositionSnapGridType.Square: - GridLinesRotation.Value = (GridLinesRotation.Value + 405) % 90 - 45; + GridLinesRotation.Value = ((GridLinesRotation.Value + 405) % 90) - 45; GridLinesRotation.MinValue = -45; GridLinesRotation.MaxValue = 45; break; case PositionSnapGridType.Triangle: - GridLinesRotation.Value = (GridLinesRotation.Value + 390) % 60 - 30; + GridLinesRotation.Value = ((GridLinesRotation.Value + 390) % 60) - 30; GridLinesRotation.MinValue = -30; GridLinesRotation.MaxValue = 30; break; From efc8e1431a11a5e6e468ba72a30411b112d1c850 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Wed, 19 Jun 2024 23:15:35 +0200 Subject: [PATCH 1131/2493] activate length change with context menu --- .../Blueprints/Sliders/Components/SliderTailPiece.cs | 10 ++++++++-- .../Blueprints/Sliders/SliderSelectionBlueprint.cs | 4 ++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderTailPiece.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderTailPiece.cs index 7d39f04596..2ebdf87606 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderTailPiece.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderTailPiece.cs @@ -20,6 +20,11 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components { public partial class SliderTailPiece : SliderCircleOverlay { + /// + /// Whether this slider tail is draggable, changing the distance of the slider. + /// + public bool IsDraggable { get; set; } + /// /// Whether this is currently being dragged. /// @@ -60,7 +65,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components { Color4 colour = colours.Yellow; - if (IsHovered && inputManager.CurrentState.Keyboard.ShiftPressed + if (IsHovered && IsDraggable && !inputManager.HoveredDrawables.Any(o => o is PathControlPointPiece)) colour = colour.Lighten(1); @@ -69,7 +74,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components protected override bool OnDragStart(DragStartEvent e) { - if (e.Button == MouseButton.Right || !inputManager.CurrentState.Keyboard.ShiftPressed) + if (e.Button == MouseButton.Right || !IsDraggable) return false; isDragging = true; @@ -103,6 +108,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components trimExcessControlPoints(Slider.Path); isDragging = false; + IsDraggable = false; editorBeatmap?.EndChange(); } diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs index 2c239a40c8..4a949f5b48 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs @@ -409,6 +409,10 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders addControlPoint(rightClickPosition); changeHandler?.EndChange(); }), + new OsuMenuItem("Adjust distance", MenuItemType.Standard, () => + { + TailPiece.IsDraggable = true; + }), new OsuMenuItem("Convert to stream", MenuItemType.Destructive, convertToStream), }; From b24bfa290806117753dc1e7969ddf3966d09094e Mon Sep 17 00:00:00 2001 From: OliBomby Date: Thu, 20 Jun 2024 00:02:43 +0200 Subject: [PATCH 1132/2493] click to choose length instead of drag --- .../Sliders/Components/SliderTailPiece.cs | 185 ------------------ .../Sliders/SliderSelectionBlueprint.cs | 129 +++++++++++- 2 files changed, 124 insertions(+), 190 deletions(-) delete mode 100644 osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderTailPiece.cs diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderTailPiece.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderTailPiece.cs deleted file mode 100644 index 2ebdf87606..0000000000 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderTailPiece.cs +++ /dev/null @@ -1,185 +0,0 @@ -// 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.Framework.Caching; -using osu.Framework.Extensions.Color4Extensions; -using osu.Framework.Input; -using osu.Framework.Input.Events; -using osu.Framework.Utils; -using osu.Game.Graphics; -using osu.Game.Rulesets.Objects; -using osu.Game.Rulesets.Osu.Objects; -using osu.Game.Screens.Edit; -using osuTK; -using osuTK.Graphics; -using osuTK.Input; - -namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components -{ - public partial class SliderTailPiece : SliderCircleOverlay - { - /// - /// Whether this slider tail is draggable, changing the distance of the slider. - /// - public bool IsDraggable { get; set; } - - /// - /// Whether this is currently being dragged. - /// - private bool isDragging; - - private InputManager inputManager = null!; - - private readonly Cached fullPathCache = new Cached(); - - [Resolved] - private EditorBeatmap? editorBeatmap { get; set; } - - [Resolved] - private OsuColour colours { get; set; } = null!; - - public SliderTailPiece(Slider slider, SliderPosition position) - : base(slider, position) - { - Slider.Path.ControlPoints.CollectionChanged += (_, _) => fullPathCache.Invalidate(); - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - inputManager = GetContainingInputManager(); - } - - public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => CirclePiece.ReceivePositionalInputAt(screenSpacePos); - - protected override void Update() - { - updateCirclePieceColour(); - base.Update(); - } - - private void updateCirclePieceColour() - { - Color4 colour = colours.Yellow; - - if (IsHovered && IsDraggable - && !inputManager.HoveredDrawables.Any(o => o is PathControlPointPiece)) - colour = colour.Lighten(1); - - CirclePiece.Colour = colour; - } - - protected override bool OnDragStart(DragStartEvent e) - { - if (e.Button == MouseButton.Right || !IsDraggable) - return false; - - isDragging = true; - editorBeatmap?.BeginChange(); - - return true; - } - - protected override void OnDrag(DragEvent e) - { - double oldDistance = Slider.Path.Distance; - double proposedDistance = findClosestPathDistance(e); - - proposedDistance = MathHelper.Clamp(proposedDistance, 0, Slider.Path.CalculatedDistance); - proposedDistance = MathHelper.Clamp(proposedDistance, - 0.1 * oldDistance / Slider.SliderVelocityMultiplier, - 10 * oldDistance / Slider.SliderVelocityMultiplier); - - if (Precision.AlmostEquals(proposedDistance, oldDistance)) - return; - - Slider.SliderVelocityMultiplier *= proposedDistance / oldDistance; - Slider.Path.ExpectedDistance.Value = proposedDistance; - editorBeatmap?.Update(Slider); - } - - protected override void OnDragEnd(DragEndEvent e) - { - if (!isDragging) return; - - trimExcessControlPoints(Slider.Path); - - isDragging = false; - IsDraggable = false; - editorBeatmap?.EndChange(); - } - - /// - /// Trims control points from the end of the slider path which are not required to reach the expected end of the slider. - /// - /// The slider path to trim control points of. - private void trimExcessControlPoints(SliderPath sliderPath) - { - if (!sliderPath.ExpectedDistance.Value.HasValue) - return; - - double[] segmentEnds = sliderPath.GetSegmentEnds().ToArray(); - int segmentIndex = 0; - - for (int i = 1; i < sliderPath.ControlPoints.Count - 1; i++) - { - if (!sliderPath.ControlPoints[i].Type.HasValue) continue; - - if (Precision.AlmostBigger(segmentEnds[segmentIndex], 1, 1E-3)) - { - sliderPath.ControlPoints.RemoveRange(i + 1, sliderPath.ControlPoints.Count - i - 1); - sliderPath.ControlPoints[^1].Type = null; - break; - } - - segmentIndex++; - } - } - - /// - /// Finds the expected distance value for which the slider end is closest to the mouse position. - /// - private double findClosestPathDistance(DragEvent e) - { - const double step1 = 10; - const double step2 = 0.1; - - var desiredPosition = e.MousePosition - Slider.Position; - - if (!fullPathCache.IsValid) - fullPathCache.Value = new SliderPath(Slider.Path.ControlPoints.ToArray()); - - // Do a linear search to find the closest point on the path to the mouse position. - double bestValue = 0; - double minDistance = double.MaxValue; - - for (double d = 0; d <= fullPathCache.Value.CalculatedDistance; d += step1) - { - double t = d / fullPathCache.Value.CalculatedDistance; - float dist = Vector2.Distance(fullPathCache.Value.PositionAt(t), desiredPosition); - - if (dist >= minDistance) continue; - - minDistance = dist; - bestValue = d; - } - - // Do another linear search to fine-tune the result. - for (double d = bestValue - step1; d <= bestValue + step1; d += step2) - { - double t = d / fullPathCache.Value.CalculatedDistance; - float dist = Vector2.Distance(fullPathCache.Value.PositionAt(t), desiredPosition); - - if (dist >= minDistance) continue; - - minDistance = dist; - bestValue = d; - } - - return bestValue; - } - } -} diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs index 4a949f5b48..f59ef298a7 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs @@ -8,6 +8,7 @@ using System.Linq; using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Caching; using osu.Framework.Graphics; using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.UserInterface; @@ -34,7 +35,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders protected SliderBodyPiece BodyPiece { get; private set; } protected SliderCircleOverlay HeadOverlay { get; private set; } - protected SliderTailPiece TailPiece { get; private set; } + protected SliderCircleOverlay TailPiece { get; private set; } [CanBeNull] protected PathControlPointVisualiser ControlPointVisualiser { get; private set; } @@ -60,6 +61,10 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders private readonly IBindable pathVersion = new Bindable(); private readonly BindableList selectedObjects = new BindableList(); + // Cached slider path which ignored the expected distance value. + private readonly Cached fullPathCache = new Cached(); + private bool isAdjustingLength; + public SliderSelectionBlueprint(Slider slider) : base(slider) { @@ -72,7 +77,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders { BodyPiece = new SliderBodyPiece(), HeadOverlay = CreateCircleOverlay(HitObject, SliderPosition.Start), - TailPiece = CreateTailPiece(HitObject, SliderPosition.End), + TailPiece = CreateCircleOverlay(HitObject, SliderPosition.End), }; } @@ -81,6 +86,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders base.LoadComplete(); controlPoints.BindTo(HitObject.Path.ControlPoints); + controlPoints.CollectionChanged += (_, _) => fullPathCache.Invalidate(); pathVersion.BindTo(HitObject.Path.Version); pathVersion.BindValueChanged(_ => editorBeatmap?.Update(HitObject)); @@ -135,6 +141,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders { base.OnDeselected(); + if (isAdjustingLength) + endAdjustLength(); + updateVisualDefinition(); BodyPiece.RecyclePath(); } @@ -164,6 +173,12 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders protected override bool OnMouseDown(MouseDownEvent e) { + if (isAdjustingLength) + { + endAdjustLength(); + return true; + } + switch (e.Button) { case MouseButton.Right: @@ -171,6 +186,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders return false; // Allow right click to be handled by context menu case MouseButton.Left: + // If there's more than two objects selected, ctrl+click should deselect if (e.ControlPressed && IsSelected && selectedObjects.Count < 2) { @@ -186,6 +202,106 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders return false; } + private void endAdjustLength() + { + trimExcessControlPoints(HitObject.Path); + isAdjustingLength = false; + changeHandler?.EndChange(); + } + + protected override bool OnMouseMove(MouseMoveEvent e) + { + if (!isAdjustingLength) + return base.OnMouseMove(e); + + double oldDistance = HitObject.Path.Distance; + double proposedDistance = findClosestPathDistance(e); + + proposedDistance = MathHelper.Clamp(proposedDistance, 0, HitObject.Path.CalculatedDistance); + proposedDistance = MathHelper.Clamp(proposedDistance, + 0.1 * oldDistance / HitObject.SliderVelocityMultiplier, + 10 * oldDistance / HitObject.SliderVelocityMultiplier); + + if (Precision.AlmostEquals(proposedDistance, oldDistance)) + return false; + + HitObject.SliderVelocityMultiplier *= proposedDistance / oldDistance; + HitObject.Path.ExpectedDistance.Value = proposedDistance; + editorBeatmap?.Update(HitObject); + + return false; + } + + /// + /// Trims control points from the end of the slider path which are not required to reach the expected end of the slider. + /// + /// The slider path to trim control points of. + private void trimExcessControlPoints(SliderPath sliderPath) + { + if (!sliderPath.ExpectedDistance.Value.HasValue) + return; + + double[] segmentEnds = sliderPath.GetSegmentEnds().ToArray(); + int segmentIndex = 0; + + for (int i = 1; i < sliderPath.ControlPoints.Count - 1; i++) + { + if (!sliderPath.ControlPoints[i].Type.HasValue) continue; + + if (Precision.AlmostBigger(segmentEnds[segmentIndex], 1, 1E-3)) + { + sliderPath.ControlPoints.RemoveRange(i + 1, sliderPath.ControlPoints.Count - i - 1); + sliderPath.ControlPoints[^1].Type = null; + break; + } + + segmentIndex++; + } + } + + /// + /// Finds the expected distance value for which the slider end is closest to the mouse position. + /// + private double findClosestPathDistance(MouseMoveEvent e) + { + const double step1 = 10; + const double step2 = 0.1; + + var desiredPosition = e.MousePosition - HitObject.Position; + + if (!fullPathCache.IsValid) + fullPathCache.Value = new SliderPath(HitObject.Path.ControlPoints.ToArray()); + + // Do a linear search to find the closest point on the path to the mouse position. + double bestValue = 0; + double minDistance = double.MaxValue; + + for (double d = 0; d <= fullPathCache.Value.CalculatedDistance; d += step1) + { + double t = d / fullPathCache.Value.CalculatedDistance; + float dist = Vector2.Distance(fullPathCache.Value.PositionAt(t), desiredPosition); + + if (dist >= minDistance) continue; + + minDistance = dist; + bestValue = d; + } + + // Do another linear search to fine-tune the result. + for (double d = bestValue - step1; d <= bestValue + step1; d += step2) + { + double t = d / fullPathCache.Value.CalculatedDistance; + float dist = Vector2.Distance(fullPathCache.Value.PositionAt(t), desiredPosition); + + if (dist >= minDistance) continue; + + minDistance = dist; + bestValue = d; + } + + return bestValue; + } + [CanBeNull] private PathControlPoint placementControlPoint; @@ -409,9 +525,10 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders addControlPoint(rightClickPosition); changeHandler?.EndChange(); }), - new OsuMenuItem("Adjust distance", MenuItemType.Standard, () => + new OsuMenuItem("Adjust length", MenuItemType.Standard, () => { - TailPiece.IsDraggable = true; + isAdjustingLength = true; + changeHandler?.BeginChange(); }), new OsuMenuItem("Convert to stream", MenuItemType.Destructive, convertToStream), }; @@ -427,6 +544,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) { + if (isAdjustingLength) + return true; + if (BodyPiece.ReceivePositionalInputAt(screenSpacePos)) return true; @@ -443,6 +563,5 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders } protected virtual SliderCircleOverlay CreateCircleOverlay(Slider slider, SliderPosition position) => new SliderCircleOverlay(slider, position); - protected virtual SliderTailPiece CreateTailPiece(Slider slider, SliderPosition position) => new SliderTailPiece(slider, position); } } From 956bdbca50afdc24e97bb6834adca8cc839c993e Mon Sep 17 00:00:00 2001 From: OliBomby Date: Thu, 20 Jun 2024 00:17:16 +0200 Subject: [PATCH 1133/2493] fix tests --- .../TestSceneSliderControlPointPiece.cs | 13 +--- .../TestSceneSliderSelectionBlueprint.cs | 60 +++++-------------- .../Sliders/SliderSelectionBlueprint.cs | 4 +- 3 files changed, 17 insertions(+), 60 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderControlPointPiece.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderControlPointPiece.cs index 1a7430704d..99ced30ffe 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderControlPointPiece.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderControlPointPiece.cs @@ -353,7 +353,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor { public new SliderBodyPiece BodyPiece => base.BodyPiece; public new TestSliderCircleOverlay HeadOverlay => (TestSliderCircleOverlay)base.HeadOverlay; - public new TestSliderTailPiece TailPiece => (TestSliderTailPiece)base.TailPiece; + public new TestSliderCircleOverlay TailOverlay => (TestSliderCircleOverlay)base.TailOverlay; public new PathControlPointVisualiser ControlPointVisualiser => base.ControlPointVisualiser; public TestSliderBlueprint(Slider slider) @@ -362,7 +362,6 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor } protected override SliderCircleOverlay CreateCircleOverlay(Slider slider, SliderPosition position) => new TestSliderCircleOverlay(slider, position); - protected override SliderTailPiece CreateTailPiece(Slider slider, SliderPosition position) => new TestSliderTailPiece(slider, position); } private partial class TestSliderCircleOverlay : SliderCircleOverlay @@ -374,15 +373,5 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor { } } - - private partial class TestSliderTailPiece : SliderTailPiece - { - public new HitCirclePiece CirclePiece => base.CirclePiece; - - public TestSliderTailPiece(Slider slider, SliderPosition position) - : base(slider, position) - { - } - } } } diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSelectionBlueprint.cs index 3faf181465..812b34dfe2 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSelectionBlueprint.cs @@ -3,6 +3,7 @@ #nullable disable +using System.Linq; using NUnit.Framework; using osu.Framework.Utils; using osu.Game.Beatmaps; @@ -164,51 +165,29 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor } [Test] - public void TestDragSliderTail() + public void TestAdjustDistance() { - AddStep("move mouse to slider tail", () => - { - Vector2 position = slider.EndPosition + new Vector2(10, 0); - InputManager.MoveMouseTo(drawableObject.Parent!.ToScreenSpace(position)); - }); - AddStep("shift + drag", () => - { - InputManager.PressKey(Key.ShiftLeft); - InputManager.PressButton(MouseButton.Left); - }); + AddStep("start adjust length", + () => blueprint.ContextMenuItems.Single(o => o.Text.Value == "Adjust length").Action.Value()); moveMouseToControlPoint(1); - AddStep("release", () => - { - InputManager.ReleaseButton(MouseButton.Left); - InputManager.ReleaseKey(Key.ShiftLeft); - }); - + AddStep("end adjust length", () => InputManager.Click(MouseButton.Right)); AddAssert("expected distance halved", () => Precision.AlmostEquals(slider.Path.Distance, 172.2, 0.1)); - AddStep("move mouse to slider tail", () => - { - Vector2 position = slider.EndPosition + new Vector2(10, 0); - InputManager.MoveMouseTo(drawableObject.Parent!.ToScreenSpace(position)); - }); - AddStep("shift + drag", () => - { - InputManager.PressKey(Key.ShiftLeft); - InputManager.PressButton(MouseButton.Left); - }); + AddStep("start adjust length", + () => blueprint.ContextMenuItems.Single(o => o.Text.Value == "Adjust length").Action.Value()); AddStep("move mouse beyond last control point", () => { Vector2 position = slider.Position + slider.Path.ControlPoints[2].Position + new Vector2(50, 0); InputManager.MoveMouseTo(drawableObject.Parent!.ToScreenSpace(position)); }); - AddStep("release", () => - { - InputManager.ReleaseButton(MouseButton.Left); - InputManager.ReleaseKey(Key.ShiftLeft); - }); - + AddStep("end adjust length", () => InputManager.Click(MouseButton.Right)); AddAssert("expected distance is calculated distance", () => Precision.AlmostEquals(slider.Path.Distance, slider.Path.CalculatedDistance, 0.1)); + + moveMouseToControlPoint(1); + AddAssert("expected distance is unchanged", + () => Precision.AlmostEquals(slider.Path.Distance, slider.Path.CalculatedDistance, 0.1)); } private void moveHitObject() @@ -227,7 +206,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor () => Precision.AlmostEquals(blueprint.HeadOverlay.CirclePiece.ScreenSpaceDrawQuad.Centre, drawableObject.HeadCircle.ScreenSpaceDrawQuad.Centre)); AddAssert("tail positioned correctly", - () => Precision.AlmostEquals(blueprint.TailPiece.CirclePiece.ScreenSpaceDrawQuad.Centre, drawableObject.TailCircle.ScreenSpaceDrawQuad.Centre)); + () => Precision.AlmostEquals(blueprint.TailOverlay.CirclePiece.ScreenSpaceDrawQuad.Centre, drawableObject.TailCircle.ScreenSpaceDrawQuad.Centre)); } private void moveMouseToControlPoint(int index) @@ -246,7 +225,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor { public new SliderBodyPiece BodyPiece => base.BodyPiece; public new TestSliderCircleOverlay HeadOverlay => (TestSliderCircleOverlay)base.HeadOverlay; - public new TestSliderTailPiece TailPiece => (TestSliderTailPiece)base.TailPiece; + public new TestSliderCircleOverlay TailOverlay => (TestSliderCircleOverlay)base.TailOverlay; public new PathControlPointVisualiser ControlPointVisualiser => base.ControlPointVisualiser; public TestSliderBlueprint(Slider slider) @@ -255,7 +234,6 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor } protected override SliderCircleOverlay CreateCircleOverlay(Slider slider, SliderPosition position) => new TestSliderCircleOverlay(slider, position); - protected override SliderTailPiece CreateTailPiece(Slider slider, SliderPosition position) => new TestSliderTailPiece(slider, position); } private partial class TestSliderCircleOverlay : SliderCircleOverlay @@ -267,15 +245,5 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor { } } - - private partial class TestSliderTailPiece : SliderTailPiece - { - public new HitCirclePiece CirclePiece => base.CirclePiece; - - public TestSliderTailPiece(Slider slider, SliderPosition position) - : base(slider, position) - { - } - } } } diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs index f59ef298a7..eb269ba680 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs @@ -35,7 +35,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders protected SliderBodyPiece BodyPiece { get; private set; } protected SliderCircleOverlay HeadOverlay { get; private set; } - protected SliderCircleOverlay TailPiece { get; private set; } + protected SliderCircleOverlay TailOverlay { get; private set; } [CanBeNull] protected PathControlPointVisualiser ControlPointVisualiser { get; private set; } @@ -77,7 +77,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders { BodyPiece = new SliderBodyPiece(), HeadOverlay = CreateCircleOverlay(HitObject, SliderPosition.Start), - TailPiece = CreateCircleOverlay(HitObject, SliderPosition.End), + TailOverlay = CreateCircleOverlay(HitObject, SliderPosition.End), }; } From ad2cd0ba8fb0b640155a704d2f39069eabec4985 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 19 Jun 2024 13:18:56 +0200 Subject: [PATCH 1134/2493] Adjust behaviour of hit animations toggle to match user expectations --- .../Components/HitCircleOverlapMarker.cs | 9 ------- .../HitCircles/HitCircleSelectionBlueprint.cs | 24 +++++++++++++++++++ .../Blueprints/Sliders/SliderCircleOverlay.cs | 14 +++++------ .../Sliders/SliderSelectionBlueprint.cs | 14 ++++++++++- .../Objects/Drawables/DrawableHitCircle.cs | 23 ++++++++++++++++++ .../Objects/Drawables/DrawableSlider.cs | 18 ++++++++++++++ .../Objects/Drawables/DrawableSliderTail.cs | 24 +++++++++++++++++++ .../Objects/Drawables/DrawableHitObject.cs | 18 +++++++------- 8 files changed, 117 insertions(+), 27 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/Components/HitCircleOverlapMarker.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/Components/HitCircleOverlapMarker.cs index fe335a048d..8ed9d0476a 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/Components/HitCircleOverlapMarker.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/Components/HitCircleOverlapMarker.cs @@ -7,7 +7,6 @@ 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.Utils; using osu.Game.Configuration; using osu.Game.Rulesets.Objects.Types; @@ -16,7 +15,6 @@ using osu.Game.Rulesets.Osu.Skinning.Default; using osu.Game.Screens.Edit; using osu.Game.Skinning; using osuTK; -using osuTK.Graphics; namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components { @@ -48,13 +46,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components RelativeSizeAxes = Axes.Both, Children = new Drawable[] { - new Circle - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Both, - Colour = Color4.White, - }, ring = new RingPiece { BorderThickness = 4, diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCircleSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCircleSelectionBlueprint.cs index 0608f8c929..fd2bbe9916 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCircleSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCircleSelectionBlueprint.cs @@ -1,8 +1,11 @@ // 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.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Primitives; +using osu.Game.Configuration; using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects.Drawables; @@ -16,6 +19,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles protected readonly HitCirclePiece CirclePiece; private readonly HitCircleOverlapMarker marker; + private readonly Bindable showHitMarkers = new Bindable(); public HitCircleSelectionBlueprint(HitCircle circle) : base(circle) @@ -27,12 +31,32 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles }; } + [BackgroundDependencyLoader] + private void load(OsuConfigManager config) + { + config.BindWith(OsuSetting.EditorShowHitMarkers, showHitMarkers); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + showHitMarkers.BindValueChanged(_ => + { + if (!showHitMarkers.Value) + DrawableObject.RestoreHitAnimations(); + }); + } + protected override void Update() { base.Update(); CirclePiece.UpdateFrom(HitObject); marker.UpdateFrom(HitObject); + + if (showHitMarkers.Value) + DrawableObject.SuppressHitAnimations(); } public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => DrawableObject.HitArea.ReceivePositionalInputAt(screenSpacePos); diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderCircleOverlay.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderCircleOverlay.cs index d47cf6bf23..bd3b4bbc54 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderCircleOverlay.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderCircleOverlay.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 osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components; using osu.Game.Rulesets.Osu.Objects; @@ -14,18 +13,17 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders private readonly Slider slider; private readonly SliderPosition position; - private readonly HitCircleOverlapMarker marker; + private readonly HitCircleOverlapMarker? marker; public SliderCircleOverlay(Slider slider, SliderPosition position) { this.slider = slider; this.position = position; - InternalChildren = new Drawable[] - { - marker = new HitCircleOverlapMarker(), - CirclePiece = new HitCirclePiece(), - }; + if (position == SliderPosition.Start) + AddInternal(marker = new HitCircleOverlapMarker()); + + AddInternal(CirclePiece = new HitCirclePiece()); } protected override void Update() @@ -35,7 +33,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders var circle = position == SliderPosition.Start ? (HitCircle)slider.HeadCircle : slider.TailCircle; CirclePiece.UpdateFrom(circle); - marker.UpdateFrom(circle); + marker?.UpdateFrom(circle); } public override void Hide() diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs index 49fdf12d60..7ee6530099 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs @@ -14,6 +14,7 @@ using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; using osu.Framework.Utils; using osu.Game.Audio; +using osu.Game.Configuration; using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects; @@ -59,6 +60,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders private readonly BindableList controlPoints = new BindableList(); private readonly IBindable pathVersion = new Bindable(); private readonly BindableList selectedObjects = new BindableList(); + private readonly Bindable showHitMarkers = new Bindable(); public SliderSelectionBlueprint(Slider slider) : base(slider) @@ -66,7 +68,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders } [BackgroundDependencyLoader] - private void load() + private void load(OsuConfigManager config) { InternalChildren = new Drawable[] { @@ -74,6 +76,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders HeadOverlay = CreateCircleOverlay(HitObject, SliderPosition.Start), TailOverlay = CreateCircleOverlay(HitObject, SliderPosition.End), }; + + config.BindWith(OsuSetting.EditorShowHitMarkers, showHitMarkers); } protected override void LoadComplete() @@ -90,6 +94,11 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders if (editorBeatmap != null) selectedObjects.BindTo(editorBeatmap.SelectedHitObjects); selectedObjects.BindCollectionChanged((_, _) => updateVisualDefinition(), true); + showHitMarkers.BindValueChanged(_ => + { + if (!showHitMarkers.Value) + DrawableObject.RestoreHitAnimations(); + }); } public override bool HandleQuickDeletion() @@ -110,6 +119,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders if (IsSelected) BodyPiece.UpdateFrom(HitObject); + + if (showHitMarkers.Value) + DrawableObject.SuppressHitAnimations(); } protected override bool OnHover(HoverEvent e) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs index c3ce6acce9..26e9773967 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs @@ -19,6 +19,7 @@ using osu.Game.Rulesets.Osu.UI; using osu.Game.Rulesets.Scoring; using osu.Game.Skinning; using osuTK; +using osuTK.Graphics; namespace osu.Game.Rulesets.Osu.Objects.Drawables { @@ -319,5 +320,27 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { } } + + #region FOR EDITOR USE ONLY, DO NOT USE FOR ANY OTHER PURPOSE + + internal void SuppressHitAnimations() + { + UpdateState(ArmedState.Idle); + UpdateComboColour(); + + using (BeginAbsoluteSequence(StateUpdateTime - 5)) + this.TransformBindableTo(AccentColour, Color4.White, Math.Max(0, HitStateUpdateTime - StateUpdateTime)); + + using (BeginAbsoluteSequence(HitStateUpdateTime)) + this.FadeOut(700).Expire(); + } + + internal void RestoreHitAnimations() + { + UpdateState(ArmedState.Hit, force: true); + UpdateComboColour(); + } + + #endregion } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs index e519e51562..7bae3cefcf 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs @@ -370,5 +370,23 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables private partial class DefaultSliderBody : PlaySliderBody { } + + #region FOR EDITOR USE ONLY, DO NOT USE FOR ANY OTHER PURPOSE + + internal void SuppressHitAnimations() + { + UpdateState(ArmedState.Idle); + HeadCircle.SuppressHitAnimations(); + TailCircle.SuppressHitAnimations(); + } + + internal void RestoreHitAnimations() + { + UpdateState(ArmedState.Hit, force: true); + HeadCircle.RestoreHitAnimations(); + TailCircle.RestoreHitAnimations(); + } + + #endregion } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs index c4731118a1..21aa672d10 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs @@ -3,6 +3,7 @@ #nullable disable +using System; using System.Diagnostics; using JetBrains.Annotations; using osu.Framework.Allocation; @@ -12,6 +13,7 @@ using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Types; using osu.Game.Skinning; using osuTK; +using osuTK.Graphics; namespace osu.Game.Rulesets.Osu.Objects.Drawables { @@ -125,5 +127,27 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables if (Slider != null) Position = Slider.CurvePositionAt(HitObject.RepeatIndex % 2 == 0 ? 1 : 0); } + + #region FOR EDITOR USE ONLY, DO NOT USE FOR ANY OTHER PURPOSE + + internal void SuppressHitAnimations() + { + UpdateState(ArmedState.Idle); + UpdateComboColour(); + + using (BeginAbsoluteSequence(StateUpdateTime - 5)) + this.TransformBindableTo(AccentColour, Color4.White, Math.Max(0, HitStateUpdateTime - StateUpdateTime)); + + using (BeginAbsoluteSequence(HitStateUpdateTime)) + this.FadeOut(700).Expire(); + } + + internal void RestoreHitAnimations() + { + UpdateState(ArmedState.Hit); + UpdateComboColour(); + } + + #endregion } } diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index 3ce6cc3cef..1f735576bc 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -314,11 +314,11 @@ namespace osu.Game.Rulesets.Objects.Drawables private void updateStateFromResult() { if (Result.IsHit) - updateState(ArmedState.Hit, true); + UpdateState(ArmedState.Hit, true); else if (Result.HasResult) - updateState(ArmedState.Miss, true); + UpdateState(ArmedState.Miss, true); else - updateState(ArmedState.Idle, true); + UpdateState(ArmedState.Idle, true); } protected sealed override void OnFree(HitObjectLifetimeEntry entry) @@ -402,7 +402,7 @@ namespace osu.Game.Rulesets.Objects.Drawables private void onRevertResult() { - updateState(ArmedState.Idle); + UpdateState(ArmedState.Idle); OnRevertResult?.Invoke(this, Result); } @@ -421,7 +421,7 @@ namespace osu.Game.Rulesets.Objects.Drawables if (Result is not null) { Result.TimeOffset = 0; - updateState(State.Value, true); + UpdateState(State.Value, true); } DefaultsApplied?.Invoke(this); @@ -461,7 +461,7 @@ namespace osu.Game.Rulesets.Objects.Drawables throw new InvalidOperationException( $"Should never clear a {nameof(DrawableHitObject)} as the base implementation adds components. If attempting to use {nameof(InternalChild)} or {nameof(InternalChildren)}, using {nameof(AddInternal)} or {nameof(AddRangeInternal)} instead."); - private void updateState(ArmedState newState, bool force = false) + protected void UpdateState(ArmedState newState, bool force = false) { if (State.Value == newState && !force) return; @@ -506,7 +506,7 @@ namespace osu.Game.Rulesets.Objects.Drawables /// /// Reapplies the current . /// - public void RefreshStateTransforms() => updateState(State.Value, true); + public void RefreshStateTransforms() => UpdateState(State.Value, true); /// /// Apply (generally fade-in) transforms leading into the start time. @@ -565,7 +565,7 @@ namespace osu.Game.Rulesets.Objects.Drawables ApplySkin(CurrentSkin, true); if (IsLoaded) - updateState(State.Value, true); + UpdateState(State.Value, true); } protected void UpdateComboColour() @@ -725,7 +725,7 @@ namespace osu.Game.Rulesets.Objects.Drawables Result.GameplayRate = (Clock as IGameplayClock)?.GetTrueGameplayRate() ?? Clock.Rate; if (Result.HasResult) - updateState(Result.IsHit ? ArmedState.Hit : ArmedState.Miss); + UpdateState(Result.IsHit ? ArmedState.Hit : ArmedState.Miss); OnNewResult?.Invoke(this, Result); } From 225b309ba357b177a6259a447afea8e18e261ffb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 20 Jun 2024 16:27:07 +0200 Subject: [PATCH 1135/2493] Reimplement stable polygon tool Addresses https://github.com/ppy/osu/discussions/19970. While yes, https://github.com/ppy/osu/pull/26303 is also a thing, in discussing with users I don't think that grids are going to be able to deprecate this feature. Logic transcribed verbatim from stable. --- .../Edit/GenerateToolboxGroup.cs | 54 +++++ .../Edit/OsuHitObjectComposer.cs | 3 +- .../Edit/PolygonGenerationPopover.cs | 193 ++++++++++++++++++ 3 files changed, 249 insertions(+), 1 deletion(-) create mode 100644 osu.Game.Rulesets.Osu/Edit/GenerateToolboxGroup.cs create mode 100644 osu.Game.Rulesets.Osu/Edit/PolygonGenerationPopover.cs diff --git a/osu.Game.Rulesets.Osu/Edit/GenerateToolboxGroup.cs b/osu.Game.Rulesets.Osu/Edit/GenerateToolboxGroup.cs new file mode 100644 index 0000000000..4e188a2b86 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Edit/GenerateToolboxGroup.cs @@ -0,0 +1,54 @@ +// 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.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Input.Events; +using osu.Game.Rulesets.Edit; +using osu.Game.Screens.Edit.Components; +using osuTK; +using osuTK.Input; + +namespace osu.Game.Rulesets.Osu.Edit +{ + public partial class GenerateToolboxGroup : EditorToolboxGroup + { + private readonly EditorToolButton polygonButton; + + public GenerateToolboxGroup() + : base("Generate") + { + Child = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Spacing = new Vector2(5), + Children = new Drawable[] + { + polygonButton = new EditorToolButton("Polygon", + () => new SpriteIcon { Icon = FontAwesome.Solid.Spinner }, + () => new PolygonGenerationPopover()), + } + }; + } + + protected override bool OnKeyDown(KeyDownEvent e) + { + if (e.Repeat) return false; + + switch (e.Key) + { + case Key.D: + if (!e.ControlPressed || !e.ShiftPressed) + return false; + + polygonButton.TriggerClick(); + return true; + + default: + return false; + } + } + } +} diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs index 41f6b41f82..fab5298554 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs @@ -64,7 +64,7 @@ namespace osu.Game.Rulesets.Osu.Edit private Bindable placementObject; [Cached(typeof(IDistanceSnapProvider))] - protected readonly OsuDistanceSnapProvider DistanceSnapProvider = new OsuDistanceSnapProvider(); + public readonly OsuDistanceSnapProvider DistanceSnapProvider = new OsuDistanceSnapProvider(); [Cached] protected readonly OsuGridToolboxGroup OsuGridToolboxGroup = new OsuGridToolboxGroup(); @@ -109,6 +109,7 @@ namespace osu.Game.Rulesets.Osu.Edit RotationHandler = BlueprintContainer.SelectionHandler.RotationHandler, ScaleHandler = (OsuSelectionScaleHandler)BlueprintContainer.SelectionHandler.ScaleHandler, }, + new GenerateToolboxGroup(), FreehandlSliderToolboxGroup } ); diff --git a/osu.Game.Rulesets.Osu/Edit/PolygonGenerationPopover.cs b/osu.Game.Rulesets.Osu/Edit/PolygonGenerationPopover.cs new file mode 100644 index 0000000000..6325de5851 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Edit/PolygonGenerationPopover.cs @@ -0,0 +1,193 @@ +// 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.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Graphics.UserInterface; +using osu.Game.Graphics.UserInterfaceV2; +using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Legacy; +using osu.Game.Rulesets.Objects.Types; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Osu.UI; +using osu.Game.Screens.Edit; +using osu.Game.Screens.Edit.Compose.Components; +using osuTK; + +namespace osu.Game.Rulesets.Osu.Edit +{ + public partial class PolygonGenerationPopover : OsuPopover + { + private SliderWithTextBoxInput distanceSnapInput = null!; + private SliderWithTextBoxInput offsetAngleInput = null!; + private SliderWithTextBoxInput repeatCountInput = null!; + private SliderWithTextBoxInput pointInput = null!; + private RoundedButton commitButton = null!; + + private readonly List insertedCircles = new List(); + private bool began; + private bool committed; + + [Resolved] + private IBeatSnapProvider beatSnapProvider { get; set; } = null!; + + [Resolved] + private EditorClock editorClock { get; set; } = null!; + + [Resolved] + private EditorBeatmap editorBeatmap { get; set; } = null!; + + [Resolved] + private IEditorChangeHandler? changeHandler { get; set; } + + [Resolved] + private HitObjectComposer composer { get; set; } = null!; + + [BackgroundDependencyLoader] + private void load() + { + Child = new FillFlowContainer + { + Width = 220, + AutoSizeAxes = Axes.Y, + Spacing = new Vector2(20), + Children = new Drawable[] + { + distanceSnapInput = new SliderWithTextBoxInput("Distance snap:") + { + Current = new BindableNumber(1) + { + MinValue = 0.1, + MaxValue = 6, + Precision = 0.1, + Value = ((OsuHitObjectComposer)composer).DistanceSnapProvider.DistanceSpacingMultiplier.Value, + }, + Instantaneous = true + }, + offsetAngleInput = new SliderWithTextBoxInput("Offset angle:") + { + Current = new BindableNumber + { + MinValue = 0, + MaxValue = 180, + Precision = 1 + }, + Instantaneous = true + }, + repeatCountInput = new SliderWithTextBoxInput("Repeats:") + { + Current = new BindableNumber(1) + { + MinValue = 1, + MaxValue = 10, + Precision = 1 + }, + Instantaneous = true + }, + pointInput = new SliderWithTextBoxInput("Vertices:") + { + Current = new BindableNumber(3) + { + MinValue = 3, + MaxValue = 10, + Precision = 1, + }, + Instantaneous = true + }, + commitButton = new RoundedButton + { + RelativeSizeAxes = Axes.X, + Text = "Create", + Action = commit + } + } + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + changeHandler?.BeginChange(); + began = true; + + distanceSnapInput.Current.BindValueChanged(_ => tryCreatePolygon()); + offsetAngleInput.Current.BindValueChanged(_ => tryCreatePolygon()); + repeatCountInput.Current.BindValueChanged(_ => tryCreatePolygon()); + pointInput.Current.BindValueChanged(_ => tryCreatePolygon()); + tryCreatePolygon(); + } + + private void tryCreatePolygon() + { + double startTime = beatSnapProvider.SnapTime(editorClock.CurrentTime); + TimingControlPoint timingPoint = editorBeatmap.ControlPointInfo.TimingPointAt(startTime); + double timeSpacing = timingPoint.BeatLength / editorBeatmap.BeatDivisor; + IHasSliderVelocity lastWithSliderVelocity = editorBeatmap.HitObjects.Where(ho => ho.GetEndTime() <= startTime).OfType().LastOrDefault() ?? new Slider(); + double velocity = OsuHitObject.BASE_SCORING_DISTANCE * editorBeatmap.Difficulty.SliderMultiplier + / LegacyRulesetExtensions.GetPrecisionAdjustedBeatLength(lastWithSliderVelocity, timingPoint, OsuRuleset.SHORT_NAME); + double length = distanceSnapInput.Current.Value * velocity * timeSpacing; + float polygonRadius = (float)(length / (2 * Math.Sin(double.Pi / pointInput.Current.Value))); + + editorBeatmap.RemoveRange(insertedCircles); + insertedCircles.Clear(); + + var selectionHandler = (EditorSelectionHandler)composer.BlueprintContainer.SelectionHandler; + bool first = true; + + for (int i = 1; i <= pointInput.Current.Value * repeatCountInput.Current.Value; ++i) + { + float angle = float.DegreesToRadians(offsetAngleInput.Current.Value) + i * (2 * float.Pi / pointInput.Current.Value); + var position = OsuPlayfield.BASE_SIZE / 2 + new Vector2(polygonRadius * float.Cos(angle), polygonRadius * float.Sin(angle)); + + var circle = new HitCircle + { + Position = position, + StartTime = startTime, + NewCombo = first && selectionHandler.SelectionNewComboState.Value == TernaryState.True, + }; + // TODO: probably ensure samples also follow current ternary status (not trivial) + circle.Samples.Add(circle.CreateHitSampleInfo()); + + if (position.X < 0 || position.Y < 0 || position.X > OsuPlayfield.BASE_SIZE.X || position.Y > OsuPlayfield.BASE_SIZE.Y) + { + commitButton.Enabled.Value = false; + return; + } + + insertedCircles.Add(circle); + startTime = beatSnapProvider.SnapTime(startTime + timeSpacing); + + first = false; + } + + editorBeatmap.AddRange(insertedCircles); + commitButton.Enabled.Value = true; + } + + private void commit() + { + changeHandler?.EndChange(); + committed = true; + Hide(); + } + + protected override void PopOut() + { + base.PopOut(); + + if (began && !committed) + { + editorBeatmap.RemoveRange(insertedCircles); + changeHandler?.EndChange(); + } + } + } +} From 74399542d2e72c4bff4d7b6155202477b9bc5567 Mon Sep 17 00:00:00 2001 From: Olivier Schipper Date: Thu, 20 Jun 2024 17:27:15 +0200 Subject: [PATCH 1136/2493] Use math instead of hardcoded constant values MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bartłomiej Dach --- .../Edit/Compose/Components/TriangularPositionSnapGrid.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/TriangularPositionSnapGrid.cs b/osu.Game/Screens/Edit/Compose/Components/TriangularPositionSnapGrid.cs index 93d2c6a74a..91aea1de8d 100644 --- a/osu.Game/Screens/Edit/Compose/Components/TriangularPositionSnapGrid.cs +++ b/osu.Game/Screens/Edit/Compose/Components/TriangularPositionSnapGrid.cs @@ -29,9 +29,9 @@ namespace osu.Game.Screens.Edit.Compose.Components GridLineRotation.BindValueChanged(_ => GridCache.Invalidate()); } - private const float sqrt3 = 1.73205080757f; - private const float sqrt3_over2 = 0.86602540378f; - private const float one_over_sqrt3 = 0.57735026919f; + private static readonly float sqrt3 = float.Sqrt(3); + private static readonly float sqrt3_over2 = sqrt3 / 2; + private static readonly float one_over_sqrt3 = 1 / sqrt3; protected override void CreateContent() { From 4c6741e8aaba74b4cd47430ee0596940b5306fe4 Mon Sep 17 00:00:00 2001 From: Olivier Schipper Date: Thu, 20 Jun 2024 17:27:38 +0200 Subject: [PATCH 1137/2493] Fix exception type MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bartłomiej Dach --- osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs index fb2e3fec27..c553f9d640 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs @@ -148,7 +148,7 @@ namespace osu.Game.Rulesets.Osu.Edit break; default: - throw new NotImplementedException($"{OsuGridToolboxGroup.GridType} has an incorrect value."); + throw new ArgumentOutOfRangeException(nameof(OsuGridToolboxGroup.GridType), OsuGridToolboxGroup.GridType, "Unsupported grid type."); } // Bind the start position to the toolbox sliders. From 89d3f67eb3486c422b9ce874e8fd1a000e320938 Mon Sep 17 00:00:00 2001 From: sometimes <76718358+ssz7-ch2@users.noreply.github.com> Date: Thu, 20 Jun 2024 22:06:00 -0400 Subject: [PATCH 1138/2493] fix accuracyProcess typo --- osu.Game/Rulesets/Scoring/ScoreProcessor.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index 70d7f0fe37..0b20f1089a 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -369,9 +369,9 @@ namespace osu.Game.Rulesets.Scoring MaximumAccuracy.Value = maximumBaseScore > 0 ? (currentBaseScore + (maximumBaseScore - currentMaximumBaseScore)) / maximumBaseScore : 1; double comboProgress = maximumComboPortion > 0 ? currentComboPortion / maximumComboPortion : 1; - double accuracyProcess = maximumAccuracyJudgementCount > 0 ? (double)currentAccuracyJudgementCount / maximumAccuracyJudgementCount : 1; + double accuracyProgress = maximumAccuracyJudgementCount > 0 ? (double)currentAccuracyJudgementCount / maximumAccuracyJudgementCount : 1; - TotalScoreWithoutMods.Value = (long)Math.Round(ComputeTotalScore(comboProgress, accuracyProcess, currentBonusPortion)); + TotalScoreWithoutMods.Value = (long)Math.Round(ComputeTotalScore(comboProgress, accuracyProgress, currentBonusPortion)); TotalScore.Value = (long)Math.Round(TotalScoreWithoutMods.Value * scoreMultiplier); } From a56751511e34d67b1eaf4ebebb2566f6184d94fb Mon Sep 17 00:00:00 2001 From: Jamie Taylor Date: Sat, 22 Jun 2024 01:36:30 +0900 Subject: [PATCH 1139/2493] Apply a ducking effect to the currently playing track when switching ruleset --- osu.Game/Overlays/MusicController.cs | 57 +++++++++++++++++++ .../Toolbar/ToolbarRulesetSelector.cs | 31 +++++++++- .../Toolbar/ToolbarRulesetTabButton.cs | 12 ---- 3 files changed, 87 insertions(+), 13 deletions(-) diff --git a/osu.Game/Overlays/MusicController.cs b/osu.Game/Overlays/MusicController.cs index 0986c0513c..678ae92d4b 100644 --- a/osu.Game/Overlays/MusicController.cs +++ b/osu.Game/Overlays/MusicController.cs @@ -16,6 +16,7 @@ using osu.Framework.Graphics.Audio; using osu.Framework.Graphics.Containers; using osu.Framework.Logging; using osu.Framework.Threading; +using osu.Game.Audio.Effects; using osu.Game.Beatmaps; using osu.Game.Database; using osu.Game.Rulesets.Mods; @@ -63,6 +64,16 @@ namespace osu.Game.Overlays [Resolved] private RealmAccess realm { get; set; } + private AudioFilter audioDuckFilter; + private readonly BindableDouble audioDuckVolume = new BindableDouble(1); + + [BackgroundDependencyLoader] + private void load(AudioManager audio) + { + AddInternal(audioDuckFilter = new AudioFilter(audio.TrackMixer)); + audio.Tracks.AddAdjustment(AdjustableProperty.Volume, audioDuckVolume); + } + protected override void LoadComplete() { base.LoadComplete(); @@ -243,6 +254,52 @@ namespace osu.Game.Overlays onSuccess?.Invoke(); }); + /// + /// Attenuates the volume and/or filters the currently playing track. + /// + /// Duration of the ducking transition, in ms. + /// Level to drop volume to (1.0 = 100%). + /// Cutoff frequency to drop `AudioFilter` to. Use `AudioFilter.MAX_LOWPASS_CUTOFF` to skip filter effect. + /// Easing for the ducking transition. + public void Duck(int duration = 0, float duckVolumeTo = 0.25f, int duckCutoffTo = 300, Easing easing = Easing.InCubic) + { + Schedule(() => + { + audioDuckFilter?.CutoffTo(duckCutoffTo, duration, easing); + this.TransformBindableTo(audioDuckVolume, duckVolumeTo, duration, easing); + }); + } + + /// + /// Restores the volume to full and stops filtering the currently playing track after having used . + /// + /// Duration of the unducking transition, in ms. + /// Easing for the unducking transition. + public void Unduck(int duration = 500, Easing easing = Easing.InCubic) + { + Schedule(() => + { + audioDuckFilter?.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF, duration, easing); + this.TransformBindableTo(audioDuckVolume, 1, duration, easing); + }); + } + + /// + /// A convenience method that ducks the currently playing track, then after a delay, unducks it. + /// + /// Delay after audio is ducked before unducking begins, in ms. + /// Duration of the unducking transition, in ms. + /// Easing for the unducking transition. + /// Level to drop volume to (1.0 = 100%). + /// Cutoff frequency to drop `AudioFilter` to. Use `AudioFilter.MAX_LOWPASS_CUTOFF` to skip filter effect. + /// Duration of the ducking transition, in ms. + /// Easing for the ducking transition. + public void TimedDuck(int delay, int unduckDuration = 500, Easing unduckEasing = Easing.InCubic, float duckVolumeTo = 0.25f, int duckCutoffTo = 300, int duckDuration = 0, Easing duckEasing = Easing.InCubic) + { + Duck(duckDuration, duckVolumeTo, duckCutoffTo, duckEasing); + Scheduler.AddDelayed(() => Unduck(unduckDuration, unduckEasing), delay); + } + private bool next() { if (beatmap.Disabled || !AllowTrackControl.Value) diff --git a/osu.Game/Overlays/Toolbar/ToolbarRulesetSelector.cs b/osu.Game/Overlays/Toolbar/ToolbarRulesetSelector.cs index d49c340ed4..93be108b71 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarRulesetSelector.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarRulesetSelector.cs @@ -3,8 +3,12 @@ #nullable disable +using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Audio.Sample; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; @@ -21,6 +25,12 @@ namespace osu.Game.Overlays.Toolbar { protected Drawable ModeButtonLine { get; private set; } + [Resolved] + private MusicController musicController { get; set; } + + private readonly Dictionary rulesetSelectionSample = new Dictionary(); + private readonly Dictionary rulesetSelectionChannel = new Dictionary(); + public ToolbarRulesetSelector() { RelativeSizeAxes = Axes.Y; @@ -28,7 +38,7 @@ namespace osu.Game.Overlays.Toolbar } [BackgroundDependencyLoader] - private void load() + private void load(AudioManager audio) { AddRangeInternal(new[] { @@ -54,6 +64,11 @@ namespace osu.Game.Overlays.Toolbar } }, }); + + foreach (var r in Rulesets.AvailableRulesets) + rulesetSelectionSample[r] = audio.Samples.Get($@"UI/ruleset-select-{r.ShortName}"); + + Current.ValueChanged += playRulesetSelectionSample; } protected override void LoadComplete() @@ -84,6 +99,20 @@ namespace osu.Game.Overlays.Toolbar } } + private void playRulesetSelectionSample(ValueChangedEvent r) + { + if (r.OldValue == null) + return; + + if (rulesetSelectionChannel.TryGetValue(r.OldValue, out var sampleChannel)) + sampleChannel?.Stop(); + + rulesetSelectionChannel[r.NewValue] = rulesetSelectionSample[r.NewValue].GetChannel(); + rulesetSelectionChannel[r.NewValue]?.Play(); + + musicController?.TimedDuck(600); + } + public override bool HandleNonPositionalInput => !Current.Disabled && base.HandleNonPositionalInput; public override bool HandlePositionalInput => !Current.Disabled && base.HandlePositionalInput; diff --git a/osu.Game/Overlays/Toolbar/ToolbarRulesetTabButton.cs b/osu.Game/Overlays/Toolbar/ToolbarRulesetTabButton.cs index 0315bede64..3287ac6eaa 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarRulesetTabButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarRulesetTabButton.cs @@ -2,8 +2,6 @@ // See the LICENCE file in the repository root for full licence text. 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.UserInterface; @@ -19,8 +17,6 @@ namespace osu.Game.Overlays.Toolbar { private readonly RulesetButton ruleset; - private Sample? selectSample; - public ToolbarRulesetTabButton(RulesetInfo value) : base(value) { @@ -38,18 +34,10 @@ namespace osu.Game.Overlays.Toolbar ruleset.SetIcon(rInstance.CreateIcon()); } - [BackgroundDependencyLoader] - private void load(AudioManager audio) - { - selectSample = audio.Samples.Get($@"UI/ruleset-select-{Value.ShortName}"); - } - protected override void OnActivated() => ruleset.Active = true; protected override void OnDeactivated() => ruleset.Active = false; - protected override void OnActivatedByUser() => selectSample?.Play(); - private partial class RulesetButton : ToolbarButton { protected override HoverSounds CreateHoverSounds(HoverSampleSet sampleSet) => new HoverSounds(); From 0d11b2b91c8e8a53ffcca580b30679f6614a2bb8 Mon Sep 17 00:00:00 2001 From: Jamie Taylor Date: Sat, 22 Jun 2024 01:57:14 +0900 Subject: [PATCH 1140/2493] Replace manual usages of `AudioFilter` with new ducking methods --- .../Collections/ManageCollectionsDialog.cs | 20 ++++++++----------- .../Dialog/PopupDialogDangerousButton.cs | 13 +++++------- osu.Game/Overlays/DialogOverlay.cs | 20 +++++-------------- 3 files changed, 18 insertions(+), 35 deletions(-) diff --git a/osu.Game/Collections/ManageCollectionsDialog.cs b/osu.Game/Collections/ManageCollectionsDialog.cs index ea663f45fe..e777da05e5 100644 --- a/osu.Game/Collections/ManageCollectionsDialog.cs +++ b/osu.Game/Collections/ManageCollectionsDialog.cs @@ -7,11 +7,11 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; -using osu.Game.Audio.Effects; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; +using osu.Game.Overlays; using osuTK; namespace osu.Game.Collections @@ -21,11 +21,12 @@ namespace osu.Game.Collections private const double enter_duration = 500; private const double exit_duration = 200; - private AudioFilter lowPassFilter = null!; - protected override string PopInSampleName => @"UI/overlay-big-pop-in"; protected override string PopOutSampleName => @"UI/overlay-big-pop-out"; + [Resolved] + private MusicController? musicController { get; set; } + public ManageCollectionsDialog() { Anchor = Anchor.Centre; @@ -110,19 +111,14 @@ namespace osu.Game.Collections }, } } - }, - lowPassFilter = new AudioFilter(audio.TrackMixer) + } }; } - public override bool IsPresent => base.IsPresent - // Safety for low pass filter potentially getting stuck in applied state due to - // transforms on `this` causing children to no longer be updated. - || lowPassFilter.IsAttached; - protected override void PopIn() { - lowPassFilter.CutoffTo(300, 100, Easing.OutCubic); + musicController?.Duck(100, 1f); + this.FadeIn(enter_duration, Easing.OutQuint); this.ScaleTo(0.9f).Then().ScaleTo(1f, enter_duration, Easing.OutQuint); } @@ -131,7 +127,7 @@ namespace osu.Game.Collections { base.PopOut(); - lowPassFilter.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF, 100, Easing.InCubic); + musicController?.Unduck(100); this.FadeOut(exit_duration, Easing.OutQuint); this.ScaleTo(0.9f, exit_duration); diff --git a/osu.Game/Overlays/Dialog/PopupDialogDangerousButton.cs b/osu.Game/Overlays/Dialog/PopupDialogDangerousButton.cs index 19d7ea7a87..e01d4e2d66 100644 --- a/osu.Game/Overlays/Dialog/PopupDialogDangerousButton.cs +++ b/osu.Game/Overlays/Dialog/PopupDialogDangerousButton.cs @@ -10,7 +10,6 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; -using osu.Game.Audio.Effects; using osu.Game.Graphics; using osu.Game.Graphics.Containers; @@ -48,6 +47,9 @@ namespace osu.Game.Overlays.Dialog private partial class DangerousConfirmContainer : HoldToConfirmContainer { + [Resolved] + private MusicController musicController { get; set; } + public DangerousConfirmContainer() : base(isDangerousAction: true) { @@ -56,7 +58,6 @@ namespace osu.Game.Overlays.Dialog private Sample tickSample; private Sample confirmSample; private double lastTickPlaybackTime; - private AudioFilter lowPassFilter = null!; private bool mouseDown; [BackgroundDependencyLoader] @@ -64,8 +65,6 @@ namespace osu.Game.Overlays.Dialog { tickSample = audio.Samples.Get(@"UI/dialog-dangerous-tick"); confirmSample = audio.Samples.Get(@"UI/dialog-dangerous-select"); - - AddInternal(lowPassFilter = new AudioFilter(audio.SampleMixer)); } protected override void LoadComplete() @@ -76,13 +75,13 @@ namespace osu.Game.Overlays.Dialog protected override void AbortConfirm() { - lowPassFilter.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF); + musicController?.Unduck(); base.AbortConfirm(); } protected override void Confirm() { - lowPassFilter.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF); + musicController?.Duck(); confirmSample?.Play(); base.Confirm(); } @@ -126,8 +125,6 @@ namespace osu.Game.Overlays.Dialog if (Clock.CurrentTime - lastTickPlaybackTime < 30) return; - lowPassFilter.CutoffTo((int)(progress.NewValue * AudioFilter.MAX_LOWPASS_CUTOFF * 0.5)); - var channel = tickSample.GetChannel(); channel.Frequency.Value = 1 + progress.NewValue * 0.5f; diff --git a/osu.Game/Overlays/DialogOverlay.cs b/osu.Game/Overlays/DialogOverlay.cs index 9ad532ae50..aa9bb99e01 100644 --- a/osu.Game/Overlays/DialogOverlay.cs +++ b/osu.Game/Overlays/DialogOverlay.cs @@ -10,9 +10,7 @@ using osu.Game.Graphics.Containers; using osu.Game.Input.Bindings; using System.Linq; using osu.Framework.Allocation; -using osu.Framework.Audio; using osu.Framework.Input.Events; -using osu.Game.Audio.Effects; namespace osu.Game.Overlays { @@ -23,15 +21,13 @@ namespace osu.Game.Overlays protected override string PopInSampleName => "UI/dialog-pop-in"; protected override string PopOutSampleName => "UI/dialog-pop-out"; - private AudioFilter lowPassFilter; + [Resolved] + private MusicController musicController { get; set; } public PopupDialog CurrentDialog { get; private set; } public override bool IsPresent => Scheduler.HasPendingTasks - || dialogContainer.Children.Count > 0 - // Safety for low pass filter potentially getting stuck in applied state due to - // transforms on `this` causing children to no longer be updated. - || lowPassFilter.IsAttached; + || dialogContainer.Children.Count > 0; public DialogOverlay() { @@ -49,12 +45,6 @@ namespace osu.Game.Overlays Origin = Anchor.Centre; } - [BackgroundDependencyLoader] - private void load(AudioManager audio) - { - AddInternal(lowPassFilter = new AudioFilter(audio.TrackMixer)); - } - public void Push(PopupDialog dialog) { if (dialog == CurrentDialog || dialog.State.Value == Visibility.Hidden) return; @@ -105,13 +95,13 @@ namespace osu.Game.Overlays protected override void PopIn() { - lowPassFilter.CutoffTo(300, 100, Easing.OutCubic); + musicController.Duck(100, 1f); } protected override void PopOut() { base.PopOut(); - lowPassFilter.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF, 100, Easing.InCubic); + musicController.Unduck(100); // PopOut gets called initially, but we only want to hide dialog when we have been loaded and are present. if (IsLoaded && CurrentDialog?.State.Value == Visibility.Visible) From 80907acaa69e09b75865441646191feb1a3ee0be Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 22 Jun 2024 22:01:05 +0800 Subject: [PATCH 1141/2493] 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 3f2fba4dc2..6114fd769e 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -36,7 +36,7 @@ - + From 2ffeb1b3618a7c3dffc08cef5862ea7cb575d20a Mon Sep 17 00:00:00 2001 From: Jamie Taylor Date: Sun, 23 Jun 2024 02:20:51 +0900 Subject: [PATCH 1142/2493] Add fallback behaviour for custom rulesets --- .../Toolbar/ToolbarRulesetSelector.cs | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/osu.Game/Overlays/Toolbar/ToolbarRulesetSelector.cs b/osu.Game/Overlays/Toolbar/ToolbarRulesetSelector.cs index 93be108b71..7da6b76aaa 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarRulesetSelector.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarRulesetSelector.cs @@ -30,6 +30,7 @@ namespace osu.Game.Overlays.Toolbar private readonly Dictionary rulesetSelectionSample = new Dictionary(); private readonly Dictionary rulesetSelectionChannel = new Dictionary(); + private Sample defaultSelectSample; public ToolbarRulesetSelector() { @@ -68,6 +69,8 @@ namespace osu.Game.Overlays.Toolbar foreach (var r in Rulesets.AvailableRulesets) rulesetSelectionSample[r] = audio.Samples.Get($@"UI/ruleset-select-{r.ShortName}"); + defaultSelectSample = audio.Samples.Get(@"UI/default-select"); + Current.ValueChanged += playRulesetSelectionSample; } @@ -101,15 +104,24 @@ namespace osu.Game.Overlays.Toolbar private void playRulesetSelectionSample(ValueChangedEvent r) { + // Don't play sample on first setting of value if (r.OldValue == null) return; - if (rulesetSelectionChannel.TryGetValue(r.OldValue, out var sampleChannel)) - sampleChannel?.Stop(); + var channel = rulesetSelectionSample[r.NewValue]?.GetChannel(); - rulesetSelectionChannel[r.NewValue] = rulesetSelectionSample[r.NewValue].GetChannel(); - rulesetSelectionChannel[r.NewValue]?.Play(); + // Skip sample choking and ducking for the default/fallback sample + if (channel == null) + { + defaultSelectSample.Play(); + return; + } + foreach (var pair in rulesetSelectionChannel) + pair.Value?.Stop(); + + rulesetSelectionChannel[r.NewValue] = channel; + channel.Play(); musicController?.TimedDuck(600); } From df43a1c6ccf42aaf8a08d659d84b718bef58b826 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 23 Jun 2024 03:31:40 +0800 Subject: [PATCH 1143/2493] Add note about every-frame-transforms --- osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs index 26e9773967..6f419073eb 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs @@ -328,6 +328,10 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables UpdateState(ArmedState.Idle); UpdateComboColour(); + // This method is called every frame. If we need to, the following can likely be converted + // to code which doesn't use transforms at all. + + // Matches stable (see https://github.com/peppy/osu-stable-reference/blob/bb57924c1552adbed11ee3d96cdcde47cf96f2b6/osu!/GameplayElements/HitObjects/Osu/HitCircleOsu.cs#L336-L338) using (BeginAbsoluteSequence(StateUpdateTime - 5)) this.TransformBindableTo(AccentColour, Color4.White, Math.Max(0, HitStateUpdateTime - StateUpdateTime)); From 04efa61156517d2fe21ea4f4995a39670eca7ebb Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sun, 23 Jun 2024 08:22:13 +0300 Subject: [PATCH 1144/2493] Add different display for mod customisation --- .../TestSceneModCustomisationPanel.cs | 47 +++++ .../Localisation/ModSelectOverlayStrings.cs | 5 + .../Overlays/Mods/ModCustomisationHeader.cs | 86 ++++++++++ .../Overlays/Mods/ModCustomisationPanel.cs | 161 ++++++++++++++++++ .../Overlays/Mods/ModCustomisationSection.cs | 82 +++++++++ 5 files changed, 381 insertions(+) create mode 100644 osu.Game.Tests/Visual/UserInterface/TestSceneModCustomisationPanel.cs create mode 100644 osu.Game/Overlays/Mods/ModCustomisationHeader.cs create mode 100644 osu.Game/Overlays/Mods/ModCustomisationPanel.cs create mode 100644 osu.Game/Overlays/Mods/ModCustomisationSection.cs diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModCustomisationPanel.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModCustomisationPanel.cs new file mode 100644 index 0000000000..0066ecd556 --- /dev/null +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModCustomisationPanel.cs @@ -0,0 +1,47 @@ +// 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 NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Overlays; +using osu.Game.Overlays.Mods; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Osu.Mods; + +namespace osu.Game.Tests.Visual.UserInterface +{ + public partial class TestSceneModCustomisationPanel : OsuManualInputManagerTestScene + { + [Cached] + private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine); + + [SetUp] + public void SetUp() => Schedule(() => + { + Child = new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding(20f), + Child = new ModCustomisationPanel + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Width = 400f, + SelectedMods = { BindTarget = SelectedMods }, + } + }; + }); + + [Test] + public void TestDisplay() + { + AddStep("set DT", () => SelectedMods.Value = new[] { new OsuModDoubleTime() }); + AddStep("set DA", () => SelectedMods.Value = new Mod[] { new OsuModDifficultyAdjust() }); + AddStep("set FL+WU+DA+AD", () => SelectedMods.Value = new Mod[] { new OsuModFlashlight(), new ModWindUp(), new OsuModDifficultyAdjust(), new OsuModApproachDifferent() }); + AddStep("set empty", () => SelectedMods.Value = Array.Empty()); + } + } +} diff --git a/osu.Game/Localisation/ModSelectOverlayStrings.cs b/osu.Game/Localisation/ModSelectOverlayStrings.cs index cf01081772..a2e1df42c6 100644 --- a/osu.Game/Localisation/ModSelectOverlayStrings.cs +++ b/osu.Game/Localisation/ModSelectOverlayStrings.cs @@ -75,6 +75,11 @@ namespace osu.Game.Localisation /// public static LocalisableString UnrankedExplanation => new TranslatableString(getKey(@"unranked_explanation"), @"Performance points will not be granted due to active mods."); + /// + /// "Customise" + /// + public static LocalisableString CustomisationPanelHeader => new TranslatableString(getKey(@"customisation_panel_header"), @"Customise"); + private static string getKey(string key) => $@"{prefix}:{key}"; } } diff --git a/osu.Game/Overlays/Mods/ModCustomisationHeader.cs b/osu.Game/Overlays/Mods/ModCustomisationHeader.cs new file mode 100644 index 0000000000..e6534921f6 --- /dev/null +++ b/osu.Game/Overlays/Mods/ModCustomisationHeader.cs @@ -0,0 +1,86 @@ +// 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.Framework.Bindables; +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.Containers; +using osu.Game.Graphics.Sprites; +using osuTK; +using osu.Game.Localisation; + +namespace osu.Game.Overlays.Mods +{ + public partial class ModCustomisationHeader : OsuHoverContainer + { + public override bool HandlePositionalInput => true; + + private Box background = null!; + private SpriteIcon icon = null!; + + [Resolved] + private OverlayColourProvider colourProvider { get; set; } = null!; + + protected override IEnumerable EffectTargets => new[] { background }; + + public readonly BindableBool Expanded = new BindableBool(); + + [BackgroundDependencyLoader] + private void load() + { + CornerRadius = 10f; + Masking = true; + + Children = new Drawable[] + { + background = new Box + { + RelativeSizeAxes = Axes.Both, + }, + new OsuSpriteText + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Text = ModSelectOverlayStrings.CustomisationPanelHeader, + UseFullGlyphHeight = false, + Font = OsuFont.Torus.With(size: 20f, weight: FontWeight.SemiBold), + Margin = new MarginPadding { Left = 20f }, + }, + new Container + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + Size = new Vector2(16f), + Margin = new MarginPadding { Right = 20f }, + Child = icon = new SpriteIcon + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Icon = FontAwesome.Solid.ChevronDown, + RelativeSizeAxes = Axes.Both, + } + } + }; + + IdleColour = colourProvider.Dark3; + HoverColour = colourProvider.Light4; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + Expanded.BindValueChanged(v => + { + icon.RotateTo(v.NewValue ? 180 : 0); + }, true); + + Action = Expanded.Toggle; + } + } +} diff --git a/osu.Game/Overlays/Mods/ModCustomisationPanel.cs b/osu.Game/Overlays/Mods/ModCustomisationPanel.cs new file mode 100644 index 0000000000..3694fe2bde --- /dev/null +++ b/osu.Game/Overlays/Mods/ModCustomisationPanel.cs @@ -0,0 +1,161 @@ +// 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.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Effects; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Input.Events; +using osu.Game.Configuration; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Rulesets.Mods; +using osuTK; + +namespace osu.Game.Overlays.Mods +{ + public partial class ModCustomisationPanel : VisibilityContainer + { + private const float header_height = 42f; + private const float content_vertical_padding = 20f; + + private Container content = null!; + private OsuScrollContainer scrollContainer = null!; + private FillFlowContainer sectionsFlow = null!; + + [Resolved] + private OverlayColourProvider colourProvider { get; set; } = null!; + + public readonly BindableBool Expanded = new BindableBool(); + + public Bindable> SelectedMods { get; } = new Bindable>(Array.Empty()); + + [BackgroundDependencyLoader] + private void load() + { + RelativeSizeAxes = Axes.Y; + + InternalChildren = new Drawable[] + { + new ModCustomisationHeader + { + Depth = float.MinValue, + RelativeSizeAxes = Axes.X, + Height = header_height, + Expanded = { BindTarget = Expanded }, + }, + content = new InputBlockingContainer + { + RelativeSizeAxes = Axes.X, + BorderColour = colourProvider.Dark3, + BorderThickness = 2f, + CornerRadius = 10f, + Masking = true, + EdgeEffect = new EdgeEffectParameters + { + Type = EdgeEffectType.Shadow, + Offset = new Vector2(0f, 5f), + Radius = 20f, + Roundness = 5f, + }, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Dark4, + }, + scrollContainer = new OsuScrollContainer(Direction.Vertical) + { + RelativeSizeAxes = Axes.X, + ScrollbarOverlapsContent = false, + Margin = new MarginPadding { Top = header_height }, + Child = sectionsFlow = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Spacing = new Vector2(0f, 40f), + Margin = new MarginPadding + { + Top = content_vertical_padding, + Bottom = 5f + content_vertical_padding + }, + } + } + } + } + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + Expanded.BindValueChanged(_ => updateDisplay(), true); + SelectedMods.BindValueChanged(_ => updateMods(), true); + + FinishTransforms(true); + } + + protected override void PopIn() => this.FadeIn(300, Easing.OutQuint); + + protected override void PopOut() => this.FadeOut(300, Easing.OutQuint); + + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; + + protected override bool OnMouseDown(MouseDownEvent e) + { + if (Expanded.Value && !content.ReceivePositionalInputAt(e.ScreenSpaceMousePosition)) + Expanded.Value = false; + + return base.OnMouseDown(e); + } + + private void updateDisplay() + { + content.ClearTransforms(); + + if (Expanded.Value) + { + content.AutoSizeDuration = 400; + content.AutoSizeEasing = Easing.OutQuint; + content.AutoSizeAxes = Axes.Y; + content.FadeEdgeEffectTo(0.25f, 120, Easing.OutQuint); + } + else + { + content.AutoSizeAxes = Axes.None; + content.ResizeHeightTo(header_height, 400, Easing.OutQuint); + content.FadeEdgeEffectTo(0f, 400, Easing.OutQuint); + } + } + + private void updateMods() + { + Expanded.Value = false; + sectionsFlow.Clear(); + + // Importantly, the selected mods bindable is already ordered by the mod select overlay (following the order of mod columns and panels). + // Using AsOrdered produces a slightly different order (e.g. DT and NC no longer becoming adjacent), + // which breaks user expectations when interacting with the overlay. + foreach (var mod in SelectedMods.Value) + { + var settings = mod.CreateSettingsControls().ToList(); + + if (settings.Count > 0) + sectionsFlow.Add(new ModCustomisationSection(mod, settings)); + } + } + + protected override void Update() + { + base.Update(); + scrollContainer.Height = Math.Min(scrollContainer.AvailableContent, DrawHeight - header_height); + } + } +} diff --git a/osu.Game/Overlays/Mods/ModCustomisationSection.cs b/osu.Game/Overlays/Mods/ModCustomisationSection.cs new file mode 100644 index 0000000000..1dc97a8b0b --- /dev/null +++ b/osu.Game/Overlays/Mods/ModCustomisationSection.cs @@ -0,0 +1,82 @@ +// 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.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.UI; +using osuTK; + +namespace osu.Game.Overlays.Mods +{ + public partial class ModCustomisationSection : CompositeDrawable + { + public readonly Mod Mod; + + private readonly IReadOnlyList settings; + + public ModCustomisationSection(Mod mod, IReadOnlyList settings) + { + Mod = mod; + + this.settings = settings; + } + + [BackgroundDependencyLoader] + private void load() + { + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + + FillFlowContainer flow; + + InternalChild = flow = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0f, 8f), + Padding = new MarginPadding { Left = 7f }, + Children = new Drawable[] + { + new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding { Left = 20f, Right = 27f }, + Margin = new MarginPadding { Bottom = 4f }, + Children = new Drawable[] + { + new OsuSpriteText + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Text = Mod.Name, + Font = OsuFont.TorusAlternate.With(size: 20, weight: FontWeight.SemiBold), + }, + new ModSwitchTiny(Mod) + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + Active = { Value = true }, + Scale = new Vector2(0.5f), + } + } + }, + } + }; + + flow.AddRange(settings); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + FinishTransforms(true); + } + } +} From e23da93c0947b4692943bdf64ce897fbaac4db93 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sun, 23 Jun 2024 08:22:50 +0300 Subject: [PATCH 1145/2493] Use new display on mod select overlay and remove old display --- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 137 ++++----------- osu.Game/Overlays/Mods/ModSettingsArea.cs | 189 --------------------- 2 files changed, 35 insertions(+), 291 deletions(-) delete mode 100644 osu.Game/Overlays/Mods/ModSettingsArea.cs diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index 145f58fb55..5ee52f32f5 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -109,15 +109,6 @@ namespace osu.Game.Overlays.Mods protected virtual IEnumerable CreateFooterButtons() { - if (AllowCustomisation) - { - yield return CustomisationButton = new ShearedToggleButton(BUTTON_WIDTH) - { - Text = ModSelectOverlayStrings.ModCustomisation, - Active = { BindTarget = customisationVisible } - }; - } - yield return deselectAllModsButton = new DeselectAllModsButton(this); } @@ -125,10 +116,8 @@ namespace osu.Game.Overlays.Mods public IEnumerable AllAvailableMods => AvailableMods.Value.SelectMany(pair => pair.Value); - private readonly BindableBool customisationVisible = new BindableBool(); private Bindable textSearchStartsActive = null!; - private ModSettingsArea modSettingsArea = null!; private ColumnScrollContainer columnScroll = null!; private ColumnFlowContainer columnFlow = null!; private FillFlowContainer footerButtonFlow = null!; @@ -138,9 +127,9 @@ namespace osu.Game.Overlays.Mods private Container aboveColumnsContent = null!; private RankingInformationDisplay? rankingInformationDisplay; private BeatmapAttributesDisplay? beatmapAttributesDisplay; + private ModCustomisationPanel customisationPanel = null!; protected ShearedButton BackButton { get; private set; } = null!; - protected ShearedToggleButton? CustomisationButton { get; private set; } protected SelectAllModsButton? SelectAllModsButton { get; set; } private Sample? columnAppearSample; @@ -173,35 +162,8 @@ namespace osu.Game.Overlays.Mods columnAppearSample = audio.Samples.Get(@"SongSelect/mod-column-pop-in"); - AddRange(new Drawable[] - { - new ClickToReturnContainer - { - RelativeSizeAxes = Axes.Both, - HandleMouse = { BindTarget = customisationVisible }, - OnClicked = () => customisationVisible.Value = false - }, - modSettingsArea = new ModSettingsArea - { - Anchor = Anchor.BottomCentre, - Origin = Anchor.BottomCentre, - Height = 0 - }, - }); - MainAreaContent.AddRange(new Drawable[] { - aboveColumnsContent = new Container - { - RelativeSizeAxes = Axes.X, - Height = RankingInformationDisplay.HEIGHT, - Padding = new MarginPadding { Horizontal = 100 }, - Child = SearchTextBox = new ShearedSearchTextBox - { - HoldFocus = false, - Width = 300 - } - }, new OsuContextMenuContainer { RelativeSizeAxes = Axes.Both, @@ -237,7 +199,27 @@ namespace osu.Game.Overlays.Mods } } } - } + }, + aboveColumnsContent = new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Horizontal = 100, Bottom = 15f }, + Children = new Drawable[] + { + SearchTextBox = new ShearedSearchTextBox + { + HoldFocus = false, + Width = 300, + }, + customisationPanel = new ModCustomisationPanel + { + Alpha = 0f, + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + Width = 400, + } + } + }, }); FooterContent.Add(footerButtonFlow = new FillFlowContainer @@ -320,7 +302,7 @@ namespace osu.Game.Overlays.Mods // This is an optimisation to prevent refreshing the available settings controls when it can be // reasonably assumed that the settings panel is never to be displayed (e.g. FreeModSelectOverlay). if (AllowCustomisation) - ((IBindable>)modSettingsArea.SelectedMods).BindTo(SelectedMods); + ((IBindable>)customisationPanel.SelectedMods).BindTo(SelectedMods); SelectedMods.BindValueChanged(_ => { @@ -347,7 +329,7 @@ namespace osu.Game.Overlays.Mods } }, true); - customisationVisible.BindValueChanged(_ => updateCustomisationVisualState(), true); + customisationPanel.Expanded.BindValueChanged(_ => updateCustomisationVisualState(), true); SearchTextBox.Current.BindValueChanged(query => { @@ -491,7 +473,7 @@ namespace osu.Game.Overlays.Mods private void updateCustomisation() { - if (CustomisationButton == null) + if (!AllowCustomisation) return; bool anyCustomisableModActive = false; @@ -506,38 +488,21 @@ namespace osu.Game.Overlays.Mods if (anyCustomisableModActive) { - customisationVisible.Disabled = false; + customisationPanel.Show(); - if (anyModPendingConfiguration && !customisationVisible.Value) - customisationVisible.Value = true; + if (anyModPendingConfiguration) + customisationPanel.Expanded.Value = true; } else { - if (customisationVisible.Value) - customisationVisible.Value = false; - - customisationVisible.Disabled = true; + customisationPanel.Expanded.Value = false; + customisationPanel.Hide(); } } private void updateCustomisationVisualState() { - const double transition_duration = 300; - - MainAreaContent.FadeColour(customisationVisible.Value ? Colour4.Gray : Colour4.White, transition_duration, Easing.InOutCubic); - - foreach (var button in footerButtonFlow) - { - if (button != CustomisationButton) - button.Enabled.Value = !customisationVisible.Value; - } - - float modAreaHeight = customisationVisible.Value ? ModSettingsArea.HEIGHT : 0; - - modSettingsArea.ResizeHeightTo(modAreaHeight, transition_duration, Easing.InOutCubic); - TopLevelContent.MoveToY(-modAreaHeight, transition_duration, Easing.InOutCubic); - - if (customisationVisible.Value) + if (customisationPanel.Expanded.Value) SearchTextBox.KillFocus(); else setTextBoxFocus(textSearchStartsActive.Value); @@ -693,6 +658,8 @@ namespace osu.Game.Overlays.Mods if (!allFiltered) nonFilteredColumnCount += 1; } + + customisationPanel.Expanded.Value = false; } #endregion @@ -758,10 +725,9 @@ namespace osu.Game.Overlays.Mods void hideOverlay(bool immediate) { - if (customisationVisible.Value) + if (customisationPanel.Expanded.Value) { - Debug.Assert(CustomisationButton != null); - CustomisationButton.TriggerClick(); + customisationPanel.Expanded.Value = false; if (!immediate) return; @@ -967,38 +933,5 @@ namespace osu.Game.Overlays.Mods updateState(); } } - - /// - /// A container which blocks and handles input, managing the "return from customisation" state change. - /// - private partial class ClickToReturnContainer : Container - { - public BindableBool HandleMouse { get; } = new BindableBool(); - - public Action? OnClicked { get; set; } - - public override bool HandlePositionalInput => base.HandlePositionalInput && HandleMouse.Value; - - protected override bool Handle(UIEvent e) - { - if (!HandleMouse.Value) - return base.Handle(e); - - switch (e) - { - case ClickEvent: - OnClicked?.Invoke(); - return true; - - case HoverEvent: - return false; - - case MouseEvent: - return true; - } - - return base.Handle(e); - } - } } } diff --git a/osu.Game/Overlays/Mods/ModSettingsArea.cs b/osu.Game/Overlays/Mods/ModSettingsArea.cs deleted file mode 100644 index d0e0f7e648..0000000000 --- a/osu.Game/Overlays/Mods/ModSettingsArea.cs +++ /dev/null @@ -1,189 +0,0 @@ -// 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.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Input.Events; -using osu.Game.Configuration; -using osu.Game.Graphics; -using osu.Game.Graphics.Containers; -using osu.Game.Graphics.Sprites; -using osu.Game.Rulesets.Mods; -using osu.Game.Rulesets.UI; -using osuTK; - -namespace osu.Game.Overlays.Mods -{ - public partial class ModSettingsArea : CompositeDrawable - { - public Bindable> SelectedMods { get; } = new Bindable>(Array.Empty()); - - public const float HEIGHT = 250; - - private readonly Box background; - private readonly FillFlowContainer modSettingsFlow; - - [Resolved] - private OverlayColourProvider colourProvider { get; set; } = null!; - - public override bool AcceptsFocus => true; - - public ModSettingsArea() - { - RelativeSizeAxes = Axes.X; - Height = HEIGHT; - - Anchor = Anchor.BottomRight; - Origin = Anchor.BottomRight; - - InternalChild = new Container - { - RelativeSizeAxes = Axes.Both, - Masking = true, - Children = new Drawable[] - { - background = new Box - { - RelativeSizeAxes = Axes.Both - }, - new OsuScrollContainer(Direction.Horizontal) - { - RelativeSizeAxes = Axes.Both, - ScrollbarOverlapsContent = false, - ClampExtension = 100, - Child = modSettingsFlow = new FillFlowContainer - { - AutoSizeAxes = Axes.X, - RelativeSizeAxes = Axes.Y, - Padding = new MarginPadding { Vertical = 7, Horizontal = 70 }, - Spacing = new Vector2(7), - Direction = FillDirection.Horizontal - } - } - } - }; - } - - [BackgroundDependencyLoader] - private void load() - { - background.Colour = colourProvider.Dark3; - } - - protected override void LoadComplete() - { - base.LoadComplete(); - SelectedMods.BindValueChanged(_ => updateMods(), true); - } - - private void updateMods() - { - modSettingsFlow.Clear(); - - // Importantly, the selected mods bindable is already ordered by the mod select overlay (following the order of mod columns and panels). - // Using AsOrdered produces a slightly different order (e.g. DT and NC no longer becoming adjacent), - // which breaks user expectations when interacting with the overlay. - foreach (var mod in SelectedMods.Value) - { - var settings = mod.CreateSettingsControls().ToList(); - - if (settings.Count > 0) - { - if (modSettingsFlow.Any()) - { - modSettingsFlow.Add(new Box - { - RelativeSizeAxes = Axes.Y, - Width = 2, - Colour = colourProvider.Dark4, - }); - } - - modSettingsFlow.Add(new ModSettingsColumn(mod, settings)); - } - } - } - - protected override bool OnMouseDown(MouseDownEvent e) => true; - protected override bool OnHover(HoverEvent e) => true; - - public partial class ModSettingsColumn : CompositeDrawable - { - public readonly Mod Mod; - - public ModSettingsColumn(Mod mod, IEnumerable settingsControls) - { - Mod = mod; - - Width = 250; - RelativeSizeAxes = Axes.Y; - Padding = new MarginPadding { Bottom = 7 }; - - InternalChild = new GridContainer - { - RelativeSizeAxes = Axes.Both, - RowDimensions = new[] - { - new Dimension(GridSizeMode.AutoSize), - new Dimension(GridSizeMode.Absolute, 10), - new Dimension() - }, - Content = new[] - { - new Drawable[] - { - new FillFlowContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(7), - Children = new Drawable[] - { - new ModSwitchTiny(mod) - { - Active = { Value = true }, - Scale = new Vector2(0.6f), - Origin = Anchor.CentreLeft, - Anchor = Anchor.CentreLeft - }, - new OsuSpriteText - { - Text = mod.Name, - Font = OsuFont.Default.With(size: 16, weight: FontWeight.SemiBold), - Origin = Anchor.CentreLeft, - Anchor = Anchor.CentreLeft, - Margin = new MarginPadding { Bottom = 2 } - } - } - } - }, - new[] { Empty() }, - new Drawable[] - { - new OsuScrollContainer(Direction.Vertical) - { - RelativeSizeAxes = Axes.Both, - ClampExtension = 100, - Child = new FillFlowContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Padding = new MarginPadding { Right = 7 }, - ChildrenEnumerable = settingsControls, - Spacing = new Vector2(0, 7) - } - } - } - } - }; - } - } - } -} From 1298b98534246c5dc325a2cb9aae3333f1f38eeb Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sun, 23 Jun 2024 08:22:54 +0300 Subject: [PATCH 1146/2493] Update test coverage --- .../TestSceneFreeModSelectOverlay.cs | 2 +- .../TestSceneModSelectOverlay.cs | 63 +++++++++++-------- .../UserInterface/TestSceneModSettingsArea.cs | 42 ------------- 3 files changed, 38 insertions(+), 69 deletions(-) delete mode 100644 osu.Game.Tests/Visual/UserInterface/TestSceneModSettingsArea.cs diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneFreeModSelectOverlay.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneFreeModSelectOverlay.cs index a4feffddfb..938ab1e9f4 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneFreeModSelectOverlay.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneFreeModSelectOverlay.cs @@ -58,7 +58,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("select difficulty adjust", () => freeModSelectOverlay.SelectedMods.Value = new[] { new OsuModDifficultyAdjust() }); AddWaitStep("wait some", 3); - AddAssert("customisation area not expanded", () => this.ChildrenOfType().Single().Height == 0); + AddAssert("customisation area not expanded", () => !this.ChildrenOfType().Single().Expanded.Value); } [Test] diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs index a1452ddb31..39017298ac 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs @@ -7,7 +7,6 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Input; @@ -26,7 +25,6 @@ using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Taiko.Mods; using osu.Game.Tests.Mods; -using osuTK; using osuTK.Input; namespace osu.Game.Tests.Visual.UserInterface @@ -225,7 +223,7 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep("dismiss mod customisation via toggle", () => { - InputManager.MoveMouseTo(modSelectOverlay.CustomisationButton.AsNonNull()); + InputManager.MoveMouseTo(modSelectOverlay.ChildrenOfType().Single()); InputManager.Click(MouseButton.Left); }); assertCustomisationToggleState(disabled: false, active: false); @@ -258,7 +256,7 @@ namespace osu.Game.Tests.Visual.UserInterface } [Test] - public void TestDismissCustomisationViaDimmedArea() + public void TestDismissCustomisationViaClickingAway() { createScreen(); assertCustomisationToggleState(disabled: true, active: false); @@ -266,18 +264,23 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep("select difficulty adjust via panel", () => getPanelForMod(typeof(OsuModDifficultyAdjust)).TriggerClick()); assertCustomisationToggleState(disabled: false, active: true); - AddStep("move mouse to settings area", () => InputManager.MoveMouseTo(this.ChildrenOfType().Single())); - AddStep("move mouse to dimmed area", () => - { - InputManager.MoveMouseTo(new Vector2( - modSelectOverlay.ScreenSpaceDrawQuad.TopLeft.X, - (modSelectOverlay.ScreenSpaceDrawQuad.TopLeft.Y + modSelectOverlay.ScreenSpaceDrawQuad.BottomLeft.Y) / 2)); - }); + AddStep("move mouse to search bar", () => InputManager.MoveMouseTo(modSelectOverlay.ChildrenOfType().Single())); AddStep("click", () => InputManager.Click(MouseButton.Left)); assertCustomisationToggleState(disabled: false, active: false); + } - AddStep("move mouse to first mod panel", () => InputManager.MoveMouseTo(modSelectOverlay.ChildrenOfType().First())); - AddAssert("first mod panel is hovered", () => modSelectOverlay.ChildrenOfType().First().IsHovered); + [Test] + public void TestDismissCustomisationWhenHidingOverlay() + { + createScreen(); + assertCustomisationToggleState(disabled: true, active: false); + + AddStep("select difficulty adjust via panel", () => getPanelForMod(typeof(OsuModDifficultyAdjust)).TriggerClick()); + assertCustomisationToggleState(disabled: false, active: true); + + AddStep("hide overlay", () => modSelectOverlay.Hide()); + AddStep("show overlay again", () => modSelectOverlay.Show()); + assertCustomisationToggleState(disabled: false, active: false); } /// @@ -339,7 +342,7 @@ namespace osu.Game.Tests.Visual.UserInterface createScreen(); changeRuleset(0); - AddStep("Select all fun mods", () => + AddStep("Select all difficulty-increase mods", () => { modSelectOverlay.ChildrenOfType() .Single(c => c.ModType == ModType.DifficultyIncrease) @@ -641,13 +644,16 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep("select DT", () => SelectedMods.Value = new Mod[] { new OsuModDoubleTime() }); AddAssert("DT selected", () => modSelectOverlay.ChildrenOfType().Count(panel => panel.Active.Value), () => Is.EqualTo(1)); - AddStep("open customisation area", () => modSelectOverlay.CustomisationButton!.TriggerClick()); - assertCustomisationToggleState(false, true); + // todo: rewrite + AddStep("open customisation area", () => modSelectOverlay.ChildrenOfType().Single().TriggerClick()); + assertCustomisationToggleState(disabled: false, active: true); + AddStep("hover over mod settings slider", () => { - var slider = modSelectOverlay.ChildrenOfType().Single().ChildrenOfType>().First(); + var slider = modSelectOverlay.ChildrenOfType().Single().ChildrenOfType>().First(); InputManager.MoveMouseTo(slider); }); + AddStep("press right arrow", () => InputManager.PressKey(Key.Right)); AddAssert("DT speed changed", () => !SelectedMods.Value.OfType().Single().SpeedChange.IsDefault); @@ -744,9 +750,10 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep("select difficulty adjust via panel", () => getPanelForMod(typeof(OsuModDifficultyAdjust)).TriggerClick()); assertCustomisationToggleState(disabled: false, active: true); - AddAssert("back button disabled", () => !modSelectOverlay.BackButton.Enabled.Value); AddStep("dismiss customisation area", () => InputManager.Key(Key.Escape)); + AddAssert("mod select still visible", () => modSelectOverlay.State.Value == Visibility.Visible); + AddStep("click back button", () => { InputManager.MoveMouseTo(modSelectOverlay.BackButton); @@ -870,8 +877,8 @@ namespace osu.Game.Tests.Visual.UserInterface // it is instrumental in the reproduction of the failure scenario that this test is supposed to cover. AddStep("force collection", GC.Collect); - AddStep("open customisation area", () => modSelectOverlay.CustomisationButton!.TriggerClick()); - AddStep("reset half time speed to default", () => modSelectOverlay.ChildrenOfType().Single() + AddStep("open customisation area", () => modSelectOverlay.ChildrenOfType().Single().TriggerClick()); + AddStep("reset half time speed to default", () => modSelectOverlay.ChildrenOfType().Single() .ChildrenOfType>().Single().TriggerClick()); AddUntilStep("difficulty multiplier display shows correct value", () => modSelectOverlay.ChildrenOfType().Single().ModMultiplier.Value, () => Is.EqualTo(0.3).Within(Precision.DOUBLE_EPSILON)); @@ -883,18 +890,23 @@ namespace osu.Game.Tests.Visual.UserInterface createScreen(); AddStep("select DT + HD + DF", () => SelectedMods.Value = new Mod[] { new OsuModDoubleTime(), new OsuModHidden(), new OsuModDeflate() }); + AddStep("open customisation panel", () => this.ChildrenOfType().Single().TriggerClick()); AddAssert("mod settings order: DT, HD, DF", () => { - var columns = this.ChildrenOfType().Single().ChildrenOfType(); + var columns = this.ChildrenOfType(); return columns.ElementAt(0).Mod is OsuModDoubleTime && columns.ElementAt(1).Mod is OsuModHidden && columns.ElementAt(2).Mod is OsuModDeflate; }); - AddStep("replace DT with NC", () => SelectedMods.Value = SelectedMods.Value.Where(m => m is not ModDoubleTime).Append(new OsuModNightcore()).ToList()); + AddStep("replace DT with NC", () => + { + SelectedMods.Value = SelectedMods.Value.Where(m => m is not ModDoubleTime).Append(new OsuModNightcore()).ToList(); + this.ChildrenOfType().Single().TriggerClick(); + }); AddAssert("mod settings order: NC, HD, DF", () => { - var columns = this.ChildrenOfType().Single().ChildrenOfType(); + var columns = this.ChildrenOfType(); return columns.ElementAt(0).Mod is OsuModNightcore && columns.ElementAt(1).Mod is OsuModHidden && columns.ElementAt(2).Mod is OsuModDeflate; @@ -915,8 +927,8 @@ namespace osu.Game.Tests.Visual.UserInterface private void assertCustomisationToggleState(bool disabled, bool active) { - AddAssert($"customisation toggle is {(disabled ? "" : "not ")}disabled", () => modSelectOverlay.CustomisationButton.AsNonNull().Active.Disabled == disabled); - AddAssert($"customisation toggle is {(active ? "" : "not ")}active", () => modSelectOverlay.CustomisationButton.AsNonNull().Active.Value == active); + AddUntilStep($"customisation panel is {(disabled ? "" : "not ")}disabled", () => modSelectOverlay.ChildrenOfType().Single().State.Value == (disabled ? Visibility.Hidden : Visibility.Visible)); + AddAssert($"customisation panel is {(active ? "" : "not ")}active", () => modSelectOverlay.ChildrenOfType().Single().Expanded.Value == active); } private T getSelectedMod() where T : Mod => SelectedMods.Value.OfType().Single(); @@ -929,7 +941,6 @@ namespace osu.Game.Tests.Visual.UserInterface protected override bool ShowPresets => true; public new ShearedButton BackButton => base.BackButton; - public new ShearedToggleButton? CustomisationButton => base.CustomisationButton; } private class TestUnimplementedMod : Mod diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSettingsArea.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSettingsArea.cs deleted file mode 100644 index dac1f94c28..0000000000 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSettingsArea.cs +++ /dev/null @@ -1,42 +0,0 @@ -// 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 NUnit.Framework; -using osu.Framework.Allocation; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Game.Overlays; -using osu.Game.Overlays.Mods; -using osu.Game.Rulesets.Mods; -using osu.Game.Rulesets.Osu.Mods; - -namespace osu.Game.Tests.Visual.UserInterface -{ - [TestFixture] - public partial class TestSceneModSettingsArea : OsuTestScene - { - [Cached] - private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Green); - - [Test] - public void TestModToggleArea() - { - ModSettingsArea modSettingsArea = null; - - AddStep("create content", () => Child = new Container - { - RelativeSizeAxes = Axes.Both, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Child = modSettingsArea = new ModSettingsArea() - }); - AddStep("set DT", () => modSettingsArea.SelectedMods.Value = new[] { new OsuModDoubleTime() }); - AddStep("set DA", () => modSettingsArea.SelectedMods.Value = new Mod[] { new OsuModDifficultyAdjust() }); - AddStep("set FL+WU+DA+AD", () => modSettingsArea.SelectedMods.Value = new Mod[] { new OsuModFlashlight(), new ModWindUp(), new OsuModDifficultyAdjust(), new OsuModApproachDifferent() }); - AddStep("set empty", () => modSettingsArea.SelectedMods.Value = Array.Empty()); - } - } -} From 2efafcaf5b7ad4608d7c1305af691a693045444a Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sun, 23 Jun 2024 08:46:55 +0300 Subject: [PATCH 1147/2493] Remove stale comment --- osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs index 39017298ac..a9db957e12 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs @@ -644,7 +644,6 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep("select DT", () => SelectedMods.Value = new Mod[] { new OsuModDoubleTime() }); AddAssert("DT selected", () => modSelectOverlay.ChildrenOfType().Count(panel => panel.Active.Value), () => Is.EqualTo(1)); - // todo: rewrite AddStep("open customisation area", () => modSelectOverlay.ChildrenOfType().Single().TriggerClick()); assertCustomisationToggleState(disabled: false, active: true); From 1dc9f102358ef72ff8a7ce41bfcbbbb66a041a1b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 24 Jun 2024 09:46:23 +0800 Subject: [PATCH 1148/2493] Fix scale control key binding breaking previous defaults Oops from ppy/osu#28309. --- osu.Game/Input/Bindings/GlobalActionContainer.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Input/Bindings/GlobalActionContainer.cs b/osu.Game/Input/Bindings/GlobalActionContainer.cs index 2af564d8ba..2452852f6f 100644 --- a/osu.Game/Input/Bindings/GlobalActionContainer.cs +++ b/osu.Game/Input/Bindings/GlobalActionContainer.cs @@ -412,9 +412,6 @@ namespace osu.Game.Input.Bindings [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorToggleRotateControl))] EditorToggleRotateControl, - [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorToggleScaleControl))] - EditorToggleScaleControl, - [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.IncreaseOffset))] IncreaseOffset, @@ -432,6 +429,9 @@ namespace osu.Game.Input.Bindings [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.DecreaseModSpeed))] DecreaseModSpeed, + + [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorToggleScaleControl))] + EditorToggleScaleControl, } public enum GlobalActionCategory From 66b093b17e4166aec436ee4fd5f6a327155b144f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 3 Jun 2024 14:29:15 +0200 Subject: [PATCH 1149/2493] Implement score breakdown display for daily challenge screen --- .../TestSceneDailyChallengeScoreBreakdown.cs | 61 ++++++ .../DailyChallengeScoreBreakdown.cs | 193 ++++++++++++++++++ 2 files changed, 254 insertions(+) create mode 100644 osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeScoreBreakdown.cs create mode 100644 osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeScoreBreakdown.cs diff --git a/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeScoreBreakdown.cs b/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeScoreBreakdown.cs new file mode 100644 index 0000000000..5523d02694 --- /dev/null +++ b/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeScoreBreakdown.cs @@ -0,0 +1,61 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Extensions.ObjectExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Utils; +using osu.Game.Overlays; +using osu.Game.Screens.OnlinePlay.DailyChallenge; +using osu.Game.Tests.Resources; + +namespace osu.Game.Tests.Visual.DailyChallenge +{ + public partial class TestSceneDailyChallengeScoreBreakdown : OsuTestScene + { + [Cached] + private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Plum); + + [Test] + public void TestBasicAppearance() + { + DailyChallengeScoreBreakdown breakdown = null!; + + AddStep("create content", () => Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background4, + }, + breakdown = new DailyChallengeScoreBreakdown + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + } + }); + AddSliderStep("adjust width", 0.1f, 1, 1, width => + { + if (breakdown.IsNotNull()) + breakdown.Width = width; + }); + AddSliderStep("adjust height", 0.1f, 1, 1, height => + { + if (breakdown.IsNotNull()) + breakdown.Height = height; + }); + + AddStep("set initial data", () => breakdown.SetInitialCounts([1, 4, 9, 16, 25, 36, 49, 36, 25, 16, 9, 4, 1])); + AddStep("add new score", () => + { + var testScore = TestResources.CreateTestScoreInfo(); + testScore.TotalScore = RNG.Next(1_000_000); + + breakdown.AddNewScore(testScore); + }); + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeScoreBreakdown.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeScoreBreakdown.cs new file mode 100644 index 0000000000..b5c44e42a5 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeScoreBreakdown.cs @@ -0,0 +1,193 @@ +// 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.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Localisation; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osu.Game.Overlays; +using osu.Game.Scoring; +using osuTK; + +namespace osu.Game.Screens.OnlinePlay.DailyChallenge +{ + public partial class DailyChallengeScoreBreakdown : CompositeDrawable + { + private FillFlowContainer barsContainer = null!; + + private const int bin_count = 13; + private long[] bins = new long[bin_count]; + + [BackgroundDependencyLoader] + private void load() + { + InternalChildren = new Drawable[] + { + new SectionHeader("Score breakdown"), + barsContainer = new FillFlowContainer + { + Direction = FillDirection.Horizontal, + RelativeSizeAxes = Axes.Both, + Height = 0.9f, + Padding = new MarginPadding { Top = 35 }, + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + } + }; + + for (int i = 0; i < bin_count; ++i) + { + LocalisableString? label = null; + + switch (i) + { + case 2: + case 4: + case 6: + case 8: + label = @$"{100 * i}k"; + break; + + case 10: + label = @"1M"; + break; + } + + barsContainer.Add(new Bar(label) + { + Width = 1f / bin_count, + }); + } + } + + public void AddNewScore(IScoreInfo scoreInfo) + { + int targetBin = (int)Math.Clamp(Math.Floor((float)scoreInfo.TotalScore / 100000), 0, bin_count - 1); + bins[targetBin] += 1; + updateCounts(); + + var text = new OsuSpriteText + { + Text = scoreInfo.TotalScore.ToString(@"N0"), + Anchor = Anchor.TopCentre, + Origin = Anchor.BottomCentre, + Font = OsuFont.Default.With(size: 30), + RelativePositionAxes = Axes.X, + X = (targetBin + 0.5f) / bin_count - 0.5f, + Alpha = 0, + }; + AddInternal(text); + + Scheduler.AddDelayed(() => + { + float startY = ToLocalSpace(barsContainer[targetBin].CircularBar.ScreenSpaceDrawQuad.TopLeft).Y; + text.FadeInFromZero() + .ScaleTo(new Vector2(0.8f), 500, Easing.OutElasticHalf) + .MoveToY(startY) + .MoveToOffset(new Vector2(0, -50), 2500, Easing.OutQuint) + .FadeOut(2500, Easing.OutQuint) + .Expire(); + }, 150); + } + + public void SetInitialCounts(long[] counts) + { + if (counts.Length != bin_count) + throw new ArgumentException(@"Incorrect number of bins.", nameof(counts)); + + bins = counts; + updateCounts(); + } + + private void updateCounts() + { + long max = bins.Max(); + for (int i = 0; i < bin_count; ++i) + barsContainer[i].UpdateCounts(bins[i], max); + } + + private partial class Bar : CompositeDrawable + { + private readonly LocalisableString? label; + + private long count; + private long max; + + public Container CircularBar { get; private set; } = null!; + + public Bar(LocalisableString? label = null) + { + this.label = label; + } + + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider) + { + RelativeSizeAxes = Axes.Both; + + AddInternal(new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding + { + Bottom = 20, + Horizontal = 3, + }, + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + Masking = true, + Child = CircularBar = new Container + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + Height = 0.01f, + Masking = true, + CornerRadius = 10, + Colour = colourProvider.Highlight1, + Child = new Box + { + RelativeSizeAxes = Axes.Both, + } + } + }); + + if (label != null) + { + AddInternal(new OsuSpriteText + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomCentre, + Text = label.Value, + Colour = colourProvider.Content2, + }); + } + } + + protected override void Update() + { + base.Update(); + + CircularBar.CornerRadius = CircularBar.DrawWidth / 4; + } + + public void UpdateCounts(long newCount, long newMax) + { + bool isIncrement = newCount > count; + + count = newCount; + max = newMax; + + CircularBar.ResizeHeightTo(0.01f + 0.99f * count / max, 300, Easing.OutQuint); + if (isIncrement) + CircularBar.FlashColour(Colour4.White, 600, Easing.OutQuint); + } + } + } +} From fbc99894279ab1da456cef2ceebc0615eefe24b4 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 25 Jun 2024 01:01:26 +0300 Subject: [PATCH 1150/2493] Simplify default layout initialisation --- osu.Game/Skinning/ArgonSkinTransformer.cs | 41 ++++++++-------------- osu.Game/Skinning/LegacySkinTransformer.cs | 37 +++++-------------- 2 files changed, 23 insertions(+), 55 deletions(-) diff --git a/osu.Game/Skinning/ArgonSkinTransformer.cs b/osu.Game/Skinning/ArgonSkinTransformer.cs index 387a7a9c0b..8ca8f79b41 100644 --- a/osu.Game/Skinning/ArgonSkinTransformer.cs +++ b/osu.Game/Skinning/ArgonSkinTransformer.cs @@ -1,8 +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.Linq; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Game.Screens.Play.HUD; using osuTK; @@ -17,34 +17,21 @@ namespace osu.Game.Skinning public override Drawable? GetDrawableComponent(ISkinComponentLookup lookup) { - switch (lookup) + if (lookup is SkinComponentsContainerLookup containerLookup + && containerLookup.Target == SkinComponentsContainerLookup.TargetArea.MainHUDComponents + && containerLookup.Ruleset != null) { - case SkinComponentsContainerLookup containerLookup: - switch (containerLookup.Target) + return base.GetDrawableComponent(lookup) ?? new Container + { + RelativeSizeAxes = Axes.Both, + Child = new ArgonComboCounter { - case SkinComponentsContainerLookup.TargetArea.MainHUDComponents when containerLookup.Ruleset != null: - var rulesetHUDComponents = Skin.GetDrawableComponent(lookup); - - rulesetHUDComponents ??= new DefaultSkinComponentsContainer(container => - { - var combo = container.OfType().FirstOrDefault(); - - if (combo != null) - { - combo.Anchor = Anchor.BottomLeft; - combo.Origin = Anchor.BottomLeft; - combo.Position = new Vector2(36, -66); - combo.Scale = new Vector2(1.3f); - } - }) - { - new ArgonComboCounter(), - }; - - return rulesetHUDComponents; - } - - break; + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Position = new Vector2(36, -66), + Scale = new Vector2(1.3f), + }, + }; } return base.GetDrawableComponent(lookup); diff --git a/osu.Game/Skinning/LegacySkinTransformer.cs b/osu.Game/Skinning/LegacySkinTransformer.cs index 3ea316c0c7..dbfa52de84 100644 --- a/osu.Game/Skinning/LegacySkinTransformer.cs +++ b/osu.Game/Skinning/LegacySkinTransformer.cs @@ -1,12 +1,11 @@ // 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.Audio.Sample; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Game.Audio; using osu.Game.Rulesets.Objects.Legacy; -using osuTK; using static osu.Game.Skinning.SkinConfiguration; namespace osu.Game.Skinning @@ -25,33 +24,15 @@ namespace osu.Game.Skinning public override Drawable? GetDrawableComponent(ISkinComponentLookup lookup) { - switch (lookup) + if (lookup is SkinComponentsContainerLookup containerLookup + && containerLookup.Target == SkinComponentsContainerLookup.TargetArea.MainHUDComponents + && containerLookup.Ruleset != null) { - case SkinComponentsContainerLookup containerLookup: - switch (containerLookup.Target) - { - case SkinComponentsContainerLookup.TargetArea.MainHUDComponents when containerLookup.Ruleset != null: - var rulesetHUDComponents = base.GetDrawableComponent(lookup); - - rulesetHUDComponents ??= new DefaultSkinComponentsContainer(container => - { - var combo = container.OfType().FirstOrDefault(); - - if (combo != null) - { - combo.Anchor = Anchor.BottomLeft; - combo.Origin = Anchor.BottomLeft; - combo.Scale = new Vector2(1.28f); - } - }) - { - new LegacyComboCounter() - }; - - return rulesetHUDComponents; - } - - break; + return base.GetDrawableComponent(lookup) ?? new Container + { + RelativeSizeAxes = Axes.Both, + Child = new LegacyComboCounter(), + }; } return base.GetDrawableComponent(lookup); From e8de293be5f813731f97ab8132c569c914dca59a Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 25 Jun 2024 01:01:32 +0300 Subject: [PATCH 1151/2493] Remove pointless assert --- .../Skinning/Legacy/CatchLegacySkinTransformer.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs b/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs index 462fd5ab64..17218b459a 100644 --- a/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Diagnostics; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Skinning; @@ -33,7 +32,6 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy switch (containerLookup.Target) { case SkinComponentsContainerLookup.TargetArea.MainHUDComponents when containerLookup.Ruleset != null: - Debug.Assert(containerLookup.Ruleset.ShortName == CatchRuleset.SHORT_NAME); // todo: remove CatchSkinComponents.CatchComboCounter and refactor LegacyCatchComboCounter to be added here instead. return Skin.GetDrawableComponent(lookup); } From 78e0126f16e90aca5459b288e42443bbf2b3387e Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 25 Jun 2024 04:24:58 +0300 Subject: [PATCH 1152/2493] Migrate combo counter layouts in custom skins to correct target-ruleset pairs --- osu.Game/Database/RealmAccess.cs | 3 ++- osu.Game/Skinning/ArgonSkin.cs | 19 +++++++++++++++++++ osu.Game/Skinning/LegacySkin.cs | 19 +++++++++++++++++++ osu.Game/Skinning/SkinImporter.cs | 2 ++ osu.Game/Skinning/SkinInfo.cs | 15 +++++++++++++++ 5 files changed, 57 insertions(+), 1 deletion(-) diff --git a/osu.Game/Database/RealmAccess.cs b/osu.Game/Database/RealmAccess.cs index 1ece81be50..606bc5e10c 100644 --- a/osu.Game/Database/RealmAccess.cs +++ b/osu.Game/Database/RealmAccess.cs @@ -93,8 +93,9 @@ namespace osu.Game.Database /// 39 2023-12-19 Migrate any EndTimeObjectCount and TotalObjectCount values of 0 to -1 to better identify non-calculated values. /// 40 2023-12-21 Add ScoreInfo.Version to keep track of which build scores were set on. /// 41 2024-04-17 Add ScoreInfo.TotalScoreWithoutMods for future mod multiplier rebalances. + /// 42 2024-06-25 Add SkinInfo.LayoutVersion to allow performing migrations of components on structural changes. /// - private const int schema_version = 41; + private const int schema_version = 42; /// /// Lock object which is held during sections, blocking realm retrieval during blocking periods. diff --git a/osu.Game/Skinning/ArgonSkin.cs b/osu.Game/Skinning/ArgonSkin.cs index 707281db31..743ce38810 100644 --- a/osu.Game/Skinning/ArgonSkin.cs +++ b/osu.Game/Skinning/ArgonSkin.cs @@ -12,6 +12,7 @@ using osu.Game.Audio; using osu.Game.Beatmaps.Formats; using osu.Game.Extensions; using osu.Game.IO; +using osu.Game.Rulesets; using osu.Game.Screens.Play; using osu.Game.Screens.Play.HUD; using osu.Game.Screens.Play.HUD.HitErrorMeters; @@ -69,6 +70,24 @@ namespace osu.Game.Skinning // Purple new Color4(92, 0, 241, 255), }; + + if (skin.LayoutVersion < 20240625 + && LayoutInfos.TryGetValue(SkinComponentsContainerLookup.TargetArea.MainHUDComponents, out var hudLayout) + && hudLayout.TryGetDrawableInfo(null, out var hudComponents)) + { + var comboCounters = hudComponents.Where(h => h.Type.Name == nameof(ArgonComboCounter)).ToArray(); + + if (comboCounters.Any()) + { + hudLayout.Update(null, hudComponents.Except(comboCounters).ToArray()); + + resources.RealmAccess.Run(r => + { + foreach (var ruleset in r.All()) + hudLayout.Update(ruleset, comboCounters); + }); + } + } } public override Texture? GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => Textures?.Get(componentName, wrapModeS, wrapModeT); diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index b71b626b4e..c3e619431e 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -19,6 +19,7 @@ using osu.Game.Audio; using osu.Game.Beatmaps.Formats; using osu.Game.Extensions; using osu.Game.IO; +using osu.Game.Rulesets; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Scoring; using osu.Game.Screens.Play.HUD; @@ -56,6 +57,24 @@ namespace osu.Game.Skinning protected LegacySkin(SkinInfo skin, IStorageResourceProvider? resources, IResourceStore? fallbackStore, string configurationFilename = @"skin.ini") : base(skin, resources, fallbackStore, configurationFilename) { + if (resources != null + && skin.LayoutVersion < 20240625 + && LayoutInfos.TryGetValue(SkinComponentsContainerLookup.TargetArea.MainHUDComponents, out var hudLayout) + && hudLayout.TryGetDrawableInfo(null, out var hudComponents)) + { + var comboCounters = hudComponents.Where(h => h.Type.Name == nameof(LegacyComboCounter)).ToArray(); + + if (comboCounters.Any()) + { + hudLayout.Update(null, hudComponents.Except(comboCounters).ToArray()); + + resources.RealmAccess.Run(r => + { + foreach (var ruleset in r.All()) + hudLayout.Update(ruleset, comboCounters); + }); + } + } } protected override IResourceStore CreateTextureLoaderStore(IStorageResourceProvider resources, IResourceStore storage) diff --git a/osu.Game/Skinning/SkinImporter.cs b/osu.Game/Skinning/SkinImporter.cs index 59c7f0ba26..714427f40d 100644 --- a/osu.Game/Skinning/SkinImporter.cs +++ b/osu.Game/Skinning/SkinImporter.cs @@ -223,6 +223,8 @@ namespace osu.Game.Skinning } } + s.LayoutVersion = SkinInfo.LATEST_LAYOUT_VERSION; + string newHash = ComputeHash(s); hadChanges = newHash != s.Hash; diff --git a/osu.Game/Skinning/SkinInfo.cs b/osu.Game/Skinning/SkinInfo.cs index 9763d3b57e..a3d5771b5e 100644 --- a/osu.Game/Skinning/SkinInfo.cs +++ b/osu.Game/Skinning/SkinInfo.cs @@ -39,6 +39,21 @@ namespace osu.Game.Skinning public bool Protected { get; set; } + /// + /// The latest version in YYYYMMDD format for skin layout migrations. + /// + /// + /// + /// 20240625: Moves combo counters from ruleset-agnostic to ruleset-specific HUD targets. + /// + /// + public const int LATEST_LAYOUT_VERSION = 20240625; + + /// + /// A version in YYYYMMDD format for applying skin layout migrations. + /// + public int LayoutVersion { get; set; } + public virtual Skin CreateInstance(IStorageResourceProvider resources) { var type = string.IsNullOrEmpty(InstantiationInfo) From fc2202e0cc9cdf1191675272607aa7e51f2037e4 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 25 Jun 2024 05:54:56 +0300 Subject: [PATCH 1153/2493] Fix migration logic overwriting existing components in ruleset targets --- osu.Game/Skinning/ArgonSkin.cs | 6 +++++- osu.Game/Skinning/LegacySkin.cs | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/osu.Game/Skinning/ArgonSkin.cs b/osu.Game/Skinning/ArgonSkin.cs index 743ce38810..4cd54c06f0 100644 --- a/osu.Game/Skinning/ArgonSkin.cs +++ b/osu.Game/Skinning/ArgonSkin.cs @@ -84,7 +84,11 @@ namespace osu.Game.Skinning resources.RealmAccess.Run(r => { foreach (var ruleset in r.All()) - hudLayout.Update(ruleset, comboCounters); + { + hudLayout.Update(ruleset, hudLayout.TryGetDrawableInfo(ruleset, out var rulesetComponents) + ? rulesetComponents.Concat(comboCounters).ToArray() + : comboCounters); + } }); } } diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index c3e619431e..f148bad96e 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -71,7 +71,11 @@ namespace osu.Game.Skinning resources.RealmAccess.Run(r => { foreach (var ruleset in r.All()) - hudLayout.Update(ruleset, comboCounters); + { + hudLayout.Update(ruleset, hudLayout.TryGetDrawableInfo(ruleset, out var rulesetComponents) + ? rulesetComponents.Concat(comboCounters).ToArray() + : comboCounters); + } }); } } From dc1fb4fdca462a8d8123e695177aa61f951a78da Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 25 Jun 2024 05:54:59 +0300 Subject: [PATCH 1154/2493] Add test coverage --- .../Visual/Gameplay/TestSceneSkinEditor.cs | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs index 3c97700fb0..2470c320cc 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs @@ -13,12 +13,14 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.UserInterface; using osu.Framework.Input; using osu.Framework.Testing; +using osu.Game.Database; using osu.Game.Overlays; using osu.Game.Overlays.Settings; using osu.Game.Overlays.SkinEditor; using osu.Game.Rulesets; using osu.Game.Rulesets.Osu; using osu.Game.Screens.Edit; +using osu.Game.Screens.Play.HUD; using osu.Game.Screens.Play.HUD.HitErrorMeters; using osu.Game.Skinning; using osu.Game.Skinning.Components; @@ -39,6 +41,9 @@ namespace osu.Game.Tests.Visual.Gameplay [Cached] public readonly EditorClipboard Clipboard = new EditorClipboard(); + [Resolved] + private SkinManager skins { get; set; } = null!; + private SkinComponentsContainer targetContainer => Player.ChildrenOfType().First(); [SetUpSteps] @@ -46,6 +51,7 @@ namespace osu.Game.Tests.Visual.Gameplay { base.SetUpSteps(); + AddStep("reset skin", () => skins.CurrentSkinInfo.SetDefault()); AddUntilStep("wait for hud load", () => targetContainer.ComponentsLoaded); AddStep("reload skin editor", () => @@ -369,6 +375,84 @@ namespace osu.Game.Tests.Visual.Gameplay () => Is.EqualTo(3)); } + private SkinComponentsContainer globalHUDTarget => Player.ChildrenOfType() + .Single(c => c.Lookup.Target == SkinComponentsContainerLookup.TargetArea.MainHUDComponents && c.Lookup.Ruleset == null); + + private SkinComponentsContainer rulesetHUDTarget => Player.ChildrenOfType() + .Single(c => c.Lookup.Target == SkinComponentsContainerLookup.TargetArea.MainHUDComponents && c.Lookup.Ruleset != null); + + [Test] + public void TestMigrationArgon() + { + AddUntilStep("wait for load", () => globalHUDTarget.ComponentsLoaded); + AddStep("add combo to global hud target", () => + { + globalHUDTarget.Add(new ArgonComboCounter + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }); + }); + + Live modifiedSkin = null!; + + AddStep("select another skin", () => + { + modifiedSkin = skins.CurrentSkinInfo.Value; + skins.CurrentSkinInfo.SetDefault(); + }); + AddStep("modify version", () => modifiedSkin.PerformWrite(s => s.LayoutVersion = 0)); + AddStep("select skin again", () => skins.CurrentSkinInfo.Value = modifiedSkin); + AddAssert("global hud target does not contain combo", () => !globalHUDTarget.Components.Any(c => c is ArgonComboCounter)); + AddAssert("ruleset hud target contains both combos", () => + { + var target = rulesetHUDTarget; + + return target.Components.Count == 2 && + target.Components[0] is ArgonComboCounter one && one.Anchor == Anchor.BottomLeft && one.Origin == Anchor.BottomLeft && + target.Components[1] is ArgonComboCounter two && two.Anchor == Anchor.Centre && two.Origin == Anchor.Centre; + }); + AddStep("save skin", () => skinEditor.Save()); + AddAssert("version updated", () => modifiedSkin.PerformRead(s => s.LayoutVersion) == SkinInfo.LATEST_LAYOUT_VERSION); + } + + [Test] + public void TestMigrationLegacy() + { + AddStep("select legacy skin", () => skins.CurrentSkinInfo.Value = skins.DefaultClassicSkin.SkinInfo); + + AddUntilStep("wait for load", () => globalHUDTarget.ComponentsLoaded); + AddStep("add combo to global hud target", () => + { + globalHUDTarget.Add(new LegacyComboCounter + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }); + }); + + Live modifiedSkin = null!; + + AddStep("select another skin", () => + { + modifiedSkin = skins.CurrentSkinInfo.Value; + skins.CurrentSkinInfo.SetDefault(); + }); + AddStep("modify version", () => modifiedSkin.PerformWrite(s => s.LayoutVersion = 0)); + AddStep("select skin again", () => skins.CurrentSkinInfo.Value = modifiedSkin); + AddAssert("global hud target does not contain combo", () => !globalHUDTarget.Components.Any(c => c is LegacyComboCounter)); + AddAssert("ruleset hud target contains both combos", () => + { + var target = rulesetHUDTarget; + + return target.Components.Count == 2 && + target.Components[0] is LegacyComboCounter one && one.Anchor == Anchor.BottomLeft && one.Origin == Anchor.BottomLeft && + target.Components[1] is LegacyComboCounter two && two.Anchor == Anchor.Centre && two.Origin == Anchor.Centre; + }); + AddStep("save skin", () => skinEditor.Save()); + AddAssert("version updated", () => modifiedSkin.PerformRead(s => s.LayoutVersion) == SkinInfo.LATEST_LAYOUT_VERSION); + } + protected override Ruleset CreatePlayerRuleset() => new OsuRuleset(); private partial class TestSkinEditorChangeHandler : SkinEditorChangeHandler From 0a72394c8a037950365c0108a1180ee20e197412 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 25 Jun 2024 06:04:39 +0300 Subject: [PATCH 1155/2493] Fix customisation panel conflicting with beatmap attributes when collapsed --- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index 5ee52f32f5..8c3c81f2e1 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -372,6 +372,7 @@ namespace osu.Game.Overlays.Mods footerContentFlow.LayoutDuration = 200; footerContentFlow.LayoutEasing = Easing.OutQuint; footerContentFlow.Direction = screenIsntWideEnough ? FillDirection.Vertical : FillDirection.Horizontal; + aboveColumnsContent.Padding = aboveColumnsContent.Padding with { Bottom = screenIsntWideEnough ? 70f : 15f }; } } From 57ee794398d70455280d66628497af762077789b Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 25 Jun 2024 06:08:13 +0300 Subject: [PATCH 1156/2493] Add extra margin to avoid 1px artifacts --- osu.Game/Overlays/Mods/ModCustomisationPanel.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Mods/ModCustomisationPanel.cs b/osu.Game/Overlays/Mods/ModCustomisationPanel.cs index 3694fe2bde..7f3a176356 100644 --- a/osu.Game/Overlays/Mods/ModCustomisationPanel.cs +++ b/osu.Game/Overlays/Mods/ModCustomisationPanel.cs @@ -74,7 +74,9 @@ namespace osu.Game.Overlays.Mods { RelativeSizeAxes = Axes.X, ScrollbarOverlapsContent = false, - Margin = new MarginPadding { Top = header_height }, + // The +2f is a workaround for masking issues (see https://github.com/ppy/osu-framework/issues/1675#issuecomment-910023157) + // Note that this actually causes the full scroll range to be reduced by 2px at the bottom, but it's not really noticeable. + Margin = new MarginPadding { Top = header_height + 2f }, Child = sectionsFlow = new FillFlowContainer { RelativeSizeAxes = Axes.X, From 3f06a0ef9e89221bd71b3895463c90d9acd5297b Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 25 Jun 2024 06:15:02 +0300 Subject: [PATCH 1157/2493] Block certain operations when customisation panel is open Normally I would just block keyboard input from going past `ModCustomisationPanel`, but it's a little complicated here since I'm dealing with global action key binding presses, and I also still want actions like `GlobalAction.Back` to get past the customisation panel even if it's expanded. --- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index 8c3c81f2e1..56e0a88b5a 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -691,7 +691,7 @@ namespace osu.Game.Overlays.Mods // wherein activating the binding will both change the contents of the search text box and deselect all mods. case GlobalAction.DeselectAllMods: { - if (!SearchTextBox.HasFocus) + if (!SearchTextBox.HasFocus && !customisationPanel.Expanded.Value) { deselectAllModsButton.TriggerClick(); return true; @@ -762,6 +762,9 @@ namespace osu.Game.Overlays.Mods if (e.Repeat || e.Key != Key.Tab) return false; + if (customisationPanel.Expanded.Value) + return true; + // TODO: should probably eventually support typical platform search shortcuts (`Ctrl-F`, `/`) setTextBoxFocus(!SearchTextBox.HasFocus); return true; From 7ddb6cb7e7c342b45e1550d8669e05343e42b581 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 25 Jun 2024 06:56:39 +0300 Subject: [PATCH 1158/2493] Scroll mod setting dropdown into view when open --- .../Configuration/SettingSourceAttribute.cs | 35 +++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/osu.Game/Configuration/SettingSourceAttribute.cs b/osu.Game/Configuration/SettingSourceAttribute.cs index 1e425c88a6..f87c8de0cb 100644 --- a/osu.Game/Configuration/SettingSourceAttribute.cs +++ b/osu.Game/Configuration/SettingSourceAttribute.cs @@ -13,8 +13,10 @@ using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Extensions.TypeExtensions; using osu.Framework.Graphics; using osu.Framework.Localisation; +using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osu.Game.Overlays.Settings; +using osuTK; namespace osu.Game.Configuration { @@ -274,8 +276,37 @@ namespace osu.Game.Configuration private partial class ModDropdownControl : DropdownControl { - // Set menu's max height low enough to workaround nested scroll issues (see https://github.com/ppy/osu-framework/issues/4536). - protected override DropdownMenu CreateMenu() => base.CreateMenu().With(m => m.MaxHeight = 100); + protected override DropdownMenu CreateMenu() => new ModDropdownMenu(); + + private partial class ModDropdownMenu : OsuDropdownMenu + { + public ModDropdownMenu() + { + // Set menu's max height low enough to workaround nested scroll issues (see https://github.com/ppy/osu-framework/issues/4536). + MaxHeight = 100; + } + + protected override void UpdateSize(Vector2 newSize) + { + base.UpdateSize(newSize); + + // todo: probably move this to OsuDropdown so that settings overlay can benefit from this as well. + if (newSize.Y > 0) + { + var scroll = this.FindClosestParent(); + + if (scroll != null) + { + const float padding = 15; + + float target = scroll.GetChildPosInContent(this, new Vector2(0, newSize.Y + padding)); + + if (target > scroll.Current + scroll.DisplayableContent) + scroll.ScrollTo(target - scroll.DisplayableContent); + } + } + } + } } } } From a277d9df13ba0257dee47c3b40adaa72ceb2f931 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 25 Jun 2024 07:13:34 +0300 Subject: [PATCH 1159/2493] Kill focus from components when clicking on an empty space in the panel --- osu.Game/Overlays/Mods/ModCustomisationPanel.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Mods/ModCustomisationPanel.cs b/osu.Game/Overlays/Mods/ModCustomisationPanel.cs index 7f3a176356..ec3499bc57 100644 --- a/osu.Game/Overlays/Mods/ModCustomisationPanel.cs +++ b/osu.Game/Overlays/Mods/ModCustomisationPanel.cs @@ -49,7 +49,7 @@ namespace osu.Game.Overlays.Mods Height = header_height, Expanded = { BindTarget = Expanded }, }, - content = new InputBlockingContainer + content = new FocusGrabbingContainer { RelativeSizeAxes = Axes.X, BorderColour = colourProvider.Dark3, @@ -159,5 +159,11 @@ namespace osu.Game.Overlays.Mods base.Update(); scrollContainer.Height = Math.Min(scrollContainer.AvailableContent, DrawHeight - header_height); } + + private partial class FocusGrabbingContainer : InputBlockingContainer + { + public override bool RequestsFocus => true; + public override bool AcceptsFocus => true; + } } } From 2de42854c3d5686f8725a7310f5f387c7a879a05 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 25 Jun 2024 15:43:52 +0900 Subject: [PATCH 1160/2493] Fix corner radius looking bad when graph bars are too short --- .../OnlinePlay/DailyChallenge/DailyChallengeScoreBreakdown.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeScoreBreakdown.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeScoreBreakdown.cs index b5c44e42a5..3d4f27c44b 100644 --- a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeScoreBreakdown.cs +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeScoreBreakdown.cs @@ -174,7 +174,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge { base.Update(); - CircularBar.CornerRadius = CircularBar.DrawWidth / 4; + CircularBar.CornerRadius = Math.Min(CircularBar.DrawHeight / 2, CircularBar.DrawWidth / 4); } public void UpdateCounts(long newCount, long newMax) From b4cefe0cc2fda0ab4b5af6138ee158bd32262f9a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 25 Jun 2024 15:50:11 +0900 Subject: [PATCH 1161/2493] Update rider `vcsConfiguration` Updated in recent rider versions --- .idea/.idea.osu.Desktop/.idea/projectSettingsUpdater.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.idea/.idea.osu.Desktop/.idea/projectSettingsUpdater.xml b/.idea/.idea.osu.Desktop/.idea/projectSettingsUpdater.xml index 4bb9f4d2a0..86cc6c63e5 100644 --- a/.idea/.idea.osu.Desktop/.idea/projectSettingsUpdater.xml +++ b/.idea/.idea.osu.Desktop/.idea/projectSettingsUpdater.xml @@ -1,6 +1,6 @@ - \ No newline at end of file From 2cb18820ea0a2052bde8fcf911f8e06571703116 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 25 Jun 2024 10:07:58 +0200 Subject: [PATCH 1162/2493] Fix incorrect slider judgement positions when classic mod is active Regressed in https://github.com/ppy/osu/pull/27977. Bit ad-hoc but don't see how to fix without just reverting the change. --- .../Objects/Drawables/DrawableOsuJudgement.cs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs index 6e252a53e2..0630ecfbb5 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs @@ -48,10 +48,20 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables if (!positionTransferred && JudgedObject is DrawableOsuHitObject osuObject && JudgedObject.IsInUse) { - Position = osuObject.ToSpaceOfOtherDrawable(osuObject.OriginPosition, Parent!); - Scale = new Vector2(osuObject.HitObject.Scale); + switch (osuObject) + { + case DrawableSlider slider: + Position = slider.TailCircle.ToSpaceOfOtherDrawable(slider.TailCircle.OriginPosition, Parent!); + break; + + default: + Position = osuObject.ToSpaceOfOtherDrawable(osuObject.OriginPosition, Parent!); + break; + } positionTransferred = true; + + Scale = new Vector2(osuObject.HitObject.Scale); } } From 1eac0c622add0e5b0cf3bfda45dcbf86a90393ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 25 Jun 2024 10:27:25 +0200 Subject: [PATCH 1163/2493] Fix legacy skin hold note bodies not appearing when scrolling upwards - Closes https://github.com/ppy/osu/issues/28567. - Regressed in https://github.com/ppy/osu/pull/28466. Bit of a facepalm moment innit... --- osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyBodyPiece.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyBodyPiece.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyBodyPiece.cs index 087b428801..6de0752671 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyBodyPiece.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyBodyPiece.cs @@ -239,7 +239,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy // i dunno this looks about right?? // the guard against zero draw height is intended for zero-length hold notes. yes, such cases have been spotted in the wild. if (sprite.DrawHeight > 0) - bodySprite.Scale = new Vector2(1, MathF.Max(1, scaleDirection * 32800 / sprite.DrawHeight)); + bodySprite.Scale = new Vector2(1, scaleDirection * MathF.Max(1, 32800 / sprite.DrawHeight)); } break; From 0d2a47167c72d29b2fa9bb46ead50c2e8e968960 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 25 Jun 2024 11:28:10 +0200 Subject: [PATCH 1164/2493] Fix crash on calculating playlist duration when rate-changing mods are present Regressed in https://github.com/ppy/osu/pull/28399. To reproduce, enter a playlist that has an item with a rate-changing mod (rather than create it yourself). This is happening because `APIRuleset` has `CreateInstance()` unimplemented: https://github.com/ppy/osu/blob/b4cefe0cc2fda0ab4b5af6138ee158bd32262f9a/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs#L159 and only triggers when the playlist items in question originate from web. This is why it is bad to have interface implementations throw outside of maybe mock implementations for tests. `CreateInstance()` is a scourge elsewhere in general, we need way less of it in the codebase (because while convenient, it's also problematic to implement in online contexts, and also expensive because reflection). --- osu.Game/Online/Rooms/PlaylistExtensions.cs | 5 +++-- .../OnlinePlay/Components/OverlinedPlaylistHeader.cs | 7 ++++++- .../OnlinePlay/Playlists/PlaylistsRoomSettingsOverlay.cs | 6 +++++- 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/osu.Game/Online/Rooms/PlaylistExtensions.cs b/osu.Game/Online/Rooms/PlaylistExtensions.cs index e9a0519f3d..8591b5bb47 100644 --- a/osu.Game/Online/Rooms/PlaylistExtensions.cs +++ b/osu.Game/Online/Rooms/PlaylistExtensions.cs @@ -6,6 +6,7 @@ using System.Linq; using Humanizer; using Humanizer.Localisation; using osu.Framework.Bindables; +using osu.Game.Rulesets; using osu.Game.Utils; namespace osu.Game.Online.Rooms @@ -42,14 +43,14 @@ namespace osu.Game.Online.Rooms /// /// Returns the total duration from the in playlist order from the supplied , /// - public static string GetTotalDuration(this BindableList playlist) => + public static string GetTotalDuration(this BindableList playlist, RulesetStore rulesetStore) => playlist.Select(p => { double rate = 1; if (p.RequiredMods.Length > 0) { - var ruleset = p.Beatmap.Ruleset.CreateInstance(); + var ruleset = rulesetStore.GetRuleset(p.RulesetID)!.CreateInstance(); rate = ModUtils.CalculateRateWithMods(p.RequiredMods.Select(mod => mod.ToMod(ruleset))); } diff --git a/osu.Game/Screens/OnlinePlay/Components/OverlinedPlaylistHeader.cs b/osu.Game/Screens/OnlinePlay/Components/OverlinedPlaylistHeader.cs index fc86cbbbdd..dd728e460b 100644 --- a/osu.Game/Screens/OnlinePlay/Components/OverlinedPlaylistHeader.cs +++ b/osu.Game/Screens/OnlinePlay/Components/OverlinedPlaylistHeader.cs @@ -1,12 +1,17 @@ // 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.Game.Online.Rooms; +using osu.Game.Rulesets; namespace osu.Game.Screens.OnlinePlay.Components { public partial class OverlinedPlaylistHeader : OverlinedHeader { + [Resolved] + private RulesetStore rulesets { get; set; } = null!; + public OverlinedPlaylistHeader() : base("Playlist") { @@ -16,7 +21,7 @@ namespace osu.Game.Screens.OnlinePlay.Components { base.LoadComplete(); - Playlist.BindCollectionChanged((_, _) => Details.Value = Playlist.GetTotalDuration(), true); + Playlist.BindCollectionChanged((_, _) => Details.Value = Playlist.GetTotalDuration(rulesets), true); } } } diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSettingsOverlay.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSettingsOverlay.cs index 84e419d67a..9166cac9de 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSettingsOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSettingsOverlay.cs @@ -24,6 +24,7 @@ using osu.Game.Overlays; using osu.Game.Screens.OnlinePlay.Match.Components; using osuTK; using osu.Game.Localisation; +using osu.Game.Rulesets; namespace osu.Game.Screens.OnlinePlay.Playlists { @@ -78,6 +79,9 @@ namespace osu.Game.Screens.OnlinePlay.Playlists [Resolved] private IAPIProvider api { get; set; } = null!; + [Resolved] + private RulesetStore rulesets { get; set; } = null!; + private IBindable localUser = null!; private readonly Room room; @@ -366,7 +370,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists public void SelectBeatmap() => editPlaylistButton.TriggerClick(); private void onPlaylistChanged(object? sender, NotifyCollectionChangedEventArgs e) => - playlistLength.Text = $"Length: {Playlist.GetTotalDuration()}"; + playlistLength.Text = $"Length: {Playlist.GetTotalDuration(rulesets)}"; private bool hasValidSettings => RoomID.Value == null && NameField.Text.Length > 0 && Playlist.Count > 0 && hasValidDuration; From 2de08525514ceac86c242368450beca37bb4e198 Mon Sep 17 00:00:00 2001 From: PercyDan54 <50285552+PercyDan54@users.noreply.github.com> Date: Tue, 25 Jun 2024 18:06:50 +0800 Subject: [PATCH 1165/2493] Fix transient rank value applied to bindable --- osu.Game/Rulesets/Scoring/ScoreProcessor.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index 0b20f1089a..44ddb8c187 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -381,9 +381,12 @@ namespace osu.Game.Rulesets.Scoring if (rank.Value == ScoreRank.F) return; - rank.Value = RankFromScore(Accuracy.Value, ScoreResultCounts); + ScoreRank newRank = RankFromScore(Accuracy.Value, ScoreResultCounts); + foreach (var mod in Mods.Value.OfType()) - rank.Value = mod.AdjustRank(Rank.Value, Accuracy.Value); + newRank = mod.AdjustRank(newRank, Accuracy.Value); + + rank.Value = newRank; } protected virtual double ComputeTotalScore(double comboProgress, double accuracyProgress, double bonusPortion) From 03cdfd06602be30c6ff130b07154debf5f3625dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 25 Jun 2024 12:25:37 +0200 Subject: [PATCH 1166/2493] Fix timeline break piece crashing on drag if there are no objects before start or after end This fixes the direct cause of https://github.com/ppy/osu/issues/28577. --- .../Screens/Edit/Compose/Components/Timeline/TimelineBreak.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBreak.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBreak.cs index 608c2bdab1..025eb8bede 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBreak.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBreak.cs @@ -170,8 +170,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline changeHandler?.BeginChange(); updateState(); - double min = beatmap.HitObjects.Last(ho => ho.GetEndTime() <= Break.Value.StartTime).GetEndTime(); - double max = beatmap.HitObjects.First(ho => ho.StartTime >= Break.Value.EndTime).StartTime; + double min = beatmap.HitObjects.LastOrDefault(ho => ho.GetEndTime() <= Break.Value.StartTime)?.GetEndTime() ?? double.NegativeInfinity; + double max = beatmap.HitObjects.FirstOrDefault(ho => ho.StartTime >= Break.Value.EndTime)?.StartTime ?? double.PositiveInfinity; if (isStartHandle) max = Math.Min(max, Break.Value.EndTime - BreakPeriod.MIN_BREAK_DURATION); From 18e2a925a8130f1a48228c706a1ce863e0e6da30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 25 Jun 2024 12:34:37 +0200 Subject: [PATCH 1167/2493] Add failing test coverage for manual breaks at start/end of map not being culled --- .../TestSceneEditorBeatmapProcessor.cs | 104 ++++++++++++++++++ 1 file changed, 104 insertions(+) diff --git a/osu.Game.Tests/Editing/TestSceneEditorBeatmapProcessor.cs b/osu.Game.Tests/Editing/TestSceneEditorBeatmapProcessor.cs index 02ce3815ec..50f37e2070 100644 --- a/osu.Game.Tests/Editing/TestSceneEditorBeatmapProcessor.cs +++ b/osu.Game.Tests/Editing/TestSceneEditorBeatmapProcessor.cs @@ -296,5 +296,109 @@ namespace osu.Game.Tests.Editing Assert.That(beatmap.Breaks[0].EndTime, Is.EqualTo(8800)); }); } + + [Test] + public void TestBreaksAtEndOfBeatmapAreRemoved() + { + var controlPoints = new ControlPointInfo(); + controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 }); + var beatmap = new Beatmap + { + ControlPointInfo = controlPoints, + HitObjects = + { + new HitCircle { StartTime = 1000 }, + new HitCircle { StartTime = 2000 }, + }, + Breaks = + { + new BreakPeriod(10000, 15000), + } + }; + + var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new OsuRuleset()); + beatmapProcessor.PreProcess(); + beatmapProcessor.PostProcess(); + + Assert.That(beatmap.Breaks, Is.Empty); + } + + [Test] + public void TestManualBreaksAtEndOfBeatmapAreRemoved() + { + var controlPoints = new ControlPointInfo(); + controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 }); + var beatmap = new Beatmap + { + ControlPointInfo = controlPoints, + HitObjects = + { + new HitCircle { StartTime = 1000 }, + new HitCircle { StartTime = 2000 }, + }, + Breaks = + { + new ManualBreakPeriod(10000, 15000), + } + }; + + var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new OsuRuleset()); + beatmapProcessor.PreProcess(); + beatmapProcessor.PostProcess(); + + Assert.That(beatmap.Breaks, Is.Empty); + } + + [Test] + public void TestBreaksAtStartOfBeatmapAreRemoved() + { + var controlPoints = new ControlPointInfo(); + controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 }); + var beatmap = new Beatmap + { + ControlPointInfo = controlPoints, + HitObjects = + { + new HitCircle { StartTime = 10000 }, + new HitCircle { StartTime = 11000 }, + }, + Breaks = + { + new BreakPeriod(0, 9000), + } + }; + + var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new OsuRuleset()); + beatmapProcessor.PreProcess(); + beatmapProcessor.PostProcess(); + + Assert.That(beatmap.Breaks, Is.Empty); + } + + [Test] + public void TestManualBreaksAtStartOfBeatmapAreRemoved() + { + var controlPoints = new ControlPointInfo(); + controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 }); + var beatmap = new Beatmap + { + ControlPointInfo = controlPoints, + HitObjects = + { + new HitCircle { StartTime = 10000 }, + new HitCircle { StartTime = 11000 }, + }, + Breaks = + { + new ManualBreakPeriod(0, 9000), + } + }; + + var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new OsuRuleset()); + beatmapProcessor.PreProcess(); + beatmapProcessor.PostProcess(); + + Assert.That(beatmap.Breaks, Is.Empty); + } } } From fae6dcfffa5bac1e4dc23f62a212a57589c00e04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 25 Jun 2024 12:48:54 +0200 Subject: [PATCH 1168/2493] Remove manual breaks at the start/end of beatmap This is the secondary cause of https://github.com/ppy/osu/issues/28577, because you could do the following: - Have a break autogenerate itself - Adjust either end of it to make it mark itself as manually-adjusted - Remove all objects before or after said break to end up in a state wherein there are no objects before or after a break. The direct fix is still correct because it is still technically possible to end up in a state wherein a break is before or after all objects (obvious one is manual `.osu` editing), but this behaviour is also undesirable for the autogeneration logic. --- osu.Game/Screens/Edit/EditorBeatmapProcessor.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/EditorBeatmapProcessor.cs b/osu.Game/Screens/Edit/EditorBeatmapProcessor.cs index 37bf915cec..bcbee78280 100644 --- a/osu.Game/Screens/Edit/EditorBeatmapProcessor.cs +++ b/osu.Game/Screens/Edit/EditorBeatmapProcessor.cs @@ -40,8 +40,12 @@ namespace osu.Game.Screens.Edit foreach (var manualBreak in Beatmap.Breaks.ToList()) { - if (Beatmap.HitObjects.Any(ho => ho.StartTime <= manualBreak.EndTime && ho.GetEndTime() >= manualBreak.StartTime)) + if (manualBreak.EndTime <= Beatmap.HitObjects.FirstOrDefault()?.StartTime + || manualBreak.StartTime >= Beatmap.HitObjects.LastOrDefault()?.GetEndTime() + || Beatmap.HitObjects.Any(ho => ho.StartTime <= manualBreak.EndTime && ho.GetEndTime() >= manualBreak.StartTime)) + { Beatmap.Breaks.Remove(manualBreak); + } } for (int i = 1; i < Beatmap.HitObjects.Count; ++i) From 2fda45cad4f6632409a396368e472fcfc7b2e859 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 25 Jun 2024 15:01:43 +0200 Subject: [PATCH 1169/2493] Fix crashes when opening scale/rotation popovers during selection box operations --- .../Edit/OsuSelectionRotationHandler.cs | 12 ++++++++---- .../Edit/OsuSelectionScaleHandler.cs | 12 ++++++++---- osu.Game.Rulesets.Osu/Edit/TransformToolboxGroup.cs | 6 ++++-- .../Compose/Components/SelectionBoxRotationHandle.cs | 3 +++ .../Compose/Components/SelectionBoxScaleHandle.cs | 3 +++ .../Compose/Components/SelectionRotationHandler.cs | 7 +++++++ .../Edit/Compose/Components/SelectionScaleHandler.cs | 7 +++++++ 7 files changed, 40 insertions(+), 10 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionRotationHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionRotationHandler.cs index 7624b2f27e..62a39d3702 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionRotationHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionRotationHandler.cs @@ -53,9 +53,11 @@ namespace osu.Game.Rulesets.Osu.Edit public override void Begin() { - if (objectsInRotation != null) + if (OperationInProgress.Value) throw new InvalidOperationException($"Cannot {nameof(Begin)} a rotate operation while another is in progress!"); + base.Begin(); + changeHandler?.BeginChange(); objectsInRotation = selectedMovableObjects.ToArray(); @@ -68,10 +70,10 @@ namespace osu.Game.Rulesets.Osu.Edit public override void Update(float rotation, Vector2? origin = null) { - if (objectsInRotation == null) + if (!OperationInProgress.Value) throw new InvalidOperationException($"Cannot {nameof(Update)} a rotate operation without calling {nameof(Begin)} first!"); - Debug.Assert(originalPositions != null && originalPathControlPointPositions != null && defaultOrigin != null); + Debug.Assert(objectsInRotation != null && originalPositions != null && originalPathControlPointPositions != null && defaultOrigin != null); Vector2 actualOrigin = origin ?? defaultOrigin.Value; @@ -91,11 +93,13 @@ namespace osu.Game.Rulesets.Osu.Edit public override void Commit() { - if (objectsInRotation == null) + if (!OperationInProgress.Value) throw new InvalidOperationException($"Cannot {nameof(Commit)} a rotate operation without calling {nameof(Begin)} first!"); changeHandler?.EndChange(); + base.Commit(); + objectsInRotation = null; originalPositions = null; originalPathControlPointPositions = null; diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs index de00aa6ad3..f4fd48f183 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs @@ -72,9 +72,11 @@ namespace osu.Game.Rulesets.Osu.Edit public override void Begin() { - if (objectsInScale != null) + if (OperationInProgress.Value) throw new InvalidOperationException($"Cannot {nameof(Begin)} a scale operation while another is in progress!"); + base.Begin(); + changeHandler?.BeginChange(); objectsInScale = selectedMovableObjects.ToDictionary(ho => ho, ho => new OriginalHitObjectState(ho)); @@ -86,10 +88,10 @@ namespace osu.Game.Rulesets.Osu.Edit public override void Update(Vector2 scale, Vector2? origin = null, Axes adjustAxis = Axes.Both) { - if (objectsInScale == null) + if (!OperationInProgress.Value) throw new InvalidOperationException($"Cannot {nameof(Update)} a scale operation without calling {nameof(Begin)} first!"); - Debug.Assert(defaultOrigin != null && OriginalSurroundingQuad != null); + Debug.Assert(objectsInScale != null && defaultOrigin != null && OriginalSurroundingQuad != null); Vector2 actualOrigin = origin ?? defaultOrigin.Value; @@ -117,11 +119,13 @@ namespace osu.Game.Rulesets.Osu.Edit public override void Commit() { - if (objectsInScale == null) + if (!OperationInProgress.Value) throw new InvalidOperationException($"Cannot {nameof(Commit)} a rotate operation without calling {nameof(Begin)} first!"); changeHandler?.EndChange(); + base.Commit(); + objectsInScale = null; OriginalSurroundingQuad = null; defaultOrigin = null; diff --git a/osu.Game.Rulesets.Osu/Edit/TransformToolboxGroup.cs b/osu.Game.Rulesets.Osu/Edit/TransformToolboxGroup.cs index 28d0f8320f..a3547d45e5 100644 --- a/osu.Game.Rulesets.Osu/Edit/TransformToolboxGroup.cs +++ b/osu.Game.Rulesets.Osu/Edit/TransformToolboxGroup.cs @@ -77,13 +77,15 @@ namespace osu.Game.Rulesets.Osu.Edit { case GlobalAction.EditorToggleRotateControl: { - rotateButton.TriggerClick(); + if (!RotationHandler.OperationInProgress.Value || rotateButton.Selected.Value) + rotateButton.TriggerClick(); return true; } case GlobalAction.EditorToggleScaleControl: { - scaleButton.TriggerClick(); + if (!ScaleHandler.OperationInProgress.Value || scaleButton.Selected.Value) + scaleButton.TriggerClick(); return true; } } diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionBoxRotationHandle.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionBoxRotationHandle.cs index 5270162189..b9383f1bad 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionBoxRotationHandle.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionBoxRotationHandle.cs @@ -67,6 +67,9 @@ namespace osu.Game.Screens.Edit.Compose.Components if (rotationHandler == null) return false; + if (rotationHandler.OperationInProgress.Value) + return false; + rotationHandler.Begin(); return true; } diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionBoxScaleHandle.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionBoxScaleHandle.cs index eca0c08ba1..3f4f2c2854 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionBoxScaleHandle.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionBoxScaleHandle.cs @@ -32,6 +32,9 @@ namespace osu.Game.Screens.Edit.Compose.Components if (scaleHandler == null) return false; + if (scaleHandler.OperationInProgress.Value) + return false; + originalAnchor = Anchor; scaleHandler.Begin(); diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionRotationHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionRotationHandler.cs index 787716a38c..532daaf7fa 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionRotationHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionRotationHandler.cs @@ -12,6 +12,11 @@ namespace osu.Game.Screens.Edit.Compose.Components /// public partial class SelectionRotationHandler : Component { + /// + /// Whether there is any ongoing scale operation right now. + /// + public Bindable OperationInProgress { get; private set; } = new BindableBool(); + /// /// Whether rotation anchored by the selection origin can currently be performed. /// @@ -50,6 +55,7 @@ namespace osu.Game.Screens.Edit.Compose.Components /// public virtual void Begin() { + OperationInProgress.Value = true; } /// @@ -85,6 +91,7 @@ namespace osu.Game.Screens.Edit.Compose.Components /// public virtual void Commit() { + OperationInProgress.Value = false; } } } diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionScaleHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionScaleHandler.cs index a96f627e56..c91362219c 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionScaleHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionScaleHandler.cs @@ -13,6 +13,11 @@ namespace osu.Game.Screens.Edit.Compose.Components /// public partial class SelectionScaleHandler : Component { + /// + /// Whether there is any ongoing scale operation right now. + /// + public Bindable OperationInProgress { get; private set; } = new BindableBool(); + /// /// Whether horizontal scaling (from the left or right edge) support should be enabled. /// @@ -63,6 +68,7 @@ namespace osu.Game.Screens.Edit.Compose.Components /// public virtual void Begin() { + OperationInProgress.Value = true; } /// @@ -99,6 +105,7 @@ namespace osu.Game.Screens.Edit.Compose.Components /// public virtual void Commit() { + OperationInProgress.Value = false; } } } From 0a440226979db17af8b76a238371362be35099eb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 25 Jun 2024 23:03:37 +0900 Subject: [PATCH 1170/2493] 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 3c115d1371..0f1a14afd8 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -10,7 +10,7 @@ true - + diff --git a/osu.iOS.props b/osu.iOS.props index 449e4b0032..6ed60b00b3 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -23,6 +23,6 @@ iossimulator-x64 - + From d722be16e39683c59738f9c66976a9b2c92f83f3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 25 Jun 2024 23:41:43 +0900 Subject: [PATCH 1171/2493] Add missing `base` calls for safety --- osu.Game.Tests/Visual/Editing/TestSceneComposeSelectBox.cs | 4 ++++ osu.Game/Overlays/SkinEditor/SkinSelectionRotationHandler.cs | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneComposeSelectBox.cs b/osu.Game.Tests/Visual/Editing/TestSceneComposeSelectBox.cs index 79a808bbd2..28763051e3 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneComposeSelectBox.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneComposeSelectBox.cs @@ -84,6 +84,8 @@ namespace osu.Game.Tests.Visual.Editing targetContainer = getTargetContainer(); initialRotation = targetContainer!.Rotation; + + base.Begin(); } public override void Update(float rotation, Vector2? origin = null) @@ -102,6 +104,8 @@ namespace osu.Game.Tests.Visual.Editing targetContainer = null; initialRotation = null; + + base.Commit(); } } diff --git a/osu.Game/Overlays/SkinEditor/SkinSelectionRotationHandler.cs b/osu.Game/Overlays/SkinEditor/SkinSelectionRotationHandler.cs index 6a118a73a8..36b38543d1 100644 --- a/osu.Game/Overlays/SkinEditor/SkinSelectionRotationHandler.cs +++ b/osu.Game/Overlays/SkinEditor/SkinSelectionRotationHandler.cs @@ -61,6 +61,8 @@ namespace osu.Game.Overlays.SkinEditor 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; + + base.Begin(); } public override void Update(float rotation, Vector2? origin = null) @@ -99,6 +101,8 @@ namespace osu.Game.Overlays.SkinEditor originalPositions = null; originalRotations = null; defaultOrigin = null; + + base.Commit(); } } } From dc817b62ccd63ed6477d8f2082d980769e5f359b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 26 Jun 2024 00:49:56 +0900 Subject: [PATCH 1172/2493] Fix editor performance dropping over time when hit markers are enabled There's probably a better solution but let's hotfix this for now. --- osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs | 3 ++- osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs | 2 +- osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs index 6f419073eb..7d707dea6c 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs @@ -325,13 +325,14 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables internal void SuppressHitAnimations() { - UpdateState(ArmedState.Idle); + UpdateState(ArmedState.Idle, true); UpdateComboColour(); // This method is called every frame. If we need to, the following can likely be converted // to code which doesn't use transforms at all. // Matches stable (see https://github.com/peppy/osu-stable-reference/blob/bb57924c1552adbed11ee3d96cdcde47cf96f2b6/osu!/GameplayElements/HitObjects/Osu/HitCircleOsu.cs#L336-L338) + using (BeginAbsoluteSequence(StateUpdateTime - 5)) this.TransformBindableTo(AccentColour, Color4.White, Math.Max(0, HitStateUpdateTime - StateUpdateTime)); diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs index 7bae3cefcf..02d0ebee83 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs @@ -375,7 +375,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables internal void SuppressHitAnimations() { - UpdateState(ArmedState.Idle); + UpdateState(ArmedState.Idle, true); HeadCircle.SuppressHitAnimations(); TailCircle.SuppressHitAnimations(); } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs index 21aa672d10..42abf41d6f 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs @@ -132,7 +132,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables internal void SuppressHitAnimations() { - UpdateState(ArmedState.Idle); + UpdateState(ArmedState.Idle, true); UpdateComboColour(); using (BeginAbsoluteSequence(StateUpdateTime - 5)) From df64d7f37458515dabde77792aeda9090e9f103e Mon Sep 17 00:00:00 2001 From: StanR Date: Tue, 25 Jun 2024 23:06:42 +0500 Subject: [PATCH 1173/2493] Refactor out taiko Peaks skill --- .../Difficulty/Skills/Peaks.cs | 93 ------------------- .../Difficulty/TaikoDifficultyCalculator.cs | 70 ++++++++++++-- 2 files changed, 64 insertions(+), 99 deletions(-) delete mode 100644 osu.Game.Rulesets.Taiko/Difficulty/Skills/Peaks.cs diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Peaks.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Peaks.cs deleted file mode 100644 index 91d8e93543..0000000000 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Peaks.cs +++ /dev/null @@ -1,93 +0,0 @@ -// 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.Rulesets.Difficulty.Preprocessing; -using osu.Game.Rulesets.Difficulty.Skills; -using osu.Game.Rulesets.Mods; - -namespace osu.Game.Rulesets.Taiko.Difficulty.Skills -{ - public class Peaks : Skill - { - private const double rhythm_skill_multiplier = 0.2 * final_multiplier; - private const double colour_skill_multiplier = 0.375 * final_multiplier; - private const double stamina_skill_multiplier = 0.375 * final_multiplier; - - private const double final_multiplier = 0.0625; - - private readonly Rhythm rhythm; - private readonly Colour colour; - private readonly Stamina stamina; - - public double ColourDifficultyValue => colour.DifficultyValue() * colour_skill_multiplier; - public double RhythmDifficultyValue => rhythm.DifficultyValue() * rhythm_skill_multiplier; - public double StaminaDifficultyValue => stamina.DifficultyValue() * stamina_skill_multiplier; - - public Peaks(Mod[] mods) - : base(mods) - { - rhythm = new Rhythm(mods); - colour = new Colour(mods); - stamina = new Stamina(mods); - } - - /// - /// Returns the p-norm of an n-dimensional vector. - /// - /// The value of p to calculate the norm for. - /// The coefficients of the vector. - private double norm(double p, params double[] values) => Math.Pow(values.Sum(x => Math.Pow(x, p)), 1 / p); - - public override void Process(DifficultyHitObject current) - { - rhythm.Process(current); - colour.Process(current); - stamina.Process(current); - } - - /// - /// Returns the combined star rating of the beatmap, calculated using peak strains from all sections of the map. - /// - /// - /// For each section, the peak strains of all separate skills are combined into a single peak strain for the section. - /// The resulting partial rating of the beatmap is a weighted sum of the combined peaks (higher peaks are weighted more). - /// - public override double DifficultyValue() - { - List peaks = new List(); - - var colourPeaks = colour.GetCurrentStrainPeaks().ToList(); - var rhythmPeaks = rhythm.GetCurrentStrainPeaks().ToList(); - var staminaPeaks = stamina.GetCurrentStrainPeaks().ToList(); - - for (int i = 0; i < colourPeaks.Count; i++) - { - double colourPeak = colourPeaks[i] * colour_skill_multiplier; - double rhythmPeak = rhythmPeaks[i] * rhythm_skill_multiplier; - double staminaPeak = staminaPeaks[i] * stamina_skill_multiplier; - - double peak = norm(1.5, colourPeak, staminaPeak); - peak = norm(2, peak, rhythmPeak); - - // Sections with 0 strain are excluded to avoid worst-case time complexity of the following sort (e.g. /b/2351871). - // These sections will not contribute to the difficulty. - if (peak > 0) - peaks.Add(peak); - } - - double difficulty = 0; - double weight = 1; - - foreach (double strain in peaks.OrderDescending()) - { - difficulty += strain * weight; - weight *= 0.9; - } - - return difficulty; - } - } -} diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs index b84c2d25ee..9b746d47ea 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs @@ -23,6 +23,11 @@ namespace osu.Game.Rulesets.Taiko.Difficulty { private const double difficulty_multiplier = 1.35; + private const double final_multiplier = 0.0625; + private const double rhythm_skill_multiplier = 0.2 * final_multiplier; + private const double colour_skill_multiplier = 0.375 * final_multiplier; + private const double stamina_skill_multiplier = 0.375 * final_multiplier; + public override int Version => 20221107; public TaikoDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap) @@ -34,7 +39,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty { return new Skill[] { - new Peaks(mods) + new Rhythm(mods), + new Colour(mods), + new Stamina(mods) }; } @@ -72,13 +79,15 @@ namespace osu.Game.Rulesets.Taiko.Difficulty if (beatmap.HitObjects.Count == 0) return new TaikoDifficultyAttributes { Mods = mods }; - var combined = (Peaks)skills[0]; + Colour colour = (Colour)skills.First(x => x is Colour); + Rhythm rhythm = (Rhythm)skills.First(x => x is Rhythm); + Stamina stamina = (Stamina)skills.First(x => x is Stamina); - double colourRating = combined.ColourDifficultyValue * difficulty_multiplier; - double rhythmRating = combined.RhythmDifficultyValue * difficulty_multiplier; - double staminaRating = combined.StaminaDifficultyValue * difficulty_multiplier; + double colourRating = colour.DifficultyValue() * colour_skill_multiplier * difficulty_multiplier; + double rhythmRating = rhythm.DifficultyValue() * rhythm_skill_multiplier * difficulty_multiplier; + double staminaRating = stamina.DifficultyValue() * stamina_skill_multiplier * difficulty_multiplier; - double combinedRating = combined.DifficultyValue() * difficulty_multiplier; + double combinedRating = combinedDifficultyValue(rhythm, colour, stamina) * difficulty_multiplier; double starRating = rescale(combinedRating * 1.4); HitWindows hitWindows = new TaikoHitWindows(); @@ -109,5 +118,54 @@ namespace osu.Game.Rulesets.Taiko.Difficulty return 10.43 * Math.Log(sr / 8 + 1); } + + /// + /// Returns the combined star rating of the beatmap, calculated using peak strains from all sections of the map. + /// + /// + /// For each section, the peak strains of all separate skills are combined into a single peak strain for the section. + /// The resulting partial rating of the beatmap is a weighted sum of the combined peaks (higher peaks are weighted more). + /// + private double combinedDifficultyValue(Rhythm rhythm, Colour colour, Stamina stamina) + { + List peaks = new List(); + + var colourPeaks = colour.GetCurrentStrainPeaks().ToList(); + var rhythmPeaks = rhythm.GetCurrentStrainPeaks().ToList(); + var staminaPeaks = stamina.GetCurrentStrainPeaks().ToList(); + + for (int i = 0; i < colourPeaks.Count; i++) + { + double colourPeak = colourPeaks[i] * colour_skill_multiplier; + double rhythmPeak = rhythmPeaks[i] * rhythm_skill_multiplier; + double staminaPeak = staminaPeaks[i] * stamina_skill_multiplier; + + double peak = norm(1.5, colourPeak, staminaPeak); + peak = norm(2, peak, rhythmPeak); + + // Sections with 0 strain are excluded to avoid worst-case time complexity of the following sort (e.g. /b/2351871). + // These sections will not contribute to the difficulty. + if (peak > 0) + peaks.Add(peak); + } + + double difficulty = 0; + double weight = 1; + + foreach (double strain in peaks.OrderDescending()) + { + difficulty += strain * weight; + weight *= 0.9; + } + + return difficulty; + } + + /// + /// Returns the p-norm of an n-dimensional vector. + /// + /// The value of p to calculate the norm for. + /// The coefficients of the vector. + private double norm(double p, params double[] values) => Math.Pow(values.Sum(x => Math.Pow(x, p)), 1 / p); } } From 5d4509150bf5b107f36c1123f9eaae81d4bc864f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 26 Jun 2024 11:56:58 +0900 Subject: [PATCH 1174/2493] Adjust beatmap carousel's spacing to remove dead-space As discussed in https://github.com/ppy/osu/discussions/28599. I think this feels better overall, and would like to apply the change before other design changes to the carousel. --- osu.Game/Screens/Select/BeatmapCarousel.cs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 49c23bdbbf..3aa980cec0 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -1000,8 +1000,6 @@ namespace osu.Game.Screens.Select return set; } - private const float panel_padding = 5; - /// /// Computes the target Y positions for every item in the carousel. /// @@ -1023,10 +1021,18 @@ namespace osu.Game.Screens.Select { case CarouselBeatmapSet set: { + bool isSelected = item.State.Value == CarouselItemState.Selected; + + float padding = isSelected ? 5 : -5; + + if (isSelected) + // double padding because we want to cancel the negative padding from the last item. + currentY += padding * 2; + visibleItems.Add(set); set.CarouselYPosition = currentY; - if (item.State.Value == CarouselItemState.Selected) + if (isSelected) { // scroll position at currentY makes the set panel appear at the very top of the carousel's screen space // move down by half of visible height (height of the carousel's visible extent, including semi-transparent areas) @@ -1048,7 +1054,7 @@ namespace osu.Game.Screens.Select } } - currentY += set.TotalHeight + panel_padding; + currentY += set.TotalHeight + padding; break; } } From e84daedbea9a5dadc6e47eb3a136e63333619eae Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 26 Jun 2024 12:01:38 +0900 Subject: [PATCH 1175/2493] Reduce length of fade-out when hiding beatmap panels --- osu.Game/Screens/Select/Carousel/DrawableCarouselItem.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselItem.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselItem.cs index 0c3de5848b..4c9ac57d9d 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselItem.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselItem.cs @@ -144,9 +144,9 @@ namespace osu.Game.Screens.Select.Carousel } if (!Item.Visible) - this.FadeOut(300, Easing.OutQuint); + this.FadeOut(100, Easing.OutQuint); else - this.FadeIn(250); + this.FadeIn(400, Easing.OutQuint); } protected virtual void Selected() From 0379abd714782a9e3d01cae2980859bd0b88541e Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 26 Jun 2024 13:56:52 +0900 Subject: [PATCH 1176/2493] Prevent multiple invocations of failure procedure --- osu.Game/Rulesets/Scoring/HealthProcessor.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Rulesets/Scoring/HealthProcessor.cs b/osu.Game/Rulesets/Scoring/HealthProcessor.cs index 9e4c06b783..2799cd4b36 100644 --- a/osu.Game/Rulesets/Scoring/HealthProcessor.cs +++ b/osu.Game/Rulesets/Scoring/HealthProcessor.cs @@ -36,6 +36,9 @@ namespace osu.Game.Rulesets.Scoring /// public void TriggerFailure() { + if (HasFailed) + return; + if (Failed?.Invoke() != false) HasFailed = true; } From fa46d8e6c91d721a88be7282a40edb8d35e24dff Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 26 Jun 2024 08:39:55 +0300 Subject: [PATCH 1177/2493] Fix intermittent failure in online menu banner tests --- osu.Game.Tests/Visual/Menus/TestSceneMainMenu.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game.Tests/Visual/Menus/TestSceneMainMenu.cs b/osu.Game.Tests/Visual/Menus/TestSceneMainMenu.cs index 240421b360..6b27de9996 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneMainMenu.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneMainMenu.cs @@ -3,6 +3,8 @@ using System.Linq; using NUnit.Framework; +using NUnit.Framework.Internal; +using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Testing; using osu.Game.Online.API.Requests.Responses; @@ -20,6 +22,7 @@ namespace osu.Game.Tests.Visual.Menus { base.SetUpSteps(); AddStep("don't fetch online content", () => onlineMenuBanner.FetchOnlineContent = false); + AddStep("disable return to top on idle", () => Game.ChildrenOfType().Single().ReturnToTopOnIdle = false); } [Test] From 2e03afb2ed4c1a779ef12f74dbecd32896dd310b Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 26 Jun 2024 14:47:58 +0900 Subject: [PATCH 1178/2493] Always log missing official build attribute --- osu.Game/OsuGame.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 667c3ecb99..cf32daab00 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -8,6 +8,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; +using System.Reflection; using System.Threading; using System.Threading.Tasks; using Humanizer; @@ -871,6 +872,9 @@ namespace osu.Game { base.LoadComplete(); + if (RuntimeInfo.EntryAssembly.GetCustomAttribute() == null) + Logger.Log(NotificationsStrings.NotOfficialBuild.ToString()); + var languages = Enum.GetValues(); var mappings = languages.Select(language => From 0c8279c5df28a9c816849c4bc3c226a6ba6bd088 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 26 Jun 2024 14:50:53 +0900 Subject: [PATCH 1179/2493] Remove unused using statements --- osu.Game.Tests/Visual/Menus/TestSceneMainMenu.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Menus/TestSceneMainMenu.cs b/osu.Game.Tests/Visual/Menus/TestSceneMainMenu.cs index 6b27de9996..57cff38ab0 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneMainMenu.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneMainMenu.cs @@ -3,8 +3,6 @@ using System.Linq; using NUnit.Framework; -using NUnit.Framework.Internal; -using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Testing; using osu.Game.Online.API.Requests.Responses; From 006184ed2f220e37dca2c0d889e4215a5759a4d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 4 Jun 2024 13:16:30 +0200 Subject: [PATCH 1180/2493] Implement carousel container for daily challenge screen --- .../TestSceneDailyChallengeCarousel.cs | 176 +++++++++++++ .../DailyChallenge/DailyChallengeCarousel.cs | 234 ++++++++++++++++++ 2 files changed, 410 insertions(+) create mode 100644 osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeCarousel.cs create mode 100644 osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeCarousel.cs diff --git a/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeCarousel.cs b/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeCarousel.cs new file mode 100644 index 0000000000..640a895751 --- /dev/null +++ b/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeCarousel.cs @@ -0,0 +1,176 @@ +// 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 NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Extensions.ObjectExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Utils; +using osu.Game.Graphics.Sprites; +using osu.Game.Online.Rooms; +using osu.Game.Overlays; +using osu.Game.Screens.OnlinePlay.DailyChallenge; +using osu.Game.Tests.Resources; + +namespace osu.Game.Tests.Visual.DailyChallenge +{ + public partial class TestSceneDailyChallengeCarousel : OsuTestScene + { + [Cached] + private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Plum); + + private readonly Bindable room = new Bindable(new Room()); + + protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) => new CachedModelDependencyContainer(base.CreateChildDependencies(parent)) + { + Model = { BindTarget = room } + }; + + [Test] + public void TestBasicAppearance() + { + DailyChallengeCarousel carousel = null!; + + AddStep("create content", () => Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background4, + }, + carousel = new DailyChallengeCarousel + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + } + }); + AddSliderStep("adjust width", 0.1f, 1, 1, width => + { + if (carousel.IsNotNull()) + carousel.Width = width; + }); + AddSliderStep("adjust height", 0.1f, 1, 1, height => + { + if (carousel.IsNotNull()) + carousel.Height = height; + }); + AddRepeatStep("add content", () => carousel.Add(new FakeContent()), 3); + } + + [Test] + public void TestIntegration() + { + GridContainer grid = null!; + DailyChallengeCarousel carousel = null!; + DailyChallengeEventFeed feed = null!; + DailyChallengeScoreBreakdown breakdown = null!; + AddStep("create content", () => Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background4, + }, + grid = new GridContainer + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RowDimensions = + [ + new Dimension(), + new Dimension() + ], + Content = new[] + { + new Drawable[] + { + carousel = new DailyChallengeCarousel + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Children = new Drawable[] + { + new DailyChallengeTimeRemainingRing(), + breakdown = new DailyChallengeScoreBreakdown(), + } + } + }, + [ + feed = new DailyChallengeEventFeed + { + RelativeSizeAxes = Axes.Both, + } + ], + } + }, + }); + AddSliderStep("adjust width", 0.1f, 1, 1, width => + { + if (grid.IsNotNull()) + grid.Width = width; + }); + AddSliderStep("adjust height", 0.1f, 1, 1, height => + { + if (grid.IsNotNull()) + grid.Height = height; + }); + AddSliderStep("update time remaining", 0f, 1f, 0f, progress => + { + var startedTimeAgo = TimeSpan.FromHours(24) * progress; + room.Value.StartDate.Value = DateTimeOffset.Now - startedTimeAgo; + room.Value.EndDate.Value = room.Value.StartDate.Value.Value.AddDays(1); + }); + AddStep("add normal score", () => + { + var testScore = TestResources.CreateTestScoreInfo(); + testScore.TotalScore = RNG.Next(1_000_000); + + feed.AddNewScore(new DailyChallengeEventFeed.NewScoreEvent(testScore, null)); + breakdown.AddNewScore(testScore); + }); + AddStep("add new user best", () => + { + var testScore = TestResources.CreateTestScoreInfo(); + testScore.TotalScore = RNG.Next(1_000_000); + + feed.AddNewScore(new DailyChallengeEventFeed.NewScoreEvent(testScore, RNG.Next(1, 1000))); + breakdown.AddNewScore(testScore); + }); + } + + private partial class FakeContent : CompositeDrawable + { + private OsuSpriteText text = null!; + + [BackgroundDependencyLoader] + private void load() + { + InternalChildren = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = new Colour4(RNG.NextSingle(), RNG.NextSingle(), RNG.NextSingle(), 1), + }, + text = new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Text = "Fake Content " + (char)('A' + RNG.Next(26)), + }, + }; + + text.FadeOut(500, Easing.OutQuint) + .Then().FadeIn(500, Easing.OutQuint) + .Loop(); + } + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeCarousel.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeCarousel.cs new file mode 100644 index 0000000000..2fd5253347 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeCarousel.cs @@ -0,0 +1,234 @@ +// 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; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Input.Events; +using osu.Game.Graphics.UserInterface; +using osu.Game.Overlays; +using osuTK; + +namespace osu.Game.Screens.OnlinePlay.DailyChallenge +{ + public partial class DailyChallengeCarousel : Container + { + private const int switch_interval = 20_500; + + private readonly Container content; + private readonly FillFlowContainer navigationFlow; + + protected override Container Content => content; + + private double clockStartTime; + private int lastDisplayed = -1; + + public DailyChallengeCarousel() + { + InternalChildren = new Drawable[] + { + content = new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Bottom = 40 }, + }, + navigationFlow = new FillFlowContainer + { + AutoSizeAxes = Axes.X, + Height = 15, + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + Spacing = new Vector2(10), + } + }; + } + + public override void Add(Drawable drawable) + { + drawable.RelativeSizeAxes = Axes.Both; + drawable.Size = Vector2.One; + drawable.AlwaysPresent = true; + drawable.Alpha = 0; + + base.Add(drawable); + + navigationFlow.Add(new NavigationDot { Clicked = onManualNavigation }); + } + + public override bool Remove(Drawable drawable, bool disposeImmediately) + { + int index = content.IndexOf(drawable); + + if (index > 0) + navigationFlow.Remove(navigationFlow[index], true); + + return base.Remove(drawable, disposeImmediately); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + clockStartTime = Clock.CurrentTime; + } + + protected override void Update() + { + base.Update(); + + if (content.Count == 0) + { + lastDisplayed = -1; + return; + } + + double elapsed = Clock.CurrentTime - clockStartTime; + + int currentDisplay = (int)(elapsed / switch_interval) % content.Count; + double displayProgress = (elapsed % switch_interval) / switch_interval; + + navigationFlow[currentDisplay].Active.Value = true; + + if (content.Count > 1) + navigationFlow[currentDisplay].Progress = (float)displayProgress; + + if (currentDisplay == lastDisplayed) + return; + + if (lastDisplayed >= 0) + { + content[lastDisplayed].FadeOutFromOne(250, Easing.OutQuint); + navigationFlow[lastDisplayed].Active.Value = false; + } + + content[currentDisplay].Delay(250).Then().FadeInFromZero(250, Easing.OutQuint); + + lastDisplayed = currentDisplay; + } + + private void onManualNavigation(NavigationDot obj) + { + int index = navigationFlow.IndexOf(obj); + + if (index < 0) + return; + + clockStartTime = Clock.CurrentTime - index * switch_interval; + } + + private partial class NavigationDot : CompositeDrawable + { + public Action? Clicked { get; set; } + + public BindableBool Active { get; set; } = new BindableBool(); + + private double progress; + + public float Progress + { + set + { + if (progress == value) + return; + + progress = value; + progressLayer.Width = value; + } + } + + [Resolved] + private OverlayColourProvider colourProvider { get; set; } = null!; + + private Box background = null!; + private Box progressLayer = null!; + private Box hoverLayer = null!; + + [BackgroundDependencyLoader] + private void load() + { + Size = new Vector2(15); + + InternalChildren = new Drawable[] + { + new CircularContainer + { + RelativeSizeAxes = Axes.Both, + Masking = true, + Children = new Drawable[] + { + background = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Light4, + }, + progressLayer = new Box + { + RelativeSizeAxes = Axes.Both, + Width = 0, + Colour = colourProvider.Highlight1, + Blending = BlendingParameters.Additive, + Alpha = 0, + }, + hoverLayer = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Colour4.White, + Blending = BlendingParameters.Additive, + Alpha = 0, + } + } + }, + new HoverClickSounds() + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + Active.BindValueChanged(val => + { + if (val.NewValue) + { + background.FadeColour(colourProvider.Highlight1, 250, Easing.OutQuint); + this.ResizeWidthTo(30, 250, Easing.OutQuint); + progressLayer.Width = 0; + progressLayer.Alpha = 0.5f; + } + else + { + background.FadeColour(colourProvider.Light4, 250, Easing.OutQuint); + this.ResizeWidthTo(15, 250, Easing.OutQuint); + progressLayer.FadeOut(250, Easing.OutQuint); + } + }, true); + } + + protected override bool OnHover(HoverEvent e) + { + base.OnHover(e); + hoverLayer.FadeTo(0.2f, 250, Easing.OutQuint); + return true; + } + + protected override void OnHoverLost(HoverLostEvent e) + { + hoverLayer.FadeOut(250, Easing.OutQuint); + base.OnHoverLost(e); + } + + protected override bool OnClick(ClickEvent e) + { + Clicked?.Invoke(this); + + hoverLayer.FadeTo(1) + .Then().FadeTo(IsHovered ? 0.2f : 0, 250, Easing.OutQuint); + + return true; + } + } + } +} From 276d8fe1582ca00e774c12be32b2abf47bd4ac8b Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 26 Jun 2024 16:20:54 +0900 Subject: [PATCH 1181/2493] Truncate break times for legacy beatmap export --- osu.Game/Database/LegacyBeatmapExporter.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/osu.Game/Database/LegacyBeatmapExporter.cs b/osu.Game/Database/LegacyBeatmapExporter.cs index 69120ea885..17c2c8c88d 100644 --- a/osu.Game/Database/LegacyBeatmapExporter.cs +++ b/osu.Game/Database/LegacyBeatmapExporter.cs @@ -8,6 +8,7 @@ using System.Text; using osu.Framework.Platform; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Formats; +using osu.Game.Beatmaps.Timing; using osu.Game.IO; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; @@ -59,9 +60,13 @@ namespace osu.Game.Database // 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 playableBeatmap.ControlPointInfo.AllControlPoints) controlPoint.Time = Math.Floor(controlPoint.Time); + for (int i = 0; i < playableBeatmap.Breaks.Count; i++) + playableBeatmap.Breaks[i] = new BreakPeriod(Math.Floor(playableBeatmap.Breaks[i].StartTime), Math.Floor(playableBeatmap.Breaks[i].EndTime)); + foreach (var hitObject in playableBeatmap.HitObjects) { // Truncate end time before truncating start time because end time is dependent on start time From ac235cb5068f5577d0feb1fc2ca39b223cc746ec Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 26 Jun 2024 16:22:25 +0900 Subject: [PATCH 1182/2493] Remove unused local variable --- .../Visual/DailyChallenge/TestSceneDailyChallengeCarousel.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeCarousel.cs b/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeCarousel.cs index 640a895751..b9143945c4 100644 --- a/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeCarousel.cs +++ b/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeCarousel.cs @@ -66,9 +66,9 @@ namespace osu.Game.Tests.Visual.DailyChallenge public void TestIntegration() { GridContainer grid = null!; - DailyChallengeCarousel carousel = null!; DailyChallengeEventFeed feed = null!; DailyChallengeScoreBreakdown breakdown = null!; + AddStep("create content", () => Children = new Drawable[] { new Box @@ -90,7 +90,7 @@ namespace osu.Game.Tests.Visual.DailyChallenge { new Drawable[] { - carousel = new DailyChallengeCarousel + new DailyChallengeCarousel { RelativeSizeAxes = Axes.Both, Anchor = Anchor.Centre, From 015a71f3d531f5d45cf81a286aec4976400fab61 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 26 Jun 2024 16:49:22 +0900 Subject: [PATCH 1183/2493] Add projectSettingsUpdater.xml to .gitignore and remove from tracking --- .gitignore | 1 + .idea/.idea.osu.Android/.idea/projectSettingsUpdater.xml | 6 ------ .idea/.idea.osu.Desktop/.idea/projectSettingsUpdater.xml | 6 ------ .idea/.idea.osu.iOS/.idea/projectSettingsUpdater.xml | 6 ------ .idea/.idea.osu/.idea/projectSettingsUpdater.xml | 6 ------ 5 files changed, 1 insertion(+), 24 deletions(-) delete mode 100644 .idea/.idea.osu.Android/.idea/projectSettingsUpdater.xml delete mode 100644 .idea/.idea.osu.Desktop/.idea/projectSettingsUpdater.xml delete mode 100644 .idea/.idea.osu.iOS/.idea/projectSettingsUpdater.xml delete mode 100644 .idea/.idea.osu/.idea/projectSettingsUpdater.xml diff --git a/.gitignore b/.gitignore index 11fee27f28..a51ad09d6c 100644 --- a/.gitignore +++ b/.gitignore @@ -265,6 +265,7 @@ __pycache__/ .idea/**/usage.statistics.xml .idea/**/dictionaries .idea/**/shelf +.idea/*/.idea/projectSettingsUpdater.xml # Generated files .idea/**/contentModel.xml diff --git a/.idea/.idea.osu.Android/.idea/projectSettingsUpdater.xml b/.idea/.idea.osu.Android/.idea/projectSettingsUpdater.xml deleted file mode 100644 index 4bb9f4d2a0..0000000000 --- a/.idea/.idea.osu.Android/.idea/projectSettingsUpdater.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - \ No newline at end of file diff --git a/.idea/.idea.osu.Desktop/.idea/projectSettingsUpdater.xml b/.idea/.idea.osu.Desktop/.idea/projectSettingsUpdater.xml deleted file mode 100644 index 86cc6c63e5..0000000000 --- a/.idea/.idea.osu.Desktop/.idea/projectSettingsUpdater.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - \ No newline at end of file diff --git a/.idea/.idea.osu.iOS/.idea/projectSettingsUpdater.xml b/.idea/.idea.osu.iOS/.idea/projectSettingsUpdater.xml deleted file mode 100644 index 4bb9f4d2a0..0000000000 --- a/.idea/.idea.osu.iOS/.idea/projectSettingsUpdater.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - \ No newline at end of file diff --git a/.idea/.idea.osu/.idea/projectSettingsUpdater.xml b/.idea/.idea.osu/.idea/projectSettingsUpdater.xml deleted file mode 100644 index 4bb9f4d2a0..0000000000 --- a/.idea/.idea.osu/.idea/projectSettingsUpdater.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - \ No newline at end of file From 5c2d4467670c21a6e5b8c10b504bc9c3420366cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 21 Jun 2024 10:54:40 +0200 Subject: [PATCH 1184/2493] Rewrite `ControlPointTable` to use virtualised list --- osu.Game/Screens/Edit/EditorTable.cs | 15 +- osu.Game/Screens/Edit/TableHeaderText.cs | 19 ++ .../Screens/Edit/Timing/ControlPointList.cs | 38 +-- .../Screens/Edit/Timing/ControlPointTable.cs | 296 +++++++++++++----- .../RowAttributes/AttributeProgressBar.cs | 1 + 5 files changed, 233 insertions(+), 136 deletions(-) create mode 100644 osu.Game/Screens/Edit/TableHeaderText.cs diff --git a/osu.Game/Screens/Edit/EditorTable.cs b/osu.Game/Screens/Edit/EditorTable.cs index 4d8393e829..cec9dd2a7d 100644 --- a/osu.Game/Screens/Edit/EditorTable.cs +++ b/osu.Game/Screens/Edit/EditorTable.cs @@ -4,15 +4,11 @@ using System; using System.Diagnostics; using osu.Framework.Allocation; -using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; -using osu.Framework.Localisation; -using osu.Game.Graphics; using osu.Game.Graphics.Containers; -using osu.Game.Graphics.Sprites; using osu.Game.Overlays; using osuTK.Graphics; @@ -89,16 +85,7 @@ namespace osu.Game.Screens.Edit return BackgroundFlow[index].Item; } - protected override Drawable CreateHeader(int index, TableColumn? column) => new HeaderText(column?.Header ?? default); - - private partial class HeaderText : OsuSpriteText - { - public HeaderText(LocalisableString text) - { - Text = text.ToUpper(); - Font = OsuFont.GetFont(size: 12, weight: FontWeight.Bold); - } - } + protected override Drawable CreateHeader(int index, TableColumn? column) => new TableHeaderText(column?.Header ?? default); public partial class RowBackground : OsuClickableContainer { diff --git a/osu.Game/Screens/Edit/TableHeaderText.cs b/osu.Game/Screens/Edit/TableHeaderText.cs new file mode 100644 index 0000000000..61301f86ed --- /dev/null +++ b/osu.Game/Screens/Edit/TableHeaderText.cs @@ -0,0 +1,19 @@ +// 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.Extensions.LocalisationExtensions; +using osu.Framework.Localisation; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; + +namespace osu.Game.Screens.Edit +{ + public partial class TableHeaderText : OsuSpriteText + { + public TableHeaderText(LocalisableString text) + { + Text = text.ToUpper(); + Font = OsuFont.GetFont(size: 12, weight: FontWeight.Bold); + } + } +} diff --git a/osu.Game/Screens/Edit/Timing/ControlPointList.cs b/osu.Game/Screens/Edit/Timing/ControlPointList.cs index 4e4090ccd0..a0d833c908 100644 --- a/osu.Game/Screens/Edit/Timing/ControlPointList.cs +++ b/osu.Game/Screens/Edit/Timing/ControlPointList.cs @@ -7,10 +7,8 @@ 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.Input.Events; using osu.Game.Beatmaps.ControlPoints; -using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Overlays; @@ -21,12 +19,8 @@ namespace osu.Game.Screens.Edit.Timing public partial class ControlPointList : CompositeDrawable { private OsuButton deleteButton = null!; - private ControlPointTable table = null!; - private OsuScrollContainer scroll = null!; private RoundedButton addButton = null!; - private readonly IBindableList controlPointGroups = new BindableList(); - [Resolved] private EditorClock clock { get; set; } = null!; @@ -36,9 +30,6 @@ namespace osu.Game.Screens.Edit.Timing [Resolved] private Bindable selectedGroup { get; set; } = null!; - [Resolved] - private IEditorChangeHandler? changeHandler { get; set; } - [BackgroundDependencyLoader] private void load(OverlayColourProvider colours) { @@ -47,21 +38,10 @@ namespace osu.Game.Screens.Edit.Timing const float margins = 10; InternalChildren = new Drawable[] { - new Box - { - Colour = colours.Background4, - RelativeSizeAxes = Axes.Both, - }, - new Box - { - Colour = colours.Background3, - RelativeSizeAxes = Axes.Y, - Width = ControlPointTable.TIMING_COLUMN_WIDTH + margins, - }, - scroll = new OsuScrollContainer + new ControlPointTable { RelativeSizeAxes = Axes.Both, - Child = table = new ControlPointTable(), + Groups = { BindTarget = Beatmap.ControlPointInfo.Groups, }, }, new FillFlowContainer { @@ -106,19 +86,7 @@ namespace osu.Game.Screens.Edit.Timing : "+ Add at current time"; }, true); - controlPointGroups.BindTo(Beatmap.ControlPointInfo.Groups); - controlPointGroups.BindCollectionChanged((_, _) => - { - // This callback can happen many times in a change operation. It gets expensive. - // We really should be handling the `CollectionChanged` event properly. - Scheduler.AddOnce(() => - { - table.ControlGroups = controlPointGroups; - changeHandler?.SaveState(); - }); - }, true); - - table.OnRowSelected += drawable => scroll.ScrollIntoView(drawable); + //table.OnRowSelected += drawable => scroll.ScrollIntoView(drawable); } protected override bool OnClick(ClickEvent e) diff --git a/osu.Game/Screens/Edit/Timing/ControlPointTable.cs b/osu.Game/Screens/Edit/Timing/ControlPointTable.cs index 219575a380..3bb801f471 100644 --- a/osu.Game/Screens/Edit/Timing/ControlPointTable.cs +++ b/osu.Game/Screens/Edit/Timing/ControlPointTable.cs @@ -2,149 +2,270 @@ // 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; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Pooling; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.UserInterface; +using osu.Framework.Input.Events; +using osu.Framework.Localisation; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Extensions; using osu.Game.Graphics; +using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; +using osu.Game.Overlays; using osu.Game.Screens.Edit.Timing.RowAttributes; using osuTK; namespace osu.Game.Screens.Edit.Timing { - public partial class ControlPointTable : EditorTable + public partial class ControlPointTable : CompositeDrawable { - [Resolved] - private Bindable selectedGroup { get; set; } = null!; + public BindableList Groups { get; } = new BindableList(); - [Resolved] - private EditorClock clock { get; set; } = null!; + private const float timing_column_width = 300; + private const float row_height = 25; + private const float row_horizontal_padding = 20; - public const float TIMING_COLUMN_WIDTH = 300; - - public IEnumerable ControlGroups + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colours) { - set + RelativeSizeAxes = Axes.Both; + + InternalChildren = new Drawable[] { - int selectedIndex = GetIndexForObject(selectedGroup.Value); - - Content = null; - BackgroundFlow.Clear(); - - if (!value.Any()) - return; - - foreach (var group in value) + new Box { - BackgroundFlow.Add(new RowBackground(group) + Colour = colours.Background4, + RelativeSizeAxes = Axes.Both, + }, + new Box + { + Colour = colours.Background3, + RelativeSizeAxes = Axes.Y, + Width = timing_column_width + 10, + }, + new Container + { + RelativeSizeAxes = Axes.X, + Height = row_height, + Padding = new MarginPadding { Horizontal = row_horizontal_padding }, + Children = new Drawable[] { - // schedule to give time for any modified focused text box to lose focus and commit changes (e.g. BPM / time signature textboxes) before switching to new point. - Action = () => Schedule(() => + new TableHeaderText("Time") { - SetSelectedRow(group); - clock.SeekSmoothlyTo(group.Time); - }) - }); - } + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + }, + new TableHeaderText("Attributes") + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Margin = new MarginPadding { Left = ControlPointTable.timing_column_width } + }, + } + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Top = row_height }, + Child = new ControlPointRowList + { + RelativeSizeAxes = Axes.Both, + RowData = { BindTarget = Groups, }, + }, + }, + }; + } - Columns = createHeaders(); - Content = value.Select(createContent).ToArray().ToRectangular(); + private partial class ControlPointRowList : VirtualisedListContainer + { + [Resolved] + private Bindable selectedGroup { get; set; } = null!; - // Attempt to retain selection. - if (SetSelectedRow(selectedGroup.Value)) - return; + public ControlPointRowList() + : base(row_height, 50) + { + } - // Some operations completely obliterate references, so best-effort reselect based on index. - if (SetSelectedRow(GetObjectAtIndex(selectedIndex))) - return; + protected override ScrollContainer CreateScrollContainer() => new OsuScrollContainer(); - // Selection could not be retained. - selectedGroup.Value = null; + protected override void LoadComplete() + { + base.LoadComplete(); + + selectedGroup.BindValueChanged(val => + { + var row = Items.FlowingChildren.SingleOrDefault(item => item.Row.Equals(val.NewValue)); + if (row != null) + Scroll.ScrollIntoView(row); + }); } } - protected override void LoadComplete() + public partial class DrawableControlGroup : PoolableDrawable, IHasCurrentValue { - base.LoadComplete(); - - // Handle external selections. - selectedGroup.BindValueChanged(g => SetSelectedRow(g.NewValue), true); - } - - protected override bool SetSelectedRow(object? item) - { - if (!base.SetSelectedRow(item)) - return false; - - selectedGroup.Value = item as ControlPointGroup; - return true; - } - - private TableColumn[] createHeaders() - { - var columns = new List + public Bindable Current { - new TableColumn("Time", Anchor.CentreLeft, new Dimension(GridSizeMode.Absolute, TIMING_COLUMN_WIDTH)), - new TableColumn("Attributes", Anchor.CentreLeft), - }; + get => current.Current; + set => current.Current = value; + } - return columns.ToArray(); - } + private readonly BindableWithCurrent current = new BindableWithCurrent(); - private Drawable[] createContent(ControlPointGroup group) - { - return new Drawable[] + private Box background = null!; + + [Resolved] + private OverlayColourProvider colourProvider { get; set; } = null!; + + [Resolved] + private Bindable selectedGroup { get; set; } = null!; + + [Resolved] + private EditorClock editorClock { get; set; } = null!; + + [BackgroundDependencyLoader] + private void load() { - new ControlGroupTiming(group), - new ControlGroupAttributes(group, c => c is not TimingControlPoint) - }; + RelativeSizeAxes = Axes.Both; + + InternalChildren = new Drawable[] + { + background = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background1, + Alpha = 0, + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Horizontal = row_horizontal_padding, }, + Children = new Drawable[] + { + new ControlGroupTiming { Group = { BindTarget = current }, }, + new ControlGroupAttributes(point => point is not TimingControlPoint) + { + Group = { BindTarget = current }, + Margin = new MarginPadding { Left = timing_column_width } + } + } + } + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + selectedGroup.BindValueChanged(_ => updateState(), true); + FinishTransforms(true); + } + + protected override void PrepareForUse() + { + base.PrepareForUse(); + + updateState(); + } + + protected override bool OnHover(HoverEvent e) + { + updateState(); + return true; + } + + protected override void OnHoverLost(HoverLostEvent e) + { + base.OnHoverLost(e); + updateState(); + } + + protected override bool OnClick(ClickEvent e) + { + // schedule to give time for any modified focused text box to lose focus and commit changes (e.g. BPM / time signature textboxes) before switching to new point. + var currentGroup = Current.Value; + Schedule(() => + { + selectedGroup.Value = currentGroup; + editorClock.SeekSmoothlyTo(currentGroup.Time); + }); + return true; + } + + private void updateState() + { + bool isSelected = selectedGroup.Value?.Equals(current.Value) == true; + + if (IsHovered || isSelected) + background.FadeIn(100, Easing.OutQuint); + else + background.FadeOut(100, Easing.OutQuint); + + background.Colour = isSelected ? colourProvider.Colour3 : colourProvider.Background1; + } } private partial class ControlGroupTiming : FillFlowContainer { - public ControlGroupTiming(ControlPointGroup group) + public Bindable Group { get; } = new Bindable(); + + private OsuSpriteText timeText = null!; + + [BackgroundDependencyLoader] + private void load() { Name = @"ControlGroupTiming"; RelativeSizeAxes = Axes.Y; - Width = TIMING_COLUMN_WIDTH; + Width = timing_column_width; Spacing = new Vector2(5); Children = new Drawable[] { - new OsuSpriteText + timeText = new OsuSpriteText { - Text = group.Time.ToEditorFormattedString(), - Font = OsuFont.GetFont(size: TEXT_SIZE, weight: FontWeight.Bold), + Font = OsuFont.GetFont(size: 14, weight: FontWeight.Bold), Width = 70, Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, }, - new ControlGroupAttributes(group, c => c is TimingControlPoint) + new ControlGroupAttributes(c => c is TimingControlPoint) { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, + Group = { BindTarget = Group }, } }; } + + protected override void LoadComplete() + { + base.LoadComplete(); + + Group.BindValueChanged(_ => timeText.Text = Group.Value?.Time.ToEditorFormattedString() ?? default(LocalisableString), true); + } } private partial class ControlGroupAttributes : CompositeDrawable { + public Bindable Group { get; } = new Bindable(); + private BindableList controlPoints { get; } = new BindableList(); + private readonly Func matchFunction; - private readonly IBindableList controlPoints = new BindableList(); + private FillFlowContainer fill = null!; - private readonly FillFlowContainer fill; - - public ControlGroupAttributes(ControlPointGroup group, Func matchFunction) + public ControlGroupAttributes(Func matchFunction) { this.matchFunction = matchFunction; + } + [BackgroundDependencyLoader] + private void load() + { AutoSizeAxes = Axes.X; RelativeSizeAxes = Axes.Y; Name = @"ControlGroupAttributes"; @@ -156,20 +277,21 @@ namespace osu.Game.Screens.Edit.Timing Direction = FillDirection.Horizontal, Spacing = new Vector2(2) }; - - controlPoints.BindTo(group.ControlPoints); - } - - [BackgroundDependencyLoader] - private void load() - { - createChildren(); } protected override void LoadComplete() { base.LoadComplete(); - controlPoints.CollectionChanged += (_, _) => createChildren(); + + Group.BindValueChanged(_ => + { + controlPoints.UnbindBindings(); + controlPoints.Clear(); + if (Group.Value != null) + ((IBindableList)controlPoints).BindTo(Group.Value.ControlPoints); + }, true); + + controlPoints.BindCollectionChanged((_, _) => createChildren(), true); } private void createChildren() diff --git a/osu.Game/Screens/Edit/Timing/RowAttributes/AttributeProgressBar.cs b/osu.Game/Screens/Edit/Timing/RowAttributes/AttributeProgressBar.cs index 4cae774078..0a89f196fa 100644 --- a/osu.Game/Screens/Edit/Timing/RowAttributes/AttributeProgressBar.cs +++ b/osu.Game/Screens/Edit/Timing/RowAttributes/AttributeProgressBar.cs @@ -36,6 +36,7 @@ namespace osu.Game.Screens.Edit.Timing.RowAttributes BackgroundColour = overlayColours.Background6; FillColour = controlPoint.GetRepresentingColour(colours); + FinishTransforms(true); } } } From b12db8fbe28d96c8f965f7a1d3fb067e287ce1c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 24 Jun 2024 14:44:02 +0200 Subject: [PATCH 1185/2493] Rewrite `IssueTable` to use virtualised list --- osu.Game/Screens/Edit/Verify/IssueList.cs | 11 +- osu.Game/Screens/Edit/Verify/IssueTable.cs | 309 ++++++++++++++------- 2 files changed, 213 insertions(+), 107 deletions(-) diff --git a/osu.Game/Screens/Edit/Verify/IssueList.cs b/osu.Game/Screens/Edit/Verify/IssueList.cs index d07190fca0..de7b760bcd 100644 --- a/osu.Game/Screens/Edit/Verify/IssueList.cs +++ b/osu.Game/Screens/Edit/Verify/IssueList.cs @@ -11,7 +11,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Game.Beatmaps; -using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Overlays; using osu.Game.Rulesets.Edit; @@ -56,10 +55,9 @@ namespace osu.Game.Screens.Edit.Verify Colour = colours.Background3, RelativeSizeAxes = Axes.Both, }, - new OsuScrollContainer + table = new IssueTable { RelativeSizeAxes = Axes.Both, - Child = table = new IssueTable(), }, new FillFlowContainer { @@ -101,9 +99,10 @@ namespace osu.Game.Screens.Edit.Verify issues = filter(issues); - table.Issues = issues - .OrderBy(issue => issue.Template.Type) - .ThenBy(issue => issue.Check.Metadata.Category); + table.Issues.Clear(); + table.Issues.AddRange(issues + .OrderBy(issue => issue.Template.Type) + .ThenBy(issue => issue.Check.Metadata.Category)); } private IEnumerable filter(IEnumerable issues) diff --git a/osu.Game/Screens/Edit/Verify/IssueTable.cs b/osu.Game/Screens/Edit/Verify/IssueTable.cs index 8fb30fb726..fbe789d452 100644 --- a/osu.Game/Screens/Edit/Verify/IssueTable.cs +++ b/osu.Game/Screens/Edit/Verify/IssueTable.cs @@ -1,132 +1,239 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Pooling; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; using osu.Game.Graphics; +using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Input.Bindings; +using osu.Game.Overlays; using osu.Game.Rulesets.Edit.Checks.Components; namespace osu.Game.Screens.Edit.Verify { - public partial class IssueTable : EditorTable + public partial class IssueTable : CompositeDrawable { - private Bindable selectedIssue = null!; + public BindableList Issues { get; } = new BindableList(); - [Resolved] - private VerifyScreen verify { get; set; } = null!; + public const float COLUMN_WIDTH = 70; + public const float COLUMN_GAP = 10; + public const float ROW_HEIGHT = 25; + public const float ROW_HORIZONTAL_PADDING = 20; + public const int TEXT_SIZE = 14; - [Resolved] - private EditorClock clock { get; set; } = null!; - - [Resolved] - private EditorBeatmap editorBeatmap { get; set; } = null!; - - [Resolved] - private Editor editor { get; set; } = null!; - - public IEnumerable Issues + [BackgroundDependencyLoader] + private void load() { - set + InternalChildren = new Drawable[] { - Content = null; - BackgroundFlow.Clear(); - - if (!value.Any()) - return; - - foreach (var issue in value) + new Container { - BackgroundFlow.Add(new RowBackground(issue) + RelativeSizeAxes = Axes.X, + Height = ROW_HEIGHT, + Padding = new MarginPadding { Horizontal = ROW_HORIZONTAL_PADDING, }, + Children = new[] { - Action = () => + new TableHeaderText("Type") { - selectedIssue.Value = issue; - - if (issue.Time != null) - { - clock.Seek(issue.Time.Value); - editor.OnPressed(new KeyBindingPressEvent(GetContainingInputManager()!.CurrentState, GlobalAction.EditorComposeMode)); - } - - if (!issue.HitObjects.Any()) - return; - - editorBeatmap.SelectedHitObjects.Clear(); - editorBeatmap.SelectedHitObjects.AddRange(issue.HitObjects); + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, }, - }); + new TableHeaderText("Time") + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Margin = new MarginPadding { Left = COLUMN_WIDTH + COLUMN_GAP }, + }, + new TableHeaderText("Message") + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Margin = new MarginPadding { Left = 2 * (COLUMN_WIDTH + COLUMN_GAP) }, + }, + new TableHeaderText("Category") + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + }, + } + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Top = ROW_HEIGHT, }, + Child = new IssueRowList + { + RelativeSizeAxes = Axes.Both, + RowData = { BindTarget = Issues } + } + } + }; + } + + private partial class IssueRowList : VirtualisedListContainer + { + public IssueRowList() + : base(ROW_HEIGHT, 50) + { + } + + protected override ScrollContainer CreateScrollContainer() => new OsuScrollContainer(); + } + + public partial class DrawableIssue : PoolableDrawable, IHasCurrentValue + { + private readonly BindableWithCurrent current = new BindableWithCurrent(); + + private readonly Bindable selectedIssue = new Bindable(); + + private Box background = null!; + private OsuSpriteText issueTypeText = null!; + private OsuSpriteText issueTimestampText = null!; + private OsuSpriteText issueDetailText = null!; + private OsuSpriteText issueCategoryText = null!; + + [Resolved] + private EditorClock clock { get; set; } = null!; + + [Resolved] + private EditorBeatmap editorBeatmap { get; set; } = null!; + + [Resolved] + private Editor editor { get; set; } = null!; + + [Resolved] + private OverlayColourProvider colourProvider { get; set; } = null!; + + public Bindable Current + { + get => current.Current; + set => current.Current = value; + } + + [BackgroundDependencyLoader] + private void load(VerifyScreen verify) + { + RelativeSizeAxes = Axes.X; + Height = ROW_HEIGHT; + + InternalChildren = new Drawable[] + { + background = new Box + { + RelativeSizeAxes = Axes.Both, + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Horizontal = 20, }, + Children = new Drawable[] + { + issueTypeText = new OsuSpriteText + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Font = OsuFont.GetFont(size: TEXT_SIZE, weight: FontWeight.Bold), + }, + issueTimestampText = new OsuSpriteText + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Font = OsuFont.GetFont(size: TEXT_SIZE, weight: FontWeight.Bold), + Margin = new MarginPadding { Left = COLUMN_WIDTH + COLUMN_GAP }, + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding + { + Left = 2 * (COLUMN_GAP + COLUMN_WIDTH), + Right = COLUMN_GAP + COLUMN_WIDTH, + }, + Child = issueDetailText = new OsuSpriteText + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Font = OsuFont.GetFont(size: TEXT_SIZE, weight: FontWeight.Medium) + }, + }, + issueCategoryText = new OsuSpriteText + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + Font = OsuFont.GetFont(size: TEXT_SIZE, weight: FontWeight.Bold), + } + } + } + }; + + selectedIssue.BindTo(verify.SelectedIssue); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + selectedIssue.BindValueChanged(_ => updateState()); + Current.BindValueChanged(_ => updateState(), true); + FinishTransforms(true); + } + + protected override bool OnHover(HoverEvent e) + { + updateState(); + return true; + } + + protected override void OnHoverLost(HoverLostEvent e) + { + updateState(); + base.OnHoverLost(e); + } + + protected override bool OnClick(ClickEvent e) + { + selectedIssue.Value = current.Value; + + if (current.Value.Time != null) + { + clock.Seek(current.Value.Time.Value); + editor.OnPressed(new KeyBindingPressEvent(GetContainingInputManager()!.CurrentState, GlobalAction.EditorComposeMode)); } - Columns = createHeaders(); - Content = value.Select((g, i) => createContent(i, g)).ToArray().ToRectangular(); + if (current.Value.HitObjects.Any()) + { + editorBeatmap.SelectedHitObjects.Clear(); + editorBeatmap.SelectedHitObjects.AddRange(current.Value.HitObjects); + } + + return true; + } + + private void updateState() + { + issueTypeText.Text = Current.Value.Template.Type.ToString(); + issueTypeText.Colour = Current.Value.Template.Colour; + issueTimestampText.Text = Current.Value.GetEditorTimestamp(); + issueDetailText.Text = Current.Value.ToString(); + issueCategoryText.Text = Current.Value.Check.Metadata.Category.ToString(); + + bool isSelected = selectedIssue.Value == current.Value; + + if (IsHovered || isSelected) + background.FadeIn(100, Easing.OutQuint); + else + background.FadeOut(100, Easing.OutQuint); + + background.Colour = isSelected ? colourProvider.Colour3 : colourProvider.Background1; } } - - protected override void LoadComplete() - { - base.LoadComplete(); - - selectedIssue = verify.SelectedIssue.GetBoundCopy(); - selectedIssue.BindValueChanged(issue => - { - SetSelectedRow(issue.NewValue); - }, true); - } - - private TableColumn[] createHeaders() - { - var columns = new List - { - new TableColumn(string.Empty, Anchor.CentreLeft, new Dimension(GridSizeMode.AutoSize)), - new TableColumn("Type", Anchor.CentreLeft, new Dimension(GridSizeMode.AutoSize, minSize: 60)), - new TableColumn("Time", Anchor.CentreLeft, new Dimension(GridSizeMode.AutoSize, minSize: 60)), - new TableColumn("Message", Anchor.CentreLeft), - new TableColumn("Category", Anchor.CentreRight, new Dimension(GridSizeMode.AutoSize)), - }; - - return columns.ToArray(); - } - - private Drawable[] createContent(int index, Issue issue) => new Drawable[] - { - new OsuSpriteText - { - Text = $"#{index + 1}", - Font = OsuFont.GetFont(size: TEXT_SIZE, weight: FontWeight.Medium), - Margin = new MarginPadding { Right = 10 } - }, - new OsuSpriteText - { - Text = issue.Template.Type.ToString(), - Font = OsuFont.GetFont(size: TEXT_SIZE, weight: FontWeight.Bold), - Margin = new MarginPadding { Right = 10 }, - Colour = issue.Template.Colour - }, - new OsuSpriteText - { - Text = issue.GetEditorTimestamp(), - Font = OsuFont.GetFont(size: TEXT_SIZE, weight: FontWeight.Bold), - Margin = new MarginPadding { Right = 10 }, - }, - new OsuSpriteText - { - Text = issue.ToString(), - Font = OsuFont.GetFont(size: TEXT_SIZE, weight: FontWeight.Medium) - }, - new OsuSpriteText - { - Text = issue.Check.Metadata.Category.ToString(), - Font = OsuFont.GetFont(size: TEXT_SIZE, weight: FontWeight.Bold), - Margin = new MarginPadding(10) - } - }; } } From 9dfd6cf9ef17c3660c5d5d7f30006405ad31ad9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 26 Jun 2024 09:55:07 +0200 Subject: [PATCH 1186/2493] Remove `EditorTable` Begone, foul beast. --- osu.Game/Screens/Edit/EditorTable.cs | 176 --------------------------- 1 file changed, 176 deletions(-) delete mode 100644 osu.Game/Screens/Edit/EditorTable.cs diff --git a/osu.Game/Screens/Edit/EditorTable.cs b/osu.Game/Screens/Edit/EditorTable.cs deleted file mode 100644 index cec9dd2a7d..0000000000 --- a/osu.Game/Screens/Edit/EditorTable.cs +++ /dev/null @@ -1,176 +0,0 @@ -// 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.Diagnostics; -using osu.Framework.Allocation; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Input.Events; -using osu.Game.Graphics.Containers; -using osu.Game.Overlays; -using osuTK.Graphics; - -namespace osu.Game.Screens.Edit -{ - public abstract partial class EditorTable : TableContainer - { - public event Action? OnRowSelected; - - private const float horizontal_inset = 20; - - protected const float ROW_HEIGHT = 25; - - public const int TEXT_SIZE = 14; - - protected readonly FillFlowContainer BackgroundFlow; - - // We can avoid potentially thousands of objects being added to the input sub-tree since item selection is being handled by the BackgroundFlow - // and no items in the underlying table are clickable. - protected override bool ShouldBeConsideredForInput(Drawable child) => child == BackgroundFlow && base.ShouldBeConsideredForInput(child); - - protected EditorTable() - { - RelativeSizeAxes = Axes.X; - AutoSizeAxes = Axes.Y; - - Padding = new MarginPadding { Horizontal = horizontal_inset }; - RowSize = new Dimension(GridSizeMode.Absolute, ROW_HEIGHT); - - AddInternal(BackgroundFlow = new FillFlowContainer - { - RelativeSizeAxes = Axes.Both, - Depth = 1f, - Padding = new MarginPadding { Horizontal = -horizontal_inset }, - Margin = new MarginPadding { Top = ROW_HEIGHT } - }); - } - - protected int GetIndexForObject(object? item) - { - for (int i = 0; i < BackgroundFlow.Count; i++) - { - if (BackgroundFlow[i].Item == item) - return i; - } - - return -1; - } - - protected virtual bool SetSelectedRow(object? item) - { - bool foundSelection = false; - - foreach (var b in BackgroundFlow) - { - b.Selected = ReferenceEquals(b.Item, item); - - if (b.Selected) - { - Debug.Assert(!foundSelection); - OnRowSelected?.Invoke(b); - foundSelection = true; - } - } - - return foundSelection; - } - - protected object? GetObjectAtIndex(int index) - { - if (index < 0 || index > BackgroundFlow.Count - 1) - return null; - - return BackgroundFlow[index].Item; - } - - protected override Drawable CreateHeader(int index, TableColumn? column) => new TableHeaderText(column?.Header ?? default); - - public partial class RowBackground : OsuClickableContainer - { - public readonly object Item; - - private const int fade_duration = 100; - - private readonly Box hoveredBackground; - - public RowBackground(object item) - { - Item = item; - - RelativeSizeAxes = Axes.X; - Height = 25; - - AlwaysPresent = true; - - CornerRadius = 3; - Masking = true; - - Children = new Drawable[] - { - hoveredBackground = new Box - { - RelativeSizeAxes = Axes.Both, - Alpha = 0, - }, - }; - } - - private Color4 colourHover; - private Color4 colourSelected; - - [BackgroundDependencyLoader] - private void load(OverlayColourProvider colours) - { - colourHover = colours.Background1; - colourSelected = colours.Colour3; - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - updateState(); - FinishTransforms(true); - } - - private bool selected; - - public bool Selected - { - get => selected; - set - { - if (value == selected) - return; - - selected = value; - updateState(); - } - } - - protected override bool OnHover(HoverEvent e) - { - updateState(); - return base.OnHover(e); - } - - protected override void OnHoverLost(HoverLostEvent e) - { - updateState(); - base.OnHoverLost(e); - } - - private void updateState() - { - hoveredBackground.FadeColour(selected ? colourSelected : colourHover, 450, Easing.OutQuint); - - if (selected || IsHovered) - hoveredBackground.FadeIn(fade_duration, Easing.OutQuint); - else - hoveredBackground.FadeOut(fade_duration, Easing.OutQuint); - } - } - } -} From 6beae91d53330ed58cb5dc2d0cc3fe5fc955a19a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 26 Jun 2024 21:10:29 +0900 Subject: [PATCH 1187/2493] Ensure carousel panel depth is consistent based on vertical position I thought this was already being handled, but it turns out that changing sort mode (and potentially other operations) could break the depth of display of panels due to pooling and what not. This ensures consistency and also employs @bdach's suggestion of reversing the depth above and below the current selection for a better visual effect. --- osu.Game/Screens/Select/BeatmapCarousel.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 3aa980cec0..4f2325adbf 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -895,7 +895,6 @@ namespace osu.Game.Screens.Select { var panel = setPool.Get(p => p.Item = item); - panel.Depth = item.CarouselYPosition; panel.Y = item.CarouselYPosition; Scroll.Add(panel); @@ -915,6 +914,8 @@ namespace osu.Game.Screens.Select { bool isSelected = item.Item.State.Value == CarouselItemState.Selected; + bool hasPassedSelection = item.Item.CarouselYPosition < selectedBeatmapSet?.CarouselYPosition; + // Cheap way of doing animations when entering / exiting song select. const double half_time = 50; const float panel_x_offset_when_inactive = 200; @@ -929,6 +930,8 @@ namespace osu.Game.Screens.Select item.Alpha = (float)Interpolation.DampContinuously(item.Alpha, 0, half_time, Clock.ElapsedFrameTime); item.X = (float)Interpolation.DampContinuously(item.X, panel_x_offset_when_inactive, half_time, Clock.ElapsedFrameTime); } + + Scroll.ChangeChildDepth(item, hasPassedSelection ? -item.Item.CarouselYPosition : item.Item.CarouselYPosition); } if (item is DrawableCarouselBeatmapSet set) From fd91210c1c1fb4b5a34dc6ae3acd86bd27b115f5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 26 Jun 2024 21:47:33 +0900 Subject: [PATCH 1188/2493] Remove unnecessary setter on bindable --- .../Screens/OnlinePlay/DailyChallenge/DailyChallengeCarousel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeCarousel.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeCarousel.cs index 2fd5253347..5f58316b25 100644 --- a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeCarousel.cs +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeCarousel.cs @@ -123,7 +123,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge { public Action? Clicked { get; set; } - public BindableBool Active { get; set; } = new BindableBool(); + public BindableBool Active { get; } = new BindableBool(); private double progress; From 2a839b3697259ed90691affb263ad36a74bad0b6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 26 Jun 2024 21:50:06 +0900 Subject: [PATCH 1189/2493] Make action `required init` --- .../OnlinePlay/DailyChallenge/DailyChallengeCarousel.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeCarousel.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeCarousel.cs index 5f58316b25..a9f9a5cd78 100644 --- a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeCarousel.cs +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeCarousel.cs @@ -109,9 +109,9 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge lastDisplayed = currentDisplay; } - private void onManualNavigation(NavigationDot obj) + private void onManualNavigation(NavigationDot dot) { - int index = navigationFlow.IndexOf(obj); + int index = navigationFlow.IndexOf(dot); if (index < 0) return; @@ -121,7 +121,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge private partial class NavigationDot : CompositeDrawable { - public Action? Clicked { get; set; } + public required Action Clicked { get; init; } public BindableBool Active { get; } = new BindableBool(); @@ -222,7 +222,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge protected override bool OnClick(ClickEvent e) { - Clicked?.Invoke(this); + Clicked(this); hoverLayer.FadeTo(1) .Then().FadeTo(IsHovered ? 0.2f : 0, 250, Easing.OutQuint); From 936a8d800d235e0576d81d29289a536a7744507a Mon Sep 17 00:00:00 2001 From: normalid Date: Wed, 26 Jun 2024 21:39:14 +0800 Subject: [PATCH 1190/2493] Swap the low and high multiplier color --- osu.Game/Screens/Select/FooterButtonMods.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Select/FooterButtonMods.cs b/osu.Game/Screens/Select/FooterButtonMods.cs index 5685910c0a..a15d315f1b 100644 --- a/osu.Game/Screens/Select/FooterButtonMods.cs +++ b/osu.Game/Screens/Select/FooterButtonMods.cs @@ -59,8 +59,8 @@ namespace osu.Game.Screens.Select { SelectedColour = colours.Yellow; DeselectedColour = SelectedColour.Opacity(0.5f); - lowMultiplierColour = colours.Red; - highMultiplierColour = colours.Green; + lowMultiplierColour = colours.Green; + highMultiplierColour = colours.Red; Text = @"mods"; Hotkey = GlobalAction.ToggleModSelection; From 8f0198ba0f775be29a79ab194c8cb316d2f1a050 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 26 Jun 2024 15:42:10 +0200 Subject: [PATCH 1191/2493] Add test coverage for encode-after-decode stability of slider sample volume specs --- .../per-slider-node-sample-settings.osu | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 osu.Game.Tests/Resources/per-slider-node-sample-settings.osu diff --git a/osu.Game.Tests/Resources/per-slider-node-sample-settings.osu b/osu.Game.Tests/Resources/per-slider-node-sample-settings.osu new file mode 100644 index 0000000000..2f56465d90 --- /dev/null +++ b/osu.Game.Tests/Resources/per-slider-node-sample-settings.osu @@ -0,0 +1,27 @@ +osu file format v128 + +[General] +SampleSet: Normal + +[TimingPoints] +15,1000,4,1,0,100,1,0 +2271,-100,4,1,0,5,0,0 +6021,-100,4,1,0,100,0,0 +9515,-100,4,1,0,5,0,0 +9521,-100,4,1,0,100,0,0 +10265,-100,4,1,0,5,0,0 +13765,-100,4,1,0,100,0,0 +13771,-100,4,1,0,5,0,0 +14770,-100,4,1,0,50,0,0 +18264,-100,4,1,0,100,0,0 +18270,-100,4,1,0,50,0,0 +21764,-100,4,1,0,5,0,0 +21770,-100,4,1,0,50,0,0 +25264,-100,4,1,0,100,0,0 +25270,-100,4,1,0,50,0,0 + +[HitObjects] +113,54,2265,6,0,L|422:55,1,300,0|0,1:0|1:0,1:0:0:0: +82,206,6015,2,0,L|457:204,1,350,0|0,2:0|2:0,2:0:0:0: +75,310,10265,2,0,L|435:312,1,350,0|0,3:0|3:0,3:0:0:0: +75,310,14764,2,0,L|435:312,3,350,0|0|0|0,3:0|3:0|3:0|3:0,3:0:0:0: From fff27e619d41802702722f7426b8000c5f1ebd6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 26 Jun 2024 14:27:14 +0200 Subject: [PATCH 1192/2493] Fix slider tail volume not saving Closes https://github.com/ppy/osu/issues/28587. As outlined in the issue thread, the tail volume wasn't saving because it wasn't actually attached to a hitobject properly, and as such the `LegacyBeatmapEncoder` logic, which is based on hitobjects, did not pick them up on save. To fix that, switch to using `NodeSamples` for objects that are `IHasRepeats`. That has one added complication in that having it work properly requires changes to the decode side too. That is because the intent is to allow the user to change the sample settings for each node (which are specified via `NodeSamples`), as well as "the rest of the object", which generally means ticks or auxiliary samples like `sliderslide` (which are specified by `Samples`). However, up until now, `Samples` always queried the control point which was _active at the end time of the slider_. This obviously can't work anymore when converting `NodeSamples` to legacy control points, because the last node's sample is _also_ at the end time of the slider. To bypass that, add extra sample points after each node (just out of reach of the 5ms leniency), which are supposed to control volume of ticks and/or slides. Upon testing, this *sort of* has the intended effect in stable, with the exception of `sliderslide`, which seems to either respect or _not_ respect the relevant volume spec dependent on... not sure what, and not sure I want to be debugging that. It might be frame alignment, or it might be the phase of the moon. --- .../Formats/LegacyBeatmapDecoderTest.cs | 13 ++++++-- .../Beatmaps/Formats/LegacyBeatmapDecoder.cs | 16 ++++++---- .../Beatmaps/Formats/LegacyBeatmapEncoder.cs | 32 +++++++++++++++---- 3 files changed, 47 insertions(+), 14 deletions(-) diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs index a4cd888823..19378821b3 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs @@ -528,8 +528,17 @@ namespace osu.Game.Tests.Beatmaps.Formats Assert.AreEqual("Gameplay/normal-hitnormal2", getTestableSampleInfo(hitObjects[2]).LookupNames.First()); Assert.AreEqual("Gameplay/normal-hitnormal", getTestableSampleInfo(hitObjects[3]).LookupNames.First()); - // The control point at the end time of the slider should be applied - Assert.AreEqual("Gameplay/soft-hitnormal8", getTestableSampleInfo(hitObjects[4]).LookupNames.First()); + // The fourth object is a slider. + // `Samples` of a slider are presumed to control the volume of sounds that last the entire duration of the slider + // (such as ticks, slider slide sounds, etc.) + // Thus, the point of query of control points used for `Samples` is just beyond the start time of the slider. + Assert.AreEqual("Gameplay/soft-hitnormal11", getTestableSampleInfo(hitObjects[4]).LookupNames.First()); + + // That said, the `NodeSamples` of the slider are responsible for the sounds of the slider's head / tail / repeats / large ticks etc. + // Therefore, they should be read at the time instant correspondent to the given node. + // This means that the tail should use bank 8 rather than 11. + Assert.AreEqual("Gameplay/soft-hitnormal11", ((ConvertSlider)hitObjects[4]).NodeSamples[0][0].LookupNames.First()); + Assert.AreEqual("Gameplay/soft-hitnormal8", ((ConvertSlider)hitObjects[4]).NodeSamples[1][0].LookupNames.First()); } static HitSampleInfo getTestableSampleInfo(HitObject hitObject) => hitObject.Samples[0]; diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs index c2f4097889..5fa85f189c 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs @@ -33,7 +33,7 @@ namespace osu.Game.Beatmaps.Formats /// /// Compare: https://github.com/peppy/osu-stable-reference/blob/master/osu!/GameplayElements/HitObjects/HitObject.cs#L319 /// - private const double control_point_leniency = 5; + public const double CONTROL_POINT_LENIENCY = 5; internal static RulesetStore? RulesetStore; @@ -160,20 +160,24 @@ namespace osu.Game.Beatmaps.Formats private void applySamples(HitObject hitObject) { - SampleControlPoint sampleControlPoint = (beatmap.ControlPointInfo as LegacyControlPointInfo)?.SamplePointAt(hitObject.GetEndTime() + control_point_leniency) ?? SampleControlPoint.DEFAULT; - - hitObject.Samples = hitObject.Samples.Select(o => sampleControlPoint.ApplyTo(o)).ToList(); - if (hitObject is IHasRepeats hasRepeats) { + SampleControlPoint sampleControlPoint = (beatmap.ControlPointInfo as LegacyControlPointInfo)?.SamplePointAt(hitObject.StartTime + CONTROL_POINT_LENIENCY + 1) ?? SampleControlPoint.DEFAULT; + hitObject.Samples = hitObject.Samples.Select(o => sampleControlPoint.ApplyTo(o)).ToList(); + for (int i = 0; i < hasRepeats.NodeSamples.Count; i++) { - double time = hitObject.StartTime + i * hasRepeats.Duration / hasRepeats.SpanCount() + control_point_leniency; + double time = hitObject.StartTime + i * hasRepeats.Duration / hasRepeats.SpanCount() + CONTROL_POINT_LENIENCY; var nodeSamplePoint = (beatmap.ControlPointInfo as LegacyControlPointInfo)?.SamplePointAt(time) ?? SampleControlPoint.DEFAULT; hasRepeats.NodeSamples[i] = hasRepeats.NodeSamples[i].Select(o => nodeSamplePoint.ApplyTo(o)).ToList(); } } + else + { + SampleControlPoint sampleControlPoint = (beatmap.ControlPointInfo as LegacyControlPointInfo)?.SamplePointAt(hitObject.GetEndTime() + CONTROL_POINT_LENIENCY) ?? SampleControlPoint.DEFAULT; + hitObject.Samples = hitObject.Samples.Select(o => sampleControlPoint.ApplyTo(o)).ToList(); + } } /// diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs index 186b565c39..09e3150359 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs @@ -282,19 +282,39 @@ namespace osu.Game.Beatmaps.Formats { foreach (var hitObject in hitObjects) { - if (hitObject.Samples.Count > 0) + if (hitObject is IHasRepeats hasNodeSamples) { - int volume = hitObject.Samples.Max(o => o.Volume); - int customIndex = hitObject.Samples.Any(o => o is ConvertHitObjectParser.LegacyHitSampleInfo) - ? hitObject.Samples.OfType().Max(o => o.CustomSampleBank) - : -1; + double spanDuration = hasNodeSamples.Duration / hasNodeSamples.SpanCount(); - yield return new LegacyBeatmapDecoder.LegacySampleControlPoint { Time = hitObject.GetEndTime(), SampleVolume = volume, CustomSampleBank = customIndex }; + for (int i = 0; i < hasNodeSamples.NodeSamples.Count; ++i) + { + double nodeTime = hitObject.StartTime + i * spanDuration; + + if (hasNodeSamples.NodeSamples[i].Any()) + yield return createSampleControlPointFor(nodeTime, hasNodeSamples.NodeSamples[i]); + + if (spanDuration > LegacyBeatmapDecoder.CONTROL_POINT_LENIENCY + 1) + yield return createSampleControlPointFor(nodeTime + LegacyBeatmapDecoder.CONTROL_POINT_LENIENCY + 1, hitObject.Samples); + } + } + else if (hitObject.Samples.Count > 0) + { + yield return createSampleControlPointFor(hitObject.GetEndTime(), hitObject.Samples); } foreach (var nested in collectSampleControlPoints(hitObject.NestedHitObjects)) yield return nested; } + + SampleControlPoint createSampleControlPointFor(double time, IList samples) + { + int volume = samples.Max(o => o.Volume); + int customIndex = samples.Any(o => o is ConvertHitObjectParser.LegacyHitSampleInfo) + ? samples.OfType().Max(o => o.CustomSampleBank) + : -1; + + return new LegacyBeatmapDecoder.LegacySampleControlPoint { Time = time, SampleVolume = volume, CustomSampleBank = customIndex }; + } } void extractSampleControlPoints(IEnumerable hitObject) From 1998742e425ef68886911646aa1b054a7c6bcbb6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 26 Jun 2024 22:56:43 +0900 Subject: [PATCH 1193/2493] 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 0f1a14afd8..e2b7fe479a 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -10,7 +10,7 @@ true - + diff --git a/osu.iOS.props b/osu.iOS.props index 6ed60b00b3..ddde2a3cec 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -23,6 +23,6 @@ iossimulator-x64 - + From b339d6a00cd3068d3c28a104004897c459f8ad8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 26 Jun 2024 16:26:32 +0200 Subject: [PATCH 1194/2493] Fix editor performance regression with hitmarkers active --- .../Objects/Drawables/DrawableHitCircle.cs | 19 ++++++++++--------- .../Objects/Drawables/DrawableSlider.cs | 4 ++-- .../Objects/Drawables/DrawableSliderTail.cs | 17 +++++++++++------ 3 files changed, 23 insertions(+), 17 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs index 7d707dea6c..101c34b725 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs @@ -9,6 +9,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; +using osu.Framework.Utils; using osu.Game.Graphics.Containers; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects.Drawables; @@ -325,19 +326,19 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables internal void SuppressHitAnimations() { - UpdateState(ArmedState.Idle, true); + UpdateState(ArmedState.Idle); UpdateComboColour(); - // This method is called every frame. If we need to, the following can likely be converted - // to code which doesn't use transforms at all. + // This method is called every frame in editor contexts, thus the lack of need for transforms. - // Matches stable (see https://github.com/peppy/osu-stable-reference/blob/bb57924c1552adbed11ee3d96cdcde47cf96f2b6/osu!/GameplayElements/HitObjects/Osu/HitCircleOsu.cs#L336-L338) + if (Time.Current >= HitStateUpdateTime) + { + // More or less matches stable (see https://github.com/peppy/osu-stable-reference/blob/bb57924c1552adbed11ee3d96cdcde47cf96f2b6/osu!/GameplayElements/HitObjects/Osu/HitCircleOsu.cs#L336-L338) + AccentColour.Value = Color4.White; + Alpha = Interpolation.ValueAt(Time.Current, 1f, 0f, HitStateUpdateTime, HitStateUpdateTime + 700); + } - using (BeginAbsoluteSequence(StateUpdateTime - 5)) - this.TransformBindableTo(AccentColour, Color4.White, Math.Max(0, HitStateUpdateTime - StateUpdateTime)); - - using (BeginAbsoluteSequence(HitStateUpdateTime)) - this.FadeOut(700).Expire(); + LifetimeEnd = HitStateUpdateTime + 700; } internal void RestoreHitAnimations() diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs index 02d0ebee83..eacd2b3e75 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs @@ -375,14 +375,14 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables internal void SuppressHitAnimations() { - UpdateState(ArmedState.Idle, true); + UpdateState(ArmedState.Idle); HeadCircle.SuppressHitAnimations(); TailCircle.SuppressHitAnimations(); } internal void RestoreHitAnimations() { - UpdateState(ArmedState.Hit, force: true); + UpdateState(ArmedState.Hit); HeadCircle.RestoreHitAnimations(); TailCircle.RestoreHitAnimations(); } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs index 42abf41d6f..8bb1b0aebc 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs @@ -3,12 +3,12 @@ #nullable disable -using System; using System.Diagnostics; using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Utils; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Types; using osu.Game.Skinning; @@ -132,14 +132,19 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables internal void SuppressHitAnimations() { - UpdateState(ArmedState.Idle, true); + UpdateState(ArmedState.Idle); UpdateComboColour(); - using (BeginAbsoluteSequence(StateUpdateTime - 5)) - this.TransformBindableTo(AccentColour, Color4.White, Math.Max(0, HitStateUpdateTime - StateUpdateTime)); + // This method is called every frame in editor contexts, thus the lack of need for transforms. - using (BeginAbsoluteSequence(HitStateUpdateTime)) - this.FadeOut(700).Expire(); + if (Time.Current >= HitStateUpdateTime) + { + // More or less matches stable (see https://github.com/peppy/osu-stable-reference/blob/bb57924c1552adbed11ee3d96cdcde47cf96f2b6/osu!/GameplayElements/HitObjects/Osu/HitCircleOsu.cs#L336-L338) + AccentColour.Value = Color4.White; + Alpha = Interpolation.ValueAt(Time.Current, 1f, 0f, HitStateUpdateTime, HitStateUpdateTime + 700); + } + + LifetimeEnd = HitStateUpdateTime + 700; } internal void RestoreHitAnimations() From 847946937ed993a0784c108411140b3202c5da19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 26 Jun 2024 16:56:43 +0200 Subject: [PATCH 1195/2493] Fix test failures --- osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs index 09e3150359..54f23d8ecc 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs @@ -290,10 +290,10 @@ namespace osu.Game.Beatmaps.Formats { double nodeTime = hitObject.StartTime + i * spanDuration; - if (hasNodeSamples.NodeSamples[i].Any()) + if (hasNodeSamples.NodeSamples[i].Count > 0) yield return createSampleControlPointFor(nodeTime, hasNodeSamples.NodeSamples[i]); - if (spanDuration > LegacyBeatmapDecoder.CONTROL_POINT_LENIENCY + 1) + if (spanDuration > LegacyBeatmapDecoder.CONTROL_POINT_LENIENCY + 1 && hitObject.Samples.Count > 0) yield return createSampleControlPointFor(nodeTime + LegacyBeatmapDecoder.CONTROL_POINT_LENIENCY + 1, hitObject.Samples); } } From 0ee89183cc28b3cd39e14c30a4ec83a353a9db9d Mon Sep 17 00:00:00 2001 From: smallketchup82 Date: Wed, 26 Jun 2024 15:25:41 -0400 Subject: [PATCH 1196/2493] initial implementation --- osu.Desktop/OsuGameDesktop.cs | 34 +++----- osu.Desktop/Program.cs | 39 +++------ ...lUpdateManager.cs => VeloUpdateManager.cs} | 86 ++++--------------- osu.Desktop/osu.Desktop.csproj | 2 +- 4 files changed, 38 insertions(+), 123 deletions(-) rename osu.Desktop/Updater/{SquirrelUpdateManager.cs => VeloUpdateManager.cs} (52%) diff --git a/osu.Desktop/OsuGameDesktop.cs b/osu.Desktop/OsuGameDesktop.cs index e8783c997a..245d00dc53 100644 --- a/osu.Desktop/OsuGameDesktop.cs +++ b/osu.Desktop/OsuGameDesktop.cs @@ -19,7 +19,6 @@ using osu.Desktop.Windows; using osu.Framework.Allocation; using osu.Game.IO; using osu.Game.IPC; -using osu.Game.Online.Multiplayer; using osu.Game.Performance; using osu.Game.Utils; using SDL; @@ -103,35 +102,22 @@ namespace osu.Desktop if (!string.IsNullOrEmpty(packageManaged)) return new NoActionUpdateManager(); - switch (RuntimeInfo.OS) - { - case RuntimeInfo.Platform.Windows: - Debug.Assert(OperatingSystem.IsWindows()); - - return new SquirrelUpdateManager(); - - default: - return new SimpleUpdateManager(); - } + return new VeloUpdateManager(); } public override bool RestartAppWhenExited() { - switch (RuntimeInfo.OS) + try { - case RuntimeInfo.Platform.Windows: - Debug.Assert(OperatingSystem.IsWindows()); - - // Of note, this is an async method in squirrel that adds an arbitrary delay before returning - // likely to ensure the external process is in a good state. - // - // We're not waiting on that here, but the outro playing before the actual exit should be enough - // to cover this. - Squirrel.UpdateManager.RestartAppWhenExited().FireAndForget(); - return true; + Process.Start(Process.GetCurrentProcess().MainModule?.FileName ?? throw new InvalidOperationException()); + Environment.Exit(0); + return true; + } + catch (Exception e) + { + Logger.Error(e, "Failed to restart application"); + return base.RestartAppWhenExited(); } - - return base.RestartAppWhenExited(); } protected override void LoadComplete() diff --git a/osu.Desktop/Program.cs b/osu.Desktop/Program.cs index 23e56cdce9..7c23c15d5a 100644 --- a/osu.Desktop/Program.cs +++ b/osu.Desktop/Program.cs @@ -3,7 +3,6 @@ using System; using System.IO; -using System.Runtime.Versioning; using osu.Desktop.LegacyIpc; using osu.Desktop.Windows; using osu.Framework; @@ -14,7 +13,7 @@ using osu.Game; using osu.Game.IPC; using osu.Game.Tournament; using SDL; -using Squirrel; +using Velopack; namespace osu.Desktop { @@ -66,10 +65,10 @@ namespace osu.Desktop return; } } - - setupSquirrel(); } + setupVelo(); + // NVIDIA profiles are based on the executable name of a process. // Lazer and stable share the same executable name. // Stable sets this setting to "Off", which may not be what we want, so let's force it back to the default "Auto" on startup. @@ -177,32 +176,14 @@ namespace osu.Desktop return false; } - [SupportedOSPlatform("windows")] - private static void setupSquirrel() + private static void setupVelo() { - SquirrelAwareApp.HandleEvents(onInitialInstall: (_, tools) => - { - tools.CreateShortcutForThisExe(); - tools.CreateUninstallerRegistryEntry(); - WindowsAssociationManager.InstallAssociations(); - }, onAppUpdate: (_, tools) => - { - tools.CreateUninstallerRegistryEntry(); - WindowsAssociationManager.UpdateAssociations(); - }, onAppUninstall: (_, tools) => - { - tools.RemoveShortcutForThisExe(); - tools.RemoveUninstallerRegistryEntry(); - WindowsAssociationManager.UninstallAssociations(); - }, onEveryRun: (_, _, _) => - { - // While setting the `ProcessAppUserModelId` fixes duplicate icons/shortcuts on the taskbar, it currently - // causes the right-click context menu to function incorrectly. - // - // This may turn out to be non-required after an alternative solution is implemented. - // see https://github.com/clowd/Clowd.Squirrel/issues/24 - // tools.SetProcessAppUserModelId(); - }); + VelopackApp + .Build() + .WithFirstRun(v => + { + if (OperatingSystem.IsWindows()) WindowsAssociationManager.InstallAssociations(); + }).Run(); } } } diff --git a/osu.Desktop/Updater/SquirrelUpdateManager.cs b/osu.Desktop/Updater/VeloUpdateManager.cs similarity index 52% rename from osu.Desktop/Updater/SquirrelUpdateManager.cs rename to osu.Desktop/Updater/VeloUpdateManager.cs index dba157a6e9..8fc68f77cd 100644 --- a/osu.Desktop/Updater/SquirrelUpdateManager.cs +++ b/osu.Desktop/Updater/VeloUpdateManager.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Runtime.Versioning; using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Logging; @@ -10,30 +9,15 @@ using osu.Game; using osu.Game.Overlays; using osu.Game.Overlays.Notifications; using osu.Game.Screens.Play; -using Squirrel.SimpleSplat; -using Squirrel.Sources; -using LogLevel = Squirrel.SimpleSplat.LogLevel; -using UpdateManager = osu.Game.Updater.UpdateManager; +using osu.Game.Updater; namespace osu.Desktop.Updater { - [SupportedOSPlatform("windows")] - public partial class SquirrelUpdateManager : UpdateManager + public partial class VeloUpdateManager : UpdateManager { - private Squirrel.UpdateManager? updateManager; + private Velopack.UpdateManager? updateManager; private INotificationOverlay notificationOverlay = null!; - public Task PrepareUpdateAsync() => Squirrel.UpdateManager.RestartAppWhenExited(); - - private static readonly Logger logger = Logger.GetLogger("updater"); - - /// - /// Whether an update has been downloaded but not yet applied. - /// - private bool updatePending; - - private readonly SquirrelLogger squirrelLogger = new SquirrelLogger(); - [Resolved] private OsuGameBase game { get; set; } = null!; @@ -44,13 +28,11 @@ namespace osu.Desktop.Updater private void load(INotificationOverlay notifications) { notificationOverlay = notifications; - - SquirrelLocator.CurrentMutable.Register(() => squirrelLogger, typeof(ILogger)); } protected override async Task PerformUpdateCheck() => await checkForUpdateAsync().ConfigureAwait(false); - private async Task checkForUpdateAsync(bool useDeltaPatching = true, UpdateProgressNotification? notification = null) + private async Task checkForUpdateAsync(UpdateProgressNotification? notification = null) { // should we schedule a retry on completion of this check? bool scheduleRecheck = true; @@ -63,27 +45,27 @@ namespace osu.Desktop.Updater if (localUserInfo?.IsPlaying.Value == true) return false; - updateManager ??= new Squirrel.UpdateManager(new GithubSource(@"https://github.com/ppy/osu", github_token, false), @"osulazer"); + updateManager ??= new Velopack.UpdateManager(new Velopack.Sources.GithubSource(@"https://github.com/ppy/osu", github_token, false)); - var info = await updateManager.CheckForUpdate(!useDeltaPatching).ConfigureAwait(false); + var info = await updateManager.CheckForUpdatesAsync().ConfigureAwait(false); - if (info.ReleasesToApply.Count == 0) + if (info == null) { - if (updatePending) + // If there is an update pending restart, show the notification again. + if (updateManager.IsUpdatePendingRestart) { - // the user may have dismissed the completion notice, so show it again. notificationOverlay.Post(new UpdateApplicationCompleteNotification { Activated = () => { restartToApplyUpdate(); return true; - }, + } }); return true; } - // no updates available. bail and retry later. + // Otherwise there's no updates available. Bail and retry later. return false; } @@ -103,31 +85,17 @@ namespace osu.Desktop.Updater try { - await updateManager.DownloadReleases(info.ReleasesToApply, p => notification.Progress = p / 100f).ConfigureAwait(false); + await updateManager.DownloadUpdatesAsync(info, p => notification.Progress = p / 100f).ConfigureAwait(false); notification.StartInstall(); - await updateManager.ApplyReleases(info, p => notification.Progress = p / 100f).ConfigureAwait(false); - notification.State = ProgressNotificationState.Completed; - updatePending = true; } catch (Exception e) { - if (useDeltaPatching) - { - logger.Add(@"delta patching failed; will attempt full download!"); - - // could fail if deltas are unavailable for full update path (https://github.com/Squirrel/Squirrel.Windows/issues/959) - // try again without deltas. - await checkForUpdateAsync(false, notification).ConfigureAwait(false); - } - else - { // In the case of an error, a separate notification will be displayed. notification.FailDownload(); Logger.Error(e, @"update failed!"); - } } } catch (Exception) @@ -149,32 +117,12 @@ namespace osu.Desktop.Updater private bool restartToApplyUpdate() { - PrepareUpdateAsync() - .ContinueWith(_ => Schedule(() => game.AttemptExit())); + if (updateManager == null) + return false; + + updateManager.WaitExitThenApplyUpdates(null); + Schedule(() => game.AttemptExit()); return true; } - - protected override void Dispose(bool isDisposing) - { - base.Dispose(isDisposing); - updateManager?.Dispose(); - } - - private class SquirrelLogger : ILogger, IDisposable - { - public LogLevel Level { get; set; } = LogLevel.Info; - - public void Write(string message, LogLevel logLevel) - { - if (logLevel < Level) - return; - - logger.Add(message); - } - - public void Dispose() - { - } - } } } diff --git a/osu.Desktop/osu.Desktop.csproj b/osu.Desktop/osu.Desktop.csproj index e7a63bd921..7df82e1281 100644 --- a/osu.Desktop/osu.Desktop.csproj +++ b/osu.Desktop/osu.Desktop.csproj @@ -23,10 +23,10 @@ - + From 1025e5b3cc756b1cbd01216be157b3e0b270225f Mon Sep 17 00:00:00 2001 From: smallketchup82 Date: Wed, 26 Jun 2024 21:55:22 -0400 Subject: [PATCH 1197/2493] rewrite the restart function --- osu.Desktop/OsuGameDesktop.cs | 11 ++++++++--- .../Screens/Setup/TournamentSwitcher.cs | 3 +-- osu.Game/OsuGameBase.cs | 2 +- .../Settings/Sections/Graphics/RendererSettings.cs | 3 +-- 4 files changed, 11 insertions(+), 8 deletions(-) diff --git a/osu.Desktop/OsuGameDesktop.cs b/osu.Desktop/OsuGameDesktop.cs index 245d00dc53..3019aefdc0 100644 --- a/osu.Desktop/OsuGameDesktop.cs +++ b/osu.Desktop/OsuGameDesktop.cs @@ -105,18 +105,23 @@ namespace osu.Desktop return new VeloUpdateManager(); } - public override bool RestartAppWhenExited() + public override bool RestartApp() { try { - Process.Start(Process.GetCurrentProcess().MainModule?.FileName ?? throw new InvalidOperationException()); + var startInfo = new ProcessStartInfo + { + FileName = Process.GetCurrentProcess().MainModule!.FileName, + UseShellExecute = true + }; + Process.Start(startInfo); Environment.Exit(0); return true; } catch (Exception e) { Logger.Error(e, "Failed to restart application"); - return base.RestartAppWhenExited(); + return base.RestartApp(); } } diff --git a/osu.Game.Tournament/Screens/Setup/TournamentSwitcher.cs b/osu.Game.Tournament/Screens/Setup/TournamentSwitcher.cs index e55cbc2dbb..69ead451ba 100644 --- a/osu.Game.Tournament/Screens/Setup/TournamentSwitcher.cs +++ b/osu.Game.Tournament/Screens/Setup/TournamentSwitcher.cs @@ -31,8 +31,7 @@ namespace osu.Game.Tournament.Screens.Setup Action = () => { - game.RestartAppWhenExited(); - game.AttemptExit(); + game.RestartApp(); }; folderButton.Action = () => storage.PresentExternally(); diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 5533ee8337..db603f0046 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -513,7 +513,7 @@ namespace osu.Game /// If supported by the platform, the game will automatically restart after the next exit. /// /// Whether a restart operation was queued. - public virtual bool RestartAppWhenExited() => false; + public virtual bool RestartApp() => false; public bool Migrate(string path) { diff --git a/osu.Game/Overlays/Settings/Sections/Graphics/RendererSettings.cs b/osu.Game/Overlays/Settings/Sections/Graphics/RendererSettings.cs index a8b127d522..17e345c2c8 100644 --- a/osu.Game/Overlays/Settings/Sections/Graphics/RendererSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Graphics/RendererSettings.cs @@ -67,9 +67,8 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics if (r.NewValue == RendererType.Automatic && automaticRendererInUse) return; - if (game?.RestartAppWhenExited() == true) + if (game?.RestartApp() == true) { - game.AttemptExit(); } else { From bbacfc8d23622d87114afb2cdd982235bd6f3b87 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 27 Jun 2024 12:10:10 +0900 Subject: [PATCH 1198/2493] Add failing test coverage of osu!mania automated break creation scenarios --- .../TestSceneEditorBeatmapProcessor.cs | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/osu.Game.Tests/Editing/TestSceneEditorBeatmapProcessor.cs b/osu.Game.Tests/Editing/TestSceneEditorBeatmapProcessor.cs index 50f37e2070..3ec61cbf80 100644 --- a/osu.Game.Tests/Editing/TestSceneEditorBeatmapProcessor.cs +++ b/osu.Game.Tests/Editing/TestSceneEditorBeatmapProcessor.cs @@ -5,6 +5,8 @@ using NUnit.Framework; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.Timing; +using osu.Game.Rulesets.Mania; +using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Screens.Edit; @@ -74,6 +76,50 @@ namespace osu.Game.Tests.Editing Assert.That(beatmap.Breaks, Is.Empty); } + [Test] + public void TestHoldNote() + { + var controlPoints = new ControlPointInfo(); + controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 }); + var beatmap = new Beatmap + { + ControlPointInfo = controlPoints, + HitObjects = + { + new HoldNote { StartTime = 1000, Duration = 10000 }, + } + }; + + var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new ManiaRuleset()); + beatmapProcessor.PreProcess(); + beatmapProcessor.PostProcess(); + + Assert.That(beatmap.Breaks, Has.Count.EqualTo(0)); + } + + [Test] + public void TestHoldNoteWithOverlappingNote() + { + var controlPoints = new ControlPointInfo(); + controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 }); + var beatmap = new Beatmap + { + ControlPointInfo = controlPoints, + HitObjects = + { + new HoldNote { StartTime = 1000, Duration = 10000 }, + new Note { StartTime = 2000 }, + new Note { StartTime = 12000 }, + } + }; + + var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new ManiaRuleset()); + beatmapProcessor.PreProcess(); + beatmapProcessor.PostProcess(); + + Assert.That(beatmap.Breaks, Has.Count.EqualTo(0)); + } + [Test] public void TestTwoObjectsFarApart() { From 7ef7e5f1638dec9d798dd869ce429142bde27876 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 27 Jun 2024 12:10:26 +0900 Subject: [PATCH 1199/2493] Fix break generation not accounting for concurrent hitobjects correctly --- osu.Game/Screens/Edit/EditorBeatmapProcessor.cs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Edit/EditorBeatmapProcessor.cs b/osu.Game/Screens/Edit/EditorBeatmapProcessor.cs index bcbee78280..c3cb79c217 100644 --- a/osu.Game/Screens/Edit/EditorBeatmapProcessor.cs +++ b/osu.Game/Screens/Edit/EditorBeatmapProcessor.cs @@ -48,15 +48,20 @@ namespace osu.Game.Screens.Edit } } + double currentMaxEndTime = double.MinValue; + for (int i = 1; i < Beatmap.HitObjects.Count; ++i) { - double previousObjectEndTime = Beatmap.HitObjects[i - 1].GetEndTime(); + // Keep track of the maximum end time encountered thus far. + // This handles cases like osu!mania's hold notes, which could have concurrent other objects after their start time. + currentMaxEndTime = Math.Max(currentMaxEndTime, Beatmap.HitObjects[i - 1].GetEndTime()); + double nextObjectStartTime = Beatmap.HitObjects[i].StartTime; - if (nextObjectStartTime - previousObjectEndTime < BreakPeriod.MIN_GAP_DURATION) + if (nextObjectStartTime - currentMaxEndTime < BreakPeriod.MIN_GAP_DURATION) continue; - double breakStartTime = previousObjectEndTime + BreakPeriod.GAP_BEFORE_BREAK; + double breakStartTime = currentMaxEndTime + BreakPeriod.GAP_BEFORE_BREAK; double breakEndTime = nextObjectStartTime - Math.Max(BreakPeriod.GAP_AFTER_BREAK, Beatmap.ControlPointInfo.TimingPointAt(nextObjectStartTime).BeatLength * 2); if (breakEndTime - breakStartTime < BreakPeriod.MIN_BREAK_DURATION) From e4335a543eba89a4ceb75c854257acab40c341fd Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 27 Jun 2024 06:41:39 +0300 Subject: [PATCH 1200/2493] Add failing test case Includes a refactor of `ThemeComparisonTestScene` to allow accessing a manual input manager. --- .../Visual/Settings/TestSceneFileSelector.cs | 2 +- .../UserInterface/TestSceneOsuDropdown.cs | 69 ++++++++++--------- .../UserInterface/TestSceneRoundedButton.cs | 4 +- .../UserInterface/ThemeComparisonTestScene.cs | 41 +++++++---- 4 files changed, 70 insertions(+), 46 deletions(-) diff --git a/osu.Game.Tests/Visual/Settings/TestSceneFileSelector.cs b/osu.Game.Tests/Visual/Settings/TestSceneFileSelector.cs index e8f74a2f1b..c70277987e 100644 --- a/osu.Game.Tests/Visual/Settings/TestSceneFileSelector.cs +++ b/osu.Game.Tests/Visual/Settings/TestSceneFileSelector.cs @@ -21,7 +21,7 @@ namespace osu.Game.Tests.Visual.Settings { AddStep("create", () => { - Cell(0, 0).Children = new Drawable[] + ContentContainer.Children = new Drawable[] { new Box { diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneOsuDropdown.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneOsuDropdown.cs index 63f7a2f2cc..2f855c8744 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneOsuDropdown.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneOsuDropdown.cs @@ -6,23 +6,51 @@ using NUnit.Framework; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.UserInterface; -using osu.Framework.Input.Events; -using osu.Framework.Input.States; using osu.Framework.Testing; using osu.Game.Graphics.UserInterface; -using osu.Game.Input.Bindings; +using osuTK.Input; namespace osu.Game.Tests.Visual.UserInterface { public partial class TestSceneOsuDropdown : ThemeComparisonTestScene { - protected override Drawable CreateContent() => - new OsuEnumDropdown - { - Anchor = Anchor.Centre, - Origin = Anchor.TopCentre, - Width = 150 - }; + protected override Drawable CreateContent() => new OsuEnumDropdown + { + Anchor = Anchor.Centre, + Origin = Anchor.TopCentre, + Width = 150 + }; + + [Test] + public void TestBackAction() + { + AddStep("open", () => dropdownMenu.Open()); + AddStep("press back", () => InputManager.Key(Key.Escape)); + AddAssert("closed", () => dropdownMenu.State == MenuState.Closed); + + AddStep("open", () => dropdownMenu.Open()); + AddStep("type something", () => dropdownSearchBar.SearchTerm.Value = "something"); + AddAssert("search bar visible", () => dropdownSearchBar.State.Value == Visibility.Visible); + AddStep("press back", () => InputManager.Key(Key.Escape)); + AddAssert("text clear", () => dropdownSearchBar.SearchTerm.Value == string.Empty); + AddAssert("search bar hidden", () => dropdownSearchBar.State.Value == Visibility.Hidden); + AddAssert("still open", () => dropdownMenu.State == MenuState.Open); + AddStep("press back", () => InputManager.Key(Key.Escape)); + AddAssert("closed", () => dropdownMenu.State == MenuState.Closed); + } + + [Test] + public void TestSelectAction() + { + AddStep("open", () => dropdownMenu.Open()); + AddStep("press down", () => InputManager.Key(Key.Down)); + AddStep("press enter", () => InputManager.Key(Key.Enter)); + AddAssert("second selected", () => dropdown.Current.Value == TestEnum.ReallyLongOption); + } + + private OsuEnumDropdown dropdown => this.ChildrenOfType>().Last(); + private Menu dropdownMenu => dropdown.ChildrenOfType().Single(); + private DropdownSearchBar dropdownSearchBar => dropdown.ChildrenOfType().Single(); private enum TestEnum { @@ -32,26 +60,5 @@ namespace osu.Game.Tests.Visual.UserInterface [System.ComponentModel.Description("Really lonnnnnnng option")] ReallyLongOption, } - - [Test] - // todo: this can be written much better if ThemeComparisonTestScene has a manual input manager - public void TestBackAction() - { - AddStep("open", () => dropdown().ChildrenOfType().Single().Open()); - AddStep("press back", () => dropdown().OnPressed(new KeyBindingPressEvent(new InputState(), GlobalAction.Back))); - AddAssert("closed", () => dropdown().ChildrenOfType().Single().State == MenuState.Closed); - - AddStep("open", () => dropdown().ChildrenOfType().Single().Open()); - AddStep("type something", () => dropdown().ChildrenOfType().Single().SearchTerm.Value = "something"); - AddAssert("search bar visible", () => dropdown().ChildrenOfType().Single().State.Value == Visibility.Visible); - AddStep("press back", () => dropdown().OnPressed(new KeyBindingPressEvent(new InputState(), GlobalAction.Back))); - AddAssert("text clear", () => dropdown().ChildrenOfType().Single().SearchTerm.Value == string.Empty); - AddAssert("search bar hidden", () => dropdown().ChildrenOfType().Single().State.Value == Visibility.Hidden); - AddAssert("still open", () => dropdown().ChildrenOfType().Single().State == MenuState.Open); - AddStep("press back", () => dropdown().OnPressed(new KeyBindingPressEvent(new InputState(), GlobalAction.Back))); - AddAssert("closed", () => dropdown().ChildrenOfType().Single().State == MenuState.Closed); - - OsuEnumDropdown dropdown() => this.ChildrenOfType>().First(); - } } } diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneRoundedButton.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneRoundedButton.cs index 8c2651f71d..2d5c2c6d57 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneRoundedButton.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneRoundedButton.cs @@ -53,8 +53,8 @@ namespace osu.Game.Tests.Visual.UserInterface public void TestBackgroundColour() { AddStep("set red scheme", () => CreateThemedContent(OverlayColourScheme.Red)); - AddAssert("rounded button has correct colour", () => Cell(0, 1).ChildrenOfType().First().BackgroundColour == new OverlayColourProvider(OverlayColourScheme.Red).Colour3); - AddAssert("settings button has correct colour", () => Cell(0, 1).ChildrenOfType().First().BackgroundColour == new OverlayColourProvider(OverlayColourScheme.Red).Colour3); + AddAssert("rounded button has correct colour", () => ContentContainer.ChildrenOfType().First().BackgroundColour == new OverlayColourProvider(OverlayColourScheme.Red).Colour3); + AddAssert("settings button has correct colour", () => ContentContainer.ChildrenOfType().First().BackgroundColour == new OverlayColourProvider(OverlayColourScheme.Red).Colour3); } } } diff --git a/osu.Game.Tests/Visual/UserInterface/ThemeComparisonTestScene.cs b/osu.Game.Tests/Visual/UserInterface/ThemeComparisonTestScene.cs index 3177695f44..4700ef72d9 100644 --- a/osu.Game.Tests/Visual/UserInterface/ThemeComparisonTestScene.cs +++ b/osu.Game.Tests/Visual/UserInterface/ThemeComparisonTestScene.cs @@ -6,18 +6,21 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Game.Graphics; using osu.Game.Overlays; +using osuTK; namespace osu.Game.Tests.Visual.UserInterface { - public abstract partial class ThemeComparisonTestScene : OsuGridTestScene + public abstract partial class ThemeComparisonTestScene : OsuManualInputManagerTestScene { private readonly bool showWithoutColourProvider; + public Container ContentContainer { get; private set; } = null!; + protected ThemeComparisonTestScene(bool showWithoutColourProvider = true) - : base(1, showWithoutColourProvider ? 2 : 1) { this.showWithoutColourProvider = showWithoutColourProvider; } @@ -25,16 +28,32 @@ namespace osu.Game.Tests.Visual.UserInterface [BackgroundDependencyLoader] private void load(OsuColour colours) { + Child = ContentContainer = new Container + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + RelativeSizeAxes = Axes.Both, + }; + if (showWithoutColourProvider) { - Cell(0, 0).AddRange(new[] + ContentContainer.Size = new Vector2(0.5f, 1f); + + Add(new Container { - new Box + Anchor = Anchor.TopLeft, + Origin = Anchor.TopLeft, + RelativeSizeAxes = Axes.Both, + Size = new Vector2(0.5f, 1f), + Children = new[] { - RelativeSizeAxes = Axes.Both, - Colour = colours.GreySeaFoam - }, - CreateContent() + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colours.GreySeaFoam + }, + CreateContent() + } }); } } @@ -43,10 +62,8 @@ namespace osu.Game.Tests.Visual.UserInterface { var colourProvider = new OverlayColourProvider(colourScheme); - int col = showWithoutColourProvider ? 1 : 0; - - Cell(0, col).Clear(); - Cell(0, col).Add(new DependencyProvidingContainer + ContentContainer.Clear(); + ContentContainer.Add(new DependencyProvidingContainer { RelativeSizeAxes = Axes.Both, CachedDependencies = new (Type, object)[] From 811621325f2746df0947e673381351377ee4bfe7 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 27 Jun 2024 07:09:46 +0300 Subject: [PATCH 1201/2493] Fix osu! dropdown search text box having commits disabled I've also removed inheritance from `SearchTextBox` because it contains logic that might interfere with the internal implementation of dropdown search bars (focus logic and stuff). --- osu.Game/Graphics/UserInterface/OsuDropdown.cs | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/osu.Game/Graphics/UserInterface/OsuDropdown.cs b/osu.Game/Graphics/UserInterface/OsuDropdown.cs index 38e90bf4ea..c8bb45b59d 100644 --- a/osu.Game/Graphics/UserInterface/OsuDropdown.cs +++ b/osu.Game/Graphics/UserInterface/OsuDropdown.cs @@ -418,16 +418,19 @@ namespace osu.Game.Graphics.UserInterface FontSize = OsuFont.Default.Size, }; - private partial class DropdownSearchTextBox : SearchTextBox + private partial class DropdownSearchTextBox : OsuTextBox { - public override bool OnPressed(KeyBindingPressEvent e) + [BackgroundDependencyLoader] + private void load(OverlayColourProvider? colourProvider) { - if (e.Action == GlobalAction.Back) - // this method is blocking Dropdown from receiving the back action, despite this text box residing in a separate input manager. - // to fix this properly, a local global action container needs to be added as well, but for simplicity, just don't handle the back action here. - return false; + BackgroundUnfocused = colourProvider?.Background5 ?? new Color4(10, 10, 10, 255); + BackgroundFocused = colourProvider?.Background5 ?? new Color4(10, 10, 10, 255); + } - return base.OnPressed(e); + protected override void OnFocus(FocusEvent e) + { + base.OnFocus(e); + BorderThickness = 0; } } } From 981340debec29090e7c172479ccffe17b7cf2983 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 27 Jun 2024 07:45:14 +0200 Subject: [PATCH 1202/2493] Add safety test coverage for removal of breaks at end of beatmap --- .../TestSceneEditorBeatmapProcessor.cs | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/osu.Game.Tests/Editing/TestSceneEditorBeatmapProcessor.cs b/osu.Game.Tests/Editing/TestSceneEditorBeatmapProcessor.cs index 3ec61cbf80..1a3f0aa3df 100644 --- a/osu.Game.Tests/Editing/TestSceneEditorBeatmapProcessor.cs +++ b/osu.Game.Tests/Editing/TestSceneEditorBeatmapProcessor.cs @@ -395,6 +395,32 @@ namespace osu.Game.Tests.Editing Assert.That(beatmap.Breaks, Is.Empty); } + [Test] + public void TestManualBreaksAtEndOfBeatmapAreRemovedCorrectlyEvenWithConcurrentObjects() + { + var controlPoints = new ControlPointInfo(); + controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 }); + var beatmap = new Beatmap + { + ControlPointInfo = controlPoints, + HitObjects = + { + new HoldNote { StartTime = 1000, EndTime = 20000 }, + new HoldNote { StartTime = 2000, EndTime = 3000 }, + }, + Breaks = + { + new ManualBreakPeriod(10000, 15000), + } + }; + + var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new OsuRuleset()); + beatmapProcessor.PreProcess(); + beatmapProcessor.PostProcess(); + + Assert.That(beatmap.Breaks, Is.Empty); + } + [Test] public void TestBreaksAtStartOfBeatmapAreRemoved() { From ef952bcd65d527ac792c107a55b2cdcb0b904969 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 27 Jun 2024 07:48:05 +0200 Subject: [PATCH 1203/2493] Use `GetLastObjectTime()` for safety Due to other circumstances this has no real effect, but may as well. --- osu.Game/Screens/Edit/EditorBeatmapProcessor.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/EditorBeatmapProcessor.cs b/osu.Game/Screens/Edit/EditorBeatmapProcessor.cs index c3cb79c217..87e5d92b25 100644 --- a/osu.Game/Screens/Edit/EditorBeatmapProcessor.cs +++ b/osu.Game/Screens/Edit/EditorBeatmapProcessor.cs @@ -41,7 +41,7 @@ namespace osu.Game.Screens.Edit foreach (var manualBreak in Beatmap.Breaks.ToList()) { if (manualBreak.EndTime <= Beatmap.HitObjects.FirstOrDefault()?.StartTime - || manualBreak.StartTime >= Beatmap.HitObjects.LastOrDefault()?.GetEndTime() + || manualBreak.StartTime >= Beatmap.GetLastObjectTime() || Beatmap.HitObjects.Any(ho => ho.StartTime <= manualBreak.EndTime && ho.GetEndTime() >= manualBreak.StartTime)) { Beatmap.Breaks.Remove(manualBreak); From b1baa49459b388f95652e685dcf55e157f29d851 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 27 Jun 2024 07:56:57 +0200 Subject: [PATCH 1204/2493] Add note about implicit reliance on sort by start time --- osu.Game/Screens/Edit/EditorBeatmapProcessor.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Screens/Edit/EditorBeatmapProcessor.cs b/osu.Game/Screens/Edit/EditorBeatmapProcessor.cs index 87e5d92b25..99c8c3572b 100644 --- a/osu.Game/Screens/Edit/EditorBeatmapProcessor.cs +++ b/osu.Game/Screens/Edit/EditorBeatmapProcessor.cs @@ -54,6 +54,8 @@ namespace osu.Game.Screens.Edit { // Keep track of the maximum end time encountered thus far. // This handles cases like osu!mania's hold notes, which could have concurrent other objects after their start time. + // Note that we're relying on the implicit assumption that objects are sorted by start time, + // which is why similar tracking is not done for start time. currentMaxEndTime = Math.Max(currentMaxEndTime, Beatmap.HitObjects[i - 1].GetEndTime()); double nextObjectStartTime = Beatmap.HitObjects[i].StartTime; From 779d2e81723425353b5ce0ffe598d7becdea4c8b Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 27 Jun 2024 16:00:22 +0900 Subject: [PATCH 1205/2493] Support increased visibility for first object with traceable mod --- osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs index 75ad00e169..9091837034 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs @@ -6,7 +6,9 @@ using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Localisation; using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Rulesets.Osu.Skinning.Default; @@ -23,6 +25,8 @@ namespace osu.Game.Rulesets.Osu.Mods public override Type[] IncompatibleMods => new[] { typeof(IHidesApproachCircles), typeof(OsuModDepth) }; + protected override bool IsFirstAdjustableObject(HitObject hitObject) => !(hitObject is Spinner || hitObject is SpinnerTick); + protected override void ApplyIncreasedVisibilityState(DrawableHitObject hitObject, ArmedState state) { } From a5aedded167f53f55ecf195521b6703e7302484b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 27 Jun 2024 09:44:55 +0200 Subject: [PATCH 1206/2493] Remove commented code --- osu.Game/Screens/Edit/Timing/ControlPointList.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game/Screens/Edit/Timing/ControlPointList.cs b/osu.Game/Screens/Edit/Timing/ControlPointList.cs index a0d833c908..b7367dddda 100644 --- a/osu.Game/Screens/Edit/Timing/ControlPointList.cs +++ b/osu.Game/Screens/Edit/Timing/ControlPointList.cs @@ -85,8 +85,6 @@ namespace osu.Game.Screens.Edit.Timing ? "+ Clone to current time" : "+ Add at current time"; }, true); - - //table.OnRowSelected += drawable => scroll.ScrollIntoView(drawable); } protected override bool OnClick(ClickEvent e) From 9384cbcdd80aa5a7126be2aadf480f065f20cf67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 27 Jun 2024 09:46:35 +0200 Subject: [PATCH 1207/2493] Fix scroll-into-view on control point table not working as it is supposed to --- osu.Game/Screens/Edit/Timing/ControlPointTable.cs | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Edit/Timing/ControlPointTable.cs b/osu.Game/Screens/Edit/Timing/ControlPointTable.cs index 3bb801f471..75b86650af 100644 --- a/osu.Game/Screens/Edit/Timing/ControlPointTable.cs +++ b/osu.Game/Screens/Edit/Timing/ControlPointTable.cs @@ -100,9 +100,20 @@ namespace osu.Game.Screens.Edit.Timing selectedGroup.BindValueChanged(val => { + // can't use `.ScrollIntoView()` here because of the list virtualisation not giving + // child items valid coordinates from the start, so ballpark something similar + // using estimated row height. var row = Items.FlowingChildren.SingleOrDefault(item => item.Row.Equals(val.NewValue)); - if (row != null) - Scroll.ScrollIntoView(row); + if (row == null) + return; + + float minPos = Items.GetLayoutPosition(row) * row_height; + float maxPos = minPos + row_height; + + if (minPos < Scroll.Current) + Scroll.ScrollTo(minPos); + else if (maxPos > Scroll.Current + Scroll.DisplayableContent) + Scroll.ScrollTo(maxPos - Scroll.DisplayableContent); }); } } From 9e07c8fff7dc921673689dfcc085b7ce49361afb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 27 Jun 2024 10:31:24 +0200 Subject: [PATCH 1208/2493] Update framework again --- 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 e2b7fe479a..349829555d 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -10,7 +10,7 @@ true - + diff --git a/osu.iOS.props b/osu.iOS.props index ddde2a3cec..8e8bac53b9 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -23,6 +23,6 @@ iossimulator-x64 - + From d6e7781be13821b52baafd1a06cb003344b43341 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 27 Jun 2024 10:44:28 +0200 Subject: [PATCH 1209/2493] Add client/server models for allowing clients to receive realtime playlist updates --- osu.Game/Online/Metadata/IMetadataClient.cs | 6 +++ osu.Game/Online/Metadata/IMetadataServer.cs | 10 ++++ osu.Game/Online/Metadata/MetadataClient.cs | 18 +++++++ .../Metadata/MultiplayerPlaylistItemStats.cs | 29 +++++++++++ .../Metadata/MultiplayerRoomScoreSetEvent.cs | 50 +++++++++++++++++++ .../Online/Metadata/OnlineMetadataClient.cs | 19 +++++++ .../DailyChallengeScoreBreakdown.cs | 3 +- .../Visual/Metadata/TestMetadataClient.cs | 5 ++ 8 files changed, 139 insertions(+), 1 deletion(-) create mode 100644 osu.Game/Online/Metadata/MultiplayerPlaylistItemStats.cs create mode 100644 osu.Game/Online/Metadata/MultiplayerRoomScoreSetEvent.cs diff --git a/osu.Game/Online/Metadata/IMetadataClient.cs b/osu.Game/Online/Metadata/IMetadataClient.cs index ee7a726bfc..97c1bbde5f 100644 --- a/osu.Game/Online/Metadata/IMetadataClient.cs +++ b/osu.Game/Online/Metadata/IMetadataClient.cs @@ -26,5 +26,11 @@ namespace osu.Game.Online.Metadata /// Null value means there is no "daily challenge" currently active. /// Task DailyChallengeUpdated(DailyChallengeInfo? info); + + /// + /// Delivers information that a multiplayer score was set in a watched room. + /// To receive these, the client must call for a given room first. + /// + Task MultiplayerRoomScoreSet(MultiplayerRoomScoreSetEvent roomScoreSetEvent); } } diff --git a/osu.Game/Online/Metadata/IMetadataServer.cs b/osu.Game/Online/Metadata/IMetadataServer.cs index 8bf3f8f56b..79ed8b5634 100644 --- a/osu.Game/Online/Metadata/IMetadataServer.cs +++ b/osu.Game/Online/Metadata/IMetadataServer.cs @@ -43,5 +43,15 @@ namespace osu.Game.Online.Metadata /// Signals to the server that the current user would like to stop receiving updates on other users' online presence. /// Task EndWatchingUserPresence(); + + /// + /// Signals to the server that the current user would like to begin receiving updates about the state of the multiplayer room with the given . + /// + Task BeginWatchingMultiplayerRoom(long id); + + /// + /// Signals to the server that the current user would like to stop receiving updates about the state of the multiplayer room with the given . + /// + Task EndWatchingMultiplayerRoom(long id); } } diff --git a/osu.Game/Online/Metadata/MetadataClient.cs b/osu.Game/Online/Metadata/MetadataClient.cs index b619970494..8a5fe1733e 100644 --- a/osu.Game/Online/Metadata/MetadataClient.cs +++ b/osu.Game/Online/Metadata/MetadataClient.cs @@ -68,6 +68,24 @@ namespace osu.Game.Online.Metadata #endregion + #region Multiplayer room watching + + public abstract Task BeginWatchingMultiplayerRoom(long id); + + public abstract Task EndWatchingMultiplayerRoom(long id); + + public event Action? MultiplayerRoomScoreSet; + + Task IMetadataClient.MultiplayerRoomScoreSet(MultiplayerRoomScoreSetEvent roomScoreSetEvent) + { + if (MultiplayerRoomScoreSet != null) + Schedule(MultiplayerRoomScoreSet, roomScoreSetEvent); + + return Task.CompletedTask; + } + + #endregion + #region Disconnection handling public event Action? Disconnecting; diff --git a/osu.Game/Online/Metadata/MultiplayerPlaylistItemStats.cs b/osu.Game/Online/Metadata/MultiplayerPlaylistItemStats.cs new file mode 100644 index 0000000000..d13705bf5b --- /dev/null +++ b/osu.Game/Online/Metadata/MultiplayerPlaylistItemStats.cs @@ -0,0 +1,29 @@ +// 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 MessagePack; + +namespace osu.Game.Online.Metadata +{ + [MessagePackObject] + [Serializable] + public class MultiplayerPlaylistItemStats + { + public const int TOTAL_SCORE_DISTRIBUTION_BINS = 13; + + /// + /// The ID of the playlist item which these stats pertain to. + /// + [Key(0)] + public long PlaylistItemID { get; set; } + + /// + /// The count of scores with given total ranges in the room. + /// The ranges are bracketed into bins, each of 100,000 score width. + /// The last bin will contain count of all scores with total of 1,200,000 or larger. + /// + [Key(1)] + public long[] TotalScoreDistribution { get; set; } = new long[TOTAL_SCORE_DISTRIBUTION_BINS]; + } +} diff --git a/osu.Game/Online/Metadata/MultiplayerRoomScoreSetEvent.cs b/osu.Game/Online/Metadata/MultiplayerRoomScoreSetEvent.cs new file mode 100644 index 0000000000..00bc5dc840 --- /dev/null +++ b/osu.Game/Online/Metadata/MultiplayerRoomScoreSetEvent.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; +using MessagePack; + +namespace osu.Game.Online.Metadata +{ + [Serializable] + [MessagePackObject] + public class MultiplayerRoomScoreSetEvent + { + /// + /// The ID of the room in which the score was set. + /// + [Key(0)] + public long RoomID { get; set; } + + /// + /// The ID of the playlist item on which the score was set. + /// + [Key(1)] + public long PlaylistItemID { get; set; } + + /// + /// The ID of the score set. + /// + [Key(2)] + public long ScoreID { get; set; } + + /// + /// The ID of the user who set the score. + /// + [Key(3)] + public int UserID { get; set; } + + /// + /// The total score set by the player. + /// + [Key(4)] + public long TotalScore { get; set; } + + /// + /// If the set score is the user's new best on a playlist item, this member will contain the user's new rank in the room overall. + /// Otherwise, it will contain . + /// + [Key(5)] + public int? NewRank { get; set; } + } +} diff --git a/osu.Game/Online/Metadata/OnlineMetadataClient.cs b/osu.Game/Online/Metadata/OnlineMetadataClient.cs index b94f26a71d..80fcf7571d 100644 --- a/osu.Game/Online/Metadata/OnlineMetadataClient.cs +++ b/osu.Game/Online/Metadata/OnlineMetadataClient.cs @@ -62,6 +62,7 @@ namespace osu.Game.Online.Metadata connection.On(nameof(IMetadataClient.BeatmapSetsUpdated), ((IMetadataClient)this).BeatmapSetsUpdated); connection.On(nameof(IMetadataClient.UserPresenceUpdated), ((IMetadataClient)this).UserPresenceUpdated); connection.On(nameof(IMetadataClient.DailyChallengeUpdated), ((IMetadataClient)this).DailyChallengeUpdated); + connection.On(nameof(IMetadataClient.MultiplayerRoomScoreSet), ((IMetadataClient)this).MultiplayerRoomScoreSet); connection.On(nameof(IStatefulUserHubClient.DisconnectRequested), ((IMetadataClient)this).DisconnectRequested); }; @@ -240,6 +241,24 @@ namespace osu.Game.Online.Metadata return Task.CompletedTask; } + public override async Task BeginWatchingMultiplayerRoom(long id) + { + if (connector?.IsConnected.Value != true) + throw new OperationCanceledException(); + + Debug.Assert(connection != null); + return await connection.InvokeAsync(nameof(IMetadataServer.BeginWatchingMultiplayerRoom), id).ConfigureAwait(false); + } + + public override async Task EndWatchingMultiplayerRoom(long id) + { + if (connector?.IsConnected.Value != true) + throw new OperationCanceledException(); + + Debug.Assert(connection != null); + await connection.InvokeAsync(nameof(IMetadataServer.EndWatchingMultiplayerRoom)).ConfigureAwait(false); + } + public override async Task DisconnectRequested() { await base.DisconnectRequested().ConfigureAwait(false); diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeScoreBreakdown.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeScoreBreakdown.cs index 3d4f27c44b..d251a10f9a 100644 --- a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeScoreBreakdown.cs +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeScoreBreakdown.cs @@ -11,6 +11,7 @@ using osu.Framework.Localisation; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; +using osu.Game.Online.Metadata; using osu.Game.Overlays; using osu.Game.Scoring; using osuTK; @@ -21,7 +22,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge { private FillFlowContainer barsContainer = null!; - private const int bin_count = 13; + private const int bin_count = MultiplayerPlaylistItemStats.TOTAL_SCORE_DISTRIBUTION_BINS; private long[] bins = new long[bin_count]; [BackgroundDependencyLoader] diff --git a/osu.Game/Tests/Visual/Metadata/TestMetadataClient.cs b/osu.Game/Tests/Visual/Metadata/TestMetadataClient.cs index b589e66d8b..fa64a83352 100644 --- a/osu.Game/Tests/Visual/Metadata/TestMetadataClient.cs +++ b/osu.Game/Tests/Visual/Metadata/TestMetadataClient.cs @@ -86,5 +86,10 @@ namespace osu.Game.Tests.Visual.Metadata dailyChallengeInfo.Value = info; return Task.CompletedTask; } + + public override Task BeginWatchingMultiplayerRoom(long id) + => Task.FromResult(new MultiplayerPlaylistItemStats[MultiplayerPlaylistItemStats.TOTAL_SCORE_DISTRIBUTION_BINS]); + + public override Task EndWatchingMultiplayerRoom(long id) => Task.CompletedTask; } } From 29412bb29b8e311b3ba825736f21729cc3f3c6e9 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Thu, 27 Jun 2024 12:22:00 +0200 Subject: [PATCH 1210/2493] Fix editor setting arbitrary beat divisor --- osu.Game/Screens/Edit/Editor.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index e91777eab2..22fc6fccdc 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -292,7 +292,7 @@ namespace osu.Game.Screens.Edit dependencies.CacheAs(changeHandler); } - beatDivisor.Value = editorBeatmap.BeatmapInfo.BeatDivisor; + beatDivisor.SetArbitraryDivisor(editorBeatmap.BeatmapInfo.BeatDivisor); beatDivisor.BindValueChanged(divisor => editorBeatmap.BeatmapInfo.BeatDivisor = divisor.NewValue); updateLastSavedHash(); From 772a68cb3e3e808317dae49cd443dc9b2803d4c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 27 Jun 2024 12:38:29 +0200 Subject: [PATCH 1211/2493] Add test coverage for correct beat divisor save --- .../Visual/Editing/TestSceneEditorSaving.cs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs index 64c48e74cf..b487fa3cec 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs @@ -193,5 +193,20 @@ namespace osu.Game.Tests.Visual.Editing AddUntilStep("Wait for song select", () => Game.ScreenStack.CurrentScreen is PlaySongSelect); AddAssert("Tags reverted correctly", () => Game.Beatmap.Value.BeatmapInfo.Metadata.Tags == tags_to_save); } + + [Test] + public void TestBeatDivisor() + { + AddStep("Set custom beat divisor", () => Editor.Dependencies.Get().SetArbitraryDivisor(7)); + + SaveEditor(); + AddAssert("Hash updated", () => !string.IsNullOrEmpty(EditorBeatmap.BeatmapInfo.BeatmapSet?.Hash)); + AddAssert("Beatmap has correct beat divisor", () => EditorBeatmap.BeatmapInfo.BeatDivisor, () => Is.EqualTo(7)); + + ReloadEditorToSameBeatmap(); + + AddAssert("Beatmap still has correct beat divisor", () => EditorBeatmap.BeatmapInfo.BeatDivisor, () => Is.EqualTo(7)); + AddAssert("Correct beat divisor actually active", () => Editor.BeatDivisor, () => Is.EqualTo(7)); + } } } From 1b741dada30dc3d68db3e85180f4327e596a792d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 27 Jun 2024 14:46:57 +0200 Subject: [PATCH 1212/2493] Show distance in pixels to previous/next object in osu! hitobject inspector --- .../Edit/OsuHitObjectComposer.cs | 5 +++ .../Edit/OsuHitObjectInspector.cs | 42 +++++++++++++++++++ .../Compose/Components/EditorInspector.cs | 2 +- .../Compose/Components/HitObjectInspector.cs | 15 +++++-- 4 files changed, 59 insertions(+), 5 deletions(-) create mode 100644 osu.Game.Rulesets.Osu/Edit/OsuHitObjectInspector.cs diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs index 41f6b41f82..cf867b4795 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs @@ -101,8 +101,13 @@ namespace osu.Game.Rulesets.Osu.Edit updatePositionSnapGrid(); + RightToolbox.Clear(); RightToolbox.AddRange(new EditorToolboxGroup[] { + new EditorToolboxGroup("inspector") + { + Child = new OsuHitObjectInspector(), + }, OsuGridToolboxGroup, new TransformToolboxGroup { diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectInspector.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectInspector.cs new file mode 100644 index 0000000000..27e7d5497c --- /dev/null +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectInspector.cs @@ -0,0 +1,42 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Diagnostics; +using System.Linq; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Screens.Edit.Compose.Components; + +namespace osu.Game.Rulesets.Osu.Edit +{ + public partial class OsuHitObjectInspector : HitObjectInspector + { + protected override void AddInspectorValues() + { + base.AddInspectorValues(); + + if (EditorBeatmap.SelectedHitObjects.Count > 0) + { + var firstInSelection = (OsuHitObject)EditorBeatmap.SelectedHitObjects.MinBy(ho => ho.StartTime)!; + var lastInSelection = (OsuHitObject)EditorBeatmap.SelectedHitObjects.MaxBy(ho => ho.GetEndTime())!; + + Debug.Assert(firstInSelection != null && lastInSelection != null); + + var precedingObject = (OsuHitObject?)EditorBeatmap.HitObjects.LastOrDefault(ho => ho.GetEndTime() < firstInSelection.StartTime); + var nextObject = (OsuHitObject?)EditorBeatmap.HitObjects.FirstOrDefault(ho => ho.StartTime > lastInSelection.GetEndTime()); + + if (precedingObject != null && precedingObject is not Spinner) + { + AddHeader("To previous"); + AddValue($"{(firstInSelection.StackedPosition - precedingObject.StackedEndPosition).Length:#,0.##}px"); + } + + if (nextObject != null && nextObject is not Spinner) + { + AddHeader("To next"); + AddValue($"{(nextObject.StackedPosition - lastInSelection.StackedEndPosition).Length:#,0.##}px"); + } + } + } + } +} diff --git a/osu.Game/Screens/Edit/Compose/Components/EditorInspector.cs b/osu.Game/Screens/Edit/Compose/Components/EditorInspector.cs index 442454f97a..5837dd7946 100644 --- a/osu.Game/Screens/Edit/Compose/Components/EditorInspector.cs +++ b/osu.Game/Screens/Edit/Compose/Components/EditorInspector.cs @@ -10,7 +10,7 @@ using osu.Game.Overlays; namespace osu.Game.Screens.Edit.Compose.Components { - internal partial class EditorInspector : CompositeDrawable + public partial class EditorInspector : CompositeDrawable { protected OsuTextFlowContainer InspectorText = null!; diff --git a/osu.Game/Screens/Edit/Compose/Components/HitObjectInspector.cs b/osu.Game/Screens/Edit/Compose/Components/HitObjectInspector.cs index ac339dc9d9..2f19888e9e 100644 --- a/osu.Game/Screens/Edit/Compose/Components/HitObjectInspector.cs +++ b/osu.Game/Screens/Edit/Compose/Components/HitObjectInspector.cs @@ -9,7 +9,7 @@ using osu.Game.Rulesets.Objects.Types; namespace osu.Game.Screens.Edit.Compose.Components { - internal partial class HitObjectInspector : EditorInspector + public partial class HitObjectInspector : EditorInspector { protected override void LoadComplete() { @@ -29,6 +29,16 @@ namespace osu.Game.Screens.Edit.Compose.Components rollingTextUpdate?.Cancel(); rollingTextUpdate = null; + AddInspectorValues(); + + // I'd hope there's a better way to do this, but I don't want to bind to each and every property above to watch for changes. + // This is a good middle-ground for the time being. + if (EditorBeatmap.SelectedHitObjects.Count > 0) + rollingTextUpdate ??= Scheduler.AddDelayed(updateInspectorText, 250); + } + + protected virtual void AddInspectorValues() + { switch (EditorBeatmap.SelectedHitObjects.Count) { case 0: @@ -90,9 +100,6 @@ namespace osu.Game.Screens.Edit.Compose.Components AddValue($"{duration.Duration:#,0.##}ms"); } - // I'd hope there's a better way to do this, but I don't want to bind to each and every property above to watch for changes. - // This is a good middle-ground for the time being. - rollingTextUpdate ??= Scheduler.AddDelayed(updateInspectorText, 250); break; default: From b293eb7930c7ae213167ce9b9e2cccd8aec2a2c7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 27 Jun 2024 22:19:06 +0900 Subject: [PATCH 1213/2493] Remove redundant array spec --- osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs index cf867b4795..ecebf6d7b1 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs @@ -102,7 +102,7 @@ namespace osu.Game.Rulesets.Osu.Edit updatePositionSnapGrid(); RightToolbox.Clear(); - RightToolbox.AddRange(new EditorToolboxGroup[] + RightToolbox.AddRange(new[] { new EditorToolboxGroup("inspector") { From fd6b77ea9295041500bb1316cd3cedd87e32ed38 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 27 Jun 2024 23:54:48 +0900 Subject: [PATCH 1214/2493] Fix distance snap control being removed --- osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs | 9 +++------ osu.Game/Rulesets/Edit/HitObjectComposer.cs | 4 +++- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs index ecebf6d7b1..f93874481d 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs @@ -51,6 +51,8 @@ namespace osu.Game.Rulesets.Osu.Edit private readonly Bindable rectangularGridSnapToggle = new Bindable(); + protected override Drawable CreateHitObjectInspector() => new OsuHitObjectInspector(); + protected override IEnumerable CreateTernaryButtons() => base.CreateTernaryButtons() .Concat(DistanceSnapProvider.CreateTernaryButtons()) @@ -101,13 +103,8 @@ namespace osu.Game.Rulesets.Osu.Edit updatePositionSnapGrid(); - RightToolbox.Clear(); - RightToolbox.AddRange(new[] + RightToolbox.AddRange(new Drawable[] { - new EditorToolboxGroup("inspector") - { - Child = new OsuHitObjectInspector(), - }, OsuGridToolboxGroup, new TransformToolboxGroup { diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs index 7d593c03ec..540e0440c6 100644 --- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs @@ -207,7 +207,7 @@ namespace osu.Game.Rulesets.Edit { Child = new EditorToolboxGroup("inspector") { - Child = new HitObjectInspector() + Child = CreateHitObjectInspector() }, } } @@ -329,6 +329,8 @@ namespace osu.Game.Rulesets.Edit /// protected virtual ComposeBlueprintContainer CreateBlueprintContainer() => new ComposeBlueprintContainer(this); + protected virtual Drawable CreateHitObjectInspector() => new HitObjectInspector(); + /// /// Construct a drawable ruleset for the provided ruleset. /// From 36a3765ee4e2ef3240a265549ebc6a734f696c62 Mon Sep 17 00:00:00 2001 From: smallketchup82 Date: Thu, 27 Jun 2024 12:57:24 -0400 Subject: [PATCH 1215/2493] Replace with attemptexit to better display how restarting is borked --- osu.Desktop/OsuGameDesktop.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Desktop/OsuGameDesktop.cs b/osu.Desktop/OsuGameDesktop.cs index b6053edf77..28f3d3dc5d 100644 --- a/osu.Desktop/OsuGameDesktop.cs +++ b/osu.Desktop/OsuGameDesktop.cs @@ -114,7 +114,7 @@ namespace osu.Desktop UseShellExecute = true }; Process.Start(startInfo); - Environment.Exit(0); + base.AttemptExit(); return true; } catch (Exception e) From 167ffac21890cf959f159ccd2a63c72d37453346 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 28 Jun 2024 06:35:12 +0300 Subject: [PATCH 1216/2493] Reorder container layout for popovers to recognize clicks on customisation panel Basically moves `PopoverContainer` to cover both the columns and the customisation panel, so that if the customisation panel is clicked on, the popover container will notice that and hide popovers like the mod preset popover. --- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 100 +++++++++++---------- 1 file changed, 52 insertions(+), 48 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index 56e0a88b5a..f447f6f722 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -162,64 +162,68 @@ namespace osu.Game.Overlays.Mods columnAppearSample = audio.Samples.Get(@"SongSelect/mod-column-pop-in"); - MainAreaContent.AddRange(new Drawable[] + MainAreaContent.Add(new OsuContextMenuContainer { - new OsuContextMenuContainer + RelativeSizeAxes = Axes.Both, + Child = new PopoverContainer { RelativeSizeAxes = Axes.Both, - Child = new PopoverContainer + Children = new Drawable[] { - Padding = new MarginPadding + new Container { - Top = RankingInformationDisplay.HEIGHT + PADDING, - Bottom = PADDING - }, - RelativeSizeAxes = Axes.Both, - RelativePositionAxes = Axes.Both, - Children = new Drawable[] - { - columnScroll = new ColumnScrollContainer + Padding = new MarginPadding { - RelativeSizeAxes = Axes.Both, - Masking = false, - ClampExtension = 100, - ScrollbarOverlapsContent = false, - Child = columnFlow = new ColumnFlowContainer + Top = RankingInformationDisplay.HEIGHT + PADDING, + Bottom = PADDING + }, + RelativeSizeAxes = Axes.Both, + RelativePositionAxes = Axes.Both, + Children = new Drawable[] + { + columnScroll = new ColumnScrollContainer { - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - Direction = FillDirection.Horizontal, - Shear = new Vector2(OsuGame.SHEAR, 0), - RelativeSizeAxes = Axes.Y, - AutoSizeAxes = Axes.X, - Margin = new MarginPadding { Horizontal = 70 }, - Padding = new MarginPadding { Bottom = 10 }, - ChildrenEnumerable = createColumns() + RelativeSizeAxes = Axes.Both, + Masking = false, + ClampExtension = 100, + ScrollbarOverlapsContent = false, + Child = columnFlow = new ColumnFlowContainer + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Direction = FillDirection.Horizontal, + Shear = new Vector2(OsuGame.SHEAR, 0), + RelativeSizeAxes = Axes.Y, + AutoSizeAxes = Axes.X, + Margin = new MarginPadding { Horizontal = 70 }, + Padding = new MarginPadding { Bottom = 10 }, + ChildrenEnumerable = createColumns() + } + } + } + }, + aboveColumnsContent = new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Horizontal = 100, Bottom = 15f }, + Children = new Drawable[] + { + SearchTextBox = new ShearedSearchTextBox + { + HoldFocus = false, + Width = 300, + }, + customisationPanel = new ModCustomisationPanel + { + Alpha = 0f, + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + Width = 400, } } } - } - }, - aboveColumnsContent = new Container - { - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding { Horizontal = 100, Bottom = 15f }, - Children = new Drawable[] - { - SearchTextBox = new ShearedSearchTextBox - { - HoldFocus = false, - Width = 300, - }, - customisationPanel = new ModCustomisationPanel - { - Alpha = 0f, - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - Width = 400, - } - } - }, + }, + } }); FooterContent.Add(footerButtonFlow = new FillFlowContainer From 28d52789350ffe240ffe23f8df49e7ad3a2a8bf5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 28 Jun 2024 13:00:30 +0900 Subject: [PATCH 1217/2493] Show preset description text in tooltip popup As proposed in https://github.com/ppy/osu/discussions/28610. --- osu.Game/Overlays/Mods/ModPresetTooltip.cs | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModPresetTooltip.cs b/osu.Game/Overlays/Mods/ModPresetTooltip.cs index 077bd14751..501b56e2fd 100644 --- a/osu.Game/Overlays/Mods/ModPresetTooltip.cs +++ b/osu.Game/Overlays/Mods/ModPresetTooltip.cs @@ -6,6 +6,8 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Shapes; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; using osu.Game.Rulesets.Mods; using osuTK; @@ -17,6 +19,8 @@ namespace osu.Game.Overlays.Mods private const double transition_duration = 200; + private readonly OsuSpriteText descriptionText; + public ModPresetTooltip(OverlayColourProvider colourProvider) { Width = 250; @@ -37,7 +41,15 @@ namespace osu.Game.Overlays.Mods RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Padding = new MarginPadding(7), - Spacing = new Vector2(7) + Spacing = new Vector2(7), + Children = new[] + { + descriptionText = new OsuSpriteText + { + Font = OsuFont.GetFont(weight: FontWeight.Regular), + Colour = colourProvider.Content2, + }, + } } }; } @@ -49,8 +61,12 @@ namespace osu.Game.Overlays.Mods if (ReferenceEquals(preset, lastPreset)) return; + descriptionText.Text = preset.Description; + lastPreset = preset; - Content.ChildrenEnumerable = preset.Mods.AsOrdered().Select(mod => new ModPresetRow(mod)); + + Content.RemoveAll(d => d is ModPresetRow, true); + Content.AddRange(preset.Mods.AsOrdered().Select(mod => new ModPresetRow(mod))); } protected override void PopIn() => this.FadeIn(transition_duration, Easing.OutQuint); From d370f50cc1234bf8422fb925d1ffb6fde09145ec Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 28 Jun 2024 13:16:47 +0900 Subject: [PATCH 1218/2493] Syncrhronise colours across mod and preset tooltips --- .../Mods/IncompatibilityDisplayingModPanel.cs | 5 ++++- .../Mods/IncompatibilityDisplayingTooltip.cs | 10 +++------ osu.Game/Overlays/Mods/ModButtonTooltip.cs | 21 ++++++------------- osu.Game/Overlays/Mods/ModPresetTooltip.cs | 4 ++-- 4 files changed, 15 insertions(+), 25 deletions(-) diff --git a/osu.Game/Overlays/Mods/IncompatibilityDisplayingModPanel.cs b/osu.Game/Overlays/Mods/IncompatibilityDisplayingModPanel.cs index 26c5b2ac49..84336319b7 100644 --- a/osu.Game/Overlays/Mods/IncompatibilityDisplayingModPanel.cs +++ b/osu.Game/Overlays/Mods/IncompatibilityDisplayingModPanel.cs @@ -16,6 +16,9 @@ namespace osu.Game.Overlays.Mods { private readonly BindableBool incompatible = new BindableBool(); + [Resolved] + private OverlayColourProvider overlayColourProvider { get; set; } = null!; + [Resolved] private Bindable> selectedMods { get; set; } = null!; @@ -55,7 +58,7 @@ namespace osu.Game.Overlays.Mods #region IHasCustomTooltip - public ITooltip GetCustomTooltip() => new IncompatibilityDisplayingTooltip(); + public ITooltip GetCustomTooltip() => new IncompatibilityDisplayingTooltip(overlayColourProvider); public Mod TooltipContent => Mod; diff --git a/osu.Game/Overlays/Mods/IncompatibilityDisplayingTooltip.cs b/osu.Game/Overlays/Mods/IncompatibilityDisplayingTooltip.cs index 2f82711162..3ac541eaa3 100644 --- a/osu.Game/Overlays/Mods/IncompatibilityDisplayingTooltip.cs +++ b/osu.Game/Overlays/Mods/IncompatibilityDisplayingTooltip.cs @@ -24,13 +24,15 @@ namespace osu.Game.Overlays.Mods [Resolved] private Bindable ruleset { get; set; } = null!; - public IncompatibilityDisplayingTooltip() + public IncompatibilityDisplayingTooltip(OverlayColourProvider colourProvider) + : base(colourProvider) { AddRange(new Drawable[] { incompatibleText = new OsuSpriteText { Margin = new MarginPadding { Top = 5 }, + Colour = colourProvider.Content2, Font = OsuFont.GetFont(weight: FontWeight.Regular), Text = "Incompatible with:" }, @@ -43,12 +45,6 @@ namespace osu.Game.Overlays.Mods }); } - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { - incompatibleText.Colour = colours.BlueLight; - } - protected override void UpdateDisplay(Mod mod) { base.UpdateDisplay(mod); diff --git a/osu.Game/Overlays/Mods/ModButtonTooltip.cs b/osu.Game/Overlays/Mods/ModButtonTooltip.cs index 52b27f1e00..061c3e3e3a 100644 --- a/osu.Game/Overlays/Mods/ModButtonTooltip.cs +++ b/osu.Game/Overlays/Mods/ModButtonTooltip.cs @@ -1,9 +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; using osu.Framework.Graphics.Cursor; @@ -18,11 +15,10 @@ namespace osu.Game.Overlays.Mods public partial class ModButtonTooltip : VisibilityContainer, ITooltip { private readonly OsuSpriteText descriptionText; - private readonly Box background; protected override Container Content { get; } - public ModButtonTooltip() + public ModButtonTooltip(OverlayColourProvider colourProvider) { AutoSizeAxes = Axes.Both; Masking = true; @@ -30,9 +26,10 @@ namespace osu.Game.Overlays.Mods InternalChildren = new Drawable[] { - background = new Box + new Box { - RelativeSizeAxes = Axes.Both + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background6, }, Content = new FillFlowContainer { @@ -43,6 +40,7 @@ namespace osu.Game.Overlays.Mods { descriptionText = new OsuSpriteText { + Colour = colourProvider.Content1, Font = OsuFont.GetFont(weight: FontWeight.Regular), }, } @@ -50,17 +48,10 @@ namespace osu.Game.Overlays.Mods }; } - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { - background.Colour = colours.Gray3; - descriptionText.Colour = colours.BlueLighter; - } - protected override void PopIn() => this.FadeIn(200, Easing.OutQuint); protected override void PopOut() => this.FadeOut(200, Easing.OutQuint); - private Mod lastMod; + private Mod? lastMod; public void SetContent(Mod mod) { diff --git a/osu.Game/Overlays/Mods/ModPresetTooltip.cs b/osu.Game/Overlays/Mods/ModPresetTooltip.cs index 501b56e2fd..ec81aa7ceb 100644 --- a/osu.Game/Overlays/Mods/ModPresetTooltip.cs +++ b/osu.Game/Overlays/Mods/ModPresetTooltip.cs @@ -40,14 +40,14 @@ namespace osu.Game.Overlays.Mods { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Padding = new MarginPadding(7), + Padding = new MarginPadding { Left = 10, Right = 10, Top = 5, Bottom = 5 }, Spacing = new Vector2(7), Children = new[] { descriptionText = new OsuSpriteText { Font = OsuFont.GetFont(weight: FontWeight.Regular), - Colour = colourProvider.Content2, + Colour = colourProvider.Content1, }, } } From 86b8357b8b7fd39be80a7881103d289ba4342a12 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 28 Jun 2024 08:52:52 +0300 Subject: [PATCH 1219/2493] Improve UX & input handling when customisation panel is open --- .../TestSceneModCustomisationPanel.cs | 1 + .../Overlays/Mods/ModCustomisationPanel.cs | 43 ++++++++++++++++--- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 33 ++++++-------- 3 files changed, 50 insertions(+), 27 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModCustomisationPanel.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModCustomisationPanel.cs index 0066ecd556..360c28acfa 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModCustomisationPanel.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModCustomisationPanel.cs @@ -30,6 +30,7 @@ namespace osu.Game.Tests.Visual.UserInterface Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, Width = 400f, + State = { Value = Visibility.Visible }, SelectedMods = { BindTarget = SelectedMods }, } }; diff --git a/osu.Game/Overlays/Mods/ModCustomisationPanel.cs b/osu.Game/Overlays/Mods/ModCustomisationPanel.cs index ec3499bc57..a5a524e109 100644 --- a/osu.Game/Overlays/Mods/ModCustomisationPanel.cs +++ b/osu.Game/Overlays/Mods/ModCustomisationPanel.cs @@ -10,16 +10,18 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Shapes; +using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Graphics.Containers; +using osu.Game.Input.Bindings; using osu.Game.Rulesets.Mods; using osuTK; namespace osu.Game.Overlays.Mods { - public partial class ModCustomisationPanel : VisibilityContainer + public partial class ModCustomisationPanel : OverlayContainer, IKeyBindingHandler { private const float header_height = 42f; private const float content_vertical_padding = 20f; @@ -35,6 +37,12 @@ namespace osu.Game.Overlays.Mods public Bindable> SelectedMods { get; } = new Bindable>(Array.Empty()); + public override bool HandlePositionalInput => Expanded.Value; + + public override bool HandleNonPositionalInput => Expanded.Value; + + protected override bool BlockPositionalInput => true; + [BackgroundDependencyLoader] private void load() { @@ -63,6 +71,7 @@ namespace osu.Game.Overlays.Mods Radius = 20f, Roundness = 5f, }, + Expanded = { BindTarget = Expanded }, Children = new Drawable[] { new Box @@ -110,12 +119,30 @@ namespace osu.Game.Overlays.Mods public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; - protected override bool OnMouseDown(MouseDownEvent e) + protected override bool OnClick(ClickEvent e) { - if (Expanded.Value && !content.ReceivePositionalInputAt(e.ScreenSpaceMousePosition)) - Expanded.Value = false; + Expanded.Value = false; + return base.OnClick(e); + } - return base.OnMouseDown(e); + protected override bool OnKeyDown(KeyDownEvent e) => true; + + protected override bool OnScroll(ScrollEvent e) => true; + + public bool OnPressed(KeyBindingPressEvent e) + { + switch (e.Action) + { + case GlobalAction.Back: + Expanded.Value = false; + return true; + } + + return false; + } + + public void OnReleased(KeyBindingReleaseEvent e) + { } private void updateDisplay() @@ -162,8 +189,10 @@ namespace osu.Game.Overlays.Mods private partial class FocusGrabbingContainer : InputBlockingContainer { - public override bool RequestsFocus => true; - public override bool AcceptsFocus => true; + public IBindable Expanded { get; } = new BindableBool(); + + public override bool RequestsFocus => Expanded.Value; + public override bool AcceptsFocus => Expanded.Value; } } } diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index f447f6f722..b0d58480db 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -29,6 +29,7 @@ using osu.Game.Localisation; using osu.Game.Rulesets.Mods; using osu.Game.Utils; using osuTK; +using osuTK.Graphics; using osuTK.Input; namespace osu.Game.Overlays.Mods @@ -215,7 +216,6 @@ namespace osu.Game.Overlays.Mods }, customisationPanel = new ModCustomisationPanel { - Alpha = 0f, Anchor = Anchor.TopRight, Origin = Anchor.TopRight, Width = 400, @@ -508,9 +508,17 @@ namespace osu.Game.Overlays.Mods private void updateCustomisationVisualState() { if (customisationPanel.Expanded.Value) + { + columnScroll.FadeColour(OsuColour.Gray(0.5f), 400, Easing.OutQuint); + SearchTextBox.FadeColour(OsuColour.Gray(0.5f), 400, Easing.OutQuint); SearchTextBox.KillFocus(); + } else + { + columnScroll.FadeColour(Color4.White, 400, Easing.OutQuint); + SearchTextBox.FadeColour(Color4.White, 400, Easing.OutQuint); setTextBoxFocus(textSearchStartsActive.Value); + } } /// @@ -678,16 +686,12 @@ namespace osu.Game.Overlays.Mods switch (e.Action) { + // If the customisation panel is expanded, the back action will be handled by it first. case GlobalAction.Back: - // Pressing the back binding should only go back one step at a time. - hideOverlay(false); - return true; - // This is handled locally here because this overlay is being registered at the game level // and therefore takes away keyboard focus from the screen stack. case GlobalAction.ToggleModSelection: - // Pressing toggle should completely hide the overlay in one shot. - hideOverlay(true); + hideOverlay(); return true; // This is handled locally here due to conflicts in input handling between the search text box and the deselect all mods button. @@ -710,7 +714,7 @@ namespace osu.Game.Overlays.Mods // If there is no search in progress, it should exit the dialog (a bit weird, but this is the expectation from stable). if (string.IsNullOrEmpty(SearchTerm)) { - hideOverlay(true); + hideOverlay(); return true; } @@ -728,18 +732,7 @@ namespace osu.Game.Overlays.Mods return base.OnPressed(e); - void hideOverlay(bool immediate) - { - if (customisationPanel.Expanded.Value) - { - customisationPanel.Expanded.Value = false; - - if (!immediate) - return; - } - - BackButton.TriggerClick(); - } + void hideOverlay() => BackButton.TriggerClick(); } /// From c1bec7a7c374f38adc33b124f93d40ff267657f7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 28 Jun 2024 14:55:55 +0900 Subject: [PATCH 1220/2493] Simplify colour logic for beatmap overlay filter rows --- .../BeatmapSearchGeneralFilterRow.cs | 5 ++-- ...BeatmapSearchMultipleSelectionFilterRow.cs | 10 ++----- .../Overlays/BeatmapListing/FilterTabItem.cs | 29 ++++++++++++++----- 3 files changed, 27 insertions(+), 17 deletions(-) diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapSearchGeneralFilterRow.cs b/osu.Game/Overlays/BeatmapListing/BeatmapSearchGeneralFilterRow.cs index a4a914db55..2d56c60de6 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapSearchGeneralFilterRow.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapSearchGeneralFilterRow.cs @@ -56,8 +56,6 @@ namespace osu.Game.Overlays.BeatmapListing [Resolved(canBeNull: true)] private IDialogOverlay dialogOverlay { get; set; } - protected override Color4 GetStateColour() => colours.Orange1; - protected override void LoadComplete() { base.LoadComplete(); @@ -65,6 +63,9 @@ namespace osu.Game.Overlays.BeatmapListing disclaimerShown = sessionStatics.GetBindable(Static.FeaturedArtistDisclaimerShownOnce); } + protected override Color4 ColourNormal => colours.Orange1; + protected override Color4 ColourActive => colours.Orange2; + protected override bool OnClick(ClickEvent e) { if (!disclaimerShown.Value && dialogOverlay != null) diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapSearchMultipleSelectionFilterRow.cs b/osu.Game/Overlays/BeatmapListing/BeatmapSearchMultipleSelectionFilterRow.cs index 4bd25f6561..9b2e1d57fe 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapSearchMultipleSelectionFilterRow.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapSearchMultipleSelectionFilterRow.cs @@ -1,13 +1,10 @@ // 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.Collections.Generic; using System.Collections.Specialized; using System.Linq; -using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -24,7 +21,7 @@ namespace osu.Game.Overlays.BeatmapListing { public new readonly BindableList Current = new BindableList(); - private MultipleSelectionFilter filter; + private MultipleSelectionFilter filter = null!; public BeatmapSearchMultipleSelectionFilterRow(LocalisableString header) : base(header) @@ -42,7 +39,6 @@ namespace osu.Game.Overlays.BeatmapListing /// /// Creates a filter control that can be used to simultaneously select multiple values of type . /// - [NotNull] protected virtual MultipleSelectionFilter CreateMultipleSelectionFilter() => new MultipleSelectionFilter(); protected partial class MultipleSelectionFilter : FillFlowContainer @@ -69,7 +65,7 @@ namespace osu.Game.Overlays.BeatmapListing Current.BindCollectionChanged(currentChanged, true); } - private void currentChanged(object sender, NotifyCollectionChangedEventArgs e) + private void currentChanged(object? sender, NotifyCollectionChangedEventArgs e) { foreach (var c in Children) c.Active.Value = Current.Contains(c.Value); @@ -122,7 +118,7 @@ namespace osu.Game.Overlays.BeatmapListing { base.UpdateState(); selectedUnderline.FadeTo(Active.Value ? 1 : 0, 200, Easing.OutQuint); - selectedUnderline.FadeColour(IsHovered ? ColourProvider.Content2 : GetStateColour(), 200, Easing.OutQuint); + selectedUnderline.FadeColour(ColourForCurrentState, 200, Easing.OutQuint); } protected override bool OnClick(ClickEvent e) diff --git a/osu.Game/Overlays/BeatmapListing/FilterTabItem.cs b/osu.Game/Overlays/BeatmapListing/FilterTabItem.cs index ee188d34ce..2896039a99 100644 --- a/osu.Game/Overlays/BeatmapListing/FilterTabItem.cs +++ b/osu.Game/Overlays/BeatmapListing/FilterTabItem.cs @@ -8,6 +8,7 @@ using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; using osu.Framework.Extensions; +using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; @@ -24,7 +25,7 @@ namespace osu.Game.Overlays.BeatmapListing [Resolved] protected OverlayColourProvider ColourProvider { get; private set; } - private OsuSpriteText text; + protected OsuSpriteText Text; protected Sample SelectSample { get; private set; } = null!; @@ -39,7 +40,7 @@ namespace osu.Game.Overlays.BeatmapListing AutoSizeAxes = Axes.Both; AddRangeInternal(new Drawable[] { - text = new OsuSpriteText + Text = new OsuSpriteText { Font = OsuFont.GetFont(size: 13, weight: FontWeight.Regular), Text = LabelFor(Value) @@ -86,14 +87,26 @@ namespace osu.Game.Overlays.BeatmapListing protected virtual bool HighlightOnHoverWhenActive => false; - protected virtual void UpdateState() - { - bool highlightHover = IsHovered && (!Active.Value || HighlightOnHoverWhenActive); + protected virtual Color4 ColourActive => ColourProvider.Content1; + protected virtual Color4 ColourNormal => ColourProvider.Light2; - text.FadeColour(highlightHover ? ColourProvider.Content2 : GetStateColour(), 200, Easing.OutQuint); - text.Font = text.Font.With(weight: Active.Value ? FontWeight.Bold : FontWeight.Regular); + protected Color4 ColourForCurrentState + { + get + { + Color4 colour = Active.Value ? ColourActive : ColourNormal; + + if (IsHovered && (!Active.Value || HighlightOnHoverWhenActive)) + colour = colour.Lighten(0.2f); + + return colour; + } } - protected virtual Color4 GetStateColour() => Active.Value ? ColourProvider.Content1 : ColourProvider.Light2; + protected virtual void UpdateState() + { + Text.FadeColour(ColourForCurrentState, 200, Easing.OutQuint); + Text.Font = Text.Font.With(weight: Active.Value ? FontWeight.Bold : FontWeight.Regular); + } } } From 77b00dac7e4956a143acdf8cbd5d3100064f403f Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 28 Jun 2024 15:20:55 +0900 Subject: [PATCH 1221/2493] Fix low pass filter sometimes not applied in dialog overlays --- .../Overlays/Dialog/PopupDialogDangerousButton.cs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/osu.Game/Overlays/Dialog/PopupDialogDangerousButton.cs b/osu.Game/Overlays/Dialog/PopupDialogDangerousButton.cs index 19d7ea7a87..d16f49eab7 100644 --- a/osu.Game/Overlays/Dialog/PopupDialogDangerousButton.cs +++ b/osu.Game/Overlays/Dialog/PopupDialogDangerousButton.cs @@ -3,6 +3,7 @@ #nullable disable +using System; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; @@ -71,7 +72,7 @@ namespace osu.Game.Overlays.Dialog protected override void LoadComplete() { base.LoadComplete(); - Progress.BindValueChanged(progressChanged); + Progress.BindValueChanged(progressChanged, true); } protected override void AbortConfirm() @@ -122,11 +123,13 @@ namespace osu.Game.Overlays.Dialog private void progressChanged(ValueChangedEvent progress) { - if (progress.NewValue < progress.OldValue) return; + lowPassFilter.Cutoff = Math.Max(1, (int)(progress.NewValue * AudioFilter.MAX_LOWPASS_CUTOFF * 0.5)); - if (Clock.CurrentTime - lastTickPlaybackTime < 30) return; + if (progress.NewValue < progress.OldValue) + return; - lowPassFilter.CutoffTo((int)(progress.NewValue * AudioFilter.MAX_LOWPASS_CUTOFF * 0.5)); + if (Clock.CurrentTime - lastTickPlaybackTime < 30) + return; var channel = tickSample.GetChannel(); From fa1527f446163f546ae7f88a551e0d4ff9ff9caa Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 28 Jun 2024 15:43:04 +0900 Subject: [PATCH 1222/2493] Update design for selected filters to better imply that they are selected --- ...BeatmapSearchMultipleSelectionFilterRow.cs | 88 ++++++++++++++++--- .../Overlays/BeatmapListing/FilterTabItem.cs | 22 ++--- 2 files changed, 83 insertions(+), 27 deletions(-) diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapSearchMultipleSelectionFilterRow.cs b/osu.Game/Overlays/BeatmapListing/BeatmapSearchMultipleSelectionFilterRow.cs index 9b2e1d57fe..9cf3328149 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapSearchMultipleSelectionFilterRow.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapSearchMultipleSelectionFilterRow.cs @@ -7,12 +7,17 @@ using System.Collections.Specialized; 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.Input.Events; using osu.Framework.Localisation; +using osu.Game.Graphics; using osuTK; +using osuTK.Graphics; +using FontWeight = osu.Game.Graphics.FontWeight; namespace osu.Game.Overlays.BeatmapListing { @@ -95,30 +100,91 @@ namespace osu.Game.Overlays.BeatmapListing protected partial class MultipleSelectionFilterTabItem : FilterTabItem { - private readonly Box selectedUnderline; - - protected override bool HighlightOnHoverWhenActive => true; + private Drawable activeContent = null!; + private Circle background = null!; public MultipleSelectionFilterTabItem(T value) : base(value) { + } + + [BackgroundDependencyLoader] + private void load() + { + AutoSizeDuration = 100; + AutoSizeEasing = Easing.OutQuint; + // This doesn't match any actual design, but should make it easier for the user to understand // that filters are applied until we settle on a final design. - AddInternal(selectedUnderline = new Box + AddInternal(activeContent = new Container { Depth = float.MaxValue, - RelativeSizeAxes = Axes.X, - Height = 1.5f, - Anchor = Anchor.BottomLeft, - Origin = Anchor.CentreLeft, + RelativeSizeAxes = Axes.Both, + Alpha = 0, + Padding = new MarginPadding + { + Left = -16, + Right = -4, + Vertical = -2 + }, + Children = new Drawable[] + { + background = new Circle + { + Colour = Color4.White, + RelativeSizeAxes = Axes.Both, + }, + new SpriteIcon + { + Icon = FontAwesome.Solid.TimesCircle, + Size = new Vector2(10), + Colour = ColourProvider.Background4, + Position = new Vector2(3, 0.5f), + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + } + } }); } + protected override Color4 ColourActive => ColourProvider.Light1; + + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) + { + return Active.Value + ? background.ReceivePositionalInputAt(screenSpacePos) + : base.ReceivePositionalInputAt(screenSpacePos); + } + protected override void UpdateState() { - base.UpdateState(); - selectedUnderline.FadeTo(Active.Value ? 1 : 0, 200, Easing.OutQuint); - selectedUnderline.FadeColour(ColourForCurrentState, 200, Easing.OutQuint); + Color4 colour = Active.Value ? ColourActive : ColourNormal; + + if (IsHovered) + colour = Active.Value ? colour.Darken(0.2f) : colour.Lighten(0.2f); + + if (Active.Value) + { + // This just allows enough spacing for adjacent tab items to show the "x". + Padding = new MarginPadding { Left = 12 }; + + activeContent.FadeIn(200, Easing.OutQuint); + background.FadeColour(colour, 200, Easing.OutQuint); + + // flipping colours + Text.FadeColour(ColourProvider.Background4, 200, Easing.OutQuint); + Text.Font = Text.Font.With(weight: FontWeight.SemiBold); + } + else + { + Padding = new MarginPadding(); + + activeContent.FadeOut(); + + background.FadeColour(colour, 200, Easing.OutQuint); + Text.FadeColour(colour, 200, Easing.OutQuint); + Text.Font = Text.Font.With(weight: FontWeight.Regular); + } } protected override bool OnClick(ClickEvent e) diff --git a/osu.Game/Overlays/BeatmapListing/FilterTabItem.cs b/osu.Game/Overlays/BeatmapListing/FilterTabItem.cs index 2896039a99..8f4ecaa0f5 100644 --- a/osu.Game/Overlays/BeatmapListing/FilterTabItem.cs +++ b/osu.Game/Overlays/BeatmapListing/FilterTabItem.cs @@ -85,27 +85,17 @@ namespace osu.Game.Overlays.BeatmapListing /// protected virtual LocalisableString LabelFor(T value) => (value as Enum)?.GetLocalisableDescription() ?? value.ToString(); - protected virtual bool HighlightOnHoverWhenActive => false; - protected virtual Color4 ColourActive => ColourProvider.Content1; protected virtual Color4 ColourNormal => ColourProvider.Light2; - protected Color4 ColourForCurrentState - { - get - { - Color4 colour = Active.Value ? ColourActive : ColourNormal; - - if (IsHovered && (!Active.Value || HighlightOnHoverWhenActive)) - colour = colour.Lighten(0.2f); - - return colour; - } - } - protected virtual void UpdateState() { - Text.FadeColour(ColourForCurrentState, 200, Easing.OutQuint); + Color4 colour = Active.Value ? ColourActive : ColourNormal; + + if (IsHovered) + colour = colour.Lighten(0.2f); + + Text.FadeColour(colour, 200, Easing.OutQuint); Text.Font = Text.Font.With(weight: Active.Value ? FontWeight.Bold : FontWeight.Regular); } } From 030bbf26414f075e55528ead9a18a518d539d237 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 28 Jun 2024 09:07:29 +0200 Subject: [PATCH 1223/2493] Fix vertical overlaps on multiselection filters when they wrap --- .../BeatmapListing/BeatmapSearchMultipleSelectionFilterRow.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapSearchMultipleSelectionFilterRow.cs b/osu.Game/Overlays/BeatmapListing/BeatmapSearchMultipleSelectionFilterRow.cs index 9cf3328149..958297b559 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapSearchMultipleSelectionFilterRow.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapSearchMultipleSelectionFilterRow.cs @@ -55,7 +55,7 @@ namespace osu.Game.Overlays.BeatmapListing { RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; - Spacing = new Vector2(10, 0); + Spacing = new Vector2(10, 5); AddRange(GetValues().Select(CreateTabItem)); } From ace6427d406555b65be69482f7303f9da7735e73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 28 Jun 2024 09:30:28 +0200 Subject: [PATCH 1224/2493] Expand test coverage - Covers fail case that wasn't covered before - Removes arbitrary wait step that was inevitably going to cause intermittent test failures --- .../Visual/Editing/TestSceneEditorTestGameplay.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorTestGameplay.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorTestGameplay.cs index 4c81fc3fe6..7dcb8766dd 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorTestGameplay.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorTestGameplay.cs @@ -126,10 +126,11 @@ namespace osu.Game.Tests.Visual.Editing AddAssert("sample playback re-enabled", () => !Editor.SamplePlaybackDisabled.Value); } - [Test] - public void TestGameplayTestAtEndOfBeatmap() + [TestCase(2000)] // chosen to be after last object in the map + [TestCase(22000)] // chosen to be in the middle of the last spinner + public void TestGameplayTestAtEndOfBeatmap(int offsetFromEnd) { - AddStep("seek to last 2 seconds", () => EditorClock.Seek(importedBeatmapSet.MaxLength - 2000)); + AddStep($"seek to end minus {offsetFromEnd}ms", () => EditorClock.Seek(importedBeatmapSet.MaxLength - offsetFromEnd)); AddStep("click test gameplay button", () => { var button = Editor.ChildrenOfType().Single(); @@ -140,8 +141,7 @@ namespace osu.Game.Tests.Visual.Editing AddUntilStep("player pushed", () => Stack.CurrentScreen is EditorPlayer); - AddWaitStep("wait some", 5); - AddAssert("current screen is editor", () => Stack.CurrentScreen is Editor); + AddUntilStep("current screen is editor", () => Stack.CurrentScreen is Editor); } [Test] From 7ac5bd4d37a4517d174c840ae32869d3e7029904 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 28 Jun 2024 09:37:10 +0200 Subject: [PATCH 1225/2493] Ensure past drawable objects also get their results populated in editor test play --- .../Screens/Edit/GameplayTest/EditorPlayer.cs | 40 ++++++++++++++++++- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs b/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs index 2028094964..9a7c1822a3 100644 --- a/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs +++ b/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs @@ -48,6 +48,7 @@ namespace osu.Game.Screens.Edit.GameplayTest base.LoadComplete(); markPreviousObjectsHit(); + markVisibleDrawableObjectsHit(); ScoreProcessor.HasCompleted.BindValueChanged(completed => { @@ -67,9 +68,10 @@ namespace osu.Game.Screens.Edit.GameplayTest foreach (var hitObject in enumerateHitObjects(DrawableRuleset.Objects, editorState.Time)) { var judgement = hitObject.Judgement; + var result = new JudgementResult(hitObject, judgement) { Type = judgement.MaxResult }; - HealthProcessor.ApplyResult(new JudgementResult(hitObject, judgement) { Type = judgement.MaxResult }); - ScoreProcessor.ApplyResult(new JudgementResult(hitObject, judgement) { Type = judgement.MaxResult }); + HealthProcessor.ApplyResult(result); + ScoreProcessor.ApplyResult(result); } static IEnumerable enumerateHitObjects(IEnumerable hitObjects, double cutoffTime) @@ -88,6 +90,40 @@ namespace osu.Game.Screens.Edit.GameplayTest } } + private void markVisibleDrawableObjectsHit() + { + if (!DrawableRuleset.Playfield.IsLoaded) + { + Schedule(markVisibleDrawableObjectsHit); + return; + } + + foreach (var drawableObjectEntry in enumerateDrawableEntries( + DrawableRuleset.Playfield.AllHitObjects + .Select(ho => ho.Entry) + .Where(e => e != null) + .Cast(), editorState.Time)) + { + drawableObjectEntry.Result = new JudgementResult(drawableObjectEntry.HitObject, drawableObjectEntry.HitObject.Judgement) + { Type = drawableObjectEntry.HitObject.Judgement.MaxResult }; + } + + static IEnumerable enumerateDrawableEntries(IEnumerable entries, double cutoffTime) + { + foreach (var entry in entries) + { + foreach (var nested in enumerateDrawableEntries(entry.NestedEntries, cutoffTime)) + { + if (nested.HitObject.GetEndTime() < cutoffTime) + yield return nested; + } + + if (entry.HitObject.GetEndTime() < cutoffTime) + yield return entry; + } + } + } + protected override void PrepareReplay() { // don't record replays. From a3ea36d2b264659c99573edf4845e8ba2a0cbca7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 28 Jun 2024 09:45:45 +0200 Subject: [PATCH 1226/2493] Fix formatting --- osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs b/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs index 9a7c1822a3..69851d0d35 100644 --- a/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs +++ b/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs @@ -105,7 +105,9 @@ namespace osu.Game.Screens.Edit.GameplayTest .Cast(), editorState.Time)) { drawableObjectEntry.Result = new JudgementResult(drawableObjectEntry.HitObject, drawableObjectEntry.HitObject.Judgement) - { Type = drawableObjectEntry.HitObject.Judgement.MaxResult }; + { + Type = drawableObjectEntry.HitObject.Judgement.MaxResult + }; } static IEnumerable enumerateDrawableEntries(IEnumerable entries, double cutoffTime) From f462aa59df34508bd4d792dcc22d1f645ac2637c Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 28 Jun 2024 11:23:49 +0300 Subject: [PATCH 1227/2493] Add test coverage for the issues that were pointed out recently --- .../TestSceneModSelectOverlay.cs | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs index a9db957e12..dedad3a40a 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs @@ -25,6 +25,7 @@ using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Taiko.Mods; using osu.Game.Tests.Mods; +using osuTK; using osuTK.Input; namespace osu.Game.Tests.Visual.UserInterface @@ -761,6 +762,19 @@ namespace osu.Game.Tests.Visual.UserInterface AddAssert("mod select hidden", () => modSelectOverlay.State.Value == Visibility.Hidden); } + [Test] + public void TestCloseViaToggleModSelectionBinding() + { + createScreen(); + changeRuleset(0); + + AddStep("select difficulty adjust via panel", () => getPanelForMod(typeof(OsuModDifficultyAdjust)).TriggerClick()); + assertCustomisationToggleState(disabled: false, active: true); + + AddStep("press F1", () => InputManager.Key(Key.F1)); + AddAssert("mod select hidden", () => modSelectOverlay.State.Value == Visibility.Hidden); + } + /// /// Covers columns hiding/unhiding on changes of . /// @@ -912,6 +926,68 @@ namespace osu.Game.Tests.Visual.UserInterface }); } + [Test] + public void TestOpeningCustomisationHidesPresetPopover() + { + createScreen(); + + AddStep("select DT", () => SelectedMods.Value = new Mod[] { new OsuModDoubleTime() }); + AddStep("click new preset", () => + { + InputManager.MoveMouseTo(this.ChildrenOfType().Single()); + InputManager.Click(MouseButton.Left); + }); + + AddAssert("preset popover shown", () => this.ChildrenOfType().SingleOrDefault()?.IsPresent, () => Is.True); + + AddStep("click customisation header", () => + { + InputManager.MoveMouseTo(this.ChildrenOfType().Single()); + InputManager.Click(MouseButton.Left); + }); + + AddUntilStep("preset popover hidden", () => this.ChildrenOfType().SingleOrDefault()?.IsPresent, () => Is.Not.True); + AddAssert("customisation panel shown", () => this.ChildrenOfType().Single().State.Value, () => Is.EqualTo(Visibility.Visible)); + } + + [Test] + public void TestCustomisationPanelAbsorbsInput([Values] bool textSearchStartsActive) + { + AddStep($"text search starts active = {textSearchStartsActive}", () => configManager.SetValue(OsuSetting.ModSelectTextSearchStartsActive, textSearchStartsActive)); + createScreen(); + + AddStep("select DT", () => SelectedMods.Value = new Mod[] { new OsuModDoubleTime() }); + AddStep("open customisation panel", () => this.ChildrenOfType().Single().TriggerClick()); + AddAssert("search lost focus", () => !this.ChildrenOfType().Single().HasFocus); + + AddStep("press tab", () => InputManager.Key(Key.Tab)); + AddAssert("search still not focused", () => !this.ChildrenOfType().Single().HasFocus); + + AddStep("press q", () => InputManager.Key(Key.Q)); + AddAssert("easy not selected", () => SelectedMods.Value.Single() is OsuModDoubleTime); + + // the "deselect all mods" action is intentionally disabled when customisation panel is open to not conflict with pressing backspace to delete characters in a textbox. + // this is supposed to be handled by the textbox itself especially since it's focused and thus prioritised in input queue, + // but it's not for some reason, and figuring out why is probably not going to be a pleasant experience (read TextBox.OnKeyDown for a head start). + AddStep("press backspace", () => InputManager.Key(Key.BackSpace)); + AddAssert("mods not deselected", () => SelectedMods.Value.Single() is OsuModDoubleTime); + + AddStep("move mouse to scroll bar", () => InputManager.MoveMouseTo(modSelectOverlay.ChildrenOfType().Single().ScreenSpaceDrawQuad.BottomLeft + new Vector2(10f, -5f))); + + AddStep("scroll down", () => InputManager.ScrollVerticalBy(-10f)); + AddAssert("column not scrolled", () => modSelectOverlay.ChildrenOfType().Single().IsScrolledToStart()); + + AddStep("press mouse", () => InputManager.PressButton(MouseButton.Left)); + AddAssert("search still not focused", () => !this.ChildrenOfType().Single().HasFocus); + AddStep("release mouse", () => InputManager.ReleaseButton(MouseButton.Left)); + AddAssert("customisation panel closed by click", () => !this.ChildrenOfType().Single().Expanded.Value); + + if (textSearchStartsActive) + AddAssert("search focused", () => this.ChildrenOfType().Single().HasFocus); + else + AddAssert("search still not focused", () => !this.ChildrenOfType().Single().HasFocus); + } + private void waitForColumnLoad() => AddUntilStep("all column content loaded", () => modSelectOverlay.ChildrenOfType().Any() && modSelectOverlay.ChildrenOfType().All(column => column.IsLoaded && column.ItemsLoaded) From 55b80f70f68f1c4040fb6f3e4558de79907c9b27 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 28 Jun 2024 18:12:20 +0900 Subject: [PATCH 1228/2493] Change "playfield" skin layer to respect shifting playfield border in osu! ruleset --- osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs index b39fc34d5d..df7f279656 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs @@ -8,6 +8,7 @@ using System.Linq; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Primitives; using osu.Game.Beatmaps; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects; @@ -47,6 +48,8 @@ namespace osu.Game.Rulesets.Osu.UI protected override GameplayCursorContainer? CreateCursor() => new OsuCursorContainer(); + public override Quad SkinnableComponentScreenSpaceDrawQuad => playfieldBorder.ScreenSpaceDrawQuad; + private readonly Container judgementAboveHitObjectLayer; public OsuPlayfield() From deeb2e99a2711ab9e9ef7bbd06249fb1723eab01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 28 Jun 2024 10:46:17 +0200 Subject: [PATCH 1229/2493] Add test for correct juice stream tick counts in editor cda9440a296304bd710c1787436ea1a948f6c999 inadvertently fixes this in the most frequent case by inverting the `TickDistanceMultiplier` from being not-1 to 1 on beatmap versions above v8. This can still potentially go wrong if a beatmap from a version below v8 is edited, because upon save it will be reencoded at the latest version, meaning that the multiplier will change from not-1 to 1 - but this can be handled separately. --- .../Editor/TestSceneCatchEditorSaving.cs | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 osu.Game.Rulesets.Catch.Tests/Editor/TestSceneCatchEditorSaving.cs diff --git a/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneCatchEditorSaving.cs b/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneCatchEditorSaving.cs new file mode 100644 index 0000000000..53ef24e02c --- /dev/null +++ b/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneCatchEditorSaving.cs @@ -0,0 +1,66 @@ +// 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.Testing; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Rulesets.Catch.Objects; +using osu.Game.Rulesets.Catch.UI; +using osu.Game.Tests.Visual; +using osuTK; +using osuTK.Input; + +namespace osu.Game.Rulesets.Catch.Tests.Editor +{ + public partial class TestSceneCatchEditorSaving : EditorSavingTestScene + { + protected override Ruleset CreateRuleset() => new CatchRuleset(); + + [Test] + public void TestCatchJuiceStreamTickCorrect() + { + AddStep("enter timing mode", () => InputManager.Key(Key.F3)); + AddStep("add timing point", () => EditorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint())); + AddStep("enter compose mode", () => InputManager.Key(Key.F1)); + + Vector2 startPoint = Vector2.Zero; + float increment = 0; + + AddUntilStep("wait for playfield", () => this.ChildrenOfType().FirstOrDefault()?.IsLoaded == true); + AddStep("move to centre", () => + { + var playfield = this.ChildrenOfType().Single(); + startPoint = playfield.ScreenSpaceDrawQuad.Centre + new Vector2(0, playfield.ScreenSpaceDrawQuad.Height / 3); + increment = playfield.ScreenSpaceDrawQuad.Height / 10; + InputManager.MoveMouseTo(startPoint); + }); + AddStep("choose juice stream placing tool", () => InputManager.Key(Key.Number3)); + AddStep("start placement", () => InputManager.Click(MouseButton.Left)); + + AddStep("move to next", () => InputManager.MoveMouseTo(startPoint + new Vector2(2 * increment, -increment))); + AddStep("add node", () => InputManager.Click(MouseButton.Left)); + + AddStep("move to next", () => InputManager.MoveMouseTo(startPoint + new Vector2(-2 * increment, -2 * increment))); + AddStep("add node", () => InputManager.Click(MouseButton.Left)); + + AddStep("move to next", () => InputManager.MoveMouseTo(startPoint + new Vector2(0, -3 * increment))); + AddStep("end placement", () => InputManager.Click(MouseButton.Right)); + + AddUntilStep("juice stream placed", () => EditorBeatmap.HitObjects, () => Has.Count.EqualTo(1)); + + int largeDropletCount = 0, tinyDropletCount = 0; + AddStep("store droplet count", () => + { + largeDropletCount = EditorBeatmap.HitObjects[0].NestedHitObjects.Count(t => t.GetType() == typeof(Droplet)); + tinyDropletCount = EditorBeatmap.HitObjects[0].NestedHitObjects.Count(t => t.GetType() == typeof(TinyDroplet)); + }); + + SaveEditor(); + ReloadEditorToSameBeatmap(); + + AddAssert("large droplet count is the same", () => EditorBeatmap.HitObjects[0].NestedHitObjects.Count(t => t.GetType() == typeof(Droplet)), () => Is.EqualTo(largeDropletCount)); + AddAssert("tiny droplet count is the same", () => EditorBeatmap.HitObjects[0].NestedHitObjects.Count(t => t.GetType() == typeof(TinyDroplet)), () => Is.EqualTo(tinyDropletCount)); + } + } +} From df972152984a79019e30a1edc25ebab72479bf14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 4 Jun 2024 15:36:30 +0200 Subject: [PATCH 1230/2493] Use room watching functionality to receive realtime daily challenge updates --- .../TestSceneDailyChallengeCarousel.cs | 27 +++-- .../TestSceneDailyChallengeEventFeed.cs | 31 ++++-- .../TestSceneDailyChallengeScoreBreakdown.cs | 13 ++- .../Online/Metadata/OnlineMetadataClient.cs | 2 +- .../DailyChallenge/DailyChallenge.cs | 100 +++++++++++++++++- .../DailyChallenge/DailyChallengeEventFeed.cs | 12 +-- .../DailyChallengeScoreBreakdown.cs | 10 +- .../DailyChallenge/Events/NewScoreEvent.cs | 23 ++++ 8 files changed, 183 insertions(+), 35 deletions(-) create mode 100644 osu.Game/Screens/OnlinePlay/DailyChallenge/Events/NewScoreEvent.cs diff --git a/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeCarousel.cs b/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeCarousel.cs index b9143945c4..d53e386ad4 100644 --- a/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeCarousel.cs +++ b/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeCarousel.cs @@ -11,10 +11,11 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Utils; using osu.Game.Graphics.Sprites; +using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Rooms; using osu.Game.Overlays; using osu.Game.Screens.OnlinePlay.DailyChallenge; -using osu.Game.Tests.Resources; +using osu.Game.Screens.OnlinePlay.DailyChallenge.Events; namespace osu.Game.Tests.Visual.DailyChallenge { @@ -129,19 +130,27 @@ namespace osu.Game.Tests.Visual.DailyChallenge }); AddStep("add normal score", () => { - var testScore = TestResources.CreateTestScoreInfo(); - testScore.TotalScore = RNG.Next(1_000_000); + var ev = new NewScoreEvent(1, new APIUser + { + Id = 2, + Username = "peppy", + CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", + }, RNG.Next(1_000_000), null); - feed.AddNewScore(new DailyChallengeEventFeed.NewScoreEvent(testScore, null)); - breakdown.AddNewScore(testScore); + feed.AddNewScore(ev); + breakdown.AddNewScore(ev); }); AddStep("add new user best", () => { - var testScore = TestResources.CreateTestScoreInfo(); - testScore.TotalScore = RNG.Next(1_000_000); + var ev = new NewScoreEvent(1, new APIUser + { + Id = 2, + Username = "peppy", + CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", + }, RNG.Next(1_000_000), RNG.Next(1, 1000)); - feed.AddNewScore(new DailyChallengeEventFeed.NewScoreEvent(testScore, RNG.Next(1, 1000))); - breakdown.AddNewScore(testScore); + feed.AddNewScore(ev); + breakdown.AddNewScore(ev); }); } diff --git a/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeEventFeed.cs b/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeEventFeed.cs index 85499f0588..d9a8ccd510 100644 --- a/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeEventFeed.cs +++ b/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeEventFeed.cs @@ -7,8 +7,10 @@ using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Shapes; using osu.Framework.Utils; +using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays; using osu.Game.Screens.OnlinePlay.DailyChallenge; +using osu.Game.Screens.OnlinePlay.DailyChallenge.Events; using osu.Game.Tests.Resources; namespace osu.Game.Tests.Visual.DailyChallenge @@ -50,26 +52,41 @@ namespace osu.Game.Tests.Visual.DailyChallenge AddStep("add normal score", () => { - var testScore = TestResources.CreateTestScoreInfo(); - testScore.TotalScore = RNG.Next(1_000_000); + var ev = new NewScoreEvent(1, new APIUser + { + Id = 2, + Username = "peppy", + CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", + }, RNG.Next(1_000_000), null); - feed.AddNewScore(new DailyChallengeEventFeed.NewScoreEvent(testScore, null)); + feed.AddNewScore(ev); }); AddStep("add new user best", () => { + var ev = new NewScoreEvent(1, new APIUser + { + Id = 2, + Username = "peppy", + CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", + }, RNG.Next(1_000_000), RNG.Next(11, 1000)); + var testScore = TestResources.CreateTestScoreInfo(); testScore.TotalScore = RNG.Next(1_000_000); - feed.AddNewScore(new DailyChallengeEventFeed.NewScoreEvent(testScore, RNG.Next(1, 1000))); + feed.AddNewScore(ev); }); AddStep("add top 10 score", () => { - var testScore = TestResources.CreateTestScoreInfo(); - testScore.TotalScore = RNG.Next(1_000_000); + var ev = new NewScoreEvent(1, new APIUser + { + Id = 2, + Username = "peppy", + CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", + }, RNG.Next(1_000_000), RNG.Next(1, 10)); - feed.AddNewScore(new DailyChallengeEventFeed.NewScoreEvent(testScore, RNG.Next(1, 10))); + feed.AddNewScore(ev); }); } } diff --git a/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeScoreBreakdown.cs b/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeScoreBreakdown.cs index 5523d02694..81ec95d8d2 100644 --- a/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeScoreBreakdown.cs +++ b/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeScoreBreakdown.cs @@ -7,9 +7,10 @@ using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Shapes; using osu.Framework.Utils; +using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays; using osu.Game.Screens.OnlinePlay.DailyChallenge; -using osu.Game.Tests.Resources; +using osu.Game.Screens.OnlinePlay.DailyChallenge.Events; namespace osu.Game.Tests.Visual.DailyChallenge { @@ -51,10 +52,14 @@ namespace osu.Game.Tests.Visual.DailyChallenge AddStep("set initial data", () => breakdown.SetInitialCounts([1, 4, 9, 16, 25, 36, 49, 36, 25, 16, 9, 4, 1])); AddStep("add new score", () => { - var testScore = TestResources.CreateTestScoreInfo(); - testScore.TotalScore = RNG.Next(1_000_000); + var ev = new NewScoreEvent(1, new APIUser + { + Id = 2, + Username = "peppy", + CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", + }, RNG.Next(1_000_000), null); - breakdown.AddNewScore(testScore); + breakdown.AddNewScore(ev); }); } } diff --git a/osu.Game/Online/Metadata/OnlineMetadataClient.cs b/osu.Game/Online/Metadata/OnlineMetadataClient.cs index 80fcf7571d..911b13ecd8 100644 --- a/osu.Game/Online/Metadata/OnlineMetadataClient.cs +++ b/osu.Game/Online/Metadata/OnlineMetadataClient.cs @@ -256,7 +256,7 @@ namespace osu.Game.Online.Metadata throw new OperationCanceledException(); Debug.Assert(connection != null); - await connection.InvokeAsync(nameof(IMetadataServer.EndWatchingMultiplayerRoom)).ConfigureAwait(false); + await connection.InvokeAsync(nameof(IMetadataServer.EndWatchingMultiplayerRoom), id).ConfigureAwait(false); } public override async Task DisconnectRequested() diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs index e2927617f8..b8d0dbbe7d 100644 --- a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs @@ -8,21 +8,29 @@ using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; using osu.Framework.Bindables; +using osu.Framework.Extensions; 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.Shapes; +using osu.Framework.Logging; using osu.Framework.Screens; using osu.Game.Beatmaps; +using osu.Game.Database; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osu.Game.Localisation; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Online.Metadata; +using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; using osu.Game.Overlays; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Screens.OnlinePlay.Components; +using osu.Game.Screens.OnlinePlay.DailyChallenge.Events; using osu.Game.Screens.OnlinePlay.Match; using osu.Game.Screens.OnlinePlay.Match.Components; using osu.Game.Screens.OnlinePlay.Playlists; @@ -47,6 +55,9 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge private Sample? sampleStart; private IDisposable? userModsSelectOverlayRegistration; + private DailyChallengeScoreBreakdown breakdown = null!; + private DailyChallengeEventFeed feed = null!; + [Cached] private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Plum); @@ -68,6 +79,12 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge [Resolved] private IOverlayManager? overlayManager { get; set; } + [Resolved] + private MetadataClient metadataClient { get; set; } = null!; + + [Resolved] + private UserLookupCache userLookupCache { get; set; } = null!; + public override bool DisallowExternalBeatmapRulesetChanges => true; public DailyChallenge(Room room) @@ -162,9 +179,39 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge { new Drawable?[] { - new DailyChallengeTimeRemainingRing + new GridContainer { RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RowDimensions = + [ + new Dimension(), + new Dimension() + ], + Content = new[] + { + new Drawable[] + { + new DailyChallengeCarousel + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Children = new Drawable[] + { + new DailyChallengeTimeRemainingRing(), + breakdown = new DailyChallengeScoreBreakdown(), + } + } + }, + [ + feed = new DailyChallengeEventFeed + { + RelativeSizeAxes = Axes.Both, + } + ], + }, }, null, // Middle column (leaderboard) @@ -275,6 +322,33 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge var allowedMods = playlistItem.AllowedMods.Select(m => m.ToMod(rulesetInstance)); userModsSelectOverlay.IsValidMod = m => allowedMods.Any(a => a.GetType() == m.GetType()); } + + metadataClient.MultiplayerRoomScoreSet += onRoomScoreSet; + } + + private void onRoomScoreSet(MultiplayerRoomScoreSetEvent e) + { + if (e.RoomID != room.RoomID.Value || e.PlaylistItemID != playlistItem.ID) + return; + + userLookupCache.GetUserAsync(e.UserID).ContinueWith(t => + { + if (t.Exception != null) + { + Logger.Log($@"Could not display room score set event: {t.Exception}", LoggingTarget.Network); + return; + } + + APIUser? user = t.GetResultSafely(); + if (user == null) return; + + var ev = new NewScoreEvent(e.ScoreID, user, e.TotalScore, e.NewRank); + Schedule(() => + { + breakdown.AddNewScore(ev); + feed.AddNewScore(ev); + }); + }); } protected override void LoadComplete() @@ -294,6 +368,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge var beatmap = beatmapManager.QueryBeatmap(b => b.OnlineID == playlistItem.Beatmap.OnlineID); Beatmap.Value = beatmapManager.GetWorkingBeatmap(beatmap); // this will gracefully fall back to dummy beatmap if missing locally. Ruleset.Value = rulesets.GetRuleset(playlistItem.RulesetID); + applyLoopingToTrack(); } public override void OnEntering(ScreenTransitionEvent e) @@ -303,6 +378,25 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge waves.Show(); roomManager.JoinRoom(room); applyLoopingToTrack(); + + metadataClient.BeginWatchingMultiplayerRoom(room.RoomID.Value!.Value).ContinueWith(t => + { + if (t.Exception != null) + { + Logger.Error(t.Exception, @"Failed to subscribe to room updates", LoggingTarget.Network); + return; + } + + MultiplayerPlaylistItemStats[] stats = t.GetResultSafely(); + var itemStats = stats.SingleOrDefault(item => item.PlaylistItemID == playlistItem.ID); + if (itemStats == null) return; + + Schedule(() => breakdown.SetInitialCounts(itemStats.TotalScoreDistribution)); + }); + + beatmapAvailabilityTracker.SelectedItem.Value = playlistItem; + beatmapAvailabilityTracker.Availability.BindValueChanged(_ => trySetDailyChallengeBeatmap(), true); + userModsSelectOverlay.SelectedItem.Value = playlistItem; } public override void OnResuming(ScreenTransitionEvent e) @@ -327,6 +421,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge this.Delay(WaveContainer.DISAPPEAR_DURATION).FadeOut(); roomManager.PartRoom(); + metadataClient.EndWatchingMultiplayerRoom(room.RoomID.Value!.Value).FireAndForget(); return base.OnExiting(e); } @@ -375,6 +470,9 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge base.Dispose(isDisposing); userModsSelectOverlayRegistration?.Dispose(); + + if (metadataClient.IsNotNull()) + metadataClient.MultiplayerRoomScoreSet -= onRoomScoreSet; } } } diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeEventFeed.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeEventFeed.cs index b415b15b65..c38a921e43 100644 --- a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeEventFeed.cs +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeEventFeed.cs @@ -9,8 +9,7 @@ using osu.Framework.Graphics.Containers; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; -using osu.Game.Online.API.Requests.Responses; -using osu.Game.Scoring; +using osu.Game.Screens.OnlinePlay.DailyChallenge.Events; using osu.Game.Users.Drawables; using osuTK; @@ -70,8 +69,6 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge } } - public record NewScoreEvent(IScoreInfo Score, int? NewRank); - private partial class DailyChallengeEventFeedFlow : FillFlowContainer { public override IEnumerable FlowingChildren => base.FlowingChildren.Reverse(); @@ -98,8 +95,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge InternalChildren = new Drawable[] { - // TODO: cast is temporary, will be removed later - new ClickableAvatar((APIUser)newScore.Score.User) + new ClickableAvatar(newScore.User) { Size = new Vector2(16), Masking = true, @@ -117,9 +113,9 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge } }; - text.AddUserLink(newScore.Score.User); + text.AddUserLink(newScore.User); text.AddText(" got "); - text.AddLink($"{newScore.Score.TotalScore:N0} points", () => { }); // TODO: present the score here + text.AddLink($"{newScore.TotalScore:N0} points", () => { }); // TODO: present the score here if (newScore.NewRank != null) text.AddText($" and achieved rank #{newScore.NewRank.Value:N0}"); diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeScoreBreakdown.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeScoreBreakdown.cs index d251a10f9a..0c7202f7cf 100644 --- a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeScoreBreakdown.cs +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeScoreBreakdown.cs @@ -13,7 +13,7 @@ using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Online.Metadata; using osu.Game.Overlays; -using osu.Game.Scoring; +using osu.Game.Screens.OnlinePlay.DailyChallenge.Events; using osuTK; namespace osu.Game.Screens.OnlinePlay.DailyChallenge @@ -67,15 +67,15 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge } } - public void AddNewScore(IScoreInfo scoreInfo) + public void AddNewScore(NewScoreEvent newScoreEvent) { - int targetBin = (int)Math.Clamp(Math.Floor((float)scoreInfo.TotalScore / 100000), 0, bin_count - 1); + int targetBin = (int)Math.Clamp(Math.Floor((float)newScoreEvent.TotalScore / 100000), 0, bin_count - 1); bins[targetBin] += 1; updateCounts(); var text = new OsuSpriteText { - Text = scoreInfo.TotalScore.ToString(@"N0"), + Text = newScoreEvent.TotalScore.ToString(@"N0"), Anchor = Anchor.TopCentre, Origin = Anchor.BottomCentre, Font = OsuFont.Default.With(size: 30), @@ -108,7 +108,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge private void updateCounts() { - long max = bins.Max(); + long max = Math.Max(bins.Max(), 1); for (int i = 0; i < bin_count; ++i) barsContainer[i].UpdateCounts(bins[i], max); } diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/Events/NewScoreEvent.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/Events/NewScoreEvent.cs new file mode 100644 index 0000000000..bc4c4e1a1e --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/Events/NewScoreEvent.cs @@ -0,0 +1,23 @@ +// 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.Online.API.Requests.Responses; + +namespace osu.Game.Screens.OnlinePlay.DailyChallenge.Events +{ + public class NewScoreEvent + { + public NewScoreEvent(long scoreID, APIUser user, long totalScore, int? newRank) + { + ScoreID = scoreID; + User = user; + TotalScore = totalScore; + NewRank = newRank; + } + + public long ScoreID { get; } + public APIUser User { get; } + public long TotalScore { get; } + public int? NewRank { get; } + } +} From 960d552dc1c1f2c34f6ac4cb79f652dce8fe70b5 Mon Sep 17 00:00:00 2001 From: Nathan Du Date: Fri, 28 Jun 2024 19:43:45 +0800 Subject: [PATCH 1231/2493] Initial implemention of the No Release mod This commit adds a new osu!mania mod No Release that relaxes tail judgements. The current implementation automatically awards Perfect (or Meh if the hold note is broken midway) for a hold note tail at the end of its Perfect window, as long as it is held by then. Tests are pending for the next commit. --- osu.Game.Rulesets.Mania/ManiaRuleset.cs | 1 + .../Mods/ManiaModNoRelease.cs | 35 +++++++++++++++++++ .../Objects/Drawables/DrawableHoldNoteTail.cs | 24 +++++++++++-- 3 files changed, 58 insertions(+), 2 deletions(-) create mode 100644 osu.Game.Rulesets.Mania/Mods/ManiaModNoRelease.cs diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs index 40eb44944c..667002533d 100644 --- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs @@ -241,6 +241,7 @@ namespace osu.Game.Rulesets.Mania new ManiaModEasy(), new ManiaModNoFail(), new MultiMod(new ManiaModHalfTime(), new ManiaModDaycore()), + new ManiaModNoRelease(), }; case ModType.DifficultyIncrease: diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModNoRelease.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModNoRelease.cs new file mode 100644 index 0000000000..f370ef15bd --- /dev/null +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModNoRelease.cs @@ -0,0 +1,35 @@ +// 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; +using osu.Game.Rulesets.Mania.Objects.Drawables; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Scoring; + +namespace osu.Game.Rulesets.Mania.Mods +{ + public class ManiaModNoRelease : Mod, IApplicableToDrawableHitObject + { + public override string Name => "No Release"; + + public override string Acronym => "NR"; + + public override LocalisableString Description => "No more timing the end of hold notes."; + + public override double ScoreMultiplier => 0.9; + + public override ModType Type => ModType.DifficultyReduction; + + public void ApplyToDrawableHitObject(DrawableHitObject drawable) + { + if (drawable is DrawableHoldNote hold) + { + hold.HitObjectApplied += dho => + { + ((DrawableHoldNote)dho).Tail.LateReleaseResult = HitResult.Perfect; + }; + } + } + } +} diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs index 79002b3819..eb1637b0ea 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs @@ -3,6 +3,7 @@ #nullable disable +using System.Diagnostics; using osu.Framework.Graphics; using osu.Framework.Input.Events; using osu.Game.Rulesets.Scoring; @@ -18,6 +19,11 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables protected internal DrawableHoldNote HoldNote => (DrawableHoldNote)ParentHitObject; + /// + /// The minimum uncapped result for a late release. + /// + public HitResult LateReleaseResult { get; set; } = HitResult.Miss; + public DrawableHoldNoteTail() : this(null) { @@ -32,9 +38,23 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables public void UpdateResult() => base.UpdateResult(true); - protected override void CheckForResult(bool userTriggered, double timeOffset) => + protected override void CheckForResult(bool userTriggered, double timeOffset) + { + Debug.Assert(HitObject.HitWindows != null); + // Factor in the release lenience - base.CheckForResult(userTriggered, timeOffset / TailNote.RELEASE_WINDOW_LENIENCE); + double scaledTimeOffset = timeOffset / TailNote.RELEASE_WINDOW_LENIENCE; + + // Check for late release + if (HoldNote.HoldStartTime != null && scaledTimeOffset > HitObject.HitWindows.WindowFor(LateReleaseResult)) + { + ApplyResult(GetCappedResult(LateReleaseResult)); + } + else + { + base.CheckForResult(userTriggered, scaledTimeOffset); + } + } protected override HitResult GetCappedResult(HitResult result) { From 679f4735b34eae83e8d9ef48a3fd7ee5270d19e0 Mon Sep 17 00:00:00 2001 From: Nathan Du Date: Sat, 29 Jun 2024 16:08:40 +0800 Subject: [PATCH 1232/2493] add tests for No Release mod The new test scene is essentially a copy of TestSceneHoldNoteInput, modified to test the judgement changes applied by the new mod. A base class might need to be abstracted out for them. --- .../Mods/TestSceneManiaModNoRelease.cs | 644 ++++++++++++++++++ .../TestSceneHoldNoteInput.cs | 2 +- 2 files changed, 645 insertions(+), 1 deletion(-) create mode 100644 osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModNoRelease.cs diff --git a/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModNoRelease.cs b/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModNoRelease.cs new file mode 100644 index 0000000000..11dcdd4f8d --- /dev/null +++ b/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModNoRelease.cs @@ -0,0 +1,644 @@ +// 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.Mods; +using osu.Game.Rulesets.Mania.Objects; +using osu.Game.Rulesets.Mania.Replays; +using osu.Game.Rulesets.Mania.Scoring; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Objects; +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.Mods +{ + public partial class TestSceneManiaModNoRelease : RateAdjustedBeatmapTestScene + { + private const double time_before_head = 250; + private const double time_head = 1500; + private const double time_during_hold_1 = 2500; + private const double time_tail = 4000; + private const double time_after_tail = 5250; + + private List judgementResults = new List(); + + /// + /// -----[ ]----- + /// o o + /// + [Test] + public void TestNoInput() + { + performTest(new List + { + new ManiaReplayFrame(time_before_head), + new ManiaReplayFrame(time_after_tail), + }); + + assertHeadJudgement(HitResult.Miss); + assertTailJudgement(HitResult.Miss); + assertNoteJudgement(HitResult.IgnoreMiss); + } + + /// + /// -----[ ]----- + /// x o + /// + [Test] + public void TestCorrectInput() + { + performTest(new List + { + new ManiaReplayFrame(time_head, ManiaAction.Key1), + new ManiaReplayFrame(time_tail), + }); + + assertHeadJudgement(HitResult.Perfect); + assertTailJudgement(HitResult.Perfect); + assertNoteJudgement(HitResult.IgnoreHit); + } + + /// + /// -----[ ]----- + /// x o + /// + [Test] + public void TestLateRelease() + { + performTest(new List + { + new ManiaReplayFrame(time_head, ManiaAction.Key1), + new ManiaReplayFrame(time_after_tail), + }); + + assertHeadJudgement(HitResult.Perfect); + assertTailJudgement(HitResult.Perfect); + assertNoteJudgement(HitResult.IgnoreHit); + } + + /// + /// -----[ ]----- + /// x o + /// + [Test] + public void TestPressTooEarlyAndReleaseAfterTail() + { + performTest(new List + { + new ManiaReplayFrame(time_before_head, ManiaAction.Key1), + new ManiaReplayFrame(time_after_tail, ManiaAction.Key1), + }); + + assertHeadJudgement(HitResult.Miss); + assertTailJudgement(HitResult.Miss); + } + + /// + /// -----[ ]----- + /// x o + /// + [Test] + public void TestPressTooEarlyAndReleaseAtTail() + { + performTest(new List + { + new ManiaReplayFrame(time_before_head, ManiaAction.Key1), + new ManiaReplayFrame(time_tail), + }); + + assertHeadJudgement(HitResult.Miss); + assertTailJudgement(HitResult.Miss); + } + + /// + /// -----[ ]----- + /// xo x o + /// + [Test] + public void TestPressTooEarlyThenPressAtStartAndReleaseAfterTail() + { + performTest(new List + { + new ManiaReplayFrame(time_before_head, ManiaAction.Key1), + new ManiaReplayFrame(time_before_head + 10), + new ManiaReplayFrame(time_head, ManiaAction.Key1), + new ManiaReplayFrame(time_after_tail), + }); + + assertHeadJudgement(HitResult.Perfect); + assertTailJudgement(HitResult.Perfect); + } + + /// + /// -----[ ]----- + /// xo x o + /// + [Test] + public void TestPressTooEarlyThenPressAtStartAndReleaseAtTail() + { + performTest(new List + { + new ManiaReplayFrame(time_before_head, ManiaAction.Key1), + new ManiaReplayFrame(time_before_head + 10), + new ManiaReplayFrame(time_head, ManiaAction.Key1), + new ManiaReplayFrame(time_tail), + }); + + assertHeadJudgement(HitResult.Perfect); + assertTailJudgement(HitResult.Perfect); + } + + /// + /// -----[ ]----- + /// xo o + /// + [Test] + public void TestPressAtStartAndBreak() + { + performTest(new List + { + new ManiaReplayFrame(time_head, ManiaAction.Key1), + new ManiaReplayFrame(time_head + 10), + new ManiaReplayFrame(time_after_tail), + }); + + assertHeadJudgement(HitResult.Perfect); + assertTailJudgement(HitResult.Miss); + } + + /// + /// -----[ ]----- + /// xox o + /// + [Test] + public void TestPressAtStartThenReleaseAndImmediatelyRepress() + { + performTest(new List + { + new ManiaReplayFrame(time_head, ManiaAction.Key1), + new ManiaReplayFrame(time_head + 1), + new ManiaReplayFrame(time_head + 2, ManiaAction.Key1), + new ManiaReplayFrame(time_tail), + }); + + assertHeadJudgement(HitResult.Perfect); + assertComboAtJudgement(0, 1); + assertTailJudgement(HitResult.Meh); + assertComboAtJudgement(1, 0); + assertComboAtJudgement(3, 1); + } + + /// + /// -----[ ]----- + /// xo x o + /// + [Test] + public void TestPressAtStartThenBreakThenRepressAndReleaseAfterTail() + { + performTest(new List + { + new ManiaReplayFrame(time_head, ManiaAction.Key1), + new ManiaReplayFrame(time_head + 10), + new ManiaReplayFrame(time_during_hold_1, ManiaAction.Key1), + new ManiaReplayFrame(time_after_tail), + }); + + assertHeadJudgement(HitResult.Perfect); + assertTailJudgement(HitResult.Meh); + } + + /// + /// -----[ ]----- + /// xo x o o + /// + [Test] + public void TestPressAtStartThenBreakThenRepressAndReleaseAtTail() + { + performTest(new List + { + new ManiaReplayFrame(time_head, ManiaAction.Key1), + new ManiaReplayFrame(time_head + 10), + new ManiaReplayFrame(time_during_hold_1, ManiaAction.Key1), + new ManiaReplayFrame(time_tail), + }); + + assertHeadJudgement(HitResult.Perfect); + assertTailJudgement(HitResult.Meh); + } + + /// + /// -----[ ]----- + /// x o + /// + [Test] + public void TestPressDuringNoteAndReleaseAfterTail() + { + performTest(new List + { + new ManiaReplayFrame(time_during_hold_1, ManiaAction.Key1), + new ManiaReplayFrame(time_after_tail), + }); + + assertHeadJudgement(HitResult.Miss); + assertTailJudgement(HitResult.Meh); + } + + /// + /// -----[ ]----- + /// x o o + /// + [Test] + public void TestPressDuringNoteAndReleaseAtTail() + { + performTest(new List + { + new ManiaReplayFrame(time_during_hold_1, ManiaAction.Key1), + new ManiaReplayFrame(time_tail), + }); + + assertHeadJudgement(HitResult.Miss); + assertTailJudgement(HitResult.Meh); + } + + /// + /// -----[ ]-------------- + /// xo + /// + [Test] + public void TestPressAndReleaseJustAfterTailWithCloseByHead() + { + const int duration = 30; + + var beatmap = new Beatmap + { + HitObjects = + { + // hold note is very short, to make the head still in range + new HoldNote + { + StartTime = time_head, + Duration = duration, + Column = 0, + } + }, + BeatmapInfo = + { + Difficulty = new BeatmapDifficulty { SliderTickRate = 4 }, + Ruleset = new ManiaRuleset().RulesetInfo + }, + }; + + performTest(new List + { + new ManiaReplayFrame(time_head + duration + 20, ManiaAction.Key1), + new ManiaReplayFrame(time_head + duration + 30), + }, beatmap); + + assertHeadJudgement(HitResult.Good); + assertTailJudgement(HitResult.Perfect); + } + + /// + /// -----[ ]-O------------- + /// xo o + /// + [Test] + public void TestPressAndReleaseJustBeforeTailWithNearbyNoteAndCloseByHead() + { + Note note; + + const int duration = 50; + + var beatmap = new Beatmap + { + HitObjects = + { + // hold note is very short, to make the head still in range + new HoldNote + { + StartTime = time_head, + Duration = duration, + Column = 0, + }, + { + // Next note within tail lenience + note = new Note + { + StartTime = time_head + duration + 10 + } + } + }, + BeatmapInfo = + { + Difficulty = new BeatmapDifficulty { SliderTickRate = 4 }, + Ruleset = new ManiaRuleset().RulesetInfo + }, + }; + + performTest(new List + { + new ManiaReplayFrame(time_head + duration, ManiaAction.Key1), + new ManiaReplayFrame(time_head + duration + 10), + }, beatmap); + + assertHeadJudgement(HitResult.Good); + assertTailJudgement(HitResult.Perfect); + + assertHitObjectJudgement(note, HitResult.Miss); + } + + /// + /// -----[ ]--O-- + /// xo o + /// + [Test] + public void TestPressAndReleaseJustBeforeTailWithNearbyNote() + { + Note note; + + var beatmap = new Beatmap + { + HitObjects = + { + new HoldNote + { + StartTime = time_head, + Duration = time_tail - time_head, + Column = 0, + }, + { + // Next note within tail lenience + note = new Note + { + StartTime = time_tail + 50 + } + } + }, + BeatmapInfo = + { + Difficulty = new BeatmapDifficulty { SliderTickRate = 4 }, + Ruleset = new ManiaRuleset().RulesetInfo + }, + }; + + performTest(new List + { + new ManiaReplayFrame(time_tail - 10, ManiaAction.Key1), + new ManiaReplayFrame(time_tail), + }, beatmap); + + assertHeadJudgement(HitResult.Miss); + assertTailJudgement(HitResult.Miss); + + assertHitObjectJudgement(note, HitResult.Good); + } + + /// + /// -----[ ]----- + /// xo + /// + [Test] + public void TestPressAndReleaseJustAfterTail() + { + performTest(new List + { + new ManiaReplayFrame(time_tail + 20, ManiaAction.Key1), + new ManiaReplayFrame(time_tail + 30), + }); + + assertHeadJudgement(HitResult.Miss); + assertTailJudgement(HitResult.Meh); + } + + /// + /// -----[ ]--O-- + /// xo o + /// + [Test] + public void TestPressAndReleaseJustAfterTailWithNearbyNote() + { + // Next note within tail lenience + Note note = new Note { StartTime = time_tail + 50 }; + + var beatmap = new Beatmap + { + HitObjects = + { + new HoldNote + { + StartTime = time_head, + Duration = time_tail - time_head, + Column = 0, + }, + note + }, + BeatmapInfo = + { + Difficulty = new BeatmapDifficulty { SliderTickRate = 4 }, + Ruleset = new ManiaRuleset().RulesetInfo + }, + }; + + performTest(new List + { + new ManiaReplayFrame(time_tail + 10, ManiaAction.Key1), + new ManiaReplayFrame(time_tail + 20), + }, beatmap); + + assertHeadJudgement(HitResult.Miss); + assertTailJudgement(HitResult.Miss); + + assertHitObjectJudgement(note, HitResult.Great); + } + + /// + /// -----[ ]----- + /// xo o + /// + [Test] + public void TestPressAndReleaseAtTail() + { + performTest(new List + { + new ManiaReplayFrame(time_tail, ManiaAction.Key1), + new ManiaReplayFrame(time_tail + 10), + }); + + assertHeadJudgement(HitResult.Miss); + assertTailJudgement(HitResult.Meh); + } + + [Test] + public void TestMissReleaseAndHitSecondRelease() + { + var windows = new ManiaHitWindows(); + windows.SetDifficulty(10); + + var beatmap = new Beatmap + { + HitObjects = + { + new HoldNote + { + StartTime = 1000, + Duration = 500, + Column = 0, + }, + new HoldNote + { + StartTime = 1000 + 500 + windows.WindowFor(HitResult.Miss) + 10, + Duration = 500, + Column = 0, + }, + }, + BeatmapInfo = + { + Difficulty = new BeatmapDifficulty + { + SliderTickRate = 4, + OverallDifficulty = 10, + }, + Ruleset = new ManiaRuleset().RulesetInfo + }, + }; + + performTest(new List + { + new ManiaReplayFrame(beatmap.HitObjects[1].StartTime, ManiaAction.Key1), + new ManiaReplayFrame(beatmap.HitObjects[1].GetEndTime()), + }, beatmap); + + AddAssert("first hold note missed", () => judgementResults.Where(j => beatmap.HitObjects[0].NestedHitObjects.Contains(j.HitObject)) + .All(j => !j.Type.IsHit())); + + AddAssert("second hold note hit", () => judgementResults.Where(j => beatmap.HitObjects[1].NestedHitObjects.Contains(j.HitObject)) + .All(j => j.Type.IsHit())); + } + + [Test] + public void TestZeroLength() + { + var beatmap = new Beatmap + { + HitObjects = + { + new HoldNote + { + StartTime = 1000, + Duration = 0, + Column = 0, + }, + }, + BeatmapInfo = { Ruleset = new ManiaRuleset().RulesetInfo }, + }; + + performTest(new List + { + new ManiaReplayFrame(beatmap.HitObjects[0].StartTime, ManiaAction.Key1), + new ManiaReplayFrame(beatmap.HitObjects[0].GetEndTime() + 1), + }, beatmap); + + AddAssert("hold note hit", () => judgementResults.Where(j => beatmap.HitObjects[0].NestedHitObjects.Contains(j.HitObject)) + .All(j => j.Type.IsHit())); + } + + private void assertHitObjectJudgement(HitObject hitObject, HitResult result) + => AddAssert($"object judged as {result}", () => judgementResults.First(j => j.HitObject == hitObject).Type, () => Is.EqualTo(result)); + + private void assertHeadJudgement(HitResult result) + => AddAssert($"head judged as {result}", () => judgementResults.First(j => j.HitObject is Note).Type, () => Is.EqualTo(result)); + + private void assertTailJudgement(HitResult result) + => AddAssert($"tail judged as {result}", () => judgementResults.Single(j => j.HitObject is TailNote).Type, () => Is.EqualTo(result)); + + private void assertNoteJudgement(HitResult result) + => AddAssert($"hold note judged as {result}", () => judgementResults.Single(j => j.HitObject is HoldNote).Type, () => Is.EqualTo(result)); + + private void assertComboAtJudgement(int judgementIndex, int combo) + => AddAssert($"combo at judgement {judgementIndex} is {combo}", () => judgementResults.ElementAt(judgementIndex).ComboAfterJudgement, () => Is.EqualTo(combo)); + + private ScoreAccessibleReplayPlayer currentPlayer = null!; + + private void performTest(List frames, Beatmap? beatmap = null) + { + if (beatmap == null) + { + beatmap = new Beatmap + { + HitObjects = + { + new HoldNote + { + StartTime = time_head, + Duration = time_tail - time_head, + Column = 0, + } + }, + BeatmapInfo = + { + Difficulty = new BeatmapDifficulty { SliderTickRate = 4 }, + Ruleset = new ManiaRuleset().RulesetInfo, + }, + }; + + beatmap.ControlPointInfo.Add(0, new EffectControlPoint { ScrollSpeed = 0.1f }); + } + + AddStep("load player", () => + { + SelectedMods.Value = new List + { + new ManiaModNoRelease() + }; + + 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, + }) + { + } + } + + } +} diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs index 5f299f419d..ef96ddb880 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs @@ -474,7 +474,7 @@ namespace osu.Game.Rulesets.Mania.Tests AddAssert("first hold note missed", () => judgementResults.Where(j => beatmap.HitObjects[0].NestedHitObjects.Contains(j.HitObject)) .All(j => !j.Type.IsHit())); - AddAssert("second hold note missed", () => judgementResults.Where(j => beatmap.HitObjects[1].NestedHitObjects.Contains(j.HitObject)) + AddAssert("second hold note hit", () => judgementResults.Where(j => beatmap.HitObjects[1].NestedHitObjects.Contains(j.HitObject)) .All(j => j.Type.IsHit())); } From 463ab46feec22971b6a1e187cf48cba80c552ff9 Mon Sep 17 00:00:00 2001 From: Nathan Du Date: Sat, 29 Jun 2024 16:46:16 +0800 Subject: [PATCH 1233/2493] formatting --- .../Mods/TestSceneManiaModNoRelease.cs | 3 +-- osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModNoRelease.cs b/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModNoRelease.cs index 11dcdd4f8d..82534ee019 100644 --- a/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModNoRelease.cs +++ b/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModNoRelease.cs @@ -523,7 +523,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods .All(j => !j.Type.IsHit())); AddAssert("second hold note hit", () => judgementResults.Where(j => beatmap.HitObjects[1].NestedHitObjects.Contains(j.HitObject)) - .All(j => j.Type.IsHit())); + .All(j => j.Type.IsHit())); } [Test] @@ -639,6 +639,5 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods { } } - } } diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs index ef96ddb880..e328d23ed4 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs @@ -475,7 +475,7 @@ namespace osu.Game.Rulesets.Mania.Tests .All(j => !j.Type.IsHit())); AddAssert("second hold note hit", () => judgementResults.Where(j => beatmap.HitObjects[1].NestedHitObjects.Contains(j.HitObject)) - .All(j => j.Type.IsHit())); + .All(j => j.Type.IsHit())); } [Test] From 892659de0f22b4bc584dc98a096c32a696d7c893 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 27 Jun 2024 08:15:12 +0300 Subject: [PATCH 1234/2493] Adjust footer design to display well with the rest of the game --- osu.Game/Screens/Footer/ScreenBackButton.cs | 9 +++------ osu.Game/Screens/Footer/ScreenFooter.cs | 4 ++-- osu.Game/Screens/Footer/ScreenFooterButton.cs | 15 +++++++-------- .../SelectV2/Footer/ScreenFooterButtonMods.cs | 12 +++++------- .../SelectV2/Footer/ScreenFooterButtonRandom.cs | 6 +++--- 5 files changed, 20 insertions(+), 26 deletions(-) diff --git a/osu.Game/Screens/Footer/ScreenBackButton.cs b/osu.Game/Screens/Footer/ScreenBackButton.cs index c5e613ea51..bf29186bb1 100644 --- a/osu.Game/Screens/Footer/ScreenBackButton.cs +++ b/osu.Game/Screens/Footer/ScreenBackButton.cs @@ -17,13 +17,10 @@ namespace osu.Game.Screens.Footer { public partial class ScreenBackButton : ShearedButton { - // todo: see https://github.com/ppy/osu-framework/issues/3271 - private const float torus_scale_factor = 1.2f; - public const float BUTTON_WIDTH = 240; public ScreenBackButton() - : base(BUTTON_WIDTH, 70) + : base(BUTTON_WIDTH) { } @@ -42,14 +39,14 @@ namespace osu.Game.Screens.Footer { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Size = new Vector2(20f), + Size = new Vector2(17f), Icon = FontAwesome.Solid.ChevronLeft, }, new OsuSpriteText { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Font = OsuFont.TorusAlternate.With(size: 20 * torus_scale_factor), + Font = OsuFont.TorusAlternate.With(size: 17), Text = CommonStrings.Back, UseFullGlyphHeight = false, } diff --git a/osu.Game/Screens/Footer/ScreenFooter.cs b/osu.Game/Screens/Footer/ScreenFooter.cs index 594cb3b9c9..c7090ba344 100644 --- a/osu.Game/Screens/Footer/ScreenFooter.cs +++ b/osu.Game/Screens/Footer/ScreenFooter.cs @@ -24,7 +24,7 @@ namespace osu.Game.Screens.Footer private const int padding = 60; private const float delay_per_button = 30; - public const int HEIGHT = 60; + public const int HEIGHT = 50; private readonly List overlays = new List(); @@ -73,7 +73,7 @@ namespace osu.Game.Screens.Footer }, backButton = new ScreenBackButton { - Margin = new MarginPadding { Bottom = 10f, Left = 12f }, + Margin = new MarginPadding { Bottom = 15f, Left = 12f }, Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, Action = () => OnBack?.Invoke(), diff --git a/osu.Game/Screens/Footer/ScreenFooterButton.cs b/osu.Game/Screens/Footer/ScreenFooterButton.cs index 1e5576e47a..cd1de2454b 100644 --- a/osu.Game/Screens/Footer/ScreenFooterButton.cs +++ b/osu.Game/Screens/Footer/ScreenFooterButton.cs @@ -28,8 +28,8 @@ namespace osu.Game.Screens.Footer private const float shear = OsuGame.SHEAR; protected const int CORNER_RADIUS = 10; - protected const int BUTTON_HEIGHT = 90; - protected const int BUTTON_WIDTH = 140; + protected const int BUTTON_HEIGHT = 75; + protected const int BUTTON_WIDTH = 116; public Bindable OverlayState = new Bindable(); @@ -116,19 +116,18 @@ namespace osu.Game.Screens.Footer { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, - Y = 42, + Y = 35, AutoSizeAxes = Axes.Both, Child = text = new OsuSpriteText { - // figma design says the size is 16, but due to the issues with font sizes 19 matches better - Font = OsuFont.TorusAlternate.With(size: 19), + Font = OsuFont.TorusAlternate.With(size: 16), AlwaysPresent = true } }, icon = new SpriteIcon { - Y = 12, - Size = new Vector2(20), + Y = 10, + Size = new Vector2(16), Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre }, @@ -140,7 +139,7 @@ namespace osu.Game.Screens.Footer Anchor = Anchor.BottomCentre, Origin = Anchor.Centre, Y = -CORNER_RADIUS, - Size = new Vector2(120, 6), + Size = new Vector2(100, 5), Masking = true, CornerRadius = 3, Child = bar = new Box diff --git a/osu.Game/Screens/SelectV2/Footer/ScreenFooterButtonMods.cs b/osu.Game/Screens/SelectV2/Footer/ScreenFooterButtonMods.cs index 841f0297e8..0992203dbc 100644 --- a/osu.Game/Screens/SelectV2/Footer/ScreenFooterButtonMods.cs +++ b/osu.Game/Screens/SelectV2/Footer/ScreenFooterButtonMods.cs @@ -32,9 +32,7 @@ namespace osu.Game.Screens.SelectV2.Footer { public partial class ScreenFooterButtonMods : ScreenFooterButton, IHasCurrentValue> { - // todo: see https://github.com/ppy/osu-framework/issues/3271 - private const float torus_scale_factor = 1.2f; - private const float bar_height = 37f; + private const float bar_height = 30f; private const float mod_display_portion = 0.65f; private readonly BindableWithCurrent> current = new BindableWithCurrent>(Array.Empty()); @@ -112,7 +110,7 @@ namespace osu.Game.Screens.SelectV2.Footer Origin = Anchor.Centre, Shear = -BUTTON_SHEAR, UseFullGlyphHeight = false, - Font = OsuFont.Torus.With(size: 14 * torus_scale_factor, weight: FontWeight.Bold) + Font = OsuFont.Torus.With(size: 14f, weight: FontWeight.Bold) } }, new Container @@ -133,7 +131,7 @@ namespace osu.Game.Screens.SelectV2.Footer Anchor = Anchor.Centre, Origin = Anchor.Centre, Shear = -BUTTON_SHEAR, - Scale = new Vector2(0.6f), + Scale = new Vector2(0.5f), Current = { BindTarget = Current }, ExpansionMode = ExpansionMode.AlwaysContracted, }, @@ -142,7 +140,7 @@ namespace osu.Game.Screens.SelectV2.Footer Anchor = Anchor.Centre, Origin = Anchor.Centre, Shear = -BUTTON_SHEAR, - Font = OsuFont.Torus.With(size: 14 * torus_scale_factor, weight: FontWeight.Bold), + Font = OsuFont.Torus.With(size: 14f, weight: FontWeight.Bold), Mods = { BindTarget = Current }, } } @@ -335,7 +333,7 @@ namespace osu.Game.Screens.SelectV2.Footer Text = ModSelectOverlayStrings.Unranked.ToUpper(), Margin = new MarginPadding { Horizontal = 15 }, UseFullGlyphHeight = false, - Font = OsuFont.Torus.With(size: 14 * torus_scale_factor, weight: FontWeight.Bold), + Font = OsuFont.Torus.With(size: 14f, weight: FontWeight.Bold), Colour = Color4.Black, } }; diff --git a/osu.Game/Screens/SelectV2/Footer/ScreenFooterButtonRandom.cs b/osu.Game/Screens/SelectV2/Footer/ScreenFooterButtonRandom.cs index e8e850a9ce..dbdb6fe79b 100644 --- a/osu.Game/Screens/SelectV2/Footer/ScreenFooterButtonRandom.cs +++ b/osu.Game/Screens/SelectV2/Footer/ScreenFooterButtonRandom.cs @@ -42,7 +42,7 @@ namespace osu.Game.Screens.SelectV2.Footer { randomSpriteText = new OsuSpriteText { - Font = OsuFont.TorusAlternate.With(size: 19), + Font = OsuFont.TorusAlternate.With(size: 16), AlwaysPresent = true, Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, @@ -50,7 +50,7 @@ namespace osu.Game.Screens.SelectV2.Footer }, rewindSpriteText = new OsuSpriteText { - Font = OsuFont.TorusAlternate.With(size: 19), + Font = OsuFont.TorusAlternate.With(size: 16), AlwaysPresent = true, Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, @@ -75,7 +75,7 @@ namespace osu.Game.Screens.SelectV2.Footer AlwaysPresent = true, // make sure the button is sized large enough to always show this Anchor = Anchor.BottomCentre, Origin = Anchor.BottomCentre, - Font = OsuFont.TorusAlternate.With(size: 19), + Font = OsuFont.TorusAlternate.With(size: 16), }); fallingRewind.FadeOutFromOne(fade_time, Easing.In); From 68b8a4fb2ac3b06904695d0884717669a968e265 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 29 Jun 2024 08:35:48 +0300 Subject: [PATCH 1235/2493] Use `ScreenFooter` for displaying footer buttons from overlays --- .../Overlays/Mods/ShearedOverlayContainer.cs | 40 +++++++-- osu.Game/Screens/Footer/ScreenFooter.cs | 83 +++++++++++++++++-- osu.Game/Screens/Footer/ScreenFooterButton.cs | 10 ++- 3 files changed, 116 insertions(+), 17 deletions(-) diff --git a/osu.Game/Overlays/Mods/ShearedOverlayContainer.cs b/osu.Game/Overlays/Mods/ShearedOverlayContainer.cs index acdd1db728..fab2fccb35 100644 --- a/osu.Game/Overlays/Mods/ShearedOverlayContainer.cs +++ b/osu.Game/Overlays/Mods/ShearedOverlayContainer.cs @@ -3,6 +3,7 @@ #nullable disable +using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -11,19 +12,20 @@ using osu.Framework.Input.Events; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; +using osu.Game.Screens.Footer; namespace osu.Game.Overlays.Mods { /// - /// A sheared overlay which provides a header and footer and basic animations. - /// Exposes , and as valid targets for content. + /// A sheared overlay which provides a header and basic animations. + /// Exposes and as valid targets for content. /// public abstract partial class ShearedOverlayContainer : OsuFocusedOverlayContainer { - protected const float PADDING = 14; + public const float PADDING = 14; [Cached] - protected readonly OverlayColourProvider ColourProvider; + public readonly OverlayColourProvider ColourProvider; /// /// The overlay's header. @@ -35,6 +37,13 @@ namespace osu.Game.Overlays.Mods /// protected Container Footer { get; private set; } + [Resolved(canBeNull: true)] + [CanBeNull] + private ScreenFooter footer { get; set; } + + // todo: very temporary property that will be removed once ModSelectOverlay and FirstRunSetupOverlay are updated to use new footer. + public virtual bool UseNewFooter => false; + /// /// A container containing all content, including the header and footer. /// May be used for overlay-wide animations. @@ -65,7 +74,7 @@ namespace osu.Game.Overlays.Mods [BackgroundDependencyLoader] private void load() { - const float footer_height = 50; + const float footer_height = ScreenFooter.HEIGHT; Child = TopLevelContent = new Container { @@ -113,6 +122,11 @@ namespace osu.Game.Overlays.Mods }; } + /// + /// Creates content to be displayed on the game-wide footer. + /// + public virtual Drawable CreateFooterContent() => Empty(); + protected override bool OnClick(ClickEvent e) { if (State.Value == Visibility.Visible) @@ -131,7 +145,13 @@ namespace osu.Game.Overlays.Mods this.FadeIn(fade_in_duration, Easing.OutQuint); Header.MoveToY(0, fade_in_duration, Easing.OutQuint); - Footer.MoveToY(0, fade_in_duration, Easing.OutQuint); + + if (UseNewFooter && footer != null) + { + footer.SetOverlayContent(this); + } + else + Footer.MoveToY(0, fade_in_duration, Easing.OutQuint); } protected override void PopOut() @@ -142,7 +162,13 @@ namespace osu.Game.Overlays.Mods this.FadeOut(fade_out_duration, Easing.OutQuint); Header.MoveToY(-Header.DrawHeight, fade_out_duration, Easing.OutQuint); - Footer.MoveToY(Footer.DrawHeight, fade_out_duration, Easing.OutQuint); + + if (UseNewFooter && footer != null) + { + footer.ClearOverlayContent(); + } + else + Footer.MoveToY(Footer.DrawHeight, fade_out_duration, Easing.OutQuint); } } } diff --git a/osu.Game/Screens/Footer/ScreenFooter.cs b/osu.Game/Screens/Footer/ScreenFooter.cs index c7090ba344..7779b6a4e5 100644 --- a/osu.Game/Screens/Footer/ScreenFooter.cs +++ b/osu.Game/Screens/Footer/ScreenFooter.cs @@ -115,8 +115,11 @@ namespace osu.Game.Screens.Footer public void SetButtons(IReadOnlyList buttons) { + temporarilyHiddenButtons.Clear(); overlays.Clear(); + ClearOverlayContent(); + var oldButtons = buttonsFlow.ToArray(); for (int i = 0; i < oldButtons.Length; i++) @@ -127,9 +130,9 @@ namespace osu.Game.Screens.Footer removedButtonsContainer.Add(oldButton); if (buttons.Count > 0) - makeButtonDisappearToRightAndExpire(oldButton, i, oldButtons.Length); + makeButtonDisappearToRight(oldButton, i, oldButtons.Length, true); else - makeButtonDisappearToBottomAndExpire(oldButton, i, oldButtons.Length); + makeButtonDisappearToBottom(oldButton, i, oldButtons.Length, true); } for (int i = 0; i < buttons.Count; i++) @@ -158,17 +161,85 @@ namespace osu.Game.Screens.Footer } } + private ShearedOverlayContainer? activeOverlay; + private Container? contentContainer; + private readonly List temporarilyHiddenButtons = new List(); + + public void SetOverlayContent(ShearedOverlayContainer overlay) + { + if (contentContainer != null) + { + throw new InvalidOperationException(@"Cannot set overlay content while one is already present. " + + $@"The previous overlay whose content is {contentContainer.Child.GetType().Name} should be hidden first."); + } + + activeOverlay = overlay; + + Debug.Assert(temporarilyHiddenButtons.Count == 0); + + var targetButton = buttonsFlow.SingleOrDefault(b => b.Overlay == overlay); + + temporarilyHiddenButtons.AddRange(targetButton != null + ? buttonsFlow.SkipWhile(b => b != targetButton).Skip(1) + : buttonsFlow); + + for (int i = 0; i < temporarilyHiddenButtons.Count; i++) + makeButtonDisappearToBottom(temporarilyHiddenButtons[i], 0, 0, false); + + var fallbackPosition = buttonsFlow.Any() + ? buttonsFlow.ToSpaceOfOtherDrawable(Vector2.Zero, this) + : BackButton.ToSpaceOfOtherDrawable(BackButton.LayoutRectangle.TopRight + new Vector2(5f, 0f), this); + + var targetPosition = targetButton?.ToSpaceOfOtherDrawable(targetButton.LayoutRectangle.TopRight, this) ?? fallbackPosition; + + var content = overlay.CreateFooterContent(); + + Add(contentContainer = new Container + { + Y = -15f, + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Left = targetPosition.X }, + Child = content, + }); + + if (temporarilyHiddenButtons.Count > 0) + this.Delay(60).Schedule(() => content.Show()); + else + content.Show(); + } + + public void ClearOverlayContent() + { + if (contentContainer == null) + return; + + contentContainer.Child.Hide(); + + double timeUntilRun = contentContainer.Child.LatestTransformEndTime - Time.Current; + + Container expireTarget = contentContainer; + contentContainer = null; + activeOverlay = null; + + for (int i = 0; i < temporarilyHiddenButtons.Count; i++) + makeButtonAppearFromBottom(temporarilyHiddenButtons[i], 0); + + temporarilyHiddenButtons.Clear(); + + expireTarget.Delay(timeUntilRun).Expire(); + } + private void makeButtonAppearFromLeft(ScreenFooterButton button, int index, int count, float startDelay) => button.AppearFromLeft(startDelay + (count - index) * delay_per_button); private void makeButtonAppearFromBottom(ScreenFooterButton button, int index) => button.AppearFromBottom(index * delay_per_button); - private void makeButtonDisappearToRightAndExpire(ScreenFooterButton button, int index, int count) - => button.DisappearToRightAndExpire((count - index) * delay_per_button); + private void makeButtonDisappearToRight(ScreenFooterButton button, int index, int count, bool expire) + => button.DisappearToRight((count - index) * delay_per_button, expire); - private void makeButtonDisappearToBottomAndExpire(ScreenFooterButton button, int index, int count) - => button.DisappearToBottomAndExpire((count - index) * delay_per_button); + private void makeButtonDisappearToBottom(ScreenFooterButton button, int index, int count, bool expire) + => button.DisappearToBottom((count - index) * delay_per_button, expire); private void showOverlay(OverlayContainer overlay) { diff --git a/osu.Game/Screens/Footer/ScreenFooterButton.cs b/osu.Game/Screens/Footer/ScreenFooterButton.cs index cd1de2454b..c1dbbb071d 100644 --- a/osu.Game/Screens/Footer/ScreenFooterButton.cs +++ b/osu.Game/Screens/Footer/ScreenFooterButton.cs @@ -243,22 +243,24 @@ namespace osu.Game.Screens.Footer .FadeIn(240, Easing.OutCubic); } - public void DisappearToRightAndExpire(double delay) + public void DisappearToRight(double delay, bool expire) { Content.Delay(delay) .FadeOut(240, Easing.InOutCubic) .MoveToX(300f, 360, Easing.InOutCubic); - this.Delay(Content.LatestTransformEndTime - Time.Current).Expire(); + if (expire) + this.Delay(Content.LatestTransformEndTime - Time.Current).Expire(); } - public void DisappearToBottomAndExpire(double delay) + public void DisappearToBottom(double delay, bool expire) { Content.Delay(delay) .FadeOut(240, Easing.InOutCubic) .MoveToY(100f, 240, Easing.InOutCubic); - this.Delay(Content.LatestTransformEndTime - Time.Current).Expire(); + if (expire) + this.Delay(Content.LatestTransformEndTime - Time.Current).Expire(); } } } From 916d0bfcc26f6de154dc729b5b8cfef0debf6d02 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 29 Jun 2024 08:39:34 +0300 Subject: [PATCH 1236/2493] Temporarily show screen footer if hidden while overlay is present --- osu.Game/Overlays/Mods/ShearedOverlayContainer.cs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/osu.Game/Overlays/Mods/ShearedOverlayContainer.cs b/osu.Game/Overlays/Mods/ShearedOverlayContainer.cs index fab2fccb35..d3b1b9244b 100644 --- a/osu.Game/Overlays/Mods/ShearedOverlayContainer.cs +++ b/osu.Game/Overlays/Mods/ShearedOverlayContainer.cs @@ -138,6 +138,8 @@ namespace osu.Game.Overlays.Mods return base.OnClick(e); } + private bool hideFooterOnPopOut; + protected override void PopIn() { const double fade_in_duration = 400; @@ -149,6 +151,12 @@ namespace osu.Game.Overlays.Mods if (UseNewFooter && footer != null) { footer.SetOverlayContent(this); + + if (footer.State.Value == Visibility.Hidden) + { + footer.Show(); + hideFooterOnPopOut = true; + } } else Footer.MoveToY(0, fade_in_duration, Easing.OutQuint); @@ -166,6 +174,12 @@ namespace osu.Game.Overlays.Mods if (UseNewFooter && footer != null) { footer.ClearOverlayContent(); + + if (hideFooterOnPopOut) + { + footer.Hide(); + hideFooterOnPopOut = false; + } } else Footer.MoveToY(Footer.DrawHeight, fade_out_duration, Easing.OutQuint); From 2319fa11ec640f3922590e9f10e2858d6ebb9168 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 29 Jun 2024 08:42:17 +0300 Subject: [PATCH 1237/2493] Support performing custom overlay-specific action with back button --- .../Overlays/Mods/ShearedOverlayContainer.cs | 6 ++++ osu.Game/Screens/Footer/ScreenFooter.cs | 31 +++++++++++++------ 2 files changed, 28 insertions(+), 9 deletions(-) diff --git a/osu.Game/Overlays/Mods/ShearedOverlayContainer.cs b/osu.Game/Overlays/Mods/ShearedOverlayContainer.cs index d3b1b9244b..c9c3c62404 100644 --- a/osu.Game/Overlays/Mods/ShearedOverlayContainer.cs +++ b/osu.Game/Overlays/Mods/ShearedOverlayContainer.cs @@ -127,6 +127,12 @@ namespace osu.Game.Overlays.Mods /// public virtual Drawable CreateFooterContent() => Empty(); + /// + /// Invoked when the back button in the footer is pressed. + /// + /// Whether the back button should not close the overlay. + public virtual bool OnBackButton() => false; + protected override bool OnClick(ClickEvent e) { if (State.Value == Visibility.Visible) diff --git a/osu.Game/Screens/Footer/ScreenFooter.cs b/osu.Game/Screens/Footer/ScreenFooter.cs index 7779b6a4e5..d6c98d1c64 100644 --- a/osu.Game/Screens/Footer/ScreenFooter.cs +++ b/osu.Game/Screens/Footer/ScreenFooter.cs @@ -28,7 +28,7 @@ namespace osu.Game.Screens.Footer private readonly List overlays = new List(); - private ScreenBackButton backButton = null!; + private Box background = null!; private FillFlowContainer buttonsFlow = null!; private Container removedButtonsContainer = null!; private LogoTrackingContainer logoTrackingContainer = null!; @@ -36,6 +36,8 @@ namespace osu.Game.Screens.Footer [Cached] private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine); + public ScreenBackButton BackButton { get; private set; } = null!; + public Action? OnBack; public ScreenFooter(BackReceptor? receptor = null) @@ -48,7 +50,7 @@ namespace osu.Game.Screens.Footer if (receptor == null) Add(receptor = new BackReceptor()); - receptor.OnBackPressed = () => backButton.TriggerClick(); + receptor.OnBackPressed = () => BackButton.TriggerClick(); } [BackgroundDependencyLoader] @@ -71,12 +73,12 @@ namespace osu.Game.Screens.Footer Spacing = new Vector2(7, 0), AutoSizeAxes = Axes.Both }, - backButton = new ScreenBackButton + BackButton = new ScreenBackButton { Margin = new MarginPadding { Bottom = 15f, Left = 12f }, Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, - Action = () => OnBack?.Invoke(), + Action = onBackPressed, }, removedButtonsContainer = new Container { @@ -243,13 +245,24 @@ namespace osu.Game.Screens.Footer private void showOverlay(OverlayContainer overlay) { - foreach (var o in overlays) + foreach (var o in overlays.Where(o => o != overlay)) + o.Hide(); + + overlay.ToggleVisibility(); + } + + private void onBackPressed() + { + if (activeOverlay != null) { - if (o == overlay) - o.ToggleVisibility(); - else - o.Hide(); + if (activeOverlay.OnBackButton()) + return; + + activeOverlay.Hide(); + return; } + + OnBack?.Invoke(); } public partial class BackReceptor : Drawable, IKeyBindingHandler From b8816bfc28f2ccc527f6d6ae9650749777cbe637 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 29 Jun 2024 09:59:40 +0300 Subject: [PATCH 1238/2493] Update colour scheme of footer in line with visible overlay --- osu.Game/Overlays/OverlayColourProvider.cs | 16 +++++++++++++--- osu.Game/Screens/Footer/ScreenFooter.cs | 17 ++++++++++++++++- osu.Game/Screens/Footer/ScreenFooterButton.cs | 10 +++++----- 3 files changed, 34 insertions(+), 9 deletions(-) diff --git a/osu.Game/Overlays/OverlayColourProvider.cs b/osu.Game/Overlays/OverlayColourProvider.cs index a4f6527024..06b42eafc0 100644 --- a/osu.Game/Overlays/OverlayColourProvider.cs +++ b/osu.Game/Overlays/OverlayColourProvider.cs @@ -9,11 +9,11 @@ namespace osu.Game.Overlays { public class OverlayColourProvider { - private readonly OverlayColourScheme colourScheme; + public OverlayColourScheme ColourScheme { get; private set; } public OverlayColourProvider(OverlayColourScheme colourScheme) { - this.colourScheme = colourScheme; + ColourScheme = colourScheme; } // Note that the following five colours are also defined in `OsuColour` as `{colourScheme}{0,1,2,3,4}`. @@ -47,7 +47,17 @@ namespace osu.Game.Overlays public Color4 Background5 => getColour(0.1f, 0.15f); public Color4 Background6 => getColour(0.1f, 0.1f); - private Color4 getColour(float saturation, float lightness) => Color4.FromHsl(new Vector4(getBaseHue(colourScheme), saturation, lightness, 1)); + /// + /// Changes the value of to a different colour scheme. + /// Note that this does not trigger any kind of signal to any drawable that received colours from here, all drawables need to be updated manually. + /// + /// The proposed colour scheme. + public void ChangeColourScheme(OverlayColourScheme colourScheme) + { + ColourScheme = colourScheme; + } + + private Color4 getColour(float saturation, float lightness) => Color4.FromHsl(new Vector4(getBaseHue(ColourScheme), saturation, lightness, 1)); // See https://github.com/ppy/osu-web/blob/5a536d217a21582aad999db50a981003d3ad5659/app/helpers.php#L1620-L1628 private static float getBaseHue(OverlayColourScheme colourScheme) diff --git a/osu.Game/Screens/Footer/ScreenFooter.cs b/osu.Game/Screens/Footer/ScreenFooter.cs index d6c98d1c64..cef891f8c0 100644 --- a/osu.Game/Screens/Footer/ScreenFooter.cs +++ b/osu.Game/Screens/Footer/ScreenFooter.cs @@ -14,6 +14,7 @@ using osu.Framework.Input.Events; using osu.Game.Graphics.Containers; using osu.Game.Input.Bindings; using osu.Game.Overlays; +using osu.Game.Overlays.Mods; using osu.Game.Screens.Menu; using osuTK; @@ -58,7 +59,7 @@ namespace osu.Game.Screens.Footer { InternalChildren = new Drawable[] { - new Box + background = new Box { RelativeSizeAxes = Axes.Both, Colour = colourProvider.Background5 @@ -194,6 +195,8 @@ namespace osu.Game.Screens.Footer var targetPosition = targetButton?.ToSpaceOfOtherDrawable(targetButton.LayoutRectangle.TopRight, this) ?? fallbackPosition; + updateColourScheme(overlay.ColourProvider.ColourScheme); + var content = overlay.CreateFooterContent(); Add(contentContainer = new Container @@ -229,6 +232,18 @@ namespace osu.Game.Screens.Footer temporarilyHiddenButtons.Clear(); expireTarget.Delay(timeUntilRun).Expire(); + + updateColourScheme(OverlayColourScheme.Aquamarine); + } + + private void updateColourScheme(OverlayColourScheme colourScheme) + { + colourProvider.ChangeColourScheme(colourScheme); + + background.FadeColour(colourProvider.Background5, 150, Easing.OutQuint); + + foreach (var button in buttonsFlow) + button.UpdateDisplay(); } private void makeButtonAppearFromLeft(ScreenFooterButton button, int index, int count, float startDelay) diff --git a/osu.Game/Screens/Footer/ScreenFooterButton.cs b/osu.Game/Screens/Footer/ScreenFooterButton.cs index c1dbbb071d..0be7ef95b5 100644 --- a/osu.Game/Screens/Footer/ScreenFooterButton.cs +++ b/osu.Game/Screens/Footer/ScreenFooterButton.cs @@ -166,8 +166,8 @@ namespace osu.Game.Screens.Footer if (Overlay != null) OverlayState.BindTo(Overlay.State); - OverlayState.BindValueChanged(_ => updateDisplay()); - Enabled.BindValueChanged(_ => updateDisplay(), true); + OverlayState.BindValueChanged(_ => UpdateDisplay()); + Enabled.BindValueChanged(_ => UpdateDisplay(), true); FinishTransforms(true); } @@ -186,11 +186,11 @@ namespace osu.Game.Screens.Footer protected override bool OnHover(HoverEvent e) { - updateDisplay(); + UpdateDisplay(); return true; } - protected override void OnHoverLost(HoverLostEvent e) => updateDisplay(); + protected override void OnHoverLost(HoverLostEvent e) => UpdateDisplay(); public virtual bool OnPressed(KeyBindingPressEvent e) { @@ -202,7 +202,7 @@ namespace osu.Game.Screens.Footer public virtual void OnReleased(KeyBindingReleaseEvent e) { } - private void updateDisplay() + public void UpdateDisplay() { Color4 backgroundColour = OverlayState.Value == Visibility.Visible ? buttonAccentColour : colourProvider.Background3; Color4 textColour = OverlayState.Value == Visibility.Visible ? colourProvider.Background6 : colourProvider.Content1; From fb77260afc617d2e0f3039987e8168a862535bd6 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 29 Jun 2024 10:06:49 +0300 Subject: [PATCH 1239/2493] Fix footer buttons receiving input while put away from screen --- osu.Game/Screens/Footer/ScreenFooterButton.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Screens/Footer/ScreenFooterButton.cs b/osu.Game/Screens/Footer/ScreenFooterButton.cs index 0be7ef95b5..b39e1e11c3 100644 --- a/osu.Game/Screens/Footer/ScreenFooterButton.cs +++ b/osu.Game/Screens/Footer/ScreenFooterButton.cs @@ -172,6 +172,9 @@ namespace osu.Game.Screens.Footer FinishTransforms(true); } + // use Content for tracking input as some buttons might be temporarily hidden with DisappearToBottom, and they become hidden by moving Content away from screen. + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => Content.ReceivePositionalInputAt(screenSpacePos); + public GlobalAction? Hotkey; protected override bool OnClick(ClickEvent e) From 56d1255f8ae819f6cd74d8fe8f61aa975ac32cde Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sun, 30 Jun 2024 06:15:50 +0300 Subject: [PATCH 1240/2493] Fix footer button transforms getting interrupted by consecutive method calls --- osu.Game/Screens/Footer/ScreenFooterButton.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/Screens/Footer/ScreenFooterButton.cs b/osu.Game/Screens/Footer/ScreenFooterButton.cs index b39e1e11c3..a88ba1aead 100644 --- a/osu.Game/Screens/Footer/ScreenFooterButton.cs +++ b/osu.Game/Screens/Footer/ScreenFooterButton.cs @@ -230,6 +230,7 @@ namespace osu.Game.Screens.Footer public void AppearFromLeft(double delay) { + Content.FinishTransforms(); Content.MoveToX(-300f) .FadeOut() .Delay(delay) @@ -239,6 +240,7 @@ namespace osu.Game.Screens.Footer public void AppearFromBottom(double delay) { + Content.FinishTransforms(); Content.MoveToY(100f) .FadeOut() .Delay(delay) @@ -248,6 +250,7 @@ namespace osu.Game.Screens.Footer public void DisappearToRight(double delay, bool expire) { + Content.FinishTransforms(); Content.Delay(delay) .FadeOut(240, Easing.InOutCubic) .MoveToX(300f, 360, Easing.InOutCubic); @@ -258,6 +261,7 @@ namespace osu.Game.Screens.Footer public void DisappearToBottom(double delay, bool expire) { + Content.FinishTransforms(); Content.Delay(delay) .FadeOut(240, Easing.InOutCubic) .MoveToY(100f, 240, Easing.InOutCubic); From 900d15e777fab79d533ea7b31dcfeaac5f410d4a Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 29 Jun 2024 10:19:58 +0300 Subject: [PATCH 1241/2493] Add test coverage --- .../UserInterface/TestSceneScreenFooter.cs | 176 ++++++++++++++++-- osu.Game/Screens/Footer/ScreenFooterButton.cs | 4 +- 2 files changed, 167 insertions(+), 13 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneScreenFooter.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneScreenFooter.cs index dabb2e7f50..70c3664b9a 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneScreenFooter.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneScreenFooter.cs @@ -2,10 +2,16 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Linq; using NUnit.Framework; +using osu.Framework.Allocation; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; +using osu.Framework.Graphics.Sprites; using osu.Framework.Testing; +using osu.Game.Graphics; +using osu.Game.Graphics.UserInterface; using osu.Game.Overlays; using osu.Game.Overlays.Mods; using osu.Game.Screens.Footer; @@ -15,25 +21,31 @@ namespace osu.Game.Tests.Visual.UserInterface { public partial class TestSceneScreenFooter : OsuManualInputManagerTestScene { + private DependencyProvidingContainer contentContainer = null!; private ScreenFooter screenFooter = null!; private TestModSelectOverlay overlay = null!; [SetUp] public void SetUp() => Schedule(() => { - Children = new Drawable[] + screenFooter = new ScreenFooter(); + + Child = contentContainer = new DependencyProvidingContainer { - overlay = new TestModSelectOverlay + RelativeSizeAxes = Axes.Both, + CachedDependencies = new (Type, object)[] { - Padding = new MarginPadding - { - Bottom = ScreenFooter.HEIGHT - } + (typeof(ScreenFooter), screenFooter) }, - new PopoverContainer + Children = new Drawable[] { - RelativeSizeAxes = Axes.Both, - Child = screenFooter = new ScreenFooter(), + overlay = new TestModSelectOverlay(), + new PopoverContainer + { + RelativeSizeAxes = Axes.Both, + Depth = float.MinValue, + Child = screenFooter, + }, }, }; @@ -82,14 +94,156 @@ namespace osu.Game.Tests.Visual.UserInterface })); } + [Test] + public void TestExternalOverlayContent() + { + TestShearedOverlayContainer externalOverlay = null!; + + AddStep("add overlay", () => contentContainer.Add(externalOverlay = new TestShearedOverlayContainer())); + AddStep("set buttons", () => screenFooter.SetButtons(new[] + { + new ScreenFooterButton(externalOverlay) + { + AccentColour = Dependencies.Get().Orange1, + Icon = FontAwesome.Solid.Toolbox, + Text = "One", + }, + new ScreenFooterButton { Text = "Two", Action = () => { } }, + new ScreenFooterButton { Text = "Three", Action = () => { } }, + })); + AddWaitStep("wait for transition", 3); + + AddStep("show overlay", () => externalOverlay.Show()); + AddAssert("content displayed in footer", () => screenFooter.ChildrenOfType().Single().IsPresent); + AddUntilStep("other buttons hidden", () => screenFooter.ChildrenOfType().Skip(1).All(b => b.Child.Parent!.Y > 0)); + + AddStep("hide overlay", () => externalOverlay.Hide()); + AddUntilStep("content hidden from footer", () => screenFooter.ChildrenOfType().SingleOrDefault()?.IsPresent != true); + AddUntilStep("other buttons returned", () => screenFooter.ChildrenOfType().Skip(1).All(b => b.ChildrenOfType().First().Y == 0)); + } + + [Test] + public void TestTemporarilyShowFooter() + { + TestShearedOverlayContainer externalOverlay = null!; + + AddStep("hide footer", () => screenFooter.Hide()); + AddStep("remove buttons", () => screenFooter.SetButtons(Array.Empty())); + + AddStep("add external overlay", () => contentContainer.Add(externalOverlay = new TestShearedOverlayContainer())); + AddStep("show external overlay", () => externalOverlay.Show()); + AddAssert("footer shown", () => screenFooter.State.Value == Visibility.Visible); + AddAssert("content displayed in footer", () => screenFooter.ChildrenOfType().Single().IsPresent); + + AddStep("hide external overlay", () => externalOverlay.Hide()); + AddAssert("footer hidden", () => screenFooter.State.Value == Visibility.Hidden); + AddUntilStep("content hidden from footer", () => screenFooter.ChildrenOfType().SingleOrDefault()?.IsPresent != true); + + AddStep("show footer", () => screenFooter.Show()); + AddAssert("content still hidden from footer", () => screenFooter.ChildrenOfType().SingleOrDefault()?.IsPresent != true); + + AddStep("show external overlay", () => externalOverlay.Show()); + AddAssert("footer still visible", () => screenFooter.State.Value == Visibility.Visible); + + AddStep("hide external overlay", () => externalOverlay.Hide()); + AddAssert("footer still visible", () => screenFooter.State.Value == Visibility.Visible); + + AddStep("hide footer", () => screenFooter.Hide()); + AddStep("show external overlay", () => externalOverlay.Show()); + } + + [Test] + public void TestBackButton() + { + TestShearedOverlayContainer externalOverlay = null!; + + AddStep("hide footer", () => screenFooter.Hide()); + AddStep("remove buttons", () => screenFooter.SetButtons(Array.Empty())); + + AddStep("add external overlay", () => contentContainer.Add(externalOverlay = new TestShearedOverlayContainer())); + AddStep("show external overlay", () => externalOverlay.Show()); + AddAssert("footer shown", () => screenFooter.State.Value == Visibility.Visible); + + AddStep("press back", () => this.ChildrenOfType().Single().TriggerClick()); + AddAssert("overlay hidden", () => externalOverlay.State.Value == Visibility.Hidden); + AddAssert("footer hidden", () => screenFooter.State.Value == Visibility.Hidden); + + AddStep("show external overlay", () => externalOverlay.Show()); + AddStep("set block count", () => externalOverlay.BackButtonCount = 1); + AddStep("press back", () => this.ChildrenOfType().Single().TriggerClick()); + AddAssert("overlay still visible", () => externalOverlay.State.Value == Visibility.Visible); + AddAssert("footer still shown", () => screenFooter.State.Value == Visibility.Visible); + AddStep("press back again", () => this.ChildrenOfType().Single().TriggerClick()); + AddAssert("overlay hidden", () => externalOverlay.State.Value == Visibility.Hidden); + AddAssert("footer hidden", () => screenFooter.State.Value == Visibility.Hidden); + } + private partial class TestModSelectOverlay : UserModSelectOverlay { protected override bool ShowPresets => true; + } - public TestModSelectOverlay() - : base(OverlayColourScheme.Aquamarine) + private partial class TestShearedOverlayContainer : ShearedOverlayContainer + { + public override bool UseNewFooter => true; + + public TestShearedOverlayContainer() + : base(OverlayColourScheme.Orange) { } + + [BackgroundDependencyLoader] + private void load() + { + Header.Title = "Test overlay"; + Header.Description = "An overlay that is made purely for testing purposes."; + } + + public int BackButtonCount; + + public override bool OnBackButton() + { + if (BackButtonCount > 0) + { + BackButtonCount--; + return true; + } + + return false; + } + + public override Drawable CreateFooterContent() => new TestFooterContent(); + + public partial class TestFooterContent : VisibilityContainer + { + [BackgroundDependencyLoader] + private void load() + { + RelativeSizeAxes = Axes.Both; + + InternalChild = new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Children = new[] + { + new ShearedButton(200) { Text = "Action #1", Action = () => { } }, + new ShearedButton(140) { Text = "Action #2", Action = () => { } }, + } + }; + } + + protected override void PopIn() + { + this.MoveToY(0, 400, Easing.OutQuint) + .FadeIn(400, Easing.OutQuint); + } + + protected override void PopOut() + { + this.MoveToY(-20f, 200, Easing.OutQuint) + .FadeOut(200, Easing.OutQuint); + } + } } } } diff --git a/osu.Game/Screens/Footer/ScreenFooterButton.cs b/osu.Game/Screens/Footer/ScreenFooterButton.cs index a88ba1aead..6515203ca0 100644 --- a/osu.Game/Screens/Footer/ScreenFooterButton.cs +++ b/osu.Game/Screens/Footer/ScreenFooterButton.cs @@ -40,7 +40,7 @@ namespace osu.Game.Screens.Footer private Colour4 buttonAccentColour; - protected Colour4 AccentColour + public Colour4 AccentColour { set { @@ -50,7 +50,7 @@ namespace osu.Game.Screens.Footer } } - protected IconUsage Icon + public IconUsage Icon { set => icon.Icon = value; } From 467d7c4f54fc833be0d92bc941db11224155b787 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 29 Jun 2024 10:17:40 +0300 Subject: [PATCH 1242/2493] Refactor game-wide layout order of footer to fix depth issues with overlays and improve UX With this new order, the logo can be easily moved to display in front of the footer in `SongSelectV2` without breaking experience when footer-based overlays are present. Such overlays (i.e. mod select overlay) will also be dimmed alongside the current screen when a game-wide overlay is open (e.g. settings). --- .../SongSelect/TestSceneSongSelectV2.cs | 6 --- osu.Game/OsuGame.cs | 31 ++++++++++---- .../Overlays/Mods/ShearedOverlayContainer.cs | 24 +++++++---- osu.Game/Screens/Footer/ScreenFooter.cs | 33 ++++++++++++--- osu.Game/Screens/SelectV2/SongSelectV2.cs | 41 ++++++++----------- 5 files changed, 82 insertions(+), 53 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneSongSelectV2.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneSongSelectV2.cs index 674eaa2ff8..0a632793cc 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneSongSelectV2.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneSongSelectV2.cs @@ -181,12 +181,6 @@ namespace osu.Game.Tests.Visual.SongSelect #endregion - protected override void Update() - { - base.Update(); - Stack.Padding = new MarginPadding { Bottom = screenScreenFooter.DrawHeight - screenScreenFooter.Y }; - } - private void updateFooter(IScreen? _, IScreen? newScreen) { if (newScreen is IOsuScreen osuScreen && osuScreen.ShowFooter) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index cf32daab00..3862fea0e2 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -51,6 +51,7 @@ using osu.Game.Online.Chat; using osu.Game.Online.Rooms; using osu.Game.Overlays; using osu.Game.Overlays.BeatmapListing; +using osu.Game.Overlays.Mods; using osu.Game.Overlays.Music; using osu.Game.Overlays.Notifications; using osu.Game.Overlays.SkinEditor; @@ -132,8 +133,12 @@ namespace osu.Game private Container topMostOverlayContent; + private Container footerBasedOverlayContent; + protected ScalingContainer ScreenContainer { get; private set; } + private Container logoContainer; + protected Container ScreenOffsetContainer { get; private set; } private Container overlayOffsetContainer; @@ -156,8 +161,6 @@ namespace osu.Game private float toolbarOffset => (Toolbar?.Position.Y ?? 0) + (Toolbar?.DrawHeight ?? 0); - private float screenFooterOffset => (ScreenFooter?.DrawHeight ?? 0) - (ScreenFooter?.Position.Y ?? 0); - private IdleTracker idleTracker; /// @@ -242,7 +245,11 @@ namespace osu.Game throw new ArgumentException($@"{overlayContainer} has already been registered via {nameof(IOverlayManager.RegisterBlockingOverlay)} once."); externalOverlays.Add(overlayContainer); - overlayContent.Add(overlayContainer); + + if (overlayContainer is ShearedOverlayContainer) + footerBasedOverlayContent.Add(overlayContainer); + else + overlayContent.Add(overlayContainer); if (overlayContainer is OsuFocusedOverlayContainer focusedOverlayContainer) focusedOverlays.Add(focusedOverlayContainer); @@ -290,6 +297,8 @@ namespace osu.Game if (hideToolbar) Toolbar.Hide(); } + public void ChangeLogoDepth(bool inFrontOfFooter) => ScreenContainer.ChangeChildDepth(logoContainer, inFrontOfFooter ? float.MinValue : 0); + protected override UserInputManager CreateUserInputManager() { var userInputManager = base.CreateUserInputManager(); @@ -934,7 +943,6 @@ namespace osu.Game return string.Join(" / ", combinations); }; - Container logoContainer; ScreenFooter.BackReceptor backReceptor; dependencies.CacheAs(idleTracker = new GameIdleTracker(6000)); @@ -976,8 +984,15 @@ namespace osu.Game Origin = Anchor.BottomLeft, Action = () => ScreenFooter.OnBack?.Invoke(), }, + logoContainer = new Container { RelativeSizeAxes = Axes.Both }, + footerBasedOverlayContent = new Container + { + Depth = -1, + RelativeSizeAxes = Axes.Both, + }, new PopoverContainer { + Depth = -1, RelativeSizeAxes = Axes.Both, Child = ScreenFooter = new ScreenFooter(backReceptor) { @@ -991,7 +1006,6 @@ namespace osu.Game } }, }, - logoContainer = new Container { RelativeSizeAxes = Axes.Both }, } }, } @@ -1025,7 +1039,7 @@ namespace osu.Game if (!IsDeployedBuild) { - dependencies.Cache(versionManager = new VersionManager { Depth = int.MinValue }); + dependencies.Cache(versionManager = new VersionManager()); loadComponentSingleFile(versionManager, ScreenContainer.Add); } @@ -1072,7 +1086,7 @@ namespace osu.Game loadComponentSingleFile(CreateUpdateManager(), Add, true); // overlay elements - loadComponentSingleFile(FirstRunOverlay = new FirstRunSetupOverlay(), overlayContent.Add, true); + loadComponentSingleFile(FirstRunOverlay = new FirstRunSetupOverlay(), footerBasedOverlayContent.Add, true); loadComponentSingleFile(new ManageCollectionsDialog(), overlayContent.Add, true); loadComponentSingleFile(beatmapListing = new BeatmapListingOverlay(), overlayContent.Add, true); loadComponentSingleFile(dashboard = new DashboardOverlay(), overlayContent.Add, true); @@ -1137,7 +1151,7 @@ namespace osu.Game } // ensure only one of these overlays are open at once. - var singleDisplayOverlays = new OverlayContainer[] { FirstRunOverlay, chatOverlay, news, dashboard, beatmapListing, changelogOverlay, rankingsOverlay, wikiOverlay }; + var singleDisplayOverlays = new OverlayContainer[] { chatOverlay, news, dashboard, beatmapListing, changelogOverlay, rankingsOverlay, wikiOverlay }; foreach (var overlay in singleDisplayOverlays) { @@ -1485,7 +1499,6 @@ namespace osu.Game ScreenOffsetContainer.Padding = new MarginPadding { Top = toolbarOffset }; overlayOffsetContainer.Padding = new MarginPadding { Top = toolbarOffset }; - ScreenStack.Padding = new MarginPadding { Bottom = screenFooterOffset }; float horizontalOffset = 0f; diff --git a/osu.Game/Overlays/Mods/ShearedOverlayContainer.cs b/osu.Game/Overlays/Mods/ShearedOverlayContainer.cs index c9c3c62404..b5435e7e58 100644 --- a/osu.Game/Overlays/Mods/ShearedOverlayContainer.cs +++ b/osu.Game/Overlays/Mods/ShearedOverlayContainer.cs @@ -1,10 +1,8 @@ // 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 JetBrains.Annotations; using osu.Framework.Allocation; +using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; @@ -30,16 +28,15 @@ namespace osu.Game.Overlays.Mods /// /// The overlay's header. /// - protected ShearedOverlayHeader Header { get; private set; } + protected ShearedOverlayHeader Header { get; private set; } = null!; /// /// The overlay's footer. /// protected Container Footer { get; private set; } - [Resolved(canBeNull: true)] - [CanBeNull] - private ScreenFooter footer { get; set; } + [Resolved] + private ScreenFooter? footer { get; set; } // todo: very temporary property that will be removed once ModSelectOverlay and FirstRunSetupOverlay are updated to use new footer. public virtual bool UseNewFooter => false; @@ -48,12 +45,12 @@ namespace osu.Game.Overlays.Mods /// A container containing all content, including the header and footer. /// May be used for overlay-wide animations. /// - protected Container TopLevelContent { get; private set; } + protected Container TopLevelContent { get; private set; } = null!; /// /// A container for content that is to be displayed between the header and footer. /// - protected Container MainAreaContent { get; private set; } + protected Container MainAreaContent { get; private set; } = null!; /// /// A container for content that is to be displayed inside the footer. @@ -64,6 +61,10 @@ namespace osu.Game.Overlays.Mods protected override bool BlockNonPositionalInput => true; + // ShearedOverlayContainers are placed at a layer within the screen container as they rely on ScreenFooter which must be placed there. + // Therefore, dimming must be managed locally, since DimMainContent dims the entire screen layer. + protected sealed override bool DimMainContent => false; + protected ShearedOverlayContainer(OverlayColourScheme colourScheme) { RelativeSizeAxes = Axes.Both; @@ -81,6 +82,11 @@ namespace osu.Game.Overlays.Mods RelativeSizeAxes = Axes.Both, Children = new Drawable[] { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = ColourProvider.Background6.Opacity(0.75f), + }, Header = new ShearedOverlayHeader { Anchor = Anchor.TopCentre, diff --git a/osu.Game/Screens/Footer/ScreenFooter.cs b/osu.Game/Screens/Footer/ScreenFooter.cs index cef891f8c0..dcf64e9291 100644 --- a/osu.Game/Screens/Footer/ScreenFooter.cs +++ b/osu.Game/Screens/Footer/ScreenFooter.cs @@ -11,6 +11,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; +using osu.Framework.Threading; using osu.Game.Graphics.Containers; using osu.Game.Input.Bindings; using osu.Game.Overlays; @@ -24,6 +25,7 @@ namespace osu.Game.Screens.Footer { private const int padding = 60; private const float delay_per_button = 30; + private const double transition_duration = 400; public const int HEIGHT = 50; @@ -37,6 +39,9 @@ namespace osu.Game.Screens.Footer [Cached] private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine); + [Resolved] + private OsuGame? game { get; set; } + public ScreenBackButton BackButton { get; private set; } = null!; public Action? OnBack; @@ -101,19 +106,35 @@ namespace osu.Game.Screens.Footer }; } - public void StartTrackingLogo(OsuLogo logo, float duration = 0, Easing easing = Easing.None) => logoTrackingContainer.StartTracking(logo, duration, easing); - public void StopTrackingLogo() => logoTrackingContainer.StopTracking(); + private ScheduledDelegate? changeLogoDepthDelegate; + + public void StartTrackingLogo(OsuLogo logo, float duration = 0, Easing easing = Easing.None) + { + changeLogoDepthDelegate?.Cancel(); + changeLogoDepthDelegate = null; + + logoTrackingContainer.StartTracking(logo, duration, easing); + game?.ChangeLogoDepth(inFrontOfFooter: true); + } + + public void StopTrackingLogo() + { + logoTrackingContainer.StopTracking(); + + if (game != null) + changeLogoDepthDelegate = Scheduler.AddDelayed(() => game.ChangeLogoDepth(inFrontOfFooter: false), transition_duration); + } protected override void PopIn() { - this.MoveToY(0, 400, Easing.OutQuint) - .FadeIn(400, Easing.OutQuint); + this.MoveToY(0, transition_duration, Easing.OutQuint) + .FadeIn(transition_duration, Easing.OutQuint); } protected override void PopOut() { - this.MoveToY(HEIGHT, 400, Easing.OutQuint) - .FadeOut(400, Easing.OutQuint); + this.MoveToY(HEIGHT, transition_duration, Easing.OutQuint) + .FadeOut(transition_duration, Easing.OutQuint); } public void SetButtons(IReadOnlyList buttons) diff --git a/osu.Game/Screens/SelectV2/SongSelectV2.cs b/osu.Game/Screens/SelectV2/SongSelectV2.cs index 10ed7783c4..a8730ad808 100644 --- a/osu.Game/Screens/SelectV2/SongSelectV2.cs +++ b/osu.Game/Screens/SelectV2/SongSelectV2.cs @@ -6,7 +6,6 @@ using System.Collections.Generic; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Cursor; using osu.Framework.Screens; using osu.Game.Overlays; using osu.Game.Overlays.Mods; @@ -23,6 +22,8 @@ namespace osu.Game.Screens.SelectV2 /// public partial class SongSelectV2 : OsuScreen { + private const float logo_scale = 0.4f; + private readonly ModSelectOverlay modSelectOverlay = new SoloModSelectOverlay(); [Cached] @@ -30,15 +31,14 @@ namespace osu.Game.Screens.SelectV2 public override bool ShowFooter => true; + [Resolved] + private OsuLogo? logo { get; set; } + [BackgroundDependencyLoader] private void load() { AddRangeInternal(new Drawable[] { - new PopoverContainer - { - RelativeSizeAxes = Axes.Both, - }, modSelectOverlay, }); } @@ -50,6 +50,17 @@ namespace osu.Game.Screens.SelectV2 new ScreenFooterButtonOptions(), }; + protected override void LoadComplete() + { + base.LoadComplete(); + + modSelectOverlay.State.BindValueChanged(v => + { + logo?.ScaleTo(v.NewValue == Visibility.Visible ? 0f : logo_scale, 400, Easing.OutQuint) + .FadeTo(v.NewValue == Visibility.Visible ? 0f : 1f, 200, Easing.OutQuint); + }, true); + } + public override void OnEntering(ScreenTransitionEvent e) { this.FadeIn(); @@ -74,17 +85,6 @@ namespace osu.Game.Screens.SelectV2 return base.OnExiting(e); } - public override bool OnBackButton() - { - if (modSelectOverlay.State.Value == Visibility.Visible) - { - modSelectOverlay.Hide(); - return true; - } - - return false; - } - protected override void LogoArriving(OsuLogo logo, bool resuming) { base.LogoArriving(logo, resuming); @@ -99,7 +99,7 @@ namespace osu.Game.Screens.SelectV2 } logo.FadeIn(240, Easing.OutQuint); - logo.ScaleTo(0.4f, 240, Easing.OutQuint); + logo.ScaleTo(logo_scale, 240, Easing.OutQuint); logo.Action = () => { @@ -122,14 +122,9 @@ namespace osu.Game.Screens.SelectV2 logo.FadeOut(120, Easing.Out); } - private partial class SoloModSelectOverlay : ModSelectOverlay + private partial class SoloModSelectOverlay : UserModSelectOverlay { protected override bool ShowPresets => true; - - public SoloModSelectOverlay() - : base(OverlayColourScheme.Aquamarine) - { - } } private partial class PlayerLoaderV2 : PlayerLoader From 48bf3f1385e6b99fd5bd0e8104d40487b22c781a Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 29 Jun 2024 08:38:43 +0300 Subject: [PATCH 1243/2493] Migrate mod select overlay footer content --- .../TestSceneModSelectOverlay.cs | 39 ++-- .../Overlays/Mods/ModSelectFooterContent.cs | 177 ++++++++++++++++++ osu.Game/Overlays/Mods/ModSelectOverlay.cs | 165 ++-------------- .../OnlinePlay/FreeModSelectOverlay.cs | 41 +++- .../Screens/OnlinePlay/Match/RoomSubScreen.cs | 2 +- osu.Game/Screens/Select/SongSelect.cs | 2 +- .../Tests/Visual/Gameplay/ScoringTestScene.cs | 1 - 7 files changed, 257 insertions(+), 170 deletions(-) create mode 100644 osu.Game/Overlays/Mods/ModSelectFooterContent.cs diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs index dedad3a40a..8b79320ffb 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs @@ -24,6 +24,7 @@ using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Taiko.Mods; +using osu.Game.Screens.Footer; using osu.Game.Tests.Mods; using osuTK; using osuTK.Input; @@ -93,12 +94,28 @@ namespace osu.Game.Tests.Visual.UserInterface private void createScreen() { - AddStep("create screen", () => Child = modSelectOverlay = new TestModSelectOverlay + AddStep("create screen", () => { - RelativeSizeAxes = Axes.Both, - State = { Value = Visibility.Visible }, - Beatmap = Beatmap.Value, - SelectedMods = { BindTarget = SelectedMods } + var receptor = new ScreenFooter.BackReceptor(); + var footer = new ScreenFooter(receptor); + + Child = new DependencyProvidingContainer + { + RelativeSizeAxes = Axes.Both, + CachedDependencies = new[] { (typeof(ScreenFooter), (object)footer) }, + Children = new Drawable[] + { + receptor, + modSelectOverlay = new TestModSelectOverlay + { + RelativeSizeAxes = Axes.Both, + State = { Value = Visibility.Visible }, + Beatmap = { Value = Beatmap.Value }, + SelectedMods = { BindTarget = SelectedMods }, + }, + footer, + } + }; }); waitForColumnLoad(); } @@ -119,7 +136,7 @@ namespace osu.Game.Tests.Visual.UserInterface AddAssert("mod multiplier correct", () => { double multiplier = SelectedMods.Value.Aggregate(1d, (m, mod) => m * mod.ScoreMultiplier); - return Precision.AlmostEquals(multiplier, modSelectOverlay.ChildrenOfType().Single().ModMultiplier.Value); + return Precision.AlmostEquals(multiplier, this.ChildrenOfType().Single().ModMultiplier.Value); }); assertCustomisationToggleState(disabled: false, active: false); AddAssert("setting items created", () => modSelectOverlay.ChildrenOfType().Any()); @@ -134,7 +151,7 @@ namespace osu.Game.Tests.Visual.UserInterface AddAssert("mod multiplier correct", () => { double multiplier = SelectedMods.Value.Aggregate(1d, (m, mod) => m * mod.ScoreMultiplier); - return Precision.AlmostEquals(multiplier, modSelectOverlay.ChildrenOfType().Single().ModMultiplier.Value); + return Precision.AlmostEquals(multiplier, this.ChildrenOfType().Single().ModMultiplier.Value); }); assertCustomisationToggleState(disabled: false, active: false); AddAssert("setting items created", () => modSelectOverlay.ChildrenOfType().Any()); @@ -756,7 +773,7 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep("click back button", () => { - InputManager.MoveMouseTo(modSelectOverlay.BackButton); + InputManager.MoveMouseTo(this.ChildrenOfType().Single()); InputManager.Click(MouseButton.Left); }); AddAssert("mod select hidden", () => modSelectOverlay.State.Value == Visibility.Hidden); @@ -884,7 +901,7 @@ namespace osu.Game.Tests.Visual.UserInterface InputManager.Click(MouseButton.Left); }); AddAssert("difficulty multiplier display shows correct value", - () => modSelectOverlay.ChildrenOfType().Single().ModMultiplier.Value, () => Is.EqualTo(0.1).Within(Precision.DOUBLE_EPSILON)); + () => this.ChildrenOfType().Single().ModMultiplier.Value, () => Is.EqualTo(0.1).Within(Precision.DOUBLE_EPSILON)); // this is highly unorthodox in a test, but because the `ModSettingChangeTracker` machinery heavily leans on events and object disposal and re-creation, // it is instrumental in the reproduction of the failure scenario that this test is supposed to cover. @@ -894,7 +911,7 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep("reset half time speed to default", () => modSelectOverlay.ChildrenOfType().Single() .ChildrenOfType>().Single().TriggerClick()); AddUntilStep("difficulty multiplier display shows correct value", - () => modSelectOverlay.ChildrenOfType().Single().ModMultiplier.Value, () => Is.EqualTo(0.3).Within(Precision.DOUBLE_EPSILON)); + () => this.ChildrenOfType().Single().ModMultiplier.Value, () => Is.EqualTo(0.3).Within(Precision.DOUBLE_EPSILON)); } [Test] @@ -1014,8 +1031,6 @@ namespace osu.Game.Tests.Visual.UserInterface private partial class TestModSelectOverlay : UserModSelectOverlay { protected override bool ShowPresets => true; - - public new ShearedButton BackButton => base.BackButton; } private class TestUnimplementedMod : Mod diff --git a/osu.Game/Overlays/Mods/ModSelectFooterContent.cs b/osu.Game/Overlays/Mods/ModSelectFooterContent.cs new file mode 100644 index 0000000000..146b8e4ebe --- /dev/null +++ b/osu.Game/Overlays/Mods/ModSelectFooterContent.cs @@ -0,0 +1,177 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Beatmaps; +using osu.Game.Configuration; +using osu.Game.Graphics.UserInterface; +using osu.Game.Rulesets.Mods; +using osuTK; + +namespace osu.Game.Overlays.Mods +{ + public partial class ModSelectFooterContent : VisibilityContainer + { + private readonly ModSelectOverlay overlay; + + private RankingInformationDisplay? rankingInformationDisplay; + private BeatmapAttributesDisplay? beatmapAttributesDisplay; + private FillFlowContainer buttonFlow = null!; + private FillFlowContainer contentFlow = null!; + + public DeselectAllModsButton? DeselectAllModsButton { get; set; } + + public readonly IBindable Beatmap = new Bindable(); + public readonly IBindable> ActiveMods = new Bindable>(); + + /// + /// Whether the effects (on score multiplier, on or beatmap difficulty) of the current selected set of mods should be shown. + /// + protected virtual bool ShowModEffects => true; + + /// + /// Whether the ranking information and beatmap attributes displays are stacked vertically due to small space. + /// + public bool DisplaysStackedVertically { get; private set; } + + public ModSelectFooterContent(ModSelectOverlay overlay) + { + this.overlay = overlay; + } + + [BackgroundDependencyLoader] + private void load() + { + RelativeSizeAxes = Axes.Both; + + InternalChild = buttonFlow = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Horizontal, + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Padding = new MarginPadding { Horizontal = 20 }, + Spacing = new Vector2(10), + ChildrenEnumerable = CreateButtons(), + }; + + if (ShowModEffects) + { + AddInternal(contentFlow = new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Spacing = new Vector2(30, 10), + Anchor = Anchor.BottomRight, + Origin = Anchor.BottomRight, + Margin = new MarginPadding { Horizontal = 20 }, + Children = new Drawable[] + { + rankingInformationDisplay = new RankingInformationDisplay + { + Anchor = Anchor.BottomRight, + Origin = Anchor.BottomRight + }, + beatmapAttributesDisplay = new BeatmapAttributesDisplay + { + Anchor = Anchor.BottomRight, + Origin = Anchor.BottomRight, + BeatmapInfo = { Value = Beatmap.Value?.BeatmapInfo }, + }, + } + }); + } + } + + private ModSettingChangeTracker? modSettingChangeTracker; + + protected override void LoadComplete() + { + base.LoadComplete(); + + Beatmap.BindValueChanged(b => + { + if (beatmapAttributesDisplay != null) + beatmapAttributesDisplay.BeatmapInfo.Value = b.NewValue?.BeatmapInfo; + }, true); + + ActiveMods.BindValueChanged(m => + { + updateInformation(); + + modSettingChangeTracker?.Dispose(); + + // Importantly, use ActiveMods.Value here (and not the ValueChanged NewValue) as the latter can + // potentially be stale, due to complexities in the way change trackers work. + // + // See https://github.com/ppy/osu/pull/23284#issuecomment-1529056988 + modSettingChangeTracker = new ModSettingChangeTracker(ActiveMods.Value); + modSettingChangeTracker.SettingChanged += _ => updateInformation(); + }, true); + } + + private void updateInformation() + { + if (rankingInformationDisplay != null) + { + double multiplier = 1.0; + + foreach (var mod in ActiveMods.Value) + multiplier *= mod.ScoreMultiplier; + + rankingInformationDisplay.ModMultiplier.Value = multiplier; + rankingInformationDisplay.Ranked.Value = ActiveMods.Value.All(m => m.Ranked); + } + + if (beatmapAttributesDisplay != null) + beatmapAttributesDisplay.Mods.Value = ActiveMods.Value; + } + + protected override void Update() + { + base.Update(); + + if (beatmapAttributesDisplay != null) + { + float rightEdgeOfLastButton = buttonFlow[^1].ScreenSpaceDrawQuad.TopRight.X; + + // this is cheating a bit; the 640 value is hardcoded based on how wide the expanded panel _generally_ is. + // due to the transition applied, the raw screenspace quad of the panel cannot be used, as it will trigger an ugly feedback cycle of expanding and collapsing. + float projectedLeftEdgeOfExpandedBeatmapAttributesDisplay = buttonFlow.ToScreenSpace(buttonFlow.DrawSize - new Vector2(640, 0)).X; + + DisplaysStackedVertically = rightEdgeOfLastButton > projectedLeftEdgeOfExpandedBeatmapAttributesDisplay; + + // only update preview panel's collapsed state after we are fully visible, to ensure all the buttons are where we expect them to be. + if (Alpha == 1) + beatmapAttributesDisplay.Collapsed.Value = DisplaysStackedVertically; + + contentFlow.LayoutDuration = 200; + contentFlow.LayoutEasing = Easing.OutQuint; + contentFlow.Direction = DisplaysStackedVertically ? FillDirection.Vertical : FillDirection.Horizontal; + } + } + + protected virtual IEnumerable CreateButtons() => new[] + { + DeselectAllModsButton = new DeselectAllModsButton(overlay) + }; + + protected override void PopIn() + { + this.MoveToY(0, 400, Easing.OutQuint) + .FadeIn(400, Easing.OutQuint); + } + + protected override void PopOut() + { + this.MoveToY(-20f, 200, Easing.OutQuint) + .FadeOut(200, Easing.OutQuint); + } + } +} diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index b0d58480db..40be4e08a6 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -27,6 +27,7 @@ using osu.Game.Graphics.UserInterface; using osu.Game.Input.Bindings; using osu.Game.Localisation; using osu.Game.Rulesets.Mods; +using osu.Game.Screens.Footer; using osu.Game.Utils; using osuTK; using osuTK.Graphics; @@ -87,11 +88,6 @@ namespace osu.Game.Overlays.Mods public ShearedSearchTextBox SearchTextBox { get; private set; } = null!; - /// - /// Whether the effects (on score multiplier, on or beatmap difficulty) of the current selected set of mods should be shown. - /// - protected virtual bool ShowModEffects => true; - /// /// Whether per-mod customisation controls are visible. /// @@ -108,11 +104,6 @@ namespace osu.Game.Overlays.Mods protected virtual IReadOnlyList ComputeActiveMods() => SelectedMods.Value; - protected virtual IEnumerable CreateFooterButtons() - { - yield return deselectAllModsButton = new DeselectAllModsButton(this); - } - private readonly Bindable>> globalAvailableMods = new Bindable>>(); public IEnumerable AllAvailableMods => AvailableMods.Value.SelectMany(pair => pair.Value); @@ -121,34 +112,18 @@ namespace osu.Game.Overlays.Mods private ColumnScrollContainer columnScroll = null!; private ColumnFlowContainer columnFlow = null!; - private FillFlowContainer footerButtonFlow = null!; - private FillFlowContainer footerContentFlow = null!; - private DeselectAllModsButton deselectAllModsButton = null!; private Container aboveColumnsContent = null!; - private RankingInformationDisplay? rankingInformationDisplay; - private BeatmapAttributesDisplay? beatmapAttributesDisplay; private ModCustomisationPanel customisationPanel = null!; - protected ShearedButton BackButton { get; private set; } = null!; - protected SelectAllModsButton? SelectAllModsButton { get; set; } + protected virtual SelectAllModsButton? SelectAllModsButton => null; private Sample? columnAppearSample; - private WorkingBeatmap? beatmap; + public readonly Bindable Beatmap = new Bindable(); - public WorkingBeatmap? Beatmap - { - get => beatmap; - set - { - if (beatmap == value) return; - - beatmap = value; - if (IsLoaded && beatmapAttributesDisplay != null) - beatmapAttributesDisplay.BeatmapInfo.Value = beatmap?.BeatmapInfo; - } - } + [Resolved] + private ScreenFooter? footer { get; set; } protected ModSelectOverlay(OverlayColourScheme colourScheme = OverlayColourScheme.Green) : base(colourScheme) @@ -226,59 +201,6 @@ namespace osu.Game.Overlays.Mods } }); - FooterContent.Add(footerButtonFlow = new FillFlowContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Direction = FillDirection.Horizontal, - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - Padding = new MarginPadding - { - Vertical = PADDING, - Horizontal = 70 - }, - Spacing = new Vector2(10), - ChildrenEnumerable = CreateFooterButtons().Prepend(BackButton = new ShearedButton(BUTTON_WIDTH) - { - Text = CommonStrings.Back, - Action = Hide, - DarkerColour = colours.Pink2, - LighterColour = colours.Pink1 - }) - }); - - if (ShowModEffects) - { - FooterContent.Add(footerContentFlow = new FillFlowContainer - { - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Vertical, - Spacing = new Vector2(30, 10), - Anchor = Anchor.BottomRight, - Origin = Anchor.BottomRight, - Margin = new MarginPadding - { - Vertical = PADDING, - Horizontal = 20 - }, - Children = new Drawable[] - { - rankingInformationDisplay = new RankingInformationDisplay - { - Anchor = Anchor.BottomRight, - Origin = Anchor.BottomRight - }, - beatmapAttributesDisplay = new BeatmapAttributesDisplay - { - Anchor = Anchor.BottomRight, - Origin = Anchor.BottomRight, - BeatmapInfo = { Value = Beatmap?.BeatmapInfo }, - }, - } - }); - } - globalAvailableMods.BindTo(game.AvailableMods); textSearchStartsActive = configManager.GetBindable(OsuSetting.ModSelectTextSearchStartsActive); @@ -292,8 +214,6 @@ namespace osu.Game.Overlays.Mods SearchTextBox.Current.Value = string.Empty; } - private ModSettingChangeTracker? modSettingChangeTracker; - protected override void LoadComplete() { // this is called before base call so that the mod state is populated early, and the transition in `PopIn()` can play out properly. @@ -316,23 +236,6 @@ namespace osu.Game.Overlays.Mods ActiveMods.Value = ComputeActiveMods(); }, true); - ActiveMods.BindValueChanged(_ => - { - updateOverlayInformation(); - - modSettingChangeTracker?.Dispose(); - - if (AllowCustomisation) - { - // Importantly, use ActiveMods.Value here (and not the ValueChanged NewValue) as the latter can - // potentially be stale, due to complexities in the way change trackers work. - // - // See https://github.com/ppy/osu/pull/23284#issuecomment-1529056988 - modSettingChangeTracker = new ModSettingChangeTracker(ActiveMods.Value); - modSettingChangeTracker.SettingChanged += _ => updateOverlayInformation(); - } - }, true); - customisationPanel.Expanded.BindValueChanged(_ => updateCustomisationVisualState(), true); SearchTextBox.Current.BindValueChanged(query => @@ -350,6 +253,16 @@ namespace osu.Game.Overlays.Mods }); } + private ModSelectFooterContent? currentFooterContent; + + public override bool UseNewFooter => true; + + public override Drawable CreateFooterContent() => currentFooterContent = new ModSelectFooterContent(this) + { + Beatmap = { BindTarget = Beatmap }, + ActiveMods = { BindTarget = ActiveMods }, + }; + private static readonly LocalisableString input_search_placeholder = Resources.Localisation.Web.CommonStrings.InputSearch; private static readonly LocalisableString tab_to_search_placeholder = ModSelectOverlayStrings.TabToSearch; @@ -358,26 +271,7 @@ namespace osu.Game.Overlays.Mods base.Update(); SearchTextBox.PlaceholderText = SearchTextBox.HasFocus ? input_search_placeholder : tab_to_search_placeholder; - - if (beatmapAttributesDisplay != null) - { - float rightEdgeOfLastButton = footerButtonFlow[^1].ScreenSpaceDrawQuad.TopRight.X; - - // this is cheating a bit; the 640 value is hardcoded based on how wide the expanded panel _generally_ is. - // due to the transition applied, the raw screenspace quad of the panel cannot be used, as it will trigger an ugly feedback cycle of expanding and collapsing. - float projectedLeftEdgeOfExpandedBeatmapAttributesDisplay = footerButtonFlow.ToScreenSpace(footerButtonFlow.DrawSize - new Vector2(640, 0)).X; - - bool screenIsntWideEnough = rightEdgeOfLastButton > projectedLeftEdgeOfExpandedBeatmapAttributesDisplay; - - // only update preview panel's collapsed state after we are fully visible, to ensure all the buttons are where we expect them to be. - if (Alpha == 1) - beatmapAttributesDisplay.Collapsed.Value = screenIsntWideEnough; - - footerContentFlow.LayoutDuration = 200; - footerContentFlow.LayoutEasing = Easing.OutQuint; - footerContentFlow.Direction = screenIsntWideEnough ? FillDirection.Vertical : FillDirection.Horizontal; - aboveColumnsContent.Padding = aboveColumnsContent.Padding with { Bottom = screenIsntWideEnough ? 70f : 15f }; - } + aboveColumnsContent.Padding = aboveColumnsContent.Padding with { Bottom = currentFooterContent?.DisplaysStackedVertically == true ? 75f : 15f }; } /// @@ -455,27 +349,6 @@ namespace osu.Game.Overlays.Mods modState.ValidForSelection.Value = modState.Mod.Type != ModType.System && modState.Mod.HasImplementation && IsValidMod.Invoke(modState.Mod); } - /// - /// Updates any information displayed on the overlay regarding the effects of the active mods. - /// This reads from instead of . - /// - private void updateOverlayInformation() - { - if (rankingInformationDisplay != null) - { - double multiplier = 1.0; - - foreach (var mod in ActiveMods.Value) - multiplier *= mod.ScoreMultiplier; - - rankingInformationDisplay.ModMultiplier.Value = multiplier; - rankingInformationDisplay.Ranked.Value = ActiveMods.Value.All(m => m.Ranked); - } - - if (beatmapAttributesDisplay != null) - beatmapAttributesDisplay.Mods.Value = ActiveMods.Value; - } - private void updateCustomisation() { if (!AllowCustomisation) @@ -701,7 +574,7 @@ namespace osu.Game.Overlays.Mods { if (!SearchTextBox.HasFocus && !customisationPanel.Expanded.Value) { - deselectAllModsButton.TriggerClick(); + currentFooterContent?.DeselectAllModsButton?.TriggerClick(); return true; } @@ -732,7 +605,7 @@ namespace osu.Game.Overlays.Mods return base.OnPressed(e); - void hideOverlay() => BackButton.TriggerClick(); + void hideOverlay() => footer?.BackButton.TriggerClick(); } /// @@ -740,7 +613,7 @@ namespace osu.Game.Overlays.Mods /// This is handled locally here due to conflicts in input handling between the search text box and the select all mods button. /// Attempting to handle this action locally in both places leads to a possible scenario /// wherein activating the "select all" platform binding will both select all text in the search box and select all mods. - /// > + /// public bool OnPressed(KeyBindingPressEvent e) { if (e.Repeat || e.Action != PlatformAction.SelectAll || SelectAllModsButton == null) diff --git a/osu.Game/Screens/OnlinePlay/FreeModSelectOverlay.cs b/osu.Game/Screens/OnlinePlay/FreeModSelectOverlay.cs index 7f090aca57..0ed45161f2 100644 --- a/osu.Game/Screens/OnlinePlay/FreeModSelectOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/FreeModSelectOverlay.cs @@ -14,8 +14,6 @@ namespace osu.Game.Screens.OnlinePlay { public partial class FreeModSelectOverlay : ModSelectOverlay { - protected override bool ShowModEffects => false; - protected override bool AllowCustomisation => false; public new Func IsValidMod @@ -24,6 +22,10 @@ namespace osu.Game.Screens.OnlinePlay set => base.IsValidMod = m => m.UserPlayable && value.Invoke(m); } + private FreeModSelectFooterContent? currentFooterContent; + + protected override SelectAllModsButton? SelectAllModsButton => currentFooterContent?.SelectAllModsButton; + public FreeModSelectOverlay() : base(OverlayColourScheme.Plum) { @@ -32,12 +34,33 @@ namespace osu.Game.Screens.OnlinePlay protected override ModColumn CreateModColumn(ModType modType) => new ModColumn(modType, true); - protected override IEnumerable CreateFooterButtons() - => base.CreateFooterButtons() - .Prepend(SelectAllModsButton = new SelectAllModsButton(this) - { - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - }); + public override Drawable CreateFooterContent() => currentFooterContent = new FreeModSelectFooterContent(this) + { + Beatmap = { BindTarget = Beatmap }, + ActiveMods = { BindTarget = ActiveMods }, + }; + + private partial class FreeModSelectFooterContent : ModSelectFooterContent + { + private readonly FreeModSelectOverlay overlay; + + protected override bool ShowModEffects => false; + + public SelectAllModsButton? SelectAllModsButton; + + public FreeModSelectFooterContent(FreeModSelectOverlay overlay) + : base(overlay) + { + this.overlay = overlay; + } + + protected override IEnumerable CreateButtons() + => base.CreateButtons() + .Prepend(SelectAllModsButton = new SelectAllModsButton(overlay) + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + }); + } } } diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs index 4eb092d08b..78ab8cfa6c 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs @@ -453,7 +453,7 @@ namespace osu.Game.Screens.OnlinePlay.Match // Retrieve the corresponding local beatmap, since we can't directly use the playlist's beatmap info var localBeatmap = beatmap == null ? null : beatmapManager.QueryBeatmap(b => b.OnlineID == beatmap.OnlineID); - UserModsSelectOverlay.Beatmap = Beatmap.Value = beatmapManager.GetWorkingBeatmap(localBeatmap); + UserModsSelectOverlay.Beatmap.Value = Beatmap.Value = beatmapManager.GetWorkingBeatmap(localBeatmap); } protected virtual void UpdateMods() diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index df7eabfd21..ecf8210002 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -851,7 +851,7 @@ namespace osu.Game.Screens.Select BeatmapDetails.Beatmap = beatmap; - ModSelect.Beatmap = beatmap; + ModSelect.Beatmap.Value = beatmap; advancedStats.BeatmapInfo = beatmap.BeatmapInfo; diff --git a/osu.Game/Tests/Visual/Gameplay/ScoringTestScene.cs b/osu.Game/Tests/Visual/Gameplay/ScoringTestScene.cs index e7053e4202..6908f7f1b4 100644 --- a/osu.Game/Tests/Visual/Gameplay/ScoringTestScene.cs +++ b/osu.Game/Tests/Visual/Gameplay/ScoringTestScene.cs @@ -658,7 +658,6 @@ namespace osu.Game.Tests.Visual.Gameplay private partial class TestModSelectOverlay : UserModSelectOverlay { - protected override bool ShowModEffects => true; protected override bool ShowPresets => false; public TestModSelectOverlay() From 5dd822ea3899eafde0b2beee191243e65378928a Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 29 Jun 2024 09:39:09 +0300 Subject: [PATCH 1244/2493] Migrate first-run setup overlay footer content --- .../TestSceneFirstRunSetupOverlay.cs | 62 ++++--- .../Overlays/FirstRunSetup/ScreenUIScale.cs | 8 +- osu.Game/Overlays/FirstRunSetupOverlay.cs | 169 +++++++++--------- 3 files changed, 129 insertions(+), 110 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneFirstRunSetupOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneFirstRunSetupOverlay.cs index 51da4d8755..2ca06bf2f4 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneFirstRunSetupOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneFirstRunSetupOverlay.cs @@ -11,6 +11,8 @@ using Moq; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Extensions.ObjectExtensions; +using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Screens; using osu.Framework.Testing; @@ -20,6 +22,7 @@ using osu.Game.Overlays; using osu.Game.Overlays.FirstRunSetup; using osu.Game.Overlays.Notifications; using osu.Game.Screens; +using osu.Game.Screens.Footer; using osuTK; using osuTK.Input; @@ -28,6 +31,7 @@ namespace osu.Game.Tests.Visual.UserInterface public partial class TestSceneFirstRunSetupOverlay : OsuManualInputManagerTestScene { private FirstRunSetupOverlay overlay; + private ScreenFooter footer; private readonly Mock performer = new Mock(); @@ -60,19 +64,16 @@ namespace osu.Game.Tests.Visual.UserInterface .Callback((Notification n) => lastNotification = n); }); - AddStep("add overlay", () => - { - Child = overlay = new FirstRunSetupOverlay - { - State = { Value = Visibility.Visible } - }; - }); + createOverlay(); + + AddStep("show overlay", () => overlay.Show()); } [Test] public void TestBasic() { AddAssert("overlay visible", () => overlay.State.Value == Visibility.Visible); + AddAssert("footer visible", () => footer.State.Value == Visibility.Visible); } [Test] @@ -82,16 +83,13 @@ namespace osu.Game.Tests.Visual.UserInterface AddUntilStep("step through", () => { - if (overlay.CurrentScreen?.IsLoaded != false) overlay.NextButton.TriggerClick(); + if (overlay.CurrentScreen?.IsLoaded != false) overlay.NextButton.AsNonNull().TriggerClick(); return overlay.State.Value == Visibility.Hidden; }); AddAssert("first run false", () => !LocalConfig.Get(OsuSetting.ShowFirstRunSetup)); - AddStep("add overlay", () => - { - Child = overlay = new FirstRunSetupOverlay(); - }); + createOverlay(); AddWaitStep("wait some", 5); @@ -109,7 +107,7 @@ namespace osu.Game.Tests.Visual.UserInterface if (keyboard) InputManager.Key(Key.Enter); else - overlay.NextButton.TriggerClick(); + overlay.NextButton.AsNonNull().TriggerClick(); } return overlay.State.Value == Visibility.Hidden; @@ -128,11 +126,9 @@ namespace osu.Game.Tests.Visual.UserInterface [TestCase(true)] public void TestBackButton(bool keyboard) { - AddAssert("back button disabled", () => !overlay.BackButton.Enabled.Value); - AddUntilStep("step to last", () => { - var nextButton = overlay.NextButton; + var nextButton = overlay.NextButton.AsNonNull(); if (overlay.CurrentScreen?.IsLoaded != false) nextButton.TriggerClick(); @@ -142,24 +138,29 @@ namespace osu.Game.Tests.Visual.UserInterface AddUntilStep("step back to start", () => { - if (overlay.CurrentScreen?.IsLoaded != false) + if (overlay.CurrentScreen?.IsLoaded != false && !(overlay.CurrentScreen is ScreenWelcome)) { if (keyboard) InputManager.Key(Key.Escape); else - overlay.BackButton.TriggerClick(); + footer.BackButton.TriggerClick(); } return overlay.CurrentScreen is ScreenWelcome; }); - AddAssert("back button disabled", () => !overlay.BackButton.Enabled.Value); + AddAssert("overlay not dismissed", () => overlay.State.Value == Visibility.Visible); if (keyboard) { AddStep("exit via keyboard", () => InputManager.Key(Key.Escape)); AddAssert("overlay dismissed", () => overlay.State.Value == Visibility.Hidden); } + else + { + AddStep("press back button", () => footer.BackButton.TriggerClick()); + AddAssert("overlay dismissed", () => overlay.State.Value == Visibility.Hidden); + } } [Test] @@ -185,7 +186,7 @@ namespace osu.Game.Tests.Visual.UserInterface [Test] public void TestResumeViaNotification() { - AddStep("step to next", () => overlay.NextButton.TriggerClick()); + AddStep("step to next", () => overlay.NextButton.AsNonNull().TriggerClick()); AddAssert("is at known screen", () => overlay.CurrentScreen is ScreenUIScale); @@ -200,6 +201,27 @@ namespace osu.Game.Tests.Visual.UserInterface AddAssert("is resumed", () => overlay.CurrentScreen is ScreenUIScale); } + private void createOverlay() + { + AddStep("add overlay", () => + { + var receptor = new ScreenFooter.BackReceptor(); + footer = new ScreenFooter(receptor); + + Child = new DependencyProvidingContainer + { + RelativeSizeAxes = Axes.Both, + CachedDependencies = new[] { (typeof(ScreenFooter), (object)footer) }, + Children = new Drawable[] + { + receptor, + overlay = new FirstRunSetupOverlay(), + footer, + } + }; + }); + } + // interface mocks break hot reload, mocking this stub implementation instead works around it. // see: https://github.com/moq/moq4/issues/1252 [UsedImplicitly] diff --git a/osu.Game/Overlays/FirstRunSetup/ScreenUIScale.cs b/osu.Game/Overlays/FirstRunSetup/ScreenUIScale.cs index 02f0ad9506..d0eefa55c5 100644 --- a/osu.Game/Overlays/FirstRunSetup/ScreenUIScale.cs +++ b/osu.Game/Overlays/FirstRunSetup/ScreenUIScale.cs @@ -23,6 +23,7 @@ using osu.Game.Overlays.Settings; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Screens; +using osu.Game.Screens.Footer; using osu.Game.Screens.Menu; using osu.Game.Screens.Select; using osu.Game.Tests.Visual; @@ -153,6 +154,7 @@ namespace osu.Game.Overlays.FirstRunSetup OsuScreenStack stack; OsuLogo logo; + ScreenFooter footer; Padding = new MarginPadding(5); @@ -166,7 +168,8 @@ namespace osu.Game.Overlays.FirstRunSetup { RelativePositionAxes = Axes.Both, Position = new Vector2(0.5f), - }) + }), + (typeof(ScreenFooter), footer = new ScreenFooter()), }, RelativeSizeAxes = Axes.Both, Children = new Drawable[] @@ -178,7 +181,8 @@ namespace osu.Game.Overlays.FirstRunSetup Children = new Drawable[] { stack = new OsuScreenStack(), - logo + footer, + logo, }, }, } diff --git a/osu.Game/Overlays/FirstRunSetupOverlay.cs b/osu.Game/Overlays/FirstRunSetupOverlay.cs index f2fdaefbb4..bc11e5d0d2 100644 --- a/osu.Game/Overlays/FirstRunSetupOverlay.cs +++ b/osu.Game/Overlays/FirstRunSetupOverlay.cs @@ -26,6 +26,7 @@ using osu.Game.Overlays.FirstRunSetup; using osu.Game.Overlays.Mods; using osu.Game.Overlays.Notifications; using osu.Game.Screens; +using osu.Game.Screens.Footer; using osu.Game.Screens.Menu; namespace osu.Game.Overlays @@ -44,8 +45,7 @@ namespace osu.Game.Overlays private ScreenStack? stack; - public ShearedButton NextButton = null!; - public ShearedButton BackButton = null!; + public ShearedButton? NextButton => currentFooterContent?.NextButton; private readonly Bindable showFirstRunSetup = new Bindable(); @@ -90,7 +90,7 @@ namespace osu.Game.Overlays Anchor = Anchor.Centre, Origin = Anchor.Centre, RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding { Bottom = 20, }, + Padding = new MarginPadding { Bottom = 20 }, Child = new GridContainer { Anchor = Anchor.Centre, @@ -134,51 +134,6 @@ namespace osu.Game.Overlays } }, }); - - FooterContent.Add(new GridContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Margin = new MarginPadding { Vertical = PADDING }, - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - ColumnDimensions = new[] - { - new Dimension(GridSizeMode.Absolute, 10), - new Dimension(GridSizeMode.AutoSize), - new Dimension(), - new Dimension(GridSizeMode.Absolute, 10), - }, - RowDimensions = new[] - { - new Dimension(GridSizeMode.AutoSize), - }, - Content = new[] - { - new[] - { - Empty(), - BackButton = new ShearedButton(300) - { - Text = CommonStrings.Back, - Action = showPreviousStep, - Enabled = { Value = false }, - DarkerColour = colours.Pink2, - LighterColour = colours.Pink1, - }, - NextButton = new ShearedButton(0) - { - RelativeSizeAxes = Axes.X, - Width = 1, - Text = FirstRunSetupOverlayStrings.GetStarted, - DarkerColour = ColourProvider.Colour2, - LighterColour = ColourProvider.Colour1, - Action = showNextStep - }, - Empty(), - }, - } - }); } protected override void LoadComplete() @@ -190,6 +145,32 @@ namespace osu.Game.Overlays if (showFirstRunSetup.Value) Show(); } + [Resolved] + private ScreenFooter footer { get; set; } = null!; + + private FirstRunSetupFooterContent? currentFooterContent; + + public override bool UseNewFooter => true; + + public override Drawable CreateFooterContent() => currentFooterContent = new FirstRunSetupFooterContent + { + ShowNextStep = showNextStep, + }; + + public override bool OnBackButton() + { + if (currentStepIndex == 0) + return false; + + Debug.Assert(stack != null); + + stack.CurrentScreen.Exit(); + currentStepIndex--; + + updateButtons(); + return true; + } + public override bool OnPressed(KeyBindingPressEvent e) { if (!e.Repeat) @@ -197,19 +178,12 @@ namespace osu.Game.Overlays switch (e.Action) { case GlobalAction.Select: - NextButton.TriggerClick(); + currentFooterContent?.NextButton.TriggerClick(); return true; case GlobalAction.Back: - if (BackButton.Enabled.Value) - { - BackButton.TriggerClick(); - return true; - } - - // If back button is disabled, we are at the first step. - // The base call will handle dismissal of the overlay. - break; + footer.BackButton.TriggerClick(); + return false; } } @@ -279,19 +253,6 @@ namespace osu.Game.Overlays showNextStep(); } - private void showPreviousStep() - { - if (currentStepIndex == 0) - return; - - Debug.Assert(stack != null); - - stack.CurrentScreen.Exit(); - currentStepIndex--; - - updateButtons(); - } - private void showNextStep() { Debug.Assert(currentStepIndex != null); @@ -322,29 +283,61 @@ namespace osu.Game.Overlays updateButtons(); } - private void updateButtons() + private void updateButtons() => currentFooterContent?.UpdateButtons(currentStepIndex, steps); + + private partial class FirstRunSetupFooterContent : VisibilityContainer { - BackButton.Enabled.Value = currentStepIndex > 0; - NextButton.Enabled.Value = currentStepIndex != null; + public ShearedButton NextButton { get; private set; } = null!; - if (currentStepIndex == null) - return; + public Action? ShowNextStep; - bool isFirstStep = currentStepIndex == 0; - bool isLastStep = currentStepIndex == steps.Count - 1; - - if (isFirstStep) + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider) { - BackButton.Text = CommonStrings.Back; - NextButton.Text = FirstRunSetupOverlayStrings.GetStarted; + RelativeSizeAxes = Axes.Both; + + InternalChild = NextButton = new ShearedButton(0) + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Margin = new MarginPadding { Right = 12f }, + RelativeSizeAxes = Axes.X, + Width = 1, + Text = FirstRunSetupOverlayStrings.GetStarted, + DarkerColour = colourProvider.Colour2, + LighterColour = colourProvider.Colour1, + Action = () => ShowNextStep?.Invoke(), + }; } - else - { - BackButton.Text = LocalisableString.Interpolate($@"{CommonStrings.Back} ({steps[currentStepIndex.Value - 1].GetLocalisableDescription()})"); - NextButton.Text = isLastStep - ? CommonStrings.Finish - : LocalisableString.Interpolate($@"{CommonStrings.Next} ({steps[currentStepIndex.Value + 1].GetLocalisableDescription()})"); + public void UpdateButtons(int? currentStep, IReadOnlyList steps) + { + NextButton.Enabled.Value = currentStep != null; + + if (currentStep == null) + return; + + bool isFirstStep = currentStep == 0; + bool isLastStep = currentStep == steps.Count - 1; + + if (isFirstStep) + NextButton.Text = FirstRunSetupOverlayStrings.GetStarted; + else + { + NextButton.Text = isLastStep + ? CommonStrings.Finish + : LocalisableString.Interpolate($@"{CommonStrings.Next} ({steps[currentStep.Value + 1].GetLocalisableDescription()})"); + } + } + + protected override void PopIn() + { + this.FadeIn(); + } + + protected override void PopOut() + { + this.Delay(400).FadeOut(); } } } From e57a0029f16619ed857ed400f5e3695db2f1d3a9 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sun, 30 Jun 2024 04:22:59 +0300 Subject: [PATCH 1245/2493] Remove local footer from `ShearedOverlayContainer` --- .../UserInterface/TestSceneScreenFooter.cs | 2 - osu.Game/Overlays/FirstRunSetupOverlay.cs | 2 - osu.Game/Overlays/Mods/ModSelectOverlay.cs | 2 - .../Overlays/Mods/ShearedOverlayContainer.cs | 47 ++----------------- 4 files changed, 3 insertions(+), 50 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneScreenFooter.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneScreenFooter.cs index 70c3664b9a..de2026e538 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneScreenFooter.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneScreenFooter.cs @@ -185,8 +185,6 @@ namespace osu.Game.Tests.Visual.UserInterface private partial class TestShearedOverlayContainer : ShearedOverlayContainer { - public override bool UseNewFooter => true; - public TestShearedOverlayContainer() : base(OverlayColourScheme.Orange) { diff --git a/osu.Game/Overlays/FirstRunSetupOverlay.cs b/osu.Game/Overlays/FirstRunSetupOverlay.cs index bc11e5d0d2..47f53a3fa6 100644 --- a/osu.Game/Overlays/FirstRunSetupOverlay.cs +++ b/osu.Game/Overlays/FirstRunSetupOverlay.cs @@ -150,8 +150,6 @@ namespace osu.Game.Overlays private FirstRunSetupFooterContent? currentFooterContent; - public override bool UseNewFooter => true; - public override Drawable CreateFooterContent() => currentFooterContent = new FirstRunSetupFooterContent { ShowNextStep = showNextStep, diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index 40be4e08a6..8e18c39cc2 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -255,8 +255,6 @@ namespace osu.Game.Overlays.Mods private ModSelectFooterContent? currentFooterContent; - public override bool UseNewFooter => true; - public override Drawable CreateFooterContent() => currentFooterContent = new ModSelectFooterContent(this) { Beatmap = { BindTarget = Beatmap }, diff --git a/osu.Game/Overlays/Mods/ShearedOverlayContainer.cs b/osu.Game/Overlays/Mods/ShearedOverlayContainer.cs index b5435e7e58..1ccf274d0d 100644 --- a/osu.Game/Overlays/Mods/ShearedOverlayContainer.cs +++ b/osu.Game/Overlays/Mods/ShearedOverlayContainer.cs @@ -7,7 +7,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; -using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osu.Game.Screens.Footer; @@ -30,17 +29,9 @@ namespace osu.Game.Overlays.Mods /// protected ShearedOverlayHeader Header { get; private set; } = null!; - /// - /// The overlay's footer. - /// - protected Container Footer { get; private set; } - [Resolved] private ScreenFooter? footer { get; set; } - // todo: very temporary property that will be removed once ModSelectOverlay and FirstRunSetupOverlay are updated to use new footer. - public virtual bool UseNewFooter => false; - /// /// A container containing all content, including the header and footer. /// May be used for overlay-wide animations. @@ -52,11 +43,6 @@ namespace osu.Game.Overlays.Mods /// protected Container MainAreaContent { get; private set; } = null!; - /// - /// A container for content that is to be displayed inside the footer. - /// - protected Container FooterContent { get; private set; } - protected override bool StartHidden => true; protected override bool BlockNonPositionalInput => true; @@ -75,8 +61,6 @@ namespace osu.Game.Overlays.Mods [BackgroundDependencyLoader] private void load() { - const float footer_height = ScreenFooter.HEIGHT; - Child = TopLevelContent = new Container { RelativeSizeAxes = Axes.Both, @@ -100,30 +84,9 @@ namespace osu.Game.Overlays.Mods Padding = new MarginPadding { Top = ShearedOverlayHeader.HEIGHT, - Bottom = footer_height + PADDING, + Bottom = ScreenFooter.HEIGHT + PADDING, } }, - Footer = new InputBlockingContainer - { - RelativeSizeAxes = Axes.X, - Depth = float.MinValue, - Height = footer_height, - Margin = new MarginPadding { Top = PADDING }, - Anchor = Anchor.BottomCentre, - Origin = Anchor.BottomCentre, - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = ColourProvider.Background5 - }, - FooterContent = new Container - { - RelativeSizeAxes = Axes.Both, - }, - } - } } }; } @@ -160,7 +123,7 @@ namespace osu.Game.Overlays.Mods Header.MoveToY(0, fade_in_duration, Easing.OutQuint); - if (UseNewFooter && footer != null) + if (footer != null) { footer.SetOverlayContent(this); @@ -170,8 +133,6 @@ namespace osu.Game.Overlays.Mods hideFooterOnPopOut = true; } } - else - Footer.MoveToY(0, fade_in_duration, Easing.OutQuint); } protected override void PopOut() @@ -183,7 +144,7 @@ namespace osu.Game.Overlays.Mods Header.MoveToY(-Header.DrawHeight, fade_out_duration, Easing.OutQuint); - if (UseNewFooter && footer != null) + if (footer != null) { footer.ClearOverlayContent(); @@ -193,8 +154,6 @@ namespace osu.Game.Overlays.Mods hideFooterOnPopOut = false; } } - else - Footer.MoveToY(Footer.DrawHeight, fade_out_duration, Easing.OutQuint); } } } From 58c7d1e7724b6a08b01f4682ccc4684bc311dba0 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 29 Jun 2024 10:19:22 +0300 Subject: [PATCH 1246/2493] Bind game-wide mods bindable to mod select overlay in new song select screen --- osu.Game/Screens/SelectV2/SongSelectV2.cs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/osu.Game/Screens/SelectV2/SongSelectV2.cs b/osu.Game/Screens/SelectV2/SongSelectV2.cs index a8730ad808..2f9667793f 100644 --- a/osu.Game/Screens/SelectV2/SongSelectV2.cs +++ b/osu.Game/Screens/SelectV2/SongSelectV2.cs @@ -64,18 +64,29 @@ namespace osu.Game.Screens.SelectV2 public override void OnEntering(ScreenTransitionEvent e) { this.FadeIn(); + + modSelectOverlay.SelectedMods.BindTo(Mods); + base.OnEntering(e); } public override void OnResuming(ScreenTransitionEvent e) { this.FadeIn(); + + // required due to https://github.com/ppy/osu-framework/issues/3218 + modSelectOverlay.SelectedMods.Disabled = false; + modSelectOverlay.SelectedMods.BindTo(Mods); + base.OnResuming(e); } public override void OnSuspending(ScreenTransitionEvent e) { this.Delay(400).FadeOut(); + + modSelectOverlay.SelectedMods.UnbindFrom(Mods); + base.OnSuspending(e); } From a65af8249c74e32e512e146943687af33555d155 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sun, 30 Jun 2024 07:27:19 +0300 Subject: [PATCH 1247/2493] Fix first-run setup buttons reset after reopening from dismiss --- osu.Game/Overlays/FirstRunSetupOverlay.cs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/osu.Game/Overlays/FirstRunSetupOverlay.cs b/osu.Game/Overlays/FirstRunSetupOverlay.cs index 47f53a3fa6..6412297663 100644 --- a/osu.Game/Overlays/FirstRunSetupOverlay.cs +++ b/osu.Game/Overlays/FirstRunSetupOverlay.cs @@ -150,10 +150,16 @@ namespace osu.Game.Overlays private FirstRunSetupFooterContent? currentFooterContent; - public override Drawable CreateFooterContent() => currentFooterContent = new FirstRunSetupFooterContent + public override Drawable CreateFooterContent() { - ShowNextStep = showNextStep, - }; + currentFooterContent = new FirstRunSetupFooterContent + { + ShowNextStep = showNextStep, + }; + + currentFooterContent.OnLoadComplete += _ => updateButtons(); + return currentFooterContent; + } public override bool OnBackButton() { From 901663b3fff921f78afe4e0bfa2fce97ce378b90 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sun, 30 Jun 2024 11:00:00 +0300 Subject: [PATCH 1248/2493] Fix test failure --- osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs index dedad3a40a..9f48b06bb6 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs @@ -55,6 +55,7 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep("clear contents", Clear); AddStep("reset ruleset", () => Ruleset.Value = rulesetStore.GetRuleset(0)); AddStep("reset mods", () => SelectedMods.SetDefault()); + AddStep("reset config", () => configManager.SetValue(OsuSetting.ModSelectTextSearchStartsActive, true)); AddStep("set beatmap", () => Beatmap.Value = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo)); AddStep("set up presets", () => { From 8bb51d5a4f08e8750dbad5ee0f97ba221be81fd2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 30 Jun 2024 20:32:16 +0900 Subject: [PATCH 1249/2493] Fix summary timeline not correctly updating after changes to breaks Closes https://github.com/ppy/osu/issues/28678. Oops. --- .../Screens/Edit/Components/Timelines/Summary/Parts/BreakPart.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/BreakPart.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/BreakPart.cs index 50062e8465..ed42ade490 100644 --- a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/BreakPart.cs +++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/BreakPart.cs @@ -25,6 +25,7 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts breaks.BindTo(beatmap.Breaks); breaks.BindCollectionChanged((_, _) => { + Clear(); foreach (var breakPeriod in beatmap.Breaks) Add(new BreakVisualisation(breakPeriod)); }, true); From 0c34e7bebbce5a989af00d8daed25331ff33321a Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 1 Jul 2024 06:48:05 +0300 Subject: [PATCH 1250/2493] Store layout version in `SkinLayoutVersion` instead and refactor migration code --- .../Archives/argon-layout-version-0.osk | Bin 0 -> 1550 bytes .../Archives/classic-layout-version-0.osk | Bin 0 -> 1382 bytes .../Archives/triangles-layout-version-0.osk | Bin 0 -> 1378 bytes .../Visual/Gameplay/TestSceneSkinEditor.cs | 119 ++++++++++-------- osu.Game/Database/RealmAccess.cs | 3 +- osu.Game/Skinning/ArgonSkin.cs | 23 ---- osu.Game/Skinning/LegacySkin.cs | 23 ---- osu.Game/Skinning/Skin.cs | 116 ++++++++++++----- osu.Game/Skinning/SkinImporter.cs | 2 - osu.Game/Skinning/SkinInfo.cs | 15 --- osu.Game/Skinning/SkinLayoutInfo.cs | 18 ++- 11 files changed, 165 insertions(+), 154 deletions(-) create mode 100644 osu.Game.Tests/Resources/Archives/argon-layout-version-0.osk create mode 100644 osu.Game.Tests/Resources/Archives/classic-layout-version-0.osk create mode 100644 osu.Game.Tests/Resources/Archives/triangles-layout-version-0.osk diff --git a/osu.Game.Tests/Resources/Archives/argon-layout-version-0.osk b/osu.Game.Tests/Resources/Archives/argon-layout-version-0.osk new file mode 100644 index 0000000000000000000000000000000000000000..f767033eb1de68d16bbb5c4fa9cb5a0fb145425d GIT binary patch literal 1550 zcmWIWW@Zs#U|`^2*qU%JLZU!8aS@Pr04&15P@J8aryU#0>+j!P%R^ZV zg}4>xPMtgTR>x7b?S}2;yNh-Te99H8o3uZn>FxoZ&$acRWS&pExZ=B&MdgApPaRI? z(gk*z3!Y?ss$+>fEq0aT?Jw3G`R8|9rytwm=6Y8p^n%5Cr^borgHH=K3P*ZvG2E`q z?f$8}cvjaZ`>3Fr6*?0|Z!d~a-1uywCWCZVC!GrLPwn(h=yT<8}Ug zcVN&310w^YOCc9dob^BHbK1w3(WuvSYKeo@obDiD?@!$hvz$I}`uyqiXN5=eDvzpm zR;teIRGs;=a$aTT&w%8wcN)W9u%@R!dz$)e>1)fT9EOi2A_nlPT3PLiRVP}MKN^@>S#MeMs(e1@6d@1uz@?tiQ^imGf3x}gznAY$ylqUq+BY%o zGGqJtB$aIWwb6?uO&=9G?5y19Gk@*WdAH6z3pSWGD}?i>le&YSGmAji7t6Vm@BK8i zJmk^EE#Y<{UBKz?F8QeKb&OhSM;9hr>prrUE9{8fzKioZ=b?oSF3L|!SI_6M;NE}m zk58YYOjpb2sK1HawJT?sta%$UG;N5 zrI_ECHt)Clp7;$5`+D!@rqBJ};JVSQjo;7BUMNxL;)}VwAK5o`bn1SLY5a92FJZ?# zqeU?X(-;<*3QxS%*Vrk!fG>on#8$Wb^W7OA6%QpnJ-xbF_qR6FqNmUA%t+qPyz$a% zrbi}IIAUjV@J8qf`Q`O}Sml}|=pZK;{cC?`!Cj{bJJL9mO|}c3(Bz#tZ}~#&I5iE? z)2|yYGk!2y`I7Oem0em^){~-*CzmdJ<&tDS$>ZChWiwoi7DycV{(ATA_Nf{i0y``x zOR{wRZA@R3cBaK(w{ooIOfN?W6`fz_!h6BZlzOlbS;rR9IPve7c-{;A2DMpviH^>U`HDJeaCupm-ZOzxDz zYJQ7J77LYH_NAzO+@o+rtaj;73)aN6$=}WixmCZOG1YP6%4p_D-{?Deg15id=0DGp zj%&XWcl-M%<1fLNrp%ta?2gmVOskzoPgm~#^JRvey_xL_Jvqn7N z^YVFa6U=o?Rx7axcO_r0*R=KT-!W5ia?tODYWD3K1=34PGS-$ComMRWspj3aqTk4= znCt51TNz^Rt9`Ai58t};zO`lY?9{nQ5xzUSCip#mzW#tF*E6y3z0s=cS4gd!|Ly0u zyy|-&{)UIv#@cWH`+feIqxJbVfA>Z8Mcw(h_v5<<@AOe~lg!klDm7p((gb2YAP&e$ ztW3*H%}GJz_99I$9e;k^z|eRCstaBSp=(7i5)fL`8F3X5=w_e?1j3B1Ea<@!;LXYg Pl4k+Jk3gE86~qGo@tS=8 literal 0 HcmV?d00001 diff --git a/osu.Game.Tests/Resources/Archives/classic-layout-version-0.osk b/osu.Game.Tests/Resources/Archives/classic-layout-version-0.osk new file mode 100644 index 0000000000000000000000000000000000000000..8240510f7c20e7d192efbfbba03ff9243f6f51dc GIT binary patch literal 1382 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 literal 0 HcmV?d00001 diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs index 2470c320cc..f44daa1ecb 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs @@ -7,6 +7,7 @@ using System.Linq; using System.Text; using NUnit.Framework; using osu.Framework.Allocation; +using osu.Framework.Extensions; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; @@ -24,6 +25,7 @@ using osu.Game.Screens.Play.HUD; using osu.Game.Screens.Play.HUD.HitErrorMeters; using osu.Game.Skinning; using osu.Game.Skinning.Components; +using osu.Game.Tests.Resources; using osuTK; using osuTK.Input; @@ -384,73 +386,82 @@ namespace osu.Game.Tests.Visual.Gameplay [Test] public void TestMigrationArgon() { - AddUntilStep("wait for load", () => globalHUDTarget.ComponentsLoaded); - AddStep("add combo to global hud target", () => - { - globalHUDTarget.Add(new ArgonComboCounter - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - }); - }); + Live importedSkin = null!; - Live modifiedSkin = null!; + AddStep("import old argon skin", () => skins.CurrentSkinInfo.Value = importedSkin = importSkinFromArchives(@"argon-layout-version-0.osk").SkinInfo); + AddUntilStep("wait for load", () => globalHUDTarget.ComponentsLoaded && rulesetHUDTarget.ComponentsLoaded); + AddAssert("no combo in global target", () => !globalHUDTarget.Components.OfType().Any()); + AddAssert("combo placed in ruleset target", () => rulesetHUDTarget.Components.OfType().Count() == 1); - AddStep("select another skin", () => + AddStep("add combo to global target", () => globalHUDTarget.Add(new ArgonComboCounter { - modifiedSkin = skins.CurrentSkinInfo.Value; - skins.CurrentSkinInfo.SetDefault(); - }); - AddStep("modify version", () => modifiedSkin.PerformWrite(s => s.LayoutVersion = 0)); - AddStep("select skin again", () => skins.CurrentSkinInfo.Value = modifiedSkin); - AddAssert("global hud target does not contain combo", () => !globalHUDTarget.Components.Any(c => c is ArgonComboCounter)); - AddAssert("ruleset hud target contains both combos", () => - { - var target = rulesetHUDTarget; + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Scale = new Vector2(2f), + })); + AddStep("save skin", () => skins.Save(skins.CurrentSkin.Value)); - return target.Components.Count == 2 && - target.Components[0] is ArgonComboCounter one && one.Anchor == Anchor.BottomLeft && one.Origin == Anchor.BottomLeft && - target.Components[1] is ArgonComboCounter two && two.Anchor == Anchor.Centre && two.Origin == Anchor.Centre; - }); - AddStep("save skin", () => skinEditor.Save()); - AddAssert("version updated", () => modifiedSkin.PerformRead(s => s.LayoutVersion) == SkinInfo.LATEST_LAYOUT_VERSION); + AddStep("select another skin", () => skins.CurrentSkinInfo.SetDefault()); + AddStep("select skin again", () => skins.CurrentSkinInfo.Value = importedSkin); + AddUntilStep("wait for load", () => globalHUDTarget.ComponentsLoaded && rulesetHUDTarget.ComponentsLoaded); + AddAssert("combo placed in global target", () => globalHUDTarget.Components.OfType().Count() == 1); + AddAssert("combo placed in ruleset target", () => rulesetHUDTarget.Components.OfType().Count() == 1); + } + + [Test] + public void TestMigrationTriangles() + { + Live importedSkin = null!; + + AddStep("import old triangles skin", () => skins.CurrentSkinInfo.Value = importedSkin = importSkinFromArchives(@"triangles-layout-version-0.osk").SkinInfo); + AddUntilStep("wait for load", () => globalHUDTarget.ComponentsLoaded && rulesetHUDTarget.ComponentsLoaded); + AddAssert("no combo in global target", () => !globalHUDTarget.Components.OfType().Any()); + AddAssert("combo placed in ruleset target", () => rulesetHUDTarget.Components.OfType().Count() == 1); + + AddStep("add combo to global target", () => globalHUDTarget.Add(new DefaultComboCounter + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Scale = new Vector2(2f), + })); + AddStep("save skin", () => skins.Save(skins.CurrentSkin.Value)); + + AddStep("select another skin", () => skins.CurrentSkinInfo.SetDefault()); + AddStep("select skin again", () => skins.CurrentSkinInfo.Value = importedSkin); + AddUntilStep("wait for load", () => globalHUDTarget.ComponentsLoaded && rulesetHUDTarget.ComponentsLoaded); + AddAssert("combo placed in global target", () => globalHUDTarget.Components.OfType().Count() == 1); + AddAssert("combo placed in ruleset target", () => rulesetHUDTarget.Components.OfType().Count() == 1); } [Test] public void TestMigrationLegacy() { - AddStep("select legacy skin", () => skins.CurrentSkinInfo.Value = skins.DefaultClassicSkin.SkinInfo); + Live importedSkin = null!; - AddUntilStep("wait for load", () => globalHUDTarget.ComponentsLoaded); - AddStep("add combo to global hud target", () => + AddStep("import old classic skin", () => skins.CurrentSkinInfo.Value = importedSkin = importSkinFromArchives(@"classic-layout-version-0.osk").SkinInfo); + AddUntilStep("wait for load", () => globalHUDTarget.ComponentsLoaded && rulesetHUDTarget.ComponentsLoaded); + AddAssert("no combo in global target", () => !globalHUDTarget.Components.OfType().Any()); + AddAssert("combo placed in ruleset target", () => rulesetHUDTarget.Components.OfType().Count() == 1); + + AddStep("add combo to global target", () => globalHUDTarget.Add(new LegacyComboCounter { - globalHUDTarget.Add(new LegacyComboCounter - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - }); - }); + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Scale = new Vector2(2f), + })); + AddStep("save skin", () => skins.Save(skins.CurrentSkin.Value)); - Live modifiedSkin = null!; + AddStep("select another skin", () => skins.CurrentSkinInfo.SetDefault()); + AddStep("select skin again", () => skins.CurrentSkinInfo.Value = importedSkin); + AddUntilStep("wait for load", () => globalHUDTarget.ComponentsLoaded && rulesetHUDTarget.ComponentsLoaded); + AddAssert("combo placed in global target", () => globalHUDTarget.Components.OfType().Count() == 1); + AddAssert("combo placed in ruleset target", () => rulesetHUDTarget.Components.OfType().Count() == 1); + } - AddStep("select another skin", () => - { - modifiedSkin = skins.CurrentSkinInfo.Value; - skins.CurrentSkinInfo.SetDefault(); - }); - AddStep("modify version", () => modifiedSkin.PerformWrite(s => s.LayoutVersion = 0)); - AddStep("select skin again", () => skins.CurrentSkinInfo.Value = modifiedSkin); - AddAssert("global hud target does not contain combo", () => !globalHUDTarget.Components.Any(c => c is LegacyComboCounter)); - AddAssert("ruleset hud target contains both combos", () => - { - var target = rulesetHUDTarget; - - return target.Components.Count == 2 && - target.Components[0] is LegacyComboCounter one && one.Anchor == Anchor.BottomLeft && one.Origin == Anchor.BottomLeft && - target.Components[1] is LegacyComboCounter two && two.Anchor == Anchor.Centre && two.Origin == Anchor.Centre; - }); - AddStep("save skin", () => skinEditor.Save()); - AddAssert("version updated", () => modifiedSkin.PerformRead(s => s.LayoutVersion) == SkinInfo.LATEST_LAYOUT_VERSION); + private Skin importSkinFromArchives(string filename) + { + var imported = skins.Import(new ImportTask(TestResources.OpenResource($@"Archives/{filename}"), filename)).GetResultSafely(); + return imported.PerformRead(skinInfo => skins.GetSkin(skinInfo)); } protected override Ruleset CreatePlayerRuleset() => new OsuRuleset(); diff --git a/osu.Game/Database/RealmAccess.cs b/osu.Game/Database/RealmAccess.cs index 606bc5e10c..1ece81be50 100644 --- a/osu.Game/Database/RealmAccess.cs +++ b/osu.Game/Database/RealmAccess.cs @@ -93,9 +93,8 @@ namespace osu.Game.Database /// 39 2023-12-19 Migrate any EndTimeObjectCount and TotalObjectCount values of 0 to -1 to better identify non-calculated values. /// 40 2023-12-21 Add ScoreInfo.Version to keep track of which build scores were set on. /// 41 2024-04-17 Add ScoreInfo.TotalScoreWithoutMods for future mod multiplier rebalances. - /// 42 2024-06-25 Add SkinInfo.LayoutVersion to allow performing migrations of components on structural changes. /// - private const int schema_version = 42; + private const int schema_version = 41; /// /// Lock object which is held during sections, blocking realm retrieval during blocking periods. diff --git a/osu.Game/Skinning/ArgonSkin.cs b/osu.Game/Skinning/ArgonSkin.cs index 4cd54c06f0..707281db31 100644 --- a/osu.Game/Skinning/ArgonSkin.cs +++ b/osu.Game/Skinning/ArgonSkin.cs @@ -12,7 +12,6 @@ using osu.Game.Audio; using osu.Game.Beatmaps.Formats; using osu.Game.Extensions; using osu.Game.IO; -using osu.Game.Rulesets; using osu.Game.Screens.Play; using osu.Game.Screens.Play.HUD; using osu.Game.Screens.Play.HUD.HitErrorMeters; @@ -70,28 +69,6 @@ namespace osu.Game.Skinning // Purple new Color4(92, 0, 241, 255), }; - - if (skin.LayoutVersion < 20240625 - && LayoutInfos.TryGetValue(SkinComponentsContainerLookup.TargetArea.MainHUDComponents, out var hudLayout) - && hudLayout.TryGetDrawableInfo(null, out var hudComponents)) - { - var comboCounters = hudComponents.Where(h => h.Type.Name == nameof(ArgonComboCounter)).ToArray(); - - if (comboCounters.Any()) - { - hudLayout.Update(null, hudComponents.Except(comboCounters).ToArray()); - - resources.RealmAccess.Run(r => - { - foreach (var ruleset in r.All()) - { - hudLayout.Update(ruleset, hudLayout.TryGetDrawableInfo(ruleset, out var rulesetComponents) - ? rulesetComponents.Concat(comboCounters).ToArray() - : comboCounters); - } - }); - } - } } public override Texture? GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => Textures?.Get(componentName, wrapModeS, wrapModeT); diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index f148bad96e..b71b626b4e 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -19,7 +19,6 @@ using osu.Game.Audio; using osu.Game.Beatmaps.Formats; using osu.Game.Extensions; using osu.Game.IO; -using osu.Game.Rulesets; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Scoring; using osu.Game.Screens.Play.HUD; @@ -57,28 +56,6 @@ namespace osu.Game.Skinning protected LegacySkin(SkinInfo skin, IStorageResourceProvider? resources, IResourceStore? fallbackStore, string configurationFilename = @"skin.ini") : base(skin, resources, fallbackStore, configurationFilename) { - if (resources != null - && skin.LayoutVersion < 20240625 - && LayoutInfos.TryGetValue(SkinComponentsContainerLookup.TargetArea.MainHUDComponents, out var hudLayout) - && hudLayout.TryGetDrawableInfo(null, out var hudComponents)) - { - var comboCounters = hudComponents.Where(h => h.Type.Name == nameof(LegacyComboCounter)).ToArray(); - - if (comboCounters.Any()) - { - hudLayout.Update(null, hudComponents.Except(comboCounters).ToArray()); - - resources.RealmAccess.Run(r => - { - foreach (var ruleset in r.All()) - { - hudLayout.Update(ruleset, hudLayout.TryGetDrawableInfo(ruleset, out var rulesetComponents) - ? rulesetComponents.Concat(comboCounters).ToArray() - : comboCounters); - } - }); - } - } } protected override IResourceStore CreateTextureLoaderStore(IStorageResourceProvider resources, IResourceStore storage) diff --git a/osu.Game/Skinning/Skin.cs b/osu.Game/Skinning/Skin.cs index e4ca908d90..5bac5c3d81 100644 --- a/osu.Game/Skinning/Skin.cs +++ b/osu.Game/Skinning/Skin.cs @@ -21,11 +21,15 @@ using osu.Framework.Logging; using osu.Game.Audio; using osu.Game.Database; using osu.Game.IO; +using osu.Game.Rulesets; +using osu.Game.Screens.Play.HUD; namespace osu.Game.Skinning { public abstract class Skin : IDisposable, ISkin { + private readonly IStorageResourceProvider? resources; + /// /// A texture store which can be used to perform user file lookups for this skin. /// @@ -68,6 +72,8 @@ namespace osu.Game.Skinning /// An optional filename to read the skin configuration from. If not provided, the configuration will be retrieved from the storage using "skin.ini". protected Skin(SkinInfo skin, IStorageResourceProvider? resources, IResourceStore? fallbackStore = null, string configurationFilename = @"skin.ini") { + this.resources = resources; + Name = skin.Name; if (resources != null) @@ -131,40 +137,9 @@ namespace osu.Game.Skinning { string jsonContent = Encoding.UTF8.GetString(bytes); - SkinLayoutInfo? layoutInfo = null; - - // handle namespace changes... - jsonContent = jsonContent.Replace(@"osu.Game.Screens.Play.SongProgress", @"osu.Game.Screens.Play.HUD.DefaultSongProgress"); - jsonContent = jsonContent.Replace(@"osu.Game.Screens.Play.HUD.LegacyComboCounter", @"osu.Game.Skinning.LegacyComboCounter"); - jsonContent = jsonContent.Replace(@"osu.Game.Screens.Play.HUD.PerformancePointsCounter", @"osu.Game.Skinning.Triangles.TrianglesPerformancePointsCounter"); - - try - { - // First attempt to deserialise using the new SkinLayoutInfo format - layoutInfo = JsonConvert.DeserializeObject(jsonContent); - } - catch - { - } - - // Of note, the migration code below runs on read of skins, but there's nothing to - // force a rewrite after migration. Let's not remove these migration rules until we - // have something in place to ensure we don't end up breaking skins of users that haven't - // manually saved their skin since a change was implemented. - - // If deserialisation using SkinLayoutInfo fails, attempt to deserialise using the old naked list. + var layoutInfo = parseLayoutInfo(jsonContent, skinnableTarget); if (layoutInfo == null) - { - var deserializedContent = JsonConvert.DeserializeObject>(jsonContent); - - if (deserializedContent == null) - continue; - - layoutInfo = new SkinLayoutInfo(); - layoutInfo.Update(null, deserializedContent.ToArray()); - - Logger.Log($"Ferrying {deserializedContent.Count()} components in {skinnableTarget} to global section of new {nameof(SkinLayoutInfo)} format"); - } + continue; LayoutInfos[skinnableTarget] = layoutInfo; } @@ -230,6 +205,81 @@ namespace osu.Game.Skinning return null; } + #region Deserialisation & Migration + + private SkinLayoutInfo? parseLayoutInfo(string jsonContent, SkinComponentsContainerLookup.TargetArea target) + { + SkinLayoutInfo? layout = null; + + // handle namespace changes... + jsonContent = jsonContent.Replace(@"osu.Game.Screens.Play.SongProgress", @"osu.Game.Screens.Play.HUD.DefaultSongProgress"); + jsonContent = jsonContent.Replace(@"osu.Game.Screens.Play.HUD.LegacyComboCounter", @"osu.Game.Skinning.LegacyComboCounter"); + jsonContent = jsonContent.Replace(@"osu.Game.Screens.Play.HUD.PerformancePointsCounter", @"osu.Game.Skinning.Triangles.TrianglesPerformancePointsCounter"); + + try + { + // First attempt to deserialise using the new SkinLayoutInfo format + layout = JsonConvert.DeserializeObject(jsonContent); + } + catch + { + } + + // If deserialisation using SkinLayoutInfo fails, attempt to deserialise using the old naked list. + if (layout == null) + { + var deserializedContent = JsonConvert.DeserializeObject>(jsonContent); + if (deserializedContent == null) + return null; + + layout = new SkinLayoutInfo { Version = 0 }; + layout.Update(null, deserializedContent.ToArray()); + + Logger.Log($"Ferrying {deserializedContent.Count()} components in {target} to global section of new {nameof(SkinLayoutInfo)} format"); + } + + for (int i = layout.Version + 1; i <= SkinLayoutInfo.LATEST_VERSION; i++) + applyMigration(layout, target, i); + + layout.Version = SkinLayoutInfo.LATEST_VERSION; + return layout; + } + + private void applyMigration(SkinLayoutInfo layout, SkinComponentsContainerLookup.TargetArea target, int version) + { + switch (version) + { + case 1: + { + if (target != SkinComponentsContainerLookup.TargetArea.MainHUDComponents || + !layout.TryGetDrawableInfo(null, out var globalHUDComponents) || + resources == null) + break; + + var comboCounters = globalHUDComponents.Where(c => + c.Type.Name == nameof(LegacyComboCounter) || + c.Type.Name == nameof(DefaultComboCounter) || + c.Type.Name == nameof(ArgonComboCounter)).ToArray(); + + layout.Update(null, globalHUDComponents.Except(comboCounters).ToArray()); + + resources.RealmAccess.Run(r => + { + foreach (var ruleset in r.All()) + { + layout.Update(ruleset, layout.TryGetDrawableInfo(ruleset, out var rulesetHUDComponents) + ? rulesetHUDComponents.Concat(comboCounters).ToArray() + : comboCounters); + } + }); + + break; + } + } + } + + #endregion + #region Disposal ~Skin() diff --git a/osu.Game/Skinning/SkinImporter.cs b/osu.Game/Skinning/SkinImporter.cs index 714427f40d..59c7f0ba26 100644 --- a/osu.Game/Skinning/SkinImporter.cs +++ b/osu.Game/Skinning/SkinImporter.cs @@ -223,8 +223,6 @@ namespace osu.Game.Skinning } } - s.LayoutVersion = SkinInfo.LATEST_LAYOUT_VERSION; - string newHash = ComputeHash(s); hadChanges = newHash != s.Hash; diff --git a/osu.Game/Skinning/SkinInfo.cs b/osu.Game/Skinning/SkinInfo.cs index a3d5771b5e..9763d3b57e 100644 --- a/osu.Game/Skinning/SkinInfo.cs +++ b/osu.Game/Skinning/SkinInfo.cs @@ -39,21 +39,6 @@ namespace osu.Game.Skinning public bool Protected { get; set; } - /// - /// The latest version in YYYYMMDD format for skin layout migrations. - /// - /// - /// - /// 20240625: Moves combo counters from ruleset-agnostic to ruleset-specific HUD targets. - /// - /// - public const int LATEST_LAYOUT_VERSION = 20240625; - - /// - /// A version in YYYYMMDD format for applying skin layout migrations. - /// - public int LayoutVersion { get; set; } - public virtual Skin CreateInstance(IStorageResourceProvider resources) { var type = string.IsNullOrEmpty(InstantiationInfo) diff --git a/osu.Game/Skinning/SkinLayoutInfo.cs b/osu.Game/Skinning/SkinLayoutInfo.cs index 115d59b9d0..22c876e5ad 100644 --- a/osu.Game/Skinning/SkinLayoutInfo.cs +++ b/osu.Game/Skinning/SkinLayoutInfo.cs @@ -19,12 +19,26 @@ namespace osu.Game.Skinning { private const string global_identifier = @"global"; - [JsonIgnore] - public IEnumerable AllDrawables => DrawableInfo.Values.SelectMany(v => v); + /// + /// Latest version representing the schema of the skin layout. + /// + /// + /// + /// 0: Initial version of all skin layouts. + /// 1: Moves existing combo counters from global to per-ruleset HUD targets. + /// + /// + public const int LATEST_VERSION = 1; + + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)] + public int Version = LATEST_VERSION; [JsonProperty] public Dictionary DrawableInfo { get; set; } = new Dictionary(); + [JsonIgnore] + public IEnumerable AllDrawables => DrawableInfo.Values.SelectMany(v => v); + public bool TryGetDrawableInfo(RulesetInfo? ruleset, [NotNullWhen(true)] out SerialisedDrawableInfo[]? components) => DrawableInfo.TryGetValue(ruleset?.ShortName ?? global_identifier, out components); From 4cb58fbe474c3c0663fb19284ae6de23ca53b2d4 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 1 Jul 2024 14:58:32 +0900 Subject: [PATCH 1251/2493] Add failing test --- .../Mods/TestSceneManiaModInvert.cs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModInvert.cs b/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModInvert.cs index 2977241dc6..95fe73db50 100644 --- a/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModInvert.cs +++ b/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModInvert.cs @@ -2,6 +2,8 @@ // See the LICENCE file in the repository root for full licence text. using NUnit.Framework; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.Timing; using osu.Game.Rulesets.Mania.Mods; using osu.Game.Tests.Visual; @@ -17,5 +19,22 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods Mod = new ManiaModInvert(), PassCondition = () => Player.ScoreProcessor.JudgedHits >= 2 }); + + [Test] + public void TestBreaksPreservedOnOriginalBeatmap() + { + var beatmap = CreateBeatmap(new ManiaRuleset().RulesetInfo); + beatmap.Breaks.Clear(); + beatmap.Breaks.Add(new BreakPeriod(0, 1000)); + + var workingBeatmap = new FlatWorkingBeatmap(beatmap); + + var playableWithInvert = workingBeatmap.GetPlayableBeatmap(new ManiaRuleset().RulesetInfo, new[] { new ManiaModInvert() }); + Assert.That(playableWithInvert.Breaks.Count, Is.Zero); + + var playableWithoutInvert = workingBeatmap.GetPlayableBeatmap(new ManiaRuleset().RulesetInfo); + Assert.That(playableWithoutInvert.Breaks.Count, Is.Not.Zero); + Assert.That(playableWithoutInvert.Breaks[0], Is.EqualTo(new BreakPeriod(0, 1000))); + } } } From f942595829336ce1334ec86b88fb04be9068412c Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 1 Jul 2024 14:58:53 +0900 Subject: [PATCH 1252/2493] Fix `ManiaModInvert` permanently messing up the beatmap --- osu.Game/Beatmaps/BeatmapConverter.cs | 5 +++++ osu.Game/Beatmaps/IBeatmap.cs | 2 +- osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs | 7 ++++++- osu.Game/Screens/Edit/EditorBeatmap.cs | 6 +++++- 4 files changed, 17 insertions(+), 3 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapConverter.cs b/osu.Game/Beatmaps/BeatmapConverter.cs index b68c80d4b3..0ec8eab5d8 100644 --- a/osu.Game/Beatmaps/BeatmapConverter.cs +++ b/osu.Game/Beatmaps/BeatmapConverter.cs @@ -7,6 +7,8 @@ using System; using System.Collections.Generic; using System.Linq; using System.Threading; +using osu.Framework.Bindables; +using osu.Game.Beatmaps.Timing; using osu.Game.Rulesets; using osu.Game.Rulesets.Objects; @@ -49,6 +51,9 @@ namespace osu.Game.Beatmaps original.BeatmapInfo = original.BeatmapInfo.Clone(); original.ControlPointInfo = original.ControlPointInfo.DeepClone(); + // Used in osu!mania conversion. + original.Breaks = new BindableList(original.Breaks); + return ConvertBeatmap(original, cancellationToken); } diff --git a/osu.Game/Beatmaps/IBeatmap.cs b/osu.Game/Beatmaps/IBeatmap.cs index 072e246a36..d8a2560559 100644 --- a/osu.Game/Beatmaps/IBeatmap.cs +++ b/osu.Game/Beatmaps/IBeatmap.cs @@ -41,7 +41,7 @@ namespace osu.Game.Beatmaps /// /// The breaks in this beatmap. /// - BindableList Breaks { get; } + BindableList Breaks { get; set; } /// /// All lines from the [Events] section which aren't handled in the encoding process yet. diff --git a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs index 4c6a4cc9c2..97037302c6 100644 --- a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs +++ b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs @@ -328,7 +328,12 @@ namespace osu.Game.Rulesets.Difficulty set => baseBeatmap.Difficulty = value; } - public BindableList Breaks => baseBeatmap.Breaks; + public BindableList Breaks + { + get => baseBeatmap.Breaks; + set => baseBeatmap.Breaks = value; + } + public List UnhandledEventLines => baseBeatmap.UnhandledEventLines; public double TotalBreakTime => baseBeatmap.TotalBreakTime; diff --git a/osu.Game/Screens/Edit/EditorBeatmap.cs b/osu.Game/Screens/Edit/EditorBeatmap.cs index ae0fd9130f..331da51888 100644 --- a/osu.Game/Screens/Edit/EditorBeatmap.cs +++ b/osu.Game/Screens/Edit/EditorBeatmap.cs @@ -172,7 +172,11 @@ namespace osu.Game.Screens.Edit set => PlayableBeatmap.ControlPointInfo = value; } - public BindableList Breaks => PlayableBeatmap.Breaks; + public BindableList Breaks + { + get => PlayableBeatmap.Breaks; + set => PlayableBeatmap.Breaks = value; + } public List UnhandledEventLines => PlayableBeatmap.UnhandledEventLines; From 1eb10e029caf98aa112800a3f16c8783be44ed73 Mon Sep 17 00:00:00 2001 From: Nathan Du Date: Mon, 1 Jul 2024 19:34:33 +0800 Subject: [PATCH 1253/2493] Rewrite no release mod Per the request of spaceman_atlas, the No Release mod is rewritten to avoid modifications to DrawableHoldNoteTail. The approach is based on that of the Strict Tracking mod for the osu!(standard) ruleset, injecting the mod behavior by replacing the normal hold note with the mod's variant. The variant inherits most bevaior from the normal hold note, but when creating nested hitobjects, it creates its own hold note tail variant instead, which in turn is used to instantiate the mod's variant of DrawableHoldNoteTail with a new behavior. The time a judgement is awarded is changed from the end of its Perfect window to the time of the tail itself. --- .../Mods/TestSceneManiaModNoRelease.cs | 10 +-- .../Mods/ManiaModNoRelease.cs | 86 +++++++++++++++++-- .../Objects/Drawables/DrawableHoldNoteTail.cs | 24 +----- osu.Game.Rulesets.Mania/Objects/HoldNote.cs | 6 +- 4 files changed, 89 insertions(+), 37 deletions(-) diff --git a/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModNoRelease.cs b/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModNoRelease.cs index 82534ee019..f6e79114de 100644 --- a/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModNoRelease.cs +++ b/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModNoRelease.cs @@ -273,10 +273,10 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods /// /// -----[ ]-------------- - /// xo + /// xo /// [Test] - public void TestPressAndReleaseJustAfterTailWithCloseByHead() + public void TestPressAndReleaseAfterTailWithCloseByHead() { const int duration = 30; @@ -301,11 +301,11 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods performTest(new List { - new ManiaReplayFrame(time_head + duration + 20, ManiaAction.Key1), - new ManiaReplayFrame(time_head + duration + 30), + new ManiaReplayFrame(time_head + duration + 60, ManiaAction.Key1), + new ManiaReplayFrame(time_head + duration + 70), }, beatmap); - assertHeadJudgement(HitResult.Good); + assertHeadJudgement(HitResult.Ok); assertTailJudgement(HitResult.Perfect); } diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModNoRelease.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModNoRelease.cs index f370ef15bd..8cb2e821e6 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModNoRelease.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModNoRelease.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.Linq; +using System.Threading; using osu.Framework.Localisation; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Mania.Beatmaps; +using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.Objects.Drawables; +using osu.Game.Rulesets.Mania.UI; using osu.Game.Rulesets.Mods; -using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Scoring; +using osu.Game.Rulesets.UI; namespace osu.Game.Rulesets.Mania.Mods { - public class ManiaModNoRelease : Mod, IApplicableToDrawableHitObject + public partial class ManiaModNoRelease : Mod, IApplicableAfterBeatmapConversion, IApplicableToDrawableRuleset { public override string Name => "No Release"; @@ -21,14 +27,80 @@ namespace osu.Game.Rulesets.Mania.Mods public override ModType Type => ModType.DifficultyReduction; - public void ApplyToDrawableHitObject(DrawableHitObject drawable) + public void ApplyToBeatmap(IBeatmap beatmap) { - if (drawable is DrawableHoldNote hold) + var maniaBeatmap = (ManiaBeatmap)beatmap; + var hitObjects = maniaBeatmap.HitObjects.Select(obj => { - hold.HitObjectApplied += dho => + if (obj is HoldNote hold) + return new NoReleaseHoldNote(hold); + + return obj; + }).ToList(); + + maniaBeatmap.HitObjects = hitObjects; + } + + public void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset) + { + var maniaRuleset = (DrawableManiaRuleset)drawableRuleset; + + foreach (var stage in maniaRuleset.Playfield.Stages) + { + foreach (var column in stage.Columns) { - ((DrawableHoldNote)dho).Tail.LateReleaseResult = HitResult.Perfect; - }; + column.RegisterPool(10, 50); + } + } + } + + private partial class NoReleaseDrawableHoldNoteTail : DrawableHoldNoteTail + { + protected override void CheckForResult(bool userTriggered, double timeOffset) + { + // apply perfect once the tail is reached + if (HoldNote.HoldStartTime != null && timeOffset >= 0) + ApplyResult(GetCappedResult(HitResult.Perfect)); + else + base.CheckForResult(userTriggered, timeOffset); + } + } + + private class NoReleaseTailNote : TailNote + { + } + + private class NoReleaseHoldNote : HoldNote + { + public NoReleaseHoldNote(HoldNote hold) + { + StartTime = hold.StartTime; + Duration = hold.Duration; + Column = hold.Column; + NodeSamples = hold.NodeSamples; + } + + protected override void CreateNestedHitObjects(CancellationToken cancellationToken) + { + AddNested(Head = new HeadNote + { + StartTime = StartTime, + Column = Column, + Samples = GetNodeSamples(0), + }); + + AddNested(Tail = new NoReleaseTailNote + { + StartTime = EndTime, + Column = Column, + Samples = GetNodeSamples((NodeSamples?.Count - 1) ?? 1), + }); + + AddNested(Body = new HoldNoteBody + { + StartTime = StartTime, + Column = Column + }); } } } diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs index eb1637b0ea..79002b3819 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs @@ -3,7 +3,6 @@ #nullable disable -using System.Diagnostics; using osu.Framework.Graphics; using osu.Framework.Input.Events; using osu.Game.Rulesets.Scoring; @@ -19,11 +18,6 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables protected internal DrawableHoldNote HoldNote => (DrawableHoldNote)ParentHitObject; - /// - /// The minimum uncapped result for a late release. - /// - public HitResult LateReleaseResult { get; set; } = HitResult.Miss; - public DrawableHoldNoteTail() : this(null) { @@ -38,23 +32,9 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables public void UpdateResult() => base.UpdateResult(true); - protected override void CheckForResult(bool userTriggered, double timeOffset) - { - Debug.Assert(HitObject.HitWindows != null); - + protected override void CheckForResult(bool userTriggered, double timeOffset) => // Factor in the release lenience - double scaledTimeOffset = timeOffset / TailNote.RELEASE_WINDOW_LENIENCE; - - // Check for late release - if (HoldNote.HoldStartTime != null && scaledTimeOffset > HitObject.HitWindows.WindowFor(LateReleaseResult)) - { - ApplyResult(GetCappedResult(LateReleaseResult)); - } - else - { - base.CheckForResult(userTriggered, scaledTimeOffset); - } - } + base.CheckForResult(userTriggered, timeOffset / TailNote.RELEASE_WINDOW_LENIENCE); protected override HitResult GetCappedResult(HitResult result) { diff --git a/osu.Game.Rulesets.Mania/Objects/HoldNote.cs b/osu.Game.Rulesets.Mania/Objects/HoldNote.cs index 3f930a310b..6be0ee2d6b 100644 --- a/osu.Game.Rulesets.Mania/Objects/HoldNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/HoldNote.cs @@ -72,18 +72,18 @@ namespace osu.Game.Rulesets.Mania.Objects /// /// The head note of the hold. /// - public HeadNote Head { get; private set; } + public HeadNote Head { get; protected set; } /// /// The tail note of the hold. /// - public TailNote Tail { get; private set; } + public TailNote Tail { get; protected set; } /// /// The body of the hold. /// This is an invisible and silent object that tracks the holding state of the . /// - public HoldNoteBody Body { get; private set; } + public HoldNoteBody Body { get; protected set; } public override double MaximumJudgementOffset => Tail.MaximumJudgementOffset; From 005af280f2ee8c05adb0bc0b5609b52eb40900fc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 2 Jul 2024 11:13:20 +0900 Subject: [PATCH 1254/2493] Isolate bindable breaks list to `EditorBeatmap` --- .../TestSceneEditorBeatmapProcessor.cs | 85 +++++++++++-------- osu.Game/Beatmaps/Beatmap.cs | 3 +- osu.Game/Beatmaps/BeatmapConverter.cs | 4 +- osu.Game/Beatmaps/IBeatmap.cs | 3 +- .../Difficulty/DifficultyCalculator.cs | 3 +- osu.Game/Screens/Edit/EditorBeatmap.cs | 8 +- .../Screens/Edit/EditorBeatmapProcessor.cs | 6 +- 7 files changed, 66 insertions(+), 46 deletions(-) diff --git a/osu.Game.Tests/Editing/TestSceneEditorBeatmapProcessor.cs b/osu.Game.Tests/Editing/TestSceneEditorBeatmapProcessor.cs index 1a3f0aa3df..251099c0e2 100644 --- a/osu.Game.Tests/Editing/TestSceneEditorBeatmapProcessor.cs +++ b/osu.Game.Tests/Editing/TestSceneEditorBeatmapProcessor.cs @@ -21,10 +21,11 @@ namespace osu.Game.Tests.Editing { var controlPoints = new ControlPointInfo(); controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 }); - var beatmap = new Beatmap + var beatmap = new EditorBeatmap(new Beatmap { ControlPointInfo = controlPoints, - }; + BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo }, + }); var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new OsuRuleset()); beatmapProcessor.PreProcess(); @@ -38,14 +39,15 @@ namespace osu.Game.Tests.Editing { var controlPoints = new ControlPointInfo(); controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 }); - var beatmap = new Beatmap + var beatmap = new EditorBeatmap(new Beatmap { ControlPointInfo = controlPoints, + BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo }, HitObjects = { new HitCircle { StartTime = 1000 }, } - }; + }); var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new OsuRuleset()); beatmapProcessor.PreProcess(); @@ -59,15 +61,16 @@ namespace osu.Game.Tests.Editing { var controlPoints = new ControlPointInfo(); controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 }); - var beatmap = new Beatmap + var beatmap = new EditorBeatmap(new Beatmap { ControlPointInfo = controlPoints, + BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo }, HitObjects = { new HitCircle { StartTime = 1000 }, new HitCircle { StartTime = 2000 }, } - }; + }); var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new OsuRuleset()); beatmapProcessor.PreProcess(); @@ -81,14 +84,15 @@ namespace osu.Game.Tests.Editing { var controlPoints = new ControlPointInfo(); controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 }); - var beatmap = new Beatmap + var beatmap = new EditorBeatmap(new Beatmap { ControlPointInfo = controlPoints, + BeatmapInfo = { Ruleset = new ManiaRuleset().RulesetInfo }, HitObjects = { new HoldNote { StartTime = 1000, Duration = 10000 }, } - }; + }); var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new ManiaRuleset()); beatmapProcessor.PreProcess(); @@ -102,16 +106,17 @@ namespace osu.Game.Tests.Editing { var controlPoints = new ControlPointInfo(); controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 }); - var beatmap = new Beatmap + var beatmap = new EditorBeatmap(new Beatmap { ControlPointInfo = controlPoints, + BeatmapInfo = { Ruleset = new ManiaRuleset().RulesetInfo }, HitObjects = { new HoldNote { StartTime = 1000, Duration = 10000 }, new Note { StartTime = 2000 }, new Note { StartTime = 12000 }, } - }; + }); var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new ManiaRuleset()); beatmapProcessor.PreProcess(); @@ -125,15 +130,16 @@ namespace osu.Game.Tests.Editing { var controlPoints = new ControlPointInfo(); controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 }); - var beatmap = new Beatmap + var beatmap = new EditorBeatmap(new Beatmap { ControlPointInfo = controlPoints, + BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo }, HitObjects = { new HitCircle { StartTime = 1000 }, new HitCircle { StartTime = 5000 }, } - }; + }); var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new OsuRuleset()); beatmapProcessor.PreProcess(); @@ -152,9 +158,10 @@ namespace osu.Game.Tests.Editing { var controlPoints = new ControlPointInfo(); controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 }); - var beatmap = new Beatmap + var beatmap = new EditorBeatmap(new Beatmap { ControlPointInfo = controlPoints, + BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo }, HitObjects = { new HitCircle { StartTime = 1000 }, @@ -165,7 +172,7 @@ namespace osu.Game.Tests.Editing new BreakPeriod(1200, 4000), new BreakPeriod(5200, 8000), } - }; + }); var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new OsuRuleset()); beatmapProcessor.PreProcess(); @@ -184,9 +191,10 @@ namespace osu.Game.Tests.Editing { var controlPoints = new ControlPointInfo(); controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 }); - var beatmap = new Beatmap + var beatmap = new EditorBeatmap(new Beatmap { ControlPointInfo = controlPoints, + BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo }, HitObjects = { new HitCircle { StartTime = 1000 }, @@ -197,7 +205,7 @@ namespace osu.Game.Tests.Editing { new BreakPeriod(1200, 8000), } - }; + }); var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new OsuRuleset()); beatmapProcessor.PreProcess(); @@ -218,9 +226,10 @@ namespace osu.Game.Tests.Editing { var controlPoints = new ControlPointInfo(); controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 }); - var beatmap = new Beatmap + var beatmap = new EditorBeatmap(new Beatmap { ControlPointInfo = controlPoints, + BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo }, HitObjects = { new HitCircle { StartTime = 1100 }, @@ -230,7 +239,7 @@ namespace osu.Game.Tests.Editing { new BreakPeriod(1200, 8000), } - }; + }); var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new OsuRuleset()); beatmapProcessor.PreProcess(); @@ -249,9 +258,10 @@ namespace osu.Game.Tests.Editing { var controlPoints = new ControlPointInfo(); controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 }); - var beatmap = new Beatmap + var beatmap = new EditorBeatmap(new Beatmap { ControlPointInfo = controlPoints, + BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo }, HitObjects = { new HitCircle { StartTime = 1000 }, @@ -262,7 +272,7 @@ namespace osu.Game.Tests.Editing new ManualBreakPeriod(1200, 4000), new ManualBreakPeriod(5200, 8000), } - }; + }); var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new OsuRuleset()); beatmapProcessor.PreProcess(); @@ -283,9 +293,10 @@ namespace osu.Game.Tests.Editing { var controlPoints = new ControlPointInfo(); controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 }); - var beatmap = new Beatmap + var beatmap = new EditorBeatmap(new Beatmap { ControlPointInfo = controlPoints, + BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo }, HitObjects = { new HitCircle { StartTime = 1000 }, @@ -296,7 +307,7 @@ namespace osu.Game.Tests.Editing { new ManualBreakPeriod(1200, 8000), } - }; + }); var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new OsuRuleset()); beatmapProcessor.PreProcess(); @@ -317,9 +328,10 @@ namespace osu.Game.Tests.Editing { var controlPoints = new ControlPointInfo(); controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 }); - var beatmap = new Beatmap + var beatmap = new EditorBeatmap(new Beatmap { ControlPointInfo = controlPoints, + BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo }, HitObjects = { new HitCircle { StartTime = 1000 }, @@ -329,7 +341,7 @@ namespace osu.Game.Tests.Editing { new ManualBreakPeriod(1200, 8800), } - }; + }); var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new OsuRuleset()); beatmapProcessor.PreProcess(); @@ -348,9 +360,10 @@ namespace osu.Game.Tests.Editing { var controlPoints = new ControlPointInfo(); controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 }); - var beatmap = new Beatmap + var beatmap = new EditorBeatmap(new Beatmap { ControlPointInfo = controlPoints, + BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo }, HitObjects = { new HitCircle { StartTime = 1000 }, @@ -360,7 +373,7 @@ namespace osu.Game.Tests.Editing { new BreakPeriod(10000, 15000), } - }; + }); var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new OsuRuleset()); beatmapProcessor.PreProcess(); @@ -374,9 +387,10 @@ namespace osu.Game.Tests.Editing { var controlPoints = new ControlPointInfo(); controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 }); - var beatmap = new Beatmap + var beatmap = new EditorBeatmap(new Beatmap { ControlPointInfo = controlPoints, + BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo }, HitObjects = { new HitCircle { StartTime = 1000 }, @@ -386,7 +400,7 @@ namespace osu.Game.Tests.Editing { new ManualBreakPeriod(10000, 15000), } - }; + }); var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new OsuRuleset()); beatmapProcessor.PreProcess(); @@ -400,9 +414,10 @@ namespace osu.Game.Tests.Editing { var controlPoints = new ControlPointInfo(); controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 }); - var beatmap = new Beatmap + var beatmap = new EditorBeatmap(new Beatmap { ControlPointInfo = controlPoints, + BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo }, HitObjects = { new HoldNote { StartTime = 1000, EndTime = 20000 }, @@ -412,7 +427,7 @@ namespace osu.Game.Tests.Editing { new ManualBreakPeriod(10000, 15000), } - }; + }); var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new OsuRuleset()); beatmapProcessor.PreProcess(); @@ -426,9 +441,10 @@ namespace osu.Game.Tests.Editing { var controlPoints = new ControlPointInfo(); controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 }); - var beatmap = new Beatmap + var beatmap = new EditorBeatmap(new Beatmap { ControlPointInfo = controlPoints, + BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo }, HitObjects = { new HitCircle { StartTime = 10000 }, @@ -438,7 +454,7 @@ namespace osu.Game.Tests.Editing { new BreakPeriod(0, 9000), } - }; + }); var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new OsuRuleset()); beatmapProcessor.PreProcess(); @@ -452,9 +468,10 @@ namespace osu.Game.Tests.Editing { var controlPoints = new ControlPointInfo(); controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 }); - var beatmap = new Beatmap + var beatmap = new EditorBeatmap(new Beatmap { ControlPointInfo = controlPoints, + BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo }, HitObjects = { new HitCircle { StartTime = 10000 }, @@ -464,7 +481,7 @@ namespace osu.Game.Tests.Editing { new ManualBreakPeriod(0, 9000), } - }; + }); var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new OsuRuleset()); beatmapProcessor.PreProcess(); diff --git a/osu.Game/Beatmaps/Beatmap.cs b/osu.Game/Beatmaps/Beatmap.cs index 510410bc09..ae77e4adcf 100644 --- a/osu.Game/Beatmaps/Beatmap.cs +++ b/osu.Game/Beatmaps/Beatmap.cs @@ -8,7 +8,6 @@ using System.Collections.Generic; using System.Linq; using osu.Game.Beatmaps.ControlPoints; using Newtonsoft.Json; -using osu.Framework.Bindables; using osu.Game.IO.Serialization.Converters; namespace osu.Game.Beatmaps @@ -62,7 +61,7 @@ namespace osu.Game.Beatmaps public ControlPointInfo ControlPointInfo { get; set; } = new ControlPointInfo(); - public BindableList Breaks { get; set; } = new BindableList(); + public List Breaks { get; set; } = new List(); public List UnhandledEventLines { get; set; } = new List(); diff --git a/osu.Game/Beatmaps/BeatmapConverter.cs b/osu.Game/Beatmaps/BeatmapConverter.cs index 0ec8eab5d8..676eb1b159 100644 --- a/osu.Game/Beatmaps/BeatmapConverter.cs +++ b/osu.Game/Beatmaps/BeatmapConverter.cs @@ -7,8 +7,6 @@ using System; using System.Collections.Generic; using System.Linq; using System.Threading; -using osu.Framework.Bindables; -using osu.Game.Beatmaps.Timing; using osu.Game.Rulesets; using osu.Game.Rulesets.Objects; @@ -52,7 +50,7 @@ namespace osu.Game.Beatmaps original.ControlPointInfo = original.ControlPointInfo.DeepClone(); // Used in osu!mania conversion. - original.Breaks = new BindableList(original.Breaks); + original.Breaks = original.Breaks.ToList(); return ConvertBeatmap(original, cancellationToken); } diff --git a/osu.Game/Beatmaps/IBeatmap.cs b/osu.Game/Beatmaps/IBeatmap.cs index d8a2560559..0d39c1f977 100644 --- a/osu.Game/Beatmaps/IBeatmap.cs +++ b/osu.Game/Beatmaps/IBeatmap.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.Linq; -using osu.Framework.Bindables; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.Timing; using osu.Game.Rulesets.Objects; @@ -41,7 +40,7 @@ namespace osu.Game.Beatmaps /// /// The breaks in this beatmap. /// - BindableList Breaks { get; set; } + List Breaks { get; set; } /// /// All lines from the [Events] section which aren't handled in the encoding process yet. diff --git a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs index 97037302c6..722263c58e 100644 --- a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs +++ b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs @@ -9,7 +9,6 @@ using System.Linq; using System.Threading; using JetBrains.Annotations; using osu.Framework.Audio.Track; -using osu.Framework.Bindables; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; @@ -328,7 +327,7 @@ namespace osu.Game.Rulesets.Difficulty set => baseBeatmap.Difficulty = value; } - public BindableList Breaks + public List Breaks { get => baseBeatmap.Breaks; set => baseBeatmap.Breaks = value; diff --git a/osu.Game/Screens/Edit/EditorBeatmap.cs b/osu.Game/Screens/Edit/EditorBeatmap.cs index 331da51888..1ebd2e6337 100644 --- a/osu.Game/Screens/Edit/EditorBeatmap.cs +++ b/osu.Game/Screens/Edit/EditorBeatmap.cs @@ -110,6 +110,9 @@ namespace osu.Game.Screens.Edit foreach (var obj in HitObjects) trackStartTime(obj); + Breaks = new BindableList(playableBeatmap.Breaks); + Breaks.BindCollectionChanged((_, _) => playableBeatmap.Breaks = Breaks.ToList()); + PreviewTime = new BindableInt(BeatmapInfo.Metadata.PreviewTime); PreviewTime.BindValueChanged(s => { @@ -172,7 +175,9 @@ namespace osu.Game.Screens.Edit set => PlayableBeatmap.ControlPointInfo = value; } - public BindableList Breaks + public readonly BindableList Breaks; + + List IBeatmap.Breaks { get => PlayableBeatmap.Breaks; set => PlayableBeatmap.Breaks = value; @@ -191,6 +196,7 @@ namespace osu.Game.Screens.Edit public IBeatmap Clone() => (EditorBeatmap)MemberwiseClone(); private IList mutableHitObjects => (IList)PlayableBeatmap.HitObjects; + private IList mutableBreaks => (IList)PlayableBeatmap.Breaks; private readonly List batchPendingInserts = new List(); diff --git a/osu.Game/Screens/Edit/EditorBeatmapProcessor.cs b/osu.Game/Screens/Edit/EditorBeatmapProcessor.cs index 99c8c3572b..377e978c4a 100644 --- a/osu.Game/Screens/Edit/EditorBeatmapProcessor.cs +++ b/osu.Game/Screens/Edit/EditorBeatmapProcessor.cs @@ -12,11 +12,13 @@ namespace osu.Game.Screens.Edit { public class EditorBeatmapProcessor : IBeatmapProcessor { - public IBeatmap Beatmap { get; } + public EditorBeatmap Beatmap { get; } + + IBeatmap IBeatmapProcessor.Beatmap => Beatmap; private readonly IBeatmapProcessor? rulesetBeatmapProcessor; - public EditorBeatmapProcessor(IBeatmap beatmap, Ruleset ruleset) + public EditorBeatmapProcessor(EditorBeatmap beatmap, Ruleset ruleset) { Beatmap = beatmap; rulesetBeatmapProcessor = ruleset.CreateBeatmapProcessor(beatmap); From f694ae416eba180f03c55a0374018a2ccf53a593 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 2 Jul 2024 11:47:40 +0900 Subject: [PATCH 1255/2493] Fix typo in xmldoc --- osu.Game/Beatmaps/IBeatmap.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/IBeatmap.cs b/osu.Game/Beatmaps/IBeatmap.cs index 0d39c1f977..176738489a 100644 --- a/osu.Game/Beatmaps/IBeatmap.cs +++ b/osu.Game/Beatmaps/IBeatmap.cs @@ -44,7 +44,7 @@ namespace osu.Game.Beatmaps /// /// All lines from the [Events] section which aren't handled in the encoding process yet. - /// These lines shoule be written out to the beatmap file on save or export. + /// These lines should be written out to the beatmap file on save or export. /// List UnhandledEventLines { get; } From 2c3b411bb5e77696a29abf5fdeb873b1d72c47a7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 2 Jul 2024 11:59:24 +0900 Subject: [PATCH 1256/2493] Change breaks list to `IReadOnlyList` --- .../Mods/TestSceneManiaModInvert.cs | 7 +++++-- osu.Game/Beatmaps/Beatmap.cs | 6 ++++++ osu.Game/Beatmaps/BeatmapConverter.cs | 7 +++---- osu.Game/Beatmaps/IBeatmap.cs | 2 +- osu.Game/Database/LegacyBeatmapExporter.cs | 6 +++++- osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs | 2 +- osu.Game/Screens/Edit/EditorBeatmap.cs | 2 +- 7 files changed, 22 insertions(+), 10 deletions(-) diff --git a/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModInvert.cs b/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModInvert.cs index 95fe73db50..576b07265b 100644 --- a/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModInvert.cs +++ b/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModInvert.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 NUnit.Framework; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Timing; @@ -24,8 +25,10 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods public void TestBreaksPreservedOnOriginalBeatmap() { var beatmap = CreateBeatmap(new ManiaRuleset().RulesetInfo); - beatmap.Breaks.Clear(); - beatmap.Breaks.Add(new BreakPeriod(0, 1000)); + var breaks = (List)beatmap.Breaks; + + breaks.Clear(); + breaks.Add(new BreakPeriod(0, 1000)); var workingBeatmap = new FlatWorkingBeatmap(beatmap); diff --git a/osu.Game/Beatmaps/Beatmap.cs b/osu.Game/Beatmaps/Beatmap.cs index ae77e4adcf..b185c1cd5a 100644 --- a/osu.Game/Beatmaps/Beatmap.cs +++ b/osu.Game/Beatmaps/Beatmap.cs @@ -61,6 +61,12 @@ namespace osu.Game.Beatmaps public ControlPointInfo ControlPointInfo { get; set; } = new ControlPointInfo(); + IReadOnlyList IBeatmap.Breaks + { + get => Breaks; + set => Breaks = new List(value); + } + public List Breaks { get; set; } = new List(); public List UnhandledEventLines { get; set; } = new List(); diff --git a/osu.Game/Beatmaps/BeatmapConverter.cs b/osu.Game/Beatmaps/BeatmapConverter.cs index 676eb1b159..e62de3e69b 100644 --- a/osu.Game/Beatmaps/BeatmapConverter.cs +++ b/osu.Game/Beatmaps/BeatmapConverter.cs @@ -7,6 +7,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Threading; +using osu.Game.Beatmaps.Timing; using osu.Game.Rulesets; using osu.Game.Rulesets.Objects; @@ -49,9 +50,6 @@ namespace osu.Game.Beatmaps original.BeatmapInfo = original.BeatmapInfo.Clone(); original.ControlPointInfo = original.ControlPointInfo.DeepClone(); - // Used in osu!mania conversion. - original.Breaks = original.Breaks.ToList(); - return ConvertBeatmap(original, cancellationToken); } @@ -68,7 +66,8 @@ namespace osu.Game.Beatmaps beatmap.BeatmapInfo = original.BeatmapInfo; beatmap.ControlPointInfo = original.ControlPointInfo; beatmap.HitObjects = convertHitObjects(original.HitObjects, original, cancellationToken).OrderBy(s => s.StartTime).ToList(); - beatmap.Breaks = original.Breaks; + // Used in osu!mania conversion. + beatmap.Breaks = new List(original.Breaks); beatmap.UnhandledEventLines = original.UnhandledEventLines; return beatmap; diff --git a/osu.Game/Beatmaps/IBeatmap.cs b/osu.Game/Beatmaps/IBeatmap.cs index 176738489a..151edc9ad8 100644 --- a/osu.Game/Beatmaps/IBeatmap.cs +++ b/osu.Game/Beatmaps/IBeatmap.cs @@ -40,7 +40,7 @@ namespace osu.Game.Beatmaps /// /// The breaks in this beatmap. /// - List Breaks { get; set; } + IReadOnlyList Breaks { get; set; } /// /// All lines from the [Events] section which aren't handled in the encoding process yet. diff --git a/osu.Game/Database/LegacyBeatmapExporter.cs b/osu.Game/Database/LegacyBeatmapExporter.cs index 17c2c8c88d..62f6f5d30d 100644 --- a/osu.Game/Database/LegacyBeatmapExporter.cs +++ b/osu.Game/Database/LegacyBeatmapExporter.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; @@ -64,8 +65,11 @@ namespace osu.Game.Database foreach (var controlPoint in playableBeatmap.ControlPointInfo.AllControlPoints) controlPoint.Time = Math.Floor(controlPoint.Time); + var breaks = new List(playableBeatmap.Breaks.Count); for (int i = 0; i < playableBeatmap.Breaks.Count; i++) - playableBeatmap.Breaks[i] = new BreakPeriod(Math.Floor(playableBeatmap.Breaks[i].StartTime), Math.Floor(playableBeatmap.Breaks[i].EndTime)); + breaks.Add(new BreakPeriod(Math.Floor(playableBeatmap.Breaks[i].StartTime), Math.Floor(playableBeatmap.Breaks[i].EndTime))); + + playableBeatmap.Breaks = breaks; foreach (var hitObject in playableBeatmap.HitObjects) { diff --git a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs index 722263c58e..7262b9d1a8 100644 --- a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs +++ b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs @@ -327,7 +327,7 @@ namespace osu.Game.Rulesets.Difficulty set => baseBeatmap.Difficulty = value; } - public List Breaks + public IReadOnlyList Breaks { get => baseBeatmap.Breaks; set => baseBeatmap.Breaks = value; diff --git a/osu.Game/Screens/Edit/EditorBeatmap.cs b/osu.Game/Screens/Edit/EditorBeatmap.cs index 1ebd2e6337..cc1d820427 100644 --- a/osu.Game/Screens/Edit/EditorBeatmap.cs +++ b/osu.Game/Screens/Edit/EditorBeatmap.cs @@ -177,7 +177,7 @@ namespace osu.Game.Screens.Edit public readonly BindableList Breaks; - List IBeatmap.Breaks + IReadOnlyList IBeatmap.Breaks { get => PlayableBeatmap.Breaks; set => PlayableBeatmap.Breaks = value; From f69bc40a4b465a6f6902d2038fcf824fb7b638bb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 2 Jul 2024 12:07:13 +0900 Subject: [PATCH 1257/2493] Move break cloning back to non-virtual method --- osu.Game/Beatmaps/BeatmapConverter.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapConverter.cs b/osu.Game/Beatmaps/BeatmapConverter.cs index e62de3e69b..5fd20d5aff 100644 --- a/osu.Game/Beatmaps/BeatmapConverter.cs +++ b/osu.Game/Beatmaps/BeatmapConverter.cs @@ -49,6 +49,8 @@ namespace osu.Game.Beatmaps // Can potentially be removed after `Beatmap.Difficulty` doesn't save back to `Beatmap.BeatmapInfo`. original.BeatmapInfo = original.BeatmapInfo.Clone(); original.ControlPointInfo = original.ControlPointInfo.DeepClone(); + // Used in osu!mania conversion. + original.Breaks = new List(original.Breaks); return ConvertBeatmap(original, cancellationToken); } @@ -66,8 +68,6 @@ namespace osu.Game.Beatmaps beatmap.BeatmapInfo = original.BeatmapInfo; beatmap.ControlPointInfo = original.ControlPointInfo; beatmap.HitObjects = convertHitObjects(original.HitObjects, original, cancellationToken).OrderBy(s => s.StartTime).ToList(); - // Used in osu!mania conversion. - beatmap.Breaks = new List(original.Breaks); beatmap.UnhandledEventLines = original.UnhandledEventLines; return beatmap; From db847112149ae8e27d5960f2a7909acb5c91f480 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 2 Jul 2024 12:16:10 +0900 Subject: [PATCH 1258/2493] Revert "Move break cloning back to non-virtual method" This reverts commit f69bc40a4b465a6f6902d2038fcf824fb7b638bb. --- osu.Game/Beatmaps/BeatmapConverter.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapConverter.cs b/osu.Game/Beatmaps/BeatmapConverter.cs index 5fd20d5aff..e62de3e69b 100644 --- a/osu.Game/Beatmaps/BeatmapConverter.cs +++ b/osu.Game/Beatmaps/BeatmapConverter.cs @@ -49,8 +49,6 @@ namespace osu.Game.Beatmaps // Can potentially be removed after `Beatmap.Difficulty` doesn't save back to `Beatmap.BeatmapInfo`. original.BeatmapInfo = original.BeatmapInfo.Clone(); original.ControlPointInfo = original.ControlPointInfo.DeepClone(); - // Used in osu!mania conversion. - original.Breaks = new List(original.Breaks); return ConvertBeatmap(original, cancellationToken); } @@ -68,6 +66,8 @@ namespace osu.Game.Beatmaps beatmap.BeatmapInfo = original.BeatmapInfo; beatmap.ControlPointInfo = original.ControlPointInfo; beatmap.HitObjects = convertHitObjects(original.HitObjects, original, cancellationToken).OrderBy(s => s.StartTime).ToList(); + // Used in osu!mania conversion. + beatmap.Breaks = new List(original.Breaks); beatmap.UnhandledEventLines = original.UnhandledEventLines; return beatmap; From 04da1209f7d4caff3bc5689bea9c53d4eb2e1c0a Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 2 Jul 2024 12:16:11 +0900 Subject: [PATCH 1259/2493] Revert "Change breaks list to `IReadOnlyList`" This reverts commit 2c3b411bb5e77696a29abf5fdeb873b1d72c47a7. --- .../Mods/TestSceneManiaModInvert.cs | 7 ++----- osu.Game/Beatmaps/Beatmap.cs | 6 ------ osu.Game/Beatmaps/BeatmapConverter.cs | 7 ++++--- osu.Game/Beatmaps/IBeatmap.cs | 2 +- osu.Game/Database/LegacyBeatmapExporter.cs | 6 +----- osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs | 2 +- osu.Game/Screens/Edit/EditorBeatmap.cs | 2 +- 7 files changed, 10 insertions(+), 22 deletions(-) diff --git a/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModInvert.cs b/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModInvert.cs index 576b07265b..95fe73db50 100644 --- a/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModInvert.cs +++ b/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModInvert.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Collections.Generic; using NUnit.Framework; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Timing; @@ -25,10 +24,8 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods public void TestBreaksPreservedOnOriginalBeatmap() { var beatmap = CreateBeatmap(new ManiaRuleset().RulesetInfo); - var breaks = (List)beatmap.Breaks; - - breaks.Clear(); - breaks.Add(new BreakPeriod(0, 1000)); + beatmap.Breaks.Clear(); + beatmap.Breaks.Add(new BreakPeriod(0, 1000)); var workingBeatmap = new FlatWorkingBeatmap(beatmap); diff --git a/osu.Game/Beatmaps/Beatmap.cs b/osu.Game/Beatmaps/Beatmap.cs index b185c1cd5a..ae77e4adcf 100644 --- a/osu.Game/Beatmaps/Beatmap.cs +++ b/osu.Game/Beatmaps/Beatmap.cs @@ -61,12 +61,6 @@ namespace osu.Game.Beatmaps public ControlPointInfo ControlPointInfo { get; set; } = new ControlPointInfo(); - IReadOnlyList IBeatmap.Breaks - { - get => Breaks; - set => Breaks = new List(value); - } - public List Breaks { get; set; } = new List(); public List UnhandledEventLines { get; set; } = new List(); diff --git a/osu.Game/Beatmaps/BeatmapConverter.cs b/osu.Game/Beatmaps/BeatmapConverter.cs index e62de3e69b..676eb1b159 100644 --- a/osu.Game/Beatmaps/BeatmapConverter.cs +++ b/osu.Game/Beatmaps/BeatmapConverter.cs @@ -7,7 +7,6 @@ using System; using System.Collections.Generic; using System.Linq; using System.Threading; -using osu.Game.Beatmaps.Timing; using osu.Game.Rulesets; using osu.Game.Rulesets.Objects; @@ -50,6 +49,9 @@ namespace osu.Game.Beatmaps original.BeatmapInfo = original.BeatmapInfo.Clone(); original.ControlPointInfo = original.ControlPointInfo.DeepClone(); + // Used in osu!mania conversion. + original.Breaks = original.Breaks.ToList(); + return ConvertBeatmap(original, cancellationToken); } @@ -66,8 +68,7 @@ namespace osu.Game.Beatmaps beatmap.BeatmapInfo = original.BeatmapInfo; beatmap.ControlPointInfo = original.ControlPointInfo; beatmap.HitObjects = convertHitObjects(original.HitObjects, original, cancellationToken).OrderBy(s => s.StartTime).ToList(); - // Used in osu!mania conversion. - beatmap.Breaks = new List(original.Breaks); + beatmap.Breaks = original.Breaks; beatmap.UnhandledEventLines = original.UnhandledEventLines; return beatmap; diff --git a/osu.Game/Beatmaps/IBeatmap.cs b/osu.Game/Beatmaps/IBeatmap.cs index 151edc9ad8..176738489a 100644 --- a/osu.Game/Beatmaps/IBeatmap.cs +++ b/osu.Game/Beatmaps/IBeatmap.cs @@ -40,7 +40,7 @@ namespace osu.Game.Beatmaps /// /// The breaks in this beatmap. /// - IReadOnlyList Breaks { get; set; } + List Breaks { get; set; } /// /// All lines from the [Events] section which aren't handled in the encoding process yet. diff --git a/osu.Game/Database/LegacyBeatmapExporter.cs b/osu.Game/Database/LegacyBeatmapExporter.cs index 62f6f5d30d..17c2c8c88d 100644 --- a/osu.Game/Database/LegacyBeatmapExporter.cs +++ b/osu.Game/Database/LegacyBeatmapExporter.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; @@ -65,11 +64,8 @@ namespace osu.Game.Database foreach (var controlPoint in playableBeatmap.ControlPointInfo.AllControlPoints) controlPoint.Time = Math.Floor(controlPoint.Time); - var breaks = new List(playableBeatmap.Breaks.Count); for (int i = 0; i < playableBeatmap.Breaks.Count; i++) - breaks.Add(new BreakPeriod(Math.Floor(playableBeatmap.Breaks[i].StartTime), Math.Floor(playableBeatmap.Breaks[i].EndTime))); - - playableBeatmap.Breaks = breaks; + playableBeatmap.Breaks[i] = new BreakPeriod(Math.Floor(playableBeatmap.Breaks[i].StartTime), Math.Floor(playableBeatmap.Breaks[i].EndTime)); foreach (var hitObject in playableBeatmap.HitObjects) { diff --git a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs index 7262b9d1a8..722263c58e 100644 --- a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs +++ b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs @@ -327,7 +327,7 @@ namespace osu.Game.Rulesets.Difficulty set => baseBeatmap.Difficulty = value; } - public IReadOnlyList Breaks + public List Breaks { get => baseBeatmap.Breaks; set => baseBeatmap.Breaks = value; diff --git a/osu.Game/Screens/Edit/EditorBeatmap.cs b/osu.Game/Screens/Edit/EditorBeatmap.cs index cc1d820427..1ebd2e6337 100644 --- a/osu.Game/Screens/Edit/EditorBeatmap.cs +++ b/osu.Game/Screens/Edit/EditorBeatmap.cs @@ -177,7 +177,7 @@ namespace osu.Game.Screens.Edit public readonly BindableList Breaks; - IReadOnlyList IBeatmap.Breaks + List IBeatmap.Breaks { get => PlayableBeatmap.Breaks; set => PlayableBeatmap.Breaks = value; From 31edca866c6d921cc0f628f4074aece1a63f8c75 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 2 Jul 2024 12:21:24 +0900 Subject: [PATCH 1260/2493] Remove unused code --- osu.Game/Screens/Edit/EditorBeatmap.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Screens/Edit/EditorBeatmap.cs b/osu.Game/Screens/Edit/EditorBeatmap.cs index 1ebd2e6337..c8592b5bea 100644 --- a/osu.Game/Screens/Edit/EditorBeatmap.cs +++ b/osu.Game/Screens/Edit/EditorBeatmap.cs @@ -196,7 +196,6 @@ namespace osu.Game.Screens.Edit public IBeatmap Clone() => (EditorBeatmap)MemberwiseClone(); private IList mutableHitObjects => (IList)PlayableBeatmap.HitObjects; - private IList mutableBreaks => (IList)PlayableBeatmap.Breaks; private readonly List batchPendingInserts = new List(); From d4a8f6c8b085e6bf8f3ed17c038c8d13b1ab76b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 2 Jul 2024 09:12:34 +0200 Subject: [PATCH 1261/2493] Do not add extra sample control point after end of `IHasRepeats` objects --- .../per-slider-node-sample-settings.osu | 19 +++++++------------ .../Beatmaps/Formats/LegacyBeatmapEncoder.cs | 2 +- 2 files changed, 8 insertions(+), 13 deletions(-) diff --git a/osu.Game.Tests/Resources/per-slider-node-sample-settings.osu b/osu.Game.Tests/Resources/per-slider-node-sample-settings.osu index 2f56465d90..8b10f21f52 100644 --- a/osu.Game.Tests/Resources/per-slider-node-sample-settings.osu +++ b/osu.Game.Tests/Resources/per-slider-node-sample-settings.osu @@ -1,4 +1,4 @@ -osu file format v128 +osu file format v128 [General] SampleSet: Normal @@ -7,18 +7,13 @@ SampleSet: Normal 15,1000,4,1,0,100,1,0 2271,-100,4,1,0,5,0,0 6021,-100,4,1,0,100,0,0 -9515,-100,4,1,0,5,0,0 -9521,-100,4,1,0,100,0,0 -10265,-100,4,1,0,5,0,0 -13765,-100,4,1,0,100,0,0 -13771,-100,4,1,0,5,0,0 +8515,-100,4,1,0,5,0,0 +12765,-100,4,1,0,100,0,0 +14764,-100,4,1,0,5,0,0 14770,-100,4,1,0,50,0,0 -18264,-100,4,1,0,100,0,0 -18270,-100,4,1,0,50,0,0 -21764,-100,4,1,0,5,0,0 -21770,-100,4,1,0,50,0,0 -25264,-100,4,1,0,100,0,0 -25270,-100,4,1,0,50,0,0 +17264,-100,4,1,0,5,0,0 +17270,-100,4,1,0,50,0,0 +22264,-100,4,1,0,100,0,0 [HitObjects] 113,54,2265,6,0,L|422:55,1,300,0|0,1:0|1:0,1:0:0:0: diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs index 54f23d8ecc..8a8964ccd4 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs @@ -293,7 +293,7 @@ namespace osu.Game.Beatmaps.Formats if (hasNodeSamples.NodeSamples[i].Count > 0) yield return createSampleControlPointFor(nodeTime, hasNodeSamples.NodeSamples[i]); - if (spanDuration > LegacyBeatmapDecoder.CONTROL_POINT_LENIENCY + 1 && hitObject.Samples.Count > 0) + if (spanDuration > LegacyBeatmapDecoder.CONTROL_POINT_LENIENCY + 1 && hitObject.Samples.Count > 0 && i < hasNodeSamples.NodeSamples.Count - 1) yield return createSampleControlPointFor(nodeTime + LegacyBeatmapDecoder.CONTROL_POINT_LENIENCY + 1, hitObject.Samples); } } From 1d94c96a8efef24d5ae54aa7159f6e7a615a50a8 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 2 Jul 2024 14:40:00 +0300 Subject: [PATCH 1262/2493] Display customisation header in disabled state when no customisable mod selected --- .../UserInterface/TestSceneModSelectOverlay.cs | 2 +- osu.Game/Localisation/ModSelectOverlayStrings.cs | 5 +++++ osu.Game/Overlays/Mods/ModCustomisationHeader.cs | 15 +++++++++++++-- osu.Game/Overlays/Mods/ModCustomisationPanel.cs | 8 ++++++++ osu.Game/Overlays/Mods/ModSelectOverlay.cs | 5 +++-- 5 files changed, 30 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs index 9f48b06bb6..21a5e3082b 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs @@ -1003,7 +1003,7 @@ namespace osu.Game.Tests.Visual.UserInterface private void assertCustomisationToggleState(bool disabled, bool active) { - AddUntilStep($"customisation panel is {(disabled ? "" : "not ")}disabled", () => modSelectOverlay.ChildrenOfType().Single().State.Value == (disabled ? Visibility.Hidden : Visibility.Visible)); + AddUntilStep($"customisation panel is {(disabled ? "" : "not ")}disabled", () => modSelectOverlay.ChildrenOfType().Single().Enabled.Value == !disabled); AddAssert($"customisation panel is {(active ? "" : "not ")}active", () => modSelectOverlay.ChildrenOfType().Single().Expanded.Value == active); } diff --git a/osu.Game/Localisation/ModSelectOverlayStrings.cs b/osu.Game/Localisation/ModSelectOverlayStrings.cs index a2e1df42c6..10037d30c3 100644 --- a/osu.Game/Localisation/ModSelectOverlayStrings.cs +++ b/osu.Game/Localisation/ModSelectOverlayStrings.cs @@ -80,6 +80,11 @@ namespace osu.Game.Localisation /// public static LocalisableString CustomisationPanelHeader => new TranslatableString(getKey(@"customisation_panel_header"), @"Customise"); + /// + /// "No mod selected which can be customised." + /// + public static LocalisableString CustomisationPanelDisabledReason => new TranslatableString(getKey(@"customisation_panel_disabled_reason"), @"No mod selected which can be customised."); + private static string getKey(string key) => $@"{prefix}:{key}"; } } diff --git a/osu.Game/Overlays/Mods/ModCustomisationHeader.cs b/osu.Game/Overlays/Mods/ModCustomisationHeader.cs index e6534921f6..2887b53548 100644 --- a/osu.Game/Overlays/Mods/ModCustomisationHeader.cs +++ b/osu.Game/Overlays/Mods/ModCustomisationHeader.cs @@ -30,6 +30,12 @@ namespace osu.Game.Overlays.Mods public readonly BindableBool Expanded = new BindableBool(); + public ModCustomisationHeader() + { + Action = Expanded.Toggle; + Enabled.Value = false; + } + [BackgroundDependencyLoader] private void load() { @@ -75,12 +81,17 @@ namespace osu.Game.Overlays.Mods { base.LoadComplete(); + Enabled.BindValueChanged(e => + { + TooltipText = e.NewValue + ? string.Empty + : ModSelectOverlayStrings.CustomisationPanelDisabledReason; + }, true); + Expanded.BindValueChanged(v => { icon.RotateTo(v.NewValue ? 180 : 0); }, true); - - Action = Expanded.Toggle; } } } diff --git a/osu.Game/Overlays/Mods/ModCustomisationPanel.cs b/osu.Game/Overlays/Mods/ModCustomisationPanel.cs index a5a524e109..240569031c 100644 --- a/osu.Game/Overlays/Mods/ModCustomisationPanel.cs +++ b/osu.Game/Overlays/Mods/ModCustomisationPanel.cs @@ -33,6 +33,8 @@ namespace osu.Game.Overlays.Mods [Resolved] private OverlayColourProvider colourProvider { get; set; } = null!; + public readonly BindableBool Enabled = new BindableBool(); + public readonly BindableBool Expanded = new BindableBool(); public Bindable> SelectedMods { get; } = new Bindable>(Array.Empty()); @@ -55,6 +57,7 @@ namespace osu.Game.Overlays.Mods Depth = float.MinValue, RelativeSizeAxes = Axes.X, Height = header_height, + Enabled = { BindTarget = Enabled }, Expanded = { BindTarget = Expanded }, }, content = new FocusGrabbingContainer @@ -107,6 +110,11 @@ namespace osu.Game.Overlays.Mods { base.LoadComplete(); + Enabled.BindValueChanged(e => + { + this.FadeColour(OsuColour.Gray(e.NewValue ? 1f : 0.6f), 300, Easing.OutQuint); + }, true); + Expanded.BindValueChanged(_ => updateDisplay(), true); SelectedMods.BindValueChanged(_ => updateMods(), true); diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index b0d58480db..d5a4d27237 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -219,6 +219,7 @@ namespace osu.Game.Overlays.Mods Anchor = Anchor.TopRight, Origin = Anchor.TopRight, Width = 400, + State = { Value = Visibility.Visible }, } } } @@ -493,7 +494,7 @@ namespace osu.Game.Overlays.Mods if (anyCustomisableModActive) { - customisationPanel.Show(); + customisationPanel.Enabled.Value = true; if (anyModPendingConfiguration) customisationPanel.Expanded.Value = true; @@ -501,7 +502,7 @@ namespace osu.Game.Overlays.Mods else { customisationPanel.Expanded.Value = false; - customisationPanel.Hide(); + customisationPanel.Enabled.Value = false; } } From 1e4db77925ddcd7dadd899fc6c0db98058276569 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 2 Jul 2024 13:32:33 +0200 Subject: [PATCH 1263/2493] Implement autoplay toggle for editor test play Contains some hacks to fix weird behaviours like rewinding to the start on enabling autoplay, or gameplay cursor hiding. --- .../Input/Bindings/GlobalActionContainer.cs | 4 ++ .../GlobalActionKeyBindingStrings.cs | 5 ++ .../Screens/Edit/GameplayTest/EditorPlayer.cs | 47 ++++++++++++++++++- 3 files changed, 55 insertions(+), 1 deletion(-) diff --git a/osu.Game/Input/Bindings/GlobalActionContainer.cs b/osu.Game/Input/Bindings/GlobalActionContainer.cs index 2452852f6f..6e2d0eb25e 100644 --- a/osu.Game/Input/Bindings/GlobalActionContainer.cs +++ b/osu.Game/Input/Bindings/GlobalActionContainer.cs @@ -143,6 +143,7 @@ namespace osu.Game.Input.Bindings new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.MouseWheelLeft }, GlobalAction.EditorCycleNextBeatSnapDivisor), new KeyBinding(new[] { InputKey.Control, InputKey.R }, GlobalAction.EditorToggleRotateControl), new KeyBinding(new[] { InputKey.Control, InputKey.E }, GlobalAction.EditorToggleScaleControl), + new KeyBinding(new[] { InputKey.Tab }, GlobalAction.EditorTestPlayToggleAutoplay), }; private static IEnumerable inGameKeyBindings => new[] @@ -432,6 +433,9 @@ namespace osu.Game.Input.Bindings [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorToggleScaleControl))] EditorToggleScaleControl, + + [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorTestPlayToggleAutoplay))] + EditorTestPlayToggleAutoplay, } public enum GlobalActionCategory diff --git a/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs b/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs index 2e44b96625..735d82d9f5 100644 --- a/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs +++ b/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs @@ -374,6 +374,11 @@ namespace osu.Game.Localisation /// public static LocalisableString EditorToggleScaleControl => new TranslatableString(getKey(@"editor_toggle_scale_control"), @"Toggle scale control"); + /// + /// "Test play: Toggle autoplay" + /// + public static LocalisableString EditorTestPlayToggleAutoplay => new TranslatableString(getKey(@"editor_test_play_toggle_autoplay"), @"Test play: Toggle autoplay"); + /// /// "Increase mod speed" /// diff --git a/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs b/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs index 69851d0d35..e70b0419ca 100644 --- a/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs +++ b/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs @@ -4,17 +4,21 @@ using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; +using osu.Framework.Input.Bindings; +using osu.Framework.Input.Events; using osu.Framework.Screens; using osu.Game.Beatmaps; +using osu.Game.Input.Bindings; using osu.Game.Overlays; using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; using osu.Game.Screens.Play; using osu.Game.Users; namespace osu.Game.Screens.Edit.GameplayTest { - public partial class EditorPlayer : Player + public partial class EditorPlayer : Player, IKeyBindingHandler { private readonly Editor editor; private readonly EditorState editorState; @@ -133,6 +137,47 @@ namespace osu.Game.Screens.Edit.GameplayTest protected override bool CheckModsAllowFailure() => false; // never fail. + public bool OnPressed(KeyBindingPressEvent e) + { + if (e.Repeat) + return false; + + switch (e.Action) + { + case GlobalAction.EditorTestPlayToggleAutoplay: + toggleAutoplay(); + return true; + + default: + return false; + } + } + + public void OnReleased(KeyBindingReleaseEvent e) + { + } + + private void toggleAutoplay() + { + if (DrawableRuleset.ReplayScore == null) + { + var autoplay = Ruleset.Value.CreateInstance().GetAutoplayMod(); + if (autoplay == null) + return; + + var score = autoplay.CreateScoreFromReplayData(GameplayState.Beatmap, [autoplay]); + + // remove past frames to prevent replay frame handler from seeking back to start in an attempt to play back the entirety of the replay. + score.Replay.Frames.RemoveAll(f => f.Time <= GameplayClockContainer.CurrentTime); + + DrawableRuleset.SetReplayScore(score); + // Without this schedule, the `GlobalCursorDisplay.Update()` machinery will fade the gameplay cursor out, but we still want it to show. + Schedule(() => DrawableRuleset.Cursor?.Show()); + } + else + DrawableRuleset.SetReplayScore(null); + } + public override void OnEntering(ScreenTransitionEvent e) { base.OnEntering(e); From e28befb98def4411de3fe3e4d0fee09523b97571 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 2 Jul 2024 14:00:18 +0200 Subject: [PATCH 1264/2493] Implement quick pause toggle for editor test play --- osu.Game/Input/Bindings/GlobalActionContainer.cs | 6 +++++- .../Localisation/GlobalActionKeyBindingStrings.cs | 5 +++++ osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs | 12 ++++++++++++ 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/osu.Game/Input/Bindings/GlobalActionContainer.cs b/osu.Game/Input/Bindings/GlobalActionContainer.cs index 6e2d0eb25e..c8afacde67 100644 --- a/osu.Game/Input/Bindings/GlobalActionContainer.cs +++ b/osu.Game/Input/Bindings/GlobalActionContainer.cs @@ -100,7 +100,6 @@ namespace osu.Game.Input.Bindings new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.F }, GlobalAction.ToggleFPSDisplay), new KeyBinding(new[] { InputKey.Control, InputKey.T }, GlobalAction.ToggleToolbar), new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.S }, GlobalAction.ToggleSkinEditor), - new KeyBinding(new[] { InputKey.Control, InputKey.P }, GlobalAction.ToggleProfile), new KeyBinding(new[] { InputKey.Control, InputKey.Alt, InputKey.R }, GlobalAction.ResetInputSettings), @@ -118,6 +117,7 @@ namespace osu.Game.Input.Bindings new KeyBinding(new[] { InputKey.Control, InputKey.B }, GlobalAction.ToggleBeatmapListing), new KeyBinding(new[] { InputKey.Control, InputKey.O }, GlobalAction.ToggleSettings), new KeyBinding(new[] { InputKey.Control, InputKey.N }, GlobalAction.ToggleNotifications), + new KeyBinding(new[] { InputKey.Control, InputKey.P }, GlobalAction.ToggleProfile), }; private static IEnumerable editorKeyBindings => new[] @@ -144,6 +144,7 @@ namespace osu.Game.Input.Bindings new KeyBinding(new[] { InputKey.Control, InputKey.R }, GlobalAction.EditorToggleRotateControl), new KeyBinding(new[] { InputKey.Control, InputKey.E }, GlobalAction.EditorToggleScaleControl), new KeyBinding(new[] { InputKey.Tab }, GlobalAction.EditorTestPlayToggleAutoplay), + new KeyBinding(new[] { InputKey.Control, InputKey.P }, GlobalAction.EditorTestPlayToggleQuickPause), }; private static IEnumerable inGameKeyBindings => new[] @@ -436,6 +437,9 @@ namespace osu.Game.Input.Bindings [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorTestPlayToggleAutoplay))] EditorTestPlayToggleAutoplay, + + [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorTestPlayToggleQuickPause))] + EditorTestPlayToggleQuickPause, } public enum GlobalActionCategory diff --git a/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs b/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs index 735d82d9f5..c039c160d0 100644 --- a/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs +++ b/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs @@ -379,6 +379,11 @@ namespace osu.Game.Localisation /// public static LocalisableString EditorTestPlayToggleAutoplay => new TranslatableString(getKey(@"editor_test_play_toggle_autoplay"), @"Test play: Toggle autoplay"); + /// + /// "Test play: Toggle quick pause" + /// + public static LocalisableString EditorTestPlayToggleQuickPause => new TranslatableString(getKey(@"editor_test_play_toggle_quick_pause"), @"Test play: Toggle quick pause"); + /// /// "Increase mod speed" /// diff --git a/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs b/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs index e70b0419ca..7bcc3e01dd 100644 --- a/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs +++ b/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs @@ -148,6 +148,10 @@ namespace osu.Game.Screens.Edit.GameplayTest toggleAutoplay(); return true; + case GlobalAction.EditorTestPlayToggleQuickPause: + toggleQuickPause(); + return true; + default: return false; } @@ -178,6 +182,14 @@ namespace osu.Game.Screens.Edit.GameplayTest DrawableRuleset.SetReplayScore(null); } + private void toggleQuickPause() + { + if (GameplayClockContainer.IsPaused.Value) + GameplayClockContainer.Start(); + else + GameplayClockContainer.Stop(); + } + public override void OnEntering(ScreenTransitionEvent e) { base.OnEntering(e); From d85c467856b53fe7f7d66ef1d0fd9914ea92052a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 2 Jul 2024 14:12:13 +0200 Subject: [PATCH 1265/2493] Implement quick exit hotkeys for editor test play --- .../Input/Bindings/GlobalActionContainer.cs | 19 ++++++++++++++++++- .../GlobalActionKeyBindingStrings.cs | 18 ++++++++++++++---- osu.Game/Localisation/InputSettingsStrings.cs | 5 +++++ .../Input/GlobalKeyBindingsSection.cs | 1 + .../Screens/Edit/GameplayTest/EditorPlayer.cs | 17 +++++++++++++++++ 5 files changed, 55 insertions(+), 5 deletions(-) diff --git a/osu.Game/Input/Bindings/GlobalActionContainer.cs b/osu.Game/Input/Bindings/GlobalActionContainer.cs index c8afacde67..ef0c60cd20 100644 --- a/osu.Game/Input/Bindings/GlobalActionContainer.cs +++ b/osu.Game/Input/Bindings/GlobalActionContainer.cs @@ -34,6 +34,7 @@ namespace osu.Game.Input.Bindings /// public override IEnumerable DefaultKeyBindings => globalKeyBindings .Concat(editorKeyBindings) + .Concat(editorTestPlayKeyBindings) .Concat(inGameKeyBindings) .Concat(replayKeyBindings) .Concat(songSelectKeyBindings) @@ -68,6 +69,9 @@ namespace osu.Game.Input.Bindings case GlobalActionCategory.Overlays: return overlayKeyBindings; + case GlobalActionCategory.EditorTestPlay: + return editorTestPlayKeyBindings; + default: throw new ArgumentOutOfRangeException(nameof(category), category, $"Unexpected {nameof(GlobalActionCategory)}"); } @@ -143,8 +147,14 @@ namespace osu.Game.Input.Bindings new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.MouseWheelLeft }, GlobalAction.EditorCycleNextBeatSnapDivisor), new KeyBinding(new[] { InputKey.Control, InputKey.R }, GlobalAction.EditorToggleRotateControl), new KeyBinding(new[] { InputKey.Control, InputKey.E }, GlobalAction.EditorToggleScaleControl), + }; + + private static IEnumerable editorTestPlayKeyBindings => new[] + { new KeyBinding(new[] { InputKey.Tab }, GlobalAction.EditorTestPlayToggleAutoplay), new KeyBinding(new[] { InputKey.Control, InputKey.P }, GlobalAction.EditorTestPlayToggleQuickPause), + new KeyBinding(new[] { InputKey.F1 }, GlobalAction.EditorTestPlayQuickExitToInitialTime), + new KeyBinding(new[] { InputKey.F2 }, GlobalAction.EditorTestPlayQuickExitToCurrentTime), }; private static IEnumerable inGameKeyBindings => new[] @@ -440,6 +450,12 @@ namespace osu.Game.Input.Bindings [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorTestPlayToggleQuickPause))] EditorTestPlayToggleQuickPause, + + [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorTestPlayQuickExitToInitialTime))] + EditorTestPlayQuickExitToInitialTime, + + [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorTestPlayQuickExitToCurrentTime))] + EditorTestPlayQuickExitToCurrentTime, } public enum GlobalActionCategory @@ -450,6 +466,7 @@ namespace osu.Game.Input.Bindings Replay, SongSelect, AudioControl, - Overlays + Overlays, + EditorTestPlay, } } diff --git a/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs b/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs index c039c160d0..450585f79a 100644 --- a/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs +++ b/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs @@ -375,14 +375,24 @@ namespace osu.Game.Localisation public static LocalisableString EditorToggleScaleControl => new TranslatableString(getKey(@"editor_toggle_scale_control"), @"Toggle scale control"); /// - /// "Test play: Toggle autoplay" + /// "Toggle autoplay" /// - public static LocalisableString EditorTestPlayToggleAutoplay => new TranslatableString(getKey(@"editor_test_play_toggle_autoplay"), @"Test play: Toggle autoplay"); + public static LocalisableString EditorTestPlayToggleAutoplay => new TranslatableString(getKey(@"editor_test_play_toggle_autoplay"), @"Toggle autoplay"); /// - /// "Test play: Toggle quick pause" + /// "Toggle quick pause" /// - public static LocalisableString EditorTestPlayToggleQuickPause => new TranslatableString(getKey(@"editor_test_play_toggle_quick_pause"), @"Test play: Toggle quick pause"); + public static LocalisableString EditorTestPlayToggleQuickPause => new TranslatableString(getKey(@"editor_test_play_toggle_quick_pause"), @"Toggle quick pause"); + + /// + /// "Quick exit to initial time" + /// + public static LocalisableString EditorTestPlayQuickExitToInitialTime => new TranslatableString(getKey(@"editor_test_play_quick_exit_to_initial_time"), @"Quick exit to initial time"); + + /// + /// "Quick exit to current time" + /// + public static LocalisableString EditorTestPlayQuickExitToCurrentTime => new TranslatableString(getKey(@"editor_test_play_quick_exit_to_current_time"), @"Quick exit to current time"); /// /// "Increase mod speed" diff --git a/osu.Game/Localisation/InputSettingsStrings.cs b/osu.Game/Localisation/InputSettingsStrings.cs index fcfe48bedb..bc1a7e68ab 100644 --- a/osu.Game/Localisation/InputSettingsStrings.cs +++ b/osu.Game/Localisation/InputSettingsStrings.cs @@ -49,6 +49,11 @@ namespace osu.Game.Localisation /// public static LocalisableString EditorSection => new TranslatableString(getKey(@"editor_section"), @"Editor"); + /// + /// "Editor: Test play" + /// + public static LocalisableString EditorTestPlaySection => new TranslatableString(getKey(@"editor_test_play_section"), @"Editor: Test play"); + /// /// "Reset all bindings in section" /// diff --git a/osu.Game/Overlays/Settings/Sections/Input/GlobalKeyBindingsSection.cs b/osu.Game/Overlays/Settings/Sections/Input/GlobalKeyBindingsSection.cs index 5a05d78905..e5bc6cbe8a 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/GlobalKeyBindingsSection.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/GlobalKeyBindingsSection.cs @@ -31,6 +31,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input new GlobalKeyBindingsSubsection(InputSettingsStrings.InGameSection, GlobalActionCategory.InGame), new GlobalKeyBindingsSubsection(InputSettingsStrings.ReplaySection, GlobalActionCategory.Replay), new GlobalKeyBindingsSubsection(InputSettingsStrings.EditorSection, GlobalActionCategory.Editor), + new GlobalKeyBindingsSubsection(InputSettingsStrings.EditorTestPlaySection, GlobalActionCategory.EditorTestPlay), }); } } diff --git a/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs b/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs index 7bcc3e01dd..616d7a09b2 100644 --- a/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs +++ b/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs @@ -152,6 +152,14 @@ namespace osu.Game.Screens.Edit.GameplayTest toggleQuickPause(); return true; + case GlobalAction.EditorTestPlayQuickExitToInitialTime: + quickExit(false); + return true; + + case GlobalAction.EditorTestPlayQuickExitToCurrentTime: + quickExit(true); + return true; + default: return false; } @@ -190,6 +198,15 @@ namespace osu.Game.Screens.Edit.GameplayTest GameplayClockContainer.Stop(); } + private void quickExit(bool useCurrentTime) + { + if (useCurrentTime) + editorState.Time = GameplayClockContainer.CurrentTime; + + editor.RestoreState(editorState); + this.Exit(); + } + public override void OnEntering(ScreenTransitionEvent e) { base.OnEntering(e); From 3d61a217ecee2dfb7b90b29f7772aadb1dcc09d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 2 Jul 2024 14:21:05 +0200 Subject: [PATCH 1266/2493] Add test coverage --- .../Editing/TestSceneEditorTestGameplay.cs | 111 ++++++++++++++++++ 1 file changed, 111 insertions(+) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorTestGameplay.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorTestGameplay.cs index 7dcb8766dd..23efb40d3f 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorTestGameplay.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorTestGameplay.cs @@ -16,6 +16,7 @@ using osu.Game.Configuration; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.UI; using osu.Game.Screens.Backgrounds; using osu.Game.Screens.Edit; using osu.Game.Screens.Edit.Components.Timelines.Summary; @@ -224,6 +225,116 @@ namespace osu.Game.Tests.Visual.Editing AddAssert("time reverted to 00:01:00", () => EditorClock.CurrentTime, () => Is.EqualTo(60_000)); } + [Test] + public void TestAutoplayToggle() + { + AddStep("click test gameplay button", () => + { + var button = Editor.ChildrenOfType().Single(); + + InputManager.MoveMouseTo(button); + InputManager.Click(MouseButton.Left); + }); + + EditorPlayer editorPlayer = null; + AddUntilStep("player pushed", () => (editorPlayer = Stack.CurrentScreen as EditorPlayer) != null); + AddUntilStep("no replay active", () => editorPlayer.ChildrenOfType().Single().ReplayScore, () => Is.Null); + AddStep("press Tab", () => InputManager.Key(Key.Tab)); + AddUntilStep("replay active", () => editorPlayer.ChildrenOfType().Single().ReplayScore, () => Is.Not.Null); + AddStep("press Tab", () => InputManager.Key(Key.Tab)); + AddUntilStep("no replay active", () => editorPlayer.ChildrenOfType().Single().ReplayScore, () => Is.Null); + AddStep("exit player", () => editorPlayer.Exit()); + AddUntilStep("current screen is editor", () => Stack.CurrentScreen is Editor); + } + + [Test] + public void TestQuickPause() + { + AddStep("click test gameplay button", () => + { + var button = Editor.ChildrenOfType().Single(); + + InputManager.MoveMouseTo(button); + InputManager.Click(MouseButton.Left); + }); + + EditorPlayer editorPlayer = null; + AddUntilStep("player pushed", () => (editorPlayer = Stack.CurrentScreen as EditorPlayer) != null); + AddUntilStep("clock running", () => editorPlayer.ChildrenOfType().Single().IsPaused.Value, () => Is.False); + AddStep("press Ctrl-P", () => + { + InputManager.PressKey(Key.ControlLeft); + InputManager.Key(Key.P); + InputManager.ReleaseKey(Key.ControlLeft); + }); + AddUntilStep("clock not running", () => editorPlayer.ChildrenOfType().Single().IsPaused.Value, () => Is.True); + AddStep("press Ctrl-P", () => + { + InputManager.PressKey(Key.ControlLeft); + InputManager.Key(Key.P); + InputManager.ReleaseKey(Key.ControlLeft); + }); + AddUntilStep("clock running", () => editorPlayer.ChildrenOfType().Single().IsPaused.Value, () => Is.False); + AddStep("exit player", () => editorPlayer.Exit()); + AddUntilStep("current screen is editor", () => Stack.CurrentScreen is Editor); + } + + [Test] + public void TestQuickExitAtInitialPosition() + { + AddStep("seek to 00:01:00", () => EditorClock.Seek(60_000)); + AddStep("click test gameplay button", () => + { + var button = Editor.ChildrenOfType().Single(); + + InputManager.MoveMouseTo(button); + InputManager.Click(MouseButton.Left); + }); + + EditorPlayer editorPlayer = null; + AddUntilStep("player pushed", () => (editorPlayer = Stack.CurrentScreen as EditorPlayer) != null); + + GameplayClockContainer gameplayClockContainer = null; + AddStep("fetch gameplay clock", () => gameplayClockContainer = editorPlayer.ChildrenOfType().First()); + AddUntilStep("gameplay clock running", () => gameplayClockContainer.IsRunning); + // when the gameplay test is entered, the clock is expected to continue from where it was in the main editor... + AddAssert("gameplay time past 00:01:00", () => gameplayClockContainer.CurrentTime >= 60_000); + + AddWaitStep("wait some", 5); + + AddStep("exit player", () => InputManager.PressKey(Key.F1)); + AddUntilStep("current screen is editor", () => Stack.CurrentScreen is Editor); + AddAssert("time reverted to 00:01:00", () => EditorClock.CurrentTime, () => Is.EqualTo(60_000)); + } + + [Test] + public void TestQuickExitAtCurrentPosition() + { + AddStep("seek to 00:01:00", () => EditorClock.Seek(60_000)); + AddStep("click test gameplay button", () => + { + var button = Editor.ChildrenOfType().Single(); + + InputManager.MoveMouseTo(button); + InputManager.Click(MouseButton.Left); + }); + + EditorPlayer editorPlayer = null; + AddUntilStep("player pushed", () => (editorPlayer = Stack.CurrentScreen as EditorPlayer) != null); + + GameplayClockContainer gameplayClockContainer = null; + AddStep("fetch gameplay clock", () => gameplayClockContainer = editorPlayer.ChildrenOfType().First()); + AddUntilStep("gameplay clock running", () => gameplayClockContainer.IsRunning); + // when the gameplay test is entered, the clock is expected to continue from where it was in the main editor... + AddAssert("gameplay time past 00:01:00", () => gameplayClockContainer.CurrentTime >= 60_000); + + AddWaitStep("wait some", 5); + + AddStep("exit player", () => InputManager.PressKey(Key.F2)); + AddUntilStep("current screen is editor", () => Stack.CurrentScreen is Editor); + AddAssert("time moved forward", () => EditorClock.CurrentTime, () => Is.GreaterThan(60_000)); + } + public override void TearDownSteps() { base.TearDownSteps(); From 9414aec8bfe19a72b52018e4de3418893a8a3335 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 2 Jul 2024 14:55:04 +0200 Subject: [PATCH 1267/2493] Add capability to remove breaks via context menu --- .../Components/Timeline/TimelineBreak.cs | 37 ++++++++++++++++++- .../Timeline/TimelineBreakDisplay.cs | 5 ++- 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBreak.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBreak.cs index 025eb8bede..7f64436267 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBreak.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBreak.cs @@ -9,20 +9,31 @@ using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; using osu.Game.Beatmaps.Timing; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osu.Game.Resources.Localisation.Web; using osu.Game.Rulesets.Objects; using osuTK; namespace osu.Game.Screens.Edit.Compose.Components.Timeline { - public partial class TimelineBreak : CompositeDrawable + public partial class TimelineBreak : CompositeDrawable, IHasContextMenu { public Bindable Break { get; } = new Bindable(); + public Action? OnDeleted { get; init; } + + private Box background = null!; + + [Resolved] + private OsuColour colours { get; set; } = null!; + public TimelineBreak(BreakPeriod b) { Break.Value = b; @@ -42,7 +53,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline { RelativeSizeAxes = Axes.Both, Padding = new MarginPadding { Horizontal = 5 }, - Child = new Box + Child = background = new Box { RelativeSizeAxes = Axes.Both, Colour = colours.Gray5, @@ -77,6 +88,28 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline }, true); } + protected override bool OnHover(HoverEvent e) + { + updateState(); + return true; + } + + protected override void OnHoverLost(HoverLostEvent e) + { + updateState(); + base.OnHoverLost(e); + } + + private void updateState() + { + background.FadeColour(IsHovered ? colours.Gray6 : colours.Gray5, 400, Easing.OutQuint); + } + + public MenuItem[]? ContextMenuItems => new MenuItem[] + { + new OsuMenuItem(CommonStrings.ButtonsDelete, MenuItemType.Destructive, () => OnDeleted?.Invoke(Break.Value)), + }; + private partial class DragHandle : FillFlowContainer { public Bindable Break { get; } = new Bindable(); diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBreakDisplay.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBreakDisplay.cs index eaa31aea1e..d0f3a831f2 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBreakDisplay.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBreakDisplay.cs @@ -71,7 +71,10 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline if (!shouldBeVisible(breakPeriod)) continue; - Add(new TimelineBreak(breakPeriod)); + Add(new TimelineBreak(breakPeriod) + { + OnDeleted = b => breaks.Remove(b), + }); } } From 3f08605277a5b9cf0ddf14e213f2df4b6c93c124 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 2 Jul 2024 15:02:09 +0200 Subject: [PATCH 1268/2493] Add test coverage --- .../Editing/TestSceneTimelineSelection.cs | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneTimelineSelection.cs b/osu.Game.Tests/Visual/Editing/TestSceneTimelineSelection.cs index 9e147f5ff1..c6d284fae6 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneTimelineSelection.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneTimelineSelection.cs @@ -6,6 +6,7 @@ using System; using System.Collections.Generic; using System.Linq; +using Humanizer; using NUnit.Framework; using osu.Framework.Graphics.UserInterface; using osu.Framework.Testing; @@ -403,6 +404,28 @@ namespace osu.Game.Tests.Visual.Editing AddAssert("placement committed", () => EditorBeatmap.HitObjects, () => Has.Count.EqualTo(2)); } + [Test] + public void TestBreakRemoval() + { + var addedObjects = new[] + { + new HitCircle { StartTime = 0 }, + new HitCircle { StartTime = 5000 }, + }; + + AddStep("add hitobjects", () => EditorBeatmap.AddRange(addedObjects)); + AddAssert("beatmap has one break", () => EditorBeatmap.Breaks, () => Has.Count.EqualTo(1)); + + AddStep("move mouse to break", () => InputManager.MoveMouseTo(this.ChildrenOfType().Single())); + AddStep("right click", () => InputManager.Click(MouseButton.Right)); + + AddStep("move mouse to delete menu item", () => InputManager.MoveMouseTo(this.ChildrenOfType().First().ChildrenOfType().First())); + AddStep("click", () => InputManager.Click(MouseButton.Left)); + + AddAssert("beatmap has no breaks", () => EditorBeatmap.Breaks, () => Is.Empty); + AddAssert("break piece went away", () => this.ChildrenOfType().Count(), () => Is.Zero); + } + private void assertSelectionIs(IEnumerable hitObjects) => AddAssert("correct hitobjects selected", () => EditorBeatmap.SelectedHitObjects.OrderBy(h => h.StartTime).SequenceEqual(hitObjects)); } From 6453522b34b3e9dd53c78a41a200fc3677a84c3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 2 Jul 2024 16:21:56 +0200 Subject: [PATCH 1269/2493] Add failing test coverage for changing banks/samples not working on node samples --- .../TestSceneHitObjectSampleAdjustments.cs | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneHitObjectSampleAdjustments.cs b/osu.Game.Tests/Visual/Editing/TestSceneHitObjectSampleAdjustments.cs index f02d2a1bb1..9988c1cb59 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneHitObjectSampleAdjustments.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneHitObjectSampleAdjustments.cs @@ -402,6 +402,88 @@ namespace osu.Game.Tests.Visual.Editing void checkPlacementSample(string expected) => AddAssert($"Placement sample is {expected}", () => EditorBeatmap.PlacementObject.Value.Samples.First().Bank, () => Is.EqualTo(expected)); } + [Test] + public void TestHotkeysAffectNodeSamples() + { + AddStep("add slider", () => + { + EditorBeatmap.Add(new Slider + { + Position = new Vector2(256, 256), + StartTime = 1000, + Path = new SliderPath(new[] { new PathControlPoint(Vector2.Zero), new PathControlPoint(new Vector2(250, 0)) }), + Samples = + { + new HitSampleInfo(HitSampleInfo.HIT_NORMAL) + }, + NodeSamples = new List> + { + new List + { + new HitSampleInfo(HitSampleInfo.HIT_NORMAL, bank: HitSampleInfo.BANK_DRUM), + new HitSampleInfo(HitSampleInfo.HIT_CLAP, bank: HitSampleInfo.BANK_DRUM), + }, + new List + { + new HitSampleInfo(HitSampleInfo.HIT_NORMAL, bank: HitSampleInfo.BANK_SOFT), + new HitSampleInfo(HitSampleInfo.HIT_WHISTLE, bank: HitSampleInfo.BANK_SOFT), + }, + } + }); + }); + AddStep("select everything", () => EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects)); + + AddStep("add clap addition", () => InputManager.Key(Key.R)); + + hitObjectHasSampleBank(0, HitSampleInfo.BANK_NORMAL); + hitObjectHasSamples(0, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_CLAP); + + hitObjectHasSampleBank(1, HitSampleInfo.BANK_SOFT); + hitObjectHasSamples(1, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_CLAP); + + hitObjectHasSampleBank(2, HitSampleInfo.BANK_NORMAL); + hitObjectHasSamples(2, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_CLAP); + hitObjectNodeHasSampleBank(2, 0, HitSampleInfo.BANK_DRUM); + hitObjectNodeHasSamples(2, 0, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_CLAP); + hitObjectNodeHasSampleBank(2, 1, HitSampleInfo.BANK_SOFT); + hitObjectNodeHasSamples(2, 1, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_WHISTLE, HitSampleInfo.HIT_CLAP); + + AddStep("remove clap addition", () => InputManager.Key(Key.R)); + + hitObjectHasSampleBank(0, HitSampleInfo.BANK_NORMAL); + hitObjectHasSamples(0, HitSampleInfo.HIT_NORMAL); + + hitObjectHasSampleBank(1, HitSampleInfo.BANK_SOFT); + hitObjectHasSamples(1, HitSampleInfo.HIT_NORMAL); + + hitObjectHasSampleBank(2, HitSampleInfo.BANK_NORMAL); + hitObjectHasSamples(2, HitSampleInfo.HIT_NORMAL); + hitObjectNodeHasSampleBank(2, 0, HitSampleInfo.BANK_DRUM); + hitObjectNodeHasSamples(2, 0, HitSampleInfo.HIT_NORMAL); + hitObjectNodeHasSampleBank(2, 1, HitSampleInfo.BANK_SOFT); + hitObjectNodeHasSamples(2, 1, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_WHISTLE); + + AddStep("set drum bank", () => + { + InputManager.PressKey(Key.LShift); + InputManager.Key(Key.R); + InputManager.ReleaseKey(Key.LShift); + }); + + hitObjectHasSampleBank(0, HitSampleInfo.BANK_DRUM); + hitObjectHasSamples(0, HitSampleInfo.HIT_NORMAL); + + hitObjectHasSampleBank(1, HitSampleInfo.BANK_DRUM); + hitObjectHasSamples(1, HitSampleInfo.HIT_NORMAL); + + hitObjectHasSampleBank(2, HitSampleInfo.BANK_DRUM); + hitObjectHasSamples(2, HitSampleInfo.HIT_NORMAL); + hitObjectNodeHasSampleBank(2, 0, HitSampleInfo.BANK_DRUM); + hitObjectNodeHasSamples(2, 0, HitSampleInfo.HIT_NORMAL); + hitObjectNodeHasSampleBank(2, 1, HitSampleInfo.BANK_DRUM); + hitObjectNodeHasSamples(2, 1, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_WHISTLE); + } + private void clickSamplePiece(int objectIndex) => AddStep($"click {objectIndex.ToOrdinalWords()} sample piece", () => { var samplePiece = this.ChildrenOfType().Single(piece => piece.HitObject == EditorBeatmap.HitObjects.ElementAt(objectIndex)); From a7b066f3ee59b9e9f13344ce3af4c5e7cf511e67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 2 Jul 2024 16:22:15 +0200 Subject: [PATCH 1270/2493] Include node samples when changing additions and banks --- .../Components/EditorSelectionHandler.cs | 86 +++++++++++++++++-- 1 file changed, 78 insertions(+), 8 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/EditorSelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/EditorSelectionHandler.cs index 7c30b73122..70c91b16fd 100644 --- a/osu.Game/Screens/Edit/Compose/Components/EditorSelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/EditorSelectionHandler.cs @@ -117,7 +117,7 @@ namespace osu.Game.Screens.Edit.Compose.Components break; } - AddSampleBank(bankName); + SetSampleBank(bankName); } break; @@ -177,14 +177,27 @@ namespace osu.Game.Screens.Edit.Compose.Components { SelectionNewComboState.Value = GetStateFromSelection(SelectedItems.OfType(), h => h.NewCombo); + var samplesInSelection = SelectedItems.SelectMany(enumerateAllSamples).ToArray(); + foreach ((string sampleName, var bindable) in SelectionSampleStates) { - bindable.Value = GetStateFromSelection(SelectedItems, h => h.Samples.Any(s => s.Name == sampleName)); + bindable.Value = GetStateFromSelection(samplesInSelection, h => h.Any(s => s.Name == sampleName)); } foreach ((string bankName, var bindable) in SelectionBankStates) { - bindable.Value = GetStateFromSelection(SelectedItems, h => h.Samples.All(s => s.Bank == bankName)); + bindable.Value = GetStateFromSelection(samplesInSelection, h => h.Any(s => s.Bank == bankName)); + } + + IEnumerable> enumerateAllSamples(HitObject hitObject) + { + yield return hitObject.Samples; + + if (hitObject is IHasRepeats withRepeats) + { + foreach (var node in withRepeats.NodeSamples) + yield return node; + } } } @@ -193,12 +206,25 @@ namespace osu.Game.Screens.Edit.Compose.Components #region Ternary state changes /// - /// Adds a sample bank to all selected s. + /// Sets the sample bank for all selected s. /// /// The name of the sample bank. - public void AddSampleBank(string bankName) + public void SetSampleBank(string bankName) { - if (SelectedItems.All(h => h.Samples.All(s => s.Bank == bankName))) + bool hasRelevantBank(HitObject hitObject) + { + bool result = hitObject.Samples.All(s => s.Bank == bankName); + + if (hitObject is IHasRepeats hasRepeats) + { + foreach (var node in hasRepeats.NodeSamples) + result &= node.All(s => s.Bank == bankName); + } + + return result; + } + + if (SelectedItems.All(hasRelevantBank)) return; EditorBeatmap.PerformOnSelection(h => @@ -207,17 +233,37 @@ namespace osu.Game.Screens.Edit.Compose.Components return; h.Samples = h.Samples.Select(s => s.With(newBank: bankName)).ToList(); + + if (h is IHasRepeats hasRepeats) + { + for (int i = 0; i < hasRepeats.NodeSamples.Count; ++i) + hasRepeats.NodeSamples[i] = hasRepeats.NodeSamples[i].Select(s => s.With(newBank: bankName)).ToList(); + } + EditorBeatmap.Update(h); }); } + private bool hasRelevantSample(HitObject hitObject, string sampleName) + { + bool result = hitObject.Samples.Any(s => s.Name == sampleName); + + if (hitObject is IHasRepeats hasRepeats) + { + foreach (var node in hasRepeats.NodeSamples) + result &= node.Any(s => s.Name == sampleName); + } + + return result; + } + /// /// Adds a hit sample to all selected s. /// /// The name of the hit sample. public void AddHitSample(string sampleName) { - if (SelectedItems.All(h => h.Samples.Any(s => s.Name == sampleName))) + if (SelectedItems.All(h => hasRelevantSample(h, sampleName))) return; EditorBeatmap.PerformOnSelection(h => @@ -228,6 +274,23 @@ namespace osu.Game.Screens.Edit.Compose.Components h.Samples.Add(h.CreateHitSampleInfo(sampleName)); + if (h is IHasRepeats hasRepeats) + { + foreach (var node in hasRepeats.NodeSamples) + { + if (node.Any(s => s.Name == sampleName)) + continue; + + var hitSample = h.CreateHitSampleInfo(sampleName); + + string? existingAdditionBank = node.FirstOrDefault(s => s.Name != HitSampleInfo.HIT_NORMAL)?.Bank; + if (existingAdditionBank != null) + hitSample = hitSample.With(newBank: existingAdditionBank); + + node.Add(hitSample); + } + } + EditorBeatmap.Update(h); }); } @@ -238,12 +301,19 @@ namespace osu.Game.Screens.Edit.Compose.Components /// The name of the hit sample. public void RemoveHitSample(string sampleName) { - if (SelectedItems.All(h => h.Samples.All(s => s.Name != sampleName))) + if (SelectedItems.All(h => !hasRelevantSample(h, sampleName))) return; EditorBeatmap.PerformOnSelection(h => { h.SamplesBindable.RemoveAll(s => s.Name == sampleName); + + if (h is IHasRepeats hasRepeats) + { + for (int i = 0; i < hasRepeats.NodeSamples.Count; ++i) + hasRepeats.NodeSamples[i] = hasRepeats.NodeSamples[i].Where(s => s.Name != sampleName).ToList(); + } + EditorBeatmap.Update(h); }); } From 9034f6186bb332424609ff3097f7ca04777b6cfb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 3 Jul 2024 00:15:34 +0900 Subject: [PATCH 1271/2493] 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 349829555d..9fd0df3036 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -10,7 +10,7 @@ true - + diff --git a/osu.iOS.props b/osu.iOS.props index 8e8bac53b9..48d9c2564a 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -23,6 +23,6 @@ iossimulator-x64 - + From 53509453405f8256fd66b05dabf84c65c5f2b0b2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 3 Jul 2024 00:19:04 +0900 Subject: [PATCH 1272/2493] Update `HasFlag` usages --- CodeAnalysis/BannedSymbols.txt | 1 - osu.Game.Rulesets.Catch/CatchRuleset.cs | 29 +++++----- .../Edit/CatchHitObjectComposer.cs | 3 +- .../TestSceneNotes.cs | 3 +- .../Legacy/HitObjectPatternGenerator.cs | 33 ++++++------ .../Legacy/PathObjectPatternGenerator.cs | 17 +++--- osu.Game.Rulesets.Mania/ManiaRuleset.cs | 53 +++++++++---------- .../Edit/OsuHitObjectComposer.cs | 9 ++-- osu.Game.Rulesets.Osu/OsuRuleset.cs | 37 +++++++------ .../UI/Cursor/CursorTrail.cs | 9 ++-- osu.Game.Rulesets.Taiko/TaikoRuleset.cs | 29 +++++----- .../Beatmaps/Formats/LegacyBeatmapDecoder.cs | 5 +- osu.Game/Database/LegacyImportManager.cs | 9 ++-- osu.Game/Database/RealmAccess.cs | 3 +- osu.Game/Graphics/ParticleSpewer.cs | 5 +- .../Graphics/UserInterface/TwoLayerButton.cs | 11 ++-- osu.Game/Overlays/Music/PlaylistOverlay.cs | 3 +- osu.Game/Overlays/SettingsToolboxGroup.cs | 3 +- .../SkinEditor/SkinSelectionHandler.cs | 5 +- .../SkinEditor/SkinSelectionScaleHandler.cs | 3 +- osu.Game/Overlays/Toolbar/ToolbarButton.cs | 5 +- osu.Game/Replays/Legacy/LegacyReplayFrame.cs | 11 ++-- osu.Game/Rulesets/Edit/HitObjectComposer.cs | 3 +- .../Objects/Legacy/ConvertHitObjectParser.cs | 19 ++++--- .../Edit/Compose/Components/SelectionBox.cs | 5 +- .../SelectionBoxDragHandleContainer.cs | 5 +- .../Components/SelectionBoxRotationHandle.cs | 5 +- .../Components/SelectionBoxScaleHandle.cs | 3 +- osu.Game/Screens/Play/HUDOverlay.cs | 9 ++-- .../HitEventTimingDistributionGraph.cs | 3 +- osu.Game/Screens/Select/BeatmapCarousel.cs | 3 +- .../SerialisableDrawableExtensions.cs | 5 +- osu.Game/Skinning/SerialisedDrawableInfo.cs | 5 +- osu.Game/Storyboards/StoryboardExtensions.cs | 9 ++-- 34 files changed, 163 insertions(+), 197 deletions(-) diff --git a/CodeAnalysis/BannedSymbols.txt b/CodeAnalysis/BannedSymbols.txt index 03fd21829d..3c60b28765 100644 --- a/CodeAnalysis/BannedSymbols.txt +++ b/CodeAnalysis/BannedSymbols.txt @@ -7,7 +7,6 @@ T:SixLabors.ImageSharp.IDeepCloneable`1;Use osu.Game.Utils.IDeepCloneable ins M:osu.Framework.Graphics.Sprites.SpriteText.#ctor;Use OsuSpriteText. M:osu.Framework.Bindables.IBindableList`1.GetBoundCopy();Fails on iOS. Use manual ctor + BindTo instead. (see https://github.com/mono/mono/issues/19900) T:NuGet.Packaging.CollectionExtensions;Don't use internal extension methods. -M:System.Enum.HasFlag(System.Enum);Use osu.Framework.Extensions.EnumExtensions.HasFlagFast() instead. M:Realms.IRealmCollection`1.SubscribeForNotifications`1(Realms.NotificationCallbackDelegate{``0});Use osu.Game.Database.RealmObjectExtensions.QueryAsyncWithNotifications(IRealmCollection,NotificationCallbackDelegate) instead. M:System.Guid.#ctor;Probably meaning to use Guid.NewGuid() instead. If actually wanting empty, use Guid.Empty. M:Realms.CollectionExtensions.SubscribeForNotifications`1(System.Linq.IQueryable{``0},Realms.NotificationCallbackDelegate{``0});Use osu.Game.Database.RealmObjectExtensions.QueryAsyncWithNotifications(IQueryable,NotificationCallbackDelegate) instead. diff --git a/osu.Game.Rulesets.Catch/CatchRuleset.cs b/osu.Game.Rulesets.Catch/CatchRuleset.cs index 72d1a161dd..ad6dedaa8f 100644 --- a/osu.Game.Rulesets.Catch/CatchRuleset.cs +++ b/osu.Game.Rulesets.Catch/CatchRuleset.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Bindings; @@ -62,43 +61,43 @@ namespace osu.Game.Rulesets.Catch public override IEnumerable ConvertFromLegacyMods(LegacyMods mods) { - if (mods.HasFlagFast(LegacyMods.Nightcore)) + if (mods.HasFlag(LegacyMods.Nightcore)) yield return new CatchModNightcore(); - else if (mods.HasFlagFast(LegacyMods.DoubleTime)) + else if (mods.HasFlag(LegacyMods.DoubleTime)) yield return new CatchModDoubleTime(); - if (mods.HasFlagFast(LegacyMods.Perfect)) + if (mods.HasFlag(LegacyMods.Perfect)) yield return new CatchModPerfect(); - else if (mods.HasFlagFast(LegacyMods.SuddenDeath)) + else if (mods.HasFlag(LegacyMods.SuddenDeath)) yield return new CatchModSuddenDeath(); - if (mods.HasFlagFast(LegacyMods.Cinema)) + if (mods.HasFlag(LegacyMods.Cinema)) yield return new CatchModCinema(); - else if (mods.HasFlagFast(LegacyMods.Autoplay)) + else if (mods.HasFlag(LegacyMods.Autoplay)) yield return new CatchModAutoplay(); - if (mods.HasFlagFast(LegacyMods.Easy)) + if (mods.HasFlag(LegacyMods.Easy)) yield return new CatchModEasy(); - if (mods.HasFlagFast(LegacyMods.Flashlight)) + if (mods.HasFlag(LegacyMods.Flashlight)) yield return new CatchModFlashlight(); - if (mods.HasFlagFast(LegacyMods.HalfTime)) + if (mods.HasFlag(LegacyMods.HalfTime)) yield return new CatchModHalfTime(); - if (mods.HasFlagFast(LegacyMods.HardRock)) + if (mods.HasFlag(LegacyMods.HardRock)) yield return new CatchModHardRock(); - if (mods.HasFlagFast(LegacyMods.Hidden)) + if (mods.HasFlag(LegacyMods.Hidden)) yield return new CatchModHidden(); - if (mods.HasFlagFast(LegacyMods.NoFail)) + if (mods.HasFlag(LegacyMods.NoFail)) yield return new CatchModNoFail(); - if (mods.HasFlagFast(LegacyMods.Relax)) + if (mods.HasFlag(LegacyMods.Relax)) yield return new CatchModRelax(); - if (mods.HasFlagFast(LegacyMods.ScoreV2)) + if (mods.HasFlag(LegacyMods.ScoreV2)) yield return new ModScoreV2(); } diff --git a/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs b/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs index 4172720ada..83f48816f9 100644 --- a/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs +++ b/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs @@ -5,7 +5,6 @@ using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Graphics; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; @@ -121,7 +120,7 @@ namespace osu.Game.Rulesets.Catch.Edit result.ScreenSpacePosition.X = screenSpacePosition.X; - if (snapType.HasFlagFast(SnapType.RelativeGrids)) + if (snapType.HasFlag(SnapType.RelativeGrids)) { if (distanceSnapGrid.IsPresent && distanceSnapGrid.GetSnappedPosition(result.ScreenSpacePosition) is SnapResult snapResult && Vector2.Distance(snapResult.ScreenSpacePosition, result.ScreenSpacePosition) < distance_snap_radius) diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneNotes.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneNotes.cs index 31ff57395c..990f545ee4 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneNotes.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneNotes.cs @@ -8,7 +8,6 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; -using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; @@ -100,7 +99,7 @@ namespace osu.Game.Rulesets.Mania.Tests } private bool verifyAnchors(DrawableHitObject hitObject, Anchor expectedAnchor) - => hitObject.Anchor.HasFlagFast(expectedAnchor) && hitObject.Origin.HasFlagFast(expectedAnchor); + => hitObject.Anchor.HasFlag(expectedAnchor) && hitObject.Origin.HasFlag(expectedAnchor); private bool verifyAnchors(DrawableHoldNote holdNote, Anchor expectedAnchor) => verifyAnchors((DrawableHitObject)holdNote, expectedAnchor) && holdNote.NestedHitObjects.All(n => verifyAnchors(n, expectedAnchor)); diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/HitObjectPatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/HitObjectPatternGenerator.cs index ad45a3fb21..9880369dfb 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/HitObjectPatternGenerator.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/HitObjectPatternGenerator.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.Linq; -using osu.Framework.Extensions.EnumExtensions; using osuTK; using osu.Game.Audio; using osu.Game.Beatmaps; @@ -79,7 +78,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy else convertType |= PatternType.LowProbability; - if (!convertType.HasFlagFast(PatternType.KeepSingle)) + if (!convertType.HasFlag(PatternType.KeepSingle)) { if (HitObject.Samples.Any(s => s.Name == HitSampleInfo.HIT_FINISH) && TotalColumns != 8) convertType |= PatternType.Mirror; @@ -102,7 +101,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy int lastColumn = PreviousPattern.HitObjects.FirstOrDefault()?.Column ?? 0; - if (convertType.HasFlagFast(PatternType.Reverse) && PreviousPattern.HitObjects.Any()) + if (convertType.HasFlag(PatternType.Reverse) && PreviousPattern.HitObjects.Any()) { // Generate a new pattern by copying the last hit objects in reverse-column order for (int i = RandomStart; i < TotalColumns; i++) @@ -114,7 +113,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy return pattern; } - if (convertType.HasFlagFast(PatternType.Cycle) && PreviousPattern.HitObjects.Count() == 1 + if (convertType.HasFlag(PatternType.Cycle) && PreviousPattern.HitObjects.Count() == 1 // If we convert to 7K + 1, let's not overload the special key && (TotalColumns != 8 || lastColumn != 0) // Make sure the last column was not the centre column @@ -127,7 +126,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy return pattern; } - if (convertType.HasFlagFast(PatternType.ForceStack) && PreviousPattern.HitObjects.Any()) + if (convertType.HasFlag(PatternType.ForceStack) && PreviousPattern.HitObjects.Any()) { // Generate a new pattern by placing on the already filled columns for (int i = RandomStart; i < TotalColumns; i++) @@ -141,7 +140,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy if (PreviousPattern.HitObjects.Count() == 1) { - if (convertType.HasFlagFast(PatternType.Stair)) + if (convertType.HasFlag(PatternType.Stair)) { // Generate a new pattern by placing on the next column, cycling back to the start if there is no "next" int targetColumn = lastColumn + 1; @@ -152,7 +151,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy return pattern; } - if (convertType.HasFlagFast(PatternType.ReverseStair)) + if (convertType.HasFlag(PatternType.ReverseStair)) { // Generate a new pattern by placing on the previous column, cycling back to the end if there is no "previous" int targetColumn = lastColumn - 1; @@ -164,10 +163,10 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy } } - if (convertType.HasFlagFast(PatternType.KeepSingle)) + if (convertType.HasFlag(PatternType.KeepSingle)) return generateRandomNotes(1); - if (convertType.HasFlagFast(PatternType.Mirror)) + if (convertType.HasFlag(PatternType.Mirror)) { if (ConversionDifficulty > 6.5) return generateRandomPatternWithMirrored(0.12, 0.38, 0.12); @@ -179,7 +178,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy if (ConversionDifficulty > 6.5) { - if (convertType.HasFlagFast(PatternType.LowProbability)) + if (convertType.HasFlag(PatternType.LowProbability)) return generateRandomPattern(0.78, 0.42, 0, 0); return generateRandomPattern(1, 0.62, 0, 0); @@ -187,7 +186,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy if (ConversionDifficulty > 4) { - if (convertType.HasFlagFast(PatternType.LowProbability)) + if (convertType.HasFlag(PatternType.LowProbability)) return generateRandomPattern(0.35, 0.08, 0, 0); return generateRandomPattern(0.52, 0.15, 0, 0); @@ -195,7 +194,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy if (ConversionDifficulty > 2) { - if (convertType.HasFlagFast(PatternType.LowProbability)) + if (convertType.HasFlag(PatternType.LowProbability)) return generateRandomPattern(0.18, 0, 0, 0); return generateRandomPattern(0.45, 0, 0, 0); @@ -208,9 +207,9 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy foreach (var obj in p.HitObjects) { - if (convertType.HasFlagFast(PatternType.Stair) && obj.Column == TotalColumns - 1) + if (convertType.HasFlag(PatternType.Stair) && obj.Column == TotalColumns - 1) StairType = PatternType.ReverseStair; - if (convertType.HasFlagFast(PatternType.ReverseStair) && obj.Column == RandomStart) + if (convertType.HasFlag(PatternType.ReverseStair) && obj.Column == RandomStart) StairType = PatternType.Stair; } @@ -230,7 +229,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy { var pattern = new Pattern(); - bool allowStacking = !convertType.HasFlagFast(PatternType.ForceNotStack); + bool allowStacking = !convertType.HasFlag(PatternType.ForceNotStack); if (!allowStacking) noteCount = Math.Min(noteCount, TotalColumns - RandomStart - PreviousPattern.ColumnWithObjects); @@ -250,7 +249,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy int getNextColumn(int last) { - if (convertType.HasFlagFast(PatternType.Gathered)) + if (convertType.HasFlag(PatternType.Gathered)) { last++; if (last == TotalColumns) @@ -297,7 +296,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy /// The containing the hit objects. private Pattern generateRandomPatternWithMirrored(double centreProbability, double p2, double p3) { - if (convertType.HasFlagFast(PatternType.ForceNotStack)) + if (convertType.HasFlag(PatternType.ForceNotStack)) return generateRandomPattern(1 / 2f + p2 / 2, p2, (p2 + p3) / 2, p3); var pattern = new Pattern(); diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PathObjectPatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PathObjectPatternGenerator.cs index 6d593a75e7..c54da74424 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PathObjectPatternGenerator.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PathObjectPatternGenerator.cs @@ -7,7 +7,6 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; -using osu.Framework.Extensions.EnumExtensions; using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; @@ -139,7 +138,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy if (ConversionDifficulty > 6.5) { - if (convertType.HasFlagFast(PatternType.LowProbability)) + if (convertType.HasFlag(PatternType.LowProbability)) return generateNRandomNotes(StartTime, 0.78, 0.3, 0); return generateNRandomNotes(StartTime, 0.85, 0.36, 0.03); @@ -147,7 +146,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy if (ConversionDifficulty > 4) { - if (convertType.HasFlagFast(PatternType.LowProbability)) + if (convertType.HasFlag(PatternType.LowProbability)) return generateNRandomNotes(StartTime, 0.43, 0.08, 0); return generateNRandomNotes(StartTime, 0.56, 0.18, 0); @@ -155,13 +154,13 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy if (ConversionDifficulty > 2.5) { - if (convertType.HasFlagFast(PatternType.LowProbability)) + if (convertType.HasFlag(PatternType.LowProbability)) return generateNRandomNotes(StartTime, 0.3, 0, 0); return generateNRandomNotes(StartTime, 0.37, 0.08, 0); } - if (convertType.HasFlagFast(PatternType.LowProbability)) + if (convertType.HasFlag(PatternType.LowProbability)) return generateNRandomNotes(StartTime, 0.17, 0, 0); return generateNRandomNotes(StartTime, 0.27, 0, 0); @@ -219,7 +218,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy var pattern = new Pattern(); int nextColumn = GetColumn((HitObject as IHasXPosition)?.X ?? 0, true); - if (convertType.HasFlagFast(PatternType.ForceNotStack) && PreviousPattern.ColumnWithObjects < TotalColumns) + if (convertType.HasFlag(PatternType.ForceNotStack) && PreviousPattern.ColumnWithObjects < TotalColumns) nextColumn = FindAvailableColumn(nextColumn, PreviousPattern); int lastColumn = nextColumn; @@ -371,7 +370,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy static bool isDoubleSample(HitSampleInfo sample) => sample.Name == HitSampleInfo.HIT_CLAP || sample.Name == HitSampleInfo.HIT_FINISH; - bool canGenerateTwoNotes = !convertType.HasFlagFast(PatternType.LowProbability); + bool canGenerateTwoNotes = !convertType.HasFlag(PatternType.LowProbability); canGenerateTwoNotes &= HitObject.Samples.Any(isDoubleSample) || sampleInfoListAt(StartTime).Any(isDoubleSample); if (canGenerateTwoNotes) @@ -404,7 +403,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy int endTime = startTime + SegmentDuration * SpanCount; int nextColumn = GetColumn((HitObject as IHasXPosition)?.X ?? 0, true); - if (convertType.HasFlagFast(PatternType.ForceNotStack) && PreviousPattern.ColumnWithObjects < TotalColumns) + if (convertType.HasFlag(PatternType.ForceNotStack) && PreviousPattern.ColumnWithObjects < TotalColumns) nextColumn = FindAvailableColumn(nextColumn, PreviousPattern); for (int i = 0; i < columnRepeat; i++) @@ -433,7 +432,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy var pattern = new Pattern(); int holdColumn = GetColumn((HitObject as IHasXPosition)?.X ?? 0, true); - if (convertType.HasFlagFast(PatternType.ForceNotStack) && PreviousPattern.ColumnWithObjects < TotalColumns) + if (convertType.HasFlag(PatternType.ForceNotStack) && PreviousPattern.ColumnWithObjects < TotalColumns) holdColumn = FindAvailableColumn(holdColumn, PreviousPattern); // Create the hold note diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs index 667002533d..0dcbb36c77 100644 --- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.Linq; -using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Bindings; @@ -89,79 +88,79 @@ namespace osu.Game.Rulesets.Mania public override IEnumerable ConvertFromLegacyMods(LegacyMods mods) { - if (mods.HasFlagFast(LegacyMods.Nightcore)) + if (mods.HasFlag(LegacyMods.Nightcore)) yield return new ManiaModNightcore(); - else if (mods.HasFlagFast(LegacyMods.DoubleTime)) + else if (mods.HasFlag(LegacyMods.DoubleTime)) yield return new ManiaModDoubleTime(); - if (mods.HasFlagFast(LegacyMods.Perfect)) + if (mods.HasFlag(LegacyMods.Perfect)) yield return new ManiaModPerfect(); - else if (mods.HasFlagFast(LegacyMods.SuddenDeath)) + else if (mods.HasFlag(LegacyMods.SuddenDeath)) yield return new ManiaModSuddenDeath(); - if (mods.HasFlagFast(LegacyMods.Cinema)) + if (mods.HasFlag(LegacyMods.Cinema)) yield return new ManiaModCinema(); - else if (mods.HasFlagFast(LegacyMods.Autoplay)) + else if (mods.HasFlag(LegacyMods.Autoplay)) yield return new ManiaModAutoplay(); - if (mods.HasFlagFast(LegacyMods.Easy)) + if (mods.HasFlag(LegacyMods.Easy)) yield return new ManiaModEasy(); - if (mods.HasFlagFast(LegacyMods.FadeIn)) + if (mods.HasFlag(LegacyMods.FadeIn)) yield return new ManiaModFadeIn(); - if (mods.HasFlagFast(LegacyMods.Flashlight)) + if (mods.HasFlag(LegacyMods.Flashlight)) yield return new ManiaModFlashlight(); - if (mods.HasFlagFast(LegacyMods.HalfTime)) + if (mods.HasFlag(LegacyMods.HalfTime)) yield return new ManiaModHalfTime(); - if (mods.HasFlagFast(LegacyMods.HardRock)) + if (mods.HasFlag(LegacyMods.HardRock)) yield return new ManiaModHardRock(); - if (mods.HasFlagFast(LegacyMods.Hidden)) + if (mods.HasFlag(LegacyMods.Hidden)) yield return new ManiaModHidden(); - if (mods.HasFlagFast(LegacyMods.Key1)) + if (mods.HasFlag(LegacyMods.Key1)) yield return new ManiaModKey1(); - if (mods.HasFlagFast(LegacyMods.Key2)) + if (mods.HasFlag(LegacyMods.Key2)) yield return new ManiaModKey2(); - if (mods.HasFlagFast(LegacyMods.Key3)) + if (mods.HasFlag(LegacyMods.Key3)) yield return new ManiaModKey3(); - if (mods.HasFlagFast(LegacyMods.Key4)) + if (mods.HasFlag(LegacyMods.Key4)) yield return new ManiaModKey4(); - if (mods.HasFlagFast(LegacyMods.Key5)) + if (mods.HasFlag(LegacyMods.Key5)) yield return new ManiaModKey5(); - if (mods.HasFlagFast(LegacyMods.Key6)) + if (mods.HasFlag(LegacyMods.Key6)) yield return new ManiaModKey6(); - if (mods.HasFlagFast(LegacyMods.Key7)) + if (mods.HasFlag(LegacyMods.Key7)) yield return new ManiaModKey7(); - if (mods.HasFlagFast(LegacyMods.Key8)) + if (mods.HasFlag(LegacyMods.Key8)) yield return new ManiaModKey8(); - if (mods.HasFlagFast(LegacyMods.Key9)) + if (mods.HasFlag(LegacyMods.Key9)) yield return new ManiaModKey9(); - if (mods.HasFlagFast(LegacyMods.KeyCoop)) + if (mods.HasFlag(LegacyMods.KeyCoop)) yield return new ManiaModDualStages(); - if (mods.HasFlagFast(LegacyMods.NoFail)) + if (mods.HasFlag(LegacyMods.NoFail)) yield return new ManiaModNoFail(); - if (mods.HasFlagFast(LegacyMods.Random)) + if (mods.HasFlag(LegacyMods.Random)) yield return new ManiaModRandom(); - if (mods.HasFlagFast(LegacyMods.Mirror)) + if (mods.HasFlag(LegacyMods.Mirror)) yield return new ManiaModMirror(); - if (mods.HasFlagFast(LegacyMods.ScoreV2)) + if (mods.HasFlag(LegacyMods.ScoreV2)) yield return new ModScoreV2(); } diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs index f93874481d..784132ec4c 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs @@ -10,7 +10,6 @@ using System.Text.RegularExpressions; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Caching; -using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; @@ -194,7 +193,7 @@ namespace osu.Game.Rulesets.Osu.Edit public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All) { - if (snapType.HasFlagFast(SnapType.NearbyObjects) && snapToVisibleBlueprints(screenSpacePosition, out var snapResult)) + if (snapType.HasFlag(SnapType.NearbyObjects) && snapToVisibleBlueprints(screenSpacePosition, out var snapResult)) { // In the case of snapping to nearby objects, a time value is not provided. // This matches the stable editor (which also uses current time), but with the introduction of time-snapping distance snap @@ -204,7 +203,7 @@ namespace osu.Game.Rulesets.Osu.Edit // We want to ensure that in this particular case, the time-snapping component of distance snap is still applied. // The easiest way to ensure this is to attempt application of distance snap after a nearby object is found, and copy over // the time value if the proposed positions are roughly the same. - if (snapType.HasFlagFast(SnapType.RelativeGrids) && DistanceSnapProvider.DistanceSnapToggle.Value == TernaryState.True && distanceSnapGrid != null) + if (snapType.HasFlag(SnapType.RelativeGrids) && DistanceSnapProvider.DistanceSnapToggle.Value == TernaryState.True && distanceSnapGrid != null) { (Vector2 distanceSnappedPosition, double distanceSnappedTime) = distanceSnapGrid.GetSnappedPosition(distanceSnapGrid.ToLocalSpace(snapResult.ScreenSpacePosition)); if (Precision.AlmostEquals(distanceSnapGrid.ToScreenSpace(distanceSnappedPosition), snapResult.ScreenSpacePosition, 1)) @@ -216,7 +215,7 @@ namespace osu.Game.Rulesets.Osu.Edit SnapResult result = base.FindSnappedPositionAndTime(screenSpacePosition, snapType); - if (snapType.HasFlagFast(SnapType.RelativeGrids)) + if (snapType.HasFlag(SnapType.RelativeGrids)) { if (DistanceSnapProvider.DistanceSnapToggle.Value == TernaryState.True && distanceSnapGrid != null) { @@ -227,7 +226,7 @@ namespace osu.Game.Rulesets.Osu.Edit } } - if (snapType.HasFlagFast(SnapType.GlobalGrids)) + if (snapType.HasFlag(SnapType.GlobalGrids)) { if (rectangularGridSnapToggle.Value == TernaryState.True) { diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index 6752712be1..73f9be3fdc 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.Linq; -using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Bindings; @@ -70,55 +69,55 @@ namespace osu.Game.Rulesets.Osu public override IEnumerable ConvertFromLegacyMods(LegacyMods mods) { - if (mods.HasFlagFast(LegacyMods.Nightcore)) + if (mods.HasFlag(LegacyMods.Nightcore)) yield return new OsuModNightcore(); - else if (mods.HasFlagFast(LegacyMods.DoubleTime)) + else if (mods.HasFlag(LegacyMods.DoubleTime)) yield return new OsuModDoubleTime(); - if (mods.HasFlagFast(LegacyMods.Perfect)) + if (mods.HasFlag(LegacyMods.Perfect)) yield return new OsuModPerfect(); - else if (mods.HasFlagFast(LegacyMods.SuddenDeath)) + else if (mods.HasFlag(LegacyMods.SuddenDeath)) yield return new OsuModSuddenDeath(); - if (mods.HasFlagFast(LegacyMods.Autopilot)) + if (mods.HasFlag(LegacyMods.Autopilot)) yield return new OsuModAutopilot(); - if (mods.HasFlagFast(LegacyMods.Cinema)) + if (mods.HasFlag(LegacyMods.Cinema)) yield return new OsuModCinema(); - else if (mods.HasFlagFast(LegacyMods.Autoplay)) + else if (mods.HasFlag(LegacyMods.Autoplay)) yield return new OsuModAutoplay(); - if (mods.HasFlagFast(LegacyMods.Easy)) + if (mods.HasFlag(LegacyMods.Easy)) yield return new OsuModEasy(); - if (mods.HasFlagFast(LegacyMods.Flashlight)) + if (mods.HasFlag(LegacyMods.Flashlight)) yield return new OsuModFlashlight(); - if (mods.HasFlagFast(LegacyMods.HalfTime)) + if (mods.HasFlag(LegacyMods.HalfTime)) yield return new OsuModHalfTime(); - if (mods.HasFlagFast(LegacyMods.HardRock)) + if (mods.HasFlag(LegacyMods.HardRock)) yield return new OsuModHardRock(); - if (mods.HasFlagFast(LegacyMods.Hidden)) + if (mods.HasFlag(LegacyMods.Hidden)) yield return new OsuModHidden(); - if (mods.HasFlagFast(LegacyMods.NoFail)) + if (mods.HasFlag(LegacyMods.NoFail)) yield return new OsuModNoFail(); - if (mods.HasFlagFast(LegacyMods.Relax)) + if (mods.HasFlag(LegacyMods.Relax)) yield return new OsuModRelax(); - if (mods.HasFlagFast(LegacyMods.SpunOut)) + if (mods.HasFlag(LegacyMods.SpunOut)) yield return new OsuModSpunOut(); - if (mods.HasFlagFast(LegacyMods.Target)) + if (mods.HasFlag(LegacyMods.Target)) yield return new OsuModTargetPractice(); - if (mods.HasFlagFast(LegacyMods.TouchDevice)) + if (mods.HasFlag(LegacyMods.TouchDevice)) yield return new OsuModTouchDevice(); - if (mods.HasFlagFast(LegacyMods.ScoreV2)) + if (mods.HasFlag(LegacyMods.ScoreV2)) yield return new ModScoreV2(); } diff --git a/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs b/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs index 95a052dadb..30a77db5a1 100644 --- a/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs +++ b/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs @@ -7,7 +7,6 @@ using System; using System.Diagnostics; using System.Runtime.InteropServices; using osu.Framework.Allocation; -using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.Rendering; @@ -243,14 +242,14 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor originPosition = Vector2.Zero; - if (Source.TrailOrigin.HasFlagFast(Anchor.x1)) + if (Source.TrailOrigin.HasFlag(Anchor.x1)) originPosition.X = 0.5f; - else if (Source.TrailOrigin.HasFlagFast(Anchor.x2)) + else if (Source.TrailOrigin.HasFlag(Anchor.x2)) originPosition.X = 1f; - if (Source.TrailOrigin.HasFlagFast(Anchor.y1)) + if (Source.TrailOrigin.HasFlag(Anchor.y1)) originPosition.Y = 0.5f; - else if (Source.TrailOrigin.HasFlagFast(Anchor.y2)) + else if (Source.TrailOrigin.HasFlag(Anchor.y2)) originPosition.Y = 1f; Source.parts.CopyTo(parts, 0); diff --git a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs index 13501c6192..2053a11426 100644 --- a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.Linq; -using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Bindings; @@ -79,43 +78,43 @@ namespace osu.Game.Rulesets.Taiko public override IEnumerable ConvertFromLegacyMods(LegacyMods mods) { - if (mods.HasFlagFast(LegacyMods.Nightcore)) + if (mods.HasFlag(LegacyMods.Nightcore)) yield return new TaikoModNightcore(); - else if (mods.HasFlagFast(LegacyMods.DoubleTime)) + else if (mods.HasFlag(LegacyMods.DoubleTime)) yield return new TaikoModDoubleTime(); - if (mods.HasFlagFast(LegacyMods.Perfect)) + if (mods.HasFlag(LegacyMods.Perfect)) yield return new TaikoModPerfect(); - else if (mods.HasFlagFast(LegacyMods.SuddenDeath)) + else if (mods.HasFlag(LegacyMods.SuddenDeath)) yield return new TaikoModSuddenDeath(); - if (mods.HasFlagFast(LegacyMods.Cinema)) + if (mods.HasFlag(LegacyMods.Cinema)) yield return new TaikoModCinema(); - else if (mods.HasFlagFast(LegacyMods.Autoplay)) + else if (mods.HasFlag(LegacyMods.Autoplay)) yield return new TaikoModAutoplay(); - if (mods.HasFlagFast(LegacyMods.Easy)) + if (mods.HasFlag(LegacyMods.Easy)) yield return new TaikoModEasy(); - if (mods.HasFlagFast(LegacyMods.Flashlight)) + if (mods.HasFlag(LegacyMods.Flashlight)) yield return new TaikoModFlashlight(); - if (mods.HasFlagFast(LegacyMods.HalfTime)) + if (mods.HasFlag(LegacyMods.HalfTime)) yield return new TaikoModHalfTime(); - if (mods.HasFlagFast(LegacyMods.HardRock)) + if (mods.HasFlag(LegacyMods.HardRock)) yield return new TaikoModHardRock(); - if (mods.HasFlagFast(LegacyMods.Hidden)) + if (mods.HasFlag(LegacyMods.Hidden)) yield return new TaikoModHidden(); - if (mods.HasFlagFast(LegacyMods.NoFail)) + if (mods.HasFlag(LegacyMods.NoFail)) yield return new TaikoModNoFail(); - if (mods.HasFlagFast(LegacyMods.Relax)) + if (mods.HasFlag(LegacyMods.Relax)) yield return new TaikoModRelax(); - if (mods.HasFlagFast(LegacyMods.ScoreV2)) + if (mods.HasFlag(LegacyMods.ScoreV2)) yield return new ModScoreV2(); } diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs index c2f4097889..011aca5d9c 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs @@ -6,7 +6,6 @@ using System.Collections.Generic; using System.IO; using System.Linq; using osu.Framework.Extensions; -using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Logging; using osu.Game.Audio; using osu.Game.Beatmaps.ControlPoints; @@ -528,8 +527,8 @@ namespace osu.Game.Beatmaps.Formats if (split.Length >= 8) { LegacyEffectFlags effectFlags = (LegacyEffectFlags)Parsing.ParseInt(split[7]); - kiaiMode = effectFlags.HasFlagFast(LegacyEffectFlags.Kiai); - omitFirstBarSignature = effectFlags.HasFlagFast(LegacyEffectFlags.OmitFirstBarLine); + kiaiMode = effectFlags.HasFlag(LegacyEffectFlags.Kiai); + omitFirstBarSignature = effectFlags.HasFlag(LegacyEffectFlags.OmitFirstBarLine); } string stringSampleSet = sampleSet.ToString().ToLowerInvariant(); diff --git a/osu.Game/Database/LegacyImportManager.cs b/osu.Game/Database/LegacyImportManager.cs index 7e1641d16f..5c2f220045 100644 --- a/osu.Game/Database/LegacyImportManager.cs +++ b/osu.Game/Database/LegacyImportManager.cs @@ -10,7 +10,6 @@ using System.Threading; using System.Threading.Tasks; using osu.Framework; using osu.Framework.Allocation; -using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Graphics; using osu.Framework.Platform; using osu.Game.Beatmaps; @@ -164,13 +163,13 @@ namespace osu.Game.Database var importTasks = new List(); Task beatmapImportTask = Task.CompletedTask; - if (content.HasFlagFast(StableContent.Beatmaps)) + if (content.HasFlag(StableContent.Beatmaps)) importTasks.Add(beatmapImportTask = new LegacyBeatmapImporter(beatmaps).ImportFromStableAsync(stableStorage)); - if (content.HasFlagFast(StableContent.Skins)) + if (content.HasFlag(StableContent.Skins)) importTasks.Add(new LegacySkinImporter(skins).ImportFromStableAsync(stableStorage)); - if (content.HasFlagFast(StableContent.Collections)) + if (content.HasFlag(StableContent.Collections)) { importTasks.Add(beatmapImportTask.ContinueWith(_ => new LegacyCollectionImporter(realmAccess) { @@ -180,7 +179,7 @@ namespace osu.Game.Database }.ImportFromStorage(stableStorage), TaskContinuationOptions.OnlyOnRanToCompletion)); } - if (content.HasFlagFast(StableContent.Scores)) + if (content.HasFlag(StableContent.Scores)) importTasks.Add(beatmapImportTask.ContinueWith(_ => new LegacyScoreImporter(scores).ImportFromStableAsync(stableStorage), TaskContinuationOptions.OnlyOnRanToCompletion)); await Task.WhenAll(importTasks.ToArray()).ConfigureAwait(false); diff --git a/osu.Game/Database/RealmAccess.cs b/osu.Game/Database/RealmAccess.cs index 1ece81be50..ff76142bcc 100644 --- a/osu.Game/Database/RealmAccess.cs +++ b/osu.Game/Database/RealmAccess.cs @@ -15,7 +15,6 @@ 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; @@ -1035,7 +1034,7 @@ namespace osu.Game.Database var legacyMods = (LegacyMods)sr.ReadInt32(); - if (!legacyMods.HasFlagFast(LegacyMods.ScoreV2) || score.APIMods.Any(mod => mod.Acronym == @"SV2")) + if (!legacyMods.HasFlag(LegacyMods.ScoreV2) || score.APIMods.Any(mod => mod.Acronym == @"SV2")) return; score.APIMods = score.APIMods.Append(new APIMod(new ModScoreV2())).ToArray(); diff --git a/osu.Game/Graphics/ParticleSpewer.cs b/osu.Game/Graphics/ParticleSpewer.cs index 64c70095bf..51fbd134d5 100644 --- a/osu.Game/Graphics/ParticleSpewer.cs +++ b/osu.Game/Graphics/ParticleSpewer.cs @@ -6,7 +6,6 @@ using System; using System.Diagnostics; using osu.Framework.Bindables; -using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.Rendering; @@ -190,9 +189,9 @@ namespace osu.Game.Graphics float width = Texture.DisplayWidth * scale; float height = Texture.DisplayHeight * scale; - if (relativePositionAxes.HasFlagFast(Axes.X)) + if (relativePositionAxes.HasFlag(Axes.X)) position.X *= sourceSize.X; - if (relativePositionAxes.HasFlagFast(Axes.Y)) + if (relativePositionAxes.HasFlag(Axes.Y)) position.Y *= sourceSize.Y; return new RectangleF( diff --git a/osu.Game/Graphics/UserInterface/TwoLayerButton.cs b/osu.Game/Graphics/UserInterface/TwoLayerButton.cs index 5532e5c6a7..6f61a14b75 100644 --- a/osu.Game/Graphics/UserInterface/TwoLayerButton.cs +++ b/osu.Game/Graphics/UserInterface/TwoLayerButton.cs @@ -12,7 +12,6 @@ using osu.Game.Graphics.Containers; using osu.Game.Beatmaps.ControlPoints; using osu.Framework.Audio.Track; using System; -using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; @@ -57,15 +56,15 @@ namespace osu.Game.Graphics.UserInterface set { base.Origin = value; - c1.Origin = c1.Anchor = value.HasFlagFast(Anchor.x2) ? Anchor.TopLeft : Anchor.TopRight; - c2.Origin = c2.Anchor = value.HasFlagFast(Anchor.x2) ? Anchor.TopRight : Anchor.TopLeft; + c1.Origin = c1.Anchor = value.HasFlag(Anchor.x2) ? Anchor.TopLeft : Anchor.TopRight; + c2.Origin = c2.Anchor = value.HasFlag(Anchor.x2) ? Anchor.TopRight : Anchor.TopLeft; - X = value.HasFlagFast(Anchor.x2) ? SIZE_RETRACTED.X * shear.X * 0.5f : 0; + X = value.HasFlag(Anchor.x2) ? SIZE_RETRACTED.X * shear.X * 0.5f : 0; Remove(c1, false); Remove(c2, false); - c1.Depth = value.HasFlagFast(Anchor.x2) ? 0 : 1; - c2.Depth = value.HasFlagFast(Anchor.x2) ? 1 : 0; + c1.Depth = value.HasFlag(Anchor.x2) ? 0 : 1; + c2.Depth = value.HasFlag(Anchor.x2) ? 1 : 0; Add(c1); Add(c2); } diff --git a/osu.Game/Overlays/Music/PlaylistOverlay.cs b/osu.Game/Overlays/Music/PlaylistOverlay.cs index 6ecd0f51d3..2d03a4a26d 100644 --- a/osu.Game/Overlays/Music/PlaylistOverlay.cs +++ b/osu.Game/Overlays/Music/PlaylistOverlay.cs @@ -8,7 +8,6 @@ using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; -using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Effects; @@ -131,7 +130,7 @@ namespace osu.Game.Overlays.Music filter.Search.HoldFocus = true; Schedule(() => filter.Search.TakeFocus()); - this.ResizeTo(new Vector2(1, RelativeSizeAxes.HasFlagFast(Axes.Y) ? 1f : PLAYLIST_HEIGHT), transition_duration, Easing.OutQuint); + this.ResizeTo(new Vector2(1, RelativeSizeAxes.HasFlag(Axes.Y) ? 1f : PLAYLIST_HEIGHT), transition_duration, Easing.OutQuint); this.FadeIn(transition_duration, Easing.OutQuint); } diff --git a/osu.Game/Overlays/SettingsToolboxGroup.cs b/osu.Game/Overlays/SettingsToolboxGroup.cs index de13bd96d4..53849fa53c 100644 --- a/osu.Game/Overlays/SettingsToolboxGroup.cs +++ b/osu.Game/Overlays/SettingsToolboxGroup.cs @@ -4,7 +4,6 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Caching; -using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; @@ -161,7 +160,7 @@ namespace osu.Game.Overlays protected override bool OnInvalidate(Invalidation invalidation, InvalidationSource source) { - if (invalidation.HasFlagFast(Invalidation.DrawSize)) + if (invalidation.HasFlag(Invalidation.DrawSize)) headerTextVisibilityCache.Invalidate(); return base.OnInvalidate(invalidation, source); diff --git a/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs b/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs index a671e7a76e..722ffd6d07 100644 --- a/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs +++ b/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs @@ -6,7 +6,6 @@ using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Extensions; -using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.UserInterface; @@ -145,9 +144,9 @@ namespace osu.Game.Overlays.SkinEditor var blueprintItem = ((Drawable)blueprint.Item); blueprintItem.Scale = Vector2.One; - if (blueprintItem.RelativeSizeAxes.HasFlagFast(Axes.X)) + if (blueprintItem.RelativeSizeAxes.HasFlag(Axes.X)) blueprintItem.Width = 1; - if (blueprintItem.RelativeSizeAxes.HasFlagFast(Axes.Y)) + if (blueprintItem.RelativeSizeAxes.HasFlag(Axes.Y)) blueprintItem.Height = 1; } }); diff --git a/osu.Game/Overlays/SkinEditor/SkinSelectionScaleHandler.cs b/osu.Game/Overlays/SkinEditor/SkinSelectionScaleHandler.cs index 08df8df7e2..4bfa7fba81 100644 --- a/osu.Game/Overlays/SkinEditor/SkinSelectionScaleHandler.cs +++ b/osu.Game/Overlays/SkinEditor/SkinSelectionScaleHandler.cs @@ -7,7 +7,6 @@ using System.Diagnostics; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Utils; @@ -51,7 +50,7 @@ namespace osu.Game.Overlays.SkinEditor CanScaleDiagonally.Value = true; } - private bool allSelectedSupportManualSizing(Axes axis) => selectedItems.All(b => (b as CompositeDrawable)?.AutoSizeAxes.HasFlagFast(axis) == false); + private bool allSelectedSupportManualSizing(Axes axis) => selectedItems.All(b => (b as CompositeDrawable)?.AutoSizeAxes.HasFlag(axis) == false); private Dictionary? objectsInScale; private Vector2? defaultOrigin; diff --git a/osu.Game/Overlays/Toolbar/ToolbarButton.cs b/osu.Game/Overlays/Toolbar/ToolbarButton.cs index 1da2e1b744..221282ef13 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarButton.cs @@ -4,7 +4,6 @@ using System.Linq; using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; -using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; @@ -150,9 +149,9 @@ namespace osu.Game.Overlays.Toolbar { Direction = FillDirection.Vertical, RelativeSizeAxes = Axes.Both, // stops us being considered in parent's autosize - Anchor = TooltipAnchor.HasFlagFast(Anchor.x0) ? Anchor.BottomLeft : Anchor.BottomRight, + Anchor = TooltipAnchor.HasFlag(Anchor.x0) ? Anchor.BottomLeft : Anchor.BottomRight, Origin = TooltipAnchor, - Position = new Vector2(TooltipAnchor.HasFlagFast(Anchor.x0) ? 5 : -5, 5), + Position = new Vector2(TooltipAnchor.HasFlag(Anchor.x0) ? 5 : -5, 5), Alpha = 0, Children = new Drawable[] { diff --git a/osu.Game/Replays/Legacy/LegacyReplayFrame.cs b/osu.Game/Replays/Legacy/LegacyReplayFrame.cs index f345504ca1..b48fc44963 100644 --- a/osu.Game/Replays/Legacy/LegacyReplayFrame.cs +++ b/osu.Game/Replays/Legacy/LegacyReplayFrame.cs @@ -3,7 +3,6 @@ using MessagePack; using Newtonsoft.Json; -using osu.Framework.Extensions.EnumExtensions; using osu.Game.Rulesets.Replays; using osuTK; @@ -32,23 +31,23 @@ namespace osu.Game.Replays.Legacy [JsonIgnore] [IgnoreMember] - public bool MouseLeft1 => ButtonState.HasFlagFast(ReplayButtonState.Left1); + public bool MouseLeft1 => ButtonState.HasFlag(ReplayButtonState.Left1); [JsonIgnore] [IgnoreMember] - public bool MouseRight1 => ButtonState.HasFlagFast(ReplayButtonState.Right1); + public bool MouseRight1 => ButtonState.HasFlag(ReplayButtonState.Right1); [JsonIgnore] [IgnoreMember] - public bool MouseLeft2 => ButtonState.HasFlagFast(ReplayButtonState.Left2); + public bool MouseLeft2 => ButtonState.HasFlag(ReplayButtonState.Left2); [JsonIgnore] [IgnoreMember] - public bool MouseRight2 => ButtonState.HasFlagFast(ReplayButtonState.Right2); + public bool MouseRight2 => ButtonState.HasFlag(ReplayButtonState.Right2); [JsonIgnore] [IgnoreMember] - public bool Smoke => ButtonState.HasFlagFast(ReplayButtonState.Smoke); + public bool Smoke => ButtonState.HasFlag(ReplayButtonState.Smoke); [Key(3)] public ReplayButtonState ButtonState; diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs index 540e0440c6..5bd720cfba 100644 --- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs @@ -10,7 +10,6 @@ using System.Linq; using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; @@ -505,7 +504,7 @@ namespace osu.Game.Rulesets.Edit var playfield = PlayfieldAtScreenSpacePosition(screenSpacePosition); double? targetTime = null; - if (snapType.HasFlagFast(SnapType.GlobalGrids)) + if (snapType.HasFlag(SnapType.GlobalGrids)) { if (playfield is ScrollingPlayfield scrollingPlayfield) { diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs index 66b3033f90..37a87462ca 100644 --- a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs @@ -12,7 +12,6 @@ using osu.Game.Beatmaps.Formats; using osu.Game.Audio; using System.Linq; using JetBrains.Annotations; -using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Utils; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.Legacy; @@ -58,7 +57,7 @@ namespace osu.Game.Rulesets.Objects.Legacy int comboOffset = (int)(type & LegacyHitObjectType.ComboOffset) >> 4; type &= ~LegacyHitObjectType.ComboOffset; - bool combo = type.HasFlagFast(LegacyHitObjectType.NewCombo); + bool combo = type.HasFlag(LegacyHitObjectType.NewCombo); type &= ~LegacyHitObjectType.NewCombo; var soundType = (LegacyHitSoundType)Parsing.ParseInt(split[4]); @@ -66,14 +65,14 @@ namespace osu.Game.Rulesets.Objects.Legacy HitObject result = null; - if (type.HasFlagFast(LegacyHitObjectType.Circle)) + if (type.HasFlag(LegacyHitObjectType.Circle)) { result = CreateHit(pos, combo, comboOffset); if (split.Length > 5) readCustomSampleBanks(split[5], bankInfo); } - else if (type.HasFlagFast(LegacyHitObjectType.Slider)) + else if (type.HasFlag(LegacyHitObjectType.Slider)) { double? length = null; @@ -145,7 +144,7 @@ namespace osu.Game.Rulesets.Objects.Legacy result = CreateSlider(pos, combo, comboOffset, convertPathString(split[5], pos), length, repeatCount, nodeSamples); } - else if (type.HasFlagFast(LegacyHitObjectType.Spinner)) + else if (type.HasFlag(LegacyHitObjectType.Spinner)) { double duration = Math.Max(0, Parsing.ParseDouble(split[5]) + Offset - startTime); @@ -154,7 +153,7 @@ namespace osu.Game.Rulesets.Objects.Legacy if (split.Length > 6) readCustomSampleBanks(split[6], bankInfo); } - else if (type.HasFlagFast(LegacyHitObjectType.Hold)) + else if (type.HasFlag(LegacyHitObjectType.Hold)) { // Note: Hold is generated by BMS converts @@ -472,7 +471,7 @@ namespace osu.Game.Rulesets.Objects.Legacy soundTypes.Add(new LegacyHitSampleInfo(HitSampleInfo.HIT_NORMAL, bankInfo.BankForNormal, bankInfo.Volume, bankInfo.CustomSampleBank, // if the sound type doesn't have the Normal flag set, attach it anyway as a layered sample. // None also counts as a normal non-layered sample: https://osu.ppy.sh/help/wiki/osu!_File_Formats/Osu_(file_format)#hitsounds - type != LegacyHitSoundType.None && !type.HasFlagFast(LegacyHitSoundType.Normal))); + type != LegacyHitSoundType.None && !type.HasFlag(LegacyHitSoundType.Normal))); } else { @@ -480,13 +479,13 @@ namespace osu.Game.Rulesets.Objects.Legacy soundTypes.Add(new FileHitSampleInfo(bankInfo.Filename, bankInfo.Volume)); } - if (type.HasFlagFast(LegacyHitSoundType.Finish)) + if (type.HasFlag(LegacyHitSoundType.Finish)) soundTypes.Add(new LegacyHitSampleInfo(HitSampleInfo.HIT_FINISH, bankInfo.BankForAdditions, bankInfo.Volume, bankInfo.CustomSampleBank)); - if (type.HasFlagFast(LegacyHitSoundType.Whistle)) + if (type.HasFlag(LegacyHitSoundType.Whistle)) soundTypes.Add(new LegacyHitSampleInfo(HitSampleInfo.HIT_WHISTLE, bankInfo.BankForAdditions, bankInfo.Volume, bankInfo.CustomSampleBank)); - if (type.HasFlagFast(LegacyHitSoundType.Clap)) + if (type.HasFlag(LegacyHitSoundType.Clap)) soundTypes.Add(new LegacyHitSampleInfo(HitSampleInfo.HIT_CLAP, bankInfo.BankForAdditions, bankInfo.Volume, bankInfo.CustomSampleBank)); return soundTypes; diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs index fec3224fad..0cc8a8273f 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs @@ -4,7 +4,6 @@ using System; using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; @@ -298,13 +297,13 @@ namespace osu.Game.Screens.Edit.Compose.Components /// public void PerformFlipFromScaleHandles(Axes axes) { - if (axes.HasFlagFast(Axes.X)) + if (axes.HasFlag(Axes.X)) { dragHandles.FlipScaleHandles(Direction.Horizontal); OnFlip?.Invoke(Direction.Horizontal, false); } - if (axes.HasFlagFast(Axes.Y)) + if (axes.HasFlag(Axes.Y)) { dragHandles.FlipScaleHandles(Direction.Vertical); OnFlip?.Invoke(Direction.Vertical, false); diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionBoxDragHandleContainer.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionBoxDragHandleContainer.cs index e7f69b7b37..e5ac05ca6a 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionBoxDragHandleContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionBoxDragHandleContainer.cs @@ -7,7 +7,6 @@ using System.Collections.Generic; using System.Linq; using JetBrains.Annotations; using osu.Framework.Allocation; -using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -74,9 +73,9 @@ namespace osu.Game.Screens.Edit.Compose.Components { foreach (var handle in scaleHandles) { - if (direction == Direction.Horizontal && !handle.Anchor.HasFlagFast(Anchor.x1)) + if (direction == Direction.Horizontal && !handle.Anchor.HasFlag(Anchor.x1)) handle.Anchor ^= Anchor.x0 | Anchor.x2; - if (direction == Direction.Vertical && !handle.Anchor.HasFlagFast(Anchor.y1)) + if (direction == Direction.Vertical && !handle.Anchor.HasFlag(Anchor.y1)) handle.Anchor ^= Anchor.y0 | Anchor.y2; } } diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionBoxRotationHandle.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionBoxRotationHandle.cs index b9383f1bad..c62e0e0d41 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionBoxRotationHandle.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionBoxRotationHandle.cs @@ -4,7 +4,6 @@ using System; using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Sprites; @@ -46,8 +45,8 @@ namespace osu.Game.Screens.Edit.Compose.Components Icon = FontAwesome.Solid.Redo, Scale = new Vector2 { - X = Anchor.HasFlagFast(Anchor.x0) ? 1f : -1f, - Y = Anchor.HasFlagFast(Anchor.y0) ? 1f : -1f + X = Anchor.HasFlag(Anchor.x0) ? 1f : -1f, + Y = Anchor.HasFlag(Anchor.y0) ? 1f : -1f } }); } diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionBoxScaleHandle.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionBoxScaleHandle.cs index 3f4f2c2854..7b0943c1d0 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionBoxScaleHandle.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionBoxScaleHandle.cs @@ -3,7 +3,6 @@ using osu.Framework.Allocation; using osu.Framework.Extensions; -using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Graphics; using osu.Framework.Input.Events; using osu.Framework.Utils; @@ -128,6 +127,6 @@ namespace osu.Game.Screens.Edit.Compose.Components } } - private bool isCornerAnchor(Anchor anchor) => !anchor.HasFlagFast(Anchor.x1) && !anchor.HasFlagFast(Anchor.y1); + private bool isCornerAnchor(Anchor anchor) => !anchor.HasFlag(Anchor.x1) && !anchor.HasFlag(Anchor.y1); } } diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index 0c0941573c..ef3bb7c04a 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -8,7 +8,6 @@ using System.Collections.Generic; using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Primitives; @@ -295,7 +294,7 @@ namespace osu.Game.Screens.Play Drawable drawable = (Drawable)element; // for now align some top components with the bottom-edge of the lowest top-anchored hud element. - if (drawable.Anchor.HasFlagFast(Anchor.y0)) + if (drawable.Anchor.HasFlag(Anchor.y0)) { // health bars are excluded for the sake of hacky legacy skins which extend the health bar to take up the full screen area. if (element is LegacyHealthDisplay) @@ -305,20 +304,20 @@ namespace osu.Game.Screens.Play bool isRelativeX = drawable.RelativeSizeAxes == Axes.X; - if (drawable.Anchor.HasFlagFast(Anchor.TopRight) || isRelativeX) + if (drawable.Anchor.HasFlag(Anchor.TopRight) || isRelativeX) { if (lowestTopScreenSpaceRight == null || bottom > lowestTopScreenSpaceRight.Value) lowestTopScreenSpaceRight = bottom; } - if (drawable.Anchor.HasFlagFast(Anchor.TopLeft) || isRelativeX) + if (drawable.Anchor.HasFlag(Anchor.TopLeft) || isRelativeX) { if (lowestTopScreenSpaceLeft == null || bottom > lowestTopScreenSpaceLeft.Value) lowestTopScreenSpaceLeft = bottom; } } // and align bottom-right components with the top-edge of the highest bottom-anchored hud element. - else if (drawable.Anchor.HasFlagFast(Anchor.BottomRight) || (drawable.Anchor.HasFlagFast(Anchor.y2) && drawable.RelativeSizeAxes == Axes.X)) + else if (drawable.Anchor.HasFlag(Anchor.BottomRight) || (drawable.Anchor.HasFlag(Anchor.y2) && drawable.RelativeSizeAxes == Axes.X)) { var topLeft = element.ScreenSpaceDrawQuad.TopLeft; if (highestBottomScreenSpace == null || topLeft.Y < highestBottomScreenSpace.Value.Y) diff --git a/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs b/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs index 47807a8346..207e19a716 100644 --- a/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs +++ b/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs @@ -5,7 +5,6 @@ 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; @@ -282,7 +281,7 @@ namespace osu.Game.Screens.Ranking.Statistics protected override bool OnInvalidate(Invalidation invalidation, InvalidationSource source) { - if (invalidation.HasFlagFast(Invalidation.DrawSize)) + if (invalidation.HasFlag(Invalidation.DrawSize)) { if (lastDrawHeight != null && lastDrawHeight != DrawHeight) Scheduler.AddOnce(updateMetrics, false); diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 4f2325adbf..56e7c24985 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -10,7 +10,6 @@ using osu.Framework.Audio; using osu.Framework.Audio.Sample; using osu.Framework.Bindables; using osu.Framework.Caching; -using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Pooling; @@ -828,7 +827,7 @@ namespace osu.Game.Screens.Select protected override bool OnInvalidate(Invalidation invalidation, InvalidationSource source) { // handles the vertical size of the carousel changing (ie. on window resize when aspect ratio has changed). - if (invalidation.HasFlagFast(Invalidation.DrawSize)) + if (invalidation.HasFlag(Invalidation.DrawSize)) itemsCache.Invalidate(); return base.OnInvalidate(invalidation, source); diff --git a/osu.Game/Skinning/SerialisableDrawableExtensions.cs b/osu.Game/Skinning/SerialisableDrawableExtensions.cs index 97c4cc8f73..a0488492ae 100644 --- a/osu.Game/Skinning/SerialisableDrawableExtensions.cs +++ b/osu.Game/Skinning/SerialisableDrawableExtensions.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Bindables; -using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Configuration; @@ -19,9 +18,9 @@ namespace osu.Game.Skinning // todo: can probably make this better via deserialisation directly using a common interface. component.Position = drawableInfo.Position; component.Rotation = drawableInfo.Rotation; - if (drawableInfo.Width is float width && width != 0 && (component as CompositeDrawable)?.AutoSizeAxes.HasFlagFast(Axes.X) != true) + if (drawableInfo.Width is float width && width != 0 && (component as CompositeDrawable)?.AutoSizeAxes.HasFlag(Axes.X) != true) component.Width = width; - if (drawableInfo.Height is float height && height != 0 && (component as CompositeDrawable)?.AutoSizeAxes.HasFlagFast(Axes.Y) != true) + if (drawableInfo.Height is float height && height != 0 && (component as CompositeDrawable)?.AutoSizeAxes.HasFlag(Axes.Y) != true) component.Height = height; component.Scale = drawableInfo.Scale; component.Anchor = drawableInfo.Anchor; diff --git a/osu.Game/Skinning/SerialisedDrawableInfo.cs b/osu.Game/Skinning/SerialisedDrawableInfo.cs index ac1aa80d29..b4be5745d1 100644 --- a/osu.Game/Skinning/SerialisedDrawableInfo.cs +++ b/osu.Game/Skinning/SerialisedDrawableInfo.cs @@ -6,7 +6,6 @@ using System.Collections.Generic; using System.Linq; using Newtonsoft.Json; using osu.Framework.Bindables; -using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Logging; @@ -68,10 +67,10 @@ namespace osu.Game.Skinning Rotation = component.Rotation; Scale = component.Scale; - if ((component as CompositeDrawable)?.AutoSizeAxes.HasFlagFast(Axes.X) != true) + if ((component as CompositeDrawable)?.AutoSizeAxes.HasFlag(Axes.X) != true) Width = component.Width; - if ((component as CompositeDrawable)?.AutoSizeAxes.HasFlagFast(Axes.Y) != true) + if ((component as CompositeDrawable)?.AutoSizeAxes.HasFlag(Axes.Y) != true) Height = component.Height; Anchor = component.Anchor; diff --git a/osu.Game/Storyboards/StoryboardExtensions.cs b/osu.Game/Storyboards/StoryboardExtensions.cs index 04c7196315..110af73cca 100644 --- a/osu.Game/Storyboards/StoryboardExtensions.cs +++ b/osu.Game/Storyboards/StoryboardExtensions.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 osu.Framework.Extensions.EnumExtensions; using osu.Framework.Graphics; using osuTK; @@ -22,18 +21,18 @@ namespace osu.Game.Storyboards // Either flip horizontally or negative X scale, but not both. if (flipH ^ (vectorScale.X < 0)) { - if (origin.HasFlagFast(Anchor.x0)) + if (origin.HasFlag(Anchor.x0)) origin = Anchor.x2 | (origin & (Anchor.y0 | Anchor.y1 | Anchor.y2)); - else if (origin.HasFlagFast(Anchor.x2)) + else if (origin.HasFlag(Anchor.x2)) origin = Anchor.x0 | (origin & (Anchor.y0 | Anchor.y1 | Anchor.y2)); } // Either flip vertically or negative Y scale, but not both. if (flipV ^ (vectorScale.Y < 0)) { - if (origin.HasFlagFast(Anchor.y0)) + if (origin.HasFlag(Anchor.y0)) origin = Anchor.y2 | (origin & (Anchor.x0 | Anchor.x1 | Anchor.x2)); - else if (origin.HasFlagFast(Anchor.y2)) + else if (origin.HasFlag(Anchor.y2)) origin = Anchor.y0 | (origin & (Anchor.x0 | Anchor.x1 | Anchor.x2)); } From 7cb3d7445cdd42c32c4a6dc995d8383878c60e16 Mon Sep 17 00:00:00 2001 From: Arthur Araujo Date: Tue, 2 Jul 2024 17:20:00 -0300 Subject: [PATCH 1273/2493] Add verify checks for title markers --- .../Editing/Checks/CheckTitleMarkersTest.cs | 235 ++++++++++++++++++ osu.Game/Rulesets/Edit/BeatmapVerifier.cs | 3 + .../Rulesets/Edit/Checks/CheckTitleMarkers.cs | 76 ++++++ 3 files changed, 314 insertions(+) create mode 100644 osu.Game.Tests/Editing/Checks/CheckTitleMarkersTest.cs create mode 100644 osu.Game/Rulesets/Edit/Checks/CheckTitleMarkers.cs diff --git a/osu.Game.Tests/Editing/Checks/CheckTitleMarkersTest.cs b/osu.Game.Tests/Editing/Checks/CheckTitleMarkersTest.cs new file mode 100644 index 0000000000..54d3136700 --- /dev/null +++ b/osu.Game.Tests/Editing/Checks/CheckTitleMarkersTest.cs @@ -0,0 +1,235 @@ +// 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.Tests.Beatmaps; + +namespace osu.Game.Tests.Editing.Checks +{ + [TestFixture] + public class CheckTitleMarkersTest + { + private CheckTitleMarkers check = null!; + + private IBeatmap beatmap = null!; + + [SetUp] + public void Setup() + { + check = new CheckTitleMarkers(); + + beatmap = new Beatmap + { + BeatmapInfo = new BeatmapInfo + { + Metadata = new BeatmapMetadata + { + Title = "Egao no Kanata", + TitleUnicode = "エガオノカナタ" + } + } + }; + } + + [Test] + public void TestNoTitleMarkers() + { + var issues = check.Run(getContext(beatmap)).ToList(); + Assert.That(issues, Has.Count.EqualTo(0)); + } + + [Test] + public void TestTVSizeMarker() + { + beatmap.BeatmapInfo.Metadata.Title += " (TV Size)"; + beatmap.BeatmapInfo.Metadata.TitleUnicode += " (TV Size)"; + + var issues = check.Run(getContext(beatmap)).ToList(); + + Assert.That(issues, Has.Count.EqualTo(0)); + } + + [Test] + public void TestMalformedTVSizeMarker() + { + beatmap.BeatmapInfo.Metadata.Title += " (tv size)"; + beatmap.BeatmapInfo.Metadata.TitleUnicode += " (tv size)"; + + var issues = check.Run(getContext(beatmap)).ToList(); + + Assert.That(issues, Has.Count.EqualTo(2)); + Assert.That(issues.Any(issue => issue.Template is CheckTitleMarkers.IssueTemplateIncorrectMarker)); + } + + [Test] + public void TestGameVerMarker() + { + beatmap.BeatmapInfo.Metadata.Title += " (Game Ver.)"; + beatmap.BeatmapInfo.Metadata.TitleUnicode += " (Game Ver.)"; + + var issues = check.Run(getContext(beatmap)).ToList(); + + Assert.That(issues, Has.Count.EqualTo(0)); + } + + [Test] + public void TestMalformedGameVerMarker() + { + beatmap.BeatmapInfo.Metadata.Title += " (game ver.)"; + beatmap.BeatmapInfo.Metadata.TitleUnicode += " (game ver.)"; + + var issues = check.Run(getContext(beatmap)).ToList(); + + Assert.That(issues, Has.Count.EqualTo(2)); + Assert.That(issues.Any(issue => issue.Template is CheckTitleMarkers.IssueTemplateIncorrectMarker)); + } + + [Test] + public void TestShortVerMarker() + { + beatmap.BeatmapInfo.Metadata.Title += " (Short Ver.)"; + beatmap.BeatmapInfo.Metadata.TitleUnicode += " (Short Ver.)"; + + var issues = check.Run(getContext(beatmap)).ToList(); + + Assert.That(issues, Has.Count.EqualTo(0)); + } + + [Test] + public void TestMalformedShortVerMarker() + { + beatmap.BeatmapInfo.Metadata.Title += " (short ver.)"; + beatmap.BeatmapInfo.Metadata.TitleUnicode += " (short ver.)"; + + var issues = check.Run(getContext(beatmap)).ToList(); + + Assert.That(issues, Has.Count.EqualTo(2)); + Assert.That(issues.Any(issue => issue.Template is CheckTitleMarkers.IssueTemplateIncorrectMarker)); + } + + [Test] + public void TestCutVerMarker() + { + beatmap.BeatmapInfo.Metadata.Title += " (Cut Ver.)"; + beatmap.BeatmapInfo.Metadata.TitleUnicode += " (Cut Ver.)"; + + var issues = check.Run(getContext(beatmap)).ToList(); + + Assert.That(issues, Has.Count.EqualTo(0)); + } + + [Test] + public void TestMalformedCutVerMarker() + { + beatmap.BeatmapInfo.Metadata.Title += " (cut ver.)"; + beatmap.BeatmapInfo.Metadata.TitleUnicode += " (cut ver.)"; + + var issues = check.Run(getContext(beatmap)).ToList(); + + Assert.That(issues, Has.Count.EqualTo(2)); + Assert.That(issues.Any(issue => issue.Template is CheckTitleMarkers.IssueTemplateIncorrectMarker)); + } + + [Test] + public void TestSpedUpVerMarker() + { + beatmap.BeatmapInfo.Metadata.Title += " (Sped Up Ver.)"; + beatmap.BeatmapInfo.Metadata.TitleUnicode += " (Sped Up Ver.)"; + + var issues = check.Run(getContext(beatmap)).ToList(); + + Assert.That(issues, Has.Count.EqualTo(0)); + } + + [Test] + public void TestMalformedSpedUpVerMarker() + { + beatmap.BeatmapInfo.Metadata.Title += " (sped up ver.)"; + beatmap.BeatmapInfo.Metadata.TitleUnicode += " (sped up ver.)"; + + var issues = check.Run(getContext(beatmap)).ToList(); + + Assert.That(issues, Has.Count.EqualTo(2)); + Assert.That(issues.Any(issue => issue.Template is CheckTitleMarkers.IssueTemplateIncorrectMarker)); + } + + [Test] + public void TestNightcoreMixMarker() + { + beatmap.BeatmapInfo.Metadata.Title += " (Nightcore Mix)"; + beatmap.BeatmapInfo.Metadata.TitleUnicode += " (Nightcore Mix)"; + + var issues = check.Run(getContext(beatmap)).ToList(); + + Assert.That(issues, Has.Count.EqualTo(0)); + } + + [Test] + public void TestMalformedNightcoreMixMarker() + { + beatmap.BeatmapInfo.Metadata.Title += " (nightcore mix)"; + beatmap.BeatmapInfo.Metadata.TitleUnicode += " (nightcore mix)"; + + var issues = check.Run(getContext(beatmap)).ToList(); + + Assert.That(issues, Has.Count.EqualTo(2)); + Assert.That(issues.Any(issue => issue.Template is CheckTitleMarkers.IssueTemplateIncorrectMarker)); + } + + [Test] + public void TestSpedUpCutVerMarker() + { + beatmap.BeatmapInfo.Metadata.Title += " (Sped Up & Cut Ver.)"; + beatmap.BeatmapInfo.Metadata.TitleUnicode += " (Sped Up & Cut Ver.)"; + + var issues = check.Run(getContext(beatmap)).ToList(); + + Assert.That(issues, Has.Count.EqualTo(0)); + } + + [Test] + public void TestMalformedSpedUpCutVerMarker() + { + beatmap.BeatmapInfo.Metadata.Title += " (sped up & cut ver.)"; + beatmap.BeatmapInfo.Metadata.TitleUnicode += " (sped up & cut ver.)"; + + var issues = check.Run(getContext(beatmap)).ToList(); + + Assert.That(issues, Has.Count.EqualTo(2)); + Assert.That(issues.Any(issue => issue.Template is CheckTitleMarkers.IssueTemplateIncorrectMarker)); + } + + [Test] + public void TestNightcoreCutVerMarker() + { + beatmap.BeatmapInfo.Metadata.Title += " (Nightcore & Cut Ver.)"; + beatmap.BeatmapInfo.Metadata.TitleUnicode += " (Nightcore & Cut Ver.)"; + + var issues = check.Run(getContext(beatmap)).ToList(); + + Assert.That(issues, Has.Count.EqualTo(0)); + } + + [Test] + public void TestMalformedNightcoreCutVerMarker() + { + beatmap.BeatmapInfo.Metadata.Title += " (nightcore & cut ver.)"; + beatmap.BeatmapInfo.Metadata.TitleUnicode += " (nightcore & cut ver.)"; + + var issues = check.Run(getContext(beatmap)).ToList(); + + Assert.That(issues, Has.Count.EqualTo(2)); + Assert.That(issues.Any(issue => issue.Template is CheckTitleMarkers.IssueTemplateIncorrectMarker)); + } + + private BeatmapVerifierContext getContext(IBeatmap beatmap) + { + return new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap)); + } + } +} \ No newline at end of file diff --git a/osu.Game/Rulesets/Edit/BeatmapVerifier.cs b/osu.Game/Rulesets/Edit/BeatmapVerifier.cs index a9681e13ba..642b878a7b 100644 --- a/osu.Game/Rulesets/Edit/BeatmapVerifier.cs +++ b/osu.Game/Rulesets/Edit/BeatmapVerifier.cs @@ -46,6 +46,9 @@ namespace osu.Game.Rulesets.Edit // Events new CheckBreaks(), + + // Metadata + new CheckTitleMarkers(), }; public IEnumerable Run(BeatmapVerifierContext context) diff --git a/osu.Game/Rulesets/Edit/Checks/CheckTitleMarkers.cs b/osu.Game/Rulesets/Edit/Checks/CheckTitleMarkers.cs new file mode 100644 index 0000000000..6753abde4d --- /dev/null +++ b/osu.Game/Rulesets/Edit/Checks/CheckTitleMarkers.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.Collections.Generic; +using System.Text.RegularExpressions; +using osu.Game.Rulesets.Edit.Checks.Components; + +namespace osu.Game.Rulesets.Edit.Checks +{ + public class CheckTitleMarkers : ICheck + { + public CheckMetadata Metadata => new CheckMetadata(CheckCategory.Metadata, "Checks for incorrect formats of (TV Size) / (Game Ver.) / (Short Ver.) / (Cut Ver.) / (Sped Up Ver.) / etc in title."); + + public IEnumerable PossibleTemplates => new IssueTemplate[] + { + new IssueTemplateIncorrectMarker(this), + }; + + public IEnumerable MarkerChecks => new MarkerCheck[] + { + new MarkerCheck("(TV Size)", @"(?i)(tv (size|ver))"), + new MarkerCheck("(Game Ver.)", @"(?i)(game (size|ver))"), + new MarkerCheck("(Short Ver.)", @"(?i)(short (size|ver))"), + new MarkerCheck("(Cut Ver.)", @"(?i)(? Run(BeatmapVerifierContext context) + { + string romanisedTitle = context.Beatmap.Metadata.Title; + string unicodeTitle = context.Beatmap.Metadata.TitleUnicode; + + foreach (var check in MarkerChecks) + { + bool hasRomanisedTitle = unicodeTitle != romanisedTitle; + + if (check.AnyRegex.IsMatch(romanisedTitle) && !check.ExactRegex.IsMatch(romanisedTitle)) + { + yield return new IssueTemplateIncorrectMarker(this).Create(hasRomanisedTitle ? "Romanised title" : "Title", check.CorrectMarkerFormat); + } + + if (hasRomanisedTitle && check.AnyRegex.IsMatch(unicodeTitle) && !check.ExactRegex.IsMatch(unicodeTitle)) + { + yield return new IssueTemplateIncorrectMarker(this).Create("Title", check.CorrectMarkerFormat); + } + } + } + + public class MarkerCheck + { + public string CorrectMarkerFormat; + public Regex ExactRegex; + public Regex AnyRegex; + + public MarkerCheck(string exact, string anyRegex) + { + CorrectMarkerFormat = exact; + ExactRegex = new Regex(Regex.Escape(exact)); + AnyRegex = new Regex(anyRegex); + } + } + + public class IssueTemplateIncorrectMarker : IssueTemplate + { + public IssueTemplateIncorrectMarker(ICheck check) + : base(check, IssueType.Problem, "{0} field has a incorrect format of marker {1}") + { + } + + public Issue Create(string titleField, string correctMarkerFormat) => new Issue(this, titleField, correctMarkerFormat); + } + } +} \ No newline at end of file From 7cdad20119481ba3ff48001a08ee3f1abb58fc71 Mon Sep 17 00:00:00 2001 From: Arthur Araujo Date: Tue, 2 Jul 2024 20:55:52 -0300 Subject: [PATCH 1274/2493] Fix explicit array type specification in MarkerChecks --- osu.Game/Rulesets/Edit/Checks/CheckTitleMarkers.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/osu.Game/Rulesets/Edit/Checks/CheckTitleMarkers.cs b/osu.Game/Rulesets/Edit/Checks/CheckTitleMarkers.cs index 6753abde4d..6e5faf3dae 100644 --- a/osu.Game/Rulesets/Edit/Checks/CheckTitleMarkers.cs +++ b/osu.Game/Rulesets/Edit/Checks/CheckTitleMarkers.cs @@ -16,8 +16,7 @@ namespace osu.Game.Rulesets.Edit.Checks new IssueTemplateIncorrectMarker(this), }; - public IEnumerable MarkerChecks => new MarkerCheck[] - { + public IEnumerable MarkerChecks = [ new MarkerCheck("(TV Size)", @"(?i)(tv (size|ver))"), new MarkerCheck("(Game Ver.)", @"(?i)(game (size|ver))"), new MarkerCheck("(Short Ver.)", @"(?i)(short (size|ver))"), @@ -26,7 +25,7 @@ namespace osu.Game.Rulesets.Edit.Checks new MarkerCheck("(Nightcore Mix)", @"(?i)(? Run(BeatmapVerifierContext context) { From 7143ff523fb9155ca7ed10e3f02d3ed38bef138b Mon Sep 17 00:00:00 2001 From: Arthur Araujo Date: Tue, 2 Jul 2024 21:09:49 -0300 Subject: [PATCH 1275/2493] Make `MarkerChecks` and `MarkerCheck` class private --- osu.Game/Rulesets/Edit/Checks/CheckTitleMarkers.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Rulesets/Edit/Checks/CheckTitleMarkers.cs b/osu.Game/Rulesets/Edit/Checks/CheckTitleMarkers.cs index 6e5faf3dae..2471d175ae 100644 --- a/osu.Game/Rulesets/Edit/Checks/CheckTitleMarkers.cs +++ b/osu.Game/Rulesets/Edit/Checks/CheckTitleMarkers.cs @@ -16,7 +16,7 @@ namespace osu.Game.Rulesets.Edit.Checks new IssueTemplateIncorrectMarker(this), }; - public IEnumerable MarkerChecks = [ + private IEnumerable markerChecks = [ new MarkerCheck("(TV Size)", @"(?i)(tv (size|ver))"), new MarkerCheck("(Game Ver.)", @"(?i)(game (size|ver))"), new MarkerCheck("(Short Ver.)", @"(?i)(short (size|ver))"), @@ -32,7 +32,7 @@ namespace osu.Game.Rulesets.Edit.Checks string romanisedTitle = context.Beatmap.Metadata.Title; string unicodeTitle = context.Beatmap.Metadata.TitleUnicode; - foreach (var check in MarkerChecks) + foreach (var check in markerChecks) { bool hasRomanisedTitle = unicodeTitle != romanisedTitle; @@ -48,7 +48,7 @@ namespace osu.Game.Rulesets.Edit.Checks } } - public class MarkerCheck + private class MarkerCheck { public string CorrectMarkerFormat; public Regex ExactRegex; From 21829e7ef46bc3dcc91ca5677ed837bbdb024d8a Mon Sep 17 00:00:00 2001 From: Arthur Araujo Date: Tue, 2 Jul 2024 21:12:15 -0300 Subject: [PATCH 1276/2493] Fix test method names --- osu.Game.Tests/Editing/Checks/CheckTitleMarkersTest.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Editing/Checks/CheckTitleMarkersTest.cs b/osu.Game.Tests/Editing/Checks/CheckTitleMarkersTest.cs index 54d3136700..a8f86a6d45 100644 --- a/osu.Game.Tests/Editing/Checks/CheckTitleMarkersTest.cs +++ b/osu.Game.Tests/Editing/Checks/CheckTitleMarkersTest.cs @@ -44,7 +44,7 @@ namespace osu.Game.Tests.Editing.Checks } [Test] - public void TestTVSizeMarker() + public void TestTvSizeMarker() { beatmap.BeatmapInfo.Metadata.Title += " (TV Size)"; beatmap.BeatmapInfo.Metadata.TitleUnicode += " (TV Size)"; @@ -55,7 +55,7 @@ namespace osu.Game.Tests.Editing.Checks } [Test] - public void TestMalformedTVSizeMarker() + public void TestMalformedTvSizeMarker() { beatmap.BeatmapInfo.Metadata.Title += " (tv size)"; beatmap.BeatmapInfo.Metadata.TitleUnicode += " (tv size)"; From abfdf90b541a94888e147554b863f24d96c2d277 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 3 Jul 2024 07:11:35 +0300 Subject: [PATCH 1277/2493] Remove unused using directive --- osu.Game.Tests/Visual/Editing/TestSceneTimelineSelection.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneTimelineSelection.cs b/osu.Game.Tests/Visual/Editing/TestSceneTimelineSelection.cs index c6d284fae6..229cb995d8 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneTimelineSelection.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneTimelineSelection.cs @@ -6,7 +6,6 @@ using System; using System.Collections.Generic; using System.Linq; -using Humanizer; using NUnit.Framework; using osu.Framework.Graphics.UserInterface; using osu.Framework.Testing; From 153138cdac5707988a1b351e96b4019f5512b13f Mon Sep 17 00:00:00 2001 From: Jamie Taylor Date: Wed, 3 Jul 2024 13:47:41 +0900 Subject: [PATCH 1278/2493] Use `null` to disable audio filter instead --- osu.Game/Overlays/MusicController.cs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/osu.Game/Overlays/MusicController.cs b/osu.Game/Overlays/MusicController.cs index 678ae92d4b..7fe9a3e33b 100644 --- a/osu.Game/Overlays/MusicController.cs +++ b/osu.Game/Overlays/MusicController.cs @@ -11,6 +11,7 @@ using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Track; using osu.Framework.Bindables; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Audio; using osu.Framework.Graphics.Containers; @@ -259,13 +260,15 @@ namespace osu.Game.Overlays /// /// Duration of the ducking transition, in ms. /// Level to drop volume to (1.0 = 100%). - /// Cutoff frequency to drop `AudioFilter` to. Use `AudioFilter.MAX_LOWPASS_CUTOFF` to skip filter effect. + /// Cutoff frequency to drop `AudioFilter` to. Use `null` to skip filter effect. /// Easing for the ducking transition. - public void Duck(int duration = 0, float duckVolumeTo = 0.25f, int duckCutoffTo = 300, Easing easing = Easing.InCubic) + public void Duck(int duration = 0, float duckVolumeTo = 0.25f, int? duckCutoffTo = 300, Easing easing = Easing.InCubic) { Schedule(() => { - audioDuckFilter?.CutoffTo(duckCutoffTo, duration, easing); + if (duckCutoffTo.IsNotNull()) + audioDuckFilter?.CutoffTo((int)duckCutoffTo, duration, easing); + this.TransformBindableTo(audioDuckVolume, duckVolumeTo, duration, easing); }); } @@ -291,10 +294,10 @@ namespace osu.Game.Overlays /// Duration of the unducking transition, in ms. /// Easing for the unducking transition. /// Level to drop volume to (1.0 = 100%). - /// Cutoff frequency to drop `AudioFilter` to. Use `AudioFilter.MAX_LOWPASS_CUTOFF` to skip filter effect. + /// Cutoff frequency to drop `AudioFilter` to. Use `null` to skip filter effect. /// Duration of the ducking transition, in ms. /// Easing for the ducking transition. - public void TimedDuck(int delay, int unduckDuration = 500, Easing unduckEasing = Easing.InCubic, float duckVolumeTo = 0.25f, int duckCutoffTo = 300, int duckDuration = 0, Easing duckEasing = Easing.InCubic) + public void TimedDuck(int delay, int unduckDuration = 500, Easing unduckEasing = Easing.InCubic, float duckVolumeTo = 0.25f, int? duckCutoffTo = 300, int duckDuration = 0, Easing duckEasing = Easing.InCubic) { Duck(duckDuration, duckVolumeTo, duckCutoffTo, duckEasing); Scheduler.AddDelayed(() => Unduck(unduckDuration, unduckEasing), delay); From b972632e4fdcfd2a6bfb866b1ab06ade5ee16c27 Mon Sep 17 00:00:00 2001 From: Jamie Taylor Date: Wed, 3 Jul 2024 13:50:35 +0900 Subject: [PATCH 1279/2493] Change default easing to match prior behaviour --- osu.Game/Overlays/MusicController.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/MusicController.cs b/osu.Game/Overlays/MusicController.cs index 7fe9a3e33b..b0e6994448 100644 --- a/osu.Game/Overlays/MusicController.cs +++ b/osu.Game/Overlays/MusicController.cs @@ -262,7 +262,7 @@ namespace osu.Game.Overlays /// Level to drop volume to (1.0 = 100%). /// Cutoff frequency to drop `AudioFilter` to. Use `null` to skip filter effect. /// Easing for the ducking transition. - public void Duck(int duration = 0, float duckVolumeTo = 0.25f, int? duckCutoffTo = 300, Easing easing = Easing.InCubic) + public void Duck(int duration = 0, float duckVolumeTo = 0.25f, int? duckCutoffTo = 300, Easing easing = Easing.OutCubic) { Schedule(() => { @@ -297,7 +297,7 @@ namespace osu.Game.Overlays /// Cutoff frequency to drop `AudioFilter` to. Use `null` to skip filter effect. /// Duration of the ducking transition, in ms. /// Easing for the ducking transition. - public void TimedDuck(int delay, int unduckDuration = 500, Easing unduckEasing = Easing.InCubic, float duckVolumeTo = 0.25f, int? duckCutoffTo = 300, int duckDuration = 0, Easing duckEasing = Easing.InCubic) + public void TimedDuck(int delay, int unduckDuration = 500, Easing unduckEasing = Easing.InCubic, float duckVolumeTo = 0.25f, int? duckCutoffTo = 300, int duckDuration = 0, Easing duckEasing = Easing.OutCubic) { Duck(duckDuration, duckVolumeTo, duckCutoffTo, duckEasing); Scheduler.AddDelayed(() => Unduck(unduckDuration, unduckEasing), delay); From d29d114133e0ecd5426a15497234718edb6e0546 Mon Sep 17 00:00:00 2001 From: Jamie Taylor Date: Wed, 3 Jul 2024 13:52:20 +0900 Subject: [PATCH 1280/2493] Match prior ducking behaviour --- osu.Game/Overlays/Dialog/PopupDialogDangerousButton.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Dialog/PopupDialogDangerousButton.cs b/osu.Game/Overlays/Dialog/PopupDialogDangerousButton.cs index e01d4e2d66..cdde35c7d0 100644 --- a/osu.Game/Overlays/Dialog/PopupDialogDangerousButton.cs +++ b/osu.Game/Overlays/Dialog/PopupDialogDangerousButton.cs @@ -81,7 +81,7 @@ namespace osu.Game.Overlays.Dialog protected override void Confirm() { - musicController?.Duck(); + musicController?.Duck(100, 1f); confirmSample?.Play(); base.Confirm(); } From b6dc483fc11ea322ffee2a54b00983d8823bd3e2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 3 Jul 2024 14:23:31 +0900 Subject: [PATCH 1281/2493] Add missing change handler to ensure undo/redo works for break removal --- .../Components/Timeline/TimelineBreakDisplay.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBreakDisplay.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBreakDisplay.cs index d0f3a831f2..b9a66266bb 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBreakDisplay.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBreakDisplay.cs @@ -15,6 +15,9 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline [Resolved] private Timeline timeline { get; set; } = null!; + [Resolved] + private IEditorChangeHandler editorChangeHandler { get; set; } = null!; + /// /// The visible time/position range of the timeline. /// @@ -73,7 +76,12 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline Add(new TimelineBreak(breakPeriod) { - OnDeleted = b => breaks.Remove(b), + OnDeleted = b => + { + editorChangeHandler.BeginChange(); + breaks.Remove(b); + editorChangeHandler.EndChange(); + }, }); } } From abfcac746692671e3750af5b0cfcdd113cf40d4e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 3 Jul 2024 15:17:00 +0900 Subject: [PATCH 1282/2493] Fix nullability --- .../Edit/Compose/Components/Timeline/TimelineBreak.cs | 2 +- .../Compose/Components/Timeline/TimelineBreakDisplay.cs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBreak.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBreak.cs index 7f64436267..29030099c8 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBreak.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBreak.cs @@ -105,7 +105,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline background.FadeColour(IsHovered ? colours.Gray6 : colours.Gray5, 400, Easing.OutQuint); } - public MenuItem[]? ContextMenuItems => new MenuItem[] + public MenuItem[] ContextMenuItems => new MenuItem[] { new OsuMenuItem(CommonStrings.ButtonsDelete, MenuItemType.Destructive, () => OnDeleted?.Invoke(Break.Value)), }; diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBreakDisplay.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBreakDisplay.cs index b9a66266bb..eca44672f6 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBreakDisplay.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBreakDisplay.cs @@ -16,7 +16,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline private Timeline timeline { get; set; } = null!; [Resolved] - private IEditorChangeHandler editorChangeHandler { get; set; } = null!; + private IEditorChangeHandler? editorChangeHandler { get; set; } /// /// The visible time/position range of the timeline. @@ -78,9 +78,9 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline { OnDeleted = b => { - editorChangeHandler.BeginChange(); + editorChangeHandler?.BeginChange(); breaks.Remove(b); - editorChangeHandler.EndChange(); + editorChangeHandler?.EndChange(); }, }); } From 29c3ff06779a865df43f7ad6e79f1d6f35c6d8a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 3 Jul 2024 09:33:48 +0200 Subject: [PATCH 1283/2493] Enable NRT in `RulesetInputManager` --- osu.Game/Rulesets/UI/RulesetInputManager.cs | 22 ++++++++++----------- 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/osu.Game/Rulesets/UI/RulesetInputManager.cs b/osu.Game/Rulesets/UI/RulesetInputManager.cs index a08c3bab08..0fc39e6fcb 100644 --- a/osu.Game/Rulesets/UI/RulesetInputManager.cs +++ b/osu.Game/Rulesets/UI/RulesetInputManager.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 osu.Framework.Allocation; @@ -32,12 +30,12 @@ namespace osu.Game.Rulesets.UI public readonly KeyBindingContainer KeyBindingContainer; - [Resolved(CanBeNull = true)] - private ScoreProcessor scoreProcessor { get; set; } + [Resolved] + private ScoreProcessor? scoreProcessor { get; set; } - private ReplayRecorder recorder; + private ReplayRecorder? recorder; - public ReplayRecorder Recorder + public ReplayRecorder? Recorder { set { @@ -103,9 +101,9 @@ namespace osu.Game.Rulesets.UI #region IHasReplayHandler - private ReplayInputHandler replayInputHandler; + private ReplayInputHandler? replayInputHandler; - public ReplayInputHandler ReplayInputHandler + public ReplayInputHandler? ReplayInputHandler { get => replayInputHandler; set @@ -124,8 +122,8 @@ namespace osu.Game.Rulesets.UI #region Setting application (disables etc.) - private Bindable mouseDisabled; - private Bindable tapsDisabled; + private Bindable mouseDisabled = null!; + private Bindable tapsDisabled = null!; protected override bool Handle(UIEvent e) { @@ -227,9 +225,9 @@ namespace osu.Game.Rulesets.UI public class RulesetInputManagerInputState : InputState where T : struct { - public ReplayState LastReplayState; + public ReplayState? LastReplayState; - public RulesetInputManagerInputState(InputState state = null) + public RulesetInputManagerInputState(InputState state) : base(state) { } From 7f1d113454fcb63de64ffe470df929099fdcbd18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 3 Jul 2024 10:18:22 +0200 Subject: [PATCH 1284/2493] Add failing test coverage for replay detach --- .../TestSceneCatchReplayHandling.cs | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 osu.Game.Rulesets.Catch.Tests/TestSceneCatchReplayHandling.cs diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatchReplayHandling.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchReplayHandling.cs new file mode 100644 index 0000000000..361ecb7c4c --- /dev/null +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchReplayHandling.cs @@ -0,0 +1,34 @@ +// 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.Testing; +using osu.Game.Rulesets.Catch.Beatmaps; +using osu.Game.Rulesets.Catch.UI; +using osu.Game.Scoring; +using osu.Game.Tests.Visual; +using osuTK.Input; + +namespace osu.Game.Rulesets.Catch.Tests +{ + public partial class TestSceneCatchReplayHandling : OsuManualInputManagerTestScene + { + [Test] + public void TestReplayDetach() + { + DrawableCatchRuleset drawableRuleset = null!; + float catcherPosition = 0; + + AddStep("create drawable ruleset", () => Child = drawableRuleset = new DrawableCatchRuleset(new CatchRuleset(), new CatchBeatmap(), [])); + AddStep("attach replay", () => drawableRuleset.SetReplayScore(new Score())); + AddStep("store catcher position", () => catcherPosition = drawableRuleset.ChildrenOfType().Single().X); + AddStep("hold down left", () => InputManager.PressKey(Key.Left)); + AddAssert("catcher didn't move", () => drawableRuleset.ChildrenOfType().Single().X, () => Is.EqualTo(catcherPosition)); + + AddStep("detach replay", () => drawableRuleset.SetReplayScore(null)); + AddUntilStep("catcher moved", () => drawableRuleset.ChildrenOfType().Single().X, () => Is.Not.EqualTo(catcherPosition)); + AddStep("release left", () => InputManager.ReleaseKey(Key.Left)); + } + } +} From 294aa09c413b4e5425ca92280842823ac1d2678d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 3 Jul 2024 10:18:45 +0200 Subject: [PATCH 1285/2493] Clear pressed keys and last replay frame when detaching replay from ruleset input manager --- osu.Game/Rulesets/UI/RulesetInputManager.cs | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/UI/RulesetInputManager.cs b/osu.Game/Rulesets/UI/RulesetInputManager.cs index 0fc39e6fcb..0bd90a6635 100644 --- a/osu.Game/Rulesets/UI/RulesetInputManager.cs +++ b/osu.Game/Rulesets/UI/RulesetInputManager.cs @@ -10,6 +10,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Input; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; +using osu.Framework.Input.StateChanges; using osu.Framework.Input.StateChanges.Events; using osu.Framework.Input.States; using osu.Game.Configuration; @@ -108,7 +109,11 @@ namespace osu.Game.Rulesets.UI get => replayInputHandler; set { - if (replayInputHandler != null) RemoveHandler(replayInputHandler); + if (replayInputHandler != null) + { + RemoveHandler(replayInputHandler); + new ReplayStateReset().Apply(CurrentState, this); + } replayInputHandler = value; UseParentInput = replayInputHandler == null; @@ -220,6 +225,19 @@ namespace osu.Game.Rulesets.UI RealmKeyBindingStore.ClearDuplicateBindings(KeyBindings); } } + + private class ReplayStateReset : IInput + { + public void Apply(InputState state, IInputStateChangeHandler handler) + { + if (!(state is RulesetInputManagerInputState inputState)) + throw new InvalidOperationException($"{nameof(ReplayState)} should only be applied to a {nameof(RulesetInputManagerInputState)}"); + + inputState.LastReplayState = null; + + handler.HandleInputStateChange(new ReplayStateChangeEvent(state, this, inputState.LastReplayState?.PressedActions.ToArray() ?? [], [])); + } + } } public class RulesetInputManagerInputState : InputState From 84c7d34b770ae8553c89f7c13722932b22cf7279 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 3 Jul 2024 10:32:08 +0200 Subject: [PATCH 1286/2493] Fix user-pressed keys remaining pressed whtn autoplay is turned on --- osu.Game/Rulesets/UI/RulesetInputManager.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/osu.Game/Rulesets/UI/RulesetInputManager.cs b/osu.Game/Rulesets/UI/RulesetInputManager.cs index 0bd90a6635..a242896ff8 100644 --- a/osu.Game/Rulesets/UI/RulesetInputManager.cs +++ b/osu.Game/Rulesets/UI/RulesetInputManager.cs @@ -109,11 +109,21 @@ namespace osu.Game.Rulesets.UI get => replayInputHandler; set { + if (replayInputHandler == value) + return; + if (replayInputHandler != null) { RemoveHandler(replayInputHandler); + // ensures that all replay keys are released, and that the last replay state is correctly cleared new ReplayStateReset().Apply(CurrentState, this); } + else + { + // ensures that all user-pressed keys are released, so that the replay handler may trigger them itself + // setting `UseParentInput` will only sync releases (https://github.com/ppy/osu-framework/blob/45cd7c7c702c081334fce41e7771b9dc6481b28d/osu.Framework/Input/PassThroughInputManager.cs#L179-L182) + SyncInputState(CreateInitialState()); + } replayInputHandler = value; UseParentInput = replayInputHandler == null; From 6fda0db9bac3c30b7d5c2fcaf15a1a4bafac09aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 3 Jul 2024 11:03:52 +0200 Subject: [PATCH 1287/2493] Fix test --- .../TestSceneModCustomisationPanel.cs | 28 +++++++++++++++---- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModCustomisationPanel.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModCustomisationPanel.cs index 360c28acfa..9c0d185892 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModCustomisationPanel.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModCustomisationPanel.cs @@ -18,6 +18,8 @@ namespace osu.Game.Tests.Visual.UserInterface [Cached] private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine); + private ModCustomisationPanel panel = null!; + [SetUp] public void SetUp() => Schedule(() => { @@ -25,7 +27,7 @@ namespace osu.Game.Tests.Visual.UserInterface { RelativeSizeAxes = Axes.Both, Padding = new MarginPadding(20f), - Child = new ModCustomisationPanel + Child = panel = new ModCustomisationPanel { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, @@ -39,10 +41,26 @@ namespace osu.Game.Tests.Visual.UserInterface [Test] public void TestDisplay() { - AddStep("set DT", () => SelectedMods.Value = new[] { new OsuModDoubleTime() }); - AddStep("set DA", () => SelectedMods.Value = new Mod[] { new OsuModDifficultyAdjust() }); - AddStep("set FL+WU+DA+AD", () => SelectedMods.Value = new Mod[] { new OsuModFlashlight(), new ModWindUp(), new OsuModDifficultyAdjust(), new OsuModApproachDifferent() }); - AddStep("set empty", () => SelectedMods.Value = Array.Empty()); + AddStep("set DT", () => + { + SelectedMods.Value = new[] { new OsuModDoubleTime() }; + panel.Enabled.Value = panel.Expanded.Value = true; + }); + AddStep("set DA", () => + { + SelectedMods.Value = new Mod[] { new OsuModDifficultyAdjust() }; + panel.Enabled.Value = panel.Expanded.Value = true; + }); + AddStep("set FL+WU+DA+AD", () => + { + SelectedMods.Value = new Mod[] { new OsuModFlashlight(), new ModWindUp(), new OsuModDifficultyAdjust(), new OsuModApproachDifferent() }; + panel.Enabled.Value = panel.Expanded.Value = true; + }); + AddStep("set empty", () => + { + SelectedMods.Value = Array.Empty(); + panel.Enabled.Value = panel.Expanded.Value = false; + }); } } } From b15028a918ececff597d694c3315d732cf784cdc Mon Sep 17 00:00:00 2001 From: OliBomby Date: Wed, 3 Jul 2024 12:36:12 +0200 Subject: [PATCH 1288/2493] fixes --- osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs index 8d80ed651e..7720afe60a 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; +using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.UserInterface; @@ -23,18 +24,9 @@ namespace osu.Game.Rulesets.Osu.Edit { public partial class OsuSelectionHandler : EditorSelectionHandler { - [Resolved(CanBeNull = true)] - private IDistanceSnapProvider? snapProvider { get; set; } - [Resolved] private OsuGridToolboxGroup gridToolbox { get; set; } = null!; - /// - /// During a transform, the initial path types of a single selected slider are stored so they - /// can be maintained throughout the operation. - /// - private List? referencePathTypes; - protected override void OnSelectionChanged() { base.OnSelectionChanged(); From 5f8512896e0eee0eda8ec0c9410704422d475182 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Wed, 3 Jul 2024 12:40:22 +0200 Subject: [PATCH 1289/2493] use grid origin in scale tool --- osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs | 8 +++++--- osu.Game.Rulesets.Osu/Edit/TransformToolboxGroup.cs | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs b/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs index a299eebbce..65a07e2e2f 100644 --- a/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs +++ b/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs @@ -10,7 +10,6 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterfaceV2; -using osu.Game.Rulesets.Osu.UI; using osu.Game.Screens.Edit.Components.RadioButtons; using osuTK; @@ -20,6 +19,8 @@ namespace osu.Game.Rulesets.Osu.Edit { private readonly OsuSelectionScaleHandler scaleHandler; + private readonly OsuGridToolboxGroup gridToolbox; + private readonly Bindable scaleInfo = new Bindable(new PreciseScaleInfo(1, ScaleOrigin.PlayfieldCentre, true, true)); private SliderWithTextBoxInput scaleInput = null!; @@ -32,9 +33,10 @@ namespace osu.Game.Rulesets.Osu.Edit private OsuCheckbox xCheckBox = null!; private OsuCheckbox yCheckBox = null!; - public PreciseScalePopover(OsuSelectionScaleHandler scaleHandler) + public PreciseScalePopover(OsuSelectionScaleHandler scaleHandler, OsuGridToolboxGroup gridToolbox) { this.scaleHandler = scaleHandler; + this.gridToolbox = gridToolbox; AllowableAnchors = new[] { Anchor.CentreLeft, Anchor.CentreRight }; } @@ -179,7 +181,7 @@ namespace osu.Game.Rulesets.Osu.Edit updateAxisCheckBoxesEnabled(); } - private Vector2? getOriginPosition(PreciseScaleInfo scale) => scale.Origin == ScaleOrigin.PlayfieldCentre ? OsuPlayfield.BASE_SIZE / 2 : null; + private Vector2? getOriginPosition(PreciseScaleInfo scale) => scale.Origin == ScaleOrigin.PlayfieldCentre ? gridToolbox.StartPosition.Value : null; private void setAxis(bool x, bool y) { diff --git a/osu.Game.Rulesets.Osu/Edit/TransformToolboxGroup.cs b/osu.Game.Rulesets.Osu/Edit/TransformToolboxGroup.cs index 2e4d7e8b91..a41412cbe3 100644 --- a/osu.Game.Rulesets.Osu/Edit/TransformToolboxGroup.cs +++ b/osu.Game.Rulesets.Osu/Edit/TransformToolboxGroup.cs @@ -49,7 +49,7 @@ namespace osu.Game.Rulesets.Osu.Edit () => new PreciseRotationPopover(RotationHandler, GridToolbox)), scaleButton = new EditorToolButton("Scale", () => new SpriteIcon { Icon = FontAwesome.Solid.ArrowsAlt }, - () => new PreciseScalePopover(ScaleHandler)) + () => new PreciseScalePopover(ScaleHandler, GridToolbox)) } }; } From 56cdd83451ac4fbc4132b92d4cdca23b90b5ce26 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 3 Jul 2024 20:42:12 +0900 Subject: [PATCH 1290/2493] Adjust padding and round corners of hover layer --- .../Screens/Edit/Components/TimeInfoContainer.cs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Edit/Components/TimeInfoContainer.cs b/osu.Game/Screens/Edit/Components/TimeInfoContainer.cs index 9365402c1c..7c03198ec0 100644 --- a/osu.Game/Screens/Edit/Components/TimeInfoContainer.cs +++ b/osu.Game/Screens/Edit/Components/TimeInfoContainer.cs @@ -88,9 +88,18 @@ namespace osu.Game.Screens.Edit.Components Padding = new MarginPadding { Top = 5, - Horizontal = -5 + Horizontal = -2 + }, + Child = new Container + { + RelativeSizeAxes = Axes.Both, + CornerRadius = 5, + Masking = true, + Children = new Drawable[] + { + new Box { RelativeSizeAxes = Axes.Both, }, + } }, - Child = new Box { RelativeSizeAxes = Axes.Both, }, Alpha = 0, }, trackTimer = new OsuSpriteText From 0ab13e44869f09415d1e96dc1d8631080a94f5f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 3 Jul 2024 14:37:07 +0200 Subject: [PATCH 1291/2493] Use alternative method of releasing user-pressed keys when activating autoplay --- osu.Game/Rulesets/UI/RulesetInputManager.cs | 25 ++++++++++++--------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/osu.Game/Rulesets/UI/RulesetInputManager.cs b/osu.Game/Rulesets/UI/RulesetInputManager.cs index a242896ff8..31c7c34572 100644 --- a/osu.Game/Rulesets/UI/RulesetInputManager.cs +++ b/osu.Game/Rulesets/UI/RulesetInputManager.cs @@ -20,6 +20,7 @@ using osu.Game.Input.Handlers; using osu.Game.Rulesets.Scoring; using osu.Game.Screens.Play.HUD; using osu.Game.Screens.Play.HUD.ClicksPerSecond; +using osuTK; using static osu.Game.Input.Handlers.ReplayInputHandler; namespace osu.Game.Rulesets.UI @@ -113,17 +114,12 @@ namespace osu.Game.Rulesets.UI return; if (replayInputHandler != null) - { RemoveHandler(replayInputHandler); - // ensures that all replay keys are released, and that the last replay state is correctly cleared - new ReplayStateReset().Apply(CurrentState, this); - } - else - { - // ensures that all user-pressed keys are released, so that the replay handler may trigger them itself - // setting `UseParentInput` will only sync releases (https://github.com/ppy/osu-framework/blob/45cd7c7c702c081334fce41e7771b9dc6481b28d/osu.Framework/Input/PassThroughInputManager.cs#L179-L182) - SyncInputState(CreateInitialState()); - } + + // ensures that all replay keys are released, that the last replay state is correctly cleared, + // and that all user-pressed keys are released, so that the replay handler may trigger them itself + // setting `UseParentInput` will only sync releases (https://github.com/ppy/osu-framework/blob/17d65f476d51cc5f2aaea818534f8fbac47e5fe6/osu.Framework/Input/PassThroughInputManager.cs#L179-L182) + new ReplayStateReset().Apply(CurrentState, this); replayInputHandler = value; UseParentInput = replayInputHandler == null; @@ -243,9 +239,16 @@ namespace osu.Game.Rulesets.UI if (!(state is RulesetInputManagerInputState inputState)) throw new InvalidOperationException($"{nameof(ReplayState)} should only be applied to a {nameof(RulesetInputManagerInputState)}"); - inputState.LastReplayState = null; + new MouseButtonInput([], state.Mouse.Buttons).Apply(state, handler); + new KeyboardKeyInput([], state.Keyboard.Keys).Apply(state, handler); + new TouchInput(Enum.GetValues().Select(s => new Touch(s, Vector2.Zero)), false).Apply(state, handler); + new JoystickButtonInput([], state.Joystick.Buttons).Apply(state, handler); + new MidiKeyInput(new MidiState(), state.Midi).Apply(state, handler); + new TabletPenButtonInput([], state.Tablet.PenButtons).Apply(state, handler); + new TabletAuxiliaryButtonInput([], state.Tablet.AuxiliaryButtons).Apply(state, handler); handler.HandleInputStateChange(new ReplayStateChangeEvent(state, this, inputState.LastReplayState?.PressedActions.ToArray() ?? [], [])); + inputState.LastReplayState = null; } } } From 901fec65efd57b347948e388c31ce71c67fe0662 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 3 Jul 2024 15:49:17 +0200 Subject: [PATCH 1292/2493] Address code quality issues --- .../Rulesets/Edit/Checks/CheckTitleMarkers.cs | 27 ++++++++++--------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/osu.Game/Rulesets/Edit/Checks/CheckTitleMarkers.cs b/osu.Game/Rulesets/Edit/Checks/CheckTitleMarkers.cs index 2471d175ae..d6fd771e9c 100644 --- a/osu.Game/Rulesets/Edit/Checks/CheckTitleMarkers.cs +++ b/osu.Game/Rulesets/Edit/Checks/CheckTitleMarkers.cs @@ -16,15 +16,16 @@ namespace osu.Game.Rulesets.Edit.Checks new IssueTemplateIncorrectMarker(this), }; - private IEnumerable markerChecks = [ - new MarkerCheck("(TV Size)", @"(?i)(tv (size|ver))"), - new MarkerCheck("(Game Ver.)", @"(?i)(game (size|ver))"), - new MarkerCheck("(Short Ver.)", @"(?i)(short (size|ver))"), - new MarkerCheck("(Cut Ver.)", @"(?i)(? markerChecks = + [ + new MarkerCheck(@"(TV Size)", @"(?i)(tv (size|ver))"), + new MarkerCheck(@"(Game Ver.)", @"(?i)(game (size|ver))"), + new MarkerCheck(@"(Short Ver.)", @"(?i)(short (size|ver))"), + new MarkerCheck(@"(Cut Ver.)", @"(?i)(? Run(BeatmapVerifierContext context) @@ -50,9 +51,9 @@ namespace osu.Game.Rulesets.Edit.Checks private class MarkerCheck { - public string CorrectMarkerFormat; - public Regex ExactRegex; - public Regex AnyRegex; + public readonly string CorrectMarkerFormat; + public readonly Regex ExactRegex; + public readonly Regex AnyRegex; public MarkerCheck(string exact, string anyRegex) { @@ -72,4 +73,4 @@ namespace osu.Game.Rulesets.Edit.Checks public Issue Create(string titleField, string correctMarkerFormat) => new Issue(this, titleField, correctMarkerFormat); } } -} \ No newline at end of file +} From 32b3d3d7dfefdffcef5698acd47df4e964ba6a5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 3 Jul 2024 15:49:21 +0200 Subject: [PATCH 1293/2493] Compile regexes for speed --- osu.Game/Rulesets/Edit/Checks/CheckTitleMarkers.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/Edit/Checks/CheckTitleMarkers.cs b/osu.Game/Rulesets/Edit/Checks/CheckTitleMarkers.cs index d6fd771e9c..fb0203fe19 100644 --- a/osu.Game/Rulesets/Edit/Checks/CheckTitleMarkers.cs +++ b/osu.Game/Rulesets/Edit/Checks/CheckTitleMarkers.cs @@ -58,8 +58,8 @@ namespace osu.Game.Rulesets.Edit.Checks public MarkerCheck(string exact, string anyRegex) { CorrectMarkerFormat = exact; - ExactRegex = new Regex(Regex.Escape(exact)); - AnyRegex = new Regex(anyRegex); + ExactRegex = new Regex(Regex.Escape(exact), RegexOptions.Compiled); + AnyRegex = new Regex(anyRegex, RegexOptions.Compiled); } } From 5696e85b68d3a343ea89a4b56d6d13d54fd11c05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 3 Jul 2024 15:50:36 +0200 Subject: [PATCH 1294/2493] Adjust conditionals The fact that checking the unicode title was gated behind a `hasRomanisedTitle` guard was breaking my brain. --- osu.Game/Rulesets/Edit/Checks/CheckTitleMarkers.cs | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/osu.Game/Rulesets/Edit/Checks/CheckTitleMarkers.cs b/osu.Game/Rulesets/Edit/Checks/CheckTitleMarkers.cs index fb0203fe19..b6339851ef 100644 --- a/osu.Game/Rulesets/Edit/Checks/CheckTitleMarkers.cs +++ b/osu.Game/Rulesets/Edit/Checks/CheckTitleMarkers.cs @@ -37,15 +37,11 @@ namespace osu.Game.Rulesets.Edit.Checks { bool hasRomanisedTitle = unicodeTitle != romanisedTitle; - if (check.AnyRegex.IsMatch(romanisedTitle) && !check.ExactRegex.IsMatch(romanisedTitle)) - { - yield return new IssueTemplateIncorrectMarker(this).Create(hasRomanisedTitle ? "Romanised title" : "Title", check.CorrectMarkerFormat); - } - - if (hasRomanisedTitle && check.AnyRegex.IsMatch(unicodeTitle) && !check.ExactRegex.IsMatch(unicodeTitle)) - { + if (check.AnyRegex.IsMatch(unicodeTitle) && !check.ExactRegex.IsMatch(unicodeTitle)) yield return new IssueTemplateIncorrectMarker(this).Create("Title", check.CorrectMarkerFormat); - } + + if (hasRomanisedTitle && check.AnyRegex.IsMatch(romanisedTitle) && !check.ExactRegex.IsMatch(romanisedTitle)) + yield return new IssueTemplateIncorrectMarker(this).Create("Romanised title", check.CorrectMarkerFormat); } } From bcb479d4f70d7f268dc54b4f6265ad2b40496507 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 3 Jul 2024 16:13:30 +0200 Subject: [PATCH 1295/2493] Use one less regex --- osu.Game/Rulesets/Edit/Checks/CheckTitleMarkers.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/osu.Game/Rulesets/Edit/Checks/CheckTitleMarkers.cs b/osu.Game/Rulesets/Edit/Checks/CheckTitleMarkers.cs index b6339851ef..00482a72fc 100644 --- a/osu.Game/Rulesets/Edit/Checks/CheckTitleMarkers.cs +++ b/osu.Game/Rulesets/Edit/Checks/CheckTitleMarkers.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.Text.RegularExpressions; using osu.Game.Rulesets.Edit.Checks.Components; @@ -37,10 +38,10 @@ namespace osu.Game.Rulesets.Edit.Checks { bool hasRomanisedTitle = unicodeTitle != romanisedTitle; - if (check.AnyRegex.IsMatch(unicodeTitle) && !check.ExactRegex.IsMatch(unicodeTitle)) + if (check.AnyRegex.IsMatch(unicodeTitle) && !unicodeTitle.Contains(check.CorrectMarkerFormat, StringComparison.Ordinal)) yield return new IssueTemplateIncorrectMarker(this).Create("Title", check.CorrectMarkerFormat); - if (hasRomanisedTitle && check.AnyRegex.IsMatch(romanisedTitle) && !check.ExactRegex.IsMatch(romanisedTitle)) + if (hasRomanisedTitle && check.AnyRegex.IsMatch(romanisedTitle) && !romanisedTitle.Contains(check.CorrectMarkerFormat, StringComparison.Ordinal)) yield return new IssueTemplateIncorrectMarker(this).Create("Romanised title", check.CorrectMarkerFormat); } } @@ -48,13 +49,11 @@ namespace osu.Game.Rulesets.Edit.Checks private class MarkerCheck { public readonly string CorrectMarkerFormat; - public readonly Regex ExactRegex; public readonly Regex AnyRegex; public MarkerCheck(string exact, string anyRegex) { CorrectMarkerFormat = exact; - ExactRegex = new Regex(Regex.Escape(exact), RegexOptions.Compiled); AnyRegex = new Regex(anyRegex, RegexOptions.Compiled); } } From 00a0058fc709320e3dc1bea4053ee3fb30c94397 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 3 Jul 2024 16:13:47 +0200 Subject: [PATCH 1296/2493] Fix typo --- osu.Game/Rulesets/Edit/Checks/CheckTitleMarkers.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Edit/Checks/CheckTitleMarkers.cs b/osu.Game/Rulesets/Edit/Checks/CheckTitleMarkers.cs index 00482a72fc..9c702ad58a 100644 --- a/osu.Game/Rulesets/Edit/Checks/CheckTitleMarkers.cs +++ b/osu.Game/Rulesets/Edit/Checks/CheckTitleMarkers.cs @@ -61,7 +61,7 @@ namespace osu.Game.Rulesets.Edit.Checks public class IssueTemplateIncorrectMarker : IssueTemplate { public IssueTemplateIncorrectMarker(ICheck check) - : base(check, IssueType.Problem, "{0} field has a incorrect format of marker {1}") + : base(check, IssueType.Problem, "{0} field has an incorrect format of marker {1}") { } From d0715c5f12a98e9a31465600d1cc69bb0efe1df2 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Wed, 3 Jul 2024 16:23:19 +0200 Subject: [PATCH 1297/2493] scale along rotated axis --- .../Edit/OsuSelectionScaleHandler.cs | 104 ++++++++++++++---- .../Edit/PreciseScalePopover.cs | 8 +- .../Editing/TestSceneComposeSelectBox.cs | 2 +- .../SkinEditor/SkinSelectionScaleHandler.cs | 2 +- .../Components/SelectionScaleHandler.cs | 8 +- osu.Game/Utils/GeometryUtils.cs | 4 +- 6 files changed, 97 insertions(+), 31 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs index f4fd48f183..cfcf90e5f5 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs @@ -86,7 +86,7 @@ namespace osu.Game.Rulesets.Osu.Edit defaultOrigin = OriginalSurroundingQuad.Value.Centre; } - public override void Update(Vector2 scale, Vector2? origin = null, Axes adjustAxis = Axes.Both) + public override void Update(Vector2 scale, Vector2? origin = null, Axes adjustAxis = Axes.Both, float axisRotation = 0) { if (!OperationInProgress.Value) throw new InvalidOperationException($"Cannot {nameof(Update)} a scale operation without calling {nameof(Begin)} first!"); @@ -94,6 +94,7 @@ namespace osu.Game.Rulesets.Osu.Edit Debug.Assert(objectsInScale != null && defaultOrigin != null && OriginalSurroundingQuad != null); Vector2 actualOrigin = origin ?? defaultOrigin.Value; + scale = clampScaleToAdjustAxis(scale, adjustAxis); // for the time being, allow resizing of slider paths only if the slider is // the only hit object selected. with a group selection, it's likely the user @@ -102,15 +103,15 @@ namespace osu.Game.Rulesets.Osu.Edit { var originalInfo = objectsInScale[slider]; Debug.Assert(originalInfo.PathControlPointPositions != null && originalInfo.PathControlPointTypes != null); - scaleSlider(slider, scale, originalInfo.PathControlPointPositions, originalInfo.PathControlPointTypes); + scaleSlider(slider, scale, originalInfo.PathControlPointPositions, originalInfo.PathControlPointTypes, axisRotation); } else { - scale = ClampScaleToPlayfieldBounds(scale, actualOrigin); + scale = ClampScaleToPlayfieldBounds(scale, actualOrigin, adjustAxis, axisRotation); foreach (var (ho, originalState) in objectsInScale) { - ho.Position = GeometryUtils.GetScaledPosition(scale, actualOrigin, originalState.Position); + ho.Position = GeometryUtils.GetScaledPosition(scale, actualOrigin, originalState.Position, axisRotation); } } @@ -134,14 +135,34 @@ namespace osu.Game.Rulesets.Osu.Edit private IEnumerable selectedMovableObjects => selectedItems.Cast() .Where(h => h is not Spinner); - private void scaleSlider(Slider slider, Vector2 scale, Vector2[] originalPathPositions, PathType?[] originalPathTypes) + private Vector2 clampScaleToAdjustAxis(Vector2 scale, Axes adjustAxis) + { + switch (adjustAxis) + { + case Axes.Y: + scale.X = 1; + break; + + case Axes.X: + scale.Y = 1; + break; + + case Axes.None: + scale = Vector2.One; + break; + } + + return scale; + } + + private void scaleSlider(Slider slider, Vector2 scale, Vector2[] originalPathPositions, PathType?[] originalPathTypes, float axisRotation = 0) { scale = Vector2.ComponentMax(scale, new Vector2(Precision.FLOAT_EPSILON)); // Maintain the path types in case they were defaulted to bezier at some point during scaling for (int i = 0; i < slider.Path.ControlPoints.Count; i++) { - slider.Path.ControlPoints[i].Position = originalPathPositions[i] * scale; + slider.Path.ControlPoints[i].Position = GeometryUtils.GetScaledPosition(scale, Vector2.Zero, originalPathPositions[i], axisRotation); slider.Path.ControlPoints[i].Type = originalPathTypes[i]; } @@ -176,11 +197,13 @@ namespace osu.Game.Rulesets.Osu.Edit /// /// The origin from which the scale operation is performed /// The scale to be clamped + /// The axes to adjust the scale in. + /// The rotation of the axes in degrees /// The clamped scale vector - public Vector2 ClampScaleToPlayfieldBounds(Vector2 scale, Vector2? origin = null) + public Vector2 ClampScaleToPlayfieldBounds(Vector2 scale, Vector2? origin = null, Axes adjustAxis = Axes.Both, float axisRotation = 0) { //todo: this is not always correct for selections involving sliders. This approximation assumes each point is scaled independently, but sliderends move with the sliderhead. - if (objectsInScale == null) + if (objectsInScale == null || adjustAxis == Axes.None) return scale; Debug.Assert(defaultOrigin != null && OriginalSurroundingQuad != null); @@ -188,24 +211,63 @@ namespace osu.Game.Rulesets.Osu.Edit if (objectsInScale.Count == 1 && objectsInScale.First().Key is Slider slider) origin = slider.Position; + scale = clampScaleToAdjustAxis(scale, adjustAxis); Vector2 actualOrigin = origin ?? defaultOrigin.Value; var selectionQuad = OriginalSurroundingQuad.Value; - var tl1 = Vector2.Divide(-actualOrigin, selectionQuad.TopLeft - actualOrigin); - var tl2 = Vector2.Divide(OsuPlayfield.BASE_SIZE - actualOrigin, selectionQuad.TopLeft - actualOrigin); - var br1 = Vector2.Divide(-actualOrigin, selectionQuad.BottomRight - actualOrigin); - var br2 = Vector2.Divide(OsuPlayfield.BASE_SIZE - actualOrigin, selectionQuad.BottomRight - actualOrigin); - - if (!Precision.AlmostEquals(selectionQuad.TopLeft.X - actualOrigin.X, 0)) - scale.X = selectionQuad.TopLeft.X - actualOrigin.X < 0 ? MathHelper.Clamp(scale.X, tl2.X, tl1.X) : MathHelper.Clamp(scale.X, tl1.X, tl2.X); - if (!Precision.AlmostEquals(selectionQuad.TopLeft.Y - actualOrigin.Y, 0)) - scale.Y = selectionQuad.TopLeft.Y - actualOrigin.Y < 0 ? MathHelper.Clamp(scale.Y, tl2.Y, tl1.Y) : MathHelper.Clamp(scale.Y, tl1.Y, tl2.Y); - if (!Precision.AlmostEquals(selectionQuad.BottomRight.X - actualOrigin.X, 0)) - scale.X = selectionQuad.BottomRight.X - actualOrigin.X < 0 ? MathHelper.Clamp(scale.X, br2.X, br1.X) : MathHelper.Clamp(scale.X, br1.X, br2.X); - if (!Precision.AlmostEquals(selectionQuad.BottomRight.Y - actualOrigin.Y, 0)) - scale.Y = selectionQuad.BottomRight.Y - actualOrigin.Y < 0 ? MathHelper.Clamp(scale.Y, br2.Y, br1.Y) : MathHelper.Clamp(scale.Y, br1.Y, br2.Y); + scale = clampToBound(scale, selectionQuad.BottomRight, OsuPlayfield.BASE_SIZE.X, Axes.X); + scale = clampToBound(scale, selectionQuad.BottomRight, OsuPlayfield.BASE_SIZE.Y, Axes.Y); + scale = clampToBound(scale, selectionQuad.TopLeft, 0, Axes.X); + scale = clampToBound(scale, selectionQuad.TopLeft, 0, Axes.Y); return Vector2.ComponentMax(scale, new Vector2(Precision.FLOAT_EPSILON)); + + Vector2 clampToBound(Vector2 s, Vector2 p, float bound, Axes axis) + { + float px = p.X - actualOrigin.X; + float py = p.Y - actualOrigin.Y; + float c = axis == Axes.X ? bound - actualOrigin.X : bound - actualOrigin.Y; + float cos = MathF.Cos(float.DegreesToRadians(-axisRotation)); + float sin = MathF.Sin(float.DegreesToRadians(-axisRotation)); + float a, b; + + if (axis == Axes.X) + { + a = cos * cos * px - sin * cos * py; + b = sin * sin * px + sin * cos * py; + } + else + { + a = -sin * cos * px + sin * sin * py; + b = sin * cos * px + cos * cos * py; + } + + switch (adjustAxis) + { + case Axes.X: + if (Precision.AlmostEquals(a, 0) || (c - b) / a < 0) + break; + + s.X = MathF.Min(scale.X, (c - b) / a); + break; + + case Axes.Y: + if (Precision.AlmostEquals(b, 0) || (c - a) / b < 0) + break; + + s.Y = MathF.Min(scale.Y, (c - a) / b); + break; + + case Axes.Both: + if (Precision.AlmostEquals(a + b, 0) || c / (a * s.X + b * s.Y) < 0) + break; + + s = Vector2.ComponentMin(s, s * c / (a * s.X + b * s.Y)); + break; + } + + return s; + } } private void moveSelectionInBounds() diff --git a/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs b/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs index 65a07e2e2f..a1907a2fd5 100644 --- a/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs +++ b/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs @@ -130,8 +130,8 @@ namespace osu.Game.Rulesets.Osu.Edit scaleInfo.BindValueChanged(scale => { - var newScale = new Vector2(scale.NewValue.XAxis ? scale.NewValue.Scale : 1, scale.NewValue.YAxis ? scale.NewValue.Scale : 1); - scaleHandler.Update(newScale, getOriginPosition(scale.NewValue)); + var newScale = new Vector2(scale.NewValue.Scale, scale.NewValue.Scale); + scaleHandler.Update(newScale, getOriginPosition(scale.NewValue), getAdjustAxis(scale.NewValue), gridToolbox.GridLinesRotation.Value); }); } @@ -164,7 +164,7 @@ namespace osu.Game.Rulesets.Osu.Edit return; const float max_scale = 10; - var scale = scaleHandler.ClampScaleToPlayfieldBounds(new Vector2(max_scale), getOriginPosition(scaleInfo.Value)); + var scale = scaleHandler.ClampScaleToPlayfieldBounds(new Vector2(max_scale), getOriginPosition(scaleInfo.Value), getAdjustAxis(scaleInfo.Value), gridToolbox.GridLinesRotation.Value); if (!scaleInfo.Value.XAxis) scale.X = max_scale; @@ -183,6 +183,8 @@ namespace osu.Game.Rulesets.Osu.Edit private Vector2? getOriginPosition(PreciseScaleInfo scale) => scale.Origin == ScaleOrigin.PlayfieldCentre ? gridToolbox.StartPosition.Value : null; + private Axes getAdjustAxis(PreciseScaleInfo scale) => scale.XAxis ? scale.YAxis ? Axes.Both : Axes.X : Axes.Y; + private void setAxis(bool x, bool y) { scaleInfo.Value = scaleInfo.Value with { XAxis = x, YAxis = y }; diff --git a/osu.Game.Tests/Visual/Editing/TestSceneComposeSelectBox.cs b/osu.Game.Tests/Visual/Editing/TestSceneComposeSelectBox.cs index 28763051e3..30f397f518 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneComposeSelectBox.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneComposeSelectBox.cs @@ -134,7 +134,7 @@ namespace osu.Game.Tests.Visual.Editing OriginalSurroundingQuad = new Quad(targetContainer!.X, targetContainer.Y, targetContainer.Width, targetContainer.Height); } - public override void Update(Vector2 scale, Vector2? origin = null, Axes adjustAxis = Axes.Both) + public override void Update(Vector2 scale, Vector2? origin = null, Axes adjustAxis = Axes.Both, float axisRotation = 0) { if (targetContainer == null) throw new InvalidOperationException($"Cannot {nameof(Update)} a scale operation without calling {nameof(Begin)} first!"); diff --git a/osu.Game/Overlays/SkinEditor/SkinSelectionScaleHandler.cs b/osu.Game/Overlays/SkinEditor/SkinSelectionScaleHandler.cs index 4bfa7fba81..977aaade99 100644 --- a/osu.Game/Overlays/SkinEditor/SkinSelectionScaleHandler.cs +++ b/osu.Game/Overlays/SkinEditor/SkinSelectionScaleHandler.cs @@ -73,7 +73,7 @@ namespace osu.Game.Overlays.SkinEditor isFlippedY = false; } - public override void Update(Vector2 scale, Vector2? origin = null, Axes adjustAxis = Axes.Both) + public override void Update(Vector2 scale, Vector2? origin = null, Axes adjustAxis = Axes.Both, float axisRotation = 0) { if (objectsInScale == null) throw new InvalidOperationException($"Cannot {nameof(Update)} a scale operation without calling {nameof(Begin)} first!"); diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionScaleHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionScaleHandler.cs index c91362219c..177de9df33 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionScaleHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionScaleHandler.cs @@ -52,10 +52,11 @@ namespace osu.Game.Screens.Edit.Compose.Components /// If the default value is supplied, a sane implementation-defined default will be used. /// /// The axes to adjust the scale in. - public void ScaleSelection(Vector2 scale, Vector2? origin = null, Axes adjustAxis = Axes.Both) + /// The rotation of the axes in degrees. + public void ScaleSelection(Vector2 scale, Vector2? origin = null, Axes adjustAxis = Axes.Both, float axisRotation = 0) { Begin(); - Update(scale, origin, adjustAxis); + Update(scale, origin, adjustAxis, axisRotation); Commit(); } @@ -91,7 +92,8 @@ namespace osu.Game.Screens.Edit.Compose.Components /// If the default value is supplied, a sane implementation-defined default will be used. /// /// The axes to adjust the scale in. - public virtual void Update(Vector2 scale, Vector2? origin = null, Axes adjustAxis = Axes.Both) + /// The rotation of the axes in degrees. + public virtual void Update(Vector2 scale, Vector2? origin = null, Axes adjustAxis = Axes.Both, float axisRotation = 0) { } diff --git a/osu.Game/Utils/GeometryUtils.cs b/osu.Game/Utils/GeometryUtils.cs index bf1addf6c8..f6e7e81007 100644 --- a/osu.Game/Utils/GeometryUtils.cs +++ b/osu.Game/Utils/GeometryUtils.cs @@ -104,9 +104,9 @@ namespace osu.Game.Utils /// Given a scale multiplier, an origin, and a position, /// will return the scaled position in screen space coordinates. /// - public static Vector2 GetScaledPosition(Vector2 scale, Vector2 origin, Vector2 position) + public static Vector2 GetScaledPosition(Vector2 scale, Vector2 origin, Vector2 position, float axisRotation = 0) { - return origin + (position - origin) * scale; + return origin + RotateVector(RotateVector(position - origin, axisRotation) * scale, -axisRotation); } /// From 979a5e9f3e5de341bcfe785ca7978f5449e0fd78 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Wed, 3 Jul 2024 16:41:41 +0200 Subject: [PATCH 1298/2493] simplify code --- .../Edit/OsuSelectionScaleHandler.cs | 46 ++++++------------- 1 file changed, 13 insertions(+), 33 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs index cfcf90e5f5..2cf5a604ed 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs @@ -194,7 +194,7 @@ namespace osu.Game.Rulesets.Osu.Edit /// /// Clamp scale for multi-object-scaling where selection does not exceed playfield bounds or flip. - /// + /// The origin from which the scale operation is performed /// The scale to be clamped /// The axes to adjust the scale in. @@ -215,54 +215,34 @@ namespace osu.Game.Rulesets.Osu.Edit Vector2 actualOrigin = origin ?? defaultOrigin.Value; var selectionQuad = OriginalSurroundingQuad.Value; - scale = clampToBound(scale, selectionQuad.BottomRight, OsuPlayfield.BASE_SIZE.X, Axes.X); - scale = clampToBound(scale, selectionQuad.BottomRight, OsuPlayfield.BASE_SIZE.Y, Axes.Y); - scale = clampToBound(scale, selectionQuad.TopLeft, 0, Axes.X); - scale = clampToBound(scale, selectionQuad.TopLeft, 0, Axes.Y); + scale = clampToBound(scale, selectionQuad.BottomRight, OsuPlayfield.BASE_SIZE); + scale = clampToBound(scale, selectionQuad.TopLeft, Vector2.Zero); return Vector2.ComponentMax(scale, new Vector2(Precision.FLOAT_EPSILON)); - Vector2 clampToBound(Vector2 s, Vector2 p, float bound, Axes axis) + float minPositiveComponent(Vector2 v) => MathF.Min(v.X < 0 ? float.PositiveInfinity : v.X, v.Y < 0 ? float.PositiveInfinity : v.Y); + + Vector2 clampToBound(Vector2 s, Vector2 p, Vector2 bound) { - float px = p.X - actualOrigin.X; - float py = p.Y - actualOrigin.Y; - float c = axis == Axes.X ? bound - actualOrigin.X : bound - actualOrigin.Y; + p -= actualOrigin; + bound -= actualOrigin; float cos = MathF.Cos(float.DegreesToRadians(-axisRotation)); float sin = MathF.Sin(float.DegreesToRadians(-axisRotation)); - float a, b; - - if (axis == Axes.X) - { - a = cos * cos * px - sin * cos * py; - b = sin * sin * px + sin * cos * py; - } - else - { - a = -sin * cos * px + sin * sin * py; - b = sin * cos * px + cos * cos * py; - } + var a = new Vector2(cos * cos * p.X - sin * cos * p.Y, -sin * cos * p.X + sin * sin * p.Y); + var b = new Vector2(sin * sin * p.X + sin * cos * p.Y, sin * cos * p.X + cos * cos * p.Y); switch (adjustAxis) { case Axes.X: - if (Precision.AlmostEquals(a, 0) || (c - b) / a < 0) - break; - - s.X = MathF.Min(scale.X, (c - b) / a); + s.X = MathF.Min(scale.X, minPositiveComponent(Vector2.Divide(bound - b, a))); break; case Axes.Y: - if (Precision.AlmostEquals(b, 0) || (c - a) / b < 0) - break; - - s.Y = MathF.Min(scale.Y, (c - a) / b); + s.Y = MathF.Min(scale.Y, minPositiveComponent(Vector2.Divide(bound - a, b))); break; case Axes.Both: - if (Precision.AlmostEquals(a + b, 0) || c / (a * s.X + b * s.Y) < 0) - break; - - s = Vector2.ComponentMin(s, s * c / (a * s.X + b * s.Y)); + s = Vector2.ComponentMin(s, s * minPositiveComponent(Vector2.Divide(bound, a * s.X + b * s.Y))); break; } From 0797d942aecde4e267fca11fbec5bf73611fc9b8 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Wed, 3 Jul 2024 16:41:57 +0200 Subject: [PATCH 1299/2493] fix warning --- osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs index 2cf5a604ed..8b87246456 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs @@ -194,7 +194,7 @@ namespace osu.Game.Rulesets.Osu.Edit /// /// Clamp scale for multi-object-scaling where selection does not exceed playfield bounds or flip. - /// /// The origin from which the scale operation is performed /// The scale to be clamped /// The axes to adjust the scale in. From 42aff953d991246006700feea63accfad795651f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 3 Jul 2024 23:59:29 +0900 Subject: [PATCH 1300/2493] Ensure menu items update when curve type changes --- .../Components/PathControlPointVisualiser.cs | 56 ++++++++++++++----- 1 file changed, 42 insertions(+), 14 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs index ddf6cd0f57..abe8be530a 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs @@ -34,7 +34,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components public partial class PathControlPointVisualiser : CompositeDrawable, IKeyBindingHandler, IHasContextMenu where T : OsuHitObject, IHasPath { - public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; // allow context menu to appear outside of the playfield. + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; // allow context menu to appear outside the playfield. internal readonly Container> Pieces; @@ -196,6 +196,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components if (allowSelection) d.RequestSelection = selectionRequested; + d.ControlPoint.Changed += controlPointChanged; d.DragStarted = DragStarted; d.DragInProgress = DragInProgress; d.DragEnded = DragEnded; @@ -209,6 +210,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components foreach (var point in e.OldItems.Cast()) { + point.Changed -= controlPointChanged; + foreach (var piece in Pieces.Where(p => p.ControlPoint == point).ToArray()) piece.RemoveAndDisposeImmediately(); } @@ -217,6 +220,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components } } + private void controlPointChanged() => updateCurveMenuItems(); + protected override bool OnClick(ClickEvent e) { if (Pieces.Any(piece => piece.IsHovered)) @@ -318,6 +323,13 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components } } + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + foreach (var p in Pieces) + p.ControlPoint.Changed -= controlPointChanged; + } + private void selectionRequested(PathControlPointPiece piece, MouseButtonEvent e) { if (e.Button == MouseButton.Left && inputManager.CurrentState.Keyboard.ControlPressed) @@ -328,7 +340,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components /// /// Attempts to set all selected control point pieces to the given path type. - /// If that would fail, try to change the path such that it instead succeeds + /// If that fails, try to change the path such that it instead succeeds /// in a UX-friendly way. /// /// The path type we want to assign to the given control point piece. @@ -371,6 +383,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components private int draggedControlPointIndex; private HashSet selectedControlPoints; + private List curveTypeItems; + public void DragStarted(PathControlPoint controlPoint) { dragStartPositions = hitObject.Path.ControlPoints.Select(point => point.Position).ToArray(); @@ -467,7 +481,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components var splittablePieces = selectedPieces.Where(isSplittable).ToList(); int splittableCount = splittablePieces.Count; - List curveTypeItems = new List(); + curveTypeItems = new List(); if (!selectedPieces.Contains(Pieces[0])) { @@ -505,25 +519,39 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components () => DeleteSelected()) ); + updateCurveMenuItems(); + return menuItems.ToArray(); + + CurveTypeMenuItem createMenuItemForPathType(PathType? type) => new CurveTypeMenuItem(type, _ => updatePathTypeOfSelectedPieces(type)); } } - private MenuItem createMenuItemForPathType(PathType? type) + private void updateCurveMenuItems() { - int totalCount = Pieces.Count(p => p.IsSelected.Value); - int countOfState = Pieces.Where(p => p.IsSelected.Value).Count(p => p.ControlPoint.Type == type); + foreach (var item in curveTypeItems.OfType()) + { + int totalCount = Pieces.Count(p => p.IsSelected.Value); + int countOfState = Pieces.Where(p => p.IsSelected.Value).Count(p => p.ControlPoint.Type == item.PathType); - var item = new TernaryStateRadioMenuItem(type?.Description ?? "Inherit", MenuItemType.Standard, _ => updatePathTypeOfSelectedPieces(type)); + if (countOfState == totalCount) + item.State.Value = TernaryState.True; + else if (countOfState > 0) + item.State.Value = TernaryState.Indeterminate; + else + item.State.Value = TernaryState.False; + } + } - if (countOfState == totalCount) - item.State.Value = TernaryState.True; - else if (countOfState > 0) - item.State.Value = TernaryState.Indeterminate; - else - item.State.Value = TernaryState.False; + private class CurveTypeMenuItem : TernaryStateRadioMenuItem + { + public readonly PathType? PathType; - return item; + public CurveTypeMenuItem(PathType? pathType, Action action) + : base(pathType?.Description ?? "Inherit", MenuItemType.Standard, action) + { + PathType = pathType; + } } } } From 6abb728cd5dc5a97bc1ced545494b3aa5d2118b8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 4 Jul 2024 00:22:46 +0900 Subject: [PATCH 1301/2493] Change menu items to be in same order as hotkeys --- .../Components/PathControlPointVisualiser.cs | 24 ++++++++++++------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs index abe8be530a..dfe334be0c 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs @@ -483,20 +483,26 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components curveTypeItems = new List(); - if (!selectedPieces.Contains(Pieces[0])) + // todo: hide/disable items which aren't valid for selected points + foreach (PathType? type in path_types) { - curveTypeItems.Add(createMenuItemForPathType(null)); - curveTypeItems.Add(new OsuMenuItemSpacer()); + // special inherit case + if (type == null) + { + if (selectedPieces.Contains(Pieces[0])) + continue; + + curveTypeItems.Add(new OsuMenuItemSpacer()); + } + + curveTypeItems.Add(createMenuItemForPathType(type)); } - // todo: hide/disable items which aren't valid for selected points - curveTypeItems.Add(createMenuItemForPathType(PathType.LINEAR)); - curveTypeItems.Add(createMenuItemForPathType(PathType.PERFECT_CURVE)); - curveTypeItems.Add(createMenuItemForPathType(PathType.BEZIER)); - curveTypeItems.Add(createMenuItemForPathType(PathType.BSpline(4))); - if (selectedPieces.Any(piece => piece.ControlPoint.Type?.Type == SplineType.Catmull)) + { + curveTypeItems.Add(new OsuMenuItemSpacer()); curveTypeItems.Add(createMenuItemForPathType(PathType.CATMULL)); + } var menuItems = new List { From f7339e3e8b9726078506baaf3f02969d34137fb1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 4 Jul 2024 00:26:00 +0900 Subject: [PATCH 1302/2493] Remove outdated(?) todo --- .../Blueprints/Sliders/Components/PathControlPointVisualiser.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs index dfe334be0c..f45dae8937 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs @@ -483,7 +483,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components curveTypeItems = new List(); - // todo: hide/disable items which aren't valid for selected points foreach (PathType? type in path_types) { // special inherit case From e151454c4ec655d349002ceab292bac1f682fbc4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 4 Jul 2024 01:00:51 +0900 Subject: [PATCH 1303/2493] Add missing check for curve menu items not yet being created --- .../Sliders/Components/PathControlPointVisualiser.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs index f45dae8937..6251d17d85 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs @@ -534,6 +534,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components private void updateCurveMenuItems() { + if (curveTypeItems == null) + return; + foreach (var item in curveTypeItems.OfType()) { int totalCount = Pieces.Count(p => p.IsSelected.Value); From 4165ded8134d05f4d6b934255a5678a6a7d74bca Mon Sep 17 00:00:00 2001 From: OliBomby Date: Wed, 3 Jul 2024 19:03:15 +0200 Subject: [PATCH 1304/2493] fix incorrect rotated bound checking --- .../Edit/OsuSelectionScaleHandler.cs | 53 +++++++++++++++---- osu.Game/Utils/GeometryUtils.cs | 20 ++++--- 2 files changed, 56 insertions(+), 17 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs index 8b87246456..d336261499 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs @@ -80,12 +80,32 @@ namespace osu.Game.Rulesets.Osu.Edit changeHandler?.BeginChange(); objectsInScale = selectedMovableObjects.ToDictionary(ho => ho, ho => new OriginalHitObjectState(ho)); - OriginalSurroundingQuad = objectsInScale.Count == 1 && objectsInScale.First().Key is Slider slider - ? GeometryUtils.GetSurroundingQuad(slider.Path.ControlPoints.Select(p => slider.Position + p.Position)) - : GeometryUtils.GetSurroundingQuad(objectsInScale.Keys); + OriginalSurroundingQuad = getOriginalSurroundingQuad()!; defaultOrigin = OriginalSurroundingQuad.Value.Centre; } + private Quad? getOriginalSurroundingQuad(float axisRotation = 0) + { + if (objectsInScale == null) + return null; + + return objectsInScale.Count == 1 && objectsInScale.First().Value.PathControlPointPositions != null + ? GeometryUtils.GetSurroundingQuad(objectsInScale.First().Value.PathControlPointPositions!.Select(p => objectsInScale.First().Value.Position + p), axisRotation) + : GeometryUtils.GetSurroundingQuad(objectsInScale.Values.SelectMany(s => + { + if (s.EndPosition.HasValue) + { + return new[] + { + s.Position, + s.Position + s.EndPosition.Value + }; + } + + return new[] { s.Position }; + }), axisRotation); + } + public override void Update(Vector2 scale, Vector2? origin = null, Axes adjustAxis = Axes.Both, float axisRotation = 0) { if (!OperationInProgress.Value) @@ -213,10 +233,23 @@ namespace osu.Game.Rulesets.Osu.Edit scale = clampScaleToAdjustAxis(scale, adjustAxis); Vector2 actualOrigin = origin ?? defaultOrigin.Value; - var selectionQuad = OriginalSurroundingQuad.Value; + var selectionQuad = axisRotation == 0 ? OriginalSurroundingQuad.Value : getOriginalSurroundingQuad(axisRotation)!.Value; + var points = new[] + { + selectionQuad.TopLeft, + selectionQuad.TopRight, + selectionQuad.BottomLeft, + selectionQuad.BottomRight + }; - scale = clampToBound(scale, selectionQuad.BottomRight, OsuPlayfield.BASE_SIZE); - scale = clampToBound(scale, selectionQuad.TopLeft, Vector2.Zero); + float cos = MathF.Cos(float.DegreesToRadians(-axisRotation)); + float sin = MathF.Sin(float.DegreesToRadians(-axisRotation)); + + foreach (var point in points) + { + scale = clampToBound(scale, point, Vector2.Zero); + scale = clampToBound(scale, point, OsuPlayfield.BASE_SIZE); + } return Vector2.ComponentMax(scale, new Vector2(Precision.FLOAT_EPSILON)); @@ -226,19 +259,17 @@ namespace osu.Game.Rulesets.Osu.Edit { p -= actualOrigin; bound -= actualOrigin; - float cos = MathF.Cos(float.DegreesToRadians(-axisRotation)); - float sin = MathF.Sin(float.DegreesToRadians(-axisRotation)); var a = new Vector2(cos * cos * p.X - sin * cos * p.Y, -sin * cos * p.X + sin * sin * p.Y); var b = new Vector2(sin * sin * p.X + sin * cos * p.Y, sin * cos * p.X + cos * cos * p.Y); switch (adjustAxis) { case Axes.X: - s.X = MathF.Min(scale.X, minPositiveComponent(Vector2.Divide(bound - b, a))); + s.X = MathF.Min(s.X, minPositiveComponent(Vector2.Divide(bound - b, a))); break; case Axes.Y: - s.Y = MathF.Min(scale.Y, minPositiveComponent(Vector2.Divide(bound - a, b))); + s.Y = MathF.Min(s.Y, minPositiveComponent(Vector2.Divide(bound - a, b))); break; case Axes.Both: @@ -275,12 +306,14 @@ namespace osu.Game.Rulesets.Osu.Edit public Vector2 Position { get; } public Vector2[]? PathControlPointPositions { get; } public PathType?[]? PathControlPointTypes { get; } + public Vector2? EndPosition { get; } public OriginalHitObjectState(OsuHitObject hitObject) { Position = hitObject.Position; PathControlPointPositions = (hitObject as IHasPath)?.Path.ControlPoints.Select(p => p.Position).ToArray(); PathControlPointTypes = (hitObject as IHasPath)?.Path.ControlPoints.Select(p => p.Type).ToArray(); + EndPosition = (hitObject as IHasPath)?.Path.PositionAt(1); } } } diff --git a/osu.Game/Utils/GeometryUtils.cs b/osu.Game/Utils/GeometryUtils.cs index f6e7e81007..23c25cfffa 100644 --- a/osu.Game/Utils/GeometryUtils.cs +++ b/osu.Game/Utils/GeometryUtils.cs @@ -113,7 +113,8 @@ namespace osu.Game.Utils /// Returns a quad surrounding the provided points. /// /// The points to calculate a quad for. - public static Quad GetSurroundingQuad(IEnumerable points) + /// The rotation in degrees of the axis to align the quad to. + public static Quad GetSurroundingQuad(IEnumerable points, float axisRotation = 0) { if (!points.Any()) return new Quad(); @@ -124,20 +125,25 @@ namespace osu.Game.Utils // Go through all hitobjects to make sure they would remain in the bounds of the editor after movement, before any movement is attempted foreach (var p in points) { - minPosition = Vector2.ComponentMin(minPosition, p); - maxPosition = Vector2.ComponentMax(maxPosition, p); + var pr = RotateVector(p, axisRotation); + minPosition = Vector2.ComponentMin(minPosition, pr); + maxPosition = Vector2.ComponentMax(maxPosition, pr); } - Vector2 size = maxPosition - minPosition; + var p1 = RotateVector(minPosition, -axisRotation); + var p2 = RotateVector(new Vector2(minPosition.X, maxPosition.Y), -axisRotation); + var p3 = RotateVector(maxPosition, -axisRotation); + var p4 = RotateVector(new Vector2(maxPosition.X, minPosition.Y), -axisRotation); - return new Quad(minPosition.X, minPosition.Y, size.X, size.Y); + return new Quad(p1, p2, p3, p4); } /// /// Returns a gamefield-space quad surrounding the provided hit objects. /// /// The hit objects to calculate a quad for. - public static Quad GetSurroundingQuad(IEnumerable hitObjects) => + /// The rotation in degrees of the axis to align the quad to. + public static Quad GetSurroundingQuad(IEnumerable hitObjects, float axisRotation = 0) => GetSurroundingQuad(hitObjects.SelectMany(h => { if (h is IHasPath path) @@ -151,6 +157,6 @@ namespace osu.Game.Utils } return new[] { h.Position }; - })); + }), axisRotation); } } From dfe6c70996b06fa0e597fffbf1aec75e5b1d508c Mon Sep 17 00:00:00 2001 From: OliBomby Date: Wed, 3 Jul 2024 19:08:31 +0200 Subject: [PATCH 1305/2493] prevent flipping objects far offscreen --- osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs index 7720afe60a..7d6ef66909 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs @@ -15,6 +15,7 @@ using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Osu.UI; using osu.Game.Screens.Edit.Compose.Components; using osu.Game.Utils; using osuTK; @@ -119,6 +120,9 @@ namespace osu.Game.Rulesets.Osu.Edit { var flippedPosition = GeometryUtils.GetFlippedPosition(flipAxis, flipQuad, h.Position); + // Clamp the flipped position inside the playfield bounds, because the flipped position might be outside the playfield bounds if the origin is not centered. + flippedPosition = Vector2.Clamp(flippedPosition, Vector2.Zero, OsuPlayfield.BASE_SIZE); + if (!Precision.AlmostEquals(flippedPosition, h.Position)) { h.Position = flippedPosition; From 3926af1053f5e4ef02b4caa7dcac1755d3658b3f Mon Sep 17 00:00:00 2001 From: OliBomby Date: Wed, 3 Jul 2024 20:17:39 +0200 Subject: [PATCH 1306/2493] Use draggable handle for length adjust --- .../TestSceneSliderSelectionBlueprint.cs | 33 +++-- .../Blueprints/Sliders/SliderCircleOverlay.cs | 129 +++++++++++++++++- .../Sliders/SliderSelectionBlueprint.cs | 57 ++++---- 3 files changed, 173 insertions(+), 46 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSelectionBlueprint.cs index 812b34dfe2..c2589f11ef 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSelectionBlueprint.cs @@ -3,7 +3,6 @@ #nullable disable -using System.Linq; using NUnit.Framework; using osu.Framework.Utils; using osu.Game.Beatmaps; @@ -165,23 +164,35 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor } [Test] - public void TestAdjustDistance() + public void TestAdjustLength() { - AddStep("start adjust length", - () => blueprint.ContextMenuItems.Single(o => o.Text.Value == "Adjust length").Action.Value()); - moveMouseToControlPoint(1); - AddStep("end adjust length", () => InputManager.Click(MouseButton.Right)); + AddStep("move mouse to drag marker", () => + { + Vector2 position = slider.Position + slider.Path.PositionAt(1) + new Vector2(60, 0); + InputManager.MoveMouseTo(drawableObject.Parent!.ToScreenSpace(position)); + }); + AddStep("start drag", () => InputManager.PressButton(MouseButton.Left)); + AddStep("move mouse to control point 1", () => + { + Vector2 position = slider.Position + slider.Path.ControlPoints[1].Position + new Vector2(60, 0); + InputManager.MoveMouseTo(drawableObject.Parent!.ToScreenSpace(position)); + }); + AddStep("end adjust length", () => InputManager.ReleaseButton(MouseButton.Left)); AddAssert("expected distance halved", () => Precision.AlmostEquals(slider.Path.Distance, 172.2, 0.1)); - AddStep("start adjust length", - () => blueprint.ContextMenuItems.Single(o => o.Text.Value == "Adjust length").Action.Value()); - AddStep("move mouse beyond last control point", () => + AddStep("move mouse to drag marker", () => { - Vector2 position = slider.Position + slider.Path.ControlPoints[2].Position + new Vector2(50, 0); + Vector2 position = slider.Position + slider.Path.PositionAt(1) + new Vector2(60, 0); InputManager.MoveMouseTo(drawableObject.Parent!.ToScreenSpace(position)); }); - AddStep("end adjust length", () => InputManager.Click(MouseButton.Right)); + AddStep("start drag", () => InputManager.PressButton(MouseButton.Left)); + AddStep("move mouse beyond last control point", () => + { + Vector2 position = slider.Position + slider.Path.ControlPoints[2].Position + new Vector2(100, 0); + InputManager.MoveMouseTo(drawableObject.Parent!.ToScreenSpace(position)); + }); + AddStep("end adjust length", () => InputManager.ReleaseButton(MouseButton.Left)); AddAssert("expected distance is calculated distance", () => Precision.AlmostEquals(slider.Path.Distance, slider.Path.CalculatedDistance, 0.1)); diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderCircleOverlay.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderCircleOverlay.cs index 55ea131dab..9752ce4a13 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderCircleOverlay.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderCircleOverlay.cs @@ -1,24 +1,47 @@ // 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.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Lines; +using osu.Framework.Graphics.Primitives; +using osu.Framework.Input.Events; +using osu.Framework.Utils; +using osu.Game.Graphics; using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components; using osu.Game.Rulesets.Osu.Objects; +using osuTK; namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders { public partial class SliderCircleOverlay : CompositeDrawable { - protected readonly HitCirclePiece CirclePiece; - protected readonly Slider Slider; + public RectangleF VisibleQuad + { + get + { + var result = CirclePiece.ScreenSpaceDrawQuad.AABBFloat; - private readonly HitCircleOverlapMarker marker; + if (endDragMarkerContainer == null) return result; + + var size = result.Size * 1.4f; + var location = result.TopLeft - result.Size * 0.2f; + return new RectangleF(location, size); + } + } + + protected readonly HitCirclePiece CirclePiece; + + private readonly Slider slider; private readonly SliderPosition position; + private readonly HitCircleOverlapMarker marker; + private readonly Container? endDragMarkerContainer; public SliderCircleOverlay(Slider slider, SliderPosition position) { - Slider = slider; + this.slider = slider; this.position = position; InternalChildren = new Drawable[] @@ -26,27 +49,121 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders marker = new HitCircleOverlapMarker(), CirclePiece = new HitCirclePiece(), }; + + if (position == SliderPosition.End) + { + AddInternal(endDragMarkerContainer = new Container + { + AutoSizeAxes = Axes.Both, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Padding = new MarginPadding(-2.5f), + Child = EndDragMarker = new SliderEndDragMarker() + }); + } } + public SliderEndDragMarker? EndDragMarker { get; } + protected override void Update() { base.Update(); - var circle = position == SliderPosition.Start ? (HitCircle)Slider.HeadCircle : - Slider.RepeatCount % 2 == 0 ? Slider.TailCircle : Slider.LastRepeat!; + var circle = position == SliderPosition.Start ? (HitCircle)slider.HeadCircle : + slider.RepeatCount % 2 == 0 ? slider.TailCircle : slider.LastRepeat!; CirclePiece.UpdateFrom(circle); marker.UpdateFrom(circle); + + if (endDragMarkerContainer != null) + { + endDragMarkerContainer.Position = circle.Position; + endDragMarkerContainer.Scale = CirclePiece.Scale * 1.2f; + var diff = slider.Path.PositionAt(1) - slider.Path.PositionAt(0.99f); + endDragMarkerContainer.Rotation = float.RadiansToDegrees(MathF.Atan2(diff.Y, diff.X)); + } } public override void Hide() { CirclePiece.Hide(); + endDragMarkerContainer?.Hide(); } public override void Show() { CirclePiece.Show(); + endDragMarkerContainer?.Show(); + } + + public partial class SliderEndDragMarker : SmoothPath + { + public Action? StartDrag { get; set; } + public Action? Drag { get; set; } + public Action? EndDrag { get; set; } + + [Resolved] + private OsuColour colours { get; set; } = null!; + + [BackgroundDependencyLoader] + private void load() + { + var path = PathApproximator.CircularArcToPiecewiseLinear([ + new Vector2(0, OsuHitObject.OBJECT_RADIUS), + new Vector2(OsuHitObject.OBJECT_RADIUS, 0), + new Vector2(0, -OsuHitObject.OBJECT_RADIUS) + ]); + + Anchor = Anchor.CentreLeft; + Origin = Anchor.CentreLeft; + PathRadius = 5; + Vertices = path; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + updateState(); + } + + protected override bool OnHover(HoverEvent e) + { + updateState(); + return true; + } + + protected override void OnHoverLost(HoverLostEvent e) + { + updateState(); + base.OnHoverLost(e); + } + + protected override bool OnDragStart(DragStartEvent e) + { + updateState(); + StartDrag?.Invoke(e); + return true; + } + + protected override void OnDrag(DragEvent e) + { + updateState(); + base.OnDrag(e); + Drag?.Invoke(e); + } + + protected override void OnDragEnd(DragEndEvent e) + { + updateState(); + EndDrag?.Invoke(); + base.OnDragEnd(e); + } + + private void updateState() + { + Colour = IsHovered || IsDragged ? colours.Red : colours.Yellow; + } } } } diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs index eb269ba680..87f9fd41e8 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs @@ -55,7 +55,18 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders [Resolved(CanBeNull = true)] private BindableBeatDivisor beatDivisor { get; set; } - public override Quad SelectionQuad => BodyPiece.ScreenSpaceDrawQuad; + public override Quad SelectionQuad + { + get + { + var result = BodyPiece.ScreenSpaceDrawQuad.AABBFloat; + + result = RectangleF.Union(result, HeadOverlay.VisibleQuad); + result = RectangleF.Union(result, TailOverlay.VisibleQuad); + + return result; + } + } private readonly BindableList controlPoints = new BindableList(); private readonly IBindable pathVersion = new Bindable(); @@ -63,7 +74,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders // Cached slider path which ignored the expected distance value. private readonly Cached fullPathCache = new Cached(); - private bool isAdjustingLength; public SliderSelectionBlueprint(Slider slider) : base(slider) @@ -79,6 +89,10 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders HeadOverlay = CreateCircleOverlay(HitObject, SliderPosition.Start), TailOverlay = CreateCircleOverlay(HitObject, SliderPosition.End), }; + + TailOverlay.EndDragMarker!.StartDrag += startAdjustingLength; + TailOverlay.EndDragMarker.Drag += adjustLength; + TailOverlay.EndDragMarker.EndDrag += endAdjustLength; } protected override void LoadComplete() @@ -141,9 +155,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders { base.OnDeselected(); - if (isAdjustingLength) - endAdjustLength(); - updateVisualDefinition(); BodyPiece.RecyclePath(); } @@ -173,12 +184,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders protected override bool OnMouseDown(MouseDownEvent e) { - if (isAdjustingLength) - { - endAdjustLength(); - return true; - } - switch (e.Button) { case MouseButton.Right: @@ -202,18 +207,22 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders return false; } + private Vector2 lengthAdjustMouseOffset; + + private void startAdjustingLength(DragStartEvent e) + { + lengthAdjustMouseOffset = ToLocalSpace(e.ScreenSpaceMouseDownPosition) - HitObject.Position - HitObject.Path.PositionAt(1); + changeHandler?.BeginChange(); + } + private void endAdjustLength() { trimExcessControlPoints(HitObject.Path); - isAdjustingLength = false; changeHandler?.EndChange(); } - protected override bool OnMouseMove(MouseMoveEvent e) + private void adjustLength(MouseEvent e) { - if (!isAdjustingLength) - return base.OnMouseMove(e); - double oldDistance = HitObject.Path.Distance; double proposedDistance = findClosestPathDistance(e); @@ -223,13 +232,11 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders 10 * oldDistance / HitObject.SliderVelocityMultiplier); if (Precision.AlmostEquals(proposedDistance, oldDistance)) - return false; + return; HitObject.SliderVelocityMultiplier *= proposedDistance / oldDistance; HitObject.Path.ExpectedDistance.Value = proposedDistance; editorBeatmap?.Update(HitObject); - - return false; } /// @@ -262,12 +269,12 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders /// /// Finds the expected distance value for which the slider end is closest to the mouse position. /// - private double findClosestPathDistance(MouseMoveEvent e) + private double findClosestPathDistance(MouseEvent e) { const double step1 = 10; const double step2 = 0.1; - var desiredPosition = e.MousePosition - HitObject.Position; + var desiredPosition = ToLocalSpace(e.ScreenSpaceMousePosition) - HitObject.Position - lengthAdjustMouseOffset; if (!fullPathCache.IsValid) fullPathCache.Value = new SliderPath(HitObject.Path.ControlPoints.ToArray()); @@ -525,11 +532,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders addControlPoint(rightClickPosition); changeHandler?.EndChange(); }), - new OsuMenuItem("Adjust length", MenuItemType.Standard, () => - { - isAdjustingLength = true; - changeHandler?.BeginChange(); - }), new OsuMenuItem("Convert to stream", MenuItemType.Destructive, convertToStream), }; @@ -544,9 +546,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) { - if (isAdjustingLength) - return true; - if (BodyPiece.ReceivePositionalInputAt(screenSpacePos)) return true; From 5697c82bb87cc58460fe053f27039a5bb9dbaf84 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Wed, 3 Jul 2024 20:33:00 +0200 Subject: [PATCH 1307/2493] add a small bias towards longer distances to prevent jittery behaviour on path self-intersections --- .../Blueprints/Sliders/SliderSelectionBlueprint.cs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs index 87f9fd41e8..586ba5b6b1 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs @@ -3,6 +3,7 @@ #nullable disable +using System; using System.Collections.Generic; using System.Linq; using JetBrains.Annotations; @@ -273,6 +274,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders { const double step1 = 10; const double step2 = 0.1; + const double longer_distance_bias = 0.01; var desiredPosition = ToLocalSpace(e.ScreenSpaceMousePosition) - HitObject.Position - lengthAdjustMouseOffset; @@ -286,7 +288,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders for (double d = 0; d <= fullPathCache.Value.CalculatedDistance; d += step1) { double t = d / fullPathCache.Value.CalculatedDistance; - float dist = Vector2.Distance(fullPathCache.Value.PositionAt(t), desiredPosition); + double dist = Vector2.Distance(fullPathCache.Value.PositionAt(t), desiredPosition) - d * longer_distance_bias; if (dist >= minDistance) continue; @@ -295,10 +297,12 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders } // Do another linear search to fine-tune the result. - for (double d = bestValue - step1; d <= bestValue + step1; d += step2) + double maxValue = Math.Min(bestValue + step1, fullPathCache.Value.CalculatedDistance); + + for (double d = bestValue - step1; d <= maxValue; d += step2) { double t = d / fullPathCache.Value.CalculatedDistance; - float dist = Vector2.Distance(fullPathCache.Value.PositionAt(t), desiredPosition); + double dist = Vector2.Distance(fullPathCache.Value.PositionAt(t), desiredPosition) - d * longer_distance_bias; if (dist >= minDistance) continue; From 8dd04b6e9a699c51fef83384e84bca800a451661 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Thu, 4 Jul 2024 00:39:12 +0200 Subject: [PATCH 1308/2493] update nodesamples on placement --- osu.Game/Rulesets/Edit/PlacementBlueprint.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/osu.Game/Rulesets/Edit/PlacementBlueprint.cs b/osu.Game/Rulesets/Edit/PlacementBlueprint.cs index 5cb9adfd72..a30434638f 100644 --- a/osu.Game/Rulesets/Edit/PlacementBlueprint.cs +++ b/osu.Game/Rulesets/Edit/PlacementBlueprint.cs @@ -163,6 +163,15 @@ namespace osu.Game.Rulesets.Edit if (lastHitNormal != null) HitObject.Samples[0] = lastHitNormal; } + + if (HitObject is IHasRepeats hasRepeats) + { + // Make sure all the node samples are identical to the hit object's samples + for (int i = 0; i < hasRepeats.NodeSamples.Count; i++) + { + hasRepeats.NodeSamples[i] = HitObject.Samples.Select(o => o.With()).ToList(); + } + } } /// From 8f3a30b0b9ba86adfd6b8c0690fc716608abad2c Mon Sep 17 00:00:00 2001 From: OliBomby Date: Thu, 4 Jul 2024 00:56:53 +0200 Subject: [PATCH 1309/2493] inherit addition bank from last hitobject --- osu.Game/Rulesets/Edit/PlacementBlueprint.cs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/osu.Game/Rulesets/Edit/PlacementBlueprint.cs b/osu.Game/Rulesets/Edit/PlacementBlueprint.cs index 5cb9adfd72..762a714088 100644 --- a/osu.Game/Rulesets/Edit/PlacementBlueprint.cs +++ b/osu.Game/Rulesets/Edit/PlacementBlueprint.cs @@ -158,10 +158,16 @@ namespace osu.Game.Rulesets.Edit if (AutomaticBankAssignment) { - // Take the hitnormal sample of the last hit object - var lastHitNormal = getPreviousHitObject()?.Samples?.FirstOrDefault(o => o.Name == HitSampleInfo.HIT_NORMAL); - if (lastHitNormal != null) - HitObject.Samples[0] = lastHitNormal; + // Create samples based on the sample settings of the previous hit object + var lastHitObject = getPreviousHitObject(); + + if (lastHitObject != null) + { + for (int i = 0; i < HitObject.Samples.Count; i++) + { + HitObject.Samples[i] = lastHitObject.CreateHitSampleInfo(HitObject.Samples[i].Name); + } + } } } From e754668daa3dc8d57d7d4f62aa61735df3e4798e Mon Sep 17 00:00:00 2001 From: OliBomby Date: Thu, 4 Jul 2024 01:09:06 +0200 Subject: [PATCH 1310/2493] Always inherit the volume from the previous hit object --- osu.Game/Rulesets/Edit/PlacementBlueprint.cs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/osu.Game/Rulesets/Edit/PlacementBlueprint.cs b/osu.Game/Rulesets/Edit/PlacementBlueprint.cs index 5cb9adfd72..84fe1584dd 100644 --- a/osu.Game/Rulesets/Edit/PlacementBlueprint.cs +++ b/osu.Game/Rulesets/Edit/PlacementBlueprint.cs @@ -163,6 +163,19 @@ namespace osu.Game.Rulesets.Edit if (lastHitNormal != null) HitObject.Samples[0] = lastHitNormal; } + else + { + // Only inherit the volume from the previous hit object + var lastHitNormal = getPreviousHitObject()?.Samples?.FirstOrDefault(o => o.Name == HitSampleInfo.HIT_NORMAL); + + if (lastHitNormal != null) + { + for (int i = 0; i < HitObject.Samples.Count; i++) + { + HitObject.Samples[i] = HitObject.Samples[i].With(newVolume: lastHitNormal.Volume); + } + } + } } /// From 371ca4cc4bb659c8d2765d9a8b38f431e1d1d049 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 4 Jul 2024 05:43:43 +0300 Subject: [PATCH 1311/2493] Remove unnecessary null-conditional operators --- osu.Game.Rulesets.Mania/UI/ManiaTouchInputArea.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Mania/UI/ManiaTouchInputArea.cs b/osu.Game.Rulesets.Mania/UI/ManiaTouchInputArea.cs index 32e4616a25..453b75ac84 100644 --- a/osu.Game.Rulesets.Mania/UI/ManiaTouchInputArea.cs +++ b/osu.Game.Rulesets.Mania/UI/ManiaTouchInputArea.cs @@ -192,12 +192,12 @@ namespace osu.Game.Rulesets.Mania.UI if (press) { - inputManager?.KeyBindingContainer?.TriggerPressed(Action.Value); + inputManager?.KeyBindingContainer.TriggerPressed(Action.Value); highlightOverlay.FadeTo(0.1f, 80, Easing.OutQuint); } else { - inputManager?.KeyBindingContainer?.TriggerReleased(Action.Value); + inputManager?.KeyBindingContainer.TriggerReleased(Action.Value); highlightOverlay.FadeTo(0, 400, Easing.OutQuint); } } From e4f90719ed15e7a17bb9be0360730eed9aa2f9a5 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 4 Jul 2024 06:22:53 +0300 Subject: [PATCH 1312/2493] Update test to match new behaviour --- osu.Game.Rulesets.Catch.Tests/TestSceneCatchReplayHandling.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatchReplayHandling.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchReplayHandling.cs index 361ecb7c4c..1721703e48 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatchReplayHandling.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchReplayHandling.cs @@ -25,8 +25,10 @@ namespace osu.Game.Rulesets.Catch.Tests AddStep("store catcher position", () => catcherPosition = drawableRuleset.ChildrenOfType().Single().X); AddStep("hold down left", () => InputManager.PressKey(Key.Left)); AddAssert("catcher didn't move", () => drawableRuleset.ChildrenOfType().Single().X, () => Is.EqualTo(catcherPosition)); + AddStep("release left", () => InputManager.ReleaseKey(Key.Left)); AddStep("detach replay", () => drawableRuleset.SetReplayScore(null)); + AddStep("hold down left", () => InputManager.PressKey(Key.Left)); AddUntilStep("catcher moved", () => drawableRuleset.ChildrenOfType().Single().X, () => Is.Not.EqualTo(catcherPosition)); AddStep("release left", () => InputManager.ReleaseKey(Key.Left)); } From 86583898548c83e3211b6c10f653b88382ada954 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 4 Jul 2024 07:22:08 +0300 Subject: [PATCH 1313/2493] Clarify how the panel blocks input --- osu.Game/Overlays/Mods/ModCustomisationPanel.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModCustomisationPanel.cs b/osu.Game/Overlays/Mods/ModCustomisationPanel.cs index 240569031c..ae162e1553 100644 --- a/osu.Game/Overlays/Mods/ModCustomisationPanel.cs +++ b/osu.Game/Overlays/Mods/ModCustomisationPanel.cs @@ -39,12 +39,14 @@ namespace osu.Game.Overlays.Mods public Bindable> SelectedMods { get; } = new Bindable>(Array.Empty()); + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; + + // Handle{Non}PositionalInput controls whether the panel should act as a blocking layer on the screen. only block when the panel is expanded. + // These properties are used because they correctly handle blocking/unblocking hover when mouse is pointing at a drawable outside + // (returning Expanded.Value to OnHover or overriding Block{Non}PositionalInput doesn't work). public override bool HandlePositionalInput => Expanded.Value; - public override bool HandleNonPositionalInput => Expanded.Value; - protected override bool BlockPositionalInput => true; - [BackgroundDependencyLoader] private void load() { @@ -125,8 +127,6 @@ namespace osu.Game.Overlays.Mods protected override void PopOut() => this.FadeOut(300, Easing.OutQuint); - public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; - protected override bool OnClick(ClickEvent e) { Expanded.Value = false; From 33711ba6167f3e0bdb7e9c7aba2b6f1dbde239ea Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 4 Jul 2024 07:25:54 +0300 Subject: [PATCH 1314/2493] Remove scroll-dropdown-into-view logic --- .../Configuration/SettingSourceAttribute.cs | 35 ++----------------- 1 file changed, 2 insertions(+), 33 deletions(-) diff --git a/osu.Game/Configuration/SettingSourceAttribute.cs b/osu.Game/Configuration/SettingSourceAttribute.cs index f87c8de0cb..1e425c88a6 100644 --- a/osu.Game/Configuration/SettingSourceAttribute.cs +++ b/osu.Game/Configuration/SettingSourceAttribute.cs @@ -13,10 +13,8 @@ using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Extensions.TypeExtensions; using osu.Framework.Graphics; using osu.Framework.Localisation; -using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osu.Game.Overlays.Settings; -using osuTK; namespace osu.Game.Configuration { @@ -276,37 +274,8 @@ namespace osu.Game.Configuration private partial class ModDropdownControl : DropdownControl { - protected override DropdownMenu CreateMenu() => new ModDropdownMenu(); - - private partial class ModDropdownMenu : OsuDropdownMenu - { - public ModDropdownMenu() - { - // Set menu's max height low enough to workaround nested scroll issues (see https://github.com/ppy/osu-framework/issues/4536). - MaxHeight = 100; - } - - protected override void UpdateSize(Vector2 newSize) - { - base.UpdateSize(newSize); - - // todo: probably move this to OsuDropdown so that settings overlay can benefit from this as well. - if (newSize.Y > 0) - { - var scroll = this.FindClosestParent(); - - if (scroll != null) - { - const float padding = 15; - - float target = scroll.GetChildPosInContent(this, new Vector2(0, newSize.Y + padding)); - - if (target > scroll.Current + scroll.DisplayableContent) - scroll.ScrollTo(target - scroll.DisplayableContent); - } - } - } - } + // Set menu's max height low enough to workaround nested scroll issues (see https://github.com/ppy/osu-framework/issues/4536). + protected override DropdownMenu CreateMenu() => base.CreateMenu().With(m => m.MaxHeight = 100); } } } From 585e09981fd655add5cd0465d5cf6debb3eef97d Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 4 Jul 2024 07:36:31 +0300 Subject: [PATCH 1315/2493] Allow scrollbar to overlap content The content is already padded enough to have the scrollbar sit on top. Having the content change padding when the scrollbar appears gives an unpleasent experience (especially when the scrollbar is hidden at first but the user increases the content's height by clicking on a dropdown or something) --- osu.Game/Overlays/Mods/ModCustomisationPanel.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Overlays/Mods/ModCustomisationPanel.cs b/osu.Game/Overlays/Mods/ModCustomisationPanel.cs index ae162e1553..f214bcb3a1 100644 --- a/osu.Game/Overlays/Mods/ModCustomisationPanel.cs +++ b/osu.Game/Overlays/Mods/ModCustomisationPanel.cs @@ -87,7 +87,6 @@ namespace osu.Game.Overlays.Mods scrollContainer = new OsuScrollContainer(Direction.Vertical) { RelativeSizeAxes = Axes.X, - ScrollbarOverlapsContent = false, // The +2f is a workaround for masking issues (see https://github.com/ppy/osu-framework/issues/1675#issuecomment-910023157) // Note that this actually causes the full scroll range to be reduced by 2px at the bottom, but it's not really noticeable. Margin = new MarginPadding { Top = header_height + 2f }, From d948193757530830d5488b531508fb96e0816362 Mon Sep 17 00:00:00 2001 From: Jamie Taylor Date: Thu, 4 Jul 2024 14:23:35 +0900 Subject: [PATCH 1316/2493] Change Duck() to be IDisposable and prevent overlapping usages --- .../Collections/ManageCollectionsDialog.cs | 16 ++++++-- .../Dialog/PopupDialogDangerousButton.cs | 10 ----- osu.Game/Overlays/DialogOverlay.cs | 13 +++++- osu.Game/Overlays/MusicController.cs | 41 ++++++++++++------- 4 files changed, 49 insertions(+), 31 deletions(-) diff --git a/osu.Game/Collections/ManageCollectionsDialog.cs b/osu.Game/Collections/ManageCollectionsDialog.cs index e777da05e5..0396fd531c 100644 --- a/osu.Game/Collections/ManageCollectionsDialog.cs +++ b/osu.Game/Collections/ManageCollectionsDialog.cs @@ -1,8 +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; using osu.Framework.Allocation; -using osu.Framework.Audio; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; @@ -24,6 +24,8 @@ namespace osu.Game.Collections protected override string PopInSampleName => @"UI/overlay-big-pop-in"; protected override string PopOutSampleName => @"UI/overlay-big-pop-out"; + private IDisposable? audioDucker; + [Resolved] private MusicController? musicController { get; set; } @@ -40,7 +42,7 @@ namespace osu.Game.Collections } [BackgroundDependencyLoader] - private void load(OsuColour colours, AudioManager audio) + private void load(OsuColour colours) { Children = new Drawable[] { @@ -115,9 +117,15 @@ namespace osu.Game.Collections }; } + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + audioDucker?.Dispose(); + } + protected override void PopIn() { - musicController?.Duck(100, 1f); + audioDucker = musicController?.Duck(100, 1f, unduckDuration: 100); this.FadeIn(enter_duration, Easing.OutQuint); this.ScaleTo(0.9f).Then().ScaleTo(1f, enter_duration, Easing.OutQuint); @@ -127,7 +135,7 @@ namespace osu.Game.Collections { base.PopOut(); - musicController?.Unduck(100); + audioDucker?.Dispose(); this.FadeOut(exit_duration, Easing.OutQuint); this.ScaleTo(0.9f, exit_duration); diff --git a/osu.Game/Overlays/Dialog/PopupDialogDangerousButton.cs b/osu.Game/Overlays/Dialog/PopupDialogDangerousButton.cs index cdde35c7d0..1878cfd131 100644 --- a/osu.Game/Overlays/Dialog/PopupDialogDangerousButton.cs +++ b/osu.Game/Overlays/Dialog/PopupDialogDangerousButton.cs @@ -47,9 +47,6 @@ namespace osu.Game.Overlays.Dialog private partial class DangerousConfirmContainer : HoldToConfirmContainer { - [Resolved] - private MusicController musicController { get; set; } - public DangerousConfirmContainer() : base(isDangerousAction: true) { @@ -73,15 +70,8 @@ namespace osu.Game.Overlays.Dialog Progress.BindValueChanged(progressChanged); } - protected override void AbortConfirm() - { - musicController?.Unduck(); - base.AbortConfirm(); - } - protected override void Confirm() { - musicController?.Duck(100, 1f); confirmSample?.Play(); base.Confirm(); } diff --git a/osu.Game/Overlays/DialogOverlay.cs b/osu.Game/Overlays/DialogOverlay.cs index aa9bb99e01..57cfa49746 100644 --- a/osu.Game/Overlays/DialogOverlay.cs +++ b/osu.Game/Overlays/DialogOverlay.cs @@ -3,6 +3,7 @@ #nullable disable +using System; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Overlays.Dialog; @@ -29,6 +30,8 @@ namespace osu.Game.Overlays public override bool IsPresent => Scheduler.HasPendingTasks || dialogContainer.Children.Count > 0; + private IDisposable? audioDucker; + public DialogOverlay() { AutoSizeAxes = Axes.Y; @@ -45,6 +48,12 @@ namespace osu.Game.Overlays Origin = Anchor.Centre; } + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + audioDucker?.Dispose(); + } + public void Push(PopupDialog dialog) { if (dialog == CurrentDialog || dialog.State.Value == Visibility.Hidden) return; @@ -95,13 +104,13 @@ namespace osu.Game.Overlays protected override void PopIn() { - musicController.Duck(100, 1f); + audioDucker = musicController.Duck(100, 1f, unduckDuration: 100); } protected override void PopOut() { base.PopOut(); - musicController.Unduck(100); + audioDucker?.Dispose(); // PopOut gets called initially, but we only want to hide dialog when we have been loaded and are present. if (IsLoaded && CurrentDialog?.State.Value == Visibility.Visible) diff --git a/osu.Game/Overlays/MusicController.cs b/osu.Game/Overlays/MusicController.cs index b0e6994448..2952d9d18e 100644 --- a/osu.Game/Overlays/MusicController.cs +++ b/osu.Game/Overlays/MusicController.cs @@ -67,6 +67,7 @@ namespace osu.Game.Overlays private AudioFilter audioDuckFilter; private readonly BindableDouble audioDuckVolume = new BindableDouble(1); + private bool audioDuckActive; [BackgroundDependencyLoader] private void load(AudioManager audio) @@ -262,8 +263,15 @@ namespace osu.Game.Overlays /// Level to drop volume to (1.0 = 100%). /// Cutoff frequency to drop `AudioFilter` to. Use `null` to skip filter effect. /// Easing for the ducking transition. - public void Duck(int duration = 0, float duckVolumeTo = 0.25f, int? duckCutoffTo = 300, Easing easing = Easing.OutCubic) + /// Duration of the unducking transition, in ms. + /// Easing for the unducking transition. + public IDisposable Duck(int duration = 0, float duckVolumeTo = 0.25f, int? duckCutoffTo = 300, Easing easing = Easing.OutCubic, int unduckDuration = 500, Easing unduckEasing = Easing.InCubic) { + if (audioDuckActive) + throw new InvalidOperationException("Cannot perform Duck() while another Duck() is in progress."); + + audioDuckActive = true; + Schedule(() => { if (duckCutoffTo.IsNotNull()) @@ -271,20 +279,8 @@ namespace osu.Game.Overlays this.TransformBindableTo(audioDuckVolume, duckVolumeTo, duration, easing); }); - } - /// - /// Restores the volume to full and stops filtering the currently playing track after having used . - /// - /// Duration of the unducking transition, in ms. - /// Easing for the unducking transition. - public void Unduck(int duration = 500, Easing easing = Easing.InCubic) - { - Schedule(() => - { - audioDuckFilter?.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF, duration, easing); - this.TransformBindableTo(audioDuckVolume, 1, duration, easing); - }); + return new InvokeOnDisposal(() => unduck(unduckDuration, unduckEasing)); } /// @@ -299,8 +295,23 @@ namespace osu.Game.Overlays /// Easing for the ducking transition. public void TimedDuck(int delay, int unduckDuration = 500, Easing unduckEasing = Easing.InCubic, float duckVolumeTo = 0.25f, int? duckCutoffTo = 300, int duckDuration = 0, Easing duckEasing = Easing.OutCubic) { + if (audioDuckActive) return; + Duck(duckDuration, duckVolumeTo, duckCutoffTo, duckEasing); - Scheduler.AddDelayed(() => Unduck(unduckDuration, unduckEasing), delay); + Scheduler.AddDelayed(() => unduck(unduckDuration, unduckEasing), delay); + } + + private void unduck(int duration, Easing easing) + { + if (!audioDuckActive) return; + + audioDuckActive = false; + + Schedule(() => + { + audioDuckFilter?.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF, duration, easing); + this.TransformBindableTo(audioDuckVolume, 1, duration, easing); + }); } private bool next() From 753463fadbb8f50b78c7ad71abcb6a795d6c0aa5 Mon Sep 17 00:00:00 2001 From: Jamie Taylor Date: Thu, 4 Jul 2024 15:01:37 +0900 Subject: [PATCH 1317/2493] Fix code style --- osu.Game/Overlays/DialogOverlay.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/DialogOverlay.cs b/osu.Game/Overlays/DialogOverlay.cs index 57cfa49746..7c52081053 100644 --- a/osu.Game/Overlays/DialogOverlay.cs +++ b/osu.Game/Overlays/DialogOverlay.cs @@ -10,6 +10,7 @@ using osu.Game.Overlays.Dialog; using osu.Game.Graphics.Containers; using osu.Game.Input.Bindings; using System.Linq; +using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Input.Events; @@ -30,7 +31,8 @@ namespace osu.Game.Overlays public override bool IsPresent => Scheduler.HasPendingTasks || dialogContainer.Children.Count > 0; - private IDisposable? audioDucker; + [CanBeNull] + private IDisposable audioDucker; public DialogOverlay() { From 82e4e884d7a3c8ac81b52c0f16b93c5f06a81810 Mon Sep 17 00:00:00 2001 From: Jamie Taylor Date: Thu, 4 Jul 2024 15:51:50 +0900 Subject: [PATCH 1318/2493] Change overlapping `Duck()` usages to be a noop instead of a throw --- osu.Game/Overlays/MusicController.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game/Overlays/MusicController.cs b/osu.Game/Overlays/MusicController.cs index 2952d9d18e..8b52c59bae 100644 --- a/osu.Game/Overlays/MusicController.cs +++ b/osu.Game/Overlays/MusicController.cs @@ -267,8 +267,7 @@ namespace osu.Game.Overlays /// Easing for the unducking transition. public IDisposable Duck(int duration = 0, float duckVolumeTo = 0.25f, int? duckCutoffTo = 300, Easing easing = Easing.OutCubic, int unduckDuration = 500, Easing unduckEasing = Easing.InCubic) { - if (audioDuckActive) - throw new InvalidOperationException("Cannot perform Duck() while another Duck() is in progress."); + if (audioDuckActive) return null; audioDuckActive = true; From a5077fcb3f3e43ce237e3aee5486acaddd20b5aa Mon Sep 17 00:00:00 2001 From: Jamie Taylor Date: Thu, 4 Jul 2024 17:22:33 +0900 Subject: [PATCH 1319/2493] Rename TimedDuck -> DuckMomentarily --- osu.Game/Overlays/MusicController.cs | 2 +- osu.Game/Overlays/Toolbar/ToolbarRulesetSelector.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/MusicController.cs b/osu.Game/Overlays/MusicController.cs index 8b52c59bae..4c563d8845 100644 --- a/osu.Game/Overlays/MusicController.cs +++ b/osu.Game/Overlays/MusicController.cs @@ -292,7 +292,7 @@ namespace osu.Game.Overlays /// Cutoff frequency to drop `AudioFilter` to. Use `null` to skip filter effect. /// Duration of the ducking transition, in ms. /// Easing for the ducking transition. - public void TimedDuck(int delay, int unduckDuration = 500, Easing unduckEasing = Easing.InCubic, float duckVolumeTo = 0.25f, int? duckCutoffTo = 300, int duckDuration = 0, Easing duckEasing = Easing.OutCubic) + public void DuckMomentarily(int delay, int unduckDuration = 500, Easing unduckEasing = Easing.InCubic, float duckVolumeTo = 0.25f, int? duckCutoffTo = 300, int duckDuration = 0, Easing duckEasing = Easing.OutCubic) { if (audioDuckActive) return; diff --git a/osu.Game/Overlays/Toolbar/ToolbarRulesetSelector.cs b/osu.Game/Overlays/Toolbar/ToolbarRulesetSelector.cs index 7da6b76aaa..c39ff60b6b 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarRulesetSelector.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarRulesetSelector.cs @@ -122,7 +122,7 @@ namespace osu.Game.Overlays.Toolbar rulesetSelectionChannel[r.NewValue] = channel; channel.Play(); - musicController?.TimedDuck(600); + musicController?.DuckMomentarily(600); } public override bool HandleNonPositionalInput => !Current.Disabled && base.HandleNonPositionalInput; From b9c6674a5885f6fda92acf72002d68f78006a47e Mon Sep 17 00:00:00 2001 From: OliBomby Date: Thu, 4 Jul 2024 11:47:45 +0200 Subject: [PATCH 1320/2493] Allow seeking to sample point on double-click --- .../Components/Timeline/NodeSamplePointPiece.cs | 10 ++++++++++ .../Compose/Components/Timeline/SamplePointPiece.cs | 10 ++++++++++ 2 files changed, 20 insertions(+) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/NodeSamplePointPiece.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/NodeSamplePointPiece.cs index ae3838bc41..e9999df76d 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/NodeSamplePointPiece.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/NodeSamplePointPiece.cs @@ -2,7 +2,9 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; +using osu.Framework.Extensions; using osu.Framework.Graphics.UserInterface; +using osu.Framework.Input.Events; using osu.Game.Audio; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; @@ -22,6 +24,14 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline NodeIndex = nodeIndex; } + protected override bool OnDoubleClick(DoubleClickEvent e) + { + var hasRepeats = (IHasRepeats)HitObject; + EditorClock?.SeekSmoothlyTo(HitObject.StartTime + hasRepeats.Duration * NodeIndex / hasRepeats.SpanCount()); + this.ShowPopover(); + return true; + } + protected override IList GetSamples() { var hasRepeats = (IHasRepeats)HitObject; diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs index 930b78b468..0507f3d3d0 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs @@ -32,6 +32,9 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline { public readonly HitObject HitObject; + [Resolved] + protected EditorClock? EditorClock { get; private set; } + public SamplePointPiece(HitObject hitObject) { HitObject = hitObject; @@ -54,6 +57,13 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline return true; } + protected override bool OnDoubleClick(DoubleClickEvent e) + { + EditorClock?.SeekSmoothlyTo(HitObject.StartTime); + this.ShowPopover(); + return true; + } + private void updateText() { Label.Text = $"{abbreviateBank(GetBankValue(GetSamples()))} {GetVolumeValue(GetSamples())}"; From 7a0a5620e10ec5f046029311bdf7c561b0529204 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 4 Jul 2024 14:06:18 +0300 Subject: [PATCH 1321/2493] Add failing test case --- .../Editing/TestSceneDifficultySwitching.cs | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneDifficultySwitching.cs b/osu.Game.Tests/Visual/Editing/TestSceneDifficultySwitching.cs index 76ed5063b0..457d4cee34 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneDifficultySwitching.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneDifficultySwitching.cs @@ -12,7 +12,9 @@ using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Overlays.Dialog; using osu.Game.Rulesets; +using osu.Game.Rulesets.Catch; using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.Taiko; using osu.Game.Rulesets.UI; using osu.Game.Screens.Edit; using osu.Game.Storyboards; @@ -169,6 +171,24 @@ namespace osu.Game.Tests.Visual.Editing AddAssert("stack empty", () => Stack.CurrentScreen == null); } + [Test] + public void TestSwitchToDifficultyOfAnotherRuleset() + { + BeatmapInfo targetDifficulty = null; + + AddAssert("ruleset is catch", () => Ruleset.Value.CreateInstance() is CatchRuleset); + + AddStep("set taiko difficulty", () => targetDifficulty = importedBeatmapSet.Beatmaps.First(b => b.Ruleset.OnlineID == 1)); + switchToDifficulty(() => targetDifficulty); + confirmEditingBeatmap(() => targetDifficulty); + + AddAssert("ruleset switched to taiko", () => Ruleset.Value.CreateInstance() is TaikoRuleset); + + AddStep("exit editor forcefully", () => Stack.Exit()); + // ensure editor loader didn't resume. + AddAssert("stack empty", () => Stack.CurrentScreen == null); + } + private void switchToDifficulty(Func difficulty) => AddStep("switch to difficulty", () => Editor.SwitchToDifficulty(difficulty.Invoke())); private void confirmEditingBeatmap(Func targetDifficulty) From 207ee8a2eea0d981efc07715559d9f8c212240b8 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 4 Jul 2024 14:06:36 +0300 Subject: [PATCH 1322/2493] Fix editor not updating ruleset when switching difficulty --- osu.Game/Screens/Edit/EditorLoader.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/EditorLoader.cs b/osu.Game/Screens/Edit/EditorLoader.cs index 8bcfa7b9f0..0e0fb9f795 100644 --- a/osu.Game/Screens/Edit/EditorLoader.cs +++ b/osu.Game/Screens/Edit/EditorLoader.cs @@ -121,7 +121,11 @@ namespace osu.Game.Screens.Edit scheduledDifficultySwitch = Schedule(() => { - Beatmap.Value = nextBeatmap.Invoke(); + var workingBeatmap = nextBeatmap.Invoke(); + + Ruleset.Value = workingBeatmap.BeatmapInfo.Ruleset; + Beatmap.Value = workingBeatmap; + state = editorState; // This screen is a weird exception to the rule that nothing after song select changes the global beatmap. From b2af49c1021d904a61e679835bd3cf2073bee170 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 4 Jul 2024 18:04:38 +0900 Subject: [PATCH 1323/2493] Fix classic fallback not having a transformer (and only add if required) --- .../Skinning/BeatmapSkinProvidingContainer.cs | 34 +++++++++++-------- .../Skinning/RulesetSkinProvidingContainer.cs | 20 +++++++---- 2 files changed, 34 insertions(+), 20 deletions(-) diff --git a/osu.Game/Skinning/BeatmapSkinProvidingContainer.cs b/osu.Game/Skinning/BeatmapSkinProvidingContainer.cs index 4486c8a9f0..94c7a3aac6 100644 --- a/osu.Game/Skinning/BeatmapSkinProvidingContainer.cs +++ b/osu.Game/Skinning/BeatmapSkinProvidingContainer.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -17,9 +15,9 @@ namespace osu.Game.Skinning /// public partial class BeatmapSkinProvidingContainer : SkinProvidingContainer { - private Bindable beatmapSkins; - private Bindable beatmapColours; - private Bindable beatmapHitsounds; + private Bindable beatmapSkins = null!; + private Bindable beatmapColours = null!; + private Bindable beatmapHitsounds = null!; protected override bool AllowConfigurationLookup { @@ -68,11 +66,15 @@ namespace osu.Game.Skinning } private readonly ISkin skin; + private readonly ISkin? classicFallback; - public BeatmapSkinProvidingContainer(ISkin skin) + private Bindable currentSkin = null!; + + public BeatmapSkinProvidingContainer(ISkin skin, ISkin? classicFallback = null) : base(skin) { this.skin = skin; + this.classicFallback = classicFallback; } protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) @@ -93,15 +95,19 @@ namespace osu.Game.Skinning beatmapColours.BindValueChanged(_ => TriggerSourceChanged()); beatmapHitsounds.BindValueChanged(_ => TriggerSourceChanged()); - // If the beatmap skin looks to have skinnable resources, add the default classic skin as a fallback opportunity. - if (skin is LegacySkinTransformer legacySkin && legacySkin.IsProvidingLegacyResources) + currentSkin = skins.CurrentSkin.GetBoundCopy(); + currentSkin.BindValueChanged(_ => { - SetSources(new[] - { - skin, - skins.DefaultClassicSkin - }); - } + bool userSkinIsLegacy = skins.CurrentSkin.Value is LegacySkin; + bool beatmapProvidingResources = skin is LegacySkinTransformer legacySkin && legacySkin.IsProvidingLegacyResources; + + // If the beatmap skin looks to have skinnable resources and the user's skin choice is not a legacy skin, + // add the default classic skin as a fallback opportunity. + if (!userSkinIsLegacy && beatmapProvidingResources && classicFallback != null) + SetSources(new[] { skin, classicFallback }); + else + SetSources(new[] { skin }); + }, true); } } } diff --git a/osu.Game/Skinning/RulesetSkinProvidingContainer.cs b/osu.Game/Skinning/RulesetSkinProvidingContainer.cs index 07e238243b..d736f4cdb5 100644 --- a/osu.Game/Skinning/RulesetSkinProvidingContainer.cs +++ b/osu.Game/Skinning/RulesetSkinProvidingContainer.cs @@ -28,25 +28,33 @@ namespace osu.Game.Skinning protected readonly Ruleset Ruleset; protected readonly IBeatmap Beatmap; + [CanBeNull] + private readonly ISkin beatmapSkin; + /// /// This container already re-exposes all parent sources in a ruleset-usable form. /// Therefore disallow falling back to any parent any further. /// protected override bool AllowFallingBackToParent => false; - protected override Container Content { get; } + protected override Container Content { get; } = new Container + { + RelativeSizeAxes = Axes.Both, + }; public RulesetSkinProvidingContainer(Ruleset ruleset, IBeatmap beatmap, [CanBeNull] ISkin beatmapSkin) { Ruleset = ruleset; Beatmap = beatmap; + this.beatmapSkin = beatmapSkin; + } - InternalChild = new BeatmapSkinProvidingContainer(GetRulesetTransformedSkin(beatmapSkin)) + [BackgroundDependencyLoader] + private void load(SkinManager skinManager) + { + InternalChild = new BeatmapSkinProvidingContainer(GetRulesetTransformedSkin(beatmapSkin), GetRulesetTransformedSkin(skinManager.DefaultClassicSkin)) { - Child = Content = new Container - { - RelativeSizeAxes = Axes.Both, - } + Child = Content, }; } From e3c8bee7d0ec7afd1a4087fd5c68cb67aac28049 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 4 Jul 2024 19:45:41 +0900 Subject: [PATCH 1324/2493] Fix nullability failure --- osu.Game/Skinning/SkinProvidingContainer.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game/Skinning/SkinProvidingContainer.cs b/osu.Game/Skinning/SkinProvidingContainer.cs index acb15da80e..9aff187c9c 100644 --- a/osu.Game/Skinning/SkinProvidingContainer.cs +++ b/osu.Game/Skinning/SkinProvidingContainer.cs @@ -7,6 +7,7 @@ using System.Linq; using osu.Framework.Allocation; using osu.Framework.Audio.Sample; using osu.Framework.Bindables; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Extensions.TypeExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -201,7 +202,10 @@ namespace osu.Game.Skinning source.SourceChanged -= TriggerSourceChanged; } - skinSources = sources.Select(skin => (skin, new DisableableSkinSource(skin, this))).ToArray(); + skinSources = sources + // Shouldn't be required after NRT is applied to all calling sources. + .Where(skin => skin.IsNotNull()) + .Select(skin => (skin, new DisableableSkinSource(skin, this))).ToArray(); foreach (var skin in skinSources) { From b29e535ca5372abc5461c434839c37c869f7ea8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 6 Jun 2024 10:42:09 +0200 Subject: [PATCH 1325/2493] Add results screen for displaying arbitrary daily challenge scores At this point its primary usage is the daily challenge event feed, but the leaderboard will be using this too shortly. Because the playlists results screen that exists in `master` is hard-coupled to showing the *local user's* best result on a given playlist by way of hard-coupling itself to the relevant API request, allowing show of *arbitrary* score by ID requires a whole bunch of subclassery as things stand. Oh well. Class naming is... best effort, due to the above. --- .../TestScenePlaylistsResultsScreen.cs | 2 +- .../Online/Rooms/ShowPlaylistScoreRequest.cs | 23 +++++++ .../DailyChallenge/DailyChallenge.cs | 5 ++ .../DailyChallenge/DailyChallengeEventFeed.cs | 8 ++- .../Multiplayer/MultiplayerResultsScreen.cs | 2 +- ...Screen.cs => PlaylistItemResultsScreen.cs} | 64 +++++++++---------- .../PlaylistItemScoreResultsScreen.cs | 37 +++++++++++ .../PlaylistItemUserResultsScreen.cs | 46 +++++++++++++ .../OnlinePlay/Playlists/PlaylistsPlayer.cs | 2 +- .../Playlists/PlaylistsRoomSubScreen.cs | 2 +- 10 files changed, 152 insertions(+), 39 deletions(-) create mode 100644 osu.Game/Online/Rooms/ShowPlaylistScoreRequest.cs rename osu.Game/Screens/OnlinePlay/Playlists/{PlaylistsResultsScreen.cs => PlaylistItemResultsScreen.cs} (80%) create mode 100644 osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemScoreResultsScreen.cs create mode 100644 osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemUserResultsScreen.cs diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs index fca965052f..a52d29a120 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs @@ -413,7 +413,7 @@ namespace osu.Game.Tests.Visual.Playlists }; } - private partial class TestResultsScreen : PlaylistsResultsScreen + private partial class TestResultsScreen : PlaylistItemUserResultsScreen { public new LoadingSpinner LeftSpinner => base.LeftSpinner; public new LoadingSpinner CentreSpinner => base.CentreSpinner; diff --git a/osu.Game/Online/Rooms/ShowPlaylistScoreRequest.cs b/osu.Game/Online/Rooms/ShowPlaylistScoreRequest.cs new file mode 100644 index 0000000000..d8f977a1d4 --- /dev/null +++ b/osu.Game/Online/Rooms/ShowPlaylistScoreRequest.cs @@ -0,0 +1,23 @@ +// 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.Online.API; + +namespace osu.Game.Online.Rooms +{ + public class ShowPlaylistScoreRequest : APIRequest + { + private readonly long roomId; + private readonly long playlistItemId; + private readonly long scoreId; + + public ShowPlaylistScoreRequest(long roomId, long playlistItemId, long scoreId) + { + this.roomId = roomId; + this.playlistItemId = playlistItemId; + this.scoreId = scoreId; + } + + protected override string Target => $@"rooms/{roomId}/playlist/{playlistItemId}/scores/{scoreId}"; + } +} diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs index b8d0dbbe7d..381c713233 100644 --- a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs @@ -209,6 +209,11 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge feed = new DailyChallengeEventFeed { RelativeSizeAxes = Axes.Both, + PresentScore = id => + { + if (this.IsCurrentScreen()) + this.Push(new PlaylistItemScoreResultsScreen(room.RoomID.Value!.Value, playlistItem, id)); + } } ], }, diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeEventFeed.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeEventFeed.cs index c38a921e43..e76238abad 100644 --- a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeEventFeed.cs +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeEventFeed.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 osu.Framework.Allocation; @@ -19,6 +20,8 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge { private DailyChallengeEventFeedFlow flow = null!; + public Action? PresentScore { get; init; } + [BackgroundDependencyLoader] private void load() { @@ -48,6 +51,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge { Anchor = Anchor.BottomCentre, Origin = Anchor.BottomCentre, + PresentScore = PresentScore, }; flow.Add(row); row.Delay(15000).Then().FadeOut(300, Easing.OutQuint).Expire(); @@ -78,6 +82,8 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge { private readonly NewScoreEvent newScore; + public Action? PresentScore { get; init; } + public NewScoreEventRow(NewScoreEvent newScore) { this.newScore = newScore; @@ -115,7 +121,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge text.AddUserLink(newScore.User); text.AddText(" got "); - text.AddLink($"{newScore.TotalScore:N0} points", () => { }); // TODO: present the score here + text.AddLink($"{newScore.TotalScore:N0} points", () => PresentScore?.Invoke(newScore.ScoreID)); if (newScore.NewRank != null) text.AddText($" and achieved rank #{newScore.NewRank.Value:N0}"); diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerResultsScreen.cs index 6ed75508dc..c439df82a6 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerResultsScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerResultsScreen.cs @@ -7,7 +7,7 @@ using osu.Game.Screens.OnlinePlay.Playlists; namespace osu.Game.Screens.OnlinePlay.Multiplayer { - public partial class MultiplayerResultsScreen : PlaylistsResultsScreen + public partial class MultiplayerResultsScreen : PlaylistItemUserResultsScreen { public MultiplayerResultsScreen(ScoreInfo score, long roomId, PlaylistItem playlistItem) : base(score, roomId, playlistItem) diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs similarity index 80% rename from osu.Game/Screens/OnlinePlay/Playlists/PlaylistsResultsScreen.cs rename to osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs index fdb83b5ae8..51fd912ccc 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsResultsScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs @@ -17,10 +17,10 @@ using osu.Game.Screens.Ranking; namespace osu.Game.Screens.OnlinePlay.Playlists { - public partial class PlaylistsResultsScreen : ResultsScreen + public abstract partial class PlaylistItemResultsScreen : ResultsScreen { - private readonly long roomId; - private readonly PlaylistItem playlistItem; + protected readonly long RoomId; + protected readonly PlaylistItem PlaylistItem; protected LoadingSpinner LeftSpinner { get; private set; } = null!; protected LoadingSpinner CentreSpinner { get; private set; } = null!; @@ -30,19 +30,19 @@ namespace osu.Game.Screens.OnlinePlay.Playlists private MultiplayerScores? lowerScores; [Resolved] - private IAPIProvider api { get; set; } = null!; + protected IAPIProvider API { get; private set; } = null!; [Resolved] - private ScoreManager scoreManager { get; set; } = null!; + protected ScoreManager ScoreManager { get; private set; } = null!; [Resolved] - private RulesetStore rulesets { get; set; } = null!; + protected RulesetStore Rulesets { get; private set; } = null!; - public PlaylistsResultsScreen(ScoreInfo? score, long roomId, PlaylistItem playlistItem) + protected PlaylistItemResultsScreen(ScoreInfo? score, long roomId, PlaylistItem playlistItem) : base(score) { - this.roomId = roomId; - this.playlistItem = playlistItem; + RoomId = roomId; + PlaylistItem = playlistItem; } [BackgroundDependencyLoader] @@ -74,13 +74,15 @@ namespace osu.Game.Screens.OnlinePlay.Playlists }); } - protected override APIRequest FetchScores(Action> scoresCallback) + protected abstract APIRequest CreateScoreRequest(); + + protected sealed override APIRequest FetchScores(Action> scoresCallback) { // This performs two requests: - // 1. A request to show the user's score (and scores around). + // 1. A request to show the relevant score (and scores around). // 2. If that fails, a request to index the room starting from the highest score. - var userScoreReq = new ShowPlaylistUserScoreRequest(roomId, playlistItem.ID, api.LocalUser.Value.Id); + var userScoreReq = CreateScoreRequest(); userScoreReq.Success += userScore => { @@ -111,11 +113,12 @@ namespace osu.Game.Screens.OnlinePlay.Playlists setPositions(lowerScores, userScore.Position.Value, 1); } - performSuccessCallback(scoresCallback, allScores); + Schedule(() => PerformSuccessCallback(scoresCallback, allScores)); + hideLoadingSpinners(); }; // On failure, fallback to a normal index. - userScoreReq.Failure += _ => api.Queue(createIndexRequest(scoresCallback)); + userScoreReq.Failure += _ => API.Queue(createIndexRequest(scoresCallback)); return userScoreReq; } @@ -147,8 +150,8 @@ namespace osu.Game.Screens.OnlinePlay.Playlists private APIRequest createIndexRequest(Action> scoresCallback, MultiplayerScores? pivot = null) { var indexReq = pivot != null - ? new IndexPlaylistScoresRequest(roomId, playlistItem.ID, pivot.Cursor, pivot.Params) - : new IndexPlaylistScoresRequest(roomId, playlistItem.ID); + ? new IndexPlaylistScoresRequest(RoomId, PlaylistItem.ID, pivot.Cursor, pivot.Params) + : new IndexPlaylistScoresRequest(RoomId, PlaylistItem.ID); indexReq.Success += r => { @@ -163,7 +166,11 @@ namespace osu.Game.Screens.OnlinePlay.Playlists setPositions(r, pivot, -1); } - performSuccessCallback(scoresCallback, r.Scores, r); + Schedule(() => + { + PerformSuccessCallback(scoresCallback, r.Scores, r); + hideLoadingSpinners(pivot); + }); }; indexReq.Failure += _ => hideLoadingSpinners(pivot); @@ -177,26 +184,15 @@ namespace osu.Game.Screens.OnlinePlay.Playlists /// The callback to invoke with the final s. /// The s that were retrieved from s. /// An optional pivot around which the scores were retrieved. - private void performSuccessCallback(Action> callback, List scores, MultiplayerScores? pivot = null) => Schedule(() => + protected virtual ScoreInfo[] PerformSuccessCallback(Action> callback, List scores, MultiplayerScores? pivot = null) { - var scoreInfos = scores.Select(s => s.CreateScoreInfo(scoreManager, rulesets, playlistItem, Beatmap.Value.BeatmapInfo)).OrderByTotalScore().ToArray(); + var scoreInfos = scores.Select(s => s.CreateScoreInfo(ScoreManager, Rulesets, PlaylistItem, Beatmap.Value.BeatmapInfo)).OrderByTotalScore().ToArray(); - // Select a score if we don't already have one selected. - // Note: This is done before the callback so that the panel list centres on the selected score before panels are added (eliminating initial scroll). - if (SelectedScore.Value == null) - { - Schedule(() => - { - // Prefer selecting the local user's score, or otherwise default to the first visible score. - SelectedScore.Value = scoreInfos.FirstOrDefault(s => s.User.OnlineID == api.LocalUser.Value.Id) ?? scoreInfos.FirstOrDefault(); - }); - } + // Invoke callback to add the scores. + callback.Invoke(scoreInfos); - // Invoke callback to add the scores. Exclude the user's current score which was added previously. - callback.Invoke(scoreInfos.Where(s => s.OnlineID != Score?.OnlineID)); - - hideLoadingSpinners(pivot); - }); + return scoreInfos; + } private void hideLoadingSpinners(MultiplayerScores? pivot = null) { diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemScoreResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemScoreResultsScreen.cs new file mode 100644 index 0000000000..831b6538a7 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemScoreResultsScreen.cs @@ -0,0 +1,37 @@ +// 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.Online.API; +using osu.Game.Online.Rooms; +using osu.Game.Scoring; + +namespace osu.Game.Screens.OnlinePlay.Playlists +{ + /// + /// Shows a selected arbitrary score for a playlist item, with scores around included. + /// + public partial class PlaylistItemScoreResultsScreen : PlaylistItemResultsScreen + { + private readonly long scoreId; + + public PlaylistItemScoreResultsScreen(long roomId, PlaylistItem playlistItem, long scoreId) + : base(null, roomId, playlistItem) + { + this.scoreId = scoreId; + } + + protected override APIRequest CreateScoreRequest() => new ShowPlaylistScoreRequest(RoomId, PlaylistItem.ID, scoreId); + + protected override ScoreInfo[] PerformSuccessCallback(Action> callback, List scores, MultiplayerScores? pivot = null) + { + var scoreInfos = base.PerformSuccessCallback(callback, scores, pivot); + + Schedule(() => SelectedScore.Value = scoreInfos.SingleOrDefault(score => score.OnlineID == scoreId)); + + return scoreInfos; + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemUserResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemUserResultsScreen.cs new file mode 100644 index 0000000000..e038cf3288 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemUserResultsScreen.cs @@ -0,0 +1,46 @@ +// 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.Online.API; +using osu.Game.Online.Rooms; +using osu.Game.Scoring; + +namespace osu.Game.Screens.OnlinePlay.Playlists +{ + /// + /// Shows the user's best score for a given playlist item, with scores around included. + /// + public partial class PlaylistItemUserResultsScreen : PlaylistItemResultsScreen + { + public PlaylistItemUserResultsScreen(ScoreInfo? score, long roomId, PlaylistItem playlistItem) + : base(score, roomId, playlistItem) + { + } + + protected override APIRequest CreateScoreRequest() => new ShowPlaylistUserScoreRequest(RoomId, PlaylistItem.ID, API.LocalUser.Value.Id); + + protected override ScoreInfo[] PerformSuccessCallback(Action> callback, List scores, MultiplayerScores? pivot = null) + { + var scoreInfos = scores.Select(s => s.CreateScoreInfo(ScoreManager, Rulesets, PlaylistItem, Beatmap.Value.BeatmapInfo)).OrderByTotalScore().ToArray(); + + // Select a score if we don't already have one selected. + // Note: This is done before the callback so that the panel list centres on the selected score before panels are added (eliminating initial scroll). + if (SelectedScore.Value == null) + { + Schedule(() => + { + // Prefer selecting the local user's score, or otherwise default to the first visible score. + SelectedScore.Value = scoreInfos.FirstOrDefault(s => s.User.OnlineID == API.LocalUser.Value.Id) ?? scoreInfos.FirstOrDefault(); + }); + } + + // Invoke callback to add the scores. Exclude the user's current score which was added previously. + callback.Invoke(scoreInfos.Where(s => s.OnlineID != Score?.OnlineID)); + + return scoreInfos; + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs index 48f63731e1..4a2d8f8f6b 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs @@ -58,7 +58,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists protected override ResultsScreen CreateResults(ScoreInfo score) { Debug.Assert(Room.RoomID.Value != null); - return new PlaylistsResultsScreen(score, Room.RoomID.Value.Value, PlaylistItem) + return new PlaylistItemUserResultsScreen(score, Room.RoomID.Value.Value, PlaylistItem) { AllowRetry = true, ShowUserStatistics = true, diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs index 3fb9de428a..3126bbf2eb 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs @@ -114,7 +114,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists RequestResults = item => { Debug.Assert(RoomId.Value != null); - ParentScreen?.Push(new PlaylistsResultsScreen(null, RoomId.Value.Value, item)); + ParentScreen?.Push(new PlaylistItemUserResultsScreen(null, RoomId.Value.Value, item)); } } }, From 8e8909c999b3a7a3df67f96b338eb7da03f68fb3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 2 Jul 2024 09:24:03 +0200 Subject: [PATCH 1326/2493] Adjust daily challenge screen background colour --- osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs index 381c713233..dedfdecf2e 100644 --- a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs @@ -9,7 +9,6 @@ using osu.Framework.Audio; using osu.Framework.Audio.Sample; using osu.Framework.Bindables; using osu.Framework.Extensions; -using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; @@ -161,7 +160,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge new Box { RelativeSizeAxes = Axes.Both, - Colour = Color4Extensions.FromHex(@"3e3a44") // Temporary. + Colour = colourProvider.Background4, }, new GridContainer { @@ -277,7 +276,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge new Box { RelativeSizeAxes = Axes.Both, - Colour = Color4Extensions.FromHex(@"28242d") // Temporary. + Colour = colourProvider.Background5, }, footerButtons = new FillFlowContainer { From 5fa586848d81a03251798b00fb702ed8cb7f4c68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 6 Jun 2024 11:06:22 +0200 Subject: [PATCH 1327/2493] Replace old bad daily challenge leaderboard with new implementation - Actually shows scores rather than playlist aggregates (which are useful... in playlists, where there is more than one item) - Actually allows scores to be shown by clicking on them - Doesn't completely break down visually on smaller window sizes The general appearance is not as polished as the old one in details but I wanted something quick that we can get out by next weekend. Also includes the naive method of refetching scores once a new top 50 score is detected. I can add a stagger if required. --- .../TestSceneDailyChallengeLeaderboard.cs | 142 ++++++++++++++ .../SongSelect/TestSceneLeaderboardScoreV2.cs | 82 +++++--- .../DailyChallenge/DailyChallenge.cs | 33 ++-- .../DailyChallengeLeaderboard.cs | 175 ++++++++++++++++++ .../Leaderboards/LeaderboardScoreV2.cs | 43 +++-- .../OnlinePlay/TestRoomRequestsHandler.cs | 48 +++++ 6 files changed, 459 insertions(+), 64 deletions(-) create mode 100644 osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeLeaderboard.cs create mode 100644 osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeLeaderboard.cs diff --git a/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeLeaderboard.cs b/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeLeaderboard.cs new file mode 100644 index 0000000000..5fff6bb010 --- /dev/null +++ b/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeLeaderboard.cs @@ -0,0 +1,142 @@ +// 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 NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Utils; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Online.Rooms; +using osu.Game.Overlays; +using osu.Game.Rulesets.Scoring; +using osu.Game.Scoring; +using osu.Game.Screens.OnlinePlay.DailyChallenge; +using osuTK; + +namespace osu.Game.Tests.Visual.DailyChallenge +{ + public partial class TestSceneDailyChallengeLeaderboard : OsuTestScene + { + private DummyAPIAccess dummyAPI => (DummyAPIAccess)API; + + [Cached] + private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Plum); + + [Test] + public void TestBasicBehaviour() + { + DailyChallengeLeaderboard leaderboard = null!; + + AddStep("set up response without user best", () => + { + dummyAPI.HandleRequest = req => + { + if (req is IndexPlaylistScoresRequest indexRequest) + { + indexRequest.TriggerSuccess(createResponse(50, false)); + return true; + } + + return false; + }; + }); + AddStep("create leaderboard", () => Child = leaderboard = new DailyChallengeLeaderboard(new Room { RoomID = { Value = 1 } }, new PlaylistItem(Beatmap.Value.BeatmapInfo)) + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(0.8f), + }); + + AddStep("set up response with user best", () => + { + dummyAPI.HandleRequest = req => + { + if (req is IndexPlaylistScoresRequest indexRequest) + { + indexRequest.TriggerSuccess(createResponse(50, true)); + return true; + } + + return false; + }; + }); + AddStep("force refetch", () => leaderboard.RefetchScores()); + } + + [Test] + public void TestLoadingBehaviour() + { + IndexPlaylistScoresRequest pendingRequest = null!; + DailyChallengeLeaderboard leaderboard = null!; + + AddStep("set up requests handler", () => + { + dummyAPI.HandleRequest = req => + { + if (req is IndexPlaylistScoresRequest indexRequest) + { + pendingRequest = indexRequest; + return true; + } + + return false; + }; + }); + AddStep("create leaderboard", () => Child = leaderboard = new DailyChallengeLeaderboard(new Room { RoomID = { Value = 1 } }, new PlaylistItem(Beatmap.Value.BeatmapInfo)) + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(0.8f), + }); + AddStep("complete load", () => pendingRequest.TriggerSuccess(createResponse(3, true))); + AddStep("force refetch", () => leaderboard.RefetchScores()); + AddStep("complete load", () => pendingRequest.TriggerSuccess(createResponse(4, true))); + } + + private IndexedMultiplayerScores createResponse(int scoreCount, bool returnUserBest) + { + var result = new IndexedMultiplayerScores(); + + for (int i = 0; i < scoreCount; ++i) + { + result.Scores.Add(new MultiplayerScore + { + ID = i, + Accuracy = 1 - (float)i / (2 * scoreCount), + Position = i + 1, + EndedAt = DateTimeOffset.Now, + Passed = true, + Rank = (ScoreRank)RNG.Next((int)ScoreRank.D, (int)ScoreRank.XH), + MaxCombo = 1000 - i, + TotalScore = (long)(1_000_000 * (1 - (float)i / (2 * scoreCount))), + User = new APIUser { Username = $"user {i}" }, + Statistics = new Dictionary() + }); + } + + if (returnUserBest) + { + result.UserScore = new MultiplayerScore + { + ID = 99999, + Accuracy = 0.91, + Position = 4, + EndedAt = DateTimeOffset.Now, + Passed = true, + Rank = ScoreRank.A, + MaxCombo = 100, + TotalScore = 800000, + User = dummyAPI.LocalUser.Value, + Statistics = new Dictionary() + }; + } + + return result; + } + } +} diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneLeaderboardScoreV2.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneLeaderboardScoreV2.cs index 0f5eb06df7..33af4907a1 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneLeaderboardScoreV2.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneLeaderboardScoreV2.cs @@ -50,35 +50,73 @@ namespace osu.Game.Tests.Visual.SongSelect }); } - [SetUp] - public void Setup() => Schedule(() => + [Test] + public void TestSheared() { - Children = new Drawable[] + AddStep("create content", () => { - fillFlow = new FillFlowContainer + Children = new Drawable[] { - Width = relativeWidth, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Spacing = new Vector2(0f, 2f), - Shear = new Vector2(OsuGame.SHEAR, 0) - }, - drawWidthText = new OsuSpriteText(), - }; + fillFlow = new FillFlowContainer + { + Width = relativeWidth, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Spacing = new Vector2(0f, 2f), + Shear = new Vector2(OsuGame.SHEAR, 0) + }, + drawWidthText = new OsuSpriteText(), + }; - foreach (var scoreInfo in getTestScores()) + foreach (var scoreInfo in getTestScores()) + { + fillFlow.Add(new LeaderboardScoreV2(scoreInfo) + { + Rank = scoreInfo.Position, + IsPersonalBest = scoreInfo.User.Id == 2, + Shear = Vector2.Zero, + }); + } + + foreach (var score in fillFlow.Children) + score.Show(); + }); + } + + [Test] + public void TestNonSheared() + { + AddStep("create content", () => { - fillFlow.Add(new LeaderboardScoreV2(scoreInfo, scoreInfo.Position, scoreInfo.User.Id == 2) + Children = new Drawable[] { - Shear = Vector2.Zero, - }); - } + fillFlow = new FillFlowContainer + { + Width = relativeWidth, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Spacing = new Vector2(0f, 2f), + }, + drawWidthText = new OsuSpriteText(), + }; - foreach (var score in fillFlow.Children) - score.Show(); - }); + foreach (var scoreInfo in getTestScores()) + { + fillFlow.Add(new LeaderboardScoreV2(scoreInfo) + { + Rank = scoreInfo.Position, + IsPersonalBest = scoreInfo.User.Id == 2, + }); + } + + foreach (var score in fillFlow.Children) + score.Show(); + }); + } [SetUpSteps] public void SetUpSteps() diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs index dedfdecf2e..2d58b3b82c 100644 --- a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs @@ -49,7 +49,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge private readonly Bindable> userMods = new Bindable>(Array.Empty()); private OnlinePlayScreenWaveContainer waves = null!; - private MatchLeaderboard leaderboard = null!; + private DailyChallengeLeaderboard leaderboard = null!; private RoomModSelectOverlay userModsSelectOverlay = null!; private Sample? sampleStart; private IDisposable? userModsSelectOverlayRegistration; @@ -208,33 +208,17 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge feed = new DailyChallengeEventFeed { RelativeSizeAxes = Axes.Both, - PresentScore = id => - { - if (this.IsCurrentScreen()) - this.Push(new PlaylistItemScoreResultsScreen(room.RoomID.Value!.Value, playlistItem, id)); - } + PresentScore = presentScore } ], }, }, null, // Middle column (leaderboard) - new GridContainer + leaderboard = new DailyChallengeLeaderboard(room, playlistItem) { RelativeSizeAxes = Axes.Both, - Content = new[] - { - new Drawable[] - { - new SectionHeader("Leaderboard") - }, - [leaderboard = new MatchLeaderboard { RelativeSizeAxes = Axes.Both }], - }, - RowDimensions = new[] - { - new Dimension(GridSizeMode.AutoSize), - new Dimension(), - } + PresentScore = presentScore, }, // Spacer null, @@ -330,6 +314,12 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge metadataClient.MultiplayerRoomScoreSet += onRoomScoreSet; } + private void presentScore(long id) + { + if (this.IsCurrentScreen()) + this.Push(new PlaylistItemScoreResultsScreen(room.RoomID.Value!.Value, playlistItem, id)); + } + private void onRoomScoreSet(MultiplayerRoomScoreSetEvent e) { if (e.RoomID != room.RoomID.Value || e.PlaylistItemID != playlistItem.ID) @@ -351,6 +341,9 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge { breakdown.AddNewScore(ev); feed.AddNewScore(ev); + + if (e.NewRank <= 50) + Schedule(() => leaderboard.RefetchScores()); }); }); } diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeLeaderboard.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeLeaderboard.cs new file mode 100644 index 0000000000..4d4ae755fc --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeLeaderboard.cs @@ -0,0 +1,175 @@ +// 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 System.Threading; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Beatmaps; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.UserInterface; +using osu.Game.Online.API; +using osu.Game.Online.Rooms; +using osu.Game.Rulesets; +using osu.Game.Scoring; +using osu.Game.Screens.SelectV2.Leaderboards; +using osuTK; + +namespace osu.Game.Screens.OnlinePlay.DailyChallenge +{ + public partial class DailyChallengeLeaderboard : CompositeDrawable + { + public Action? PresentScore { get; init; } + + private readonly Room room; + private readonly PlaylistItem playlistItem; + + private FillFlowContainer scoreFlow = null!; + private Container userBestContainer = null!; + private SectionHeader userBestHeader = null!; + private LoadingLayer loadingLayer = null!; + + private CancellationTokenSource? cancellationTokenSource; + + [Resolved] + private IAPIProvider api { get; set; } = null!; + + [Resolved] + private ScoreManager scoreManager { get; set; } = null!; + + [Resolved] + private RulesetStore rulesets { get; set; } = null!; + + [Resolved] + private IBindable beatmap { get; set; } = null!; + + public DailyChallengeLeaderboard(Room room, PlaylistItem playlistItem) + { + this.room = room; + this.playlistItem = playlistItem; + } + + [BackgroundDependencyLoader] + private void load() + { + InternalChild = new GridContainer + { + RelativeSizeAxes = Axes.Both, + RowDimensions = + [ + new Dimension(GridSizeMode.AutoSize), + new Dimension(), + new Dimension(GridSizeMode.AutoSize), + new Dimension(GridSizeMode.AutoSize) + ], + Content = new[] + { + new Drawable[] { new SectionHeader("Leaderboard") }, + new Drawable[] + { + new Container + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + new OsuScrollContainer + { + RelativeSizeAxes = Axes.Both, + Child = scoreFlow = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding { Right = 20, }, + Direction = FillDirection.Vertical, + Spacing = new Vector2(5), + Scale = new Vector2(0.8f), + Width = 1 / 0.8f, + } + }, + loadingLayer = new LoadingLayer + { + RelativeSizeAxes = Axes.Both, + }, + } + } + }, + new Drawable[] { userBestHeader = new SectionHeader("Personal best") { Alpha = 0, } }, + new Drawable[] + { + userBestContainer = new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding { Right = 20, }, + Scale = new Vector2(0.8f), + Width = 1 / 0.8f, + } + } + } + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + RefetchScores(); + } + + public void RefetchScores() + { + var request = new IndexPlaylistScoresRequest(room.RoomID.Value!.Value, playlistItem.ID); + + request.Success += req => + { + var best = req.Scores.Select(s => s.CreateScoreInfo(scoreManager, rulesets, playlistItem, beatmap.Value.BeatmapInfo)).ToArray(); + var userBest = req.UserScore?.CreateScoreInfo(scoreManager, rulesets, playlistItem, beatmap.Value.BeatmapInfo); + + cancellationTokenSource?.Cancel(); + cancellationTokenSource = null; + cancellationTokenSource ??= new CancellationTokenSource(); + + if (best.Length == 0) + { + scoreFlow.Clear(); + loadingLayer.Hide(); + } + else + { + LoadComponentsAsync(best.Select(s => new LeaderboardScoreV2(s, sheared: false) + { + Rank = s.Position, + IsPersonalBest = s.UserID == api.LocalUser.Value.Id, + Action = () => PresentScore?.Invoke(s.OnlineID), + }), loaded => + { + scoreFlow.Clear(); + scoreFlow.AddRange(loaded); + scoreFlow.FadeTo(1, 400, Easing.OutQuint); + loadingLayer.Hide(); + }, cancellationTokenSource.Token); + } + + userBestContainer.Clear(); + + if (userBest != null) + { + userBestContainer.Add(new LeaderboardScoreV2(userBest, sheared: false) + { + Rank = userBest.Position, + IsPersonalBest = true, + Action = () => PresentScore?.Invoke(userBest.OnlineID), + }); + } + + userBestHeader.FadeTo(userBest == null ? 0 : 1); + }; + + loadingLayer.Show(); + scoreFlow.FadeTo(0.5f, 400, Easing.OutQuint); + api.Queue(request); + } + } +} diff --git a/osu.Game/Screens/SelectV2/Leaderboards/LeaderboardScoreV2.cs b/osu.Game/Screens/SelectV2/Leaderboards/LeaderboardScoreV2.cs index 804a9d24b7..700f889d7f 100644 --- a/osu.Game/Screens/SelectV2/Leaderboards/LeaderboardScoreV2.cs +++ b/osu.Game/Screens/SelectV2/Leaderboards/LeaderboardScoreV2.cs @@ -43,6 +43,9 @@ namespace osu.Game.Screens.SelectV2.Leaderboards { public partial class LeaderboardScoreV2 : OsuClickableContainer, IHasContextMenu, IHasCustomTooltip { + public int? Rank { get; init; } + public bool IsPersonalBest { get; init; } + private const float expanded_right_content_width = 210; private const float grade_width = 40; private const float username_min_width = 125; @@ -52,15 +55,12 @@ namespace osu.Game.Screens.SelectV2.Leaderboards private const float rank_label_visibility_width_cutoff = rank_label_width + height + username_min_width + statistics_regular_min_width + expanded_right_content_width; private readonly ScoreInfo score; + private readonly bool sheared; private const int height = 60; private const int corner_radius = 10; private const int transition_duration = 200; - private readonly int? rank; - - private readonly bool isPersonalBest; - private Colour4 foregroundColour; private Colour4 backgroundColour; private ColourInfo totalScoreBackgroundGradient; @@ -104,13 +104,12 @@ namespace osu.Game.Screens.SelectV2.Leaderboards public ITooltip GetCustomTooltip() => new LeaderboardScoreTooltip(); public virtual ScoreInfo TooltipContent => score; - public LeaderboardScoreV2(ScoreInfo score, int? rank, bool isPersonalBest = false) + public LeaderboardScoreV2(ScoreInfo score, bool sheared = true) { this.score = score; - this.rank = rank; - this.isPersonalBest = isPersonalBest; + this.sheared = sheared; - Shear = new Vector2(OsuGame.SHEAR, 0); + Shear = new Vector2(sheared ? OsuGame.SHEAR : 0, 0); RelativeSizeAxes = Axes.X; Height = height; } @@ -120,8 +119,8 @@ namespace osu.Game.Screens.SelectV2.Leaderboards { var user = score.User; - foregroundColour = isPersonalBest ? colourProvider.Background1 : colourProvider.Background5; - backgroundColour = isPersonalBest ? colourProvider.Background2 : colourProvider.Background4; + foregroundColour = IsPersonalBest ? colourProvider.Background1 : colourProvider.Background5; + backgroundColour = IsPersonalBest ? colourProvider.Background2 : colourProvider.Background4; totalScoreBackgroundGradient = ColourInfo.GradientHorizontal(backgroundColour.Opacity(0), backgroundColour); statisticsLabels = GetStatistics(score).Select(s => new ScoreComponentLabel(s, score) @@ -159,7 +158,7 @@ namespace osu.Game.Screens.SelectV2.Leaderboards { AutoSizeAxes = Axes.X, RelativeSizeAxes = Axes.Y, - Child = rankLabel = new RankLabel(rank) + Child = rankLabel = new RankLabel(Rank, sheared) { Width = rank_label_width, RelativeSizeAxes = Axes.Y, @@ -243,7 +242,7 @@ namespace osu.Game.Screens.SelectV2.Leaderboards { RelativeSizeAxes = Axes.Both, User = score.User, - Shear = new Vector2(-OsuGame.SHEAR, 0), + Shear = new Vector2(sheared ? -OsuGame.SHEAR : 0, 0), Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, Colour = ColourInfo.GradientHorizontal(Colour4.White.Opacity(0.5f), Colour4.FromHex(@"222A27").Opacity(1)), @@ -274,7 +273,7 @@ namespace osu.Game.Screens.SelectV2.Leaderboards Anchor = Anchor.Centre, Origin = Anchor.Centre, Scale = new Vector2(1.1f), - Shear = new Vector2(-OsuGame.SHEAR, 0), + Shear = new Vector2(sheared ? -OsuGame.SHEAR : 0, 0), RelativeSizeAxes = Axes.Both, }) { @@ -292,7 +291,7 @@ namespace osu.Game.Screens.SelectV2.Leaderboards RelativeSizeAxes = Axes.Both, Colour = Colour4.Black.Opacity(0.5f), }, - new RankLabel(rank) + new RankLabel(Rank, sheared) { AutoSizeAxes = Axes.Both, Anchor = Anchor.Centre, @@ -314,7 +313,7 @@ namespace osu.Game.Screens.SelectV2.Leaderboards { flagBadgeAndDateContainer = new FillFlowContainer { - Shear = new Vector2(-OsuGame.SHEAR, 0), + Shear = new Vector2(sheared ? -OsuGame.SHEAR : 0, 0), Direction = FillDirection.Horizontal, Spacing = new Vector2(5), AutoSizeAxes = Axes.Both, @@ -338,7 +337,7 @@ namespace osu.Game.Screens.SelectV2.Leaderboards nameLabel = new TruncatingSpriteText { RelativeSizeAxes = Axes.X, - Shear = new Vector2(-OsuGame.SHEAR, 0), + Shear = new Vector2(sheared ? -OsuGame.SHEAR : 0, 0), Text = user.Username, Font = OsuFont.GetFont(size: 20, weight: FontWeight.SemiBold) } @@ -354,7 +353,7 @@ namespace osu.Game.Screens.SelectV2.Leaderboards Name = @"Statistics container", Padding = new MarginPadding { Right = 40 }, Spacing = new Vector2(25, 0), - Shear = new Vector2(-OsuGame.SHEAR, 0), + Shear = new Vector2(sheared ? -OsuGame.SHEAR : 0, 0), Anchor = Anchor.CentreRight, Origin = Anchor.CentreRight, AutoSizeAxes = Axes.Both, @@ -412,7 +411,7 @@ namespace osu.Game.Screens.SelectV2.Leaderboards }, RankContainer = new Container { - Shear = new Vector2(-OsuGame.SHEAR, 0), + Shear = new Vector2(sheared ? -OsuGame.SHEAR : 0, 0), Anchor = Anchor.CentreRight, Origin = Anchor.CentreRight, RelativeSizeAxes = Axes.Y, @@ -470,7 +469,7 @@ namespace osu.Game.Screens.SelectV2.Leaderboards Anchor = Anchor.TopRight, Origin = Anchor.TopRight, UseFullGlyphHeight = false, - Shear = new Vector2(-OsuGame.SHEAR, 0), + Shear = new Vector2(sheared ? -OsuGame.SHEAR : 0, 0), Current = scoreManager.GetBindableTotalScoreString(score), Font = OsuFont.GetFont(size: 30, weight: FontWeight.Light), }, @@ -478,7 +477,7 @@ namespace osu.Game.Screens.SelectV2.Leaderboards { Anchor = Anchor.TopRight, Origin = Anchor.TopRight, - Shear = new Vector2(-OsuGame.SHEAR, 0), + Shear = new Vector2(sheared ? -OsuGame.SHEAR : 0, 0), AutoSizeAxes = Axes.Both, Direction = FillDirection.Horizontal, Spacing = new Vector2(2f, 0f), @@ -656,14 +655,14 @@ namespace osu.Game.Screens.SelectV2.Leaderboards private partial class RankLabel : Container, IHasTooltip { - public RankLabel(int? rank) + public RankLabel(int? rank, bool sheared) { if (rank >= 1000) TooltipText = $"#{rank:N0}"; Child = new OsuSpriteText { - Shear = new Vector2(-OsuGame.SHEAR, 0), + Shear = new Vector2(sheared ? -OsuGame.SHEAR : 0, 0), Anchor = Anchor.Centre, Origin = Anchor.Centre, Font = OsuFont.GetFont(size: 20, weight: FontWeight.SemiBold, italics: true), diff --git a/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs b/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs index ef4539ba56..36e256b920 100644 --- a/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs +++ b/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs @@ -99,6 +99,54 @@ namespace osu.Game.Tests.Visual.OnlinePlay }); return true; + case IndexPlaylistScoresRequest roomLeaderboardRequest: + roomLeaderboardRequest.TriggerSuccess(new IndexedMultiplayerScores + { + Scores = + { + new MultiplayerScore + { + ID = currentScoreId++, + Accuracy = 1, + Position = 1, + EndedAt = DateTimeOffset.Now, + Passed = true, + Rank = ScoreRank.S, + MaxCombo = 1000, + TotalScore = 1000000, + User = new APIUser { Username = "best user" }, + Statistics = new Dictionary() + }, + new MultiplayerScore + { + ID = currentScoreId++, + Accuracy = 0.7, + Position = 2, + EndedAt = DateTimeOffset.Now, + Passed = true, + Rank = ScoreRank.B, + MaxCombo = 100, + TotalScore = 200000, + User = new APIUser { Username = "worst user" }, + Statistics = new Dictionary() + }, + }, + UserScore = new MultiplayerScore + { + ID = currentScoreId++, + Accuracy = 0.91, + Position = 4, + EndedAt = DateTimeOffset.Now, + Passed = true, + Rank = ScoreRank.A, + MaxCombo = 100, + TotalScore = 800000, + User = localUser, + Statistics = new Dictionary() + }, + }); + return true; + case PartRoomRequest partRoomRequest: partRoomRequest.TriggerSuccess(); return true; From ea4e6cf1d7aac04a664e135a9b43251498ad6167 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 4 Jul 2024 14:39:11 +0200 Subject: [PATCH 1328/2493] Add test coverage --- .../Editing/TestScenePlacementBlueprint.cs | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/osu.Game.Tests/Visual/Editing/TestScenePlacementBlueprint.cs b/osu.Game.Tests/Visual/Editing/TestScenePlacementBlueprint.cs index a5681bea4a..c16533126b 100644 --- a/osu.Game.Tests/Visual/Editing/TestScenePlacementBlueprint.cs +++ b/osu.Game.Tests/Visual/Editing/TestScenePlacementBlueprint.cs @@ -5,11 +5,13 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Screens; using osu.Framework.Testing; +using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Input.Bindings; using osu.Game.Rulesets; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.UI; using osu.Game.Screens.Edit.Compose.Components; using osu.Game.Tests.Beatmaps; @@ -102,5 +104,56 @@ namespace osu.Game.Tests.Visual.Editing AddStep("change tool to circle", () => InputManager.Key(Key.Number2)); AddAssert("slider placed", () => EditorBeatmap.HitObjects.Count, () => Is.EqualTo(1)); } + + [Test] + public void TestAutomaticBankAssignment() + { + AddStep("add object with soft bank", () => EditorBeatmap.Add(new HitCircle + { + StartTime = 0, + Samples = + { + new HitSampleInfo(name: HitSampleInfo.HIT_NORMAL, bank: HitSampleInfo.BANK_SOFT, volume: 70), + new HitSampleInfo(name: HitSampleInfo.HIT_WHISTLE, bank: HitSampleInfo.BANK_SOFT, volume: 70), + } + })); + AddStep("seek to 500", () => EditorClock.Seek(500)); + AddStep("enable automatic bank assignment", () => + { + InputManager.PressKey(Key.LShift); + InputManager.Key(Key.Q); + InputManager.ReleaseKey(Key.LShift); + }); + AddStep("select circle placement tool", () => InputManager.Key(Key.Number2)); + AddStep("move mouse to center of playfield", () => InputManager.MoveMouseTo(this.ChildrenOfType().Single())); + AddStep("place circle", () => InputManager.Click(MouseButton.Left)); + AddAssert("circle has soft bank", () => EditorBeatmap.HitObjects[1].Samples.All(s => s.Bank == HitSampleInfo.BANK_SOFT)); + AddAssert("circle inherited volume", () => EditorBeatmap.HitObjects[1].Samples.All(s => s.Volume == 70)); + } + + [Test] + public void TestVolumeIsInheritedFromLastObject() + { + AddStep("add object with soft bank", () => EditorBeatmap.Add(new HitCircle + { + StartTime = 0, + Samples = + { + new HitSampleInfo(name: HitSampleInfo.HIT_NORMAL, bank: HitSampleInfo.BANK_SOFT, volume: 70), + } + })); + AddStep("seek to 500", () => EditorClock.Seek(500)); + AddStep("select drum bank", () => + { + InputManager.PressKey(Key.LShift); + InputManager.Key(Key.R); + InputManager.ReleaseKey(Key.LShift); + }); + AddStep("select circle placement tool", () => InputManager.Key(Key.Number2)); + AddStep("move mouse to center of playfield", () => InputManager.MoveMouseTo(this.ChildrenOfType().Single())); + AddStep("place circle", () => InputManager.Click(MouseButton.Left)); + AddAssert("circle has drum bank", () => EditorBeatmap.HitObjects[1].Samples.All(s => s.Bank == HitSampleInfo.BANK_DRUM)); + AddAssert("circle inherited volume", () => EditorBeatmap.HitObjects[1].Samples.All(s => s.Volume == 70)); + } } } From 72492a79cdedf0ca75da171125f90dce52dbad31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 4 Jul 2024 14:40:40 +0200 Subject: [PATCH 1329/2493] Reduce duplication in new logic --- osu.Game/Rulesets/Edit/PlacementBlueprint.cs | 24 ++++++++------------ 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/osu.Game/Rulesets/Edit/PlacementBlueprint.cs b/osu.Game/Rulesets/Edit/PlacementBlueprint.cs index 84fe1584dd..63e38bf5de 100644 --- a/osu.Game/Rulesets/Edit/PlacementBlueprint.cs +++ b/osu.Game/Rulesets/Edit/PlacementBlueprint.cs @@ -156,24 +156,20 @@ namespace osu.Game.Rulesets.Edit comboInformation.UpdateComboInformation(getPreviousHitObject() as IHasComboInformation); } - if (AutomaticBankAssignment) - { - // Take the hitnormal sample of the last hit object - var lastHitNormal = getPreviousHitObject()?.Samples?.FirstOrDefault(o => o.Name == HitSampleInfo.HIT_NORMAL); - if (lastHitNormal != null) - HitObject.Samples[0] = lastHitNormal; - } - else - { - // Only inherit the volume from the previous hit object - var lastHitNormal = getPreviousHitObject()?.Samples?.FirstOrDefault(o => o.Name == HitSampleInfo.HIT_NORMAL); + var lastHitNormal = getPreviousHitObject()?.Samples?.FirstOrDefault(o => o.Name == HitSampleInfo.HIT_NORMAL); - if (lastHitNormal != null) + if (lastHitNormal != null) + { + if (AutomaticBankAssignment) { + // Take the hitnormal sample of the last hit object + HitObject.Samples[0] = lastHitNormal; + } + else + { + // Only inherit the volume from the previous hit object for (int i = 0; i < HitObject.Samples.Count; i++) - { HitObject.Samples[i] = HitObject.Samples[i].With(newVolume: lastHitNormal.Volume); - } } } } From 08a77bfe38e740d0df364107383a8a1db9117be4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 4 Jul 2024 15:02:35 +0200 Subject: [PATCH 1330/2493] Extend test coverage --- .../Editing/TestScenePlacementBlueprint.cs | 27 ++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestScenePlacementBlueprint.cs b/osu.Game.Tests/Visual/Editing/TestScenePlacementBlueprint.cs index c16533126b..ee2855354a 100644 --- a/osu.Game.Tests/Visual/Editing/TestScenePlacementBlueprint.cs +++ b/osu.Game.Tests/Visual/Editing/TestScenePlacementBlueprint.cs @@ -114,10 +114,11 @@ namespace osu.Game.Tests.Visual.Editing Samples = { new HitSampleInfo(name: HitSampleInfo.HIT_NORMAL, bank: HitSampleInfo.BANK_SOFT, volume: 70), - new HitSampleInfo(name: HitSampleInfo.HIT_WHISTLE, bank: HitSampleInfo.BANK_SOFT, volume: 70), + new HitSampleInfo(name: HitSampleInfo.HIT_WHISTLE, bank: HitSampleInfo.BANK_DRUM, volume: 70), } })); - AddStep("seek to 500", () => EditorClock.Seek(500)); + + AddStep("seek to 500", () => EditorClock.Seek(500)); // previous object is the one at time 0 AddStep("enable automatic bank assignment", () => { InputManager.PressKey(Key.LShift); @@ -127,8 +128,28 @@ namespace osu.Game.Tests.Visual.Editing AddStep("select circle placement tool", () => InputManager.Key(Key.Number2)); AddStep("move mouse to center of playfield", () => InputManager.MoveMouseTo(this.ChildrenOfType().Single())); AddStep("place circle", () => InputManager.Click(MouseButton.Left)); - AddAssert("circle has soft bank", () => EditorBeatmap.HitObjects[1].Samples.All(s => s.Bank == HitSampleInfo.BANK_SOFT)); + AddAssert("circle has soft bank", () => EditorBeatmap.HitObjects[1].Samples.Single().Bank, () => Is.EqualTo(HitSampleInfo.BANK_SOFT)); AddAssert("circle inherited volume", () => EditorBeatmap.HitObjects[1].Samples.All(s => s.Volume == 70)); + + AddStep("seek to 250", () => EditorClock.Seek(250)); // previous object is the one at time 0 + AddStep("enable clap addition", () => InputManager.Key(Key.R)); + AddStep("select circle placement tool", () => InputManager.Key(Key.Number2)); + AddStep("move mouse to center of playfield", () => InputManager.MoveMouseTo(this.ChildrenOfType().Single())); + AddStep("place circle", () => InputManager.Click(MouseButton.Left)); + AddAssert("circle has 2 samples", () => EditorBeatmap.HitObjects[1].Samples, () => Has.Count.EqualTo(2)); + AddAssert("normal sample has soft bank", () => EditorBeatmap.HitObjects[1].Samples.Single(s => s.Name == HitSampleInfo.HIT_NORMAL).Bank, + () => Is.EqualTo(HitSampleInfo.BANK_SOFT)); + AddAssert("clap sample has drum bank", () => EditorBeatmap.HitObjects[1].Samples.Single(s => s.Name == HitSampleInfo.HIT_CLAP).Bank, + () => Is.EqualTo(HitSampleInfo.BANK_DRUM)); + AddAssert("circle inherited volume", () => EditorBeatmap.HitObjects[1].Samples.All(s => s.Volume == 70)); + + AddStep("seek to 1000", () => EditorClock.Seek(1000)); // previous object is the one at time 500, which has no additions + AddStep("select circle placement tool", () => InputManager.Key(Key.Number2)); + AddStep("move mouse to center of playfield", () => InputManager.MoveMouseTo(this.ChildrenOfType().Single())); + AddStep("place circle", () => InputManager.Click(MouseButton.Left)); + AddAssert("circle has 2 samples", () => EditorBeatmap.HitObjects[3].Samples, () => Has.Count.EqualTo(2)); + AddAssert("all samples have soft bank", () => EditorBeatmap.HitObjects[3].Samples.All(s => s.Bank == HitSampleInfo.BANK_SOFT)); + AddAssert("circle inherited volume", () => EditorBeatmap.HitObjects[3].Samples.All(s => s.Volume == 70)); } [Test] From e005b46df97397126220167289143e335002535a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 4 Jul 2024 15:19:28 +0200 Subject: [PATCH 1331/2493] Extend test coverage --- .../Editing/TestScenePlacementBlueprint.cs | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/osu.Game.Tests/Visual/Editing/TestScenePlacementBlueprint.cs b/osu.Game.Tests/Visual/Editing/TestScenePlacementBlueprint.cs index ee2855354a..e9b442f8dd 100644 --- a/osu.Game.Tests/Visual/Editing/TestScenePlacementBlueprint.cs +++ b/osu.Game.Tests/Visual/Editing/TestScenePlacementBlueprint.cs @@ -10,6 +10,7 @@ using osu.Game.Beatmaps; using osu.Game.Input.Bindings; using osu.Game.Rulesets; using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.UI; @@ -176,5 +177,48 @@ namespace osu.Game.Tests.Visual.Editing AddAssert("circle has drum bank", () => EditorBeatmap.HitObjects[1].Samples.All(s => s.Bank == HitSampleInfo.BANK_DRUM)); AddAssert("circle inherited volume", () => EditorBeatmap.HitObjects[1].Samples.All(s => s.Volume == 70)); } + + [Test] + public void TestNodeSamplesAndSamplesAreSame() + { + Playfield playfield = null!; + + AddStep("select drum bank", () => + { + InputManager.PressKey(Key.LShift); + InputManager.Key(Key.R); + InputManager.ReleaseKey(Key.LShift); + }); + AddStep("enable clap addition", () => InputManager.Key(Key.R)); + + AddStep("select slider placement tool", () => InputManager.Key(Key.Number3)); + AddStep("move mouse to top left of playfield", () => + { + playfield = this.ChildrenOfType().Single(); + var location = (3 * playfield.ScreenSpaceDrawQuad.TopLeft + playfield.ScreenSpaceDrawQuad.BottomRight) / 4; + InputManager.MoveMouseTo(location); + }); + AddStep("begin placement", () => InputManager.Click(MouseButton.Left)); + AddStep("move mouse to bottom right of playfield", () => + { + var location = (playfield.ScreenSpaceDrawQuad.TopLeft + 3 * playfield.ScreenSpaceDrawQuad.BottomRight) / 4; + InputManager.MoveMouseTo(location); + }); + AddStep("confirm via global action", () => + { + globalActionContainer.TriggerPressed(GlobalAction.Select); + globalActionContainer.TriggerReleased(GlobalAction.Select); + }); + AddAssert("slider placed", () => EditorBeatmap.HitObjects.Count, () => Is.EqualTo(1)); + + AddAssert("slider samples have drum bank", () => EditorBeatmap.HitObjects[0].Samples.All(s => s.Bank == HitSampleInfo.BANK_DRUM)); + AddAssert("slider node samples have drum bank", + () => ((IHasRepeats)EditorBeatmap.HitObjects[0]).NodeSamples.SelectMany(s => s).All(s => s.Bank == HitSampleInfo.BANK_DRUM)); + + AddAssert("slider samples have clap addition", + () => EditorBeatmap.HitObjects[0].Samples.Select(s => s.Name), () => Does.Contain(HitSampleInfo.HIT_CLAP)); + AddAssert("slider node samples have clap addition", + () => ((IHasRepeats)EditorBeatmap.HitObjects[0]).NodeSamples.All(samples => samples.Any(s => s.Name == HitSampleInfo.HIT_CLAP))); + } } } From 652d2e963313f53b19b97108c3ad6e454910961c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 4 Jul 2024 15:19:36 +0200 Subject: [PATCH 1332/2493] Adjust code style --- osu.Game/Rulesets/Edit/PlacementBlueprint.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game/Rulesets/Edit/PlacementBlueprint.cs b/osu.Game/Rulesets/Edit/PlacementBlueprint.cs index 8a3fac6d3a..2817e26abd 100644 --- a/osu.Game/Rulesets/Edit/PlacementBlueprint.cs +++ b/osu.Game/Rulesets/Edit/PlacementBlueprint.cs @@ -183,9 +183,7 @@ namespace osu.Game.Rulesets.Edit { // Make sure all the node samples are identical to the hit object's samples for (int i = 0; i < hasRepeats.NodeSamples.Count; i++) - { hasRepeats.NodeSamples[i] = HitObject.Samples.Select(o => o.With()).ToList(); - } } } From 00f7a34139f1a8c4d2e0112b23b7e26464893495 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Thu, 4 Jul 2024 15:25:43 +0200 Subject: [PATCH 1333/2493] Add test coverage --- .../TestSceneHitObjectSampleAdjustments.cs | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneHitObjectSampleAdjustments.cs b/osu.Game.Tests/Visual/Editing/TestSceneHitObjectSampleAdjustments.cs index 9988c1cb59..28bafb79ee 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneHitObjectSampleAdjustments.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneHitObjectSampleAdjustments.cs @@ -7,6 +7,7 @@ using Humanizer; using NUnit.Framework; using osu.Framework.Input; using osu.Framework.Testing; +using osu.Framework.Utils; using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Graphics.UserInterface; @@ -307,6 +308,40 @@ namespace osu.Game.Tests.Visual.Editing hitObjectNodeHasSampleVolume(0, 1, 10); } + [Test] + public void TestSamplePointSeek() + { + AddStep("add slider", () => + { + EditorBeatmap.Clear(); + EditorBeatmap.Add(new Slider + { + Position = new Vector2(256, 256), + StartTime = 0, + Path = new SliderPath(new[] { new PathControlPoint(Vector2.Zero), new PathControlPoint(new Vector2(250, 0)) }), + Samples = + { + new HitSampleInfo(HitSampleInfo.HIT_NORMAL) + }, + NodeSamples = + { + new List { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) }, + new List { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) }, + }, + RepeatCount = 1 + }); + }); + + doubleClickNodeSamplePiece(0, 0); + editorTimeIs(0); + doubleClickNodeSamplePiece(0, 1); + editorTimeIs(813); + doubleClickNodeSamplePiece(0, 2); + editorTimeIs(1627); + doubleClickSamplePiece(0); + editorTimeIs(0); + } + [Test] public void TestHotkeysMultipleSelectionWithSameSampleBank() { @@ -500,6 +535,24 @@ namespace osu.Game.Tests.Visual.Editing InputManager.Click(MouseButton.Left); }); + private void doubleClickSamplePiece(int objectIndex) => AddStep($"double-click {objectIndex.ToOrdinalWords()} sample piece", () => + { + var samplePiece = this.ChildrenOfType().Single(piece => piece is not NodeSamplePointPiece && piece.HitObject == EditorBeatmap.HitObjects.ElementAt(objectIndex)); + + InputManager.MoveMouseTo(samplePiece); + InputManager.Click(MouseButton.Left); + InputManager.Click(MouseButton.Left); + }); + + private void doubleClickNodeSamplePiece(int objectIndex, int nodeIndex) => AddStep($"double-click {objectIndex.ToOrdinalWords()} object {nodeIndex.ToOrdinalWords()} node sample piece", () => + { + var samplePiece = this.ChildrenOfType().Where(piece => piece.HitObject == EditorBeatmap.HitObjects.ElementAt(objectIndex)).ToArray()[nodeIndex]; + + InputManager.MoveMouseTo(samplePiece); + InputManager.Click(MouseButton.Left); + InputManager.Click(MouseButton.Left); + }); + private void samplePopoverHasNoFocus() => AddUntilStep("sample popover textbox not focused", () => { var popover = this.ChildrenOfType().SingleOrDefault(); @@ -644,5 +697,7 @@ namespace osu.Game.Tests.Visual.Editing var h = EditorBeatmap.HitObjects.ElementAt(objectIndex) as IHasRepeats; return h is not null && h.NodeSamples[nodeIndex].Where(o => o.Name != HitSampleInfo.HIT_NORMAL).All(o => o.Bank == bank); }); + + private void editorTimeIs(double time) => AddAssert($"editor time is {time}", () => Precision.AlmostEquals(EditorClock.CurrentTimeAccurate, time, 1)); } } From fae8f5f81b4d2de10fe1e2f2e58f0258158a794b Mon Sep 17 00:00:00 2001 From: smallketchup82 Date: Thu, 4 Jul 2024 17:28:49 -0400 Subject: [PATCH 1334/2493] Refactor VeloUpdateManager --- osu.Desktop/Updater/VeloUpdateManager.cs | 49 +++++++++++------------- 1 file changed, 23 insertions(+), 26 deletions(-) diff --git a/osu.Desktop/Updater/VeloUpdateManager.cs b/osu.Desktop/Updater/VeloUpdateManager.cs index 8fc68f77cd..137e48f135 100644 --- a/osu.Desktop/Updater/VeloUpdateManager.cs +++ b/osu.Desktop/Updater/VeloUpdateManager.cs @@ -10,12 +10,13 @@ using osu.Game.Overlays; using osu.Game.Overlays.Notifications; using osu.Game.Screens.Play; using osu.Game.Updater; +using Velopack.Sources; namespace osu.Desktop.Updater { public partial class VeloUpdateManager : UpdateManager { - private Velopack.UpdateManager? updateManager; + private readonly Velopack.UpdateManager updateManager; private INotificationOverlay notificationOverlay = null!; [Resolved] @@ -24,6 +25,12 @@ namespace osu.Desktop.Updater [Resolved] private ILocalUserPlayInfo? localUserInfo { get; set; } + public VeloUpdateManager() + { + const string? github_token = null; // TODO: populate. + updateManager = new Velopack.UpdateManager(new GithubSource(@"https://github.com/ppy/osu", github_token, false)); + } + [BackgroundDependencyLoader] private void load(INotificationOverlay notifications) { @@ -37,36 +44,30 @@ namespace osu.Desktop.Updater // should we schedule a retry on completion of this check? bool scheduleRecheck = true; - const string? github_token = null; // TODO: populate. - try { // Avoid any kind of update checking while gameplay is running. if (localUserInfo?.IsPlaying.Value == true) return false; - updateManager ??= new Velopack.UpdateManager(new Velopack.Sources.GithubSource(@"https://github.com/ppy/osu", github_token, false)); - var info = await updateManager.CheckForUpdatesAsync().ConfigureAwait(false); + // Handle no updates available. if (info == null) { - // If there is an update pending restart, show the notification again. - if (updateManager.IsUpdatePendingRestart) - { - notificationOverlay.Post(new UpdateApplicationCompleteNotification - { - Activated = () => - { - restartToApplyUpdate(); - return true; - } - }); - return true; - } + // If there's no updates pending restart, bail and retry later. + if (!updateManager.IsUpdatePendingRestart) return false; - // Otherwise there's no updates available. Bail and retry later. - return false; + // If there is an update pending restart, show the notification to restart again. + notificationOverlay.Post(new UpdateApplicationCompleteNotification + { + Activated = () => + { + restartToApplyUpdate(); + return true; + } + }); + return true; } scheduleRecheck = false; @@ -87,8 +88,6 @@ namespace osu.Desktop.Updater { await updateManager.DownloadUpdatesAsync(info, p => notification.Progress = p / 100f).ConfigureAwait(false); - notification.StartInstall(); - notification.State = ProgressNotificationState.Completed; } catch (Exception e) @@ -98,10 +97,11 @@ namespace osu.Desktop.Updater Logger.Error(e, @"update failed!"); } } - catch (Exception) + catch (Exception e) { // we'll ignore this and retry later. can be triggered by no internet connection or thread abortion. scheduleRecheck = true; + Logger.Error(e, @"update check failed!"); } finally { @@ -117,9 +117,6 @@ namespace osu.Desktop.Updater private bool restartToApplyUpdate() { - if (updateManager == null) - return false; - updateManager.WaitExitThenApplyUpdates(null); Schedule(() => game.AttemptExit()); return true; From cae3607caf0d9322497c7be8c0bab4a9d2314cee Mon Sep 17 00:00:00 2001 From: smallketchup82 Date: Thu, 4 Jul 2024 17:30:42 -0400 Subject: [PATCH 1335/2493] Fix up restarting Earlier I changed the restarting logic to not wait until the program exits and instead try to facilitate restarting alone. This did not work, and it became clear we'd need Velopack to do the restarting. This reverts back and supposedly brings restarting logic in line with how Velopack does it --- osu.Desktop/OsuGameDesktop.cs | 13 +++---------- .../Screens/Setup/TournamentSwitcher.cs | 3 ++- osu.Game/OsuGameBase.cs | 2 +- .../Settings/Sections/Graphics/RendererSettings.cs | 3 ++- 4 files changed, 8 insertions(+), 13 deletions(-) diff --git a/osu.Desktop/OsuGameDesktop.cs b/osu.Desktop/OsuGameDesktop.cs index 28f3d3dc5d..c0c8d1e504 100644 --- a/osu.Desktop/OsuGameDesktop.cs +++ b/osu.Desktop/OsuGameDesktop.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Diagnostics; using System.IO; using System.Reflection; using System.Runtime.Versioning; @@ -104,23 +103,17 @@ namespace osu.Desktop return new VeloUpdateManager(); } - public override bool RestartApp() + public override bool RestartAppWhenExited() { try { - var startInfo = new ProcessStartInfo - { - FileName = Process.GetCurrentProcess().MainModule!.FileName, - UseShellExecute = true - }; - Process.Start(startInfo); - base.AttemptExit(); + Velopack.UpdateExe.Start(null, true); return true; } catch (Exception e) { Logger.Error(e, "Failed to restart application"); - return base.RestartApp(); + return base.RestartAppWhenExited(); } } diff --git a/osu.Game.Tournament/Screens/Setup/TournamentSwitcher.cs b/osu.Game.Tournament/Screens/Setup/TournamentSwitcher.cs index 69ead451ba..e55cbc2dbb 100644 --- a/osu.Game.Tournament/Screens/Setup/TournamentSwitcher.cs +++ b/osu.Game.Tournament/Screens/Setup/TournamentSwitcher.cs @@ -31,7 +31,8 @@ namespace osu.Game.Tournament.Screens.Setup Action = () => { - game.RestartApp(); + game.RestartAppWhenExited(); + game.AttemptExit(); }; folderButton.Action = () => storage.PresentExternally(); diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 573695613d..5e4ec5a61d 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -513,7 +513,7 @@ namespace osu.Game /// If supported by the platform, the game will automatically restart after the next exit. /// /// Whether a restart operation was queued. - public virtual bool RestartApp() => false; + public virtual bool RestartAppWhenExited() => false; public bool Migrate(string path) { diff --git a/osu.Game/Overlays/Settings/Sections/Graphics/RendererSettings.cs b/osu.Game/Overlays/Settings/Sections/Graphics/RendererSettings.cs index 17e345c2c8..a8b127d522 100644 --- a/osu.Game/Overlays/Settings/Sections/Graphics/RendererSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Graphics/RendererSettings.cs @@ -67,8 +67,9 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics if (r.NewValue == RendererType.Automatic && automaticRendererInUse) return; - if (game?.RestartApp() == true) + if (game?.RestartAppWhenExited() == true) { + game.AttemptExit(); } else { From c13f24d553a829b0d55ccdad733cee43d1509b5d Mon Sep 17 00:00:00 2001 From: smallketchup82 Date: Thu, 4 Jul 2024 17:32:30 -0400 Subject: [PATCH 1336/2493] Remove InstallingUpdate progress notification Velopack won't install the updates while the program is open, it'll do it in between restarts or before starting. --- osu.Game/Localisation/NotificationsStrings.cs | 5 ----- osu.Game/Updater/UpdateManager.cs | 6 ------ 2 files changed, 11 deletions(-) diff --git a/osu.Game/Localisation/NotificationsStrings.cs b/osu.Game/Localisation/NotificationsStrings.cs index 698fe230b2..d8f768f2d8 100644 --- a/osu.Game/Localisation/NotificationsStrings.cs +++ b/osu.Game/Localisation/NotificationsStrings.cs @@ -135,11 +135,6 @@ Click to see what's new!", version); /// public static LocalisableString DownloadingUpdate => new TranslatableString(getKey(@"downloading_update"), @"Downloading update..."); - /// - /// "Installing update..." - /// - public static LocalisableString InstallingUpdate => new TranslatableString(getKey(@"installing_update"), @"Installing update..."); - private static string getKey(string key) => $@"{prefix}:{key}"; } } diff --git a/osu.Game/Updater/UpdateManager.cs b/osu.Game/Updater/UpdateManager.cs index bcb28d8b14..c114e3a8d0 100644 --- a/osu.Game/Updater/UpdateManager.cs +++ b/osu.Game/Updater/UpdateManager.cs @@ -176,12 +176,6 @@ namespace osu.Game.Updater Text = NotificationsStrings.DownloadingUpdate; } - public void StartInstall() - { - Progress = 0; - Text = NotificationsStrings.InstallingUpdate; - } - public void FailDownload() { State = ProgressNotificationState.Cancelled; From 461b791532ee9abebebc615d2e23f5a38f7324c8 Mon Sep 17 00:00:00 2001 From: smallketchup82 Date: Thu, 4 Jul 2024 17:32:56 -0400 Subject: [PATCH 1337/2493] Remove SimpleUpdateManager No longer needed. Velopack supports the platforms that this covers for --- osu.Game/Updater/SimpleUpdateManager.cs | 116 ------------------------ 1 file changed, 116 deletions(-) delete mode 100644 osu.Game/Updater/SimpleUpdateManager.cs diff --git a/osu.Game/Updater/SimpleUpdateManager.cs b/osu.Game/Updater/SimpleUpdateManager.cs deleted file mode 100644 index 0f9d5b929f..0000000000 --- a/osu.Game/Updater/SimpleUpdateManager.cs +++ /dev/null @@ -1,116 +0,0 @@ -// 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.Diagnostics.CodeAnalysis; -using System.Linq; -using System.Runtime.InteropServices; -using System.Threading.Tasks; -using osu.Framework; -using osu.Framework.Allocation; -using osu.Framework.Graphics.Sprites; -using osu.Framework.Platform; -using osu.Game.Online.API; -using osu.Game.Overlays.Notifications; - -namespace osu.Game.Updater -{ - /// - /// An update manager that shows notifications if a newer release is detected. - /// Installation is left up to the user. - /// - public partial class SimpleUpdateManager : UpdateManager - { - private string version = null!; - - [Resolved] - private GameHost host { get; set; } = null!; - - [BackgroundDependencyLoader] - private void load(OsuGameBase game) - { - version = game.Version; - } - - protected override async Task PerformUpdateCheck() - { - try - { - var releases = new OsuJsonWebRequest("https://api.github.com/repos/ppy/osu/releases/latest"); - - await releases.PerformAsync().ConfigureAwait(false); - - var latest = releases.ResponseObject; - - // avoid any discrepancies due to build suffixes for now. - // eventually we will want to support release streams and consider these. - version = version.Split('-').First(); - string latestTagName = latest.TagName.Split('-').First(); - - if (latestTagName != version && tryGetBestUrl(latest, out string? url)) - { - Notifications.Post(new SimpleNotification - { - 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.Download, - Activated = () => - { - host.OpenUrlExternally(url); - return true; - } - }); - - return true; - } - } - catch - { - // we shouldn't crash on a web failure. or any failure for the matter. - return true; - } - - return false; - } - - private bool tryGetBestUrl(GitHubRelease release, [NotNullWhen(true)] out string? url) - { - url = null; - GitHubAsset? bestAsset = null; - - switch (RuntimeInfo.OS) - { - case RuntimeInfo.Platform.Windows: - bestAsset = release.Assets?.Find(f => f.Name.EndsWith(".exe", StringComparison.Ordinal)); - break; - - case RuntimeInfo.Platform.macOS: - string arch = RuntimeInformation.OSArchitecture == Architecture.Arm64 ? "Apple.Silicon" : "Intel"; - bestAsset = release.Assets?.Find(f => f.Name.EndsWith($".app.{arch}.zip", StringComparison.Ordinal)); - break; - - case RuntimeInfo.Platform.Linux: - bestAsset = release.Assets?.Find(f => f.Name.EndsWith(".AppImage", StringComparison.Ordinal)); - break; - - case RuntimeInfo.Platform.iOS: - if (release.Assets?.Exists(f => f.Name.EndsWith(".ipa", StringComparison.Ordinal)) == true) - // iOS releases are available via testflight. this link seems to work well enough for now. - // see https://stackoverflow.com/a/32960501 - url = "itms-beta://beta.itunes.apple.com/v1/app/1447765923"; - - break; - - case RuntimeInfo.Platform.Android: - if (release.Assets?.Exists(f => f.Name.EndsWith(".apk", StringComparison.Ordinal)) == true) - // on our testing device using the .apk URL causes the download to magically disappear. - url = release.HtmlUrl; - - break; - } - - url ??= bestAsset?.BrowserDownloadUrl; - return url != null; - } - } -} From 9e01cf7fc23812c4d0a76b09dd78ce6b38c06028 Mon Sep 17 00:00:00 2001 From: smallketchup82 Date: Thu, 4 Jul 2024 17:33:05 -0400 Subject: [PATCH 1338/2493] Move setupVelo logic higher up --- osu.Desktop/Program.cs | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/osu.Desktop/Program.cs b/osu.Desktop/Program.cs index 7c23c15d5a..92c8f2104c 100644 --- a/osu.Desktop/Program.cs +++ b/osu.Desktop/Program.cs @@ -30,19 +30,9 @@ namespace osu.Desktop [STAThread] public static void Main(string[] args) { - /* - * WARNING: DO NOT PLACE **ANY** CODE ABOVE THE FOLLOWING BLOCK! - * - * Logic handling Squirrel MUST run before EVERYTHING if you do not want to break it. - * To be more precise: Squirrel is internally using a rather... crude method to determine whether it is running under NUnit, - * namely by checking loaded assemblies: - * https://github.com/clowd/Clowd.Squirrel/blob/24427217482deeeb9f2cacac555525edfc7bd9ac/src/Squirrel/SimpleSplat/PlatformModeDetector.cs#L17-L32 - * - * If it finds ANY assembly from the ones listed above - REGARDLESS of the reason why it is loaded - - * the app will then do completely broken things like: - * - not creating system shortcuts (as the logic is if'd out if "running tests") - * - not exiting after the install / first-update / uninstall hooks are ran (as the `Environment.Exit()` calls are if'd out if "running tests") - */ + // Velopack needs to run before anything else + setupVelo(); + if (OperatingSystem.IsWindows()) { var windowsVersion = Environment.OSVersion.Version; @@ -67,8 +57,6 @@ namespace osu.Desktop } } - setupVelo(); - // NVIDIA profiles are based on the executable name of a process. // Lazer and stable share the same executable name. // Stable sets this setting to "Off", which may not be what we want, so let's force it back to the default "Auto" on startup. From 6a0309294440dfd7136d2ceed72ca19e1be951eb Mon Sep 17 00:00:00 2001 From: smallketchup82 Date: Thu, 4 Jul 2024 17:45:34 -0400 Subject: [PATCH 1339/2493] Reformat --- osu.Desktop/Updater/VeloUpdateManager.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Desktop/Updater/VeloUpdateManager.cs b/osu.Desktop/Updater/VeloUpdateManager.cs index 137e48f135..8aa580caa8 100644 --- a/osu.Desktop/Updater/VeloUpdateManager.cs +++ b/osu.Desktop/Updater/VeloUpdateManager.cs @@ -92,9 +92,9 @@ namespace osu.Desktop.Updater } catch (Exception e) { - // In the case of an error, a separate notification will be displayed. - notification.FailDownload(); - Logger.Error(e, @"update failed!"); + // In the case of an error, a separate notification will be displayed. + notification.FailDownload(); + Logger.Error(e, @"update failed!"); } } catch (Exception e) From 72cf6bb12c71405464ce606a0334b2d6836c1bbc Mon Sep 17 00:00:00 2001 From: smallketchup82 Date: Thu, 4 Jul 2024 18:00:45 -0400 Subject: [PATCH 1340/2493] Allow downgrading Also better address UpdateManager conflict --- osu.Desktop/Updater/VeloUpdateManager.cs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/osu.Desktop/Updater/VeloUpdateManager.cs b/osu.Desktop/Updater/VeloUpdateManager.cs index 8aa580caa8..6d3eb3f3f0 100644 --- a/osu.Desktop/Updater/VeloUpdateManager.cs +++ b/osu.Desktop/Updater/VeloUpdateManager.cs @@ -9,14 +9,15 @@ using osu.Game; using osu.Game.Overlays; using osu.Game.Overlays.Notifications; using osu.Game.Screens.Play; -using osu.Game.Updater; +using Velopack; using Velopack.Sources; +using UpdateManager = Velopack.UpdateManager; namespace osu.Desktop.Updater { - public partial class VeloUpdateManager : UpdateManager + public partial class VeloUpdateManager : Game.Updater.UpdateManager { - private readonly Velopack.UpdateManager updateManager; + private readonly UpdateManager updateManager; private INotificationOverlay notificationOverlay = null!; [Resolved] @@ -28,7 +29,10 @@ namespace osu.Desktop.Updater public VeloUpdateManager() { const string? github_token = null; // TODO: populate. - updateManager = new Velopack.UpdateManager(new GithubSource(@"https://github.com/ppy/osu", github_token, false)); + updateManager = new UpdateManager(new GithubSource(@"https://github.com/ppy/osu", github_token, false), new UpdateOptions + { + AllowVersionDowngrade = true + }); } [BackgroundDependencyLoader] From 4898cff7a4186c3202cdef540c2480b48e22d55d Mon Sep 17 00:00:00 2001 From: smallketchup82 Date: Thu, 4 Jul 2024 18:25:02 -0400 Subject: [PATCH 1341/2493] Restart patch --- osu.Desktop/OsuGameDesktop.cs | 2 +- osu.Desktop/osu.Desktop.csproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Desktop/OsuGameDesktop.cs b/osu.Desktop/OsuGameDesktop.cs index c0c8d1e504..ee73c84ba3 100644 --- a/osu.Desktop/OsuGameDesktop.cs +++ b/osu.Desktop/OsuGameDesktop.cs @@ -107,7 +107,7 @@ namespace osu.Desktop { try { - Velopack.UpdateExe.Start(null, true); + Velopack.UpdateExe.Start(); return true; } catch (Exception e) diff --git a/osu.Desktop/osu.Desktop.csproj b/osu.Desktop/osu.Desktop.csproj index 7df82e1281..7a2bb599fd 100644 --- a/osu.Desktop/osu.Desktop.csproj +++ b/osu.Desktop/osu.Desktop.csproj @@ -26,7 +26,7 @@ - + From f201cc3feaf9af6bed621d06c761595a9b1eb36e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 5 Jul 2024 10:09:06 +0900 Subject: [PATCH 1342/2493] Expand explanation in inline comment --- osu.Game/Skinning/BeatmapSkinProvidingContainer.cs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/osu.Game/Skinning/BeatmapSkinProvidingContainer.cs b/osu.Game/Skinning/BeatmapSkinProvidingContainer.cs index 94c7a3aac6..41fa7fcc66 100644 --- a/osu.Game/Skinning/BeatmapSkinProvidingContainer.cs +++ b/osu.Game/Skinning/BeatmapSkinProvidingContainer.cs @@ -101,8 +101,16 @@ namespace osu.Game.Skinning bool userSkinIsLegacy = skins.CurrentSkin.Value is LegacySkin; bool beatmapProvidingResources = skin is LegacySkinTransformer legacySkin && legacySkin.IsProvidingLegacyResources; - // If the beatmap skin looks to have skinnable resources and the user's skin choice is not a legacy skin, - // add the default classic skin as a fallback opportunity. + // Some beatmaps provide a limited selection of skin elements to add some visual flair. + // In stable, these elements will take lookup priority over the selected skin (whether that be a user skin or default). + // + // To replicate this we need to pay special attention to the fallback order. + // If a user has a non-legacy skin (argon, triangles) selected, the game won't normally fall back to a legacy skin. + // In turn this can create an unexpected visual experience. + // + // So here, check what skin the user has selected. If it's already a legacy skin then we don't need to do anything special. + // If it isn't, we insert the classic default. Note that this is only done if the beatmap seems to be providing skin elements, + // as we only want to override the user's (non-legacy) skin choice when required for beatmap skin visuals. if (!userSkinIsLegacy && beatmapProvidingResources && classicFallback != null) SetSources(new[] { skin, classicFallback }); else From d21eec9542a778d559b2ed679db1489092c8ad91 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 5 Jul 2024 11:55:27 +0900 Subject: [PATCH 1343/2493] Apply nullability to `MusicController` --- osu.Game/Overlays/MusicController.cs | 43 +++++++++++++++------------- 1 file changed, 23 insertions(+), 20 deletions(-) diff --git a/osu.Game/Overlays/MusicController.cs b/osu.Game/Overlays/MusicController.cs index 0986c0513c..ef12d1eba2 100644 --- a/osu.Game/Overlays/MusicController.cs +++ b/osu.Game/Overlays/MusicController.cs @@ -1,12 +1,9 @@ // 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.Collections.Generic; using System.Linq; -using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Track; @@ -28,7 +25,7 @@ namespace osu.Game.Overlays public partial class MusicController : CompositeDrawable { [Resolved] - private BeatmapManager beatmaps { get; set; } + private BeatmapManager beatmaps { get; set; } = null!; /// /// Point in time after which the current track will be restarted on triggering a "previous track" action. @@ -49,25 +46,28 @@ namespace osu.Game.Overlays /// Fired when the global has changed. /// Includes direction information for display purposes. /// - public event Action TrackChanged; + public event Action? TrackChanged; [Resolved] - private IBindable beatmap { get; set; } + private IBindable beatmap { get; set; } = null!; [Resolved] - private IBindable> mods { get; set; } + private IBindable> mods { get; set; } = null!; - [NotNull] public DrawableTrack CurrentTrack { get; private set; } = new DrawableTrack(new TrackVirtual(1000)); [Resolved] - private RealmAccess realm { get; set; } + private RealmAccess realm { get; set; } = null!; protected override void LoadComplete() { base.LoadComplete(); - beatmap.BindValueChanged(b => changeBeatmap(b.NewValue), true); + beatmap.BindValueChanged(b => + { + if (b.NewValue != null) + changeBeatmap(b.NewValue); + }, true); mods.BindValueChanged(_ => ResetTrackAdjustments(), true); } @@ -76,6 +76,9 @@ namespace osu.Game.Overlays /// public void ReloadCurrentTrack() { + if (current == null) + return; + changeTrack(); TrackChanged?.Invoke(current, TrackChangeDirection.None); } @@ -90,7 +93,7 @@ namespace osu.Game.Overlays /// public bool TrackLoaded => CurrentTrack.TrackLoaded; - private ScheduledDelegate seekDelegate; + private ScheduledDelegate? seekDelegate; public void SeekTo(double position) { @@ -192,7 +195,7 @@ namespace osu.Game.Overlays /// Play the previous track or restart the current track if it's current time below . /// /// Invoked when the operation has been performed successfully. - public void PreviousTrack(Action onSuccess = null) => Schedule(() => + public void PreviousTrack(Action? onSuccess = null) => Schedule(() => { PreviousTrackResult res = prev(); if (res != PreviousTrackResult.None) @@ -218,7 +221,7 @@ namespace osu.Game.Overlays queuedDirection = TrackChangeDirection.Prev; - var playableSet = getBeatmapSets().AsEnumerable().TakeWhile(i => !i.Equals(current.BeatmapSetInfo)).LastOrDefault() + var playableSet = getBeatmapSets().AsEnumerable().TakeWhile(i => !i.Equals(current?.BeatmapSetInfo)).LastOrDefault() ?? getBeatmapSets().LastOrDefault(); if (playableSet != null) @@ -236,7 +239,7 @@ namespace osu.Game.Overlays /// /// Invoked when the operation has been performed successfully. /// A of the operation. - public void NextTrack(Action onSuccess = null) => Schedule(() => + public void NextTrack(Action? onSuccess = null) => Schedule(() => { bool res = next(); if (res) @@ -250,7 +253,7 @@ namespace osu.Game.Overlays queuedDirection = TrackChangeDirection.Next; - var playableSet = getBeatmapSets().AsEnumerable().SkipWhile(i => !i.Equals(current.BeatmapSetInfo)).ElementAtOrDefault(1) + var playableSet = getBeatmapSets().AsEnumerable().SkipWhile(i => !i.Equals(current?.BeatmapSetInfo)).ElementAtOrDefault(1) ?? getBeatmapSets().FirstOrDefault(); var playableBeatmap = playableSet?.Beatmaps.FirstOrDefault(); @@ -272,7 +275,7 @@ namespace osu.Game.Overlays Schedule(() => CurrentTrack.RestartAsync()); } - private WorkingBeatmap current; + private WorkingBeatmap? current; private TrackChangeDirection? queuedDirection; @@ -289,7 +292,7 @@ namespace osu.Game.Overlays TrackChangeDirection direction = TrackChangeDirection.None; - bool audioEquals = newWorking?.BeatmapInfo?.AudioEquals(current?.BeatmapInfo) == true; + bool audioEquals = newWorking.BeatmapInfo?.AudioEquals(current?.BeatmapInfo) == true; if (current != null) { @@ -304,7 +307,7 @@ namespace osu.Game.Overlays { // figure out the best direction based on order in playlist. int last = getBeatmapSets().AsEnumerable().TakeWhile(b => !b.Equals(current.BeatmapSetInfo)).Count(); - int next = newWorking == null ? -1 : getBeatmapSets().AsEnumerable().TakeWhile(b => !b.Equals(newWorking.BeatmapSetInfo)).Count(); + int next = getBeatmapSets().AsEnumerable().TakeWhile(b => !b.Equals(newWorking.BeatmapSetInfo)).Count(); direction = last > next ? TrackChangeDirection.Prev : TrackChangeDirection.Next; } @@ -361,7 +364,7 @@ namespace osu.Game.Overlays { // Important to keep this in its own method to avoid inadvertently capturing unnecessary variables in the callback. // Can lead to leaks. - var queuedTrack = new DrawableTrack(current.LoadTrack()); + var queuedTrack = new DrawableTrack(current!.LoadTrack()); queuedTrack.Completed += onTrackCompleted; return queuedTrack; } @@ -390,7 +393,7 @@ namespace osu.Game.Overlays } } - private AudioAdjustments modTrackAdjustments; + private AudioAdjustments? modTrackAdjustments; /// /// Resets the adjustments currently applied on and applies the mod adjustments if is true. From 0696e2df32bd0629f0a1314d9f87fd364074954d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 5 Jul 2024 12:58:18 +0900 Subject: [PATCH 1344/2493] Apply nullability to ducking methods --- osu.Game/Overlays/MusicController.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/osu.Game/Overlays/MusicController.cs b/osu.Game/Overlays/MusicController.cs index 493acb183d..54a0436f2a 100644 --- a/osu.Game/Overlays/MusicController.cs +++ b/osu.Game/Overlays/MusicController.cs @@ -61,8 +61,9 @@ namespace osu.Game.Overlays [Resolved] private RealmAccess realm { get; set; } = null!; - private AudioFilter audioDuckFilter; private readonly BindableDouble audioDuckVolume = new BindableDouble(1); + + private AudioFilter? audioDuckFilter; private bool audioDuckActive; [BackgroundDependencyLoader] @@ -268,7 +269,7 @@ namespace osu.Game.Overlays /// Easing for the ducking transition. /// Duration of the unducking transition, in ms. /// Easing for the unducking transition. - public IDisposable Duck(int duration = 0, float duckVolumeTo = 0.25f, int? duckCutoffTo = 300, Easing easing = Easing.OutCubic, int unduckDuration = 500, Easing unduckEasing = Easing.InCubic) + public IDisposable? Duck(int duration = 0, float duckVolumeTo = 0.25f, int? duckCutoffTo = 300, Easing easing = Easing.OutCubic, int unduckDuration = 500, Easing unduckEasing = Easing.InCubic) { if (audioDuckActive) return null; @@ -295,7 +296,8 @@ namespace osu.Game.Overlays /// Cutoff frequency to drop `AudioFilter` to. Use `null` to skip filter effect. /// Duration of the ducking transition, in ms. /// Easing for the ducking transition. - public void DuckMomentarily(int delay, int unduckDuration = 500, Easing unduckEasing = Easing.InCubic, float duckVolumeTo = 0.25f, int? duckCutoffTo = 300, int duckDuration = 0, Easing duckEasing = Easing.OutCubic) + public void DuckMomentarily(int delay, int unduckDuration = 500, Easing unduckEasing = Easing.InCubic, float duckVolumeTo = 0.25f, int? duckCutoffTo = 300, int duckDuration = 0, + Easing duckEasing = Easing.OutCubic) { if (audioDuckActive) return; From 482ac32f0143e25ad780b7a14e9d3ce398aeae24 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 5 Jul 2024 13:00:10 +0900 Subject: [PATCH 1345/2493] Remove and ignore `encodings.xml` Disappeared in latest EAP and doesn't look like something we would have wanted to be committed in the first place. --- .gitignore | 1 + .idea/.idea.osu.Desktop/.idea/encodings.xml | 4 ---- 2 files changed, 1 insertion(+), 4 deletions(-) delete mode 100644 .idea/.idea.osu.Desktop/.idea/encodings.xml diff --git a/.gitignore b/.gitignore index a51ad09d6c..1fec94d82b 100644 --- a/.gitignore +++ b/.gitignore @@ -266,6 +266,7 @@ __pycache__/ .idea/**/dictionaries .idea/**/shelf .idea/*/.idea/projectSettingsUpdater.xml +.idea/*/.idea/encodings.xml # Generated files .idea/**/contentModel.xml diff --git a/.idea/.idea.osu.Desktop/.idea/encodings.xml b/.idea/.idea.osu.Desktop/.idea/encodings.xml deleted file mode 100644 index 15a15b218a..0000000000 --- a/.idea/.idea.osu.Desktop/.idea/encodings.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file From 4528daf7fa5d663996bc4e9eac6368e7b0951e21 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 5 Jul 2024 13:15:15 +0900 Subject: [PATCH 1346/2493] 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 67c9f2e100..447c783b29 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -36,7 +36,7 @@ - + From ec4623d49f3efec2e06328616d1e2640cc18acc7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 5 Jul 2024 13:51:29 +0900 Subject: [PATCH 1347/2493] Reduce duck length slightly on toolbar ruleset selector --- osu.Game/Overlays/Toolbar/ToolbarRulesetSelector.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Toolbar/ToolbarRulesetSelector.cs b/osu.Game/Overlays/Toolbar/ToolbarRulesetSelector.cs index c39ff60b6b..796477f9f6 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarRulesetSelector.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarRulesetSelector.cs @@ -122,7 +122,7 @@ namespace osu.Game.Overlays.Toolbar rulesetSelectionChannel[r.NewValue] = channel; channel.Play(); - musicController?.DuckMomentarily(600); + musicController?.DuckMomentarily(500); } public override bool HandleNonPositionalInput => !Current.Disabled && base.HandleNonPositionalInput; From 0d858ce8f8eea091b40214a2709d5862021aa33b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 5 Jul 2024 13:51:41 +0900 Subject: [PATCH 1348/2493] Change default easings to `In`/`Out` for all ducking operations --- osu.Game/Overlays/MusicController.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Overlays/MusicController.cs b/osu.Game/Overlays/MusicController.cs index 54a0436f2a..94078a3dec 100644 --- a/osu.Game/Overlays/MusicController.cs +++ b/osu.Game/Overlays/MusicController.cs @@ -269,7 +269,7 @@ namespace osu.Game.Overlays /// Easing for the ducking transition. /// Duration of the unducking transition, in ms. /// Easing for the unducking transition. - public IDisposable? Duck(int duration = 0, float duckVolumeTo = 0.25f, int? duckCutoffTo = 300, Easing easing = Easing.OutCubic, int unduckDuration = 500, Easing unduckEasing = Easing.InCubic) + public IDisposable? Duck(int duration = 0, float duckVolumeTo = 0.25f, int? duckCutoffTo = 300, Easing easing = Easing.Out, int unduckDuration = 500, Easing unduckEasing = Easing.In) { if (audioDuckActive) return null; @@ -296,8 +296,8 @@ namespace osu.Game.Overlays /// Cutoff frequency to drop `AudioFilter` to. Use `null` to skip filter effect. /// Duration of the ducking transition, in ms. /// Easing for the ducking transition. - public void DuckMomentarily(int delay, int unduckDuration = 500, Easing unduckEasing = Easing.InCubic, float duckVolumeTo = 0.25f, int? duckCutoffTo = 300, int duckDuration = 0, - Easing duckEasing = Easing.OutCubic) + public void DuckMomentarily(int delay, int unduckDuration = 500, Easing unduckEasing = Easing.In, float duckVolumeTo = 0.25f, int? duckCutoffTo = 300, int duckDuration = 0, + Easing duckEasing = Easing.Out) { if (audioDuckActive) return; From 554740af1010855598b3f2c40e9dee369438b11b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 5 Jul 2024 14:41:50 +0900 Subject: [PATCH 1349/2493] Adjust ducking API to use a parameters `record` --- .../Collections/ManageCollectionsDialog.cs | 13 ++- osu.Game/Overlays/DialogOverlay.cs | 13 ++- osu.Game/Overlays/MusicController.cs | 104 ++++++++++++------ 3 files changed, 86 insertions(+), 44 deletions(-) diff --git a/osu.Game/Collections/ManageCollectionsDialog.cs b/osu.Game/Collections/ManageCollectionsDialog.cs index 0396fd531c..11d50f3ce4 100644 --- a/osu.Game/Collections/ManageCollectionsDialog.cs +++ b/osu.Game/Collections/ManageCollectionsDialog.cs @@ -24,7 +24,7 @@ namespace osu.Game.Collections protected override string PopInSampleName => @"UI/overlay-big-pop-in"; protected override string PopOutSampleName => @"UI/overlay-big-pop-out"; - private IDisposable? audioDucker; + private IDisposable? duckOperation; [Resolved] private MusicController? musicController { get; set; } @@ -120,12 +120,17 @@ namespace osu.Game.Collections protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); - audioDucker?.Dispose(); + duckOperation?.Dispose(); } protected override void PopIn() { - audioDucker = musicController?.Duck(100, 1f, unduckDuration: 100); + duckOperation = musicController?.Duck(new DuckParameters + { + DuckDuration = 100, + DuckVolumeTo = 1, + RestoreDuration = 100, + }); this.FadeIn(enter_duration, Easing.OutQuint); this.ScaleTo(0.9f).Then().ScaleTo(1f, enter_duration, Easing.OutQuint); @@ -135,7 +140,7 @@ namespace osu.Game.Collections { base.PopOut(); - audioDucker?.Dispose(); + duckOperation?.Dispose(); this.FadeOut(exit_duration, Easing.OutQuint); this.ScaleTo(0.9f, exit_duration); diff --git a/osu.Game/Overlays/DialogOverlay.cs b/osu.Game/Overlays/DialogOverlay.cs index 7c52081053..97d77ae2d3 100644 --- a/osu.Game/Overlays/DialogOverlay.cs +++ b/osu.Game/Overlays/DialogOverlay.cs @@ -32,7 +32,7 @@ namespace osu.Game.Overlays || dialogContainer.Children.Count > 0; [CanBeNull] - private IDisposable audioDucker; + private IDisposable duckOperation; public DialogOverlay() { @@ -53,7 +53,7 @@ namespace osu.Game.Overlays protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); - audioDucker?.Dispose(); + duckOperation?.Dispose(); } public void Push(PopupDialog dialog) @@ -106,13 +106,18 @@ namespace osu.Game.Overlays protected override void PopIn() { - audioDucker = musicController.Duck(100, 1f, unduckDuration: 100); + duckOperation = musicController?.Duck(new DuckParameters + { + DuckDuration = 100, + DuckVolumeTo = 1, + RestoreDuration = 100, + }); } protected override void PopOut() { base.PopOut(); - audioDucker?.Dispose(); + duckOperation?.Dispose(); // PopOut gets called initially, but we only want to hide dialog when we have been loaded and are present. if (IsLoaded && CurrentDialog?.State.Value == Visibility.Visible) diff --git a/osu.Game/Overlays/MusicController.cs b/osu.Game/Overlays/MusicController.cs index 94078a3dec..cd770bef28 100644 --- a/osu.Game/Overlays/MusicController.cs +++ b/osu.Game/Overlays/MusicController.cs @@ -8,7 +8,6 @@ using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Track; using osu.Framework.Bindables; -using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Audio; using osu.Framework.Graphics.Containers; @@ -263,59 +262,53 @@ namespace osu.Game.Overlays /// /// Attenuates the volume and/or filters the currently playing track. /// - /// Duration of the ducking transition, in ms. - /// Level to drop volume to (1.0 = 100%). - /// Cutoff frequency to drop `AudioFilter` to. Use `null` to skip filter effect. - /// Easing for the ducking transition. - /// Duration of the unducking transition, in ms. - /// Easing for the unducking transition. - public IDisposable? Duck(int duration = 0, float duckVolumeTo = 0.25f, int? duckCutoffTo = 300, Easing easing = Easing.Out, int unduckDuration = 500, Easing unduckEasing = Easing.In) + public IDisposable? Duck(DuckParameters? parameters = null) { + parameters ??= new DuckParameters(); + if (audioDuckActive) return null; audioDuckActive = true; Schedule(() => { - if (duckCutoffTo.IsNotNull()) - audioDuckFilter?.CutoffTo((int)duckCutoffTo, duration, easing); + if (parameters.DuckCutoffTo != null) + audioDuckFilter?.CutoffTo(parameters.DuckCutoffTo.Value, parameters.DuckDuration, parameters.DuckEasing); - this.TransformBindableTo(audioDuckVolume, duckVolumeTo, duration, easing); + this.TransformBindableTo(audioDuckVolume, parameters.DuckVolumeTo, parameters.DuckDuration, parameters.DuckEasing); }); - return new InvokeOnDisposal(() => unduck(unduckDuration, unduckEasing)); + return new InvokeOnDisposal(restoreDucking); + + void restoreDucking() + { + if (!audioDuckActive) return; + + audioDuckActive = false; + + Schedule(() => + { + audioDuckFilter?.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF, parameters.RestoreDuration, parameters.RestoreEasing); + this.TransformBindableTo(audioDuckVolume, 1, parameters.RestoreDuration, parameters.RestoreEasing); + }); + } } /// - /// A convenience method that ducks the currently playing track, then after a delay, unducks it. + /// A convenience method that ducks the currently playing track, then after a delay, restores automatically. /// - /// Delay after audio is ducked before unducking begins, in ms. - /// Duration of the unducking transition, in ms. - /// Easing for the unducking transition. - /// Level to drop volume to (1.0 = 100%). - /// Cutoff frequency to drop `AudioFilter` to. Use `null` to skip filter effect. - /// Duration of the ducking transition, in ms. - /// Easing for the ducking transition. - public void DuckMomentarily(int delay, int unduckDuration = 500, Easing unduckEasing = Easing.In, float duckVolumeTo = 0.25f, int? duckCutoffTo = 300, int duckDuration = 0, - Easing duckEasing = Easing.Out) + /// A delay in milliseconds which defines how long to delay restoration after ducking completes. + /// Parameters defining the ducking operation. + public void DuckMomentarily(double delayUntilRestore, DuckParameters? parameters = null) { - if (audioDuckActive) return; + parameters ??= new DuckParameters(); - Duck(duckDuration, duckVolumeTo, duckCutoffTo, duckEasing); - Scheduler.AddDelayed(() => unduck(unduckDuration, unduckEasing), delay); - } + IDisposable? duckOperation = Duck(parameters); - private void unduck(int duration, Easing easing) - { - if (!audioDuckActive) return; + if (duckOperation == null) + return; - audioDuckActive = false; - - Schedule(() => - { - audioDuckFilter?.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF, duration, easing); - this.TransformBindableTo(audioDuckVolume, 1, duration, easing); - }); + Scheduler.AddDelayed(() => duckOperation.Dispose(), delayUntilRestore); } private bool next() @@ -491,6 +484,45 @@ namespace osu.Game.Overlays } } + public record DuckParameters + { + /// + /// The duration of the ducking transition in milliseconds. + /// Defaults to no duration (immediate ducking). + /// + public double DuckDuration = 0; + + /// + /// The final volume which should be reached during ducking, when 0 is silent and 1 is original volume. + /// Defaults to 25%. + /// + public float DuckVolumeTo = 0.25f; + + /// + /// The low-pass cutoff frequency which should be reached during ducking. Use `null` to skip filter effect. + /// Defaults to 300 Hz. + /// + public int? DuckCutoffTo = 300; + + /// + /// The easing curve to be applied during ducking. + /// Defaults to . + /// + public Easing DuckEasing = Easing.Out; + + /// + /// The duration of the restoration transition in milliseconds. + /// Defaults to 500 ms. + /// + public double RestoreDuration = 500; + + /// + /// The easing curve to be applied during restoration. + /// Defaults to . + /// + public Easing RestoreEasing = Easing.In; + } + public enum TrackChangeDirection { None, From 20ba6ca867477ad45ab296013fdda94ea820ba3f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 5 Jul 2024 15:11:11 +0900 Subject: [PATCH 1350/2493] Add mention of return type for `Duck` method --- osu.Game/Overlays/MusicController.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Overlays/MusicController.cs b/osu.Game/Overlays/MusicController.cs index cd770bef28..fb8ba988a7 100644 --- a/osu.Game/Overlays/MusicController.cs +++ b/osu.Game/Overlays/MusicController.cs @@ -262,6 +262,7 @@ namespace osu.Game.Overlays /// /// Attenuates the volume and/or filters the currently playing track. /// + /// A which will restore the duck operation when disposed, or null if another duck operation was already in progress. public IDisposable? Duck(DuckParameters? parameters = null) { parameters ??= new DuckParameters(); From d6c1b5d3991a4e9187e85806697b7f3b1e49206b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 5 Jul 2024 08:54:47 +0200 Subject: [PATCH 1351/2493] Add failing test coverage --- .../TestSceneHitObjectSampleAdjustments.cs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneHitObjectSampleAdjustments.cs b/osu.Game.Tests/Visual/Editing/TestSceneHitObjectSampleAdjustments.cs index 9988c1cb59..558d8dce94 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneHitObjectSampleAdjustments.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneHitObjectSampleAdjustments.cs @@ -484,6 +484,25 @@ namespace osu.Game.Tests.Visual.Editing hitObjectNodeHasSamples(2, 1, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_WHISTLE); } + [Test] + public void TestSelectingObjectDoesNotMutateSamples() + { + clickSamplePiece(0); + toggleAdditionViaPopover(1); + setAdditionBankViaPopover(HitSampleInfo.BANK_SOFT); + dismissPopover(); + + hitObjectHasSamples(0, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_FINISH); + hitObjectHasSampleNormalBank(0, HitSampleInfo.BANK_NORMAL); + hitObjectHasSampleAdditionBank(0, HitSampleInfo.BANK_SOFT); + + AddStep("select first object", () => EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects[0])); + + hitObjectHasSamples(0, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_FINISH); + hitObjectHasSampleNormalBank(0, HitSampleInfo.BANK_NORMAL); + hitObjectHasSampleAdditionBank(0, HitSampleInfo.BANK_SOFT); + } + private void clickSamplePiece(int objectIndex) => AddStep($"click {objectIndex.ToOrdinalWords()} sample piece", () => { var samplePiece = this.ChildrenOfType().Single(piece => piece.HitObject == EditorBeatmap.HitObjects.ElementAt(objectIndex)); From 4c59ec1d94489857b12705a2d195b59aed019f70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 5 Jul 2024 09:10:38 +0200 Subject: [PATCH 1352/2493] Fix incorrect ternary state computation for bank toggles Closes https://github.com/ppy/osu/issues/28741. Regressed in a7b066f3ee59b9e9f13344ce3af4c5e7cf511e67. The intent of the original change there was to ensure that addition banks being set will put the ternary state toggles in indeterminate state (to at least provide a visual indication that the selection does not use a single bank). This would previously not be the case due to the use of `.All()` in the original condition (a single object/node was considered to have a bank enabled if and only if *all* samples within it used it). However the attempt to fix that via switching to `Any()` was not correct. The logic used in the offending commit operates on extracted `Samples` and `NodeSamples` from the selection, and would consider the ternary toggle: - fully off if none of the samples/node samples contained a sample with the given bank, - indeterminate if the some of the samples/node samples contained a sample with the given bank, - fully on if at least one sample from every samples/node samples contained a sample with the given bank. This is a *two-tiered* process, as in first a *binary* on/off state is extracted from each object's samples/node samples, and *then* a ternary state is extracted from all objects/nodes. This is insufficient to express the *desired* behaviour, which is that the toggle should be: - fully off if *none of the individual samples in the selection* use the given bank, - indeterminate if *at least one individual sample in the selection* uses the given bank, - fully on if *all individual samples in the selection* use the given bank. The second wording is flattened, and no longer tries to consider "nodes" or "objects", it just looks at all of the samples in the selection without concern as to whether they're from separate objects/nodes or not. To explain why this discrepancy caused the bug, consider a single object with a `soft` normal bank and `drum` addition bank. Selecting the object would cause a ternary button state update; as per the incorrect logic, there were two samples on the object and each had its own separate banks, so two ternary toggles would have their state set to `True` (rather than the correct `Indeterminate`), thus triggering a bindable feedback loop that would cause one of these banks to win and actually overwrite the other. Note that the addition indeterminate state computation *still* needs to do the two-tiered process, because there it actually makes sense (for a selection to have an addition fully on rather than indeterminate, *every* object/node *must* contain that addition). --- .../Screens/Edit/Compose/Components/EditorSelectionHandler.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/EditorSelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/EditorSelectionHandler.cs index 70c91b16fd..a4efe66bf8 100644 --- a/osu.Game/Screens/Edit/Compose/Components/EditorSelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/EditorSelectionHandler.cs @@ -186,7 +186,7 @@ namespace osu.Game.Screens.Edit.Compose.Components foreach ((string bankName, var bindable) in SelectionBankStates) { - bindable.Value = GetStateFromSelection(samplesInSelection, h => h.Any(s => s.Bank == bankName)); + bindable.Value = GetStateFromSelection(samplesInSelection.SelectMany(s => s), h => h.Bank == bankName); } IEnumerable> enumerateAllSamples(HitObject hitObject) From 71816c09dccf0a17932acb647e749190371e84a0 Mon Sep 17 00:00:00 2001 From: smallketchup82 Date: Fri, 5 Jul 2024 03:29:09 -0400 Subject: [PATCH 1353/2493] Resurrect SimpleUpdateManager as MobileUpdateNotifier While removing the desktop specific logic from it --- osu.Android/OsuGameAndroid.cs | 2 +- osu.Game/Updater/MobileUpdateNotifier.cs | 102 +++++++++++++++++++++++ osu.iOS/OsuGameIOS.cs | 2 +- 3 files changed, 104 insertions(+), 2 deletions(-) create mode 100644 osu.Game/Updater/MobileUpdateNotifier.cs diff --git a/osu.Android/OsuGameAndroid.cs b/osu.Android/OsuGameAndroid.cs index a235913ef3..ffab7dd86d 100644 --- a/osu.Android/OsuGameAndroid.cs +++ b/osu.Android/OsuGameAndroid.cs @@ -80,7 +80,7 @@ namespace osu.Android host.Window.CursorState |= CursorState.Hidden; } - protected override UpdateManager CreateUpdateManager() => new SimpleUpdateManager(); + protected override UpdateManager CreateUpdateManager() => new MobileUpdateNotifier(); protected override BatteryInfo CreateBatteryInfo() => new AndroidBatteryInfo(); diff --git a/osu.Game/Updater/MobileUpdateNotifier.cs b/osu.Game/Updater/MobileUpdateNotifier.cs new file mode 100644 index 0000000000..04b54df3c0 --- /dev/null +++ b/osu.Game/Updater/MobileUpdateNotifier.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 System; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Threading.Tasks; +using osu.Framework; +using osu.Framework.Allocation; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Platform; +using osu.Game.Online.API; +using osu.Game.Overlays.Notifications; + +namespace osu.Game.Updater +{ + /// + /// An update manager that shows notifications if a newer release is detected for mobile platforms. + /// Installation is left up to the user. + /// + public partial class MobileUpdateNotifier : UpdateManager + { + private string version = null!; + + [Resolved] + private GameHost host { get; set; } = null!; + + [BackgroundDependencyLoader] + private void load(OsuGameBase game) + { + version = game.Version; + } + + protected override async Task PerformUpdateCheck() + { + try + { + var releases = new OsuJsonWebRequest("https://api.github.com/repos/ppy/osu/releases/latest"); + + await releases.PerformAsync().ConfigureAwait(false); + + var latest = releases.ResponseObject; + + // avoid any discrepancies due to build suffixes for now. + // eventually we will want to support release streams and consider these. + version = version.Split('-').First(); + string latestTagName = latest.TagName.Split('-').First(); + + if (latestTagName != version && tryGetBestUrl(latest, out string? url)) + { + Notifications.Post(new SimpleNotification + { + 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.Download, + Activated = () => + { + host.OpenUrlExternally(url); + return true; + } + }); + + return true; + } + } + catch + { + // we shouldn't crash on a web failure. or any failure for the matter. + return true; + } + + return false; + } + + private bool tryGetBestUrl(GitHubRelease release, [NotNullWhen(true)] out string? url) + { + url = null; + GitHubAsset? bestAsset = null; + + switch (RuntimeInfo.OS) + { + case RuntimeInfo.Platform.iOS: + if (release.Assets?.Exists(f => f.Name.EndsWith(".ipa", StringComparison.Ordinal)) == true) + // iOS releases are available via testflight. this link seems to work well enough for now. + // see https://stackoverflow.com/a/32960501 + url = "itms-beta://beta.itunes.apple.com/v1/app/1447765923"; + + break; + + case RuntimeInfo.Platform.Android: + if (release.Assets?.Exists(f => f.Name.EndsWith(".apk", StringComparison.Ordinal)) == true) + // on our testing device using the .apk URL causes the download to magically disappear. + url = release.HtmlUrl; + + break; + } + + url ??= bestAsset?.BrowserDownloadUrl; + return url != null; + } + } +} diff --git a/osu.iOS/OsuGameIOS.cs b/osu.iOS/OsuGameIOS.cs index 502f302157..2a4f9b87ac 100644 --- a/osu.iOS/OsuGameIOS.cs +++ b/osu.iOS/OsuGameIOS.cs @@ -15,7 +15,7 @@ namespace osu.iOS { public override Version AssemblyVersion => new Version(NSBundle.MainBundle.InfoDictionary["CFBundleVersion"].ToString()); - protected override UpdateManager CreateUpdateManager() => new SimpleUpdateManager(); + protected override UpdateManager CreateUpdateManager() => new MobileUpdateNotifier(); protected override BatteryInfo CreateBatteryInfo() => new IOSBatteryInfo(); From 717f7ba9f01d0f2ec8a78c71b86c8433c1f8b2c4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 5 Jul 2024 18:12:40 +0900 Subject: [PATCH 1354/2493] Better support multiple concurrent ducking operations --- osu.Game/Overlays/MusicController.cs | 56 ++++++++++++++-------------- 1 file changed, 27 insertions(+), 29 deletions(-) diff --git a/osu.Game/Overlays/MusicController.cs b/osu.Game/Overlays/MusicController.cs index fb8ba988a7..637fcb57e2 100644 --- a/osu.Game/Overlays/MusicController.cs +++ b/osu.Game/Overlays/MusicController.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Audio; @@ -62,8 +63,7 @@ namespace osu.Game.Overlays private readonly BindableDouble audioDuckVolume = new BindableDouble(1); - private AudioFilter? audioDuckFilter; - private bool audioDuckActive; + private AudioFilter audioDuckFilter = null!; [BackgroundDependencyLoader] private void load(AudioManager audio) @@ -259,40 +259,41 @@ namespace osu.Game.Overlays onSuccess?.Invoke(); }); + private readonly List duckOperations = new List(); + /// - /// Attenuates the volume and/or filters the currently playing track. + /// Applies ducking, attenuating the volume and/or low-pass cutoff of the currently playing track to make headroom for effects (or just to apply an effect). /// - /// A which will restore the duck operation when disposed, or null if another duck operation was already in progress. - public IDisposable? Duck(DuckParameters? parameters = null) + /// A which will restore the duck operation when disposed. + public IDisposable Duck(DuckParameters? parameters = null) { parameters ??= new DuckParameters(); - if (audioDuckActive) return null; + if (duckOperations.Contains(parameters)) + throw new ArgumentException("Ducking has already been applied for the provided parameters.", nameof(parameters)); - audioDuckActive = true; + duckOperations.Add(parameters); - Schedule(() => - { - if (parameters.DuckCutoffTo != null) - audioDuckFilter?.CutoffTo(parameters.DuckCutoffTo.Value, parameters.DuckDuration, parameters.DuckEasing); + DuckParameters volumeOperation = duckOperations.MinBy(p => p.DuckVolumeTo)!; + DuckParameters lowPassOperation = duckOperations.MinBy(p => p.DuckCutoffTo)!; - this.TransformBindableTo(audioDuckVolume, parameters.DuckVolumeTo, parameters.DuckDuration, parameters.DuckEasing); - }); + audioDuckFilter.CutoffTo(lowPassOperation.DuckCutoffTo, lowPassOperation.DuckDuration, lowPassOperation.DuckEasing); + this.TransformBindableTo(audioDuckVolume, volumeOperation.DuckVolumeTo, volumeOperation.DuckDuration, volumeOperation.DuckEasing); return new InvokeOnDisposal(restoreDucking); - void restoreDucking() + void restoreDucking() => Schedule(() => { - if (!audioDuckActive) return; + Debug.Assert(duckOperations.Contains(parameters)); + duckOperations.Remove(parameters); - audioDuckActive = false; + DuckParameters? restoreVolumeOperation = duckOperations.MinBy(p => p.DuckVolumeTo); + DuckParameters? restoreLowPassOperation = duckOperations.MinBy(p => p.DuckCutoffTo); - Schedule(() => - { - audioDuckFilter?.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF, parameters.RestoreDuration, parameters.RestoreEasing); - this.TransformBindableTo(audioDuckVolume, 1, parameters.RestoreDuration, parameters.RestoreEasing); - }); - } + // If another duck operation is in the list, restore ducking to its level, else reset back to defaults. + audioDuckFilter.CutoffTo(restoreLowPassOperation?.DuckCutoffTo ?? AudioFilter.MAX_LOWPASS_CUTOFF, parameters.RestoreDuration, parameters.RestoreEasing); + this.TransformBindableTo(audioDuckVolume, restoreVolumeOperation?.DuckVolumeTo ?? 1, parameters.RestoreDuration, parameters.RestoreEasing); + }); } /// @@ -304,10 +305,7 @@ namespace osu.Game.Overlays { parameters ??= new DuckParameters(); - IDisposable? duckOperation = Duck(parameters); - - if (duckOperation == null) - return; + IDisposable duckOperation = Duck(parameters); Scheduler.AddDelayed(() => duckOperation.Dispose(), delayUntilRestore); } @@ -485,7 +483,7 @@ namespace osu.Game.Overlays } } - public record DuckParameters + public class DuckParameters { /// /// The duration of the ducking transition in milliseconds. @@ -500,10 +498,10 @@ namespace osu.Game.Overlays public float DuckVolumeTo = 0.25f; /// - /// The low-pass cutoff frequency which should be reached during ducking. Use `null` to skip filter effect. + /// The low-pass cutoff frequency which should be reached during ducking. If not required, set to . /// Defaults to 300 Hz. /// - public int? DuckCutoffTo = 300; + public int DuckCutoffTo = 300; /// /// The easing curve to be applied during ducking. From 65418aca1adf4ec167bb0ea3f27cad016daa4ac5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 5 Jul 2024 18:27:46 +0900 Subject: [PATCH 1355/2493] Add basic test coverage --- .../Visual/Menus/TestAudioDucking.cs | 134 ++++++++++++++++++ 1 file changed, 134 insertions(+) create mode 100644 osu.Game.Tests/Visual/Menus/TestAudioDucking.cs diff --git a/osu.Game.Tests/Visual/Menus/TestAudioDucking.cs b/osu.Game.Tests/Visual/Menus/TestAudioDucking.cs new file mode 100644 index 0000000000..1639912d29 --- /dev/null +++ b/osu.Game.Tests/Visual/Menus/TestAudioDucking.cs @@ -0,0 +1,134 @@ +// 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 NUnit.Framework; +using osu.Game.Audio.Effects; +using osu.Game.Overlays; + +namespace osu.Game.Tests.Visual.Menus +{ + public partial class TestSceneAudioDucking : OsuGameTestScene + { + [Test] + public void TestMomentaryDuck() + { + AddStep("duck momentarily", () => Game.MusicController.DuckMomentarily(1000, new DuckParameters + { + DuckDuration = 300, + })); + } + + [Test] + public void TestMultipleDucks() + { + IDisposable duckOp1 = null!; + IDisposable duckOp2 = null!; + + double normalVolume = 1; + + AddStep("get initial volume", () => + { + normalVolume = Game.Audio.Tracks.AggregateVolume.Value; + }); + + AddStep("duck one", () => + { + duckOp1 = Game.MusicController.Duck(new DuckParameters + { + DuckVolumeTo = 0.5f, + }); + }); + + AddUntilStep("wait for duck to complete", () => Game.Audio.Tracks.AggregateVolume.Value, () => Is.EqualTo(normalVolume * 0.5f).Within(float.Epsilon)); + + AddStep("duck two", () => + { + duckOp2 = Game.MusicController.Duck(new DuckParameters + { + DuckVolumeTo = 0.2f, + }); + }); + + AddUntilStep("wait for duck to complete", () => Game.Audio.Tracks.AggregateVolume.Value, () => Is.EqualTo(normalVolume * 0.2f).Within(float.Epsilon)); + + AddStep("restore two", () => duckOp2.Dispose()); + AddUntilStep("wait for restore to complete", () => Game.Audio.Tracks.AggregateVolume.Value, () => Is.EqualTo(normalVolume * 0.5f).Within(float.Epsilon)); + + AddStep("restore one", () => duckOp1.Dispose()); + AddUntilStep("wait for restore to complete", () => Game.Audio.Tracks.AggregateVolume.Value, () => Is.EqualTo(normalVolume).Within(float.Epsilon)); + } + + [Test] + public void TestMultipleDucksReverseOrder() + { + IDisposable duckOp1 = null!; + IDisposable duckOp2 = null!; + + double normalVolume = 1; + + AddStep("get initial volume", () => + { + normalVolume = Game.Audio.Tracks.AggregateVolume.Value; + }); + + AddStep("duck one", () => + { + duckOp1 = Game.MusicController.Duck(new DuckParameters + { + DuckVolumeTo = 0.5f, + }); + }); + + AddUntilStep("wait for duck to complete", () => Game.Audio.Tracks.AggregateVolume.Value, () => Is.EqualTo(normalVolume * 0.5f).Within(float.Epsilon)); + + AddStep("duck two", () => + { + duckOp2 = Game.MusicController.Duck(new DuckParameters + { + DuckVolumeTo = 0.2f, + }); + }); + + AddUntilStep("wait for duck to complete", () => Game.Audio.Tracks.AggregateVolume.Value, () => Is.EqualTo(normalVolume * 0.2f).Within(float.Epsilon)); + + AddStep("restore one", () => duckOp1.Dispose()); + + // reverse order, less extreme duck removed so won't change + AddUntilStep("wait for restore to complete", () => Game.Audio.Tracks.AggregateVolume.Value, () => Is.EqualTo(normalVolume * 0.2f).Within(float.Epsilon)); + + AddStep("restore two", () => duckOp2.Dispose()); + AddUntilStep("wait for restore to complete", () => Game.Audio.Tracks.AggregateVolume.Value, () => Is.EqualTo(normalVolume).Within(float.Epsilon)); + } + + [Test] + public void TestMultipleDucksDifferentPieces() + { + IDisposable duckOp1 = null!; + IDisposable duckOp2 = null!; + + AddStep("duck volume", () => + { + duckOp1 = Game.MusicController.Duck(new DuckParameters + { + DuckVolumeTo = 0.2f, + DuckCutoffTo = AudioFilter.MAX_LOWPASS_CUTOFF, + DuckDuration = 500, + }); + }); + + AddStep("duck lowpass", () => + { + duckOp2 = Game.MusicController.Duck(new DuckParameters + { + DuckVolumeTo = 1, + DuckCutoffTo = 300, + DuckDuration = 500, + }); + }); + + AddStep("restore lowpass", () => duckOp2.Dispose()); + AddStep("restore volume", () => duckOp1.Dispose()); + } + } +} From 7efb4ce30a0d50a16503ce0a75d24db5a961be75 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 5 Jul 2024 18:36:40 +0900 Subject: [PATCH 1356/2493] Fix multiple disposals resulting in assert being hit --- osu.Game.Tests/Visual/Menus/TestAudioDucking.cs | 10 ++++++++++ osu.Game/Overlays/MusicController.cs | 5 ++--- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Menus/TestAudioDucking.cs b/osu.Game.Tests/Visual/Menus/TestAudioDucking.cs index 1639912d29..e704fc0e83 100644 --- a/osu.Game.Tests/Visual/Menus/TestAudioDucking.cs +++ b/osu.Game.Tests/Visual/Menus/TestAudioDucking.cs @@ -101,6 +101,16 @@ namespace osu.Game.Tests.Visual.Menus AddUntilStep("wait for restore to complete", () => Game.Audio.Tracks.AggregateVolume.Value, () => Is.EqualTo(normalVolume).Within(float.Epsilon)); } + [Test] + public void TestMultipleDisposalIsNoop() + { + IDisposable duckOp1 = null!; + + AddStep("duck", () => duckOp1 = Game.MusicController.Duck()); + AddStep("restore", () => duckOp1.Dispose()); + AddStep("restore", () => duckOp1.Dispose()); + } + [Test] public void TestMultipleDucksDifferentPieces() { diff --git a/osu.Game/Overlays/MusicController.cs b/osu.Game/Overlays/MusicController.cs index 637fcb57e2..8fa77b6153 100644 --- a/osu.Game/Overlays/MusicController.cs +++ b/osu.Game/Overlays/MusicController.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Audio; @@ -284,8 +283,8 @@ namespace osu.Game.Overlays void restoreDucking() => Schedule(() => { - Debug.Assert(duckOperations.Contains(parameters)); - duckOperations.Remove(parameters); + if (!duckOperations.Remove(parameters)) + return; DuckParameters? restoreVolumeOperation = duckOperations.MinBy(p => p.DuckVolumeTo); DuckParameters? restoreLowPassOperation = duckOperations.MinBy(p => p.DuckCutoffTo); From 5907e0d1eb590dfbd55f79843dd31a041d78f3c0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 5 Jul 2024 18:38:24 +0900 Subject: [PATCH 1357/2493] Make `DuckDuration` non-zero by default --- osu.Game.Tests/Visual/Menus/TestAudioDucking.cs | 5 +---- osu.Game/Collections/ManageCollectionsDialog.cs | 2 +- osu.Game/Overlays/DialogOverlay.cs | 2 +- osu.Game/Overlays/MusicController.cs | 4 ++-- osu.Game/Overlays/Toolbar/ToolbarRulesetSelector.cs | 2 +- 5 files changed, 6 insertions(+), 9 deletions(-) diff --git a/osu.Game.Tests/Visual/Menus/TestAudioDucking.cs b/osu.Game.Tests/Visual/Menus/TestAudioDucking.cs index e704fc0e83..459bdd3e2e 100644 --- a/osu.Game.Tests/Visual/Menus/TestAudioDucking.cs +++ b/osu.Game.Tests/Visual/Menus/TestAudioDucking.cs @@ -13,10 +13,7 @@ namespace osu.Game.Tests.Visual.Menus [Test] public void TestMomentaryDuck() { - AddStep("duck momentarily", () => Game.MusicController.DuckMomentarily(1000, new DuckParameters - { - DuckDuration = 300, - })); + AddStep("duck momentarily", () => Game.MusicController.DuckMomentarily(1000)); } [Test] diff --git a/osu.Game/Collections/ManageCollectionsDialog.cs b/osu.Game/Collections/ManageCollectionsDialog.cs index 11d50f3ce4..9f8158af53 100644 --- a/osu.Game/Collections/ManageCollectionsDialog.cs +++ b/osu.Game/Collections/ManageCollectionsDialog.cs @@ -127,8 +127,8 @@ namespace osu.Game.Collections { duckOperation = musicController?.Duck(new DuckParameters { - DuckDuration = 100, DuckVolumeTo = 1, + DuckDuration = 100, RestoreDuration = 100, }); diff --git a/osu.Game/Overlays/DialogOverlay.cs b/osu.Game/Overlays/DialogOverlay.cs index 97d77ae2d3..4e7aff84bc 100644 --- a/osu.Game/Overlays/DialogOverlay.cs +++ b/osu.Game/Overlays/DialogOverlay.cs @@ -108,8 +108,8 @@ namespace osu.Game.Overlays { duckOperation = musicController?.Duck(new DuckParameters { - DuckDuration = 100, DuckVolumeTo = 1, + DuckDuration = 100, RestoreDuration = 100, }); } diff --git a/osu.Game/Overlays/MusicController.cs b/osu.Game/Overlays/MusicController.cs index 8fa77b6153..c4817a8f26 100644 --- a/osu.Game/Overlays/MusicController.cs +++ b/osu.Game/Overlays/MusicController.cs @@ -486,9 +486,9 @@ namespace osu.Game.Overlays { /// /// The duration of the ducking transition in milliseconds. - /// Defaults to no duration (immediate ducking). + /// Defaults to 100 ms. /// - public double DuckDuration = 0; + public double DuckDuration = 100; /// /// The final volume which should be reached during ducking, when 0 is silent and 1 is original volume. diff --git a/osu.Game/Overlays/Toolbar/ToolbarRulesetSelector.cs b/osu.Game/Overlays/Toolbar/ToolbarRulesetSelector.cs index 796477f9f6..05ab505417 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarRulesetSelector.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarRulesetSelector.cs @@ -122,7 +122,7 @@ namespace osu.Game.Overlays.Toolbar rulesetSelectionChannel[r.NewValue] = channel; channel.Play(); - musicController?.DuckMomentarily(500); + musicController?.DuckMomentarily(500, new DuckParameters { DuckDuration = 0 }); } public override bool HandleNonPositionalInput => !Current.Disabled && base.HandleNonPositionalInput; From 98610f4f6d1536842febaec22659bcf75b021872 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Fri, 5 Jul 2024 12:41:50 +0200 Subject: [PATCH 1358/2493] alt left/right or scroll to seek to neighbouring hit objects --- osu.Game/Screens/Edit/Editor.cs | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index c00b7ac4f2..c50cd09dd8 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -593,7 +593,7 @@ namespace osu.Game.Screens.Edit protected override bool OnKeyDown(KeyDownEvent e) { - if (e.ControlPressed || e.AltPressed || e.SuperPressed) return false; + if (e.ControlPressed || e.SuperPressed) return false; switch (e.Key) { @@ -674,7 +674,7 @@ namespace osu.Game.Screens.Edit protected override bool OnScroll(ScrollEvent e) { - if (e.ControlPressed || e.AltPressed || e.SuperPressed) + if (e.ControlPressed || e.SuperPressed) return false; const double precision = 1; @@ -1064,8 +1064,24 @@ namespace osu.Game.Screens.Edit clock.Seek(found.Time); } + private void seekHitObject(int direction) + { + var found = direction < 1 + ? editorBeatmap.HitObjects.LastOrDefault(p => p.StartTime < clock.CurrentTimeAccurate) + : editorBeatmap.HitObjects.FirstOrDefault(p => p.StartTime > clock.CurrentTimeAccurate); + + if (found != null) + clock.SeekSmoothlyTo(found.StartTime); + } + private void seek(UIEvent e, int direction) { + if (e.AltPressed) + { + seekHitObject(direction); + return; + } + double amount = e.ShiftPressed ? 4 : 1; bool trackPlaying = clock.IsRunning; From 7d6ade7e844df0abde9a4a25e0725c26db62d4d5 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Fri, 5 Jul 2024 14:16:51 +0200 Subject: [PATCH 1359/2493] shift alt seek to open next sample edit popover --- .../Timeline/NodeSamplePointPiece.cs | 8 +-- .../Components/Timeline/SamplePointPiece.cs | 27 ++++++++- osu.Game/Screens/Edit/Editor.cs | 58 ++++++++++++++++++- 3 files changed, 84 insertions(+), 9 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/NodeSamplePointPiece.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/NodeSamplePointPiece.cs index e9999df76d..1245d94a92 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/NodeSamplePointPiece.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/NodeSamplePointPiece.cs @@ -2,9 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; -using osu.Framework.Extensions; using osu.Framework.Graphics.UserInterface; -using osu.Framework.Input.Events; using osu.Game.Audio; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; @@ -24,12 +22,10 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline NodeIndex = nodeIndex; } - protected override bool OnDoubleClick(DoubleClickEvent e) + protected override double GetTime() { var hasRepeats = (IHasRepeats)HitObject; - EditorClock?.SeekSmoothlyTo(HitObject.StartTime + hasRepeats.Duration * NodeIndex / hasRepeats.SpanCount()); - this.ShowPopover(); - return true; + return HitObject.StartTime + hasRepeats.Duration * NodeIndex / hasRepeats.SpanCount(); } protected override IList GetSamples() diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs index 0507f3d3d0..8c05a8806e 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs @@ -21,6 +21,7 @@ using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Rulesets.Objects; using osu.Game.Screens.Edit.Components.TernaryButtons; using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Objects.Types; using osu.Game.Screens.Edit.Timing; using osuTK; using osuTK.Graphics; @@ -33,7 +34,10 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline public readonly HitObject HitObject; [Resolved] - protected EditorClock? EditorClock { get; private set; } + private EditorClock? editorClock { get; set; } + + [Resolved] + private Editor? editor { get; set; } public SamplePointPiece(HitObject hitObject) { @@ -44,11 +48,30 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline protected override Color4 GetRepresentingColour(OsuColour colours) => AlternativeColor ? colours.Pink2 : colours.Pink1; + protected virtual double GetTime() => HitObject is IHasRepeats r ? HitObject.StartTime + r.Duration / r.SpanCount() / 2 : HitObject.StartTime; + [BackgroundDependencyLoader] private void load() { HitObject.DefaultsApplied += _ => updateText(); updateText(); + + if (editor != null) + editor.ShowSampleEditPopoverRequested += OnShowSampleEditPopoverRequested; + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (editor != null) + editor.ShowSampleEditPopoverRequested -= OnShowSampleEditPopoverRequested; + } + + private void OnShowSampleEditPopoverRequested(double time) + { + if (time == GetTime()) + this.ShowPopover(); } protected override bool OnClick(ClickEvent e) @@ -59,7 +82,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline protected override bool OnDoubleClick(DoubleClickEvent e) { - EditorClock?.SeekSmoothlyTo(HitObject.StartTime); + editorClock?.SeekSmoothlyTo(GetTime()); this.ShowPopover(); return true; } diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index c50cd09dd8..973908dfcb 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -43,6 +43,7 @@ using osu.Game.Overlays.OSD; using osu.Game.Rulesets; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; using osu.Game.Screens.Edit.Components.Menus; using osu.Game.Screens.Edit.Compose; using osu.Game.Screens.Edit.Compose.Components.Timeline; @@ -1074,11 +1075,66 @@ namespace osu.Game.Screens.Edit clock.SeekSmoothlyTo(found.StartTime); } + [CanBeNull] + public event Action ShowSampleEditPopoverRequested; + + private void seekSamplePoint(int direction) + { + double currentTime = clock.CurrentTimeAccurate; + + var current = direction < 1 + ? editorBeatmap.HitObjects.LastOrDefault(p => p is IHasRepeats r && p.StartTime < currentTime && r.EndTime >= currentTime) + : editorBeatmap.HitObjects.LastOrDefault(p => p is IHasRepeats r && p.StartTime <= currentTime && r.EndTime > currentTime); + + if (current == null) + { + if (direction < 1) + { + current = editorBeatmap.HitObjects.LastOrDefault(p => p.StartTime < currentTime); + if (current != null) + clock.SeekSmoothlyTo(current is IHasRepeats r ? r.EndTime : current.StartTime); + } + else + { + current = editorBeatmap.HitObjects.FirstOrDefault(p => p.StartTime > currentTime); + if (current != null) + clock.SeekSmoothlyTo(current.StartTime); + } + } + else + { + // Find the next node sample point + var r = (IHasRepeats)current; + double[] nodeSamplePointTimes = new double[r.RepeatCount + 3]; + + nodeSamplePointTimes[0] = current.StartTime; + // The sample point for the main samples is sandwiched between the head and the first repeat + nodeSamplePointTimes[1] = current.StartTime + r.Duration / r.SpanCount() / 2; + + for (int i = 0; i < r.SpanCount(); i++) + { + nodeSamplePointTimes[i + 2] = current.StartTime + r.Duration / r.SpanCount() * (i + 1); + } + + double found = direction < 1 + ? nodeSamplePointTimes.Last(p => p < currentTime) + : nodeSamplePointTimes.First(p => p > currentTime); + + clock.SeekSmoothlyTo(found); + } + + // Show the sample edit popover at the current time + ShowSampleEditPopoverRequested?.Invoke(clock.CurrentTimeAccurate); + } + private void seek(UIEvent e, int direction) { if (e.AltPressed) { - seekHitObject(direction); + if (e.ShiftPressed) + seekSamplePoint(direction); + else + seekHitObject(direction); return; } From 8d46d6c6976039975414e34b54678293f2f9b574 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Fri, 5 Jul 2024 14:18:17 +0200 Subject: [PATCH 1360/2493] always seek on click --- .../Edit/Compose/Components/Timeline/SamplePointPiece.cs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs index 8c05a8806e..a3c781260d 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs @@ -75,12 +75,6 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline } protected override bool OnClick(ClickEvent e) - { - this.ShowPopover(); - return true; - } - - protected override bool OnDoubleClick(DoubleClickEvent e) { editorClock?.SeekSmoothlyTo(GetTime()); this.ShowPopover(); From c05f48979bec3377c68fc462d17a95ce87c9a35d Mon Sep 17 00:00:00 2001 From: OliBomby Date: Fri, 5 Jul 2024 14:33:05 +0200 Subject: [PATCH 1361/2493] fix naming violation --- .../Edit/Compose/Components/Timeline/SamplePointPiece.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs index a3c781260d..731fe8ae6a 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs @@ -57,7 +57,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline updateText(); if (editor != null) - editor.ShowSampleEditPopoverRequested += OnShowSampleEditPopoverRequested; + editor.ShowSampleEditPopoverRequested += onShowSampleEditPopoverRequested; } protected override void Dispose(bool isDisposing) @@ -65,10 +65,10 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline base.Dispose(isDisposing); if (editor != null) - editor.ShowSampleEditPopoverRequested -= OnShowSampleEditPopoverRequested; + editor.ShowSampleEditPopoverRequested -= onShowSampleEditPopoverRequested; } - private void OnShowSampleEditPopoverRequested(double time) + private void onShowSampleEditPopoverRequested(double time) { if (time == GetTime()) this.ShowPopover(); From 9013c119ab586684e74a2d94aabd1c522a17f4b9 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Fri, 5 Jul 2024 14:33:15 +0200 Subject: [PATCH 1362/2493] update tests --- .../TestSceneHitObjectSampleAdjustments.cs | 46 ++++++++++++------- 1 file changed, 29 insertions(+), 17 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneHitObjectSampleAdjustments.cs b/osu.Game.Tests/Visual/Editing/TestSceneHitObjectSampleAdjustments.cs index 28bafb79ee..af68948bb7 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneHitObjectSampleAdjustments.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneHitObjectSampleAdjustments.cs @@ -332,14 +332,29 @@ namespace osu.Game.Tests.Visual.Editing }); }); - doubleClickNodeSamplePiece(0, 0); + clickNodeSamplePiece(0, 0); editorTimeIs(0); - doubleClickNodeSamplePiece(0, 1); + clickNodeSamplePiece(0, 1); editorTimeIs(813); - doubleClickNodeSamplePiece(0, 2); + clickNodeSamplePiece(0, 2); editorTimeIs(1627); - doubleClickSamplePiece(0); + clickSamplePiece(0); + editorTimeIs(406); + + seekSamplePiece(-1); editorTimeIs(0); + samplePopoverIsOpen(); + seekSamplePiece(-1); + editorTimeIs(0); + samplePopoverIsOpen(); + seekSamplePiece(1); + editorTimeIs(406); + seekSamplePiece(1); + editorTimeIs(813); + seekSamplePiece(1); + editorTimeIs(1627); + seekSamplePiece(1); + editorTimeIs(1627); } [Test] @@ -521,7 +536,7 @@ namespace osu.Game.Tests.Visual.Editing private void clickSamplePiece(int objectIndex) => AddStep($"click {objectIndex.ToOrdinalWords()} sample piece", () => { - var samplePiece = this.ChildrenOfType().Single(piece => piece.HitObject == EditorBeatmap.HitObjects.ElementAt(objectIndex)); + var samplePiece = this.ChildrenOfType().Single(piece => piece is not NodeSamplePointPiece && piece.HitObject == EditorBeatmap.HitObjects.ElementAt(objectIndex)); InputManager.MoveMouseTo(samplePiece); InputManager.Click(MouseButton.Left); @@ -535,22 +550,19 @@ namespace osu.Game.Tests.Visual.Editing InputManager.Click(MouseButton.Left); }); - private void doubleClickSamplePiece(int objectIndex) => AddStep($"double-click {objectIndex.ToOrdinalWords()} sample piece", () => + private void seekSamplePiece(int direction) => AddStep($"seek sample piece {direction}", () => { - var samplePiece = this.ChildrenOfType().Single(piece => piece is not NodeSamplePointPiece && piece.HitObject == EditorBeatmap.HitObjects.ElementAt(objectIndex)); - - InputManager.MoveMouseTo(samplePiece); - InputManager.Click(MouseButton.Left); - InputManager.Click(MouseButton.Left); + InputManager.PressKey(Key.ShiftLeft); + InputManager.PressKey(Key.AltLeft); + InputManager.Key(direction < 1 ? Key.Left : Key.Right); + InputManager.ReleaseKey(Key.AltLeft); + InputManager.ReleaseKey(Key.ShiftLeft); }); - private void doubleClickNodeSamplePiece(int objectIndex, int nodeIndex) => AddStep($"double-click {objectIndex.ToOrdinalWords()} object {nodeIndex.ToOrdinalWords()} node sample piece", () => + private void samplePopoverIsOpen() => AddUntilStep("sample popover is open", () => { - var samplePiece = this.ChildrenOfType().Where(piece => piece.HitObject == EditorBeatmap.HitObjects.ElementAt(objectIndex)).ToArray()[nodeIndex]; - - InputManager.MoveMouseTo(samplePiece); - InputManager.Click(MouseButton.Left); - InputManager.Click(MouseButton.Left); + var popover = this.ChildrenOfType().SingleOrDefault(o => o.IsPresent); + return popover != null; }); private void samplePopoverHasNoFocus() => AddUntilStep("sample popover textbox not focused", () => From ba44757c86f6a36e0debb7b88a6ce7b06f162dff Mon Sep 17 00:00:00 2001 From: OliBomby Date: Fri, 5 Jul 2024 15:24:39 +0200 Subject: [PATCH 1363/2493] clarify logic --- osu.Game/Screens/Edit/Editor.cs | 33 +++++++++++++++++---------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 973908dfcb..847ad3eba8 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -1082,26 +1082,12 @@ namespace osu.Game.Screens.Edit { double currentTime = clock.CurrentTimeAccurate; + // Check if we are currently inside a hit object with node samples, if so seek to the next node sample point var current = direction < 1 ? editorBeatmap.HitObjects.LastOrDefault(p => p is IHasRepeats r && p.StartTime < currentTime && r.EndTime >= currentTime) : editorBeatmap.HitObjects.LastOrDefault(p => p is IHasRepeats r && p.StartTime <= currentTime && r.EndTime > currentTime); - if (current == null) - { - if (direction < 1) - { - current = editorBeatmap.HitObjects.LastOrDefault(p => p.StartTime < currentTime); - if (current != null) - clock.SeekSmoothlyTo(current is IHasRepeats r ? r.EndTime : current.StartTime); - } - else - { - current = editorBeatmap.HitObjects.FirstOrDefault(p => p.StartTime > currentTime); - if (current != null) - clock.SeekSmoothlyTo(current.StartTime); - } - } - else + if (current != null) { // Find the next node sample point var r = (IHasRepeats)current; @@ -1122,6 +1108,21 @@ namespace osu.Game.Screens.Edit clock.SeekSmoothlyTo(found); } + else + { + if (direction < 1) + { + current = editorBeatmap.HitObjects.LastOrDefault(p => p.StartTime < currentTime); + if (current != null) + clock.SeekSmoothlyTo(current is IHasRepeats r ? r.EndTime : current.StartTime); + } + else + { + current = editorBeatmap.HitObjects.FirstOrDefault(p => p.StartTime > currentTime); + if (current != null) + clock.SeekSmoothlyTo(current.StartTime); + } + } // Show the sample edit popover at the current time ShowSampleEditPopoverRequested?.Invoke(clock.CurrentTimeAccurate); From 5da8bb5becf461f571257c0bbd2041f7781e57cf Mon Sep 17 00:00:00 2001 From: OliBomby Date: Sun, 7 Jul 2024 21:33:27 +0200 Subject: [PATCH 1364/2493] prevent volume control from eating inputs --- osu.Game/Overlays/Volume/VolumeControlReceptor.cs | 8 ++++---- osu.Game/Overlays/VolumeOverlay.cs | 12 ++++++++---- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/osu.Game/Overlays/Volume/VolumeControlReceptor.cs b/osu.Game/Overlays/Volume/VolumeControlReceptor.cs index 4ddbc9dd48..2e8d86d4c7 100644 --- a/osu.Game/Overlays/Volume/VolumeControlReceptor.cs +++ b/osu.Game/Overlays/Volume/VolumeControlReceptor.cs @@ -23,15 +23,15 @@ namespace osu.Game.Overlays.Volume { case GlobalAction.DecreaseVolume: case GlobalAction.IncreaseVolume: - ActionRequested?.Invoke(e.Action); - return true; + return ActionRequested?.Invoke(e.Action) == true; case GlobalAction.ToggleMute: case GlobalAction.NextVolumeMeter: case GlobalAction.PreviousVolumeMeter: if (!e.Repeat) - ActionRequested?.Invoke(e.Action); - return true; + return ActionRequested?.Invoke(e.Action) == true; + + return false; } return false; diff --git a/osu.Game/Overlays/VolumeOverlay.cs b/osu.Game/Overlays/VolumeOverlay.cs index 5470c70400..fa6e797c9c 100644 --- a/osu.Game/Overlays/VolumeOverlay.cs +++ b/osu.Game/Overlays/VolumeOverlay.cs @@ -120,14 +120,18 @@ namespace osu.Game.Overlays return true; case GlobalAction.NextVolumeMeter: - if (State.Value == Visibility.Visible) - volumeMeters.SelectNext(); + if (State.Value != Visibility.Visible) + return false; + + volumeMeters.SelectNext(); Show(); return true; case GlobalAction.PreviousVolumeMeter: - if (State.Value == Visibility.Visible) - volumeMeters.SelectPrevious(); + if (State.Value != Visibility.Visible) + return false; + + volumeMeters.SelectPrevious(); Show(); return true; From f36321a8ea74a599f77263ef4d127bc58263006b Mon Sep 17 00:00:00 2001 From: OliBomby Date: Sun, 7 Jul 2024 21:33:43 +0200 Subject: [PATCH 1365/2493] allow alt scroll for volume in editor --- osu.Game/Screens/Edit/Editor.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 847ad3eba8..acb9b93114 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -675,7 +675,7 @@ namespace osu.Game.Screens.Edit protected override bool OnScroll(ScrollEvent e) { - if (e.ControlPressed || e.SuperPressed) + if (e.ControlPressed || e.AltPressed || e.SuperPressed) return false; const double precision = 1; From 2bfa03c6d83b321bedcb6a2b659b692548f0490a Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 8 Jul 2024 07:37:42 +0300 Subject: [PATCH 1366/2493] Rename test scene file --- .../Menus/{TestAudioDucking.cs => TestSceneAudioDucking.cs} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename osu.Game.Tests/Visual/Menus/{TestAudioDucking.cs => TestSceneAudioDucking.cs} (100%) diff --git a/osu.Game.Tests/Visual/Menus/TestAudioDucking.cs b/osu.Game.Tests/Visual/Menus/TestSceneAudioDucking.cs similarity index 100% rename from osu.Game.Tests/Visual/Menus/TestAudioDucking.cs rename to osu.Game.Tests/Visual/Menus/TestSceneAudioDucking.cs From 0067450b226d06d5812e4d836a64e017ba049727 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 8 Jul 2024 13:47:04 +0900 Subject: [PATCH 1367/2493] Change volume parameter to `double` --- osu.Game.Tests/Visual/Menus/TestSceneAudioDucking.cs | 10 +++++----- osu.Game/Overlays/MusicController.cs | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Game.Tests/Visual/Menus/TestSceneAudioDucking.cs b/osu.Game.Tests/Visual/Menus/TestSceneAudioDucking.cs index 459bdd3e2e..20549018e0 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneAudioDucking.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneAudioDucking.cs @@ -33,7 +33,7 @@ namespace osu.Game.Tests.Visual.Menus { duckOp1 = Game.MusicController.Duck(new DuckParameters { - DuckVolumeTo = 0.5f, + DuckVolumeTo = 0.5, }); }); @@ -43,7 +43,7 @@ namespace osu.Game.Tests.Visual.Menus { duckOp2 = Game.MusicController.Duck(new DuckParameters { - DuckVolumeTo = 0.2f, + DuckVolumeTo = 0.2, }); }); @@ -73,7 +73,7 @@ namespace osu.Game.Tests.Visual.Menus { duckOp1 = Game.MusicController.Duck(new DuckParameters { - DuckVolumeTo = 0.5f, + DuckVolumeTo = 0.5, }); }); @@ -83,7 +83,7 @@ namespace osu.Game.Tests.Visual.Menus { duckOp2 = Game.MusicController.Duck(new DuckParameters { - DuckVolumeTo = 0.2f, + DuckVolumeTo = 0.2, }); }); @@ -118,7 +118,7 @@ namespace osu.Game.Tests.Visual.Menus { duckOp1 = Game.MusicController.Duck(new DuckParameters { - DuckVolumeTo = 0.2f, + DuckVolumeTo = 0.2, DuckCutoffTo = AudioFilter.MAX_LOWPASS_CUTOFF, DuckDuration = 500, }); diff --git a/osu.Game/Overlays/MusicController.cs b/osu.Game/Overlays/MusicController.cs index c4817a8f26..a01f3bfa5f 100644 --- a/osu.Game/Overlays/MusicController.cs +++ b/osu.Game/Overlays/MusicController.cs @@ -494,7 +494,7 @@ namespace osu.Game.Overlays /// The final volume which should be reached during ducking, when 0 is silent and 1 is original volume. /// Defaults to 25%. /// - public float DuckVolumeTo = 0.25f; + public double DuckVolumeTo = 0.25; /// /// The low-pass cutoff frequency which should be reached during ducking. If not required, set to . From aa36a844be987d50ff1fe4a79645fc65e3eb3441 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 8 Jul 2024 13:49:25 +0900 Subject: [PATCH 1368/2493] Reduce precision requirement for tests --- .../Visual/Menus/TestSceneAudioDucking.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/osu.Game.Tests/Visual/Menus/TestSceneAudioDucking.cs b/osu.Game.Tests/Visual/Menus/TestSceneAudioDucking.cs index 20549018e0..a7ec7f5bdc 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneAudioDucking.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneAudioDucking.cs @@ -37,7 +37,7 @@ namespace osu.Game.Tests.Visual.Menus }); }); - AddUntilStep("wait for duck to complete", () => Game.Audio.Tracks.AggregateVolume.Value, () => Is.EqualTo(normalVolume * 0.5f).Within(float.Epsilon)); + AddUntilStep("wait for duck to complete", () => Game.Audio.Tracks.AggregateVolume.Value, () => Is.EqualTo(normalVolume * 0.5f).Within(0.01)); AddStep("duck two", () => { @@ -47,13 +47,13 @@ namespace osu.Game.Tests.Visual.Menus }); }); - AddUntilStep("wait for duck to complete", () => Game.Audio.Tracks.AggregateVolume.Value, () => Is.EqualTo(normalVolume * 0.2f).Within(float.Epsilon)); + AddUntilStep("wait for duck to complete", () => Game.Audio.Tracks.AggregateVolume.Value, () => Is.EqualTo(normalVolume * 0.2f).Within(0.01)); AddStep("restore two", () => duckOp2.Dispose()); - AddUntilStep("wait for restore to complete", () => Game.Audio.Tracks.AggregateVolume.Value, () => Is.EqualTo(normalVolume * 0.5f).Within(float.Epsilon)); + AddUntilStep("wait for restore to complete", () => Game.Audio.Tracks.AggregateVolume.Value, () => Is.EqualTo(normalVolume * 0.5f).Within(0.01)); AddStep("restore one", () => duckOp1.Dispose()); - AddUntilStep("wait for restore to complete", () => Game.Audio.Tracks.AggregateVolume.Value, () => Is.EqualTo(normalVolume).Within(float.Epsilon)); + AddUntilStep("wait for restore to complete", () => Game.Audio.Tracks.AggregateVolume.Value, () => Is.EqualTo(normalVolume).Within(0.01)); } [Test] @@ -77,7 +77,7 @@ namespace osu.Game.Tests.Visual.Menus }); }); - AddUntilStep("wait for duck to complete", () => Game.Audio.Tracks.AggregateVolume.Value, () => Is.EqualTo(normalVolume * 0.5f).Within(float.Epsilon)); + AddUntilStep("wait for duck to complete", () => Game.Audio.Tracks.AggregateVolume.Value, () => Is.EqualTo(normalVolume * 0.5f).Within(0.01)); AddStep("duck two", () => { @@ -87,15 +87,15 @@ namespace osu.Game.Tests.Visual.Menus }); }); - AddUntilStep("wait for duck to complete", () => Game.Audio.Tracks.AggregateVolume.Value, () => Is.EqualTo(normalVolume * 0.2f).Within(float.Epsilon)); + AddUntilStep("wait for duck to complete", () => Game.Audio.Tracks.AggregateVolume.Value, () => Is.EqualTo(normalVolume * 0.2f).Within(0.01)); AddStep("restore one", () => duckOp1.Dispose()); // reverse order, less extreme duck removed so won't change - AddUntilStep("wait for restore to complete", () => Game.Audio.Tracks.AggregateVolume.Value, () => Is.EqualTo(normalVolume * 0.2f).Within(float.Epsilon)); + AddUntilStep("wait for restore to complete", () => Game.Audio.Tracks.AggregateVolume.Value, () => Is.EqualTo(normalVolume * 0.2f).Within(0.01)); AddStep("restore two", () => duckOp2.Dispose()); - AddUntilStep("wait for restore to complete", () => Game.Audio.Tracks.AggregateVolume.Value, () => Is.EqualTo(normalVolume).Within(float.Epsilon)); + AddUntilStep("wait for restore to complete", () => Game.Audio.Tracks.AggregateVolume.Value, () => Is.EqualTo(normalVolume).Within(0.01)); } [Test] From 3650f3c47978244befd871e7ae705ae033d50288 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 8 Jul 2024 13:52:40 +0900 Subject: [PATCH 1369/2493] Allow multiple ducks with same parameters --- .../Visual/Menus/TestSceneAudioDucking.cs | 39 +++++++++++++++++++ osu.Game/Overlays/MusicController.cs | 3 -- 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Menus/TestSceneAudioDucking.cs b/osu.Game.Tests/Visual/Menus/TestSceneAudioDucking.cs index a7ec7f5bdc..8d20d8e0d5 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneAudioDucking.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneAudioDucking.cs @@ -56,6 +56,45 @@ namespace osu.Game.Tests.Visual.Menus AddUntilStep("wait for restore to complete", () => Game.Audio.Tracks.AggregateVolume.Value, () => Is.EqualTo(normalVolume).Within(0.01)); } + [Test] + public void TestMultipleDucksSameParameters() + { + var duckParameters = new DuckParameters + { + DuckVolumeTo = 0.5, + }; + + IDisposable duckOp1 = null!; + IDisposable duckOp2 = null!; + + double normalVolume = 1; + + AddStep("get initial volume", () => + { + normalVolume = Game.Audio.Tracks.AggregateVolume.Value; + }); + + AddStep("duck one", () => + { + duckOp1 = Game.MusicController.Duck(duckParameters); + }); + + AddUntilStep("wait for duck to complete", () => Game.Audio.Tracks.AggregateVolume.Value, () => Is.EqualTo(normalVolume * 0.5f).Within(0.01)); + + AddStep("duck two", () => + { + duckOp2 = Game.MusicController.Duck(duckParameters); + }); + + AddUntilStep("wait for duck to complete", () => Game.Audio.Tracks.AggregateVolume.Value, () => Is.EqualTo(normalVolume * 0.5f).Within(0.01)); + + AddStep("restore two", () => duckOp2.Dispose()); + AddUntilStep("wait for restore to complete", () => Game.Audio.Tracks.AggregateVolume.Value, () => Is.EqualTo(normalVolume * 0.5f).Within(0.01)); + + AddStep("restore one", () => duckOp1.Dispose()); + AddUntilStep("wait for restore to complete", () => Game.Audio.Tracks.AggregateVolume.Value, () => Is.EqualTo(normalVolume).Within(0.01)); + } + [Test] public void TestMultipleDucksReverseOrder() { diff --git a/osu.Game/Overlays/MusicController.cs b/osu.Game/Overlays/MusicController.cs index a01f3bfa5f..116e60a014 100644 --- a/osu.Game/Overlays/MusicController.cs +++ b/osu.Game/Overlays/MusicController.cs @@ -268,9 +268,6 @@ namespace osu.Game.Overlays { parameters ??= new DuckParameters(); - if (duckOperations.Contains(parameters)) - throw new ArgumentException("Ducking has already been applied for the provided parameters.", nameof(parameters)); - duckOperations.Add(parameters); DuckParameters volumeOperation = duckOperations.MinBy(p => p.DuckVolumeTo)!; From 0c5a1410d3395cb712c3f1b05c2f42296c6d7bdb Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 8 Jul 2024 07:51:17 +0300 Subject: [PATCH 1370/2493] Add fade transition to content during open/close --- osu.Game/Overlays/Mods/ModCustomisationPanel.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModCustomisationPanel.cs b/osu.Game/Overlays/Mods/ModCustomisationPanel.cs index f214bcb3a1..1553f37878 100644 --- a/osu.Game/Overlays/Mods/ModCustomisationPanel.cs +++ b/osu.Game/Overlays/Mods/ModCustomisationPanel.cs @@ -6,6 +6,7 @@ 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.Effects; @@ -18,6 +19,7 @@ using osu.Game.Graphics.Containers; using osu.Game.Input.Bindings; using osu.Game.Rulesets.Mods; using osuTK; +using osuTK.Graphics; namespace osu.Game.Overlays.Mods { @@ -75,6 +77,7 @@ namespace osu.Game.Overlays.Mods Offset = new Vector2(0f, 5f), Radius = 20f, Roundness = 5f, + Colour = Color4.Black.Opacity(0.25f), }, Expanded = { BindTarget = Expanded }, Children = new Drawable[] @@ -161,13 +164,13 @@ namespace osu.Game.Overlays.Mods content.AutoSizeDuration = 400; content.AutoSizeEasing = Easing.OutQuint; content.AutoSizeAxes = Axes.Y; - content.FadeEdgeEffectTo(0.25f, 120, Easing.OutQuint); + content.FadeIn(120, Easing.OutQuint); } else { content.AutoSizeAxes = Axes.None; content.ResizeHeightTo(header_height, 400, Easing.OutQuint); - content.FadeEdgeEffectTo(0f, 400, Easing.OutQuint); + content.FadeOut(400, Easing.OutSine); } } From a0ffe9bcbc014940f579ef4ff81aa24b5eb57878 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 8 Jul 2024 07:58:40 +0300 Subject: [PATCH 1371/2493] Remove unnecessary `HandlePositionalInput` override --- osu.Game/Overlays/Mods/ModCustomisationHeader.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModCustomisationHeader.cs b/osu.Game/Overlays/Mods/ModCustomisationHeader.cs index 2887b53548..42bc9f16b1 100644 --- a/osu.Game/Overlays/Mods/ModCustomisationHeader.cs +++ b/osu.Game/Overlays/Mods/ModCustomisationHeader.cs @@ -18,8 +18,6 @@ namespace osu.Game.Overlays.Mods { public partial class ModCustomisationHeader : OsuHoverContainer { - public override bool HandlePositionalInput => true; - private Box background = null!; private SpriteIcon icon = null!; From 9375f79879fe6ccd6e1d1dacc87dba63d33cee9f Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 8 Jul 2024 13:58:42 +0900 Subject: [PATCH 1372/2493] Add frenzibyte to users that can use diffcalc workflow --- .github/workflows/diffcalc.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/diffcalc.yml b/.github/workflows/diffcalc.yml index 7fd0f798cd..9f129a697c 100644 --- a/.github/workflows/diffcalc.yml +++ b/.github/workflows/diffcalc.yml @@ -111,7 +111,7 @@ jobs: steps: - name: Check permissions run: | - ALLOWED_USERS=(smoogipoo peppy bdach) + ALLOWED_USERS=(smoogipoo peppy bdach frenzibyte) for i in "${ALLOWED_USERS[@]}"; do if [[ "${{ github.actor }}" == "$i" ]]; then exit 0 From 22f2f8369521abf943db38387ed173d884f6b0ea Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 8 Jul 2024 08:07:45 +0300 Subject: [PATCH 1373/2493] Rescale chevron up and down instead --- osu.Game/Overlays/Mods/ModCustomisationHeader.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Mods/ModCustomisationHeader.cs b/osu.Game/Overlays/Mods/ModCustomisationHeader.cs index 42bc9f16b1..bf10e13515 100644 --- a/osu.Game/Overlays/Mods/ModCustomisationHeader.cs +++ b/osu.Game/Overlays/Mods/ModCustomisationHeader.cs @@ -88,7 +88,7 @@ namespace osu.Game.Overlays.Mods Expanded.BindValueChanged(v => { - icon.RotateTo(v.NewValue ? 180 : 0); + icon.ScaleTo(v.NewValue ? new Vector2(1, -1) : Vector2.One, 300, Easing.OutQuint); }, true); } } From 5223e0aeaef1ac2d05fbca542f5efa5527d9fb39 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 8 Jul 2024 08:31:33 +0300 Subject: [PATCH 1374/2493] Use scale instead of rotation for overlay scroll button --- osu.Game/Overlays/OverlayScrollContainer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/OverlayScrollContainer.cs b/osu.Game/Overlays/OverlayScrollContainer.cs index 9ff0a65652..a99cf08abb 100644 --- a/osu.Game/Overlays/OverlayScrollContainer.cs +++ b/osu.Game/Overlays/OverlayScrollContainer.cs @@ -163,7 +163,7 @@ namespace osu.Game.Overlays LastScrollTarget.BindValueChanged(target => { - spriteIcon.RotateTo(target.NewValue != null ? 180 : 0, fade_duration, Easing.OutQuint); + spriteIcon.ScaleTo(target.NewValue != null ? new Vector2(1f, -1f) : Vector2.One, fade_duration, Easing.OutQuint); TooltipText = target.NewValue != null ? CommonStrings.ButtonsBackToPrevious : CommonStrings.ButtonsBackToTop; }, true); } From be039b85adef209ec7020b1e7c767fce10f55374 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 8 Jul 2024 08:32:29 +0300 Subject: [PATCH 1375/2493] Use scale instead of rotation in profile cover toggle button --- .../Overlays/Profile/Header/Components/ToggleCoverButton.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/Profile/Header/Components/ToggleCoverButton.cs b/osu.Game/Overlays/Profile/Header/Components/ToggleCoverButton.cs index 9171d5de7d..b2d024c1d7 100644 --- a/osu.Game/Overlays/Profile/Header/Components/ToggleCoverButton.cs +++ b/osu.Game/Overlays/Profile/Header/Components/ToggleCoverButton.cs @@ -50,12 +50,13 @@ namespace osu.Game.Overlays.Profile.Header.Components { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Size = new Vector2(10.5f, 12) + Size = new Vector2(10.5f, 12), + Icon = FontAwesome.Solid.ChevronDown, }; CoverExpanded.BindValueChanged(visible => updateState(visible.NewValue), true); } - private void updateState(bool detailsVisible) => icon.Icon = detailsVisible ? FontAwesome.Solid.ChevronUp : FontAwesome.Solid.ChevronDown; + private void updateState(bool detailsVisible) => icon.ScaleTo(detailsVisible ? new Vector2(1f, -1f) : Vector2.One, 300, Easing.OutQuint); } } From dc630ddc9d7ebcc9c338894a42579dff0321cbd8 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 8 Jul 2024 08:45:44 +0300 Subject: [PATCH 1376/2493] Use scale instead of rotation in news month sidebar --- osu.Game/Overlays/News/Sidebar/MonthSection.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/News/Sidebar/MonthSection.cs b/osu.Game/Overlays/News/Sidebar/MonthSection.cs index 9a748b2001..26490c36c8 100644 --- a/osu.Game/Overlays/News/Sidebar/MonthSection.cs +++ b/osu.Game/Overlays/News/Sidebar/MonthSection.cs @@ -118,7 +118,7 @@ namespace osu.Game.Overlays.News.Sidebar Expanded.BindValueChanged(open => { - icon.Scale = new Vector2(1, open.NewValue ? -1 : 1); + icon.ScaleTo(open.NewValue ? new Vector2(1f, -1f) : Vector2.One, 300, Easing.OutQuint); }, true); } } From 4c2ae07eba8f66aec496ddec1a9c148f90bb124b Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 8 Jul 2024 08:48:07 +0300 Subject: [PATCH 1377/2493] Use scale instead of rotation in `ChevronButton` (used in top-right of comments section) --- osu.Game/Overlays/Comments/Buttons/ChevronButton.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Comments/Buttons/ChevronButton.cs b/osu.Game/Overlays/Comments/Buttons/ChevronButton.cs index 45024f25db..3902f89688 100644 --- a/osu.Game/Overlays/Comments/Buttons/ChevronButton.cs +++ b/osu.Game/Overlays/Comments/Buttons/ChevronButton.cs @@ -24,6 +24,7 @@ namespace osu.Game.Overlays.Comments.Buttons Anchor = Anchor.Centre, Origin = Anchor.Centre, Size = new Vector2(12), + Icon = FontAwesome.Solid.ChevronDown }; } @@ -38,11 +39,12 @@ namespace osu.Game.Overlays.Comments.Buttons base.LoadComplete(); Action = Expanded.Toggle; Expanded.BindValueChanged(onExpandedChanged, true); + FinishTransforms(true); } private void onExpandedChanged(ValueChangedEvent expanded) { - icon.Icon = expanded.NewValue ? FontAwesome.Solid.ChevronUp : FontAwesome.Solid.ChevronDown; + icon.ScaleTo(expanded.NewValue ? new Vector2(1f, -1f) : Vector2.One, 300, Easing.OutQuint); } } } From 58e236a2471f2f815f947fe5141d3feaa5046de8 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 8 Jul 2024 08:48:52 +0300 Subject: [PATCH 1378/2493] Add transition to dropdown chevrons --- osu.Game/Collections/CollectionDropdown.cs | 2 +- .../Graphics/UserInterface/OsuDropdown.cs | 25 ++++++++++++++++--- .../Music/NowPlayingCollectionDropdown.cs | 4 +-- .../Overlays/Rankings/SpotlightSelector.cs | 2 +- 4 files changed, 25 insertions(+), 8 deletions(-) diff --git a/osu.Game/Collections/CollectionDropdown.cs b/osu.Game/Collections/CollectionDropdown.cs index c04689b097..6d8f65c257 100644 --- a/osu.Game/Collections/CollectionDropdown.cs +++ b/osu.Game/Collections/CollectionDropdown.cs @@ -163,7 +163,7 @@ namespace osu.Game.Collections public CollectionDropdownHeader() { Height = 25; - Icon.Size = new Vector2(16); + Chevron.Size = new Vector2(16); Foreground.Padding = new MarginPadding { Top = 4, Bottom = 4, Left = 8, Right = 4 }; } } diff --git a/osu.Game/Graphics/UserInterface/OsuDropdown.cs b/osu.Game/Graphics/UserInterface/OsuDropdown.cs index c8bb45b59d..562258f5b2 100644 --- a/osu.Game/Graphics/UserInterface/OsuDropdown.cs +++ b/osu.Game/Graphics/UserInterface/OsuDropdown.cs @@ -30,6 +30,12 @@ namespace osu.Game.Graphics.UserInterface protected override DropdownMenu CreateMenu() => new OsuDropdownMenu(); + public OsuDropdown() + { + if (Header is OsuDropdownHeader osuHeader) + osuHeader.Dropdown = this; + } + public bool OnPressed(KeyBindingPressEvent e) { if (e.Repeat) return false; @@ -307,7 +313,9 @@ namespace osu.Game.Graphics.UserInterface set => Text.Text = value; } - protected readonly SpriteIcon Icon; + protected readonly SpriteIcon Chevron; + + public OsuDropdown? Dropdown { get; set; } public OsuDropdownHeader() { @@ -341,7 +349,7 @@ namespace osu.Game.Graphics.UserInterface Origin = Anchor.CentreLeft, RelativeSizeAxes = Axes.X, }, - Icon = new SpriteIcon + Chevron = new SpriteIcon { Icon = FontAwesome.Solid.ChevronDown, Anchor = Anchor.CentreRight, @@ -365,6 +373,9 @@ namespace osu.Game.Graphics.UserInterface { base.LoadComplete(); + if (Dropdown != null) + Dropdown.Menu.StateChanged += _ => updateChevron(); + SearchBar.State.ValueChanged += _ => updateColour(); Enabled.BindValueChanged(_ => updateColour()); updateColour(); @@ -392,16 +403,22 @@ namespace osu.Game.Graphics.UserInterface if (SearchBar.State.Value == Visibility.Visible) { - Icon.Colour = hovered ? hoveredColour.Lighten(0.5f) : Colour4.White; + Chevron.Colour = hovered ? hoveredColour.Lighten(0.5f) : Colour4.White; Background.Colour = unhoveredColour; } else { - Icon.Colour = Color4.White; + Chevron.Colour = Color4.White; Background.Colour = hovered ? hoveredColour : unhoveredColour; } } + private void updateChevron() + { + bool open = Dropdown?.Menu.State == MenuState.Open; + Chevron.ScaleTo(open ? new Vector2(1f, -1f) : Vector2.One, 300, Easing.OutQuint); + } + protected override DropdownSearchBar CreateSearchBar() => new OsuDropdownSearchBar { Padding = new MarginPadding { Right = 26 }, diff --git a/osu.Game/Overlays/Music/NowPlayingCollectionDropdown.cs b/osu.Game/Overlays/Music/NowPlayingCollectionDropdown.cs index fa9a2e3972..0f2e9400d9 100644 --- a/osu.Game/Overlays/Music/NowPlayingCollectionDropdown.cs +++ b/osu.Game/Overlays/Music/NowPlayingCollectionDropdown.cs @@ -53,8 +53,8 @@ namespace osu.Game.Overlays.Music { CornerRadius = 5; Height = 30; - Icon.Size = new Vector2(14); - Icon.Margin = new MarginPadding(0); + Chevron.Size = new Vector2(14); + Chevron.Margin = new MarginPadding(0); Foreground.Padding = new MarginPadding { Top = 4, Bottom = 4, Left = 10, Right = 10 }; EdgeEffect = new EdgeEffectParameters { diff --git a/osu.Game/Overlays/Rankings/SpotlightSelector.cs b/osu.Game/Overlays/Rankings/SpotlightSelector.cs index 190da04a5d..69dc8aba85 100644 --- a/osu.Game/Overlays/Rankings/SpotlightSelector.cs +++ b/osu.Game/Overlays/Rankings/SpotlightSelector.cs @@ -200,7 +200,7 @@ namespace osu.Game.Overlays.Rankings Text.Font = OsuFont.GetFont(size: 15); Text.Padding = new MarginPadding { Vertical = 1.5f }; // osu-web line-height difference compensation Foreground.Padding = new MarginPadding { Horizontal = 10, Vertical = 15 }; - Margin = Icon.Margin = new MarginPadding(0); + Margin = Chevron.Margin = new MarginPadding(0); } [BackgroundDependencyLoader] From 7dc901df11ec893cb45f19ad1a006d1a89cbc0d3 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 8 Jul 2024 08:54:05 +0300 Subject: [PATCH 1379/2493] Do not be lenient on nullability of dropdown --- osu.Game/Graphics/UserInterface/OsuDropdown.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Graphics/UserInterface/OsuDropdown.cs b/osu.Game/Graphics/UserInterface/OsuDropdown.cs index 562258f5b2..71ae149cf6 100644 --- a/osu.Game/Graphics/UserInterface/OsuDropdown.cs +++ b/osu.Game/Graphics/UserInterface/OsuDropdown.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.Diagnostics; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Audio; @@ -415,7 +416,8 @@ namespace osu.Game.Graphics.UserInterface private void updateChevron() { - bool open = Dropdown?.Menu.State == MenuState.Open; + Debug.Assert(Dropdown != null); + bool open = Dropdown.Menu.State == MenuState.Open; Chevron.ScaleTo(open ? new Vector2(1f, -1f) : Vector2.One, 300, Easing.OutQuint); } From 95321469783c2e359c4d8efa8cead7b2e94c6b46 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Sun, 7 Jul 2024 23:12:20 -0700 Subject: [PATCH 1380/2493] Fix content overflowing to border The +2 for the top isn't really needed for the original purpose as content fades out now, but visually, having the header and content spacing looks more correct. --- osu.Game/Overlays/Mods/ModCustomisationPanel.cs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModCustomisationPanel.cs b/osu.Game/Overlays/Mods/ModCustomisationPanel.cs index 1553f37878..a1e64e8c49 100644 --- a/osu.Game/Overlays/Mods/ModCustomisationPanel.cs +++ b/osu.Game/Overlays/Mods/ModCustomisationPanel.cs @@ -27,6 +27,7 @@ namespace osu.Game.Overlays.Mods { private const float header_height = 42f; private const float content_vertical_padding = 20f; + private const float content_border_thickness = 2f; private Container content = null!; private OsuScrollContainer scrollContainer = null!; @@ -68,7 +69,7 @@ namespace osu.Game.Overlays.Mods { RelativeSizeAxes = Axes.X, BorderColour = colourProvider.Dark3, - BorderThickness = 2f, + BorderThickness = content_border_thickness, CornerRadius = 10f, Masking = true, EdgeEffect = new EdgeEffectParameters @@ -90,9 +91,11 @@ namespace osu.Game.Overlays.Mods scrollContainer = new OsuScrollContainer(Direction.Vertical) { RelativeSizeAxes = Axes.X, - // The +2f is a workaround for masking issues (see https://github.com/ppy/osu-framework/issues/1675#issuecomment-910023157) - // Note that this actually causes the full scroll range to be reduced by 2px at the bottom, but it's not really noticeable. - Margin = new MarginPadding { Top = header_height + 2f }, + Margin = new MarginPadding + { + Top = header_height + content_border_thickness, + Bottom = content_border_thickness + }, Child = sectionsFlow = new FillFlowContainer { RelativeSizeAxes = Axes.X, From 03bd6069d88827575f19018c0d599cc3e865972d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 8 Jul 2024 15:50:27 +0900 Subject: [PATCH 1381/2493] Add slight animation when revert to default button is displayed This also fixes the transforms running too often (could make the initial transform take longer than expected if adjusting a slider bar, for instance). --- osu.Game/Overlays/RevertToDefaultButton.cs | 28 ++++++++++++++++++---- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/osu.Game/Overlays/RevertToDefaultButton.cs b/osu.Game/Overlays/RevertToDefaultButton.cs index 6fa5209f64..1ebe7b7934 100644 --- a/osu.Game/Overlays/RevertToDefaultButton.cs +++ b/osu.Game/Overlays/RevertToDefaultButton.cs @@ -87,6 +87,7 @@ namespace osu.Game.Overlays protected override void LoadComplete() { base.LoadComplete(); + updateState(); FinishTransforms(true); } @@ -95,33 +96,50 @@ namespace osu.Game.Overlays protected override bool OnHover(HoverEvent e) { - UpdateState(); + updateHover(); return false; } protected override void OnHoverLost(HoverLostEvent e) { - UpdateState(); + updateHover(); } public void UpdateState() => Scheduler.AddOnce(updateState); private const double fade_duration = 200; + private bool? isDisplayed; + private void updateState() { if (current == null) return; - Enabled.Value = !current.Disabled; + // Avoid running animations if we are already in an up-to-date state. + if (Enabled.Value == !current.Disabled && isDisplayed == !current.IsDefault) + return; - if (current.IsDefault) + Enabled.Value = !current.Disabled; + isDisplayed = !current.IsDefault; + + updateHover(); + + if (isDisplayed == false) this.FadeTo(0, fade_duration, Easing.OutQuint); else if (current.Disabled) this.FadeTo(0.2f, fade_duration, Easing.OutQuint); else - this.FadeTo(1, fade_duration, Easing.OutQuint); + { + icon.RotateTo(150).RotateTo(0, fade_duration * 2, Easing.OutQuint); + icon.ScaleTo(0.7f).ScaleTo(1, fade_duration * 2, Easing.OutQuint); + this.FadeTo(1, fade_duration, Easing.OutQuint); + } + } + + private void updateHover() + { if (IsHovered && Enabled.Value) { icon.RotateTo(-40, 500, Easing.OutQuint); From 73d164e0d0d7861d6d81c361e271ea09116ba9a2 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Mon, 8 Jul 2024 00:30:16 -0700 Subject: [PATCH 1382/2493] Add chevron transition to `CommentRepliesButton` --- osu.Game/Overlays/Comments/Buttons/CommentRepliesButton.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Comments/Buttons/CommentRepliesButton.cs b/osu.Game/Overlays/Comments/Buttons/CommentRepliesButton.cs index 400820ddd9..543ed7e722 100644 --- a/osu.Game/Overlays/Comments/Buttons/CommentRepliesButton.cs +++ b/osu.Game/Overlays/Comments/Buttons/CommentRepliesButton.cs @@ -89,7 +89,7 @@ namespace osu.Game.Overlays.Comments.Buttons background.Colour = colourProvider.Background2; } - protected void SetIconDirection(bool upwards) => icon.ScaleTo(new Vector2(1, upwards ? -1 : 1)); + protected void SetIconDirection(bool upwards) => icon.ScaleTo(upwards ? new Vector2(1f, -1f) : Vector2.One, 300, Easing.OutQuint); public void ToggleTextVisibility(bool visible) => text.FadeTo(visible ? 1 : 0, 200, Easing.OutQuint); From 257de9d08b01dbd56e3caa9a158d78c896c7cae8 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Mon, 8 Jul 2024 01:00:36 -0700 Subject: [PATCH 1383/2493] Fix arrow direction test --- .../Visual/UserInterface/TestSceneCommentRepliesButton.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneCommentRepliesButton.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneCommentRepliesButton.cs index eaaf40fb36..0aef56bc2e 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneCommentRepliesButton.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneCommentRepliesButton.cs @@ -47,9 +47,9 @@ namespace osu.Game.Tests.Visual.UserInterface public void TestArrowDirection() { AddStep("Set upwards", () => button.SetIconDirection(true)); - AddAssert("Icon facing upwards", () => button.Icon.Scale.Y == -1); + AddUntilStep("Icon facing upwards", () => button.Icon.Scale.Y == -1); AddStep("Set downwards", () => button.SetIconDirection(false)); - AddAssert("Icon facing downwards", () => button.Icon.Scale.Y == 1); + AddUntilStep("Icon facing downwards", () => button.Icon.Scale.Y == 1); } private partial class TestButton : CommentRepliesButton From 2ceedb0f93cfa2e263372113668c0d307cc34dd2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 8 Jul 2024 11:04:15 +0200 Subject: [PATCH 1384/2493] Fix editor menus overflowing off screen Closes https://github.com/ppy/osu/issues/28750. Yes this is not the perfect change to fix this (which would probably be some framework change to take bounds of the parenting input manager into account). I really do not want to go there and would like to just fix this locally and move on. Due to the game-wide scaling container this sorta works for any resolution anyhow. --- .../Screens/Edit/Components/Menus/EditorMenuBar.cs | 10 ++++++++-- osu.Game/Screens/Edit/Editor.cs | 1 + 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Edit/Components/Menus/EditorMenuBar.cs b/osu.Game/Screens/Edit/Components/Menus/EditorMenuBar.cs index 0e125d0ec0..ee954a7ea0 100644 --- a/osu.Game/Screens/Edit/Components/Menus/EditorMenuBar.cs +++ b/osu.Game/Screens/Edit/Components/Menus/EditorMenuBar.cs @@ -71,7 +71,10 @@ namespace osu.Game.Screens.Edit.Components.Menus }); } - protected override Framework.Graphics.UserInterface.Menu CreateSubMenu() => new SubMenu(); + protected override Framework.Graphics.UserInterface.Menu CreateSubMenu() => new SubMenu + { + MaxHeight = MaxHeight, + }; protected override DrawableMenuItem CreateDrawableMenuItem(MenuItem item) => new DrawableEditorBarMenuItem(item); @@ -143,7 +146,10 @@ namespace osu.Game.Screens.Edit.Components.Menus BackgroundColour = colourProvider.Background2; } - protected override Framework.Graphics.UserInterface.Menu CreateSubMenu() => new SubMenu(); + protected override Framework.Graphics.UserInterface.Menu CreateSubMenu() => new SubMenu + { + MaxHeight = MaxHeight, + }; protected override DrawableMenuItem CreateDrawableMenuItem(MenuItem item) { diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index c00b7ac4f2..2278af040f 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -343,6 +343,7 @@ namespace osu.Game.Screens.Edit Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, RelativeSizeAxes = Axes.Both, + MaxHeight = 600, Items = new[] { new MenuItem(CommonStrings.MenuBarFile) From db7774485a94c6c38914093d2a3fde7318c406f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 8 Jul 2024 11:14:32 +0200 Subject: [PATCH 1385/2493] Fix editor player crashing with UR counter present in skin Closes https://github.com/ppy/osu/issues/28764. --- osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs b/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs index 616d7a09b2..4377cc6219 100644 --- a/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs +++ b/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs @@ -72,7 +72,11 @@ namespace osu.Game.Screens.Edit.GameplayTest foreach (var hitObject in enumerateHitObjects(DrawableRuleset.Objects, editorState.Time)) { var judgement = hitObject.Judgement; - var result = new JudgementResult(hitObject, judgement) { Type = judgement.MaxResult }; + var result = new JudgementResult(hitObject, judgement) + { + Type = judgement.MaxResult, + GameplayRate = GameplayClockContainer.GetTrueGameplayRate(), + }; HealthProcessor.ApplyResult(result); ScoreProcessor.ApplyResult(result); From 910153c2e06c267819f78d225baae7720c0e17f1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 8 Jul 2024 19:32:28 +0900 Subject: [PATCH 1386/2493] Fix sizing / padding of collection dropdown header --- osu.Game/Collections/CollectionDropdown.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Collections/CollectionDropdown.cs b/osu.Game/Collections/CollectionDropdown.cs index 6d8f65c257..1e47aff3ec 100644 --- a/osu.Game/Collections/CollectionDropdown.cs +++ b/osu.Game/Collections/CollectionDropdown.cs @@ -163,8 +163,8 @@ namespace osu.Game.Collections public CollectionDropdownHeader() { Height = 25; - Chevron.Size = new Vector2(16); - Foreground.Padding = new MarginPadding { Top = 4, Bottom = 4, Left = 8, Right = 4 }; + Chevron.Size = new Vector2(12); + Foreground.Padding = new MarginPadding { Top = 4, Bottom = 4, Left = 8, Right = 8 }; } } From e51d510ea3efea73cbe05620b5d97d87b6beed29 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 8 Jul 2024 20:05:07 +0900 Subject: [PATCH 1387/2493] Add failing test for beatmap set hard deletion --- .../Visual/SongSelect/TestScenePlaySongSelect.cs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs index 6581ce0323..4c6a5c93d9 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs @@ -1211,6 +1211,20 @@ namespace osu.Game.Tests.Visual.SongSelect AddAssert("0 matching shown", () => songSelect.ChildrenOfType().Single().InformationalText == "0 matches"); } + [Test] + [Solo] + public void TestHardDeleteHandledCorrectly() + { + createSongSelect(); + + addRulesetImportStep(0); + AddAssert("3 matching shown", () => songSelect.ChildrenOfType().Single().InformationalText == "3 matches"); + + AddStep("hard delete beatmap", () => Realm.Write(r => r.RemoveRange(r.All().Where(s => !s.Protected)))); + + AddUntilStep("0 matching shown", () => songSelect.ChildrenOfType().Single().InformationalText == "0 matches"); + } + [Test] public void TestDeleteHotkey() { From 151c44853558b15440c11dd4dc7f74737eb1ff2c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 8 Jul 2024 18:31:36 +0900 Subject: [PATCH 1388/2493] Simplify tracking of beatmap sets in `BeatmapCarousel` --- osu.Game/Screens/Select/BeatmapCarousel.cs | 73 ++++++---------------- 1 file changed, 19 insertions(+), 54 deletions(-) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 56e7c24985..cab258aaab 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -197,7 +197,6 @@ namespace osu.Game.Screens.Select private CarouselRoot root; private IDisposable? subscriptionSets; - private IDisposable? subscriptionDeletedSets; private IDisposable? subscriptionBeatmaps; private IDisposable? subscriptionHiddenBeatmaps; @@ -253,6 +252,11 @@ namespace osu.Game.Screens.Select [Resolved] private RealmAccess realm { get; set; } = null!; + /// + /// Track GUIDs of all sets in realm to allow handling deletions. + /// + private readonly List realmBeatmapSets = new List(); + protected override void LoadComplete() { base.LoadComplete(); @@ -262,45 +266,9 @@ namespace osu.Game.Screens.Select // Can't use main subscriptions because we can't lookup deleted indices. // https://github.com/realm/realm-dotnet/discussions/2634#discussioncomment-1605595. - subscriptionDeletedSets = realm.RegisterForNotifications(r => r.All().Where(s => s.DeletePending && !s.Protected), deletedBeatmapSetsChanged); subscriptionHiddenBeatmaps = realm.RegisterForNotifications(r => r.All().Where(b => b.Hidden), beatmapsChanged); } - private void deletedBeatmapSetsChanged(IRealmCollection sender, ChangeSet? changes) - { - // If loading test beatmaps, avoid overwriting with realm subscription callbacks. - if (loadedTestBeatmaps) - return; - - if (changes == null) - return; - - var removeableSets = changes.InsertedIndices.Select(i => sender[i].ID).ToHashSet(); - - // This schedule is required to retain selection of beatmaps over an ImportAsUpdate operation. - // This is covered by TestPlaySongSelect.TestSelectionRetainedOnBeatmapUpdate. - // - // In short, we have specialised logic in `beatmapSetsChanged` (directly below) to infer that an - // update operation has occurred. For this to work, we need to confirm the `DeletePending` flag - // of the current selection. - // - // If we don't schedule the following code, it is possible for the `deleteBeatmapSetsChanged` handler - // to be invoked before the `beatmapSetsChanged` handler (realm call order seems non-deterministic) - // which will lead to the currently selected beatmap changing via `CarouselGroupEagerSelect`. - // - // We need a better path forward here. A few ideas: - // - Avoid the necessity of having realm subscriptions on deleted/hidden items, maybe by storing all guids in realm - // to a local list so we can better look them up on receiving `DeletedIndices`. - // - Add a new property on `BeatmapSetInfo` to link to the pre-update set, and use that to handle the update case. - Schedule(() => - { - foreach (var set in removeableSets) - removeBeatmapSet(set); - - invalidateAfterChange(); - }); - } - private void beatmapSetsChanged(IRealmCollection sender, ChangeSet? changes) { // If loading test beatmaps, avoid overwriting with realm subscription callbacks. @@ -312,32 +280,30 @@ namespace osu.Game.Screens.Select if (changes == null) { - // During initial population, we must manually account for the fact that our original query was done on an async thread. - // Since then, there may have been imports or deletions. - // Here we manually catch up on any changes. - var realmSets = new HashSet(); + realmBeatmapSets.Clear(); + realmBeatmapSets.AddRange(sender.Select(r => r.ID)); - for (int i = 0; i < sender.Count; i++) - realmSets.Add(sender[i].ID); - - foreach (var id in realmSets) + foreach (var id in realmBeatmapSets) { if (!root.BeatmapSetsByID.ContainsKey(id)) setsRequiringUpdate.Add(realm.Realm.Find(id)!.Detach()); } - - foreach (var id in root.BeatmapSetsByID.Keys) - { - if (!realmSets.Contains(id)) - setsRequiringRemoval.Add(id); - } } else { - foreach (int i in changes.NewModifiedIndices) - setsRequiringUpdate.Add(sender[i].Detach()); + foreach (int i in changes.DeletedIndices.OrderDescending()) + { + setsRequiringRemoval.Add(realmBeatmapSets[i]); + realmBeatmapSets.RemoveAt(i); + } foreach (int i in changes.InsertedIndices) + { + realmBeatmapSets.Insert(i, sender[i].ID); + setsRequiringUpdate.Add(sender[i].Detach()); + } + + foreach (int i in changes.NewModifiedIndices) setsRequiringUpdate.Add(sender[i].Detach()); } @@ -1312,7 +1278,6 @@ namespace osu.Game.Screens.Select base.Dispose(isDisposing); subscriptionSets?.Dispose(); - subscriptionDeletedSets?.Dispose(); subscriptionBeatmaps?.Dispose(); subscriptionHiddenBeatmaps?.Dispose(); } From 1095137a5b82d7c7978d8d80ea6f379a5edf5b70 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 8 Jul 2024 18:48:53 +0900 Subject: [PATCH 1389/2493] Simplify tracking of hidden beatmaps Handling a few extra events is preferrable to keeping a second realm subscription live. --- osu.Game/Screens/Select/BeatmapCarousel.cs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index cab258aaab..b7c9c16112 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -198,7 +198,6 @@ namespace osu.Game.Screens.Select private IDisposable? subscriptionSets; private IDisposable? subscriptionBeatmaps; - private IDisposable? subscriptionHiddenBeatmaps; private readonly DrawablePool setPool = new DrawablePool(100); @@ -263,10 +262,6 @@ namespace osu.Game.Screens.Select subscriptionSets = realm.RegisterForNotifications(getBeatmapSets, beatmapSetsChanged); subscriptionBeatmaps = realm.RegisterForNotifications(r => r.All().Where(b => !b.Hidden), beatmapsChanged); - - // Can't use main subscriptions because we can't lookup deleted indices. - // https://github.com/realm/realm-dotnet/discussions/2634#discussioncomment-1605595. - subscriptionHiddenBeatmaps = realm.RegisterForNotifications(r => r.All().Where(b => b.Hidden), beatmapsChanged); } private void beatmapSetsChanged(IRealmCollection sender, ChangeSet? changes) @@ -1279,7 +1274,6 @@ namespace osu.Game.Screens.Select subscriptionSets?.Dispose(); subscriptionBeatmaps?.Dispose(); - subscriptionHiddenBeatmaps?.Dispose(); } } } From 8f271170e94fa7fdf547b70b5f107a7679194d0c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 8 Jul 2024 19:29:03 +0900 Subject: [PATCH 1390/2493] Coalesce beatmap updates --- osu.Game/Screens/Select/BeatmapCarousel.cs | 103 +++++++++++---------- 1 file changed, 52 insertions(+), 51 deletions(-) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index b7c9c16112..1224bcc6de 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -244,6 +244,9 @@ namespace osu.Game.Screens.Select if (!loadedTestBeatmaps) { + // This is performing an unnecessary second lookup on realm (in addition to the subscription), but for performance reasons + // we require it to be separate: the subscription's initial callback (with `ChangeSet` of `null`) will run on the update + // thread. If we attempt to detach beatmaps in this callback the game will fall over (it takes time). realm.Run(r => loadBeatmapSets(getBeatmapSets(r))); } } @@ -264,25 +267,21 @@ namespace osu.Game.Screens.Select subscriptionBeatmaps = realm.RegisterForNotifications(r => r.All().Where(b => !b.Hidden), beatmapsChanged); } + private readonly HashSet setsRequiringUpdate = new HashSet(); + private readonly HashSet setsRequiringRemoval = new HashSet(); + private void beatmapSetsChanged(IRealmCollection sender, ChangeSet? changes) { // If loading test beatmaps, avoid overwriting with realm subscription callbacks. if (loadedTestBeatmaps) return; - var setsRequiringUpdate = new HashSet(); - var setsRequiringRemoval = new HashSet(); - if (changes == null) { realmBeatmapSets.Clear(); realmBeatmapSets.AddRange(sender.Select(r => r.ID)); - foreach (var id in realmBeatmapSets) - { - if (!root.BeatmapSetsByID.ContainsKey(id)) - setsRequiringUpdate.Add(realm.Realm.Find(id)!.Detach()); - } + loadBeatmapSets(sender); } else { @@ -302,62 +301,64 @@ namespace osu.Game.Screens.Select setsRequiringUpdate.Add(sender[i].Detach()); } - // All local operations must be scheduled. - // - // If we don't schedule, beatmaps getting changed while song select is suspended (ie. last played being updated) - // will cause unexpected sounds and operations to occur in the background. - Schedule(() => + Scheduler.AddOnce(processBeatmapChanges); + } + + // All local operations must be scheduled. + // + // If we don't schedule, beatmaps getting changed while song select is suspended (ie. last played being updated) + // will cause unexpected sounds and operations to occur in the background. + private void processBeatmapChanges() + { + try { - try + foreach (var set in setsRequiringRemoval) removeBeatmapSet(set); + + foreach (var set in setsRequiringUpdate) updateBeatmapSet(set); + + if (setsRequiringRemoval.Count > 0 && SelectedBeatmapInfo != null) { - foreach (var set in setsRequiringRemoval) - removeBeatmapSet(set); + // If SelectedBeatmapInfo is non-null, the set should also be non-null. + Debug.Assert(SelectedBeatmapSet != null); - foreach (var set in setsRequiringUpdate) - updateBeatmapSet(set); + // To handle the beatmap update flow, attempt to track selection changes across delete-insert transactions. + // When an update occurs, the previous beatmap set is either soft or hard deleted. + // Check if the current selection was potentially deleted by re-querying its validity. + bool selectedSetMarkedDeleted = realm.Run(r => r.Find(SelectedBeatmapSet.ID)?.DeletePending != false); - if (changes?.DeletedIndices.Length > 0 && SelectedBeatmapInfo != null) + if (selectedSetMarkedDeleted && setsRequiringUpdate.Any()) { - // If SelectedBeatmapInfo is non-null, the set should also be non-null. - Debug.Assert(SelectedBeatmapSet != null); - - // To handle the beatmap update flow, attempt to track selection changes across delete-insert transactions. - // When an update occurs, the previous beatmap set is either soft or hard deleted. - // Check if the current selection was potentially deleted by re-querying its validity. - bool selectedSetMarkedDeleted = realm.Run(r => r.Find(SelectedBeatmapSet.ID)?.DeletePending != false); - - if (selectedSetMarkedDeleted && setsRequiringUpdate.Any()) + // If it is no longer valid, make the bold assumption that an updated version will be available in the modified/inserted indices. + // This relies on the full update operation being in a single transaction, so please don't change that. + foreach (var set in setsRequiringUpdate) { - // If it is no longer valid, make the bold assumption that an updated version will be available in the modified/inserted indices. - // This relies on the full update operation being in a single transaction, so please don't change that. - foreach (var set in setsRequiringUpdate) + foreach (var beatmapInfo in set.Beatmaps) { - foreach (var beatmapInfo in set.Beatmaps) - { - if (!((IBeatmapMetadataInfo)beatmapInfo.Metadata).Equals(SelectedBeatmapInfo.Metadata)) - continue; + if (!((IBeatmapMetadataInfo)beatmapInfo.Metadata).Equals(SelectedBeatmapInfo.Metadata)) continue; - // Best effort matching. We can't use ID because in the update flow a new version will get its own GUID. - if (beatmapInfo.DifficultyName == SelectedBeatmapInfo.DifficultyName) - { - SelectBeatmap(beatmapInfo); - return; - } + // Best effort matching. We can't use ID because in the update flow a new version will get its own GUID. + if (beatmapInfo.DifficultyName == SelectedBeatmapInfo.DifficultyName) + { + SelectBeatmap(beatmapInfo); + return; } } - - // If a direct selection couldn't be made, it's feasible that the difficulty name (or beatmap metadata) changed. - // Let's attempt to follow set-level selection anyway. - SelectBeatmap(setsRequiringUpdate.First().Beatmaps.First()); } + + // If a direct selection couldn't be made, it's feasible that the difficulty name (or beatmap metadata) changed. + // Let's attempt to follow set-level selection anyway. + SelectBeatmap(setsRequiringUpdate.First().Beatmaps.First()); } } - finally - { - BeatmapSetsLoaded = true; - invalidateAfterChange(); - } - }); + } + finally + { + BeatmapSetsLoaded = true; + invalidateAfterChange(); + } + + setsRequiringRemoval.Clear(); + setsRequiringUpdate.Clear(); } private void beatmapsChanged(IRealmCollection sender, ChangeSet? changes) From 6433f29651855fe8a5379d8b8e8f9635b256a568 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 8 Jul 2024 13:15:24 +0200 Subject: [PATCH 1391/2493] Add failing test case --- .../Visual/Editing/TestSceneEditorClock.cs | 38 +++++++++++++------ 1 file changed, 26 insertions(+), 12 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorClock.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorClock.cs index ed58c59ff0..54cc86bc6b 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorClock.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorClock.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.Linq; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Testing; +using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets.Osu; using osu.Game.Screens.Edit; using osu.Game.Screens.Edit.Components; @@ -19,9 +22,10 @@ namespace osu.Game.Tests.Visual.Editing [Cached] private EditorBeatmap editorBeatmap = new EditorBeatmap(new TestBeatmap(new OsuRuleset().RulesetInfo)); - public TestSceneEditorClock() + [SetUpSteps] + public void SetUpSteps() { - Add(new FillFlowContainer + AddStep("create content", () => Add(new FillFlowContainer { RelativeSizeAxes = Axes.Both, Children = new Drawable[] @@ -39,19 +43,17 @@ namespace osu.Game.Tests.Visual.Editing Size = new Vector2(200, 100) } } + })); + AddStep("set working beatmap", () => + { + Beatmap.Disabled = false; + Beatmap.Value = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo); + // ensure that music controller does not change this beatmap due to it + // completing naturally as part of the test. + Beatmap.Disabled = true; }); } - protected override void LoadComplete() - { - base.LoadComplete(); - - Beatmap.Value = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo); - // ensure that music controller does not change this beatmap due to it - // completing naturally as part of the test. - Beatmap.Disabled = true; - } - [Test] public void TestStopAtTrackEnd() { @@ -102,6 +104,18 @@ namespace osu.Game.Tests.Visual.Editing AddUntilStep("time is clamped to track length", () => EditorClock.CurrentTime, () => Is.EqualTo(EditorClock.TrackLength)); } + [Test] + public void TestAdjustmentsRemovedOnDisposal() + { + AddStep("reset clock", () => EditorClock.Seek(0)); + + AddStep("set 0.25x speed", () => this.ChildrenOfType>().First().Current.Value = 0.25); + AddAssert("track has 0.25x tempo", () => Beatmap.Value.Track.AggregateTempo.Value, () => Is.EqualTo(0.25)); + + AddStep("dispose playback control", () => Clear(disposeChildren: true)); + AddAssert("track has 1x tempo", () => Beatmap.Value.Track.AggregateTempo.Value, () => Is.EqualTo(1)); + } + protected override void Dispose(bool isDisposing) { Beatmap.Disabled = false; From 0fe2c45e1d61fd18aadcfa52762a4600b9f952ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 8 Jul 2024 13:15:50 +0200 Subject: [PATCH 1392/2493] Fix editor playback control not removing correct adjustment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes https://github.com/ppy/osu/issues/28768. great job past me 🤦🤦🤦🤦🤦🤦🤦🤦🤦🤦🤦 --- osu.Game/Screens/Edit/Components/PlaybackControl.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Components/PlaybackControl.cs b/osu.Game/Screens/Edit/Components/PlaybackControl.cs index 0546878788..6319dc892e 100644 --- a/osu.Game/Screens/Edit/Components/PlaybackControl.cs +++ b/osu.Game/Screens/Edit/Components/PlaybackControl.cs @@ -105,7 +105,7 @@ namespace osu.Game.Screens.Edit.Components protected override void Dispose(bool isDisposing) { - Track.Value?.RemoveAdjustment(AdjustableProperty.Frequency, tempoAdjustment); + Track.Value?.RemoveAdjustment(AdjustableProperty.Tempo, tempoAdjustment); base.Dispose(isDisposing); } From 2822ba23770e42d34f46425caeef6c2a061467a1 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Mon, 8 Jul 2024 13:30:11 +0200 Subject: [PATCH 1393/2493] Fix CurrentTimeAccurate being inaccurate if seeking smoothly in the same frame and a transform is already active --- osu.Game.Tests/Visual/Editing/TestSceneEditorClock.cs | 11 +++++++++++ osu.Game/Screens/Edit/EditorClock.cs | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorClock.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorClock.cs index ed58c59ff0..b38c9abfb6 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorClock.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorClock.cs @@ -102,6 +102,17 @@ namespace osu.Game.Tests.Visual.Editing AddUntilStep("time is clamped to track length", () => EditorClock.CurrentTime, () => Is.EqualTo(EditorClock.TrackLength)); } + [Test] + public void TestCurrentTimeDoubleTransform() + { + AddAssert("seek smoothly twice and current time is accurate", () => + { + EditorClock.SeekSmoothlyTo(1000); + EditorClock.SeekSmoothlyTo(2000); + return 2000 == EditorClock.CurrentTimeAccurate; + }); + } + protected override void Dispose(bool isDisposing) { Beatmap.Disabled = false; diff --git a/osu.Game/Screens/Edit/EditorClock.cs b/osu.Game/Screens/Edit/EditorClock.cs index d5ca6fc35e..773abaa737 100644 --- a/osu.Game/Screens/Edit/EditorClock.cs +++ b/osu.Game/Screens/Edit/EditorClock.cs @@ -154,7 +154,7 @@ namespace osu.Game.Screens.Edit /// The current time of this clock, include any active transform seeks performed via . /// public double CurrentTimeAccurate => - Transforms.OfType().FirstOrDefault()?.EndValue ?? CurrentTime; + Transforms.OfType().LastOrDefault()?.EndValue ?? CurrentTime; public double CurrentTime => underlyingClock.CurrentTime; From d5158d10356ee9ef11b6eeffeb0b75f5ad3df4ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 8 Jul 2024 13:36:30 +0200 Subject: [PATCH 1394/2493] Fix incorrect changes around success callback refactor --- .../OnlinePlay/Playlists/PlaylistItemResultsScreen.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs index 51fd912ccc..dc06b88823 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs @@ -113,8 +113,11 @@ namespace osu.Game.Screens.OnlinePlay.Playlists setPositions(lowerScores, userScore.Position.Value, 1); } - Schedule(() => PerformSuccessCallback(scoresCallback, allScores)); - hideLoadingSpinners(); + Schedule(() => + { + PerformSuccessCallback(scoresCallback, allScores); + hideLoadingSpinners(); + }); }; // On failure, fallback to a normal index. @@ -169,7 +172,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists Schedule(() => { PerformSuccessCallback(scoresCallback, r.Scores, r); - hideLoadingSpinners(pivot); + hideLoadingSpinners(r); }); }; From da4067d059b504261002d943afef5c7fc78d661f Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 8 Jul 2024 16:06:31 +0300 Subject: [PATCH 1395/2493] Add failing test case --- .../Gameplay/TestSceneUnorderedBreaks.cs | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 osu.Game.Tests/Visual/Gameplay/TestSceneUnorderedBreaks.cs diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneUnorderedBreaks.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneUnorderedBreaks.cs new file mode 100644 index 0000000000..04265ccc03 --- /dev/null +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneUnorderedBreaks.cs @@ -0,0 +1,70 @@ +// 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.Allocation; +using osu.Framework.Audio; +using osu.Framework.Testing; +using osu.Framework.Timing; +using osu.Framework.Utils; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.Timing; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Osu.Beatmaps; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Screens.Play; +using osu.Game.Storyboards; + +namespace osu.Game.Tests.Visual.Gameplay +{ + public partial class TestSceneUnorderedBreaks : OsuPlayerTestScene + { + [Resolved] + private AudioManager audioManager { get; set; } = null!; + + protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) + { + var beatmap = new OsuBeatmap(); + beatmap.HitObjects.Add(new HitCircle { StartTime = 0 }); + beatmap.HitObjects.Add(new HitCircle { StartTime = 5000 }); + beatmap.HitObjects.Add(new HitCircle { StartTime = 10000 }); + beatmap.Breaks.Add(new BreakPeriod(6000, 9000)); + beatmap.Breaks.Add(new BreakPeriod(1000, 4000)); + return beatmap; + } + + protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard? storyboard = null) => + new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audioManager); + + [SetUpSteps] + public override void SetUpSteps() + { + base.SetUpSteps(); + + AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning); + } + + [Test] + public void TestBreakOverlayVisibility() + { + AddAssert("break overlay hidden", () => !this.ChildrenOfType().Single().Child.IsPresent); + addSeekStep(2000); + AddUntilStep("break overlay visible", () => this.ChildrenOfType().Single().Child.IsPresent); + addSeekStep(5000); + AddAssert("break overlay hidden", () => !this.ChildrenOfType().Single().Child.IsPresent); + addSeekStep(7000); + AddUntilStep("break overlay visible", () => this.ChildrenOfType().Single().Child.IsPresent); + addSeekStep(10000); + AddAssert("break overlay hidden", () => !this.ChildrenOfType().Single().Child.IsPresent); + } + + private void addSeekStep(double time) + { + AddStep($"seek to {time}", () => Beatmap.Value.Track.Seek(time)); + + // Allow a few frames of lenience + AddUntilStep("wait for seek to finish", () => Precision.AlmostEquals(time, Player.DrawableRuleset.FrameStableClock.CurrentTime, 100)); + } + } +} From b33e54d0647916c5f37743f6bcf4b115c04aa62d Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 8 Jul 2024 12:05:15 +0300 Subject: [PATCH 1396/2493] Enforce `IBeatmap.Breaks` to be sorted chronologically --- osu.Game/Beatmaps/Beatmap.cs | 3 ++- osu.Game/Beatmaps/BeatmapConverter.cs | 5 ++++- osu.Game/Beatmaps/IBeatmap.cs | 3 ++- osu.Game/Beatmaps/Timing/BreakPeriod.cs | 14 +++++++++++++- .../Rulesets/Difficulty/DifficultyCalculator.cs | 3 ++- osu.Game/Screens/Edit/EditorBeatmap.cs | 9 +++++++-- 6 files changed, 30 insertions(+), 7 deletions(-) diff --git a/osu.Game/Beatmaps/Beatmap.cs b/osu.Game/Beatmaps/Beatmap.cs index ae77e4adcf..d12ff59198 100644 --- a/osu.Game/Beatmaps/Beatmap.cs +++ b/osu.Game/Beatmaps/Beatmap.cs @@ -8,6 +8,7 @@ using System.Collections.Generic; using System.Linq; using osu.Game.Beatmaps.ControlPoints; using Newtonsoft.Json; +using osu.Framework.Lists; using osu.Game.IO.Serialization.Converters; namespace osu.Game.Beatmaps @@ -61,7 +62,7 @@ namespace osu.Game.Beatmaps public ControlPointInfo ControlPointInfo { get; set; } = new ControlPointInfo(); - public List Breaks { get; set; } = new List(); + public SortedList Breaks { get; set; } = new SortedList(); public List UnhandledEventLines { get; set; } = new List(); diff --git a/osu.Game/Beatmaps/BeatmapConverter.cs b/osu.Game/Beatmaps/BeatmapConverter.cs index 676eb1b159..c43bd494e9 100644 --- a/osu.Game/Beatmaps/BeatmapConverter.cs +++ b/osu.Game/Beatmaps/BeatmapConverter.cs @@ -7,6 +7,8 @@ using System; using System.Collections.Generic; using System.Linq; using System.Threading; +using osu.Framework.Lists; +using osu.Game.Beatmaps.Timing; using osu.Game.Rulesets; using osu.Game.Rulesets.Objects; @@ -50,7 +52,8 @@ namespace osu.Game.Beatmaps original.ControlPointInfo = original.ControlPointInfo.DeepClone(); // Used in osu!mania conversion. - original.Breaks = original.Breaks.ToList(); + original.Breaks = new SortedList(Comparer.Default); + original.Breaks.AddRange(Beatmap.Breaks); return ConvertBeatmap(original, cancellationToken); } diff --git a/osu.Game/Beatmaps/IBeatmap.cs b/osu.Game/Beatmaps/IBeatmap.cs index 176738489a..430a31769b 100644 --- a/osu.Game/Beatmaps/IBeatmap.cs +++ b/osu.Game/Beatmaps/IBeatmap.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; +using osu.Framework.Lists; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.Timing; using osu.Game.Rulesets.Objects; @@ -40,7 +41,7 @@ namespace osu.Game.Beatmaps /// /// The breaks in this beatmap. /// - List Breaks { get; set; } + SortedList Breaks { get; set; } /// /// All lines from the [Events] section which aren't handled in the encoding process yet. diff --git a/osu.Game/Beatmaps/Timing/BreakPeriod.cs b/osu.Game/Beatmaps/Timing/BreakPeriod.cs index d8b500227a..921cfe9c51 100644 --- a/osu.Game/Beatmaps/Timing/BreakPeriod.cs +++ b/osu.Game/Beatmaps/Timing/BreakPeriod.cs @@ -6,7 +6,7 @@ using osu.Game.Screens.Play; namespace osu.Game.Beatmaps.Timing { - public class BreakPeriod : IEquatable + public class BreakPeriod : IEquatable, IComparable { /// /// The minimum gap between the start of the break and the previous object. @@ -76,5 +76,17 @@ namespace osu.Game.Beatmaps.Timing && EndTime == other.EndTime; public override int GetHashCode() => HashCode.Combine(StartTime, EndTime); + + public int CompareTo(BreakPeriod? other) + { + if (ReferenceEquals(this, other)) return 0; + if (ReferenceEquals(null, other)) return 1; + + int result = StartTime.CompareTo(other.StartTime); + if (result != 0) + return result; + + return EndTime.CompareTo(other.EndTime); + } } } diff --git a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs index 722263c58e..63b27243d0 100644 --- a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs +++ b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs @@ -10,6 +10,7 @@ using System.Threading; using JetBrains.Annotations; using osu.Framework.Audio.Track; using osu.Framework.Extensions.IEnumerableExtensions; +using osu.Framework.Lists; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.Timing; @@ -327,7 +328,7 @@ namespace osu.Game.Rulesets.Difficulty set => baseBeatmap.Difficulty = value; } - public List Breaks + public SortedList Breaks { get => baseBeatmap.Breaks; set => baseBeatmap.Breaks = value; diff --git a/osu.Game/Screens/Edit/EditorBeatmap.cs b/osu.Game/Screens/Edit/EditorBeatmap.cs index c8592b5bea..ad31c2ccc3 100644 --- a/osu.Game/Screens/Edit/EditorBeatmap.cs +++ b/osu.Game/Screens/Edit/EditorBeatmap.cs @@ -10,6 +10,7 @@ using System.Linq; using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Lists; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.Legacy; @@ -111,7 +112,11 @@ namespace osu.Game.Screens.Edit trackStartTime(obj); Breaks = new BindableList(playableBeatmap.Breaks); - Breaks.BindCollectionChanged((_, _) => playableBeatmap.Breaks = Breaks.ToList()); + Breaks.BindCollectionChanged((_, _) => + { + playableBeatmap.Breaks.Clear(); + playableBeatmap.Breaks.AddRange(Breaks); + }); PreviewTime = new BindableInt(BeatmapInfo.Metadata.PreviewTime); PreviewTime.BindValueChanged(s => @@ -177,7 +182,7 @@ namespace osu.Game.Screens.Edit public readonly BindableList Breaks; - List IBeatmap.Breaks + SortedList IBeatmap.Breaks { get => PlayableBeatmap.Breaks; set => PlayableBeatmap.Breaks = value; From 0d22c9a9c6b2c0deafdf3cae0afeb557d1584a96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 8 Jul 2024 15:58:21 +0200 Subject: [PATCH 1397/2493] Pass comparer in all usages for consistency --- osu.Game/Beatmaps/Beatmap.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/Beatmap.cs b/osu.Game/Beatmaps/Beatmap.cs index d12ff59198..282f8fe794 100644 --- a/osu.Game/Beatmaps/Beatmap.cs +++ b/osu.Game/Beatmaps/Beatmap.cs @@ -62,7 +62,7 @@ namespace osu.Game.Beatmaps public ControlPointInfo ControlPointInfo { get; set; } = new ControlPointInfo(); - public SortedList Breaks { get; set; } = new SortedList(); + public SortedList Breaks { get; set; } = new SortedList(Comparer.Default); public List UnhandledEventLines { get; set; } = new List(); From 449d50a46a6641b1920104a383366a3a8ae25983 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 8 Jul 2024 16:25:19 +0200 Subject: [PATCH 1398/2493] Add failing test case --- .../Editor/TestSceneEditor.cs | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneEditor.cs b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneEditor.cs index 762238be47..ef4e09c26b 100644 --- a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneEditor.cs +++ b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneEditor.cs @@ -1,13 +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 System.Linq; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.ObjectExtensions; +using osu.Framework.Testing; +using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Mania.Configuration; using osu.Game.Rulesets.Mania.UI; +using osu.Game.Screens.Edit.Timing; using osu.Game.Tests.Visual; +using osuTK.Input; namespace osu.Game.Rulesets.Mania.Tests.Editor { @@ -30,5 +35,43 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor var config = (ManiaRulesetConfigManager)RulesetConfigs.GetConfigFor(Ruleset.Value.CreateInstance()).AsNonNull(); config.BindWith(ManiaRulesetSetting.ScrollDirection, direction); } + + [Test] + public void TestReloadOnBPMChange() + { + HitObjectComposer oldComposer = null!; + + AddStep("store composer", () => oldComposer = this.ChildrenOfType().Single()); + AddUntilStep("composer stored", () => oldComposer, () => Is.Not.Null); + AddStep("switch to timing tab", () => InputManager.Key(Key.F3)); + AddUntilStep("wait for loaded", () => this.ChildrenOfType().ElementAtOrDefault(1), () => Is.Not.Null); + AddStep("change timing point BPM", () => + { + var bpmControl = this.ChildrenOfType().ElementAt(1); + InputManager.MoveMouseTo(bpmControl); + InputManager.Click(MouseButton.Left); + }); + + AddStep("switch back to composer", () => InputManager.Key(Key.F1)); + AddUntilStep("composer reloaded", () => + { + var composer = this.ChildrenOfType().SingleOrDefault(); + return composer != null && composer != oldComposer; + }); + + AddStep("store composer", () => oldComposer = this.ChildrenOfType().Single()); + AddUntilStep("composer stored", () => oldComposer, () => Is.Not.Null); + AddStep("undo", () => + { + InputManager.PressKey(Key.ControlLeft); + InputManager.Key(Key.Z); + InputManager.ReleaseKey(Key.ControlLeft); + }); + AddUntilStep("composer reloaded", () => + { + var composer = this.ChildrenOfType().SingleOrDefault(); + return composer != null && composer != oldComposer; + }); + } } } From 275b959c02801f43563afa471f2ccd0bdd02b9f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 8 Jul 2024 16:28:52 +0200 Subject: [PATCH 1399/2493] Fix composer disappearing when undoing change to control points As mentioned in https://github.com/ppy/osu/issues/28752. Regressed in https://github.com/ppy/osu/pull/28444. --- osu.Game/Screens/Edit/Editor.cs | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 2278af040f..a8a28ef0b8 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -1023,11 +1023,15 @@ namespace osu.Game.Screens.Edit /// /// Forces a reload of the compose screen after significant configuration changes. /// - /// - /// This can be necessary for scrolling rulesets, as they do not easily support control points changing under them. - /// The reason that this works is that will re-instantiate the screen whenever it is requested next. - /// - public void ReloadComposeScreen() => screenContainer.SingleOrDefault(s => s.Type == EditorScreenMode.Compose)?.RemoveAndDisposeImmediately(); + public void ReloadComposeScreen() + { + screenContainer.SingleOrDefault(s => s.Type == EditorScreenMode.Compose)?.RemoveAndDisposeImmediately(); + + // If not currently on compose screen, the reload will happen on next mode change. + // That said, control points *can* change on compose screen (e.g. via undo), so we have to handle that case too. + if (Mode.Value == EditorScreenMode.Compose) + Mode.TriggerChange(); + } [CanBeNull] private ScheduledDelegate playbackDisabledDebounce; From 9a61adc4bc02021f7321d942fc0c7a000b61a666 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 8 Jul 2024 23:59:20 +0900 Subject: [PATCH 1400/2493] Ensure other lists are cleared when realm is reset --- osu.Game/Screens/Select/BeatmapCarousel.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 1224bcc6de..9d61e2a977 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -280,6 +280,8 @@ namespace osu.Game.Screens.Select { realmBeatmapSets.Clear(); realmBeatmapSets.AddRange(sender.Select(r => r.ID)); + setsRequiringRemoval.Clear(); + setsRequiringUpdate.Clear(); loadBeatmapSets(sender); } From ae78c13684b74d6433492e792edd38600e48bfa8 Mon Sep 17 00:00:00 2001 From: Huo Yaoyuan Date: Tue, 5 Mar 2024 20:44:32 +0800 Subject: [PATCH 1401/2493] Use Environment.IsPrivilegedProcess --- .../Security/ElevatedPrivilegesChecker.cs | 37 +------------------ osu.Desktop/osu.Desktop.csproj | 1 - 2 files changed, 1 insertion(+), 37 deletions(-) diff --git a/osu.Desktop/Security/ElevatedPrivilegesChecker.cs b/osu.Desktop/Security/ElevatedPrivilegesChecker.cs index 6665733656..0bed9830df 100644 --- a/osu.Desktop/Security/ElevatedPrivilegesChecker.cs +++ b/osu.Desktop/Security/ElevatedPrivilegesChecker.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Security.Principal; using osu.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; @@ -21,48 +20,14 @@ namespace osu.Desktop.Security [Resolved] private INotificationOverlay notifications { get; set; } = null!; - private bool elevated; - - [BackgroundDependencyLoader] - private void load() - { - elevated = checkElevated(); - } - protected override void LoadComplete() { base.LoadComplete(); - if (elevated) + if (Environment.IsPrivilegedProcess) notifications.Post(new ElevatedPrivilegesNotification()); } - private bool checkElevated() - { - try - { - switch (RuntimeInfo.OS) - { - case RuntimeInfo.Platform.Windows: - if (!OperatingSystem.IsWindows()) return false; - - var windowsIdentity = WindowsIdentity.GetCurrent(); - var windowsPrincipal = new WindowsPrincipal(windowsIdentity); - - return windowsPrincipal.IsInRole(WindowsBuiltInRole.Administrator); - - case RuntimeInfo.Platform.macOS: - case RuntimeInfo.Platform.Linux: - return Mono.Unix.Native.Syscall.geteuid() == 0; - } - } - catch - { - } - - return false; - } - private partial class ElevatedPrivilegesNotification : SimpleNotification { public override bool IsImportant => true; diff --git a/osu.Desktop/osu.Desktop.csproj b/osu.Desktop/osu.Desktop.csproj index e7a63bd921..acb53835a3 100644 --- a/osu.Desktop/osu.Desktop.csproj +++ b/osu.Desktop/osu.Desktop.csproj @@ -24,7 +24,6 @@ - From cd968d4185bb34dbe177673c5960eb68714b2ac5 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Mon, 8 Jul 2024 16:46:35 -0700 Subject: [PATCH 1402/2493] Add caret transition to beatmap sort tab items --- .../Overlays/BeatmapListing/BeatmapListingSortTabControl.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapListingSortTabControl.cs b/osu.Game/Overlays/BeatmapListing/BeatmapListingSortTabControl.cs index e3e2bcaf9a..7f8b68fd6c 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapListingSortTabControl.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapListingSortTabControl.cs @@ -126,7 +126,8 @@ namespace osu.Game.Overlays.BeatmapListing Origin = Anchor.Centre, AlwaysPresent = true, Alpha = 0, - Size = new Vector2(6) + Size = new Vector2(6), + Icon = FontAwesome.Solid.CaretDown, }); } @@ -136,7 +137,7 @@ namespace osu.Game.Overlays.BeatmapListing SortDirection.BindValueChanged(direction => { - icon.Icon = direction.NewValue == Overlays.SortDirection.Ascending && Active.Value ? FontAwesome.Solid.CaretUp : FontAwesome.Solid.CaretDown; + icon.ScaleTo(direction.NewValue == Overlays.SortDirection.Ascending && Active.Value ? new Vector2(1f, -1f) : Vector2.One, 300, Easing.OutQuint); }, true); } From c8b9c117cded884aaa4d7d97e5116300f07b81b7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 9 Jul 2024 15:47:41 +0900 Subject: [PATCH 1403/2493] Add failing test showing realm not sending through null `ChangeSet` --- .../RealmSubscriptionRegistrationTests.cs | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/osu.Game.Tests/Database/RealmSubscriptionRegistrationTests.cs b/osu.Game.Tests/Database/RealmSubscriptionRegistrationTests.cs index 45842a952a..14864f7aa1 100644 --- a/osu.Game.Tests/Database/RealmSubscriptionRegistrationTests.cs +++ b/osu.Game.Tests/Database/RealmSubscriptionRegistrationTests.cs @@ -71,6 +71,35 @@ namespace osu.Game.Tests.Database } } + [Test] + public void TestSubscriptionInitialChangeSetNull() + { + ChangeSet? firstChanges = null; + int receivedChangesCount = 0; + + RunTestWithRealm((realm, _) => + { + var registration = realm.RegisterForNotifications(r => r.All(), onChanged); + + realm.WriteAsync(r => r.Add(TestResources.CreateTestBeatmapSetInfo())).WaitSafely(); + + realm.Run(r => r.Refresh()); + + Assert.That(receivedChangesCount, Is.EqualTo(2)); + Assert.That(firstChanges, Is.Null); + + registration.Dispose(); + }); + + void onChanged(IRealmCollection sender, ChangeSet? changes) + { + if (receivedChangesCount == 0) + firstChanges = changes; + + receivedChangesCount++; + } + } + [Test] public void TestSubscriptionWithAsyncWrite() { From 2423bbb776bb0fd5042693684cd2e57ac7f3eff8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 9 Jul 2024 15:28:35 +0900 Subject: [PATCH 1404/2493] Ensure realm subscriptions always fire initial callback with null `ChangeSet` We expect this to be the case, but it turns out that it [may be coalesced](https://www.mongodb.com/docs/realm-sdks/dotnet/latest/reference/Realms.IRealmCollection-1.html#Realms_IRealmCollection_1_SubscribeForNotifications_Realms_NotificationCallbackDelegate__0__Realms_KeyPathsCollection_): > Notifications are delivered via the standard event loop, and so can't > be delivered while the event loop is blocked by other activity. When > notifications can't be delivered instantly, multiple notifications may > be coalesced into a single notification. This can include the > notification with the initial collection. Rather than struggle with handling this locally every time, let's fix the callback at our end to ensure we receive the initial null case. I've raised concern for the API being a bit silly with realm (https://github.com/realm/realm-dotnet/issues/3641). --- osu.Game/Database/RealmObjectExtensions.cs | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/osu.Game/Database/RealmObjectExtensions.cs b/osu.Game/Database/RealmObjectExtensions.cs index 72529ed9ff..bd8c52bb85 100644 --- a/osu.Game/Database/RealmObjectExtensions.cs +++ b/osu.Game/Database/RealmObjectExtensions.cs @@ -65,7 +65,8 @@ namespace osu.Game.Database if (!d.Beatmaps.Contains(existingBeatmap)) { Debug.Fail("Beatmaps should never become detached under normal circumstances. If this ever triggers, it should be investigated further."); - Logger.Log("WARNING: One of the difficulties in a beatmap was detached from its set. Please save a copy of logs and report this to devs.", LoggingTarget.Database, LogLevel.Important); + Logger.Log("WARNING: One of the difficulties in a beatmap was detached from its set. Please save a copy of logs and report this to devs.", LoggingTarget.Database, + LogLevel.Important); d.Beatmaps.Add(existingBeatmap); } @@ -291,7 +292,21 @@ namespace osu.Game.Database if (!RealmAccess.CurrentThreadSubscriptionsAllowed) throw new InvalidOperationException($"Make sure to call {nameof(RealmAccess)}.{nameof(RealmAccess.RegisterForNotifications)}"); - return collection.SubscribeForNotifications(callback); + bool initial = true; + return collection.SubscribeForNotifications(((sender, changes) => + { + if (initial) + { + initial = false; + + // Realm might coalesce the initial callback, meaning we never receive a `ChangeSet` of `null` marking the first callback. + // Let's decouple it for simplicity in handling. + if (changes != null) + callback(sender, null); + } + + callback(sender, changes); + })); } /// From ee9e329db33c11cca18699e5d6396cedff3e07f9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 9 Jul 2024 16:05:58 +0900 Subject: [PATCH 1405/2493] Inhibit original callback from firing when sending initial changeset --- osu.Game.Tests/Database/RealmSubscriptionRegistrationTests.cs | 2 +- osu.Game/Database/RealmObjectExtensions.cs | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Database/RealmSubscriptionRegistrationTests.cs b/osu.Game.Tests/Database/RealmSubscriptionRegistrationTests.cs index 14864f7aa1..e5be4d665b 100644 --- a/osu.Game.Tests/Database/RealmSubscriptionRegistrationTests.cs +++ b/osu.Game.Tests/Database/RealmSubscriptionRegistrationTests.cs @@ -85,7 +85,7 @@ namespace osu.Game.Tests.Database realm.Run(r => r.Refresh()); - Assert.That(receivedChangesCount, Is.EqualTo(2)); + Assert.That(receivedChangesCount, Is.EqualTo(1)); Assert.That(firstChanges, Is.Null); registration.Dispose(); diff --git a/osu.Game/Database/RealmObjectExtensions.cs b/osu.Game/Database/RealmObjectExtensions.cs index bd8c52bb85..2fa3b8a880 100644 --- a/osu.Game/Database/RealmObjectExtensions.cs +++ b/osu.Game/Database/RealmObjectExtensions.cs @@ -302,7 +302,10 @@ namespace osu.Game.Database // Realm might coalesce the initial callback, meaning we never receive a `ChangeSet` of `null` marking the first callback. // Let's decouple it for simplicity in handling. if (changes != null) + { callback(sender, null); + return; + } } callback(sender, changes); From aadcc5384d77201aacdac55e93e3f5743e2757bf Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 9 Jul 2024 16:16:19 +0900 Subject: [PATCH 1406/2493] Adjust editor transparent tweens to be less "flashy" Touched on in https://github.com/ppy/osu/discussions/28581. After a bit more usage of the editor I do agree with this and think that making the fades a bit more gentle helps a lot. --- osu.Game/Rulesets/Edit/HitObjectComposer.cs | 13 ++++++++++--- osu.Game/Screens/Edit/BottomBar.cs | 9 ++++++--- .../Compose/Components/Timeline/TimelineArea.cs | 8 +++++++- 3 files changed, 23 insertions(+), 7 deletions(-) diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs index 18a50763db..1ba488d027 100644 --- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs @@ -269,9 +269,16 @@ namespace osu.Game.Rulesets.Edit composerFocusMode.BindValueChanged(_ => { - float targetAlpha = composerFocusMode.Value ? 0.5f : 1; - leftToolboxBackground.FadeTo(targetAlpha, 400, Easing.OutQuint); - rightToolboxBackground.FadeTo(targetAlpha, 400, Easing.OutQuint); + if (!composerFocusMode.Value) + { + leftToolboxBackground.FadeIn(750, Easing.OutQuint); + rightToolboxBackground.FadeIn(750, Easing.OutQuint); + } + else + { + leftToolboxBackground.Delay(600).FadeTo(0.5f, 4000, Easing.OutQuint); + rightToolboxBackground.Delay(600).FadeTo(0.5f, 4000, Easing.OutQuint); + } }, true); } diff --git a/osu.Game/Screens/Edit/BottomBar.cs b/osu.Game/Screens/Edit/BottomBar.cs index 6118adc0d7..dd56752119 100644 --- a/osu.Game/Screens/Edit/BottomBar.cs +++ b/osu.Game/Screens/Edit/BottomBar.cs @@ -82,10 +82,13 @@ namespace osu.Game.Screens.Edit saveInProgress.BindValueChanged(_ => TestGameplayButton.Enabled.Value = !saveInProgress.Value, true); composerFocusMode.BindValueChanged(_ => { - float targetAlpha = composerFocusMode.Value ? 0.5f : 1; - foreach (var c in this.ChildrenOfType()) - c.Background.FadeTo(targetAlpha, 400, Easing.OutQuint); + { + if (!composerFocusMode.Value) + c.Background.FadeIn(750, Easing.OutQuint); + else + c.Background.Delay(600).FadeTo(0.5f, 4000, Easing.OutQuint); + } }, true); } } diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineArea.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineArea.cs index cee7212a5d..ff92e658d9 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineArea.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineArea.cs @@ -132,7 +132,13 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline { base.LoadComplete(); - composerFocusMode.BindValueChanged(_ => timelineBackground.FadeTo(composerFocusMode.Value ? 0.5f : 1, 400, Easing.OutQuint), true); + composerFocusMode.BindValueChanged(_ => + { + if (!composerFocusMode.Value) + timelineBackground.FadeIn(750, Easing.OutQuint); + else + timelineBackground.Delay(600).FadeTo(0.5f, 4000, Easing.OutQuint); + }, true); } } } From 50818da166110bbaadf47ad7c27d05dd9c5008ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 8 Jul 2024 12:33:00 +0200 Subject: [PATCH 1407/2493] Ensure timeline ticks aren't hidden by other pieces Addresses https://github.com/ppy/osu/issues/28667. --- .../Compose/Components/BeatDivisorControl.cs | 6 ++--- .../Compose/Components/Timeline/Timeline.cs | 12 +++++++--- .../Timeline/TimelineTickDisplay.cs | 23 +++++++++++++++++-- 3 files changed, 33 insertions(+), 8 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs b/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs index faab5e7f78..1d8266d610 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs @@ -150,7 +150,7 @@ namespace osu.Game.Screens.Edit.Compose.Components { new TextFlowContainer(s => s.Font = s.Font.With(size: 14)) { - Padding = new MarginPadding { Horizontal = 15, Vertical = 8 }, + Padding = new MarginPadding { Horizontal = 15, Vertical = 2 }, Text = "beat snap", RelativeSizeAxes = Axes.X, TextAnchor = Anchor.TopCentre, @@ -159,7 +159,7 @@ namespace osu.Game.Screens.Edit.Compose.Components }, RowDimensions = new[] { - new Dimension(GridSizeMode.Absolute, 30), + new Dimension(GridSizeMode.Absolute, 40), new Dimension(GridSizeMode.Absolute, 20), new Dimension(GridSizeMode.Absolute, 15) } @@ -526,7 +526,7 @@ namespace osu.Game.Screens.Edit.Compose.Components AlwaysDisplayed = alwaysDisplayed; Divisor = divisor; - Size = new Vector2(6f, 12) * BindableBeatDivisor.GetSize(divisor); + Size = new Vector2(6f, 18) * BindableBeatDivisor.GetSize(divisor); Alpha = alwaysDisplayed ? 1 : 0; InternalChild = new Box { RelativeSizeAxes = Axes.Both }; diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs index fa9964b104..05e44d4737 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs @@ -25,7 +25,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline [Cached] public partial class Timeline : ZoomableScrollContainer, IPositionSnapProvider { - private const float timeline_height = 72; + private const float timeline_height = 80; private const float timeline_expanded_height = 94; private readonly Drawable userContent; @@ -97,6 +97,11 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline // We don't want the centre marker to scroll AddInternal(centreMarker = new CentreMarker()); + ticks = new TimelineTickDisplay + { + Padding = new MarginPadding { Vertical = 2, }, + }; + AddRange(new Drawable[] { controlPoints = new TimelineControlPointDisplay @@ -104,6 +109,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline RelativeSizeAxes = Axes.X, Height = timeline_expanded_height, }, + ticks, mainContent = new Container { RelativeSizeAxes = Axes.X, @@ -120,7 +126,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline HighColour = colours.BlueDarker, }, centreMarker.CreateProxy(), - ticks = new TimelineTickDisplay(), + ticks.CreateProxy(), new Box { Name = "zero marker", @@ -175,7 +181,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline if (visible.NewValue) { this.ResizeHeightTo(timeline_expanded_height, 200, Easing.OutQuint); - mainContent.MoveToY(20, 200, Easing.OutQuint); + mainContent.MoveToY(15, 200, Easing.OutQuint); // delay the fade in else masking looks weird. controlPoints.Delay(180).FadeIn(400, Easing.OutQuint); diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs index e16c8519e5..8de5087850 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs @@ -9,6 +9,7 @@ using osu.Framework.Caching; using osu.Framework.Graphics; using osu.Framework.Logging; using osu.Game.Beatmaps; +using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Screens.Edit.Components.Timelines.Summary.Parts; using osu.Game.Screens.Edit.Components.Timelines.Summary.Visualisations; @@ -41,16 +42,21 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline RelativeSizeAxes = Axes.Both; } + private readonly BindableBool showTimingChanges = new BindableBool(true); + private readonly Cached tickCache = new Cached(); [BackgroundDependencyLoader] - private void load() + private void load(OsuConfigManager configManager) { beatDivisor.BindValueChanged(_ => invalidateTicks()); if (changeHandler != null) // currently this is the best way to handle any kind of timing changes. changeHandler.OnStateChange += invalidateTicks; + + configManager.BindWith(OsuSetting.EditorTimelineShowTimingChanges, showTimingChanges); + showTimingChanges.BindValueChanged(_ => invalidateTicks()); } private void invalidateTicks() @@ -142,7 +148,20 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline var line = getNextUsableLine(); line.X = xPos; line.Width = PointVisualisation.MAX_WIDTH * size.X; - line.Height = 0.9f * size.Y; + + if (showTimingChanges.Value) + { + line.Anchor = Anchor.BottomLeft; + line.Origin = Anchor.BottomCentre; + line.Height = 0.7f + size.Y * 0.28f; + } + else + { + line.Anchor = Anchor.CentreLeft; + line.Origin = Anchor.Centre; + line.Height = 0.92f + size.Y * 0.07f; + } + line.Colour = colour; } From 29b89486097bc3c5877e32d78817f2d3f46d0d73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 8 Jul 2024 12:33:32 +0200 Subject: [PATCH 1408/2493] Slim down bottom timeline This removes the BPM display, which is commonly cited to have no functional purpose by users, and reduces the height of the bottom bar in exchange for more space for the playfield. --- osu.Game/Screens/Edit/BottomBar.cs | 2 +- .../Edit/Components/PlaybackControl.cs | 4 +-- .../Edit/Components/TimeInfoContainer.cs | 26 +------------------ osu.Game/Screens/Edit/Editor.cs | 2 +- 4 files changed, 5 insertions(+), 29 deletions(-) diff --git a/osu.Game/Screens/Edit/BottomBar.cs b/osu.Game/Screens/Edit/BottomBar.cs index 6118adc0d7..fb233381aa 100644 --- a/osu.Game/Screens/Edit/BottomBar.cs +++ b/osu.Game/Screens/Edit/BottomBar.cs @@ -31,7 +31,7 @@ namespace osu.Game.Screens.Edit RelativeSizeAxes = Axes.X; - Height = 60; + Height = 40; Masking = true; EdgeEffect = new EdgeEffectParameters diff --git a/osu.Game/Screens/Edit/Components/PlaybackControl.cs b/osu.Game/Screens/Edit/Components/PlaybackControl.cs index 6319dc892e..9fe6160ab4 100644 --- a/osu.Game/Screens/Edit/Components/PlaybackControl.cs +++ b/osu.Game/Screens/Edit/Components/PlaybackControl.cs @@ -46,8 +46,8 @@ namespace osu.Game.Screens.Edit.Components { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, - Scale = new Vector2(1.4f), - IconScale = new Vector2(1.4f), + Scale = new Vector2(1.2f), + IconScale = new Vector2(1.2f), Icon = FontAwesome.Regular.PlayCircle, Action = togglePause, }, diff --git a/osu.Game/Screens/Edit/Components/TimeInfoContainer.cs b/osu.Game/Screens/Edit/Components/TimeInfoContainer.cs index 7c03198ec0..37facb3b95 100644 --- a/osu.Game/Screens/Edit/Components/TimeInfoContainer.cs +++ b/osu.Game/Screens/Edit/Components/TimeInfoContainer.cs @@ -17,8 +17,6 @@ namespace osu.Game.Screens.Edit.Components { public partial class TimeInfoContainer : BottomBarContainer { - private OsuSpriteText bpm = null!; - [Resolved] private EditorBeatmap editorBeatmap { get; set; } = null!; @@ -26,38 +24,16 @@ namespace osu.Game.Screens.Edit.Components private EditorClock editorClock { get; set; } = null!; [BackgroundDependencyLoader] - private void load(OsuColour colours, OverlayColourProvider colourProvider) + private void load(OverlayColourProvider colourProvider) { Background.Colour = colourProvider.Background5; Children = new Drawable[] { new TimestampControl(), - bpm = new OsuSpriteText - { - Colour = colours.Orange1, - Anchor = Anchor.CentreLeft, - Font = OsuFont.Torus.With(size: 18, weight: FontWeight.SemiBold), - Position = new Vector2(2, 5), - } }; } - private double? lastBPM; - - protected override void Update() - { - base.Update(); - - double newBPM = editorBeatmap.ControlPointInfo.TimingPointAt(editorClock.CurrentTime).BPM; - - if (lastBPM != newBPM) - { - lastBPM = newBPM; - bpm.Text = @$"{newBPM:0} BPM"; - } - } - private partial class TimestampControl : OsuClickableContainer { private Container hoverLayer = null!; diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index a8a28ef0b8..3f117ebcff 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -325,7 +325,7 @@ namespace osu.Game.Screens.Edit { Name = "Screen container", RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding { Top = 40, Bottom = 60 }, + Padding = new MarginPadding { Top = 40, Bottom = 40 }, Child = screenContainer = new Container { RelativeSizeAxes = Axes.Both, From 5d5dd0de00feb6a06250f6fc427c343d39492b7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 8 Jul 2024 12:41:23 +0200 Subject: [PATCH 1409/2493] Redesign bottom timeline pieces to match stable better --- .../ControlPoints/EffectControlPoint.cs | 2 +- .../ControlPoints/TimingControlPoint.cs | 2 +- .../Timelines/Summary/Parts/BookmarkPart.cs | 1 + .../Parts/ControlPointVisualisation.cs | 4 +--- .../Summary/Parts/EffectPointVisualisation.cs | 9 +++++---- .../Summary/Parts/GroupVisualisation.cs | 12 ++--------- .../Timelines/Summary/Parts/MarkerPart.cs | 20 ++----------------- .../Summary/Parts/PreviewTimePart.cs | 1 + .../Timelines/Summary/SummaryTimeline.cs | 7 +++---- .../Components/Timeline/CentreMarker.cs | 6 +++--- 10 files changed, 20 insertions(+), 44 deletions(-) diff --git a/osu.Game/Beatmaps/ControlPoints/EffectControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/EffectControlPoint.cs index d48ed957ee..4b73994dcf 100644 --- a/osu.Game/Beatmaps/ControlPoints/EffectControlPoint.cs +++ b/osu.Game/Beatmaps/ControlPoints/EffectControlPoint.cs @@ -34,7 +34,7 @@ namespace osu.Game.Beatmaps.ControlPoints set => ScrollSpeedBindable.Value = value; } - public override Color4 GetRepresentingColour(OsuColour colours) => colours.Purple; + public override Color4 GetRepresentingColour(OsuColour colours) => colours.Orange1; /// /// Whether this control point enables Kiai mode. diff --git a/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs index 9ac361cffe..3360b1d1fa 100644 --- a/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs +++ b/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs @@ -26,7 +26,7 @@ namespace osu.Game.Beatmaps.ControlPoints /// private const double default_beat_length = 60000.0 / 60.0; - public override Color4 GetRepresentingColour(OsuColour colours) => colours.Orange1; + public override Color4 GetRepresentingColour(OsuColour colours) => colours.Red1; public static readonly TimingControlPoint DEFAULT = new TimingControlPoint { diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/BookmarkPart.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/BookmarkPart.cs index 3102bf7c06..ea71f24e9c 100644 --- a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/BookmarkPart.cs +++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/BookmarkPart.cs @@ -24,6 +24,7 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts public BookmarkVisualisation(double startTime) : base(startTime) { + Width = 2; } [BackgroundDependencyLoader] diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/ControlPointVisualisation.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/ControlPointVisualisation.cs index 12620963e1..9ba0cac53f 100644 --- a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/ControlPointVisualisation.cs +++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/ControlPointVisualisation.cs @@ -16,9 +16,7 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts public ControlPointVisualisation(ControlPoint point) { Point = point; - - Height = 0.25f; - Origin = Anchor.TopCentre; + Width = 2; } [BackgroundDependencyLoader] diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/EffectPointVisualisation.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/EffectPointVisualisation.cs index bf87470e01..a3885bc2cc 100644 --- a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/EffectPointVisualisation.cs +++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/EffectPointVisualisation.cs @@ -6,9 +6,9 @@ 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.Game.Beatmaps.ControlPoints; using osu.Game.Graphics; -using osu.Game.Screens.Edit.Components.Timelines.Summary.Visualisations; namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts { @@ -91,12 +91,13 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts Width = (float)(nextControlPoint.Time - effect.Time); - AddInternal(new PointVisualisation + AddInternal(new Circle { RelativeSizeAxes = Axes.Both, - Origin = Anchor.TopLeft, + Anchor = Anchor.BottomLeft, + Origin = Anchor.CentreLeft, Width = 1, - Height = 0.25f, + Height = 0.75f, Depth = float.MaxValue, Colour = effect.GetRepresentingColour(colours).Darken(0.5f), }); diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/GroupVisualisation.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/GroupVisualisation.cs index b39365277f..2c806be162 100644 --- a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/GroupVisualisation.cs +++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/GroupVisualisation.cs @@ -39,19 +39,11 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts switch (point) { case TimingControlPoint: - AddInternal(new ControlPointVisualisation(point) { Y = 0, }); - break; - - case DifficultyControlPoint: - AddInternal(new ControlPointVisualisation(point) { Y = 0.25f, }); - break; - - case SampleControlPoint: - AddInternal(new ControlPointVisualisation(point) { Y = 0.5f, }); + AddInternal(new ControlPointVisualisation(point)); break; case EffectControlPoint effect: - AddInternal(new EffectPointVisualisation(effect) { Y = 0.75f }); + AddInternal(new EffectPointVisualisation(effect)); break; } } 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 ff707407dd..21b3b38388 100644 --- a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/MarkerPart.cs +++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/MarkerPart.cs @@ -8,7 +8,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; using osu.Framework.Threading; -using osu.Game.Graphics; +using osu.Game.Overlays; using osuTK; namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts @@ -73,8 +73,6 @@ 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; @@ -82,32 +80,18 @@ 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), - Y = -box_height, - }, - new Box - { - Anchor = Anchor.BottomCentre, - Origin = Anchor.BottomCentre, - Size = new Vector2(14, box_height), }, new Box { @@ -121,7 +105,7 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts } [BackgroundDependencyLoader] - private void load(OsuColour colours) => Colour = colours.Red1; + private void load(OverlayColourProvider colours) => Colour = colours.Highlight1; } } } diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/PreviewTimePart.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/PreviewTimePart.cs index c63bb7ac24..407173034e 100644 --- a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/PreviewTimePart.cs +++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/PreviewTimePart.cs @@ -32,6 +32,7 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts public PreviewTimeVisualisation(double time) : base(time) { + Width = 2; } [BackgroundDependencyLoader] diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/SummaryTimeline.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/SummaryTimeline.cs index 6199cefb57..92012936bc 100644 --- a/osu.Game/Screens/Edit/Components/Timelines/Summary/SummaryTimeline.cs +++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/SummaryTimeline.cs @@ -23,13 +23,11 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary Children = new Drawable[] { - new MarkerPart { RelativeSizeAxes = Axes.Both }, new ControlPointPart { Anchor = Anchor.Centre, Origin = Anchor.BottomCentre, RelativeSizeAxes = Axes.Both, - Y = -10, Height = 0.35f }, new BookmarkPart @@ -80,8 +78,9 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary Anchor = Anchor.Centre, Origin = Anchor.Centre, RelativeSizeAxes = Axes.Both, - Height = 0.10f - } + Height = 0.15f + }, + new MarkerPart { RelativeSizeAxes = Axes.Both }, }; } } diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/CentreMarker.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/CentreMarker.cs index 74786cc0c9..be1888684e 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/CentreMarker.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/CentreMarker.cs @@ -5,7 +5,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; -using osu.Game.Graphics; +using osu.Game.Overlays; using osuTK; namespace osu.Game.Screens.Edit.Compose.Components.Timeline @@ -44,9 +44,9 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline } [BackgroundDependencyLoader] - private void load(OsuColour colours) + private void load(OverlayColourProvider colours) { - Colour = colours.Red1; + Colour = colours.Highlight1; } } } From 63b43279780c35ed51e1a3799f298ca770135b5c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 9 Jul 2024 16:58:35 +0900 Subject: [PATCH 1410/2493] Ensure beatmap set is always detached when updating Slight performance improvement by doing the detach as early as possible. --- osu.Game/Screens/Select/BeatmapCarousel.cs | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 9d61e2a977..e49b1bc3b5 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -417,16 +417,23 @@ namespace osu.Game.Screens.Select } } - public void UpdateBeatmapSet(BeatmapSetInfo beatmapSet) => Schedule(() => + public void UpdateBeatmapSet(BeatmapSetInfo beatmapSet) { - updateBeatmapSet(beatmapSet); - invalidateAfterChange(); - }); + beatmapSet = beatmapSet.Detach(); + + Schedule(() => + { + updateBeatmapSet(beatmapSet); + invalidateAfterChange(); + }); + } private void updateBeatmapSet(BeatmapSetInfo beatmapSet) { + beatmapSet = beatmapSet.Detach(); + originalBeatmapSetsDetached.RemoveAll(set => set.ID == beatmapSet.ID); - originalBeatmapSetsDetached.Add(beatmapSet.Detach()); + originalBeatmapSetsDetached.Add(beatmapSet); var newSets = new List(); From 920c0e4d25999acef016eb1ad5ccb1031688fd79 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 9 Jul 2024 17:04:51 +0900 Subject: [PATCH 1411/2493] Fix deleted beatmap sets potentially reappearing due to pending update requests --- osu.Game/Screens/Select/BeatmapCarousel.cs | 27 ++++++++++++++-------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index e49b1bc3b5..b17c74e473 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -267,7 +267,7 @@ namespace osu.Game.Screens.Select subscriptionBeatmaps = realm.RegisterForNotifications(r => r.All().Where(b => !b.Hidden), beatmapsChanged); } - private readonly HashSet setsRequiringUpdate = new HashSet(); + private readonly HashSet setsRequiringUpdate = new HashSet(); private readonly HashSet setsRequiringRemoval = new HashSet(); private void beatmapSetsChanged(IRealmCollection sender, ChangeSet? changes) @@ -280,6 +280,7 @@ namespace osu.Game.Screens.Select { realmBeatmapSets.Clear(); realmBeatmapSets.AddRange(sender.Select(r => r.ID)); + setsRequiringRemoval.Clear(); setsRequiringUpdate.Clear(); @@ -289,18 +290,24 @@ namespace osu.Game.Screens.Select { foreach (int i in changes.DeletedIndices.OrderDescending()) { - setsRequiringRemoval.Add(realmBeatmapSets[i]); + Guid id = realmBeatmapSets[i]; + + setsRequiringRemoval.Add(id); + setsRequiringUpdate.Remove(id); + realmBeatmapSets.RemoveAt(i); } foreach (int i in changes.InsertedIndices) { - realmBeatmapSets.Insert(i, sender[i].ID); - setsRequiringUpdate.Add(sender[i].Detach()); + Guid id = sender[i].ID; + + realmBeatmapSets.Insert(i, id); + setsRequiringUpdate.Add(id); } foreach (int i in changes.NewModifiedIndices) - setsRequiringUpdate.Add(sender[i].Detach()); + setsRequiringUpdate.Add(sender[i].ID); } Scheduler.AddOnce(processBeatmapChanges); @@ -316,7 +323,7 @@ namespace osu.Game.Screens.Select { foreach (var set in setsRequiringRemoval) removeBeatmapSet(set); - foreach (var set in setsRequiringUpdate) updateBeatmapSet(set); + foreach (var set in setsRequiringUpdate) updateBeatmapSet(fetchFromID(set)!); if (setsRequiringRemoval.Count > 0 && SelectedBeatmapInfo != null) { @@ -326,7 +333,7 @@ namespace osu.Game.Screens.Select // To handle the beatmap update flow, attempt to track selection changes across delete-insert transactions. // When an update occurs, the previous beatmap set is either soft or hard deleted. // Check if the current selection was potentially deleted by re-querying its validity. - bool selectedSetMarkedDeleted = realm.Run(r => r.Find(SelectedBeatmapSet.ID)?.DeletePending != false); + bool selectedSetMarkedDeleted = fetchFromID(SelectedBeatmapSet.ID)?.DeletePending != false; if (selectedSetMarkedDeleted && setsRequiringUpdate.Any()) { @@ -334,7 +341,7 @@ namespace osu.Game.Screens.Select // This relies on the full update operation being in a single transaction, so please don't change that. foreach (var set in setsRequiringUpdate) { - foreach (var beatmapInfo in set.Beatmaps) + foreach (var beatmapInfo in fetchFromID(set)!.Beatmaps) { if (!((IBeatmapMetadataInfo)beatmapInfo.Metadata).Equals(SelectedBeatmapInfo.Metadata)) continue; @@ -349,7 +356,7 @@ namespace osu.Game.Screens.Select // If a direct selection couldn't be made, it's feasible that the difficulty name (or beatmap metadata) changed. // Let's attempt to follow set-level selection anyway. - SelectBeatmap(setsRequiringUpdate.First().Beatmaps.First()); + SelectBeatmap(fetchFromID(setsRequiringUpdate.First())!.Beatmaps.First()); } } } @@ -361,6 +368,8 @@ namespace osu.Game.Screens.Select setsRequiringRemoval.Clear(); setsRequiringUpdate.Clear(); + + BeatmapSetInfo? fetchFromID(Guid id) => realm.Realm.Find(id); } private void beatmapsChanged(IRealmCollection sender, ChangeSet? changes) From ca4c0aa7e211471f26ee5aa776cd85bc786577c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 9 Jul 2024 10:57:21 +0200 Subject: [PATCH 1412/2493] Remove unused using --- .../Timelines/Summary/Parts/ControlPointVisualisation.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/ControlPointVisualisation.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/ControlPointVisualisation.cs index 9ba0cac53f..47169481e2 100644 --- a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/ControlPointVisualisation.cs +++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/ControlPointVisualisation.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; -using osu.Framework.Graphics; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Graphics; using osu.Game.Screens.Edit.Components.Timelines.Summary.Visualisations; From 306dc37ab5159d825adf9d5db29d50ab491e1e83 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Tue, 9 Jul 2024 12:28:23 +0200 Subject: [PATCH 1413/2493] Make hit object and sample point seek keybinds configurable --- .../Input/Bindings/GlobalActionContainer.cs | 16 +++++++++++ .../GlobalActionKeyBindingStrings.cs | 20 ++++++++++++++ osu.Game/Screens/Edit/Editor.cs | 27 ++++++++++++------- 3 files changed, 53 insertions(+), 10 deletions(-) diff --git a/osu.Game/Input/Bindings/GlobalActionContainer.cs b/osu.Game/Input/Bindings/GlobalActionContainer.cs index ef0c60cd20..542073476f 100644 --- a/osu.Game/Input/Bindings/GlobalActionContainer.cs +++ b/osu.Game/Input/Bindings/GlobalActionContainer.cs @@ -147,6 +147,10 @@ namespace osu.Game.Input.Bindings new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.MouseWheelLeft }, GlobalAction.EditorCycleNextBeatSnapDivisor), new KeyBinding(new[] { InputKey.Control, InputKey.R }, GlobalAction.EditorToggleRotateControl), new KeyBinding(new[] { InputKey.Control, InputKey.E }, GlobalAction.EditorToggleScaleControl), + new KeyBinding(new[] { InputKey.Alt, InputKey.Left }, GlobalAction.EditorSeekToPreviousHitObject), + new KeyBinding(new[] { InputKey.Alt, InputKey.Right }, GlobalAction.EditorSeekToNextHitObject), + new KeyBinding(new[] { InputKey.Alt, InputKey.Shift, InputKey.Left }, GlobalAction.EditorSeekToPreviousSamplePoint), + new KeyBinding(new[] { InputKey.Alt, InputKey.Shift, InputKey.Right }, GlobalAction.EditorSeekToNextSamplePoint), }; private static IEnumerable editorTestPlayKeyBindings => new[] @@ -456,6 +460,18 @@ namespace osu.Game.Input.Bindings [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorTestPlayQuickExitToCurrentTime))] EditorTestPlayQuickExitToCurrentTime, + + [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorSeekToPreviousHitObject))] + EditorSeekToPreviousHitObject, + + [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorSeekToNextHitObject))] + EditorSeekToNextHitObject, + + [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorSeekToPreviousSamplePoint))] + EditorSeekToPreviousSamplePoint, + + [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorSeekToNextSamplePoint))] + EditorSeekToNextSamplePoint, } public enum GlobalActionCategory diff --git a/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs b/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs index 450585f79a..206db1a166 100644 --- a/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs +++ b/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs @@ -404,6 +404,26 @@ namespace osu.Game.Localisation /// public static LocalisableString DecreaseModSpeed => new TranslatableString(getKey(@"decrease_mod_speed"), @"Decrease mod speed"); + /// + /// "Seek to previous hit object" + /// + public static LocalisableString EditorSeekToPreviousHitObject => new TranslatableString(getKey(@"editor_seek_to_previous_hit_object"), @"Seek to previous hit object"); + + /// + /// "Seek to next hit object" + /// + public static LocalisableString EditorSeekToNextHitObject => new TranslatableString(getKey(@"editor_seek_to_next_hit_object"), @"Seek to next hit object"); + + /// + /// "Seek to previous sample point" + /// + public static LocalisableString EditorSeekToPreviousSamplePoint => new TranslatableString(getKey(@"editor_seek_to_previous_sample_point"), @"Seek to previous sample point"); + + /// + /// "Seek to next sample point" + /// + public static LocalisableString EditorSeekToNextSamplePoint => new TranslatableString(getKey(@"editor_seek_to_next_sample_point"), @"Seek to next sample point"); + private static string getKey(string key) => $@"{prefix}:{key}"; } } diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index acb9b93114..214549a68d 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -594,7 +594,7 @@ namespace osu.Game.Screens.Edit protected override bool OnKeyDown(KeyDownEvent e) { - if (e.ControlPressed || e.SuperPressed) return false; + if (e.ControlPressed || e.AltPressed || e.SuperPressed) return false; switch (e.Key) { @@ -746,6 +746,22 @@ namespace osu.Game.Screens.Edit bottomBar.TestGameplayButton.TriggerClick(); return true; + case GlobalAction.EditorSeekToPreviousHitObject: + seekHitObject(-1); + return true; + + case GlobalAction.EditorSeekToNextHitObject: + seekHitObject(1); + return true; + + case GlobalAction.EditorSeekToPreviousSamplePoint: + seekSamplePoint(-1); + return true; + + case GlobalAction.EditorSeekToNextSamplePoint: + seekSamplePoint(1); + return true; + default: return false; } @@ -1130,15 +1146,6 @@ namespace osu.Game.Screens.Edit private void seek(UIEvent e, int direction) { - if (e.AltPressed) - { - if (e.ShiftPressed) - seekSamplePoint(direction); - else - seekHitObject(direction); - return; - } - double amount = e.ShiftPressed ? 4 : 1; bool trackPlaying = clock.IsRunning; From 0e2e44a2f50349e45d370a043fe1b5d4f493bb74 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 9 Jul 2024 20:34:44 +0900 Subject: [PATCH 1414/2493] Add failing test case showing editor save then delete failure --- .../TestSceneBeatmapEditorNavigation.cs | 42 +++++++++++++++++-- osu.Game/Screens/Edit/Editor.cs | 2 +- 2 files changed, 40 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneBeatmapEditorNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneBeatmapEditorNavigation.cs index e3a8e575f8..1ac4bb347b 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneBeatmapEditorNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneBeatmapEditorNavigation.cs @@ -29,6 +29,35 @@ namespace osu.Game.Tests.Visual.Navigation { public partial class TestSceneBeatmapEditorNavigation : OsuGameTestScene { + [Test] + public void TestSaveThenDeleteActuallyDeletesAtSongSelect() + { + BeatmapSetInfo beatmapSet = null!; + + AddStep("import test beatmap", () => Game.BeatmapManager.Import(TestResources.GetTestBeatmapForImport()).WaitSafely()); + AddStep("retrieve beatmap", () => beatmapSet = Game.BeatmapManager.QueryBeatmapSet(set => !set.Protected).AsNonNull().Value.Detach()); + + AddStep("present beatmap", () => Game.PresentBeatmap(beatmapSet)); + AddUntilStep("wait for song select", + () => Game.Beatmap.Value.BeatmapSetInfo.Equals(beatmapSet) + && Game.ScreenStack.CurrentScreen is PlaySongSelect songSelect + && songSelect.IsLoaded); + + AddStep("open editor", () => ((PlaySongSelect)Game.ScreenStack.CurrentScreen).Edit(beatmapSet.Beatmaps.First(beatmap => beatmap.Ruleset.OnlineID == 0))); + AddUntilStep("wait for editor open", () => Game.ScreenStack.CurrentScreen is Editor editor && editor.ReadyForUse); + + makeMetadataChange(); + + AddAssert("save", () => Game.ChildrenOfType().Single().Save()); + + AddStep("delete beatmap", () => Game.BeatmapManager.Delete(beatmapSet)); + + AddStep("exit", () => Game.ChildrenOfType().Single().Exit()); + + AddUntilStep("wait for song select", () => Game.ScreenStack.CurrentScreen is PlaySongSelect songSelect + && songSelect.Beatmap.Value is DummyWorkingBeatmap); + } + [Test] public void TestChangeMetadataExitWhileTextboxFocusedPromptsSave() { @@ -47,6 +76,15 @@ namespace osu.Game.Tests.Visual.Navigation AddStep("open editor", () => ((PlaySongSelect)Game.ScreenStack.CurrentScreen).Edit(beatmapSet.Beatmaps.First(beatmap => beatmap.Ruleset.OnlineID == 0))); AddUntilStep("wait for editor open", () => Game.ScreenStack.CurrentScreen is Editor editor && editor.ReadyForUse); + makeMetadataChange(commit: false); + + AddStep("exit", () => Game.ChildrenOfType().Single().Exit()); + + AddUntilStep("save dialog displayed", () => Game.ChildrenOfType().SingleOrDefault()?.CurrentDialog is PromptForSaveDialog); + } + + private void makeMetadataChange(bool commit = true) + { AddStep("change to song setup", () => InputManager.Key(Key.F4)); TextBox textbox = null!; @@ -77,9 +115,7 @@ namespace osu.Game.Tests.Visual.Navigation InputManager.Keys(PlatformAction.Paste); }); - AddStep("exit", () => Game.ChildrenOfType().Single().Exit()); - - AddUntilStep("save dialog displayed", () => Game.ChildrenOfType().SingleOrDefault()?.CurrentDialog is PromptForSaveDialog); + if (commit) AddStep("commit", () => InputManager.Key(Key.Enter)); } [Test] diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index a8a28ef0b8..27d0392b1e 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -521,7 +521,7 @@ namespace osu.Game.Screens.Edit /// Saves the currently edited beatmap. /// /// Whether the save was successful. - protected bool Save() + internal bool Save() { if (!canSave) { From 123d3d2ff814bb0fca56334de90c7a8ba5f1e7f9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 9 Jul 2024 20:44:55 +0900 Subject: [PATCH 1415/2493] Add similar special case for insert after removal --- osu.Game/Screens/Select/BeatmapCarousel.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index b17c74e473..3f9e676068 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -302,8 +302,10 @@ namespace osu.Game.Screens.Select { Guid id = sender[i].ID; - realmBeatmapSets.Insert(i, id); + setsRequiringRemoval.Remove(id); setsRequiringUpdate.Add(id); + + realmBeatmapSets.Insert(i, id); } foreach (int i in changes.NewModifiedIndices) From ec9040798f233da015e928cd3d2b0273ec567be1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 9 Jul 2024 13:52:36 +0200 Subject: [PATCH 1416/2493] Run stacking when performing movement in osu! composer Closes https://github.com/ppy/osu/issues/28635. --- .../Edit/OsuSelectionHandler.cs | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs index 41d47d31d0..9b4b77b625 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs @@ -50,12 +50,33 @@ namespace osu.Game.Rulesets.Osu.Edit { var hitObjects = selectedMovableObjects; + var localDelta = this.ScreenSpaceDeltaToParentSpace(moveEvent.ScreenSpaceDelta); + + // this conditional is a rather ugly special case for stacks. + // as it turns out, adding the `EditorBeatmap.Update()` call at the end of this would cause stacked objects to jitter when moved around + // (they would stack and then unstack every frame). + // the reason for that is that the selection handling abstractions are not aware of the distinction between "displayed" and "actual" position + // which is unique to osu! due to stacking being applied as a post-processing step. + // therefore, the following loop would occur: + // - on frame 1 the blueprint is snapped to the stack's baseline position. `EditorBeatmap.Update()` applies stacking successfully, + // the blueprint moves up the stack from its original drag position. + // - on frame 2 the blueprint's position is now the *stacked* position, which is interpreted higher up as *manually performing an unstack* + // to the blueprint's unstacked position (as the machinery higher up only cares about differences in screen space position). + if (hitObjects.Any(h => Precision.AlmostEquals(localDelta, -h.StackOffset))) + return true; + // this will potentially move the selection out of bounds... foreach (var h in hitObjects) - h.Position += this.ScreenSpaceDeltaToParentSpace(moveEvent.ScreenSpaceDelta); + h.Position += localDelta; // but this will be corrected. moveSelectionInBounds(); + + // update all of the objects in order to update stacking. + // in particular, this causes stacked objects to instantly unstack on drag. + foreach (var h in hitObjects) + EditorBeatmap.Update(h); + return true; } From 9cc0e0137b5c7d0c5486fd993936ea34cec7c1a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 9 Jul 2024 13:58:58 +0200 Subject: [PATCH 1417/2493] Snap to stack in osu! composer when dragging to any of the items on it Previously it would be required to drag to the starting position of the stack which feels weird. --- osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs index 26bd96cc3a..3c1d0fbb1c 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs @@ -295,6 +295,12 @@ namespace osu.Game.Rulesets.Osu.Edit if (Vector2.Distance(closestSnapPosition, screenSpacePosition) < snapRadius) { + // if the snap target is a stacked object, snap to its unstacked position rather than its stacked position. + // this is intended to make working with stacks easier (because thanks to this, you can drag an object to any + // of the items on the stack to add an object to it, rather than having to drag to the position of the *first* object on it at all times). + if (b.Item is OsuHitObject osuObject && osuObject.StackOffset != Vector2.Zero) + closestSnapPosition = b.ToScreenSpace(b.ToLocalSpace(closestSnapPosition) - osuObject.StackOffset); + // only return distance portion, since time is not really valid snapResult = new SnapResult(closestSnapPosition, null, playfield); return true; From 8c81ba3357dc91e2329d88a93240abfd7efd0c55 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Tue, 9 Jul 2024 14:36:24 -0700 Subject: [PATCH 1418/2493] Fix preview track persisting to play after leaving multi/playlists room --- osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs index 4eb092d08b..515d9fc7a5 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs @@ -80,6 +80,9 @@ namespace osu.Game.Screens.OnlinePlay.Match [Resolved(canBeNull: true)] protected OnlinePlayScreen ParentScreen { get; private set; } + [Resolved] + private PreviewTrackManager previewTrackManager { get; set; } = null!; + [Cached] private readonly OnlinePlayBeatmapAvailabilityTracker beatmapAvailabilityTracker = new OnlinePlayBeatmapAvailabilityTracker(); @@ -483,6 +486,8 @@ namespace osu.Game.Screens.OnlinePlay.Match { UserModsSelectOverlay.Hide(); endHandlingTrack(); + + previewTrackManager.StopAnyPlaying(this); } private void endHandlingTrack() From 4e1240c349169b3493d799f85427f65a1c53717b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 10 Jul 2024 13:54:27 +0900 Subject: [PATCH 1419/2493] Migrate `ShearedOverlayContainer` to NRT --- .../Overlays/Mods/ShearedOverlayContainer.cs | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/osu.Game/Overlays/Mods/ShearedOverlayContainer.cs b/osu.Game/Overlays/Mods/ShearedOverlayContainer.cs index c9c3c62404..d3326cb86b 100644 --- a/osu.Game/Overlays/Mods/ShearedOverlayContainer.cs +++ b/osu.Game/Overlays/Mods/ShearedOverlayContainer.cs @@ -1,9 +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 JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -30,16 +27,15 @@ namespace osu.Game.Overlays.Mods /// /// The overlay's header. /// - protected ShearedOverlayHeader Header { get; private set; } + protected ShearedOverlayHeader Header { get; private set; } = null!; /// /// The overlay's footer. /// - protected Container Footer { get; private set; } + protected Container Footer { get; private set; } = null!; - [Resolved(canBeNull: true)] - [CanBeNull] - private ScreenFooter footer { get; set; } + [Resolved] + private ScreenFooter? footer { get; set; } // todo: very temporary property that will be removed once ModSelectOverlay and FirstRunSetupOverlay are updated to use new footer. public virtual bool UseNewFooter => false; @@ -48,17 +44,17 @@ namespace osu.Game.Overlays.Mods /// A container containing all content, including the header and footer. /// May be used for overlay-wide animations. /// - protected Container TopLevelContent { get; private set; } + protected Container TopLevelContent { get; private set; } = null!; /// /// A container for content that is to be displayed between the header and footer. /// - protected Container MainAreaContent { get; private set; } + protected Container MainAreaContent { get; private set; } = null!; /// /// A container for content that is to be displayed inside the footer. /// - protected Container FooterContent { get; private set; } + protected Container FooterContent { get; private set; } = null!; protected override bool StartHidden => true; From f2810193588f55ebc91f17ce4e39a68be7ca54fc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 10 Jul 2024 13:58:50 +0900 Subject: [PATCH 1420/2493] Rename method to match provided argument --- osu.Game/Overlays/Mods/ShearedOverlayContainer.cs | 4 ++-- osu.Game/Screens/Footer/ScreenFooter.cs | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game/Overlays/Mods/ShearedOverlayContainer.cs b/osu.Game/Overlays/Mods/ShearedOverlayContainer.cs index d3326cb86b..aed9b395f6 100644 --- a/osu.Game/Overlays/Mods/ShearedOverlayContainer.cs +++ b/osu.Game/Overlays/Mods/ShearedOverlayContainer.cs @@ -152,7 +152,7 @@ namespace osu.Game.Overlays.Mods if (UseNewFooter && footer != null) { - footer.SetOverlayContent(this); + footer.SetActiveOverlayContainer(this); if (footer.State.Value == Visibility.Hidden) { @@ -175,7 +175,7 @@ namespace osu.Game.Overlays.Mods if (UseNewFooter && footer != null) { - footer.ClearOverlayContent(); + footer.ClearActiveOverlayContainer(); if (hideFooterOnPopOut) { diff --git a/osu.Game/Screens/Footer/ScreenFooter.cs b/osu.Game/Screens/Footer/ScreenFooter.cs index cef891f8c0..f9a6d54b96 100644 --- a/osu.Game/Screens/Footer/ScreenFooter.cs +++ b/osu.Game/Screens/Footer/ScreenFooter.cs @@ -121,7 +121,7 @@ namespace osu.Game.Screens.Footer temporarilyHiddenButtons.Clear(); overlays.Clear(); - ClearOverlayContent(); + ClearActiveOverlayContainer(); var oldButtons = buttonsFlow.ToArray(); @@ -168,7 +168,7 @@ namespace osu.Game.Screens.Footer private Container? contentContainer; private readonly List temporarilyHiddenButtons = new List(); - public void SetOverlayContent(ShearedOverlayContainer overlay) + public void SetActiveOverlayContainer(ShearedOverlayContainer overlay) { if (contentContainer != null) { @@ -213,7 +213,7 @@ namespace osu.Game.Screens.Footer content.Show(); } - public void ClearOverlayContent() + public void ClearActiveOverlayContainer() { if (contentContainer == null) return; From 002679ebb0e75ee6d72e9e0e8bfb08a3be4314e9 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 10 Jul 2024 11:12:52 +0300 Subject: [PATCH 1421/2493] Ask for `VisibilityContainer` explicitly --- osu.Game.Tests/Visual/UserInterface/TestSceneScreenFooter.cs | 2 +- osu.Game/Overlays/FirstRunSetupOverlay.cs | 2 +- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 2 +- osu.Game/Overlays/Mods/ShearedOverlayContainer.cs | 2 +- osu.Game/Screens/Footer/ScreenFooter.cs | 2 +- osu.Game/Screens/OnlinePlay/FreeModSelectOverlay.cs | 3 ++- 6 files changed, 7 insertions(+), 6 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneScreenFooter.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneScreenFooter.cs index de2026e538..c8cf6a6ffb 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneScreenFooter.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneScreenFooter.cs @@ -210,7 +210,7 @@ namespace osu.Game.Tests.Visual.UserInterface return false; } - public override Drawable CreateFooterContent() => new TestFooterContent(); + public override VisibilityContainer CreateFooterContent() => new TestFooterContent(); public partial class TestFooterContent : VisibilityContainer { diff --git a/osu.Game/Overlays/FirstRunSetupOverlay.cs b/osu.Game/Overlays/FirstRunSetupOverlay.cs index 6412297663..2c8ceba82c 100644 --- a/osu.Game/Overlays/FirstRunSetupOverlay.cs +++ b/osu.Game/Overlays/FirstRunSetupOverlay.cs @@ -150,7 +150,7 @@ namespace osu.Game.Overlays private FirstRunSetupFooterContent? currentFooterContent; - public override Drawable CreateFooterContent() + public override VisibilityContainer CreateFooterContent() { currentFooterContent = new FirstRunSetupFooterContent { diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index bd04a1f6b3..da93539679 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -256,7 +256,7 @@ namespace osu.Game.Overlays.Mods private ModSelectFooterContent? currentFooterContent; - public override Drawable CreateFooterContent() => currentFooterContent = new ModSelectFooterContent(this) + public override VisibilityContainer CreateFooterContent() => currentFooterContent = new ModSelectFooterContent(this) { Beatmap = { BindTarget = Beatmap }, ActiveMods = { BindTarget = ActiveMods }, diff --git a/osu.Game/Overlays/Mods/ShearedOverlayContainer.cs b/osu.Game/Overlays/Mods/ShearedOverlayContainer.cs index 9e5a336c17..9ea98c1ae4 100644 --- a/osu.Game/Overlays/Mods/ShearedOverlayContainer.cs +++ b/osu.Game/Overlays/Mods/ShearedOverlayContainer.cs @@ -104,7 +104,7 @@ namespace osu.Game.Overlays.Mods /// /// Creates content to be displayed on the game-wide footer. /// - public virtual Drawable CreateFooterContent() => Empty(); + public virtual VisibilityContainer? CreateFooterContent() => null; /// /// Invoked when the back button in the footer is pressed. diff --git a/osu.Game/Screens/Footer/ScreenFooter.cs b/osu.Game/Screens/Footer/ScreenFooter.cs index a841f2a50b..4464b9d7da 100644 --- a/osu.Game/Screens/Footer/ScreenFooter.cs +++ b/osu.Game/Screens/Footer/ScreenFooter.cs @@ -218,7 +218,7 @@ namespace osu.Game.Screens.Footer updateColourScheme(overlay.ColourProvider.ColourScheme); - var content = overlay.CreateFooterContent(); + var content = overlay.CreateFooterContent() ?? Empty(); Add(contentContainer = new Container { diff --git a/osu.Game/Screens/OnlinePlay/FreeModSelectOverlay.cs b/osu.Game/Screens/OnlinePlay/FreeModSelectOverlay.cs index 0ed45161f2..2b3ab94916 100644 --- a/osu.Game/Screens/OnlinePlay/FreeModSelectOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/FreeModSelectOverlay.cs @@ -6,6 +6,7 @@ using osu.Game.Overlays; using System.Collections.Generic; using System.Linq; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osu.Game.Overlays.Mods; using osu.Game.Rulesets.Mods; @@ -34,7 +35,7 @@ namespace osu.Game.Screens.OnlinePlay protected override ModColumn CreateModColumn(ModType modType) => new ModColumn(modType, true); - public override Drawable CreateFooterContent() => currentFooterContent = new FreeModSelectFooterContent(this) + public override VisibilityContainer CreateFooterContent() => currentFooterContent = new FreeModSelectFooterContent(this) { Beatmap = { BindTarget = Beatmap }, ActiveMods = { BindTarget = ActiveMods }, From f0a7a3f85657caf09c639bd5cef154d339e5b9de Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 10 Jul 2024 11:51:49 +0300 Subject: [PATCH 1422/2493] Add failing test case for edge case --- .../UserInterface/TestSceneScreenFooter.cs | 24 ++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneScreenFooter.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneScreenFooter.cs index c8cf6a6ffb..a4cf8a276f 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneScreenFooter.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneScreenFooter.cs @@ -23,7 +23,7 @@ namespace osu.Game.Tests.Visual.UserInterface { private DependencyProvidingContainer contentContainer = null!; private ScreenFooter screenFooter = null!; - private TestModSelectOverlay overlay = null!; + private TestModSelectOverlay modOverlay = null!; [SetUp] public void SetUp() => Schedule(() => @@ -39,7 +39,7 @@ namespace osu.Game.Tests.Visual.UserInterface }, Children = new Drawable[] { - overlay = new TestModSelectOverlay(), + modOverlay = new TestModSelectOverlay(), new PopoverContainer { RelativeSizeAxes = Axes.Both, @@ -51,7 +51,7 @@ namespace osu.Game.Tests.Visual.UserInterface screenFooter.SetButtons(new ScreenFooterButton[] { - new ScreenFooterButtonMods(overlay) { Current = SelectedMods }, + new ScreenFooterButtonMods(modOverlay) { Current = SelectedMods }, new ScreenFooterButtonRandom(), new ScreenFooterButtonOptions(), }); @@ -178,6 +178,24 @@ namespace osu.Game.Tests.Visual.UserInterface AddAssert("footer hidden", () => screenFooter.State.Value == Visibility.Hidden); } + [Test] + public void TestLoadOverlayAfterFooterIsDisplayed() + { + TestShearedOverlayContainer externalOverlay = null!; + + AddStep("show mod overlay", () => modOverlay.Show()); + AddUntilStep("mod footer content shown", () => this.ChildrenOfType().SingleOrDefault()?.IsPresent, () => Is.True); + + AddStep("add external overlay", () => contentContainer.Add(externalOverlay = new TestShearedOverlayContainer())); + AddUntilStep("wait for load", () => externalOverlay.IsLoaded); + AddAssert("mod footer content still shown", () => this.ChildrenOfType().SingleOrDefault()?.IsPresent, () => Is.True); + AddAssert("external overlay content not shown", () => this.ChildrenOfType().SingleOrDefault()?.IsPresent, () => Is.Not.True); + + AddStep("hide mod overlay", () => modOverlay.Hide()); + AddUntilStep("mod footer content hidden", () => this.ChildrenOfType().SingleOrDefault()?.IsPresent, () => Is.Not.True); + AddAssert("external overlay content still not shown", () => this.ChildrenOfType().SingleOrDefault()?.IsPresent, () => Is.Not.True); + } + private partial class TestModSelectOverlay : UserModSelectOverlay { protected override bool ShowPresets => true; From 337f05f9a454019bed39d6a32f7da94ac4be43fb Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 10 Jul 2024 11:54:04 +0300 Subject: [PATCH 1423/2493] Fix loading (but not showing) a sheared overlay hiding displayed footer content Identified by tests. See https://github.com/ppy/osu/actions/runs/9869382635/job/27253010485 & https://github.com/ppy/osu/actions/runs/9869382635/job/27253009622. This change also prevents the initial `PopOut` call in overlays from calling `clearActiveOverlayContainer`, since it's not in the update thread and it's never meant to be called at that point anyway (it's supposed to be accompanied by a previous `PopIn` call adding the footer content). --- .../Overlays/Mods/ShearedOverlayContainer.cs | 7 +++-- osu.Game/Screens/Footer/ScreenFooter.cs | 26 ++++++++++--------- 2 files changed, 19 insertions(+), 14 deletions(-) diff --git a/osu.Game/Overlays/Mods/ShearedOverlayContainer.cs b/osu.Game/Overlays/Mods/ShearedOverlayContainer.cs index 9ea98c1ae4..8c6b9e805b 100644 --- a/osu.Game/Overlays/Mods/ShearedOverlayContainer.cs +++ b/osu.Game/Overlays/Mods/ShearedOverlayContainer.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.Extensions.Color4Extensions; using osu.Framework.Graphics; @@ -123,6 +124,7 @@ namespace osu.Game.Overlays.Mods return base.OnClick(e); } + private IDisposable? activeOverlayRegistration; private bool hideFooterOnPopOut; protected override void PopIn() @@ -135,7 +137,7 @@ namespace osu.Game.Overlays.Mods if (footer != null) { - footer.SetActiveOverlayContainer(this); + activeOverlayRegistration = footer.RegisterActiveOverlayContainer(this); if (footer.State.Value == Visibility.Hidden) { @@ -156,7 +158,8 @@ namespace osu.Game.Overlays.Mods if (footer != null) { - footer.ClearActiveOverlayContainer(); + activeOverlayRegistration?.Dispose(); + activeOverlayRegistration = null; if (hideFooterOnPopOut) { diff --git a/osu.Game/Screens/Footer/ScreenFooter.cs b/osu.Game/Screens/Footer/ScreenFooter.cs index 4464b9d7da..f8d222e510 100644 --- a/osu.Game/Screens/Footer/ScreenFooter.cs +++ b/osu.Game/Screens/Footer/ScreenFooter.cs @@ -142,7 +142,7 @@ namespace osu.Game.Screens.Footer temporarilyHiddenButtons.Clear(); overlays.Clear(); - ClearActiveOverlayContainer(); + clearActiveOverlayContainer(); var oldButtons = buttonsFlow.ToArray(); @@ -187,14 +187,15 @@ namespace osu.Game.Screens.Footer private ShearedOverlayContainer? activeOverlay; private Container? contentContainer; + private readonly List temporarilyHiddenButtons = new List(); - public void SetActiveOverlayContainer(ShearedOverlayContainer overlay) + public IDisposable RegisterActiveOverlayContainer(ShearedOverlayContainer overlay) { - if (contentContainer != null) + if (activeOverlay != null) { throw new InvalidOperationException(@"Cannot set overlay content while one is already present. " + - $@"The previous overlay whose content is {contentContainer.Child.GetType().Name} should be hidden first."); + $@"The previous overlay ({activeOverlay.GetType().Name}) should be hidden first."); } activeOverlay = overlay; @@ -232,29 +233,30 @@ namespace osu.Game.Screens.Footer this.Delay(60).Schedule(() => content.Show()); else content.Show(); + + return new InvokeOnDisposal(clearActiveOverlayContainer); } - public void ClearActiveOverlayContainer() + private void clearActiveOverlayContainer() { - if (contentContainer == null) + if (activeOverlay == null) return; + Debug.Assert(contentContainer != null); contentContainer.Child.Hide(); double timeUntilRun = contentContainer.Child.LatestTransformEndTime - Time.Current; - Container expireTarget = contentContainer; - contentContainer = null; - activeOverlay = null; - for (int i = 0; i < temporarilyHiddenButtons.Count; i++) makeButtonAppearFromBottom(temporarilyHiddenButtons[i], 0); temporarilyHiddenButtons.Clear(); - expireTarget.Delay(timeUntilRun).Expire(); - updateColourScheme(OverlayColourScheme.Aquamarine); + + contentContainer.Delay(timeUntilRun).Expire(); + contentContainer = null; + activeOverlay = null; } private void updateColourScheme(OverlayColourScheme colourScheme) From d3c66e240459f6d563476c5c02c223e12252819c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 1 Jul 2024 12:07:13 +0900 Subject: [PATCH 1424/2493] Add basic flow for mounting beatmaps for external editing --- osu.Game/Beatmaps/BeatmapManager.cs | 3 ++ osu.Game/Database/ExternalEditOperation.cs | 48 +++++++++++++++++++ osu.Game/Database/IModelImporter.cs | 6 +++ .../Database/RealmArchiveModelImporter.cs | 24 ++++++++++ osu.Game/Scoring/ScoreManager.cs | 3 +- osu.Game/Screens/Edit/Editor.cs | 43 +++++++++++++++++ osu.Game/Skinning/SkinManager.cs | 2 + 7 files changed, 128 insertions(+), 1 deletion(-) create mode 100644 osu.Game/Database/ExternalEditOperation.cs diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 0610f7f6fb..e90b3c703f 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -415,6 +415,9 @@ namespace osu.Game.Beatmaps public Task?> ImportAsUpdate(ProgressNotification notification, ImportTask importTask, BeatmapSetInfo original) => beatmapImporter.ImportAsUpdate(notification, importTask, original); + public Task> BeginExternalEditing(BeatmapSetInfo model) => + beatmapImporter.BeginExternalEditing(model); + public Task Export(BeatmapSetInfo beatmap) => beatmapExporter.ExportAsync(beatmap.ToLive(Realm)); public Task ExportLegacy(BeatmapSetInfo beatmap) => legacyBeatmapExporter.ExportAsync(beatmap.ToLive(Realm)); diff --git a/osu.Game/Database/ExternalEditOperation.cs b/osu.Game/Database/ExternalEditOperation.cs new file mode 100644 index 0000000000..ab74cba7d5 --- /dev/null +++ b/osu.Game/Database/ExternalEditOperation.cs @@ -0,0 +1,48 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.IO; +using System.Threading.Tasks; +using osu.Game.Overlays.Notifications; + +namespace osu.Game.Database +{ + public class ExternalEditOperation where TModel : class, IHasGuidPrimaryKey + { + public readonly string MountedPath; + + private readonly IModelImporter importer; + private readonly TModel original; + + private bool isMounted; + + public ExternalEditOperation(IModelImporter importer, TModel original, string path) + { + this.importer = importer; + this.original = original; + + MountedPath = path; + + isMounted = true; + } + + public async Task?> Finish() + { + if (!Directory.Exists(MountedPath) || !isMounted) + return null; + + Live? imported = await importer.ImportAsUpdate(new ProgressNotification(), new ImportTask(MountedPath), original) + .ConfigureAwait(false); + + try + { + Directory.Delete(MountedPath, true); + } + catch { } + + isMounted = false; + + return imported; + } + } +} diff --git a/osu.Game/Database/IModelImporter.cs b/osu.Game/Database/IModelImporter.cs index dcbbad0d35..c2e5517f2a 100644 --- a/osu.Game/Database/IModelImporter.cs +++ b/osu.Game/Database/IModelImporter.cs @@ -34,6 +34,12 @@ namespace osu.Game.Database /// The imported model. Task?> ImportAsUpdate(ProgressNotification notification, ImportTask task, TModel original); + /// + /// Mount all files for a to a temporary directory to allow for external editing. + /// + /// The to mount. + public Task> BeginExternalEditing(TModel model); + /// /// A user displayable name for the model type associated with this manager. /// diff --git a/osu.Game/Database/RealmArchiveModelImporter.cs b/osu.Game/Database/RealmArchiveModelImporter.cs index 0014e246dc..38df2ac1dc 100644 --- a/osu.Game/Database/RealmArchiveModelImporter.cs +++ b/osu.Game/Database/RealmArchiveModelImporter.cs @@ -179,6 +179,30 @@ namespace osu.Game.Database public virtual Task?> ImportAsUpdate(ProgressNotification notification, ImportTask task, TModel original) => throw new NotImplementedException(); + public async Task> BeginExternalEditing(TModel model) + { + string mountedPath = Path.Join(Path.GetTempPath(), model.Hash); + + if (Directory.Exists(mountedPath)) + Directory.Delete(mountedPath, true); + + Directory.CreateDirectory(mountedPath); + + foreach (var realmFile in model.Files) + { + string sourcePath = Files.Storage.GetFullPath(realmFile.File.GetStoragePath()); + string destinationPath = Path.Join(mountedPath, realmFile.Filename); + + Directory.CreateDirectory(Path.GetDirectoryName(destinationPath)!); + + using (var inStream = Files.Storage.GetStream(sourcePath)) + using (var outStream = File.Create(destinationPath)) + await inStream.CopyToAsync(outStream).ConfigureAwait(false); + } + + return new ExternalEditOperation(this, model, mountedPath); + } + /// /// Import one from the filesystem and delete the file on success. /// Note that this bypasses the UI flow and should only be used for special cases or testing. diff --git a/osu.Game/Scoring/ScoreManager.cs b/osu.Game/Scoring/ScoreManager.cs index df4735b5e6..e3601fe91e 100644 --- a/osu.Game/Scoring/ScoreManager.cs +++ b/osu.Game/Scoring/ScoreManager.cs @@ -15,10 +15,10 @@ using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.Database; using osu.Game.IO.Archives; +using osu.Game.Online.API; using osu.Game.Overlays.Notifications; using osu.Game.Rulesets; using osu.Game.Rulesets.Scoring; -using osu.Game.Online.API; using osu.Game.Scoring.Legacy; namespace osu.Game.Scoring @@ -214,6 +214,7 @@ namespace osu.Game.Scoring } public Task?> ImportAsUpdate(ProgressNotification notification, ImportTask task, ScoreInfo original) => scoreImporter.ImportAsUpdate(notification, task, original); + public Task> BeginExternalEditing(ScoreInfo model) => scoreImporter.BeginExternalEditing(model); public Live? Import(ScoreInfo item, ArchiveReader? archive = null, ImportParameters parameters = default, CancellationToken cancellationToken = default) => scoreImporter.ImportModel(item, archive, parameters, cancellationToken); diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 27d0392b1e..ff8cf3997e 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Threading.Tasks; using JetBrains.Annotations; @@ -13,6 +14,7 @@ using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Track; using osu.Framework.Bindables; +using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; @@ -22,6 +24,7 @@ using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Framework.Localisation; using osu.Framework.Logging; +using osu.Framework.Platform; using osu.Framework.Screens; using osu.Framework.Testing; using osu.Framework.Threading; @@ -820,6 +823,9 @@ namespace osu.Game.Screens.Edit resetTrack(); + fileMountOperation?.Dispose(); + fileMountOperation = null; + refetchBeatmap(); return base.OnExiting(e); @@ -1095,6 +1101,11 @@ namespace osu.Game.Screens.Edit lastSavedHash = changeHandler?.CurrentStateHash; } + private EditorMenuItem mountFilesItem; + + [CanBeNull] + private Task> fileMountOperation; + private IEnumerable createFileMenuItems() { yield return createDifficultyCreationMenu(); @@ -1112,12 +1123,44 @@ namespace osu.Game.Screens.Edit var export = createExportMenu(); saveRelatedMenuItems.AddRange(export.Items); yield return export; + + yield return mountFilesItem = new EditorMenuItem("Mount files", MenuItemType.Standard, mountFiles); } yield return new OsuMenuItemSpacer(); yield return new EditorMenuItem(CommonStrings.Exit, MenuItemType.Standard, this.Exit); } + [Resolved] + private GameHost gameHost { get; set; } + + private void mountFiles() + { + if (fileMountOperation == null) + { + Save(); + + fileMountOperation = beatmapManager.BeginExternalEditing(editorBeatmap.BeatmapInfo.BeatmapSet!); + mountFilesItem.Text.Value = "Dismount files"; + + fileMountOperation.ContinueWith(t => + { + var operation = t.GetResultSafely(); + + // Ensure the trailing separator is present in order to show the folder contents. + gameHost.OpenFileExternally(operation.MountedPath.TrimDirectorySeparator() + Path.DirectorySeparatorChar); + }); + } + else + { + fileMountOperation.GetResultSafely().Finish().ContinueWith(t => Schedule(() => + { + fileMountOperation = null; + SwitchToDifficulty(t.GetResultSafely().Value.Detach().Beatmaps.First()); + })); + } + } + private EditorMenuItem createExportMenu() { var exportItems = new List diff --git a/osu.Game/Skinning/SkinManager.cs b/osu.Game/Skinning/SkinManager.cs index 59c2a8bca0..4f816d88d2 100644 --- a/osu.Game/Skinning/SkinManager.cs +++ b/osu.Game/Skinning/SkinManager.cs @@ -312,6 +312,8 @@ namespace osu.Game.Skinning public Task> ImportAsUpdate(ProgressNotification notification, ImportTask task, SkinInfo original) => skinImporter.ImportAsUpdate(notification, task, original); + public Task> BeginExternalEditing(SkinInfo model) => skinImporter.BeginExternalEditing(model); + public Task> Import(ImportTask task, ImportParameters parameters = default, CancellationToken cancellationToken = default) => skinImporter.Import(task, parameters, cancellationToken); From 118162c6315a7d93023dac06d8d253e56b0073e8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 9 Jul 2024 20:57:29 +0900 Subject: [PATCH 1425/2493] Add sub screen to limit user interactions --- osu.Game/Screens/Edit/Editor.cs | 59 +++++------------- osu.Game/Screens/Edit/ExternalEditScreen.cs | 68 +++++++++++++++++++++ 2 files changed, 84 insertions(+), 43 deletions(-) create mode 100644 osu.Game/Screens/Edit/ExternalEditScreen.cs diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index ff8cf3997e..a675b41833 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -5,7 +5,6 @@ using System; using System.Collections.Generic; -using System.IO; using System.Linq; using System.Threading.Tasks; using JetBrains.Annotations; @@ -14,7 +13,6 @@ using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Track; using osu.Framework.Bindables; -using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; @@ -24,7 +22,6 @@ using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Framework.Localisation; using osu.Framework.Logging; -using osu.Framework.Platform; using osu.Framework.Screens; using osu.Framework.Testing; using osu.Framework.Threading; @@ -823,9 +820,6 @@ namespace osu.Game.Screens.Edit resetTrack(); - fileMountOperation?.Dispose(); - fileMountOperation = null; - refetchBeatmap(); return base.OnExiting(e); @@ -1101,11 +1095,6 @@ namespace osu.Game.Screens.Edit lastSavedHash = changeHandler?.CurrentStateHash; } - private EditorMenuItem mountFilesItem; - - [CanBeNull] - private Task> fileMountOperation; - private IEnumerable createFileMenuItems() { yield return createDifficultyCreationMenu(); @@ -1124,43 +1113,15 @@ namespace osu.Game.Screens.Edit saveRelatedMenuItems.AddRange(export.Items); yield return export; - yield return mountFilesItem = new EditorMenuItem("Mount files", MenuItemType.Standard, mountFiles); + var externalEdit = new EditorMenuItem("Edit externally", MenuItemType.Standard, editExternally); + saveRelatedMenuItems.Add(externalEdit); + yield return externalEdit; } yield return new OsuMenuItemSpacer(); yield return new EditorMenuItem(CommonStrings.Exit, MenuItemType.Standard, this.Exit); } - [Resolved] - private GameHost gameHost { get; set; } - - private void mountFiles() - { - if (fileMountOperation == null) - { - Save(); - - fileMountOperation = beatmapManager.BeginExternalEditing(editorBeatmap.BeatmapInfo.BeatmapSet!); - mountFilesItem.Text.Value = "Dismount files"; - - fileMountOperation.ContinueWith(t => - { - var operation = t.GetResultSafely(); - - // Ensure the trailing separator is present in order to show the folder contents. - gameHost.OpenFileExternally(operation.MountedPath.TrimDirectorySeparator() + Path.DirectorySeparatorChar); - }); - } - else - { - fileMountOperation.GetResultSafely().Finish().ContinueWith(t => Schedule(() => - { - fileMountOperation = null; - SwitchToDifficulty(t.GetResultSafely().Value.Detach().Beatmaps.First()); - })); - } - } - private EditorMenuItem createExportMenu() { var exportItems = new List @@ -1172,6 +1133,14 @@ namespace osu.Game.Screens.Edit return new EditorMenuItem(CommonStrings.Export) { Items = exportItems }; } + private void editExternally() + { + Save(); + + var editOperation = beatmapManager.BeginExternalEditing(editorBeatmap.BeatmapInfo.BeatmapSet!); + this.Push(new ExternalEditScreen(editOperation, this)); + } + private void exportBeatmap(bool legacy) { if (HasUnsavedChanges) @@ -1303,7 +1272,11 @@ namespace osu.Game.Screens.Edit return new EditorMenuItem(EditorStrings.ChangeDifficulty) { Items = difficultyItems }; } - protected void SwitchToDifficulty(BeatmapInfo nextBeatmap) => loader?.ScheduleSwitchToExistingDifficulty(nextBeatmap, GetState(nextBeatmap.Ruleset)); + public void SwitchToDifficulty(BeatmapInfo nextBeatmap) + { + switchingDifficulty = true; + loader?.ScheduleSwitchToExistingDifficulty(nextBeatmap, GetState(nextBeatmap.Ruleset)); + } private void cancelExit() { diff --git a/osu.Game/Screens/Edit/ExternalEditScreen.cs b/osu.Game/Screens/Edit/ExternalEditScreen.cs new file mode 100644 index 0000000000..79a10c6292 --- /dev/null +++ b/osu.Game/Screens/Edit/ExternalEditScreen.cs @@ -0,0 +1,68 @@ +#nullable enable +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using osu.Framework.Allocation; +using osu.Framework.Extensions; +using osu.Framework.Graphics; +using osu.Framework.Platform; +using osu.Game.Beatmaps; +using osu.Game.Database; +using osu.Game.Overlays.Settings; + +namespace osu.Game.Screens.Edit +{ + internal partial class ExternalEditScreen : OsuScreen + { + private readonly Task> fileMountOperation; + + [Resolved] + private GameHost gameHost { get; set; } = null!; + + private readonly Editor? editor; + + public ExternalEditScreen(Task> fileMountOperation, Editor editor) + { + this.fileMountOperation = fileMountOperation; + this.editor = editor; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + fileMountOperation.ContinueWith(t => + { + var operation = t.GetResultSafely>(); + + // Ensure the trailing separator is present in order to show the folder contents. + gameHost.OpenFileExternally(operation.MountedPath.TrimDirectorySeparator() + Path.DirectorySeparatorChar); + }); + + InternalChildren = new Drawable[] + { + new SettingsButton + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Text = "end editing", + Action = finish, + } + }; + } + + private void finish() + { + fileMountOperation.GetResultSafely().Finish().ContinueWith(t => + { + Schedule(() => + { + editor?.SwitchToDifficulty(t.GetResultSafely>().Value.Detach().Beatmaps.First()); + }); + }); + } + } +} From 74aa05fa6ed5eec8bad5e2d6b0ccef0788b93677 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 9 Jul 2024 21:20:29 +0900 Subject: [PATCH 1426/2493] Improve UX and styling of external edit screen --- osu.Game/Screens/Edit/ExternalEditScreen.cs | 108 +++++++++++++++++--- 1 file changed, 92 insertions(+), 16 deletions(-) diff --git a/osu.Game/Screens/Edit/ExternalEditScreen.cs b/osu.Game/Screens/Edit/ExternalEditScreen.cs index 79a10c6292..047a4d442e 100644 --- a/osu.Game/Screens/Edit/ExternalEditScreen.cs +++ b/osu.Game/Screens/Edit/ExternalEditScreen.cs @@ -1,4 +1,3 @@ -#nullable enable // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. @@ -8,10 +7,17 @@ using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Extensions; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; using osu.Framework.Platform; using osu.Game.Beatmaps; using osu.Game.Database; -using osu.Game.Overlays.Settings; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osu.Game.Overlays; +using osu.Game.Screens.OnlinePlay.Match.Components; +using osuTK; namespace osu.Game.Screens.Edit { @@ -22,36 +28,106 @@ namespace osu.Game.Screens.Edit [Resolved] private GameHost gameHost { get; set; } = null!; + [Resolved] + private OverlayColourProvider colourProvider { get; set; } = null!; + private readonly Editor? editor; + private ExternalEditOperation? operation; + public ExternalEditScreen(Task> fileMountOperation, Editor editor) { this.fileMountOperation = fileMountOperation; this.editor = editor; } + [BackgroundDependencyLoader] + private void load() + { + InternalChild = new Container + { + Masking = true, + CornerRadius = 20, + AutoSizeAxes = Axes.Both, + AutoSizeDuration = 500, + AutoSizeEasing = Easing.OutQuint, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Children = new Drawable[] + { + new Box + { + Colour = colourProvider.Background5, + RelativeSizeAxes = Axes.Both, + }, + new FillFlowContainer + { + Margin = new MarginPadding(20), + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Spacing = new Vector2(15), + Children = new Drawable[] + { + new OsuSpriteText + { + Text = "Beatmap is mounted externally", + Font = OsuFont.Default.With(size: 30), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }, + new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Spacing = new Vector2(15), + Children = new Drawable[] + { + } + }, + new PurpleRoundedButton + { + Text = "Open folder", + Width = 350, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Action = open, + }, + new DangerousRoundedButton + { + Text = "Finish editing and import changes", + Width = 350, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Action = finish, + } + } + } + } + }; + } + protected override void LoadComplete() { base.LoadComplete(); fileMountOperation.ContinueWith(t => { - var operation = t.GetResultSafely>(); - - // Ensure the trailing separator is present in order to show the folder contents. - gameHost.OpenFileExternally(operation.MountedPath.TrimDirectorySeparator() + Path.DirectorySeparatorChar); + operation = t.GetResultSafely(); + Schedule(open); }); + } - InternalChildren = new Drawable[] - { - new SettingsButton - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Text = "end editing", - Action = finish, - } - }; + private void open() + { + if (operation == null) + return; + + // Ensure the trailing separator is present in order to show the folder contents. + gameHost.OpenFileExternally(operation.MountedPath.TrimDirectorySeparator() + Path.DirectorySeparatorChar); } private void finish() From 27ab54882b16e83d3e487da0c19ebdd652d5875c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 9 Jul 2024 21:50:33 +0900 Subject: [PATCH 1427/2493] Add loading segments and tidy things up --- osu.Game/Screens/Edit/ExternalEditScreen.cs | 158 ++++++++++++++------ 1 file changed, 114 insertions(+), 44 deletions(-) diff --git a/osu.Game/Screens/Edit/ExternalEditScreen.cs b/osu.Game/Screens/Edit/ExternalEditScreen.cs index 047a4d442e..fd438eacb3 100644 --- a/osu.Game/Screens/Edit/ExternalEditScreen.cs +++ b/osu.Game/Screens/Edit/ExternalEditScreen.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.IO; using System.Linq; using System.Threading.Tasks; @@ -10,11 +11,15 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Platform; +using osu.Framework.Screens; +using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Database; using osu.Game.Graphics; +using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; +using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Overlays; using osu.Game.Screens.OnlinePlay.Match.Components; using osuTK; @@ -31,10 +36,14 @@ namespace osu.Game.Screens.Edit [Resolved] private OverlayColourProvider colourProvider { get; set; } = null!; - private readonly Editor? editor; + private readonly Editor editor; private ExternalEditOperation? operation; + private double timeLoaded; + + private FillFlowContainer flow = null!; + public ExternalEditScreen(Task> fileMountOperation, Editor editor) { this.fileMountOperation = fileMountOperation; @@ -60,64 +69,78 @@ namespace osu.Game.Screens.Edit Colour = colourProvider.Background5, RelativeSizeAxes = Axes.Both, }, - new FillFlowContainer + flow = new FillFlowContainer { Margin = new MarginPadding(20), AutoSizeAxes = Axes.Both, Direction = FillDirection.Vertical, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, Spacing = new Vector2(15), - Children = new Drawable[] - { - new OsuSpriteText - { - Text = "Beatmap is mounted externally", - Font = OsuFont.Default.With(size: 30), - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - }, - new FillFlowContainer - { - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Spacing = new Vector2(15), - Children = new Drawable[] - { - } - }, - new PurpleRoundedButton - { - Text = "Open folder", - Width = 350, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Action = open, - }, - new DangerousRoundedButton - { - Text = "Finish editing and import changes", - Width = 350, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Action = finish, - } - } } } }; + + showSpinner("Exporting for edit..."); } protected override void LoadComplete() { base.LoadComplete(); + timeLoaded = Time.Current; + fileMountOperation.ContinueWith(t => { operation = t.GetResultSafely(); - Schedule(open); + + Scheduler.AddDelayed(() => + { + flow.Children = new Drawable[] + { + new OsuSpriteText + { + Text = "Beatmap is mounted externally", + Font = OsuFont.Default.With(size: 30), + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + }, + new OsuTextFlowContainer + { + Padding = new MarginPadding(5), + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Width = 350, + AutoSizeAxes = Axes.Y, + Text = "Any changes made to the exported folder will be imported to the game, including file additions, modifications and deletions.", + }, + new PurpleRoundedButton + { + Text = "Open folder", + Width = 350, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Action = open, + Enabled = { Value = false } + }, + new DangerousRoundedButton + { + Text = "Finish editing and import changes", + Width = 350, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Action = finish, + Enabled = { Value = false } + } + }; + + Scheduler.AddDelayed(() => + { + foreach (var b in flow.ChildrenOfType()) + b.Enabled.Value = true; + open(); + }, 1000); + }, Math.Max(0, 1000 - (Time.Current - timeLoaded))); }); } @@ -130,15 +153,62 @@ namespace osu.Game.Screens.Edit gameHost.OpenFileExternally(operation.MountedPath.TrimDirectorySeparator() + Path.DirectorySeparatorChar); } + public override bool OnExiting(ScreenExitEvent e) + { + if (!fileMountOperation.IsCompleted) + return false; + + if (operation != null) + { + finish(); + return false; + } + + return base.OnExiting(e); + } + private void finish() { - fileMountOperation.GetResultSafely().Finish().ContinueWith(t => + showSpinner("Cleaning up..."); + + EditOperation!.Finish().ContinueWith(t => { Schedule(() => { - editor?.SwitchToDifficulty(t.GetResultSafely>().Value.Detach().Beatmaps.First()); + // Setting to null will allow exit to succeed. + operation = null; + + var beatmap = t.GetResultSafely(); + + if (beatmap == null) + this.Exit(); + else + editor.SwitchToDifficulty(beatmap.Value.Detach().Beatmaps.First()); }); }); } + + private void showSpinner(string text) + { + foreach (var b in flow.ChildrenOfType()) + b.Enabled.Value = false; + + flow.Children = new Drawable[] + { + new OsuSpriteText + { + Text = text, + Font = OsuFont.Default.With(size: 30), + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + }, + new LoadingSpinner + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + State = { Value = Visibility.Visible } + }, + }; + } } } From 3beca64cc514f4667340b004e6ee0553ca7cd92c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 9 Jul 2024 21:53:57 +0900 Subject: [PATCH 1428/2493] Attempt to stay on correct difficulty --- osu.Game/Screens/Edit/ExternalEditScreen.cs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Edit/ExternalEditScreen.cs b/osu.Game/Screens/Edit/ExternalEditScreen.cs index fd438eacb3..ae5fad3ec0 100644 --- a/osu.Game/Screens/Edit/ExternalEditScreen.cs +++ b/osu.Game/Screens/Edit/ExternalEditScreen.cs @@ -169,6 +169,8 @@ namespace osu.Game.Screens.Edit private void finish() { + string originalDifficulty = editor.Beatmap.Value.Beatmap.BeatmapInfo.DifficultyName; + showSpinner("Cleaning up..."); EditOperation!.Finish().ContinueWith(t => @@ -178,12 +180,18 @@ namespace osu.Game.Screens.Edit // Setting to null will allow exit to succeed. operation = null; - var beatmap = t.GetResultSafely(); + Live? beatmap = t.GetResultSafely(); if (beatmap == null) this.Exit(); else - editor.SwitchToDifficulty(beatmap.Value.Detach().Beatmaps.First()); + { + var closestMatchingBeatmap = + beatmap.Value.Beatmaps.FirstOrDefault(b => b.DifficultyName == originalDifficulty) + ?? beatmap.Value.Beatmaps.First(); + + editor.SwitchToDifficulty(closestMatchingBeatmap); + } }); }); } From 72091b43df03c996b9b5cbb7534e984426c7a29d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 10 Jul 2024 14:34:25 +0900 Subject: [PATCH 1429/2493] Simplify editor navigation tests --- .../TestSceneBeatmapEditorNavigation.cs | 143 +++++------------- 1 file changed, 40 insertions(+), 103 deletions(-) diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneBeatmapEditorNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneBeatmapEditorNavigation.cs index 1ac4bb347b..efdcde9161 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneBeatmapEditorNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneBeatmapEditorNavigation.cs @@ -29,30 +29,20 @@ namespace osu.Game.Tests.Visual.Navigation { public partial class TestSceneBeatmapEditorNavigation : OsuGameTestScene { + private BeatmapSetInfo beatmapSet = null!; + [Test] public void TestSaveThenDeleteActuallyDeletesAtSongSelect() { - BeatmapSetInfo beatmapSet = null!; - - AddStep("import test beatmap", () => Game.BeatmapManager.Import(TestResources.GetTestBeatmapForImport()).WaitSafely()); - AddStep("retrieve beatmap", () => beatmapSet = Game.BeatmapManager.QueryBeatmapSet(set => !set.Protected).AsNonNull().Value.Detach()); - - AddStep("present beatmap", () => Game.PresentBeatmap(beatmapSet)); - AddUntilStep("wait for song select", - () => Game.Beatmap.Value.BeatmapSetInfo.Equals(beatmapSet) - && Game.ScreenStack.CurrentScreen is PlaySongSelect songSelect - && songSelect.IsLoaded); - - AddStep("open editor", () => ((PlaySongSelect)Game.ScreenStack.CurrentScreen).Edit(beatmapSet.Beatmaps.First(beatmap => beatmap.Ruleset.OnlineID == 0))); - AddUntilStep("wait for editor open", () => Game.ScreenStack.CurrentScreen is Editor editor && editor.ReadyForUse); - + prepareBeatmap(); + openEditor(); makeMetadataChange(); - AddAssert("save", () => Game.ChildrenOfType().Single().Save()); + AddAssert("save", () => getEditor().Save()); AddStep("delete beatmap", () => Game.BeatmapManager.Delete(beatmapSet)); - AddStep("exit", () => Game.ChildrenOfType().Single().Exit()); + AddStep("exit", () => getEditor().Exit()); AddUntilStep("wait for song select", () => Game.ScreenStack.CurrentScreen is PlaySongSelect songSelect && songSelect.Beatmap.Value is DummyWorkingBeatmap); @@ -61,24 +51,14 @@ namespace osu.Game.Tests.Visual.Navigation [Test] public void TestChangeMetadataExitWhileTextboxFocusedPromptsSave() { - BeatmapSetInfo beatmapSet = null!; - - AddStep("import test beatmap", () => Game.BeatmapManager.Import(TestResources.GetTestBeatmapForImport()).WaitSafely()); - AddStep("retrieve beatmap", () => beatmapSet = Game.BeatmapManager.QueryBeatmapSet(set => !set.Protected).AsNonNull().Value.Detach()); - - AddStep("present beatmap", () => Game.PresentBeatmap(beatmapSet)); - AddUntilStep("wait for song select", - () => Game.Beatmap.Value.BeatmapSetInfo.Equals(beatmapSet) - && Game.ScreenStack.CurrentScreen is PlaySongSelect songSelect - && songSelect.IsLoaded); AddStep("switch ruleset", () => Game.Ruleset.Value = new ManiaRuleset().RulesetInfo); - AddStep("open editor", () => ((PlaySongSelect)Game.ScreenStack.CurrentScreen).Edit(beatmapSet.Beatmaps.First(beatmap => beatmap.Ruleset.OnlineID == 0))); - AddUntilStep("wait for editor open", () => Game.ScreenStack.CurrentScreen is Editor editor && editor.ReadyForUse); + prepareBeatmap(); + openEditor(); makeMetadataChange(commit: false); - AddStep("exit", () => Game.ChildrenOfType().Single().Exit()); + AddStep("exit", () => getEditor().Exit()); AddUntilStep("save dialog displayed", () => Game.ChildrenOfType().SingleOrDefault()?.CurrentDialog is PromptForSaveDialog); } @@ -121,16 +101,8 @@ namespace osu.Game.Tests.Visual.Navigation [Test] public void TestEditorGameplayTestAlwaysUsesOriginalRuleset() { - BeatmapSetInfo beatmapSet = null!; + prepareBeatmap(); - AddStep("import test beatmap", () => Game.BeatmapManager.Import(TestResources.GetTestBeatmapForImport()).WaitSafely()); - AddStep("retrieve beatmap", () => beatmapSet = Game.BeatmapManager.QueryBeatmapSet(set => !set.Protected).AsNonNull().Value.Detach()); - - AddStep("present beatmap", () => Game.PresentBeatmap(beatmapSet)); - AddUntilStep("wait for song select", - () => Game.Beatmap.Value.BeatmapSetInfo.Equals(beatmapSet) - && Game.ScreenStack.CurrentScreen is PlaySongSelect songSelect - && songSelect.IsLoaded); AddStep("switch ruleset", () => Game.Ruleset.Value = new ManiaRuleset().RulesetInfo); AddStep("open editor", () => ((PlaySongSelect)Game.ScreenStack.CurrentScreen).Edit(beatmapSet.Beatmaps.First(beatmap => beatmap.Ruleset.OnlineID == 0))); @@ -183,19 +155,8 @@ namespace osu.Game.Tests.Visual.Navigation [Test] public void TestExitEditorWithoutSelection() { - BeatmapSetInfo beatmapSet = null!; - - AddStep("import test beatmap", () => Game.BeatmapManager.Import(TestResources.GetTestBeatmapForImport()).WaitSafely()); - AddStep("retrieve beatmap", () => beatmapSet = Game.BeatmapManager.QueryBeatmapSet(set => !set.Protected).AsNonNull().Value.Detach()); - - AddStep("present beatmap", () => Game.PresentBeatmap(beatmapSet)); - AddUntilStep("wait for song select", - () => Game.Beatmap.Value.BeatmapSetInfo.Equals(beatmapSet) - && Game.ScreenStack.CurrentScreen is PlaySongSelect songSelect - && songSelect.IsLoaded); - - AddStep("open editor", () => ((PlaySongSelect)Game.ScreenStack.CurrentScreen).Edit(beatmapSet.Beatmaps.First(beatmap => beatmap.Ruleset.OnlineID == 0))); - AddUntilStep("wait for editor open", () => Game.ScreenStack.CurrentScreen is Editor editor && editor.ReadyForUse); + prepareBeatmap(); + openEditor(); AddStep("escape once", () => InputManager.Key(Key.Escape)); @@ -205,19 +166,8 @@ namespace osu.Game.Tests.Visual.Navigation [Test] public void TestExitEditorWithSelection() { - BeatmapSetInfo beatmapSet = null!; - - AddStep("import test beatmap", () => Game.BeatmapManager.Import(TestResources.GetTestBeatmapForImport()).WaitSafely()); - AddStep("retrieve beatmap", () => beatmapSet = Game.BeatmapManager.QueryBeatmapSet(set => !set.Protected).AsNonNull().Value.Detach()); - - AddStep("present beatmap", () => Game.PresentBeatmap(beatmapSet)); - AddUntilStep("wait for song select", - () => Game.Beatmap.Value.BeatmapSetInfo.Equals(beatmapSet) - && Game.ScreenStack.CurrentScreen is PlaySongSelect songSelect - && songSelect.IsLoaded); - - AddStep("open editor", () => ((PlaySongSelect)Game.ScreenStack.CurrentScreen).Edit(beatmapSet.Beatmaps.First(beatmap => beatmap.Ruleset.OnlineID == 0))); - AddUntilStep("wait for editor open", () => Game.ScreenStack.CurrentScreen is Editor editor && editor.ReadyForUse); + prepareBeatmap(); + openEditor(); AddStep("make selection", () => { @@ -239,19 +189,8 @@ namespace osu.Game.Tests.Visual.Navigation [Test] public void TestLastTimestampRememberedOnExit() { - BeatmapSetInfo beatmapSet = null!; - - AddStep("import test beatmap", () => Game.BeatmapManager.Import(TestResources.GetTestBeatmapForImport()).WaitSafely()); - AddStep("retrieve beatmap", () => beatmapSet = Game.BeatmapManager.QueryBeatmapSet(set => !set.Protected).AsNonNull().Value.Detach()); - - AddStep("present beatmap", () => Game.PresentBeatmap(beatmapSet)); - AddUntilStep("wait for song select", - () => Game.Beatmap.Value.BeatmapSetInfo.Equals(beatmapSet) - && Game.ScreenStack.CurrentScreen is PlaySongSelect songSelect - && songSelect.IsLoaded); - - AddStep("open editor", () => ((PlaySongSelect)Game.ScreenStack.CurrentScreen).Edit(beatmapSet.Beatmaps.First(beatmap => beatmap.Ruleset.OnlineID == 0))); - AddUntilStep("wait for editor open", () => Game.ScreenStack.CurrentScreen is Editor editor && editor.ReadyForUse); + prepareBeatmap(); + openEditor(); AddStep("seek to arbitrary time", () => getEditor().ChildrenOfType().First().Seek(1234)); AddUntilStep("time is correct", () => getEditor().ChildrenOfType().First().CurrentTime, () => Is.EqualTo(1234)); @@ -259,32 +198,21 @@ namespace osu.Game.Tests.Visual.Navigation AddStep("exit editor", () => InputManager.Key(Key.Escape)); AddUntilStep("wait for editor exit", () => Game.ScreenStack.CurrentScreen is not Editor); - AddStep("open editor", () => ((PlaySongSelect)Game.ScreenStack.CurrentScreen).Edit()); + openEditor(); - AddUntilStep("wait for editor open", () => Game.ScreenStack.CurrentScreen is Editor editor && editor.ReadyForUse); AddUntilStep("time is correct", () => getEditor().ChildrenOfType().First().CurrentTime, () => Is.EqualTo(1234)); } [Test] public void TestAttemptGlobalMusicOperationFromEditor() { - BeatmapSetInfo beatmapSet = null!; - - AddStep("import test beatmap", () => Game.BeatmapManager.Import(TestResources.GetTestBeatmapForImport()).WaitSafely()); - AddStep("retrieve beatmap", () => beatmapSet = Game.BeatmapManager.QueryBeatmapSet(set => !set.Protected).AsNonNull().Value.Detach()); - - AddStep("present beatmap", () => Game.PresentBeatmap(beatmapSet)); - AddUntilStep("wait for song select", - () => Game.Beatmap.Value.BeatmapSetInfo.Equals(beatmapSet) - && Game.ScreenStack.CurrentScreen is PlaySongSelect songSelect - && songSelect.IsLoaded); + prepareBeatmap(); AddUntilStep("wait for music playing", () => Game.MusicController.IsPlaying); AddStep("user request stop", () => Game.MusicController.Stop(requestedByUser: true)); AddUntilStep("wait for music stopped", () => !Game.MusicController.IsPlaying); - AddStep("open editor", () => ((PlaySongSelect)Game.ScreenStack.CurrentScreen).Edit(beatmapSet.Beatmaps.First(beatmap => beatmap.Ruleset.OnlineID == 0))); - AddUntilStep("wait for editor open", () => Game.ScreenStack.CurrentScreen is Editor editor && editor.ReadyForUse); + openEditor(); AddUntilStep("music still stopped", () => !Game.MusicController.IsPlaying); AddStep("user request play", () => Game.MusicController.Play(requestedByUser: true)); @@ -302,20 +230,10 @@ namespace osu.Game.Tests.Visual.Navigation [TestCase(SortMode.Difficulty)] public void TestSelectionRetainedOnExit(SortMode sortMode) { - BeatmapSetInfo beatmapSet = null!; - - AddStep("import test beatmap", () => Game.BeatmapManager.Import(TestResources.GetTestBeatmapForImport()).WaitSafely()); - AddStep("retrieve beatmap", () => beatmapSet = Game.BeatmapManager.QueryBeatmapSet(set => !set.Protected).AsNonNull().Value.Detach()); - AddStep($"set sort mode to {sortMode}", () => Game.LocalConfig.SetValue(OsuSetting.SongSelectSortingMode, sortMode)); - AddStep("present beatmap", () => Game.PresentBeatmap(beatmapSet)); - AddUntilStep("wait for song select", - () => Game.Beatmap.Value.BeatmapSetInfo.Equals(beatmapSet) - && Game.ScreenStack.CurrentScreen is PlaySongSelect songSelect - && songSelect.IsLoaded); - AddStep("open editor", () => ((PlaySongSelect)Game.ScreenStack.CurrentScreen).Edit(beatmapSet.Beatmaps.First(beatmap => beatmap.Ruleset.OnlineID == 0))); - AddUntilStep("wait for editor open", () => Game.ScreenStack.CurrentScreen is Editor editor && editor.ReadyForUse); + prepareBeatmap(); + openEditor(); AddStep("exit editor", () => InputManager.Key(Key.Escape)); AddUntilStep("wait for editor exit", () => Game.ScreenStack.CurrentScreen is not Editor); @@ -332,6 +250,7 @@ namespace osu.Game.Tests.Visual.Navigation AddStep("open editor", () => Game.ChildrenOfType().Single().OnEditBeatmap?.Invoke()); AddUntilStep("wait for editor", () => Game.ScreenStack.CurrentScreen is Editor editor && editor.IsLoaded); + AddStep("click on file", () => { var item = getEditor().ChildrenOfType().Single(i => i.Item.Text.Value.ToString() == "File"); @@ -354,6 +273,24 @@ namespace osu.Game.Tests.Visual.Navigation AddAssert("editor beatmap uses catch ruleset", () => getEditorBeatmap().BeatmapInfo.Ruleset.ShortName == "fruits"); } + private void prepareBeatmap() + { + AddStep("import test beatmap", () => Game.BeatmapManager.Import(TestResources.GetTestBeatmapForImport()).WaitSafely()); + AddStep("retrieve beatmap", () => beatmapSet = Game.BeatmapManager.QueryBeatmapSet(set => !set.Protected).AsNonNull().Value.Detach()); + + AddStep("present beatmap", () => Game.PresentBeatmap(beatmapSet)); + AddUntilStep("wait for song select", + () => Game.Beatmap.Value.BeatmapSetInfo.Equals(beatmapSet) + && Game.ScreenStack.CurrentScreen is PlaySongSelect songSelect + && songSelect.IsLoaded); + } + + private void openEditor() + { + AddStep("open editor", () => ((PlaySongSelect)Game.ScreenStack.CurrentScreen).Edit(beatmapSet.Beatmaps.First(beatmap => beatmap.Ruleset.OnlineID == 0))); + AddUntilStep("wait for editor open", () => Game.ScreenStack.CurrentScreen is Editor editor && editor.ReadyForUse); + } + private EditorBeatmap getEditorBeatmap() => getEditor().ChildrenOfType().Single(); private Editor getEditor() => (Editor)Game.ScreenStack.CurrentScreen; From aa16c72e0661b2a83337461c5b9255ff1ec0bb75 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 10 Jul 2024 14:51:34 +0900 Subject: [PATCH 1430/2493] Add test coverage of external editing --- .../TestSceneBeatmapEditorNavigation.cs | 50 +++++++++++++++++++ osu.Game/Screens/Edit/ExternalEditScreen.cs | 12 ++--- 2 files changed, 56 insertions(+), 6 deletions(-) diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneBeatmapEditorNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneBeatmapEditorNavigation.cs index efdcde9161..5d9c3bae97 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneBeatmapEditorNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneBeatmapEditorNavigation.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.IO; using System.Linq; using NUnit.Framework; using osu.Framework.Extensions; @@ -13,6 +14,7 @@ using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.Database; +using osu.Game.Graphics.UserInterface; using osu.Game.Overlays; using osu.Game.Rulesets.Mania; using osu.Game.Rulesets.Osu; @@ -31,6 +33,54 @@ namespace osu.Game.Tests.Visual.Navigation { private BeatmapSetInfo beatmapSet = null!; + [Test] + public void TestExternalEditingNoChange() + { + prepareBeatmap(); + openEditor(); + + AddStep("open file menu", () => getEditor().ChildrenOfType().Single(m => m.Item.Text.Value.ToString() == "File").TriggerClick()); + AddStep("click external edit", () => getEditor().ChildrenOfType().Single(m => m.Item.Text.Value.ToString() == "Edit externally").TriggerClick()); + + AddUntilStep("wait for external edit screen", () => Game.ScreenStack.CurrentScreen is ExternalEditScreen externalEditScreen && externalEditScreen.IsLoaded); + + AddUntilStep("wait for button ready", () => ((ExternalEditScreen)Game.ScreenStack.CurrentScreen).ChildrenOfType().FirstOrDefault()?.Enabled.Value == true); + + AddStep("finish external edit", () => ((ExternalEditScreen)Game.ScreenStack.CurrentScreen).ChildrenOfType().First().TriggerClick()); + + AddUntilStep("wait for editor", () => Game.ScreenStack.CurrentScreen is Editor editor && editor.ReadyForUse); + + AddAssert("beatmap didn't change", () => getEditor().Beatmap.Value.BeatmapSetInfo.Equals(beatmapSet)); + AddAssert("old beatmapset not deleted", () => Game.BeatmapManager.QueryBeatmapSet(s => s.ID == beatmapSet.ID), () => Is.Not.Null); + } + + [Test] + public void TestExternalEditingWithChange() + { + prepareBeatmap(); + openEditor(); + + AddStep("open file menu", () => getEditor().ChildrenOfType().Single(m => m.Item.Text.Value.ToString() == "File").TriggerClick()); + AddStep("click external edit", () => getEditor().ChildrenOfType().Single(m => m.Item.Text.Value.ToString() == "Edit externally").TriggerClick()); + + AddUntilStep("wait for external edit screen", () => Game.ScreenStack.CurrentScreen is ExternalEditScreen externalEditScreen && externalEditScreen.IsLoaded); + + AddUntilStep("wait for button ready", () => ((ExternalEditScreen)Game.ScreenStack.CurrentScreen).ChildrenOfType().FirstOrDefault()?.Enabled.Value == true); + + AddStep("add file externally", () => + { + var op = ((ExternalEditScreen)Game.ScreenStack.CurrentScreen).EditOperation!; + File.WriteAllText(Path.Combine(op.MountedPath, "test.txt"), "test"); + }); + + AddStep("finish external edit", () => ((ExternalEditScreen)Game.ScreenStack.CurrentScreen).ChildrenOfType().First().TriggerClick()); + + AddUntilStep("wait for editor", () => Game.ScreenStack.CurrentScreen is Editor editor && editor.ReadyForUse); + + AddAssert("beatmap changed", () => !getEditor().Beatmap.Value.BeatmapSetInfo.Equals(beatmapSet)); + AddAssert("old beatmapset deleted", () => Game.BeatmapManager.QueryBeatmapSet(s => s.ID == beatmapSet.ID), () => Is.Null); + } + [Test] public void TestSaveThenDeleteActuallyDeletesAtSongSelect() { diff --git a/osu.Game/Screens/Edit/ExternalEditScreen.cs b/osu.Game/Screens/Edit/ExternalEditScreen.cs index ae5fad3ec0..9cae44be78 100644 --- a/osu.Game/Screens/Edit/ExternalEditScreen.cs +++ b/osu.Game/Screens/Edit/ExternalEditScreen.cs @@ -38,7 +38,7 @@ namespace osu.Game.Screens.Edit private readonly Editor editor; - private ExternalEditOperation? operation; + public ExternalEditOperation? EditOperation; private double timeLoaded; @@ -92,7 +92,7 @@ namespace osu.Game.Screens.Edit fileMountOperation.ContinueWith(t => { - operation = t.GetResultSafely(); + EditOperation = t.GetResultSafely(); Scheduler.AddDelayed(() => { @@ -146,11 +146,11 @@ namespace osu.Game.Screens.Edit private void open() { - if (operation == null) + if (EditOperation == null) return; // Ensure the trailing separator is present in order to show the folder contents. - gameHost.OpenFileExternally(operation.MountedPath.TrimDirectorySeparator() + Path.DirectorySeparatorChar); + gameHost.OpenFileExternally(EditOperation.MountedPath.TrimDirectorySeparator() + Path.DirectorySeparatorChar); } public override bool OnExiting(ScreenExitEvent e) @@ -158,7 +158,7 @@ namespace osu.Game.Screens.Edit if (!fileMountOperation.IsCompleted) return false; - if (operation != null) + if (EditOperation != null) { finish(); return false; @@ -178,7 +178,7 @@ namespace osu.Game.Screens.Edit Schedule(() => { // Setting to null will allow exit to succeed. - operation = null; + EditOperation = null; Live? beatmap = t.GetResultSafely(); From 106d558147124f1e17a8d7b04226a54e2fbddc6a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 10 Jul 2024 18:14:54 +0900 Subject: [PATCH 1431/2493] Add test coverage of difficulty being retained --- .../Navigation/TestSceneBeatmapEditorNavigation.cs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneBeatmapEditorNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneBeatmapEditorNavigation.cs index 5d9c3bae97..1f227520c1 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneBeatmapEditorNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneBeatmapEditorNavigation.cs @@ -36,9 +36,13 @@ namespace osu.Game.Tests.Visual.Navigation [Test] public void TestExternalEditingNoChange() { + string difficultyName = null!; + prepareBeatmap(); openEditor(); + AddStep("store difficulty name", () => difficultyName = getEditor().Beatmap.Value.BeatmapInfo.DifficultyName); + AddStep("open file menu", () => getEditor().ChildrenOfType().Single(m => m.Item.Text.Value.ToString() == "File").TriggerClick()); AddStep("click external edit", () => getEditor().ChildrenOfType().Single(m => m.Item.Text.Value.ToString() == "Edit externally").TriggerClick()); @@ -50,16 +54,21 @@ namespace osu.Game.Tests.Visual.Navigation AddUntilStep("wait for editor", () => Game.ScreenStack.CurrentScreen is Editor editor && editor.ReadyForUse); - AddAssert("beatmap didn't change", () => getEditor().Beatmap.Value.BeatmapSetInfo.Equals(beatmapSet)); + AddAssert("beatmapset didn't change", () => getEditor().Beatmap.Value.BeatmapSetInfo, () => Is.EqualTo(beatmapSet)); + AddAssert("difficulty didn't change", () => getEditor().Beatmap.Value.BeatmapInfo.DifficultyName, () => Is.EqualTo(difficultyName)); AddAssert("old beatmapset not deleted", () => Game.BeatmapManager.QueryBeatmapSet(s => s.ID == beatmapSet.ID), () => Is.Not.Null); } [Test] public void TestExternalEditingWithChange() { + string difficultyName = null!; + prepareBeatmap(); openEditor(); + AddStep("store difficulty name", () => difficultyName = getEditor().Beatmap.Value.BeatmapInfo.DifficultyName); + AddStep("open file menu", () => getEditor().ChildrenOfType().Single(m => m.Item.Text.Value.ToString() == "File").TriggerClick()); AddStep("click external edit", () => getEditor().ChildrenOfType().Single(m => m.Item.Text.Value.ToString() == "Edit externally").TriggerClick()); @@ -77,7 +86,8 @@ namespace osu.Game.Tests.Visual.Navigation AddUntilStep("wait for editor", () => Game.ScreenStack.CurrentScreen is Editor editor && editor.ReadyForUse); - AddAssert("beatmap changed", () => !getEditor().Beatmap.Value.BeatmapSetInfo.Equals(beatmapSet)); + AddAssert("beatmapset changed", () => getEditor().Beatmap.Value.BeatmapSetInfo, () => Is.Not.EqualTo(beatmapSet)); + AddAssert("difficulty didn't change", () => getEditor().Beatmap.Value.BeatmapInfo.DifficultyName, () => Is.EqualTo(difficultyName)); AddAssert("old beatmapset deleted", () => Game.BeatmapManager.QueryBeatmapSet(s => s.ID == beatmapSet.ID), () => Is.Null); } From 704e7e843fc6769b391a3b455b723a9c58d335be Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 10 Jul 2024 18:28:11 +0900 Subject: [PATCH 1432/2493] More xmldoc across new methods and classes --- osu.Game/Database/ExternalEditOperation.cs | 29 +++++++++++++++++----- osu.Game/Database/IModelImporter.cs | 3 +++ 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/osu.Game/Database/ExternalEditOperation.cs b/osu.Game/Database/ExternalEditOperation.cs index ab74cba7d5..a98d597b3c 100644 --- a/osu.Game/Database/ExternalEditOperation.cs +++ b/osu.Game/Database/ExternalEditOperation.cs @@ -7,15 +7,24 @@ using osu.Game.Overlays.Notifications; namespace osu.Game.Database { + /// + /// Contains information related to an active external edit operation. + /// public class ExternalEditOperation where TModel : class, IHasGuidPrimaryKey { + /// + /// The temporary path at which the model has been exported to for editing. + /// public readonly string MountedPath; + /// + /// Whether the model is still mounted at . + /// + public bool IsMounted { get; private set; } + private readonly IModelImporter importer; private readonly TModel original; - private bool isMounted; - public ExternalEditOperation(IModelImporter importer, TModel original, string path) { this.importer = importer; @@ -23,14 +32,24 @@ namespace osu.Game.Database MountedPath = path; - isMounted = true; + IsMounted = true; } + /// + /// Finish the external edit operation. + /// + /// + /// This will trigger an asynchronous reimport of the model. + /// Subsequent calls will be a no-op. + /// + /// A task which will eventuate in the newly imported model with changes applied. public async Task?> Finish() { - if (!Directory.Exists(MountedPath) || !isMounted) + if (!Directory.Exists(MountedPath) || !IsMounted) return null; + IsMounted = false; + Live? imported = await importer.ImportAsUpdate(new ProgressNotification(), new ImportTask(MountedPath), original) .ConfigureAwait(false); @@ -40,8 +59,6 @@ namespace osu.Game.Database } catch { } - isMounted = false; - return imported; } } diff --git a/osu.Game/Database/IModelImporter.cs b/osu.Game/Database/IModelImporter.cs index c2e5517f2a..bf19bac5dd 100644 --- a/osu.Game/Database/IModelImporter.cs +++ b/osu.Game/Database/IModelImporter.cs @@ -37,6 +37,9 @@ namespace osu.Game.Database /// /// Mount all files for a to a temporary directory to allow for external editing. /// + /// + /// When editing is completed, call to begin the import-and-update process. + /// /// The to mount. public Task> BeginExternalEditing(TModel model); From 343090e3b140780fd708bf69107e70bd328c0172 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 10 Jul 2024 10:58:29 +0200 Subject: [PATCH 1433/2493] Do not regenerate breaks unless meaningful change to object start/end times is detected Tangentially found when profiling https://github.com/ppy/osu/pull/28792. For reproduction, import https://osu.ppy.sh/beatmapsets/972#osu/9007, move any object on the playfield, and observe a half-second freeze when ending the drag. --- osu.Game/Screens/Edit/EditorBeatmapProcessor.cs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/osu.Game/Screens/Edit/EditorBeatmapProcessor.cs b/osu.Game/Screens/Edit/EditorBeatmapProcessor.cs index 377e978c4a..9b6d956a4c 100644 --- a/osu.Game/Screens/Edit/EditorBeatmapProcessor.cs +++ b/osu.Game/Screens/Edit/EditorBeatmapProcessor.cs @@ -2,6 +2,7 @@ // 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.Beatmaps.Timing; @@ -18,6 +19,11 @@ namespace osu.Game.Screens.Edit private readonly IBeatmapProcessor? rulesetBeatmapProcessor; + /// + /// Kept for the purposes of reducing redundant regeneration of automatic breaks. + /// + private HashSet<(double, double)> objectDurationCache = new HashSet<(double, double)>(); + public EditorBeatmapProcessor(EditorBeatmap beatmap, Ruleset ruleset) { Beatmap = beatmap; @@ -38,6 +44,13 @@ namespace osu.Game.Screens.Edit private void autoGenerateBreaks() { + var objectDuration = Beatmap.HitObjects.Select(ho => (ho.StartTime, ho.GetEndTime())).ToHashSet(); + + if (objectDuration.SetEquals(objectDurationCache)) + return; + + objectDurationCache = objectDuration; + Beatmap.Breaks.RemoveAll(b => b is not ManualBreakPeriod); foreach (var manualBreak in Beatmap.Breaks.ToList()) From b881c25b17f7ae8615737432a63915c6dbbba1ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 10 Jul 2024 11:34:05 +0200 Subject: [PATCH 1434/2493] Pool summary timeline break visualisations to reduce allocations --- .../Timelines/Summary/Parts/BreakPart.cs | 43 ++++++++++++++----- 1 file changed, 33 insertions(+), 10 deletions(-) diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/BreakPart.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/BreakPart.cs index ed42ade490..100f37fd27 100644 --- a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/BreakPart.cs +++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/BreakPart.cs @@ -4,6 +4,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Framework.Graphics.Pooling; using osu.Framework.Graphics.Shapes; using osu.Game.Beatmaps.Timing; using osu.Game.Graphics; @@ -17,32 +18,54 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts { private readonly BindableList breaks = new BindableList(); + private DrawablePool pool = null!; + + [BackgroundDependencyLoader] + private void load() + { + AddInternal(pool = new DrawablePool(10)); + } + protected override void LoadBeatmap(EditorBeatmap beatmap) { base.LoadBeatmap(beatmap); breaks.UnbindAll(); breaks.BindTo(beatmap.Breaks); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + breaks.BindCollectionChanged((_, _) => { - Clear(); - foreach (var breakPeriod in beatmap.Breaks) - Add(new BreakVisualisation(breakPeriod)); + Clear(disposeChildren: false); + foreach (var breakPeriod in breaks) + Add(pool.Get(v => v.BreakPeriod = breakPeriod)); }, true); } - private partial class BreakVisualisation : Circle + private partial class BreakVisualisation : PoolableDrawable { - public BreakVisualisation(BreakPeriod breakPeriod) + public BreakPeriod BreakPeriod { - RelativePositionAxes = Axes.X; - RelativeSizeAxes = Axes.Both; - X = (float)breakPeriod.StartTime; - Width = (float)breakPeriod.Duration; + set + { + X = (float)value.StartTime; + Width = (float)value.Duration; + } } [BackgroundDependencyLoader] - private void load(OsuColour colours) => Colour = colours.Gray7; + private void load(OsuColour colours) + { + RelativePositionAxes = Axes.X; + RelativeSizeAxes = Axes.Both; + + InternalChild = new Circle { RelativeSizeAxes = Axes.Both }; + Colour = colours.Gray7; + } } } } From 2ba1ebe410d088d5d05944f18a9ac6c2f4fa3ab3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 10 Jul 2024 19:19:49 +0900 Subject: [PATCH 1435/2493] Fix beatmap card progress bar becoming pancake when starting --- .../Cards/BeatmapCardDownloadProgressBar.cs | 23 +++++++------------ 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardDownloadProgressBar.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardDownloadProgressBar.cs index 5ea42fe4b1..d21e8e7c76 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardDownloadProgressBar.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardDownloadProgressBar.cs @@ -22,8 +22,6 @@ namespace osu.Game.Beatmaps.Drawables.Cards public override bool IsPresent => true; - private readonly CircularContainer foreground; - private readonly Box backgroundFill; private readonly Box foregroundFill; @@ -35,22 +33,17 @@ namespace osu.Game.Beatmaps.Drawables.Cards public BeatmapCardDownloadProgressBar() { - InternalChildren = new Drawable[] + InternalChild = new CircularContainer { - new CircularContainer + RelativeSizeAxes = Axes.Both, + Masking = true, + Children = new Drawable[] { - RelativeSizeAxes = Axes.Both, - Masking = true, - Child = backgroundFill = new Box + backgroundFill = new Box { RelativeSizeAxes = Axes.Both, - } - }, - foreground = new CircularContainer - { - RelativeSizeAxes = Axes.Both, - Masking = true, - Child = foregroundFill = new Box + }, + foregroundFill = new Box { RelativeSizeAxes = Axes.Both, } @@ -89,7 +82,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards private void progressChanged() { - foreground.ResizeWidthTo((float)progress.Value, progress.Value > 0 ? BeatmapCard.TRANSITION_DURATION : 0, Easing.OutQuint); + foregroundFill.ResizeWidthTo((float)progress.Value, progress.Value > 0 ? BeatmapCard.TRANSITION_DURATION : 0, Easing.OutQuint); } } } From 6cee0210c380e7903873ea4fa4fa29b28d417df2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 10 Jul 2024 18:57:53 +0900 Subject: [PATCH 1436/2493] Fix(?) xmldoc --- osu.Game/Database/IModelImporter.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Database/IModelImporter.cs b/osu.Game/Database/IModelImporter.cs index bf19bac5dd..ce1563f2df 100644 --- a/osu.Game/Database/IModelImporter.cs +++ b/osu.Game/Database/IModelImporter.cs @@ -35,12 +35,12 @@ namespace osu.Game.Database Task?> ImportAsUpdate(ProgressNotification notification, ImportTask task, TModel original); /// - /// Mount all files for a to a temporary directory to allow for external editing. + /// Mount all files for a model to a temporary directory to allow for external editing. /// /// - /// When editing is completed, call to begin the import-and-update process. + /// When editing is completed, call Finish() on the returned operation class to begin the import-and-update process. /// - /// The to mount. + /// The model to mount. public Task> BeginExternalEditing(TModel model); /// From 75344f9c5c7ad67aa82df35aa6f3d8b0a6041b74 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 10 Jul 2024 19:28:25 +0900 Subject: [PATCH 1437/2493] Fix break overlay progress bar becoming a pancake near end of break --- osu.Game/Screens/Play/BreakOverlay.cs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Play/BreakOverlay.cs b/osu.Game/Screens/Play/BreakOverlay.cs index e18612c955..ece3105b42 100644 --- a/osu.Game/Screens/Play/BreakOverlay.cs +++ b/osu.Game/Screens/Play/BreakOverlay.cs @@ -3,6 +3,7 @@ #nullable disable +using System; using System.Collections.Generic; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -75,15 +76,13 @@ namespace osu.Game.Screens.Play AutoSizeAxes = Axes.Y, RelativeSizeAxes = Axes.X, Width = 0, - Child = remainingTimeBox = new Container + Child = remainingTimeBox = new Circle { Anchor = Anchor.Centre, Origin = Anchor.Centre, RelativeSizeAxes = Axes.X, Height = 8, - CornerRadius = 4, Masking = true, - Child = new Box { RelativeSizeAxes = Axes.Both } } }, remainingTimeCounter = new RemainingTimeCounter @@ -119,6 +118,13 @@ namespace osu.Game.Screens.Play } } + protected override void Update() + { + base.Update(); + + remainingTimeBox.Height = Math.Min(8, remainingTimeBox.DrawWidth); + } + private void initializeBreaks() { FinishTransforms(true); From b6741ee4eab7c8bceed3fa1e94e11e60e6a7383b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 10 Jul 2024 20:00:34 +0900 Subject: [PATCH 1438/2493] Fix back-to-front exit blocking conditionals --- osu.Game/Screens/Edit/ExternalEditScreen.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Edit/ExternalEditScreen.cs b/osu.Game/Screens/Edit/ExternalEditScreen.cs index 9cae44be78..a8a75f22db 100644 --- a/osu.Game/Screens/Edit/ExternalEditScreen.cs +++ b/osu.Game/Screens/Edit/ExternalEditScreen.cs @@ -156,12 +156,12 @@ namespace osu.Game.Screens.Edit public override bool OnExiting(ScreenExitEvent e) { if (!fileMountOperation.IsCompleted) - return false; + return true; if (EditOperation != null) { finish(); - return false; + return true; } return base.OnExiting(e); From 94f51c92e0d4643c3821c7d9df6804d700b1fcc9 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 10 Jul 2024 15:08:30 +0300 Subject: [PATCH 1439/2493] Select all text when focusing a number box --- .../Visual/UserInterface/TestSceneOsuTextBox.cs | 17 +++++++++++++++++ osu.Game/Graphics/UserInterface/OsuNumberBox.cs | 5 +++++ osu.Game/Graphics/UserInterface/OsuTextBox.cs | 8 ++++++++ .../Graphics/UserInterfaceV2/LabelledTextBox.cs | 6 ++++++ 4 files changed, 36 insertions(+) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneOsuTextBox.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneOsuTextBox.cs index 69fe8ad105..921c5bbbfa 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneOsuTextBox.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneOsuTextBox.cs @@ -11,6 +11,7 @@ using osu.Framework.Testing; using osu.Game.Graphics.UserInterface; using osu.Game.Overlays; using osuTK; +using osuTK.Input; namespace osu.Game.Tests.Visual.UserInterface { @@ -61,6 +62,22 @@ namespace osu.Game.Tests.Visual.UserInterface clearTextboxes(numberBoxes); } + [Test] + public void TestSelectAllOnFocus() + { + AddStep("create themed content", () => CreateThemedContent(OverlayColourScheme.Red)); + + AddStep("enter numbers", () => numberBoxes.ForEach(numberBox => numberBox.Text = "987654321")); + + AddAssert("nothing selected", () => string.IsNullOrEmpty(numberBoxes.First().SelectedText)); + AddStep("click on a number box", () => + { + InputManager.MoveMouseTo(numberBoxes.First()); + InputManager.Click(MouseButton.Left); + }); + AddAssert("text selected", () => numberBoxes.First().SelectedText == "987654321"); + } + private void clearTextboxes(IEnumerable textBoxes) => AddStep("clear textbox", () => textBoxes.ForEach(textBox => textBox.Text = null)); private void expectedValue(IEnumerable textBoxes, string value) => AddAssert("expected textbox value", () => textBoxes.All(textBox => textBox.Text == value)); } diff --git a/osu.Game/Graphics/UserInterface/OsuNumberBox.cs b/osu.Game/Graphics/UserInterface/OsuNumberBox.cs index e9b28f4771..db4b7b2ab3 100644 --- a/osu.Game/Graphics/UserInterface/OsuNumberBox.cs +++ b/osu.Game/Graphics/UserInterface/OsuNumberBox.cs @@ -7,6 +7,11 @@ namespace osu.Game.Graphics.UserInterface { protected override bool AllowIme => false; + public OsuNumberBox() + { + SelectAllOnFocus = true; + } + protected override bool CanAddCharacter(char character) => char.IsAsciiDigit(character); } } diff --git a/osu.Game/Graphics/UserInterface/OsuTextBox.cs b/osu.Game/Graphics/UserInterface/OsuTextBox.cs index 08d38837f6..90a000d441 100644 --- a/osu.Game/Graphics/UserInterface/OsuTextBox.cs +++ b/osu.Game/Graphics/UserInterface/OsuTextBox.cs @@ -63,6 +63,11 @@ namespace osu.Game.Graphics.UserInterface private Dictionary sampleMap = new Dictionary(); + /// + /// Whether all text should be selected when the gains focus. + /// + public bool SelectAllOnFocus { get; set; } + public OsuTextBox() { Height = 40; @@ -255,6 +260,9 @@ namespace osu.Game.Graphics.UserInterface BorderThickness = 3; base.OnFocus(e); + + if (SelectAllOnFocus) + SelectAll(); } protected override void OnFocusLost(FocusLostEvent e) diff --git a/osu.Game/Graphics/UserInterfaceV2/LabelledTextBox.cs b/osu.Game/Graphics/UserInterfaceV2/LabelledTextBox.cs index fabfde4333..b2e3ff077e 100644 --- a/osu.Game/Graphics/UserInterfaceV2/LabelledTextBox.cs +++ b/osu.Game/Graphics/UserInterfaceV2/LabelledTextBox.cs @@ -28,6 +28,12 @@ namespace osu.Game.Graphics.UserInterfaceV2 set => Component.ReadOnly = value; } + public bool SelectAllOnFocus + { + get => Component.SelectAllOnFocus; + set => Component.SelectAllOnFocus = value; + } + public LocalisableString PlaceholderText { set => Component.PlaceholderText = value; From ce93455aa8620c7b34bf3290529e3f67c27fc7e4 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 10 Jul 2024 15:08:36 +0300 Subject: [PATCH 1440/2493] Extend behaviour to sample edit popover --- .../Edit/Compose/Components/Timeline/SamplePointPiece.cs | 2 ++ .../Screens/Edit/Timing/IndeterminateSliderWithTextBoxInput.cs | 1 + 2 files changed, 3 insertions(+) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs index 930b78b468..1f9c7a891b 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs @@ -148,10 +148,12 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline bank = new LabelledTextBox { Label = "Bank Name", + SelectAllOnFocus = true, }, additionBank = new LabelledTextBox { Label = "Addition Bank", + SelectAllOnFocus = true, }, volume = new IndeterminateSliderWithTextBoxInput("Volume", new BindableInt(100) { diff --git a/osu.Game/Screens/Edit/Timing/IndeterminateSliderWithTextBoxInput.cs b/osu.Game/Screens/Edit/Timing/IndeterminateSliderWithTextBoxInput.cs index 01e1856e6c..00cf2e3493 100644 --- a/osu.Game/Screens/Edit/Timing/IndeterminateSliderWithTextBoxInput.cs +++ b/osu.Game/Screens/Edit/Timing/IndeterminateSliderWithTextBoxInput.cs @@ -75,6 +75,7 @@ namespace osu.Game.Screens.Edit.Timing textBox = new LabelledTextBox { Label = labelText, + SelectAllOnFocus = true, }, slider = new SettingsSlider { From c8a64c5950358af9ddfa407aa6f453cb5840f921 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 10 Jul 2024 14:42:11 +0200 Subject: [PATCH 1441/2493] Remove setup screen controls that do nothing useful Before I go with a hammer to redesign these, I want to remove stuff that does nothing first. Hard-breaks API to allow rulesets to specify an enumerable of custom sections rather than two specific weird ones. For specific rulesets: - osu!: - Stack leniency slider merged into difficulty section. - osu!taiko: - Approach rate and circle size sliders removed. - Colours section removed. - osu!catch: - No functional changes. - osu!mania: - Special style toggle merged into difficulty section. - Colours section removed. --- osu.Game.Rulesets.Catch/CatchRuleset.cs | 7 + .../Edit/Setup/ManiaDifficultySection.cs | 9 ++ .../Edit/Setup/ManiaSetupSection.cs | 49 ------ osu.Game.Rulesets.Mania/ManiaRuleset.cs | 7 +- .../Edit/Setup/OsuDifficultySection.cs | 150 ++++++++++++++++++ .../Edit/Setup/OsuSetupSection.cs | 56 ------- osu.Game.Rulesets.Osu/OsuRuleset.cs | 6 +- .../Edit/Setup/TaikoDifficultySection.cs | 105 ++++++++++++ osu.Game.Rulesets.Taiko/TaikoRuleset.cs | 7 + osu.Game/Rulesets/Ruleset.cs | 13 +- osu.Game/Screens/Edit/Setup/ColoursSection.cs | 2 +- .../Screens/Edit/Setup/DifficultySection.cs | 36 ++--- osu.Game/Screens/Edit/Setup/SetupScreen.cs | 17 +- 13 files changed, 318 insertions(+), 146 deletions(-) delete mode 100644 osu.Game.Rulesets.Mania/Edit/Setup/ManiaSetupSection.cs create mode 100644 osu.Game.Rulesets.Osu/Edit/Setup/OsuDifficultySection.cs delete mode 100644 osu.Game.Rulesets.Osu/Edit/Setup/OsuSetupSection.cs create mode 100644 osu.Game.Rulesets.Taiko/Edit/Setup/TaikoDifficultySection.cs diff --git a/osu.Game.Rulesets.Catch/CatchRuleset.cs b/osu.Game.Rulesets.Catch/CatchRuleset.cs index ad6dedaa8f..3edc23a8b7 100644 --- a/osu.Game.Rulesets.Catch/CatchRuleset.cs +++ b/osu.Game.Rulesets.Catch/CatchRuleset.cs @@ -28,6 +28,7 @@ using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring.Legacy; using osu.Game.Rulesets.UI; using osu.Game.Scoring; +using osu.Game.Screens.Edit.Setup; using osu.Game.Screens.Ranking.Statistics; using osu.Game.Skinning; @@ -222,6 +223,12 @@ namespace osu.Game.Rulesets.Catch public override HitObjectComposer CreateHitObjectComposer() => new CatchHitObjectComposer(this); + public override IEnumerable CreateEditorSetupSections() => + [ + new DifficultySection(), + new ColoursSection(), + ]; + public override IBeatmapVerifier CreateBeatmapVerifier() => new CatchBeatmapVerifier(); public override StatisticItem[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap) diff --git a/osu.Game.Rulesets.Mania/Edit/Setup/ManiaDifficultySection.cs b/osu.Game.Rulesets.Mania/Edit/Setup/ManiaDifficultySection.cs index 62b54a7215..7168504309 100644 --- a/osu.Game.Rulesets.Mania/Edit/Setup/ManiaDifficultySection.cs +++ b/osu.Game.Rulesets.Mania/Edit/Setup/ManiaDifficultySection.cs @@ -20,6 +20,7 @@ namespace osu.Game.Rulesets.Mania.Edit.Setup public override LocalisableString Title => EditorSetupStrings.DifficultyHeader; private LabelledSliderBar keyCountSlider { get; set; } = null!; + private LabelledSwitchButton specialStyle { get; set; } = null!; private LabelledSliderBar healthDrainSlider { get; set; } = null!; private LabelledSliderBar overallDifficultySlider { get; set; } = null!; private LabelledSliderBar baseVelocitySlider { get; set; } = null!; @@ -49,6 +50,13 @@ namespace osu.Game.Rulesets.Mania.Edit.Setup Precision = 1, } }, + specialStyle = new LabelledSwitchButton + { + Label = "Use special (N+1) style", + FixedLabelWidth = LABEL_WIDTH, + Description = "Changes one column to act as a classic \"scratch\" or \"special\" column, which can be moved around by the user's skin (to the left/right/centre). Generally used in 6K (5+1) or 8K (7+1) configurations.", + Current = { Value = Beatmap.BeatmapInfo.SpecialStyle } + }, healthDrainSlider = new LabelledSliderBar { Label = BeatmapsetsStrings.ShowStatsDrain, @@ -145,6 +153,7 @@ namespace osu.Game.Rulesets.Mania.Edit.Setup // for now, update these on commit rather than making BeatmapMetadata bindables. // after switching database engines we can reconsider if switching to bindables is a good direction. Beatmap.Difficulty.CircleSize = keyCountSlider.Current.Value; + Beatmap.BeatmapInfo.SpecialStyle = specialStyle.Current.Value; Beatmap.Difficulty.DrainRate = healthDrainSlider.Current.Value; Beatmap.Difficulty.OverallDifficulty = overallDifficultySlider.Current.Value; Beatmap.Difficulty.SliderMultiplier = baseVelocitySlider.Current.Value; diff --git a/osu.Game.Rulesets.Mania/Edit/Setup/ManiaSetupSection.cs b/osu.Game.Rulesets.Mania/Edit/Setup/ManiaSetupSection.cs deleted file mode 100644 index d5a9a311bc..0000000000 --- a/osu.Game.Rulesets.Mania/Edit/Setup/ManiaSetupSection.cs +++ /dev/null @@ -1,49 +0,0 @@ -// 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.Game.Graphics.UserInterfaceV2; -using osu.Game.Screens.Edit.Setup; - -namespace osu.Game.Rulesets.Mania.Edit.Setup -{ - public partial class ManiaSetupSection : RulesetSetupSection - { - private LabelledSwitchButton specialStyle; - - public ManiaSetupSection() - : base(new ManiaRuleset().RulesetInfo) - { - } - - [BackgroundDependencyLoader] - private void load() - { - Children = new Drawable[] - { - specialStyle = new LabelledSwitchButton - { - Label = "Use special (N+1) style", - Description = "Changes one column to act as a classic \"scratch\" or \"special\" column, which can be moved around by the user's skin (to the left/right/centre). Generally used in 6K (5+1) or 8K (7+1) configurations.", - Current = { Value = Beatmap.BeatmapInfo.SpecialStyle } - } - }; - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - specialStyle.Current.BindValueChanged(_ => updateBeatmap()); - } - - private void updateBeatmap() - { - Beatmap.BeatmapInfo.SpecialStyle = specialStyle.Current.Value; - Beatmap.SaveState(); - } - } -} diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs index 0dcbb36c77..c01fa508fe 100644 --- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs @@ -419,9 +419,10 @@ namespace osu.Game.Rulesets.Mania return new ManiaFilterCriteria(); } - public override RulesetSetupSection CreateEditorSetupSection() => new ManiaSetupSection(); - - public override SetupSection CreateEditorDifficultySection() => new ManiaDifficultySection(); + public override IEnumerable CreateEditorSetupSections() => + [ + new ManiaDifficultySection(), + ]; public int GetKeyCount(IBeatmapInfo beatmapInfo, IReadOnlyList? mods = null) => ManiaBeatmapConverter.GetColumnCount(LegacyBeatmapConversionDifficultyInfo.FromBeatmapInfo(beatmapInfo), mods); diff --git a/osu.Game.Rulesets.Osu/Edit/Setup/OsuDifficultySection.cs b/osu.Game.Rulesets.Osu/Edit/Setup/OsuDifficultySection.cs new file mode 100644 index 0000000000..b61faa0ae9 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Edit/Setup/OsuDifficultySection.cs @@ -0,0 +1,150 @@ +// 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.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Localisation; +using osu.Game.Beatmaps; +using osu.Game.Graphics.UserInterfaceV2; +using osu.Game.Localisation; +using osu.Game.Resources.Localisation.Web; +using osu.Game.Screens.Edit.Setup; + +namespace osu.Game.Rulesets.Osu.Edit.Setup +{ + public partial class OsuDifficultySection : SetupSection + { + private LabelledSliderBar circleSizeSlider { get; set; } = null!; + private LabelledSliderBar healthDrainSlider { get; set; } = null!; + private LabelledSliderBar approachRateSlider { get; set; } = null!; + private LabelledSliderBar overallDifficultySlider { get; set; } = null!; + private LabelledSliderBar baseVelocitySlider { get; set; } = null!; + private LabelledSliderBar tickRateSlider { get; set; } = null!; + private LabelledSliderBar stackLeniency { get; set; } = null!; + + public override LocalisableString Title => EditorSetupStrings.DifficultyHeader; + + [BackgroundDependencyLoader] + private void load() + { + Children = new Drawable[] + { + circleSizeSlider = new LabelledSliderBar + { + Label = BeatmapsetsStrings.ShowStatsCs, + FixedLabelWidth = LABEL_WIDTH, + Description = EditorSetupStrings.CircleSizeDescription, + Current = new BindableFloat(Beatmap.Difficulty.CircleSize) + { + Default = BeatmapDifficulty.DEFAULT_DIFFICULTY, + MinValue = 0, + MaxValue = 10, + Precision = 0.1f, + } + }, + healthDrainSlider = new LabelledSliderBar + { + Label = BeatmapsetsStrings.ShowStatsDrain, + FixedLabelWidth = LABEL_WIDTH, + Description = EditorSetupStrings.DrainRateDescription, + Current = new BindableFloat(Beatmap.Difficulty.DrainRate) + { + Default = BeatmapDifficulty.DEFAULT_DIFFICULTY, + MinValue = 0, + MaxValue = 10, + Precision = 0.1f, + } + }, + approachRateSlider = new LabelledSliderBar + { + Label = BeatmapsetsStrings.ShowStatsAr, + FixedLabelWidth = LABEL_WIDTH, + Description = EditorSetupStrings.ApproachRateDescription, + Current = new BindableFloat(Beatmap.Difficulty.ApproachRate) + { + Default = BeatmapDifficulty.DEFAULT_DIFFICULTY, + MinValue = 0, + MaxValue = 10, + Precision = 0.1f, + } + }, + overallDifficultySlider = new LabelledSliderBar + { + Label = BeatmapsetsStrings.ShowStatsAccuracy, + FixedLabelWidth = LABEL_WIDTH, + Description = EditorSetupStrings.OverallDifficultyDescription, + Current = new BindableFloat(Beatmap.Difficulty.OverallDifficulty) + { + Default = BeatmapDifficulty.DEFAULT_DIFFICULTY, + MinValue = 0, + MaxValue = 10, + Precision = 0.1f, + } + }, + baseVelocitySlider = new LabelledSliderBar + { + Label = EditorSetupStrings.BaseVelocity, + FixedLabelWidth = LABEL_WIDTH, + Description = EditorSetupStrings.BaseVelocityDescription, + Current = new BindableDouble(Beatmap.Difficulty.SliderMultiplier) + { + Default = 1.4, + MinValue = 0.4, + MaxValue = 3.6, + Precision = 0.01f, + } + }, + tickRateSlider = new LabelledSliderBar + { + Label = EditorSetupStrings.TickRate, + FixedLabelWidth = LABEL_WIDTH, + Description = EditorSetupStrings.TickRateDescription, + Current = new BindableDouble(Beatmap.Difficulty.SliderTickRate) + { + Default = 1, + MinValue = 1, + MaxValue = 4, + Precision = 1, + } + }, + stackLeniency = new LabelledSliderBar + { + Label = "Stack Leniency", + FixedLabelWidth = LABEL_WIDTH, + Description = "In play mode, osu! automatically stacks notes which occur at the same location. Increasing this value means it is more likely to snap notes of further time-distance.", + Current = new BindableFloat(Beatmap.BeatmapInfo.StackLeniency) + { + Default = 0.7f, + MinValue = 0, + MaxValue = 1, + Precision = 0.1f + } + }, + }; + + foreach (var item in Children.OfType>()) + item.Current.ValueChanged += _ => updateValues(); + + foreach (var item in Children.OfType>()) + item.Current.ValueChanged += _ => updateValues(); + } + + private void updateValues() + { + // for now, update these on commit rather than making BeatmapMetadata bindables. + // after switching database engines we can reconsider if switching to bindables is a good direction. + Beatmap.Difficulty.CircleSize = circleSizeSlider.Current.Value; + Beatmap.Difficulty.DrainRate = healthDrainSlider.Current.Value; + Beatmap.Difficulty.ApproachRate = approachRateSlider.Current.Value; + Beatmap.Difficulty.OverallDifficulty = overallDifficultySlider.Current.Value; + Beatmap.Difficulty.SliderMultiplier = baseVelocitySlider.Current.Value; + Beatmap.Difficulty.SliderTickRate = tickRateSlider.Current.Value; + Beatmap.BeatmapInfo.StackLeniency = stackLeniency.Current.Value; + + Beatmap.UpdateAllHitObjects(); + Beatmap.SaveState(); + } + } +} diff --git a/osu.Game.Rulesets.Osu/Edit/Setup/OsuSetupSection.cs b/osu.Game.Rulesets.Osu/Edit/Setup/OsuSetupSection.cs deleted file mode 100644 index 552b887081..0000000000 --- a/osu.Game.Rulesets.Osu/Edit/Setup/OsuSetupSection.cs +++ /dev/null @@ -1,56 +0,0 @@ -// 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.Game.Graphics.UserInterfaceV2; -using osu.Game.Screens.Edit.Setup; - -namespace osu.Game.Rulesets.Osu.Edit.Setup -{ - public partial class OsuSetupSection : RulesetSetupSection - { - private LabelledSliderBar stackLeniency; - - public OsuSetupSection() - : base(new OsuRuleset().RulesetInfo) - { - } - - [BackgroundDependencyLoader] - private void load() - { - Children = new[] - { - stackLeniency = new LabelledSliderBar - { - Label = "Stack Leniency", - Description = "In play mode, osu! automatically stacks notes which occur at the same location. Increasing this value means it is more likely to snap notes of further time-distance.", - Current = new BindableFloat(Beatmap.BeatmapInfo.StackLeniency) - { - Default = 0.7f, - MinValue = 0, - MaxValue = 1, - Precision = 0.1f - } - } - }; - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - stackLeniency.Current.BindValueChanged(_ => updateBeatmap()); - } - - private void updateBeatmap() - { - Beatmap.BeatmapInfo.StackLeniency = stackLeniency.Current.Value; - Beatmap.UpdateAllHitObjects(); - Beatmap.SaveState(); - } - } -} diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index 73f9be3fdc..7042ad0cd4 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -336,7 +336,11 @@ namespace osu.Game.Rulesets.Osu }; } - public override RulesetSetupSection CreateEditorSetupSection() => new OsuSetupSection(); + public override IEnumerable CreateEditorSetupSections() => + [ + new OsuDifficultySection(), + new ColoursSection(), + ]; /// /// diff --git a/osu.Game.Rulesets.Taiko/Edit/Setup/TaikoDifficultySection.cs b/osu.Game.Rulesets.Taiko/Edit/Setup/TaikoDifficultySection.cs new file mode 100644 index 0000000000..2aaa16ee0b --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Edit/Setup/TaikoDifficultySection.cs @@ -0,0 +1,105 @@ +// 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.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Localisation; +using osu.Game.Beatmaps; +using osu.Game.Graphics.UserInterfaceV2; +using osu.Game.Localisation; +using osu.Game.Resources.Localisation.Web; +using osu.Game.Screens.Edit.Setup; + +namespace osu.Game.Rulesets.Taiko.Edit.Setup +{ + public partial class TaikoDifficultySection : SetupSection + { + private LabelledSliderBar healthDrainSlider { get; set; } = null!; + private LabelledSliderBar overallDifficultySlider { get; set; } = null!; + private LabelledSliderBar baseVelocitySlider { get; set; } = null!; + private LabelledSliderBar tickRateSlider { get; set; } = null!; + + public override LocalisableString Title => EditorSetupStrings.DifficultyHeader; + + [BackgroundDependencyLoader] + private void load() + { + Children = new Drawable[] + { + healthDrainSlider = new LabelledSliderBar + { + Label = BeatmapsetsStrings.ShowStatsDrain, + FixedLabelWidth = LABEL_WIDTH, + Description = EditorSetupStrings.DrainRateDescription, + Current = new BindableFloat(Beatmap.Difficulty.DrainRate) + { + Default = BeatmapDifficulty.DEFAULT_DIFFICULTY, + MinValue = 0, + MaxValue = 10, + Precision = 0.1f, + } + }, + overallDifficultySlider = new LabelledSliderBar + { + Label = BeatmapsetsStrings.ShowStatsAccuracy, + FixedLabelWidth = LABEL_WIDTH, + Description = EditorSetupStrings.OverallDifficultyDescription, + Current = new BindableFloat(Beatmap.Difficulty.OverallDifficulty) + { + Default = BeatmapDifficulty.DEFAULT_DIFFICULTY, + MinValue = 0, + MaxValue = 10, + Precision = 0.1f, + } + }, + baseVelocitySlider = new LabelledSliderBar + { + Label = EditorSetupStrings.BaseVelocity, + FixedLabelWidth = LABEL_WIDTH, + Description = EditorSetupStrings.BaseVelocityDescription, + Current = new BindableDouble(Beatmap.Difficulty.SliderMultiplier) + { + Default = 1.4, + MinValue = 0.4, + MaxValue = 3.6, + Precision = 0.01f, + } + }, + tickRateSlider = new LabelledSliderBar + { + Label = EditorSetupStrings.TickRate, + FixedLabelWidth = LABEL_WIDTH, + Description = EditorSetupStrings.TickRateDescription, + Current = new BindableDouble(Beatmap.Difficulty.SliderTickRate) + { + Default = 1, + MinValue = 1, + MaxValue = 4, + Precision = 1, + } + }, + }; + + foreach (var item in Children.OfType>()) + item.Current.ValueChanged += _ => updateValues(); + + foreach (var item in Children.OfType>()) + item.Current.ValueChanged += _ => updateValues(); + } + + private void updateValues() + { + // for now, update these on commit rather than making BeatmapMetadata bindables. + // after switching database engines we can reconsider if switching to bindables is a good direction. + Beatmap.Difficulty.DrainRate = healthDrainSlider.Current.Value; + Beatmap.Difficulty.OverallDifficulty = overallDifficultySlider.Current.Value; + Beatmap.Difficulty.SliderMultiplier = baseVelocitySlider.Current.Value; + Beatmap.Difficulty.SliderTickRate = tickRateSlider.Current.Value; + + Beatmap.UpdateAllHitObjects(); + Beatmap.SaveState(); + } + } +} diff --git a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs index 2053a11426..2447a4a247 100644 --- a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs @@ -35,6 +35,8 @@ using osu.Game.Rulesets.Configuration; using osu.Game.Configuration; using osu.Game.Rulesets.Scoring.Legacy; using osu.Game.Rulesets.Taiko.Configuration; +using osu.Game.Rulesets.Taiko.Edit.Setup; +using osu.Game.Screens.Edit.Setup; namespace osu.Game.Rulesets.Taiko { @@ -188,6 +190,11 @@ namespace osu.Game.Rulesets.Taiko public override HitObjectComposer CreateHitObjectComposer() => new TaikoHitObjectComposer(this); + public override IEnumerable CreateEditorSetupSections() => + [ + new TaikoDifficultySection(), + ]; + public override IBeatmapVerifier CreateBeatmapVerifier() => new TaikoBeatmapVerifier(); public override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => new TaikoDifficultyCalculator(RulesetInfo, beatmap); diff --git a/osu.Game/Rulesets/Ruleset.cs b/osu.Game/Rulesets/Ruleset.cs index cae2ce610e..fb0e225c94 100644 --- a/osu.Game/Rulesets/Ruleset.cs +++ b/osu.Game/Rulesets/Ruleset.cs @@ -394,13 +394,12 @@ namespace osu.Game.Rulesets public virtual IRulesetFilterCriteria? CreateRulesetFilterCriteria() => null; /// - /// Can be overridden to add a ruleset-specific section to the editor beatmap setup screen. + /// Can be overridden to add ruleset-specific sections to the editor beatmap setup screen. /// - public virtual RulesetSetupSection? CreateEditorSetupSection() => null; - - /// - /// Can be overridden to alter the difficulty section to the editor beatmap setup screen. - /// - public virtual SetupSection? CreateEditorDifficultySection() => null; + public virtual IEnumerable CreateEditorSetupSections() => + [ + new DifficultySection(), + new ColoursSection(), + ]; } } diff --git a/osu.Game/Screens/Edit/Setup/ColoursSection.cs b/osu.Game/Screens/Edit/Setup/ColoursSection.cs index 8cd5c0f779..a5d79b5b52 100644 --- a/osu.Game/Screens/Edit/Setup/ColoursSection.cs +++ b/osu.Game/Screens/Edit/Setup/ColoursSection.cs @@ -9,7 +9,7 @@ using osu.Game.Localisation; namespace osu.Game.Screens.Edit.Setup { - internal partial class ColoursSection : SetupSection + public partial class ColoursSection : SetupSection { public override LocalisableString Title => EditorSetupStrings.ColoursHeader; diff --git a/osu.Game/Screens/Edit/Setup/DifficultySection.cs b/osu.Game/Screens/Edit/Setup/DifficultySection.cs index 8028df6c0f..b9ba2d9cb7 100644 --- a/osu.Game/Screens/Edit/Setup/DifficultySection.cs +++ b/osu.Game/Screens/Edit/Setup/DifficultySection.cs @@ -15,12 +15,12 @@ namespace osu.Game.Screens.Edit.Setup { public partial class DifficultySection : SetupSection { - protected LabelledSliderBar CircleSizeSlider { get; private set; } = null!; - protected LabelledSliderBar HealthDrainSlider { get; private set; } = null!; - protected LabelledSliderBar ApproachRateSlider { get; private set; } = null!; - protected LabelledSliderBar OverallDifficultySlider { get; private set; } = null!; - protected LabelledSliderBar BaseVelocitySlider { get; private set; } = null!; - protected LabelledSliderBar TickRateSlider { get; private set; } = null!; + private LabelledSliderBar circleSizeSlider { get; set; } = null!; + private LabelledSliderBar healthDrainSlider { get; set; } = null!; + private LabelledSliderBar approachRateSlider { get; set; } = null!; + private LabelledSliderBar overallDifficultySlider { get; set; } = null!; + private LabelledSliderBar baseVelocitySlider { get; set; } = null!; + private LabelledSliderBar tickRateSlider { get; set; } = null!; public override LocalisableString Title => EditorSetupStrings.DifficultyHeader; @@ -29,7 +29,7 @@ namespace osu.Game.Screens.Edit.Setup { Children = new Drawable[] { - CircleSizeSlider = new LabelledSliderBar + circleSizeSlider = new LabelledSliderBar { Label = BeatmapsetsStrings.ShowStatsCs, FixedLabelWidth = LABEL_WIDTH, @@ -42,7 +42,7 @@ namespace osu.Game.Screens.Edit.Setup Precision = 0.1f, } }, - HealthDrainSlider = new LabelledSliderBar + healthDrainSlider = new LabelledSliderBar { Label = BeatmapsetsStrings.ShowStatsDrain, FixedLabelWidth = LABEL_WIDTH, @@ -55,7 +55,7 @@ namespace osu.Game.Screens.Edit.Setup Precision = 0.1f, } }, - ApproachRateSlider = new LabelledSliderBar + approachRateSlider = new LabelledSliderBar { Label = BeatmapsetsStrings.ShowStatsAr, FixedLabelWidth = LABEL_WIDTH, @@ -68,7 +68,7 @@ namespace osu.Game.Screens.Edit.Setup Precision = 0.1f, } }, - OverallDifficultySlider = new LabelledSliderBar + overallDifficultySlider = new LabelledSliderBar { Label = BeatmapsetsStrings.ShowStatsAccuracy, FixedLabelWidth = LABEL_WIDTH, @@ -81,7 +81,7 @@ namespace osu.Game.Screens.Edit.Setup Precision = 0.1f, } }, - BaseVelocitySlider = new LabelledSliderBar + baseVelocitySlider = new LabelledSliderBar { Label = EditorSetupStrings.BaseVelocity, FixedLabelWidth = LABEL_WIDTH, @@ -94,7 +94,7 @@ namespace osu.Game.Screens.Edit.Setup Precision = 0.01f, } }, - TickRateSlider = new LabelledSliderBar + tickRateSlider = new LabelledSliderBar { Label = EditorSetupStrings.TickRate, FixedLabelWidth = LABEL_WIDTH, @@ -120,12 +120,12 @@ namespace osu.Game.Screens.Edit.Setup { // for now, update these on commit rather than making BeatmapMetadata bindables. // after switching database engines we can reconsider if switching to bindables is a good direction. - Beatmap.Difficulty.CircleSize = CircleSizeSlider.Current.Value; - Beatmap.Difficulty.DrainRate = HealthDrainSlider.Current.Value; - Beatmap.Difficulty.ApproachRate = ApproachRateSlider.Current.Value; - Beatmap.Difficulty.OverallDifficulty = OverallDifficultySlider.Current.Value; - Beatmap.Difficulty.SliderMultiplier = BaseVelocitySlider.Current.Value; - Beatmap.Difficulty.SliderTickRate = TickRateSlider.Current.Value; + Beatmap.Difficulty.CircleSize = circleSizeSlider.Current.Value; + Beatmap.Difficulty.DrainRate = healthDrainSlider.Current.Value; + Beatmap.Difficulty.ApproachRate = approachRateSlider.Current.Value; + Beatmap.Difficulty.OverallDifficulty = overallDifficultySlider.Current.Value; + Beatmap.Difficulty.SliderMultiplier = baseVelocitySlider.Current.Value; + Beatmap.Difficulty.SliderTickRate = tickRateSlider.Current.Value; Beatmap.UpdateAllHitObjects(); Beatmap.SaveState(); diff --git a/osu.Game/Screens/Edit/Setup/SetupScreen.cs b/osu.Game/Screens/Edit/Setup/SetupScreen.cs index 7a7907d08a..6eba678245 100644 --- a/osu.Game/Screens/Edit/Setup/SetupScreen.cs +++ b/osu.Game/Screens/Edit/Setup/SetupScreen.cs @@ -29,18 +29,13 @@ namespace osu.Game.Screens.Edit.Setup { var ruleset = beatmap.BeatmapInfo.Ruleset.CreateInstance(); - var sectionsEnumerable = new List - { - new ResourcesSection(), - new MetadataSection(), - ruleset.CreateEditorDifficultySection() ?? new DifficultySection(), - new ColoursSection(), - new DesignSection(), - }; + // ReSharper disable once UseObjectOrCollectionInitializer + var sectionsEnumerable = new List(); - var rulesetSpecificSection = ruleset.CreateEditorSetupSection(); - if (rulesetSpecificSection != null) - sectionsEnumerable.Add(rulesetSpecificSection); + sectionsEnumerable.Add(new ResourcesSection()); + sectionsEnumerable.Add(new MetadataSection()); + sectionsEnumerable.AddRange(ruleset.CreateEditorSetupSections()); + sectionsEnumerable.Add(new DesignSection()); Add(new Box { From 7d667ac46bcae641a51c0aee9995bd2a6ae44e48 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 10 Jul 2024 16:01:45 +0300 Subject: [PATCH 1442/2493] Fix confirm exit dialog overflowing from too many ongoing operations --- .../Navigation/TestSceneScreenNavigation.cs | 23 ++++++++++--------- osu.Game/Screens/Menu/ConfirmExitDialog.cs | 8 ++++++- 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs index 0fa2fd4b0b..88235d58d3 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs @@ -49,6 +49,7 @@ using osu.Game.Screens.Select.Options; using osu.Game.Tests.Beatmaps.IO; using osuTK; using osuTK.Input; +using SharpCompress; namespace osu.Game.Tests.Visual.Navigation { @@ -839,18 +840,15 @@ namespace osu.Game.Tests.Visual.Navigation { AddUntilStep("wait for dialog overlay", () => Game.ChildrenOfType().SingleOrDefault() != null); - ProgressNotification progressNotification = null!; - - AddStep("start ongoing operation", () => + AddRepeatStep("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); - }); + }); + }, 15); AddStep("Hold escape", () => InputManager.PressKey(Key.Escape)); AddUntilStep("confirmation dialog shown", () => Game.ChildrenOfType().Single().CurrentDialog is ConfirmExitDialog); @@ -861,8 +859,11 @@ namespace osu.Game.Tests.Visual.Navigation AddStep("complete operation", () => { - progressNotification.Progress = 100; - progressNotification.State = ProgressNotificationState.Completed; + this.ChildrenOfType().ForEach(n => + { + n.Progress = 100; + n.State = ProgressNotificationState.Completed; + }); }); AddStep("Hold escape", () => InputManager.PressKey(Key.Escape)); @@ -878,7 +879,7 @@ 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); - AddStep("start ongoing operation", () => + AddRepeatStep("start ongoing operation", () => { Game.Notifications.Post(new ProgressNotification { @@ -886,7 +887,7 @@ namespace osu.Game.Tests.Visual.Navigation Progress = 0.5f, State = ProgressNotificationState.Active, }); - }); + }, 15); AddRepeatStep("attempt force exit", () => Game.ScreenStack.CurrentScreen.Exit(), 2); AddUntilStep("stopped at exit confirm", () => Game.ChildrenOfType().Single().CurrentDialog is ConfirmExitDialog); diff --git a/osu.Game/Screens/Menu/ConfirmExitDialog.cs b/osu.Game/Screens/Menu/ConfirmExitDialog.cs index 0041d047bd..9243f2be54 100644 --- a/osu.Game/Screens/Menu/ConfirmExitDialog.cs +++ b/osu.Game/Screens/Menu/ConfirmExitDialog.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.Graphics.Sprites; using osu.Game.Localisation; @@ -37,9 +38,14 @@ namespace osu.Game.Screens.Menu { string text = "There are currently some background operations which will be aborted if you continue:\n\n"; - foreach (var n in notifications.OngoingOperations) + var ongoingOperations = notifications.OngoingOperations.ToArray(); + + foreach (var n in ongoingOperations.Take(Math.Min(ongoingOperations.Length, 10))) text += $"{n.Text} ({n.Progress:0%})\n"; + if (ongoingOperations.Length > 10) + text += $"\nAnd {ongoingOperations.Length - 10} other operation(s).\n"; + text += "\nLast chance to turn back"; BodyText = text; From fa749d317e0bb24a5591143023713ae2aa1d6f07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 10 Jul 2024 15:12:03 +0200 Subject: [PATCH 1443/2493] Enable NRT on `ManiaHitObjectComposer` --- osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs index c229039dc3..7a197f9d6f 100644 --- a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs +++ b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.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.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; @@ -21,7 +19,7 @@ namespace osu.Game.Rulesets.Mania.Edit { public partial class ManiaHitObjectComposer : ScrollingHitObjectComposer { - private DrawableManiaEditorRuleset drawableRuleset; + private DrawableManiaEditorRuleset drawableRuleset = null!; public ManiaHitObjectComposer(Ruleset ruleset) : base(ruleset) @@ -72,7 +70,7 @@ namespace osu.Game.Rulesets.Mania.Edit if (!double.TryParse(split[0], out double time) || !int.TryParse(split[1], out int column)) continue; - ManiaHitObject current = remainingHitObjects.FirstOrDefault(h => h.StartTime == time && h.Column == column); + ManiaHitObject? current = remainingHitObjects.FirstOrDefault(h => h.StartTime == time && h.Column == column); if (current == null) continue; From 8ca8648a0916099b85fb034e472cbf630d31845d Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 10 Jul 2024 16:14:12 +0300 Subject: [PATCH 1444/2493] Add failing test case --- .../Visual/Navigation/TestSceneScreenNavigation.cs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs index 88235d58d3..e81c6d2e86 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs @@ -838,18 +838,25 @@ namespace osu.Game.Tests.Visual.Navigation [Test] public void TestExitWithOperationInProgress() { - AddUntilStep("wait for dialog overlay", () => Game.ChildrenOfType().SingleOrDefault() != null); + int x = 0; + + AddUntilStep("wait for dialog overlay", () => + { + x = 0; + return Game.ChildrenOfType().SingleOrDefault() != null; + }); AddRepeatStep("start ongoing operation", () => { Game.Notifications.Post(new ProgressNotification { - Text = "Something is still running", + Text = $"Something is still running #{++x}", Progress = 0.5f, State = ProgressNotificationState.Active, }); }, 15); + AddAssert("all notifications = 15", () => Game.Notifications.AllNotifications.Count(), () => Is.EqualTo(15)); AddStep("Hold escape", () => InputManager.PressKey(Key.Escape)); AddUntilStep("confirmation dialog shown", () => Game.ChildrenOfType().Single().CurrentDialog is ConfirmExitDialog); AddStep("Release escape", () => InputManager.ReleaseKey(Key.Escape)); From 846fd73ac9c55405d4605c6968f82b78f79bb488 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 10 Jul 2024 16:15:02 +0300 Subject: [PATCH 1445/2493] Fix notification toast tray potentially hiding some notifications --- .../Overlays/NotificationOverlayToastTray.cs | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 deletions(-) diff --git a/osu.Game/Overlays/NotificationOverlayToastTray.cs b/osu.Game/Overlays/NotificationOverlayToastTray.cs index 0ebaff9437..e019b31620 100644 --- a/osu.Game/Overlays/NotificationOverlayToastTray.cs +++ b/osu.Game/Overlays/NotificationOverlayToastTray.cs @@ -24,16 +24,16 @@ namespace osu.Game.Overlays /// public partial class NotificationOverlayToastTray : CompositeDrawable { - public override bool IsPresent => toastContentBackground.Height > 0 || toastFlow.Count > 0; + public override bool IsPresent => toastContentBackground.Height > 0 || Notifications.Any(); public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => toastFlow.ReceivePositionalInputAt(screenSpacePos); /// /// All notifications currently being displayed by the toast tray. /// - public IEnumerable Notifications => toastFlow; + public IEnumerable Notifications => toastFlow.Concat(InternalChildren.OfType()); - public bool IsDisplayingToasts => toastFlow.Count > 0; + public bool IsDisplayingToasts => Notifications.Any(); private FillFlowContainer toastFlow = null!; private BufferedContainer toastContentBackground = null!; @@ -43,12 +43,7 @@ namespace osu.Game.Overlays public Action? ForwardNotificationToPermanentStore { get; set; } - public int UnreadCount => allDisplayedNotifications.Count(n => !n.WasClosed && !n.Read); - - /// - /// Notifications contained in the toast flow, or in a detached state while they animate during forwarding to the main overlay. - /// - private IEnumerable allDisplayedNotifications => toastFlow.Concat(InternalChildren.OfType()); + public int UnreadCount => Notifications.Count(n => !n.WasClosed && !n.Read); private int runningDepth; @@ -91,11 +86,7 @@ namespace osu.Game.Overlays }; } - public void MarkAllRead() - { - toastFlow.Children.ForEach(n => n.Read = true); - InternalChildren.OfType().ForEach(n => n.Read = true); - } + public void MarkAllRead() => Notifications.ForEach(n => n.Read = true); public void FlushAllToasts() { From bb9a2b705e576bead9e6102d3da75f0940304ee0 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 10 Jul 2024 16:29:28 +0300 Subject: [PATCH 1446/2493] Remove unnecessary math min --- osu.Game/Screens/Menu/ConfirmExitDialog.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Menu/ConfirmExitDialog.cs b/osu.Game/Screens/Menu/ConfirmExitDialog.cs index 9243f2be54..1e444a896b 100644 --- a/osu.Game/Screens/Menu/ConfirmExitDialog.cs +++ b/osu.Game/Screens/Menu/ConfirmExitDialog.cs @@ -40,7 +40,7 @@ namespace osu.Game.Screens.Menu var ongoingOperations = notifications.OngoingOperations.ToArray(); - foreach (var n in ongoingOperations.Take(Math.Min(ongoingOperations.Length, 10))) + foreach (var n in ongoingOperations.Take(10)) text += $"{n.Text} ({n.Progress:0%})\n"; if (ongoingOperations.Length > 10) From b58ba5f5f1fd7815d0754efb6651f62e9dee4418 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 10 Jul 2024 23:02:19 +0900 Subject: [PATCH 1447/2493] Just give in to silly code quality inspection --- osu.Game/Screens/Edit/Setup/SetupScreen.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Edit/Setup/SetupScreen.cs b/osu.Game/Screens/Edit/Setup/SetupScreen.cs index 6eba678245..17bbc7daa2 100644 --- a/osu.Game/Screens/Edit/Setup/SetupScreen.cs +++ b/osu.Game/Screens/Edit/Setup/SetupScreen.cs @@ -29,11 +29,12 @@ namespace osu.Game.Screens.Edit.Setup { var ruleset = beatmap.BeatmapInfo.Ruleset.CreateInstance(); - // ReSharper disable once UseObjectOrCollectionInitializer - var sectionsEnumerable = new List(); + List sectionsEnumerable = + [ + new ResourcesSection(), + new MetadataSection() + ]; - sectionsEnumerable.Add(new ResourcesSection()); - sectionsEnumerable.Add(new MetadataSection()); sectionsEnumerable.AddRange(ruleset.CreateEditorSetupSections()); sectionsEnumerable.Add(new DesignSection()); From 92dc125d391fa5bfd0420a1a220a2417b2dd6b37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 10 Jul 2024 17:24:03 +0200 Subject: [PATCH 1448/2493] Match mania editor playfield time range with timeline zoom --- .../Edit/DrawableManiaEditorRuleset.cs | 9 +++++++++ .../Edit/ManiaHitObjectComposer.cs | 13 +++++++++++++ .../UI/DrawableManiaRuleset.cs | 18 +++++++++++++----- .../Screens/Edit/EditorScreenWithTimeline.cs | 9 ++++----- 4 files changed, 39 insertions(+), 10 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Edit/DrawableManiaEditorRuleset.cs b/osu.Game.Rulesets.Mania/Edit/DrawableManiaEditorRuleset.cs index 8d34373f82..4c4cf519ce 100644 --- a/osu.Game.Rulesets.Mania/Edit/DrawableManiaEditorRuleset.cs +++ b/osu.Game.Rulesets.Mania/Edit/DrawableManiaEditorRuleset.cs @@ -6,6 +6,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Beatmaps; using osu.Game.Configuration; +using osu.Game.Rulesets.Mania.Configuration; using osu.Game.Rulesets.Mania.UI; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.UI; @@ -18,6 +19,8 @@ namespace osu.Game.Rulesets.Mania.Edit { public BindableBool ShowSpeedChanges { get; } = new BindableBool(); + public double? TimelineTimeRange { get; set; } + public new IScrollingInfo ScrollingInfo => base.ScrollingInfo; public DrawableManiaEditorRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList? mods) @@ -38,5 +41,11 @@ namespace osu.Game.Rulesets.Mania.Edit Origin = Anchor.Centre, Size = Vector2.One }; + + protected override void Update() + { + TargetTimeRange = TimelineTimeRange == null || ShowSpeedChanges.Value ? ComputeScrollTime(Config.Get(ManiaRulesetSetting.ScrollSpeed)) : TimelineTimeRange.Value; + base.Update(); + } } } diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs index 7a197f9d6f..02a4f3a022 100644 --- a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs +++ b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; +using osu.Framework.Allocation; using osu.Game.Beatmaps; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit.Tools; @@ -12,6 +13,7 @@ using osu.Game.Rulesets.Mania.UI; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI.Scrolling; +using osu.Game.Screens.Edit; using osu.Game.Screens.Edit.Compose.Components; using osuTK; @@ -21,6 +23,9 @@ namespace osu.Game.Rulesets.Mania.Edit { private DrawableManiaEditorRuleset drawableRuleset = null!; + [Resolved] + private EditorScreenWithTimeline? screenWithTimeline { get; set; } + public ManiaHitObjectComposer(Ruleset ruleset) : base(ruleset) { @@ -81,5 +86,13 @@ namespace osu.Game.Rulesets.Mania.Edit remainingHitObjects = remainingHitObjects.Where(h => h != current && h.StartTime >= current.StartTime).ToList(); } } + + protected override void Update() + { + base.Update(); + + if (screenWithTimeline?.TimelineArea.Timeline != null) + drawableRuleset.TimelineTimeRange = EditorClock.TrackLength / screenWithTimeline.TimelineArea.Timeline.CurrentZoom / 2; + } } } diff --git a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs index ce53862c76..aed53e157a 100644 --- a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs @@ -8,9 +8,10 @@ using osu.Framework.Audio.Track; using osu.Framework.Bindables; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Extensions.ObjectExtensions; -using osu.Framework.Graphics; using osu.Framework.Input; +using osu.Framework.Platform; using osu.Framework.Threading; +using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Input.Handlers; @@ -56,13 +57,18 @@ namespace osu.Game.Rulesets.Mania.UI private readonly Bindable configDirection = new Bindable(); private readonly BindableInt configScrollSpeed = new BindableInt(); - private double smoothTimeRange; + + private double currentTimeRange; + protected double TargetTimeRange; // Stores the current speed adjustment active in gameplay. private readonly Track speedAdjustmentTrack = new TrackVirtual(0); private ISkinSource currentSkin = null!; + [Resolved] + private GameHost gameHost { get; set; } = null!; + public DrawableManiaRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList? mods = null) : base(ruleset, beatmap, mods) { @@ -101,9 +107,9 @@ namespace osu.Game.Rulesets.Mania.UI configDirection.BindValueChanged(direction => Direction.Value = (ScrollingDirection)direction.NewValue, true); Config.BindWith(ManiaRulesetSetting.ScrollSpeed, configScrollSpeed); - configScrollSpeed.BindValueChanged(speed => this.TransformTo(nameof(smoothTimeRange), ComputeScrollTime(speed.NewValue), 200, Easing.OutQuint)); + configScrollSpeed.BindValueChanged(speed => TargetTimeRange = ComputeScrollTime(speed.NewValue)); - TimeRange.Value = smoothTimeRange = ComputeScrollTime(configScrollSpeed.Value); + TimeRange.Value = TargetTimeRange = currentTimeRange = ComputeScrollTime(configScrollSpeed.Value); KeyBindingInputManager.Add(new ManiaTouchInputArea()); } @@ -144,7 +150,9 @@ namespace osu.Game.Rulesets.Mania.UI // This scaling factor preserves the scroll speed as the scroll length varies from changes to the hit position. float scale = lengthToHitPosition / length_to_default_hit_position; - TimeRange.Value = smoothTimeRange * speedAdjustmentTrack.AggregateTempo.Value * speedAdjustmentTrack.AggregateFrequency.Value * scale; + // we're intentionally using the game host's update clock here to decouple the time range tween from the gameplay clock (which can be arbitrarily paused, or even rewinding) + currentTimeRange = Interpolation.DampContinuously(currentTimeRange, TargetTimeRange, 50, gameHost.UpdateThread.Clock.ElapsedFrameTime); + TimeRange.Value = currentTimeRange * speedAdjustmentTrack.AggregateTempo.Value * speedAdjustmentTrack.AggregateFrequency.Value * scale; } /// diff --git a/osu.Game/Screens/Edit/EditorScreenWithTimeline.cs b/osu.Game/Screens/Edit/EditorScreenWithTimeline.cs index cdc8a26c35..38d2a1e7e4 100644 --- a/osu.Game/Screens/Edit/EditorScreenWithTimeline.cs +++ b/osu.Game/Screens/Edit/EditorScreenWithTimeline.cs @@ -13,13 +13,12 @@ namespace osu.Game.Screens.Edit [Cached] public abstract partial class EditorScreenWithTimeline : EditorScreen { - public const float PADDING = 10; - - public Container TimelineContent { get; private set; } = null!; + public TimelineArea TimelineArea { get; private set; } = null!; public Container MainContent { get; private set; } = null!; private LoadingSpinner spinner = null!; + private Container timelineContent = null!; protected EditorScreenWithTimeline(EditorScreenMode type) : base(type) @@ -60,7 +59,7 @@ namespace osu.Game.Screens.Edit { new Drawable[] { - TimelineContent = new Container + timelineContent = new Container { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, @@ -108,7 +107,7 @@ namespace osu.Game.Screens.Edit MainContent.Add(content); content.FadeInFromZero(300, Easing.OutQuint); - LoadComponentAsync(new TimelineArea(CreateTimelineContent()), TimelineContent.Add); + LoadComponentAsync(TimelineArea = new TimelineArea(CreateTimelineContent()), timelineContent.Add); }); } From 55b4dd9b99bbe637d90e7e9ebc2b8ff36b10952d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 10 Jul 2024 18:17:10 +0200 Subject: [PATCH 1449/2493] Adjust punctuation --- osu.Game/Screens/Menu/ConfirmExitDialog.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Menu/ConfirmExitDialog.cs b/osu.Game/Screens/Menu/ConfirmExitDialog.cs index 1e444a896b..e33071e78c 100644 --- a/osu.Game/Screens/Menu/ConfirmExitDialog.cs +++ b/osu.Game/Screens/Menu/ConfirmExitDialog.cs @@ -44,7 +44,7 @@ namespace osu.Game.Screens.Menu text += $"{n.Text} ({n.Progress:0%})\n"; if (ongoingOperations.Length > 10) - text += $"\nAnd {ongoingOperations.Length - 10} other operation(s).\n"; + text += $"\nand {ongoingOperations.Length - 10} other operation(s).\n"; text += "\nLast chance to turn back"; From b0d6c8ca6d59e3ced2684eafcc9d66135abe5b76 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 11 Jul 2024 11:42:06 +0900 Subject: [PATCH 1450/2493] Abort operation on save failure --- osu.Game/Screens/Edit/Editor.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index a675b41833..8585aa910f 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -1135,7 +1135,8 @@ namespace osu.Game.Screens.Edit private void editExternally() { - Save(); + if (!Save()) + return; var editOperation = beatmapManager.BeginExternalEditing(editorBeatmap.BeatmapInfo.BeatmapSet!); this.Push(new ExternalEditScreen(editOperation, this)); From cd6b0e875a90ddc0fa5423c57afd3fcad4038d67 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 11 Jul 2024 12:15:17 +0900 Subject: [PATCH 1451/2493] Simplify save dialogs --- osu.Game/Screens/Edit/Editor.cs | 19 ++++++++----------- .../Screens/Edit/SaveRequiredPopupDialog.cs | 4 ++-- 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 8585aa910f..700f355207 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -481,7 +481,7 @@ namespace osu.Game.Screens.Edit { if (HasUnsavedChanges) { - dialogOverlay.Push(new SaveRequiredPopupDialog("The beatmap will be saved in order to test it.", () => attemptMutationOperation(() => + dialogOverlay.Push(new SaveRequiredPopupDialog(() => attemptMutationOperation(() => { if (!Save()) return false; @@ -1146,7 +1146,7 @@ namespace osu.Game.Screens.Edit { if (HasUnsavedChanges) { - dialogOverlay.Push(new SaveRequiredPopupDialog("The beatmap will be saved in order to export it.", () => attemptAsyncMutationOperation(() => + dialogOverlay.Push(new SaveRequiredPopupDialog(() => attemptAsyncMutationOperation(() => { if (!Save()) return Task.CompletedTask; @@ -1224,17 +1224,14 @@ namespace osu.Game.Screens.Edit { if (isNewBeatmap) { - dialogOverlay.Push(new SaveRequiredPopupDialog("This beatmap will be saved in order to create another difficulty.", () => + dialogOverlay.Push(new SaveRequiredPopupDialog(() => attemptMutationOperation(() => { - attemptMutationOperation(() => - { - if (!Save()) - return false; + if (!Save()) + return false; - CreateNewDifficulty(rulesetInfo); - return true; - }); - })); + CreateNewDifficulty(rulesetInfo); + return true; + }))); return; } diff --git a/osu.Game/Screens/Edit/SaveRequiredPopupDialog.cs b/osu.Game/Screens/Edit/SaveRequiredPopupDialog.cs index 3ca92876f1..618efb7cda 100644 --- a/osu.Game/Screens/Edit/SaveRequiredPopupDialog.cs +++ b/osu.Game/Screens/Edit/SaveRequiredPopupDialog.cs @@ -9,9 +9,9 @@ namespace osu.Game.Screens.Edit { public partial class SaveRequiredPopupDialog : PopupDialog { - public SaveRequiredPopupDialog(string headerText, Action saveAndAction) + public SaveRequiredPopupDialog(Action saveAndAction) { - HeaderText = headerText; + HeaderText = "The beatmap will be saved to continue with this operation."; Icon = FontAwesome.Regular.Save; From 599a765fd18f70e96e23da40462b689e7eca0e66 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 11 Jul 2024 12:20:33 +0900 Subject: [PATCH 1452/2493] Add confirmation before saving for external edit --- osu.Game/Screens/Edit/Editor.cs | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 700f355207..7115147d0b 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -1135,11 +1135,27 @@ namespace osu.Game.Screens.Edit private void editExternally() { - if (!Save()) - return; + if (HasUnsavedChanges) + { + dialogOverlay.Push(new SaveRequiredPopupDialog(() => attemptMutationOperation(() => + { + if (!Save()) + return false; - var editOperation = beatmapManager.BeginExternalEditing(editorBeatmap.BeatmapInfo.BeatmapSet!); - this.Push(new ExternalEditScreen(editOperation, this)); + startEdit(); + return true; + }))); + } + else + { + startEdit(); + } + + void startEdit() + { + var editOperation = beatmapManager.BeginExternalEditing(editorBeatmap.BeatmapInfo.BeatmapSet!); + this.Push(new ExternalEditScreen(editOperation, this)); + } } private void exportBeatmap(bool legacy) From 2eb6cf57afd0b15ac19352283dbe468e8e9b09a8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 11 Jul 2024 13:42:24 +0900 Subject: [PATCH 1453/2493] Fix incorrect continuation in `ImportAsUpdate` causing UI blockage --- osu.Game/Beatmaps/BeatmapImporter.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapImporter.cs b/osu.Game/Beatmaps/BeatmapImporter.cs index 2137f33e77..71aa5b0333 100644 --- a/osu.Game/Beatmaps/BeatmapImporter.cs +++ b/osu.Game/Beatmaps/BeatmapImporter.cs @@ -43,7 +43,9 @@ namespace osu.Game.Beatmaps public override async Task?> ImportAsUpdate(ProgressNotification notification, ImportTask importTask, BeatmapSetInfo original) { - var imported = await Import(notification, new[] { importTask }).ConfigureAwait(true); + Guid originalId = original.ID; + + var imported = await Import(notification, new[] { importTask }).ConfigureAwait(false); if (!imported.Any()) return null; @@ -53,7 +55,7 @@ namespace osu.Game.Beatmaps var first = imported.First(); // If there were no changes, ensure we don't accidentally nuke ourselves. - if (first.ID == original.ID) + if (first.ID == originalId) { first.PerformRead(s => { @@ -69,7 +71,8 @@ namespace osu.Game.Beatmaps Logger.Log($"Beatmap \"{updated}\" update completed successfully", LoggingTarget.Database); - original = realm!.Find(original.ID)!; + // Re-fetch as we are likely on a different thread. + original = realm!.Find(originalId)!; // 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). From bdbdc3592ee8deb9aaeccc23d8bcf13fcfefbe12 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 11 Jul 2024 14:27:12 +0900 Subject: [PATCH 1454/2493] Move full export async flow inside screen and add error handling --- osu.Game/Screens/Edit/Editor.cs | 3 +- osu.Game/Screens/Edit/ExternalEditScreen.cs | 218 +++++++++++--------- 2 files changed, 118 insertions(+), 103 deletions(-) diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 7115147d0b..d841e68263 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -1153,8 +1153,7 @@ namespace osu.Game.Screens.Edit void startEdit() { - var editOperation = beatmapManager.BeginExternalEditing(editorBeatmap.BeatmapInfo.BeatmapSet!); - this.Push(new ExternalEditScreen(editOperation, this)); + this.Push(new ExternalEditScreen()); } } diff --git a/osu.Game/Screens/Edit/ExternalEditScreen.cs b/osu.Game/Screens/Edit/ExternalEditScreen.cs index a8a75f22db..ef497020f8 100644 --- a/osu.Game/Screens/Edit/ExternalEditScreen.cs +++ b/osu.Game/Screens/Edit/ExternalEditScreen.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.IO; using System.Linq; using System.Threading.Tasks; @@ -20,6 +19,7 @@ using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterfaceV2; +using osu.Game.Online.Multiplayer; using osu.Game.Overlays; using osu.Game.Screens.OnlinePlay.Match.Components; using osuTK; @@ -28,28 +28,27 @@ namespace osu.Game.Screens.Edit { internal partial class ExternalEditScreen : OsuScreen { - private readonly Task> fileMountOperation; - [Resolved] private GameHost gameHost { get; set; } = null!; [Resolved] private OverlayColourProvider colourProvider { get; set; } = null!; - private readonly Editor editor; + [Resolved] + private BeatmapManager beatmapManager { get; set; } = null!; + + [Resolved] + private Editor editor { get; set; } = null!; + + [Resolved] + private EditorBeatmap editorBeatmap { get; set; } = null!; + + private Task? fileMountOperation; public ExternalEditOperation? EditOperation; - private double timeLoaded; - private FillFlowContainer flow = null!; - public ExternalEditScreen(Task> fileMountOperation, Editor editor) - { - this.fileMountOperation = fileMountOperation; - this.editor = editor; - } - [BackgroundDependencyLoader] private void load() { @@ -80,71 +79,98 @@ namespace osu.Game.Screens.Edit } } }; - - showSpinner("Exporting for edit..."); } protected override void LoadComplete() { base.LoadComplete(); - timeLoaded = Time.Current; - - fileMountOperation.ContinueWith(t => - { - EditOperation = t.GetResultSafely(); - - Scheduler.AddDelayed(() => - { - flow.Children = new Drawable[] - { - new OsuSpriteText - { - Text = "Beatmap is mounted externally", - Font = OsuFont.Default.With(size: 30), - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - }, - new OsuTextFlowContainer - { - Padding = new MarginPadding(5), - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - Width = 350, - AutoSizeAxes = Axes.Y, - Text = "Any changes made to the exported folder will be imported to the game, including file additions, modifications and deletions.", - }, - new PurpleRoundedButton - { - Text = "Open folder", - Width = 350, - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - Action = open, - Enabled = { Value = false } - }, - new DangerousRoundedButton - { - Text = "Finish editing and import changes", - Width = 350, - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - Action = finish, - Enabled = { Value = false } - } - }; - - Scheduler.AddDelayed(() => - { - foreach (var b in flow.ChildrenOfType()) - b.Enabled.Value = true; - open(); - }, 1000); - }, Math.Max(0, 1000 - (Time.Current - timeLoaded))); - }); + fileMountOperation = begin(); } - private void open() + public override bool OnExiting(ScreenExitEvent e) + { + // Don't allow exiting until the file mount operation has completed. + // This is mainly to simplify the flow (once the screen is pushed we are guaranteed an attempted mount). + if (fileMountOperation?.IsCompleted == false) + return true; + + // If the operation completed successfully, ensure that we finish the operation before exiting. + // The finish() call will subsequently call Exit() when done. + if (EditOperation != null) + { + finish().FireAndForget(); + return true; + } + + return base.OnExiting(e); + } + + private async Task begin() + { + showSpinner("Exporting for edit..."); + + await Task.Delay(500).ConfigureAwait(true); + + try + { + EditOperation = await beatmapManager.BeginExternalEditing(editorBeatmap.BeatmapInfo.BeatmapSet!).ConfigureAwait(true); + } + catch + { + fileMountOperation = null; + showSpinner("Export failed!"); + await Task.Delay(1000).ConfigureAwait(true); + this.Exit(); + } + + flow.Children = new Drawable[] + { + new OsuSpriteText + { + Text = "Beatmap is mounted externally", + Font = OsuFont.Default.With(size: 30), + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + }, + new OsuTextFlowContainer + { + Padding = new MarginPadding(5), + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Width = 350, + AutoSizeAxes = Axes.Y, + Text = "Any changes made to the exported folder will be imported to the game, including file additions, modifications and deletions.", + }, + new PurpleRoundedButton + { + Text = "Open folder", + Width = 350, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Action = openDirectory, + Enabled = { Value = false } + }, + new DangerousRoundedButton + { + Text = "Finish editing and import changes", + Width = 350, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Action = () => finish().FireAndForget(), + Enabled = { Value = false } + } + }; + + Scheduler.AddDelayed(() => + { + foreach (var b in flow.ChildrenOfType()) + b.Enabled.Value = true; + openDirectory(); + }, 1000); + } + + private void openDirectory() { if (EditOperation == null) return; @@ -153,47 +179,37 @@ namespace osu.Game.Screens.Edit gameHost.OpenFileExternally(EditOperation.MountedPath.TrimDirectorySeparator() + Path.DirectorySeparatorChar); } - public override bool OnExiting(ScreenExitEvent e) - { - if (!fileMountOperation.IsCompleted) - return true; - - if (EditOperation != null) - { - finish(); - return true; - } - - return base.OnExiting(e); - } - - private void finish() + private async Task finish() { string originalDifficulty = editor.Beatmap.Value.Beatmap.BeatmapInfo.DifficultyName; showSpinner("Cleaning up..."); - EditOperation!.Finish().ContinueWith(t => + Live? beatmap = null; + + try { - Schedule(() => - { - // Setting to null will allow exit to succeed. - EditOperation = null; + beatmap = await EditOperation!.Finish().ConfigureAwait(true); + } + catch + { + showSpinner("Import failed!"); + await Task.Delay(1000).ConfigureAwait(true); + } - Live? beatmap = t.GetResultSafely(); + // Setting to null will allow exit to succeed. + EditOperation = null; - if (beatmap == null) - this.Exit(); - else - { - var closestMatchingBeatmap = - beatmap.Value.Beatmaps.FirstOrDefault(b => b.DifficultyName == originalDifficulty) - ?? beatmap.Value.Beatmaps.First(); + if (beatmap == null) + this.Exit(); + else + { + var closestMatchingBeatmap = + beatmap.Value.Beatmaps.FirstOrDefault(b => b.DifficultyName == originalDifficulty) + ?? beatmap.Value.Beatmaps.First(); - editor.SwitchToDifficulty(closestMatchingBeatmap); - } - }); - }); + editor.SwitchToDifficulty(closestMatchingBeatmap); + } } private void showSpinner(string text) From a9c8c6e74d5e1524c69648d0b49ad2efbe10b4c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 11 Jul 2024 08:18:16 +0200 Subject: [PATCH 1455/2493] Attempt to fix test failures by forcing refresh --- osu.Game.Tests/Database/BeatmapImporterUpdateTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Database/BeatmapImporterUpdateTests.cs b/osu.Game.Tests/Database/BeatmapImporterUpdateTests.cs index d30b3c089e..16e66cb2c5 100644 --- a/osu.Game.Tests/Database/BeatmapImporterUpdateTests.cs +++ b/osu.Game.Tests/Database/BeatmapImporterUpdateTests.cs @@ -168,12 +168,12 @@ namespace osu.Game.Tests.Database Assert.That(importAfterUpdate, Is.Not.Null); Debug.Assert(importAfterUpdate != null); + realm.Run(r => r.Refresh()); + // should only contain the modified beatmap (others purged). Assert.That(importBeforeUpdate.Value.Beatmaps, Has.Count.EqualTo(1)); Assert.That(importAfterUpdate.Value.Beatmaps, Has.Count.EqualTo(count_beatmaps)); - realm.Run(r => r.Refresh()); - checkCount(realm, count_beatmaps + 1); checkCount(realm, count_beatmaps + 1); From fe421edd8f2b86d1097c650cb89771af67be8f80 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 11 Jul 2024 16:41:21 +0900 Subject: [PATCH 1456/2493] Fix editor UI transparency being incorrectly opaque when hovering slider control points As mentioned at https://github.com/ppy/osu/pull/28787#issuecomment-2221150025. --- osu.Game/Rulesets/Edit/HitObjectComposer.cs | 30 +++---------------- osu.Game/Screens/Edit/BottomBar.cs | 1 + .../Components/Timeline/TimelineArea.cs | 1 + osu.Game/Screens/Edit/Editor.cs | 3 ++ 4 files changed, 9 insertions(+), 26 deletions(-) diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs index 1ba488d027..3c38a7258e 100644 --- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs @@ -132,7 +132,7 @@ namespace osu.Game.Rulesets.Edit InternalChildren = new[] { - PlayfieldContentContainer = new ContentContainer + PlayfieldContentContainer = new Container { Name = "Playfield content", RelativeSizeAxes = Axes.Y, @@ -269,6 +269,7 @@ namespace osu.Game.Rulesets.Edit composerFocusMode.BindValueChanged(_ => { + // Transforms should be kept in sync with other usages of composer focus mode. if (!composerFocusMode.Value) { leftToolboxBackground.FadeIn(750, Easing.OutQuint); @@ -303,6 +304,8 @@ namespace osu.Game.Rulesets.Edit PlayfieldContentContainer.Width = Math.Max(1024, DrawWidth) - (TOOLBOX_CONTRACTED_SIZE_LEFT + TOOLBOX_CONTRACTED_SIZE_RIGHT); PlayfieldContentContainer.X = TOOLBOX_CONTRACTED_SIZE_LEFT; } + + composerFocusMode.Value = PlayfieldContentContainer.Contains(InputManager.CurrentState.Mouse.Position); } public override Playfield Playfield => drawableRulesetWrapper.Playfield; @@ -529,31 +532,6 @@ namespace osu.Game.Rulesets.Edit } #endregion - - private partial class ContentContainer : Container - { - public override bool HandlePositionalInput => true; - - private readonly Bindable composerFocusMode = new Bindable(); - - [BackgroundDependencyLoader(true)] - private void load([CanBeNull] Editor editor) - { - if (editor != null) - composerFocusMode.BindTo(editor.ComposerFocusMode); - } - - protected override bool OnHover(HoverEvent e) - { - composerFocusMode.Value = true; - return false; - } - - protected override void OnHoverLost(HoverLostEvent e) - { - composerFocusMode.Value = false; - } - } } /// diff --git a/osu.Game/Screens/Edit/BottomBar.cs b/osu.Game/Screens/Edit/BottomBar.cs index dd56752119..680f61ceaa 100644 --- a/osu.Game/Screens/Edit/BottomBar.cs +++ b/osu.Game/Screens/Edit/BottomBar.cs @@ -82,6 +82,7 @@ namespace osu.Game.Screens.Edit saveInProgress.BindValueChanged(_ => TestGameplayButton.Enabled.Value = !saveInProgress.Value, true); composerFocusMode.BindValueChanged(_ => { + // Transforms should be kept in sync with other usages of composer focus mode. foreach (var c in this.ChildrenOfType()) { if (!composerFocusMode.Value) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineArea.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineArea.cs index ff92e658d9..1db067c846 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineArea.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineArea.cs @@ -134,6 +134,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline composerFocusMode.BindValueChanged(_ => { + // Transforms should be kept in sync with other usages of composer focus mode. if (!composerFocusMode.Value) timelineBackground.FadeIn(750, Easing.OutQuint); else diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 27d0392b1e..db94b08406 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -218,6 +218,9 @@ namespace osu.Game.Screens.Edit /// This controls the opacity of components like the timelines, sidebars, etc. /// In "composer focus" mode the opacity of the aforementioned components is reduced so that the user can focus on the composer better. /// + /// + /// The state of this bindable is controlled by . + /// public Bindable ComposerFocusMode { get; } = new Bindable(); public Editor(EditorLoader loader = null) From a859978efddc9e9e4761078fc8daa65ee5cb0b9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 11 Jul 2024 09:43:00 +0200 Subject: [PATCH 1457/2493] Add failing test steps for locally modified state not being set --- .../Visual/Navigation/TestSceneBeatmapEditorNavigation.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneBeatmapEditorNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneBeatmapEditorNavigation.cs index 1f227520c1..b5dfa9a87f 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneBeatmapEditorNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneBeatmapEditorNavigation.cs @@ -87,6 +87,8 @@ namespace osu.Game.Tests.Visual.Navigation AddUntilStep("wait for editor", () => Game.ScreenStack.CurrentScreen is Editor editor && editor.ReadyForUse); AddAssert("beatmapset changed", () => getEditor().Beatmap.Value.BeatmapSetInfo, () => Is.Not.EqualTo(beatmapSet)); + AddAssert("beatmapset is locally modified", () => getEditor().Beatmap.Value.BeatmapSetInfo.Status, () => Is.EqualTo(BeatmapOnlineStatus.LocallyModified)); + AddAssert("all difficulties are locally modified", () => getEditor().Beatmap.Value.BeatmapSetInfo.Beatmaps.All(b => b.Status == BeatmapOnlineStatus.LocallyModified)); AddAssert("difficulty didn't change", () => getEditor().Beatmap.Value.BeatmapInfo.DifficultyName, () => Is.EqualTo(difficultyName)); AddAssert("old beatmapset deleted", () => Game.BeatmapManager.QueryBeatmapSet(s => s.ID == beatmapSet.ID), () => Is.Null); } From ac467cf73a2bcd1af6c7e709227b7e5f77180fbb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 11 Jul 2024 09:43:20 +0200 Subject: [PATCH 1458/2493] Set locally modified state for all externally modified beatmap(sets) that could not be mapped to online --- osu.Game/Screens/Edit/ExternalEditScreen.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/osu.Game/Screens/Edit/ExternalEditScreen.cs b/osu.Game/Screens/Edit/ExternalEditScreen.cs index ef497020f8..edfaa59e30 100644 --- a/osu.Game/Screens/Edit/ExternalEditScreen.cs +++ b/osu.Game/Screens/Edit/ExternalEditScreen.cs @@ -204,6 +204,16 @@ namespace osu.Game.Screens.Edit this.Exit(); else { + // the `ImportAsUpdate()` flow will yield beatmap(sets) with online status of `None` if online lookup fails. + // coerce such models to `LocallyModified` state instead to unify behaviour with normal editing flow. + beatmap.PerformWrite(s => + { + if (s.Status == BeatmapOnlineStatus.None) + s.Status = BeatmapOnlineStatus.LocallyModified; + foreach (var difficulty in s.Beatmaps.Where(b => b.Status == BeatmapOnlineStatus.None)) + difficulty.Status = BeatmapOnlineStatus.LocallyModified; + }); + var closestMatchingBeatmap = beatmap.Value.Beatmaps.FirstOrDefault(b => b.DifficultyName == originalDifficulty) ?? beatmap.Value.Beatmaps.First(); From cc0d7e99814e70068bf283583efeff864c80834d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 11 Jul 2024 09:54:12 +0200 Subject: [PATCH 1459/2493] Add error logging on failure to begin/end external edit --- osu.Game/Screens/Edit/ExternalEditScreen.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Edit/ExternalEditScreen.cs b/osu.Game/Screens/Edit/ExternalEditScreen.cs index edfaa59e30..8a97e3dcb2 100644 --- a/osu.Game/Screens/Edit/ExternalEditScreen.cs +++ b/osu.Game/Screens/Edit/ExternalEditScreen.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.IO; using System.Linq; using System.Threading.Tasks; @@ -9,6 +10,7 @@ using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Framework.Logging; using osu.Framework.Platform; using osu.Framework.Screens; using osu.Framework.Testing; @@ -116,8 +118,9 @@ namespace osu.Game.Screens.Edit { EditOperation = await beatmapManager.BeginExternalEditing(editorBeatmap.BeatmapInfo.BeatmapSet!).ConfigureAwait(true); } - catch + catch (Exception ex) { + Logger.Log($@"Failed to initiate external edit operation: {ex}", LoggingTarget.Database); fileMountOperation = null; showSpinner("Export failed!"); await Task.Delay(1000).ConfigureAwait(true); @@ -191,8 +194,9 @@ namespace osu.Game.Screens.Edit { beatmap = await EditOperation!.Finish().ConfigureAwait(true); } - catch + catch (Exception ex) { + Logger.Log($@"Failed to finish external edit operation: {ex}", LoggingTarget.Database); showSpinner("Import failed!"); await Task.Delay(1000).ConfigureAwait(true); } From 7b0c1e34989f8f7c1c90d0306db20061aa331e6a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 11 Jul 2024 16:54:27 +0900 Subject: [PATCH 1460/2493] Also apply to timing textboxes --- osu.Game/Screens/Edit/Timing/GroupSection.cs | 3 ++- osu.Game/Screens/Edit/Timing/TimingSection.cs | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Timing/GroupSection.cs b/osu.Game/Screens/Edit/Timing/GroupSection.cs index 487a871881..d715c3ebc9 100644 --- a/osu.Game/Screens/Edit/Timing/GroupSection.cs +++ b/osu.Game/Screens/Edit/Timing/GroupSection.cs @@ -51,7 +51,8 @@ namespace osu.Game.Screens.Edit.Timing { textBox = new LabelledTextBox { - Label = "Time" + Label = "Time", + SelectAllOnFocus = true, }, button = new RoundedButton { diff --git a/osu.Game/Screens/Edit/Timing/TimingSection.cs b/osu.Game/Screens/Edit/Timing/TimingSection.cs index 2757753b07..838eb1f9fd 100644 --- a/osu.Game/Screens/Edit/Timing/TimingSection.cs +++ b/osu.Game/Screens/Edit/Timing/TimingSection.cs @@ -79,6 +79,7 @@ namespace osu.Game.Screens.Edit.Timing public BPMTextBox() { Label = "BPM"; + SelectAllOnFocus = true; OnCommit += (_, isNew) => { From 6801ccbbc51da07312bb228aa64c0eb87ff70d41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 11 Jul 2024 11:23:09 +0200 Subject: [PATCH 1461/2493] Fix editor UI remaining transparent when switching away from compose tab Could still happen if using the keyboard F-key shortcuts. In that case the composer becomes non-present, so its `Update()` can't really do anything. --- osu.Game/Screens/Edit/Editor.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index db94b08406..6be8dafa66 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -219,7 +219,7 @@ namespace osu.Game.Screens.Edit /// In "composer focus" mode the opacity of the aforementioned components is reduced so that the user can focus on the composer better. /// /// - /// The state of this bindable is controlled by . + /// The state of this bindable is controlled by when in mode. /// public Bindable ComposerFocusMode { get; } = new Bindable(); @@ -1018,6 +1018,9 @@ namespace osu.Game.Screens.Edit } finally { + if (Mode.Value != EditorScreenMode.Compose) + ComposerFocusMode.Value = false; + updateSampleDisabledState(); rebindClipboardBindables(); } From 7b541d378c5cb896ee0dc3bbdf2ec377b87587e7 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 11 Jul 2024 14:22:23 +0300 Subject: [PATCH 1462/2493] Revert some changes I can see `IsDisplayingToast` being removed and `IsPresent` becoming `=> Notifications.Any()` but I'll just leave this for another day. --- osu.Game/Overlays/NotificationOverlayToastTray.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/NotificationOverlayToastTray.cs b/osu.Game/Overlays/NotificationOverlayToastTray.cs index e019b31620..d2899f29b8 100644 --- a/osu.Game/Overlays/NotificationOverlayToastTray.cs +++ b/osu.Game/Overlays/NotificationOverlayToastTray.cs @@ -24,7 +24,7 @@ namespace osu.Game.Overlays /// public partial class NotificationOverlayToastTray : CompositeDrawable { - public override bool IsPresent => toastContentBackground.Height > 0 || Notifications.Any(); + public override bool IsPresent => toastContentBackground.Height > 0 || toastFlow.Count > 0; public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => toastFlow.ReceivePositionalInputAt(screenSpacePos); @@ -33,7 +33,7 @@ namespace osu.Game.Overlays /// public IEnumerable Notifications => toastFlow.Concat(InternalChildren.OfType()); - public bool IsDisplayingToasts => Notifications.Any(); + public bool IsDisplayingToasts => toastFlow.Count > 0; private FillFlowContainer toastFlow = null!; private BufferedContainer toastContentBackground = null!; From 3190f8bb7e69e28dcef6cccfa260ca8f6483c910 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 11 Jul 2024 14:29:30 +0300 Subject: [PATCH 1463/2493] Remove key arrow handling in `VolumeOverlay` --- osu.Game/Overlays/VolumeOverlay.cs | 24 ------------------------ 1 file changed, 24 deletions(-) diff --git a/osu.Game/Overlays/VolumeOverlay.cs b/osu.Game/Overlays/VolumeOverlay.cs index 5470c70400..9747a543fc 100644 --- a/osu.Game/Overlays/VolumeOverlay.cs +++ b/osu.Game/Overlays/VolumeOverlay.cs @@ -179,30 +179,6 @@ namespace osu.Game.Overlays return base.OnMouseMove(e); } - protected override bool OnKeyDown(KeyDownEvent e) - { - switch (e.Key) - { - case Key.Left: - Adjust(GlobalAction.PreviousVolumeMeter); - return true; - - case Key.Right: - Adjust(GlobalAction.NextVolumeMeter); - return true; - - case Key.Down: - Adjust(GlobalAction.DecreaseVolume); - return true; - - case Key.Up: - Adjust(GlobalAction.IncreaseVolume); - return true; - } - - return base.OnKeyDown(e); - } - protected override bool OnHover(HoverEvent e) { schedulePopOut(); From bd44c17079764184e9ebd591825ed536221b0acf Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 11 Jul 2024 14:29:56 +0300 Subject: [PATCH 1464/2493] Enable NRT in `VolumeOverlay` --- osu.Game/Overlays/VolumeOverlay.cs | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/osu.Game/Overlays/VolumeOverlay.cs b/osu.Game/Overlays/VolumeOverlay.cs index 9747a543fc..6f9861c703 100644 --- a/osu.Game/Overlays/VolumeOverlay.cs +++ b/osu.Game/Overlays/VolumeOverlay.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.Audio; using osu.Framework.Bindables; @@ -19,7 +17,6 @@ using osu.Game.Input.Bindings; using osu.Game.Overlays.Volume; using osuTK; using osuTK.Graphics; -using osuTK.Input; namespace osu.Game.Overlays { @@ -27,17 +24,17 @@ namespace osu.Game.Overlays { private const float offset = 10; - private VolumeMeter volumeMeterMaster; - private VolumeMeter volumeMeterEffect; - private VolumeMeter volumeMeterMusic; - private MuteButton muteButton; + private VolumeMeter volumeMeterMaster = null!; + private VolumeMeter volumeMeterEffect = null!; + private VolumeMeter volumeMeterMusic = null!; + private MuteButton muteButton = null!; + + private SelectionCycleFillFlowContainer volumeMeters = null!; private readonly BindableDouble muteAdjustment = new BindableDouble(); public Bindable IsMuted { get; } = new Bindable(); - private SelectionCycleFillFlowContainer volumeMeters; - [BackgroundDependencyLoader] private void load(AudioManager audio, OsuColour colours) { @@ -140,8 +137,6 @@ namespace osu.Game.Overlays return false; } - private ScheduledDelegate popOutDelegate; - public void FocusMasterVolume() { volumeMeters.Select(volumeMeterMaster); @@ -191,6 +186,8 @@ namespace osu.Game.Overlays base.OnHoverLost(e); } + private ScheduledDelegate? popOutDelegate; + private void schedulePopOut() { popOutDelegate?.Cancel(); From 37a296ba4c3808f8fdaf322ceba1387d9a05ebde Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 11 Jul 2024 13:22:36 +0200 Subject: [PATCH 1465/2493] Limit per-frame movement hitobject processing to stacking updates --- .../Beatmaps/OsuBeatmapProcessor.cs | 17 +++++++++++------ .../Edit/OsuSelectionHandler.cs | 9 +++++---- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapProcessor.cs b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapProcessor.cs index d335913586..0e77553177 100644 --- a/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapProcessor.cs +++ b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapProcessor.cs @@ -42,7 +42,12 @@ namespace osu.Game.Rulesets.Osu.Beatmaps { base.PostProcess(); - var hitObjects = Beatmap.HitObjects as List ?? Beatmap.HitObjects.OfType().ToList(); + ApplyStacking(Beatmap); + } + + internal static void ApplyStacking(IBeatmap beatmap) + { + var hitObjects = beatmap.HitObjects as List ?? beatmap.HitObjects.OfType().ToList(); if (hitObjects.Count > 0) { @@ -50,14 +55,14 @@ namespace osu.Game.Rulesets.Osu.Beatmaps foreach (var h in hitObjects) h.StackHeight = 0; - if (Beatmap.BeatmapInfo.BeatmapVersion >= 6) - applyStacking(Beatmap.BeatmapInfo, hitObjects, 0, hitObjects.Count - 1); + if (beatmap.BeatmapInfo.BeatmapVersion >= 6) + applyStacking(beatmap.BeatmapInfo, hitObjects, 0, hitObjects.Count - 1); else - applyStackingOld(Beatmap.BeatmapInfo, hitObjects); + applyStackingOld(beatmap.BeatmapInfo, hitObjects); } } - private void applyStacking(BeatmapInfo beatmapInfo, List hitObjects, int startIndex, int endIndex) + private static void applyStacking(BeatmapInfo beatmapInfo, List hitObjects, int startIndex, int endIndex) { ArgumentOutOfRangeException.ThrowIfGreaterThan(startIndex, endIndex); ArgumentOutOfRangeException.ThrowIfNegative(startIndex); @@ -209,7 +214,7 @@ namespace osu.Game.Rulesets.Osu.Beatmaps } } - private void applyStackingOld(BeatmapInfo beatmapInfo, List hitObjects) + private static void applyStackingOld(BeatmapInfo beatmapInfo, List hitObjects) { for (int i = 0; i < hitObjects.Count; i++) { diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs index 9b4b77b625..2ca7664d5d 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs @@ -13,6 +13,7 @@ using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; +using osu.Game.Rulesets.Osu.Beatmaps; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.UI; using osu.Game.Screens.Edit.Compose.Components; @@ -72,10 +73,10 @@ namespace osu.Game.Rulesets.Osu.Edit // but this will be corrected. moveSelectionInBounds(); - // update all of the objects in order to update stacking. - // in particular, this causes stacked objects to instantly unstack on drag. - foreach (var h in hitObjects) - EditorBeatmap.Update(h); + // manually update stacking. + // this intentionally bypasses the editor `UpdateState()` / beatmap processor flow for performance reasons, + // as the entire flow is too expensive to run on every movement. + Scheduler.AddOnce(OsuBeatmapProcessor.ApplyStacking, EditorBeatmap); return true; } From 669e945fc334c544274c22b63ebb856ed394b37a Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 11 Jul 2024 14:56:17 +0300 Subject: [PATCH 1466/2493] Fix `ModSelectOverlay` not hiding without a footer --- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index da93539679..4143a7fe8a 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -604,7 +604,13 @@ namespace osu.Game.Overlays.Mods return base.OnPressed(e); - void hideOverlay() => footer?.BackButton.TriggerClick(); + void hideOverlay() + { + if (footer != null) + footer.BackButton.TriggerClick(); + else + Hide(); + } } /// From 7a5624fd0efd1c422afc4595b284bf8af444df3e Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 11 Jul 2024 15:30:07 +0300 Subject: [PATCH 1467/2493] Add screen footer to `ScreenTestScene` --- .../Multiplayer/TestSceneMultiplayerMatchSubScreen.cs | 8 ++++---- osu.Game/Tests/Visual/ScreenTestScene.cs | 7 ++++++- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs index f9ef085838..e2593e68e5 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs @@ -312,14 +312,14 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("wait for join", () => RoomJoined); ClickButtonWhenEnabled(); - AddAssert("mod select shows unranked", () => screen.UserModsSelectOverlay.ChildrenOfType().Single().Ranked.Value == false); - AddAssert("score multiplier = 1.20", () => screen.UserModsSelectOverlay.ChildrenOfType().Single().ModMultiplier.Value, () => Is.EqualTo(1.2).Within(0.01)); + AddAssert("mod select shows unranked", () => this.ChildrenOfType().Single().Ranked.Value == false); + AddAssert("score multiplier = 1.20", () => this.ChildrenOfType().Single().ModMultiplier.Value, () => Is.EqualTo(1.2).Within(0.01)); AddStep("select flashlight", () => screen.UserModsSelectOverlay.ChildrenOfType().Single(m => m.Mod is ModFlashlight).TriggerClick()); - AddAssert("score multiplier = 1.35", () => screen.UserModsSelectOverlay.ChildrenOfType().Single().ModMultiplier.Value, () => Is.EqualTo(1.35).Within(0.01)); + AddAssert("score multiplier = 1.35", () => this.ChildrenOfType().Single().ModMultiplier.Value, () => Is.EqualTo(1.35).Within(0.01)); AddStep("change flashlight setting", () => ((OsuModFlashlight)screen.UserModsSelectOverlay.SelectedMods.Value.Single()).FollowDelay.Value = 1200); - AddAssert("score multiplier = 1.20", () => screen.UserModsSelectOverlay.ChildrenOfType().Single().ModMultiplier.Value, () => Is.EqualTo(1.2).Within(0.01)); + AddAssert("score multiplier = 1.20", () => this.ChildrenOfType().Single().ModMultiplier.Value, () => Is.EqualTo(1.2).Within(0.01)); } private partial class TestMultiplayerMatchSubScreen : MultiplayerMatchSubScreen diff --git a/osu.Game/Tests/Visual/ScreenTestScene.cs b/osu.Game/Tests/Visual/ScreenTestScene.cs index 3cca1e59cc..f780b1a8f8 100644 --- a/osu.Game/Tests/Visual/ScreenTestScene.cs +++ b/osu.Game/Tests/Visual/ScreenTestScene.cs @@ -12,6 +12,7 @@ using osu.Framework.Testing; using osu.Game.Graphics; using osu.Game.Overlays; using osu.Game.Screens; +using osu.Game.Screens.Footer; namespace osu.Game.Tests.Visual { @@ -30,6 +31,9 @@ namespace osu.Game.Tests.Visual [Cached(typeof(IDialogOverlay))] protected DialogOverlay DialogOverlay { get; private set; } + [Cached] + private ScreenFooter footer; + protected ScreenTestScene() { base.Content.AddRange(new Drawable[] @@ -44,7 +48,8 @@ namespace osu.Game.Tests.Visual { RelativeSizeAxes = Axes.Both, Child = DialogOverlay = new DialogOverlay() - } + }, + footer = new ScreenFooter(), }); Stack.ScreenPushed += (_, newScreen) => Logger.Log($"{nameof(ScreenTestScene)} screen changed → {newScreen}"); From d4a4a059d4a68affe7563d63ede5c9112a919482 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 11 Jul 2024 15:31:02 +0300 Subject: [PATCH 1468/2493] Fix footer content not accessible by overlay when overriden by a subclass --- osu.Game/Overlays/FirstRunSetupOverlay.cs | 16 ++++++++-------- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 8 ++++---- .../Overlays/Mods/ShearedOverlayContainer.cs | 6 +++++- osu.Game/Screens/Footer/ScreenFooter.cs | 6 ++++-- .../Screens/OnlinePlay/FreeModSelectOverlay.cs | 10 +++++----- 5 files changed, 26 insertions(+), 20 deletions(-) diff --git a/osu.Game/Overlays/FirstRunSetupOverlay.cs b/osu.Game/Overlays/FirstRunSetupOverlay.cs index 2c8ceba82c..1a302cf51d 100644 --- a/osu.Game/Overlays/FirstRunSetupOverlay.cs +++ b/osu.Game/Overlays/FirstRunSetupOverlay.cs @@ -45,7 +45,7 @@ namespace osu.Game.Overlays private ScreenStack? stack; - public ShearedButton? NextButton => currentFooterContent?.NextButton; + public ShearedButton? NextButton => DisplayedFooterContent?.NextButton; private readonly Bindable showFirstRunSetup = new Bindable(); @@ -148,17 +148,17 @@ namespace osu.Game.Overlays [Resolved] private ScreenFooter footer { get; set; } = null!; - private FirstRunSetupFooterContent? currentFooterContent; + public new FirstRunSetupFooterContent? DisplayedFooterContent => base.DisplayedFooterContent as FirstRunSetupFooterContent; public override VisibilityContainer CreateFooterContent() { - currentFooterContent = new FirstRunSetupFooterContent + var footerContent = new FirstRunSetupFooterContent { ShowNextStep = showNextStep, }; - currentFooterContent.OnLoadComplete += _ => updateButtons(); - return currentFooterContent; + footerContent.OnLoadComplete += _ => updateButtons(); + return footerContent; } public override bool OnBackButton() @@ -182,7 +182,7 @@ namespace osu.Game.Overlays switch (e.Action) { case GlobalAction.Select: - currentFooterContent?.NextButton.TriggerClick(); + DisplayedFooterContent?.NextButton.TriggerClick(); return true; case GlobalAction.Back: @@ -287,9 +287,9 @@ namespace osu.Game.Overlays updateButtons(); } - private void updateButtons() => currentFooterContent?.UpdateButtons(currentStepIndex, steps); + private void updateButtons() => DisplayedFooterContent?.UpdateButtons(currentStepIndex, steps); - private partial class FirstRunSetupFooterContent : VisibilityContainer + public partial class FirstRunSetupFooterContent : VisibilityContainer { public ShearedButton NextButton { get; private set; } = null!; diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index 4143a7fe8a..7469590895 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -254,9 +254,9 @@ namespace osu.Game.Overlays.Mods }); } - private ModSelectFooterContent? currentFooterContent; + public new ModSelectFooterContent? DisplayedFooterContent => base.DisplayedFooterContent as ModSelectFooterContent; - public override VisibilityContainer CreateFooterContent() => currentFooterContent = new ModSelectFooterContent(this) + public override VisibilityContainer CreateFooterContent() => new ModSelectFooterContent(this) { Beatmap = { BindTarget = Beatmap }, ActiveMods = { BindTarget = ActiveMods }, @@ -270,7 +270,7 @@ namespace osu.Game.Overlays.Mods base.Update(); SearchTextBox.PlaceholderText = SearchTextBox.HasFocus ? input_search_placeholder : tab_to_search_placeholder; - aboveColumnsContent.Padding = aboveColumnsContent.Padding with { Bottom = currentFooterContent?.DisplaysStackedVertically == true ? 75f : 15f }; + aboveColumnsContent.Padding = aboveColumnsContent.Padding with { Bottom = DisplayedFooterContent?.DisplaysStackedVertically == true ? 75f : 15f }; } /// @@ -573,7 +573,7 @@ namespace osu.Game.Overlays.Mods { if (!SearchTextBox.HasFocus && !customisationPanel.Expanded.Value) { - currentFooterContent?.DeselectAllModsButton?.TriggerClick(); + DisplayedFooterContent?.DeselectAllModsButton?.TriggerClick(); return true; } diff --git a/osu.Game/Overlays/Mods/ShearedOverlayContainer.cs b/osu.Game/Overlays/Mods/ShearedOverlayContainer.cs index 8c6b9e805b..dfa49f3779 100644 --- a/osu.Game/Overlays/Mods/ShearedOverlayContainer.cs +++ b/osu.Game/Overlays/Mods/ShearedOverlayContainer.cs @@ -102,6 +102,8 @@ namespace osu.Game.Overlays.Mods }; } + public VisibilityContainer? DisplayedFooterContent { get; private set; } + /// /// Creates content to be displayed on the game-wide footer. /// @@ -137,7 +139,8 @@ namespace osu.Game.Overlays.Mods if (footer != null) { - activeOverlayRegistration = footer.RegisterActiveOverlayContainer(this); + activeOverlayRegistration = footer.RegisterActiveOverlayContainer(this, out var footerContent); + DisplayedFooterContent = footerContent; if (footer.State.Value == Visibility.Hidden) { @@ -160,6 +163,7 @@ namespace osu.Game.Overlays.Mods { activeOverlayRegistration?.Dispose(); activeOverlayRegistration = null; + DisplayedFooterContent = null; if (hideFooterOnPopOut) { diff --git a/osu.Game/Screens/Footer/ScreenFooter.cs b/osu.Game/Screens/Footer/ScreenFooter.cs index f8d222e510..4c020fc95e 100644 --- a/osu.Game/Screens/Footer/ScreenFooter.cs +++ b/osu.Game/Screens/Footer/ScreenFooter.cs @@ -190,7 +190,7 @@ namespace osu.Game.Screens.Footer private readonly List temporarilyHiddenButtons = new List(); - public IDisposable RegisterActiveOverlayContainer(ShearedOverlayContainer overlay) + public IDisposable RegisterActiveOverlayContainer(ShearedOverlayContainer overlay, out VisibilityContainer? footerContent) { if (activeOverlay != null) { @@ -219,7 +219,9 @@ namespace osu.Game.Screens.Footer updateColourScheme(overlay.ColourProvider.ColourScheme); - var content = overlay.CreateFooterContent() ?? Empty(); + footerContent = overlay.CreateFooterContent(); + + var content = footerContent ?? Empty(); Add(contentContainer = new Container { diff --git a/osu.Game/Screens/OnlinePlay/FreeModSelectOverlay.cs b/osu.Game/Screens/OnlinePlay/FreeModSelectOverlay.cs index 2b3ab94916..8937abb775 100644 --- a/osu.Game/Screens/OnlinePlay/FreeModSelectOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/FreeModSelectOverlay.cs @@ -23,9 +23,7 @@ namespace osu.Game.Screens.OnlinePlay set => base.IsValidMod = m => m.UserPlayable && value.Invoke(m); } - private FreeModSelectFooterContent? currentFooterContent; - - protected override SelectAllModsButton? SelectAllModsButton => currentFooterContent?.SelectAllModsButton; + protected override SelectAllModsButton? SelectAllModsButton => DisplayedFooterContent?.SelectAllModsButton; public FreeModSelectOverlay() : base(OverlayColourScheme.Plum) @@ -35,13 +33,15 @@ namespace osu.Game.Screens.OnlinePlay protected override ModColumn CreateModColumn(ModType modType) => new ModColumn(modType, true); - public override VisibilityContainer CreateFooterContent() => currentFooterContent = new FreeModSelectFooterContent(this) + public new FreeModSelectFooterContent? DisplayedFooterContent => base.DisplayedFooterContent as FreeModSelectFooterContent; + + public override VisibilityContainer CreateFooterContent() => new FreeModSelectFooterContent(this) { Beatmap = { BindTarget = Beatmap }, ActiveMods = { BindTarget = ActiveMods }, }; - private partial class FreeModSelectFooterContent : ModSelectFooterContent + public partial class FreeModSelectFooterContent : ModSelectFooterContent { private readonly FreeModSelectOverlay overlay; From 3ea0f58daabed92d8e86191379f7d417f9176738 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 11 Jul 2024 15:31:23 +0300 Subject: [PATCH 1469/2493] Update `TestSceneFreeModSelectOverlay` to work again --- .../TestSceneFreeModSelectOverlay.cs | 31 +++++++++++++------ 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneFreeModSelectOverlay.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneFreeModSelectOverlay.cs index 938ab1e9f4..497faa28d0 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneFreeModSelectOverlay.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneFreeModSelectOverlay.cs @@ -3,6 +3,7 @@ #nullable disable +using System; using System.Collections.Generic; using System.Linq; using NUnit.Framework; @@ -16,6 +17,7 @@ using osu.Framework.Testing; using osu.Game.Overlays.Mods; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Mods; +using osu.Game.Screens.Footer; using osu.Game.Screens.OnlinePlay; using osu.Game.Utils; using osuTK.Input; @@ -26,6 +28,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { private FreeModSelectOverlay freeModSelectOverlay; private FooterButtonFreeMods footerButtonFreeMods; + private ScreenFooter footer; private readonly Bindable>> availableMods = new Bindable>>(); [BackgroundDependencyLoader] @@ -127,7 +130,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { createFreeModSelect(); - AddAssert("overlay select all button enabled", () => freeModSelectOverlay.ChildrenOfType().Single().Enabled.Value); + AddAssert("overlay select all button enabled", () => this.ChildrenOfType().Single().Enabled.Value); AddAssert("footer button displays off", () => footerButtonFreeMods.ChildrenOfType().Any(t => t.Text == "off")); AddStep("click footer select all button", () => @@ -150,19 +153,27 @@ namespace osu.Game.Tests.Visual.Multiplayer private void createFreeModSelect() { - AddStep("create free mod select screen", () => Children = new Drawable[] + AddStep("create free mod select screen", () => Child = new DependencyProvidingContainer { - freeModSelectOverlay = new FreeModSelectOverlay + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] { - State = { Value = Visibility.Visible } - }, - footerButtonFreeMods = new FooterButtonFreeMods(freeModSelectOverlay) - { - Anchor = Anchor.BottomRight, - Origin = Anchor.BottomRight, - Current = { BindTarget = freeModSelectOverlay.SelectedMods }, + freeModSelectOverlay = new FreeModSelectOverlay + { + State = { Value = Visibility.Visible } + }, + footerButtonFreeMods = new FooterButtonFreeMods(freeModSelectOverlay) + { + Anchor = Anchor.BottomRight, + Origin = Anchor.BottomRight, + Y = -ScreenFooter.HEIGHT, + Current = { BindTarget = freeModSelectOverlay.SelectedMods }, + }, + footer = new ScreenFooter(), }, + CachedDependencies = new (Type, object)[] { (typeof(ScreenFooter), footer) }, }); + AddUntilStep("all column content loaded", () => freeModSelectOverlay.ChildrenOfType().Any() && freeModSelectOverlay.ChildrenOfType().All(column => column.IsLoaded && column.ItemsLoaded)); From fb4f620c90409c19be1ae49893813f8bd2b95ae2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 12 Jul 2024 12:34:43 +0900 Subject: [PATCH 1470/2493] Add back BPM and adjust sizing of bottom bar a bit more --- osu.Game/Screens/Edit/BottomBar.cs | 4 +- .../Edit/Components/TimeInfoContainer.cs | 46 ++++++++++++++++--- 2 files changed, 42 insertions(+), 8 deletions(-) diff --git a/osu.Game/Screens/Edit/BottomBar.cs b/osu.Game/Screens/Edit/BottomBar.cs index a11f40c8fd..514a06f1c5 100644 --- a/osu.Game/Screens/Edit/BottomBar.cs +++ b/osu.Game/Screens/Edit/BottomBar.cs @@ -31,7 +31,7 @@ namespace osu.Game.Screens.Edit RelativeSizeAxes = Axes.X; - Height = 40; + Height = 50; Masking = true; EdgeEffect = new EdgeEffectParameters @@ -48,7 +48,7 @@ namespace osu.Game.Screens.Edit RelativeSizeAxes = Axes.Both, ColumnDimensions = new[] { - new Dimension(GridSizeMode.Absolute, 170), + new Dimension(GridSizeMode.Absolute, 150), new Dimension(), new Dimension(GridSizeMode.Absolute, 220), new Dimension(GridSizeMode.Absolute, HitObjectComposer.TOOLBOX_CONTRACTED_SIZE_RIGHT), diff --git a/osu.Game/Screens/Edit/Components/TimeInfoContainer.cs b/osu.Game/Screens/Edit/Components/TimeInfoContainer.cs index 37facb3b95..8f2a3d49ca 100644 --- a/osu.Game/Screens/Edit/Components/TimeInfoContainer.cs +++ b/osu.Game/Screens/Edit/Components/TimeInfoContainer.cs @@ -17,6 +17,8 @@ namespace osu.Game.Screens.Edit.Components { public partial class TimeInfoContainer : BottomBarContainer { + private OsuSpriteText bpm = null!; + [Resolved] private EditorBeatmap editorBeatmap { get; set; } = null!; @@ -24,16 +26,38 @@ namespace osu.Game.Screens.Edit.Components private EditorClock editorClock { get; set; } = null!; [BackgroundDependencyLoader] - private void load(OverlayColourProvider colourProvider) + private void load(OsuColour colours, OverlayColourProvider colourProvider) { Background.Colour = colourProvider.Background5; Children = new Drawable[] { new TimestampControl(), + bpm = new OsuSpriteText + { + Colour = colours.Orange1, + Anchor = Anchor.CentreLeft, + Font = OsuFont.Torus.With(size: 14, weight: FontWeight.SemiBold), + Position = new Vector2(2, 4), + } }; } + private double? lastBPM; + + protected override void Update() + { + base.Update(); + + double newBPM = editorBeatmap.ControlPointInfo.TimingPointAt(editorClock.CurrentTime).BPM; + + if (lastBPM != newBPM) + { + lastBPM = newBPM; + bpm.Text = @$"{newBPM:0} BPM"; + } + } + private partial class TimestampControl : OsuClickableContainer { private Container hoverLayer = null!; @@ -63,7 +87,8 @@ namespace osu.Game.Screens.Edit.Components RelativeSizeAxes = Axes.Both, Padding = new MarginPadding { - Top = 5, + Top = 4, + Bottom = 1, Horizontal = -2 }, Child = new Container @@ -83,12 +108,13 @@ namespace osu.Game.Screens.Edit.Components Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, Spacing = new Vector2(-2, 0), - Font = OsuFont.Torus.With(size: 36, fixedWidth: true, weight: FontWeight.Light), + Font = OsuFont.Torus.With(size: 32, fixedWidth: true, weight: FontWeight.Light), }, - inputTextBox = new OsuTextBox + inputTextBox = new TimestampTextBox { - Width = 150, - Height = 36, + Position = new Vector2(-2, 4), + Width = 128, + Height = 26, Alpha = 0, CommitOnFocusLost = true, }, @@ -136,6 +162,14 @@ namespace osu.Game.Screens.Edit.Components showingHoverLayer = shouldShowHoverLayer; } } + + private partial class TimestampTextBox : OsuTextBox + { + public TimestampTextBox() + { + TextContainer.Height = 0.8f; + } + } } } } From e6b7d2530eef28859c4bde9fde5dc3e2c31afea7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 12 Jul 2024 13:39:40 +0900 Subject: [PATCH 1471/2493] Change red shade for timing control points --- osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs index 3360b1d1fa..db1d440f18 100644 --- a/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs +++ b/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs @@ -26,7 +26,7 @@ namespace osu.Game.Beatmaps.ControlPoints /// private const double default_beat_length = 60000.0 / 60.0; - public override Color4 GetRepresentingColour(OsuColour colours) => colours.Red1; + public override Color4 GetRepresentingColour(OsuColour colours) => colours.Red2; public static readonly TimingControlPoint DEFAULT = new TimingControlPoint { From 685b19a5714435b3a9f9f3442504b74002a65122 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 12 Jul 2024 13:57:36 +0900 Subject: [PATCH 1472/2493] Adjust various visuals of summary timeline in a hope for user acceptance .. with a somewhat appealing design. --- .../Timelines/Summary/Parts/BreakPart.cs | 2 +- .../Parts/ControlPointVisualisation.cs | 4 +- .../Summary/Parts/EffectPointVisualisation.cs | 6 +-- .../Summary/Parts/GroupVisualisation.cs | 5 ++- .../Summary/Parts/PreviewTimePart.cs | 2 +- .../Timelines/Summary/SummaryTimeline.cs | 42 +++++++++---------- .../Visualisations/PointVisualisation.cs | 5 ++- 7 files changed, 36 insertions(+), 30 deletions(-) diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/BreakPart.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/BreakPart.cs index 100f37fd27..ef1a825969 100644 --- a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/BreakPart.cs +++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/BreakPart.cs @@ -64,7 +64,7 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts RelativeSizeAxes = Axes.Both; InternalChild = new Circle { RelativeSizeAxes = Axes.Both }; - Colour = colours.Gray7; + Colour = colours.Gray6; } } } diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/ControlPointVisualisation.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/ControlPointVisualisation.cs index 47169481e2..1df128461e 100644 --- a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/ControlPointVisualisation.cs +++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/ControlPointVisualisation.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; +using osu.Framework.Graphics; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Graphics; using osu.Game.Screens.Edit.Components.Timelines.Summary.Visualisations; @@ -15,7 +16,8 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts public ControlPointVisualisation(ControlPoint point) { Point = point; - Width = 2; + Alpha = 0.3f; + Blending = BlendingParameters.Additive; } [BackgroundDependencyLoader] diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/EffectPointVisualisation.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/EffectPointVisualisation.cs index a3885bc2cc..41f4b3a365 100644 --- a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/EffectPointVisualisation.cs +++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/EffectPointVisualisation.cs @@ -3,7 +3,6 @@ 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; @@ -96,10 +95,9 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts RelativeSizeAxes = Axes.Both, Anchor = Anchor.BottomLeft, Origin = Anchor.CentreLeft, - Width = 1, - Height = 0.75f, + Height = 0.4f, Depth = float.MaxValue, - Colour = effect.GetRepresentingColour(colours).Darken(0.5f), + Colour = effect.GetRepresentingColour(colours), }); } } diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/GroupVisualisation.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/GroupVisualisation.cs index 2c806be162..e01900b129 100644 --- a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/GroupVisualisation.cs +++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/GroupVisualisation.cs @@ -39,7 +39,10 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts switch (point) { case TimingControlPoint: - AddInternal(new ControlPointVisualisation(point)); + AddInternal(new ControlPointVisualisation(point) + { + Y = -0.4f, + }); break; case EffectControlPoint effect: diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/PreviewTimePart.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/PreviewTimePart.cs index 407173034e..3a63d1e9b3 100644 --- a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/PreviewTimePart.cs +++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/PreviewTimePart.cs @@ -32,7 +32,7 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts public PreviewTimeVisualisation(double time) : base(time) { - Width = 2; + Alpha = 0.8f; } [BackgroundDependencyLoader] diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/SummaryTimeline.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/SummaryTimeline.cs index 92012936bc..49110ccee3 100644 --- a/osu.Game/Screens/Edit/Components/Timelines/Summary/SummaryTimeline.cs +++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/SummaryTimeline.cs @@ -23,27 +23,6 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary Children = new Drawable[] { - new ControlPointPart - { - Anchor = Anchor.Centre, - Origin = Anchor.BottomCentre, - RelativeSizeAxes = Axes.Both, - Height = 0.35f - }, - new BookmarkPart - { - Anchor = Anchor.Centre, - Origin = Anchor.TopCentre, - RelativeSizeAxes = Axes.Both, - Height = 0.35f - }, - new PreviewTimePart - { - Anchor = Anchor.Centre, - Origin = Anchor.TopCentre, - RelativeSizeAxes = Axes.Both, - Height = 0.35f - }, new Container { Name = "centre line", @@ -73,6 +52,27 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary }, } }, + new PreviewTimePart + { + Anchor = Anchor.Centre, + Origin = Anchor.TopCentre, + RelativeSizeAxes = Axes.Both, + Height = 0.4f, + }, + new ControlPointPart + { + Anchor = Anchor.Centre, + Origin = Anchor.BottomCentre, + RelativeSizeAxes = Axes.Both, + Height = 0.4f + }, + new BookmarkPart + { + Anchor = Anchor.Centre, + Origin = Anchor.TopCentre, + RelativeSizeAxes = Axes.Both, + Height = 0.35f + }, new BreakPart { Anchor = Anchor.Centre, diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/Visualisations/PointVisualisation.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/Visualisations/PointVisualisation.cs index 3f0c125ada..571494860f 100644 --- a/osu.Game/Screens/Edit/Components/Timelines/Summary/Visualisations/PointVisualisation.cs +++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/Visualisations/PointVisualisation.cs @@ -11,12 +11,15 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Visualisations /// public partial class PointVisualisation : Circle { + public readonly double StartTime; + public const float MAX_WIDTH = 4; public PointVisualisation(double startTime) : this() { X = (float)startTime; + StartTime = startTime; } public PointVisualisation() @@ -28,7 +31,7 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Visualisations Origin = Anchor.Centre; Width = MAX_WIDTH; - Height = 0.75f; + Height = 0.4f; } } } From 9a7a0cdb341f507ea3ed0f4f9e25f5a01aef6fbf Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 12 Jul 2024 14:29:18 +0900 Subject: [PATCH 1473/2493] Adjust timeline ticks to add a bit more body back --- .../Components/Timeline/TimelineTickDisplay.cs | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs index 8de5087850..1f357283bd 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs @@ -147,20 +147,12 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline var line = getNextUsableLine(); line.X = xPos; - line.Width = PointVisualisation.MAX_WIDTH * size.X; - if (showTimingChanges.Value) - { - line.Anchor = Anchor.BottomLeft; - line.Origin = Anchor.BottomCentre; - line.Height = 0.7f + size.Y * 0.28f; - } - else - { - line.Anchor = Anchor.CentreLeft; - line.Origin = Anchor.Centre; - line.Height = 0.92f + size.Y * 0.07f; - } + line.Anchor = Anchor.CentreLeft; + line.Origin = Anchor.Centre; + + line.Height = 0.6f + size.Y * 0.4f; + line.Width = PointVisualisation.MAX_WIDTH * (0.6f + 0.4f * size.X); line.Colour = colour; } From 2ad2ae0c16a969bdb7337b85087d70d54f94f449 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 12 Jul 2024 14:34:01 +0900 Subject: [PATCH 1474/2493] Add slight offset for timeline BPM display to avoid overlaps --- .../Compose/Components/Timeline/TimelineControlPointGroup.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointGroup.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointGroup.cs index c1b6069523..98556fda45 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointGroup.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointGroup.cs @@ -24,7 +24,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline Origin = Anchor.TopLeft; - X = (float)group.Time; + // offset visually to avoid overlapping timeline tick display. + X = (float)group.Time + 6; } protected override void LoadComplete() From f65ab6736db5fd5854224ee7a00e6a383e025b1f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 12 Jul 2024 15:41:51 +0900 Subject: [PATCH 1475/2493] Adjust breaks in timeline to be centered with waveform / hitobjects --- .../Components/Timeline/TimelineBreak.cs | 32 +++---------------- .../Screens/Edit/Compose/ComposeScreen.cs | 8 ++++- 2 files changed, 12 insertions(+), 28 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBreak.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBreak.cs index 29030099c8..721af97674 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBreak.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBreak.cs @@ -29,11 +29,6 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline public Action? OnDeleted { get; init; } - private Box background = null!; - - [Resolved] - private OsuColour colours { get; set; } = null!; - public TimelineBreak(BreakPeriod b) { Break.Value = b; @@ -53,11 +48,11 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline { RelativeSizeAxes = Axes.Both, Padding = new MarginPadding { Horizontal = 5 }, - Child = background = new Box + Child = new Box { RelativeSizeAxes = Axes.Both, Colour = colours.Gray5, - Alpha = 0.7f, + Alpha = 0.9f, }, }, new DragHandle(isStartHandle: true) @@ -88,23 +83,6 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline }, true); } - protected override bool OnHover(HoverEvent e) - { - updateState(); - return true; - } - - protected override void OnHoverLost(HoverLostEvent e) - { - updateState(); - base.OnHoverLost(e); - } - - private void updateState() - { - background.FadeColour(IsHovered ? colours.Gray6 : colours.Gray5, 400, Easing.OutQuint); - } - public MenuItem[] ContextMenuItems => new MenuItem[] { new OsuMenuItem(CommonStrings.ButtonsDelete, MenuItemType.Destructive, () => OnDeleted?.Invoke(Break.Value)), @@ -159,7 +137,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline Anchor = Anchor, Origin = Anchor, RelativeSizeAxes = Axes.Y, - CornerRadius = 5, + CornerRadius = 4, Masking = true, Child = new Box { @@ -249,8 +227,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline if (active) colour = colour.Lighten(0.3f); - this.FadeColour(colour, 400, Easing.OutQuint); - handle.ResizeWidthTo(active ? 20 : 10, 400, Easing.OutElasticHalf); + handle.FadeColour(colour, 400, Easing.OutQuint); + handle.ResizeWidthTo(active ? 10 : 8, 400, Easing.OutElasticHalf); } } } diff --git a/osu.Game/Screens/Edit/Compose/ComposeScreen.cs b/osu.Game/Screens/Edit/Compose/ComposeScreen.cs index 9b945e1d6d..cc33840929 100644 --- a/osu.Game/Screens/Edit/Compose/ComposeScreen.cs +++ b/osu.Game/Screens/Edit/Compose/ComposeScreen.cs @@ -75,7 +75,13 @@ namespace osu.Game.Screens.Edit.Compose Children = new Drawable[] { new TimelineBlueprintContainer(composer), - new TimelineBreakDisplay { RelativeSizeAxes = Axes.Both, }, + new TimelineBreakDisplay + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Height = 0.75f, + }, } }); } From ca2fc7295967bec77de252d39091e04acdccadd9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 12 Jul 2024 16:26:38 +0900 Subject: [PATCH 1476/2493] Adjust timeline centre marker visuals and bring in front of ticks --- .../Compose/Components/Timeline/CentreMarker.cs | 17 ++++++++++------- .../Compose/Components/Timeline/Timeline.cs | 2 +- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/CentreMarker.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/CentreMarker.cs index be1888684e..e3542cbf9b 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/CentreMarker.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/CentreMarker.cs @@ -3,10 +3,12 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Game.Overlays; using osuTK; +using osuTK.Graphics; namespace osu.Game.Screens.Edit.Compose.Components.Timeline { @@ -23,7 +25,11 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline Anchor = Anchor.TopCentre; Origin = Anchor.TopCentre; + } + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colours) + { InternalChildren = new Drawable[] { new Box @@ -32,21 +38,18 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline Origin = Anchor.Centre, RelativeSizeAxes = Axes.Y, Width = bar_width, + Blending = BlendingParameters.Additive, + Colour = ColourInfo.GradientVertical(colours.Colour2, Color4.Black), }, new Triangle { Anchor = Anchor.TopCentre, Origin = Anchor.BottomCentre, Size = new Vector2(triangle_width, triangle_width * 0.8f), - Scale = new Vector2(1, -1) + Scale = new Vector2(1, -1), + Colour = colours.Colour2, }, }; } - - [BackgroundDependencyLoader] - private void load(OverlayColourProvider colours) - { - Colour = colours.Highlight1; - } } } diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs index 05e44d4737..6ce5c06801 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs @@ -125,8 +125,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline MidColour = colours.BlueDark, HighColour = colours.BlueDarker, }, - centreMarker.CreateProxy(), ticks.CreateProxy(), + centreMarker.CreateProxy(), new Box { Name = "zero marker", From 43addc84003c32caa2b5417143f5843dace4cba9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 12 Jul 2024 16:28:31 +0900 Subject: [PATCH 1477/2493] Fix test regression --- osu.Game.Tests/Visual/Editing/TestScenePreviewTime.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Editing/TestScenePreviewTime.cs b/osu.Game.Tests/Visual/Editing/TestScenePreviewTime.cs index 3319788c8a..ad8c29d180 100644 --- a/osu.Game.Tests/Visual/Editing/TestScenePreviewTime.cs +++ b/osu.Game.Tests/Visual/Editing/TestScenePreviewTime.cs @@ -29,7 +29,7 @@ namespace osu.Game.Tests.Visual.Editing AddStep("set preview time to -1", () => EditorBeatmap.PreviewTime.Value = -1); AddAssert("preview time line should not show", () => !Editor.ChildrenOfType().Single().Children.Any()); AddStep("set preview time to 1000", () => EditorBeatmap.PreviewTime.Value = 1000); - AddAssert("preview time line should show", () => Editor.ChildrenOfType().Single().Children.Single().Alpha == 1); + AddAssert("preview time line should show", () => Editor.ChildrenOfType().Single().Children.Single().Alpha, () => Is.GreaterThan(0)); } } } From 275e7aa451f5f9a0d8de2889a307903fa653124e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 12 Jul 2024 16:26:38 +0900 Subject: [PATCH 1478/2493] Adjust timeline centre marker visuals and bring in front of ticks --- .../Screens/Edit/Compose/Components/Timeline/CentreMarker.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/CentreMarker.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/CentreMarker.cs index e3542cbf9b..3d76656a14 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/CentreMarker.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/CentreMarker.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; +using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; From 24547226019dc7a080f317b436a98c5765f82bb0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 12 Jul 2024 14:18:41 +0900 Subject: [PATCH 1479/2493] Add tooltips to summary timeline display --- .../Timelines/Summary/Parts/BreakPart.cs | 10 +++++++- .../Parts/ControlPointVisualisation.cs | 24 ++++++++++++++++++- .../Summary/Parts/EffectPointVisualisation.cs | 19 ++++++++++++++- .../Summary/Parts/PreviewTimePart.cs | 7 +++++- .../Visualisations/PointVisualisation.cs | 10 +++----- .../Timeline/TimelineTickDisplay.cs | 2 +- 6 files changed, 60 insertions(+), 12 deletions(-) diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/BreakPart.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/BreakPart.cs index ef1a825969..c20738bbd9 100644 --- a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/BreakPart.cs +++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/BreakPart.cs @@ -4,9 +4,12 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Pooling; using osu.Framework.Graphics.Shapes; +using osu.Framework.Localisation; using osu.Game.Beatmaps.Timing; +using osu.Game.Extensions; using osu.Game.Graphics; namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts @@ -46,12 +49,15 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts }, true); } - private partial class BreakVisualisation : PoolableDrawable + private partial class BreakVisualisation : PoolableDrawable, IHasTooltip { + private BreakPeriod breakPeriod; + public BreakPeriod BreakPeriod { set { + breakPeriod = value; X = (float)value.StartTime; Width = (float)value.Duration; } @@ -66,6 +72,8 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts InternalChild = new Circle { RelativeSizeAxes = Axes.Both }; Colour = colours.Gray6; } + + public LocalisableString TooltipText => $"{breakPeriod.StartTime.ToEditorFormattedString()} - {breakPeriod.EndTime.ToEditorFormattedString()} break time"; } } } diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/ControlPointVisualisation.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/ControlPointVisualisation.cs index 1df128461e..977aadd6c3 100644 --- a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/ControlPointVisualisation.cs +++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/ControlPointVisualisation.cs @@ -2,18 +2,23 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; +using osu.Framework.Extensions; using osu.Framework.Graphics; +using osu.Framework.Graphics.Cursor; +using osu.Framework.Localisation; using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Extensions; using osu.Game.Graphics; using osu.Game.Screens.Edit.Components.Timelines.Summary.Visualisations; namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts { - public partial class ControlPointVisualisation : PointVisualisation, IControlPointVisualisation + public partial class ControlPointVisualisation : PointVisualisation, IControlPointVisualisation, IHasTooltip { protected readonly ControlPoint Point; public ControlPointVisualisation(ControlPoint point) + : base(point.Time) { Point = point; Alpha = 0.3f; @@ -27,5 +32,22 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts } public bool IsVisuallyRedundant(ControlPoint other) => other.GetType() == Point.GetType(); + + public LocalisableString TooltipText + { + get + { + switch (Point) + { + case EffectControlPoint effect: + return $"{StartTime.ToEditorFormattedString()} effect [{effect.ScrollSpeed:N2}x scroll{(effect.KiaiMode ? " kiai" : "")}]"; + + case TimingControlPoint timing: + return $"{StartTime.ToEditorFormattedString()} timing [{timing.BPM:N2} bpm {timing.TimeSignature.GetDescription()}]"; + } + + return string.Empty; + } + } } } diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/EffectPointVisualisation.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/EffectPointVisualisation.cs index 41f4b3a365..f1e2b52ad8 100644 --- a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/EffectPointVisualisation.cs +++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/EffectPointVisualisation.cs @@ -5,8 +5,11 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Shapes; +using osu.Framework.Localisation; using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Extensions; using osu.Game.Graphics; namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts @@ -90,7 +93,7 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts Width = (float)(nextControlPoint.Time - effect.Time); - AddInternal(new Circle + AddInternal(new KiaiVisualisation(effect.Time, nextControlPoint.Time) { RelativeSizeAxes = Axes.Both, Anchor = Anchor.BottomLeft, @@ -102,6 +105,20 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts } } + private partial class KiaiVisualisation : Circle, IHasTooltip + { + private readonly double startTime; + private readonly double endTime; + + public KiaiVisualisation(double startTime, double endTime) + { + this.startTime = startTime; + this.endTime = endTime; + } + + public LocalisableString TooltipText => $"{startTime.ToEditorFormattedString()} - {endTime.ToEditorFormattedString()} kiai time"; + } + // kiai sections display duration, so are required to be visualised. public bool IsVisuallyRedundant(ControlPoint other) => other is EffectControlPoint otherEffect && effect.KiaiMode == otherEffect.KiaiMode; } diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/PreviewTimePart.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/PreviewTimePart.cs index 3a63d1e9b3..67bb1ef500 100644 --- a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/PreviewTimePart.cs +++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/PreviewTimePart.cs @@ -3,6 +3,9 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Graphics.Cursor; +using osu.Framework.Localisation; +using osu.Game.Extensions; using osu.Game.Graphics; using osu.Game.Screens.Edit.Components.Timelines.Summary.Visualisations; @@ -27,7 +30,7 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts }, true); } - private partial class PreviewTimeVisualisation : PointVisualisation + private partial class PreviewTimeVisualisation : PointVisualisation, IHasTooltip { public PreviewTimeVisualisation(double time) : base(time) @@ -37,6 +40,8 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts [BackgroundDependencyLoader] private void load(OsuColour colours) => Colour = colours.Green1; + + public LocalisableString TooltipText => $"{StartTime.ToEditorFormattedString()} preview time"; } } } diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/Visualisations/PointVisualisation.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/Visualisations/PointVisualisation.cs index 571494860f..9c16f457f7 100644 --- a/osu.Game/Screens/Edit/Components/Timelines/Summary/Visualisations/PointVisualisation.cs +++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/Visualisations/PointVisualisation.cs @@ -16,13 +16,6 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Visualisations public const float MAX_WIDTH = 4; public PointVisualisation(double startTime) - : this() - { - X = (float)startTime; - StartTime = startTime; - } - - public PointVisualisation() { RelativePositionAxes = Axes.Both; RelativeSizeAxes = Axes.Y; @@ -32,6 +25,9 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Visualisations Width = MAX_WIDTH; Height = 0.4f; + + X = (float)startTime; + StartTime = startTime; } } } diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs index 1f357283bd..def528d9e5 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs @@ -191,7 +191,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline { PointVisualisation point; if (drawableIndex >= Count) - Add(point = new PointVisualisation()); + Add(point = new PointVisualisation(0)); else point = Children[drawableIndex]; From d2cb07b15796d24e2b20753a06c520d059131ad7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 12 Jul 2024 17:22:36 +0900 Subject: [PATCH 1480/2493] Add a bit more padding between node overlays and hitobjects in timeline --- .../Edit/Compose/Components/Timeline/DifficultyPointPiece.cs | 1 + .../Edit/Compose/Components/Timeline/SamplePointPiece.cs | 1 + .../Compose/Components/Timeline/TimelineHitObjectBlueprint.cs | 2 +- 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/DifficultyPointPiece.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/DifficultyPointPiece.cs index 3ad6095965..44235e5d0b 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/DifficultyPointPiece.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/DifficultyPointPiece.cs @@ -33,6 +33,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline public DifficultyPointPiece(HitObject hitObject) { HitObject = hitObject; + Y = -2.5f; speedMultiplier = (hitObject as IHasSliderVelocity)?.SliderVelocityMultiplierBindable.GetBoundCopy(); } diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs index 1f9c7a891b..8c7603021a 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs @@ -35,6 +35,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline public SamplePointPiece(HitObject hitObject) { HitObject = hitObject; + Y = 2.5f; } public bool AlternativeColor { get; init; } diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs index 753856199a..a168dcbd3e 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs @@ -519,7 +519,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline { Type = EdgeEffectType.Shadow, Radius = 5, - Colour = Color4.Black.Opacity(0.4f) + Colour = Color4.Black.Opacity(0.05f) } }; } From 4be5d056c573234912aafb5b19f769fd8e81afca Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 12 Jul 2024 17:23:48 +0900 Subject: [PATCH 1481/2493] Reduce opacity of centre marker slightly --- .../Screens/Edit/Compose/Components/Timeline/CentreMarker.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/CentreMarker.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/CentreMarker.cs index e3542cbf9b..7d8622905c 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/CentreMarker.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/CentreMarker.cs @@ -2,13 +2,13 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; +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.Game.Overlays; using osuTK; -using osuTK.Graphics; namespace osu.Game.Screens.Edit.Compose.Components.Timeline { @@ -39,7 +39,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline RelativeSizeAxes = Axes.Y, Width = bar_width, Blending = BlendingParameters.Additive, - Colour = ColourInfo.GradientVertical(colours.Colour2, Color4.Black), + Colour = ColourInfo.GradientVertical(colours.Colour2.Opacity(0.6f), colours.Colour2.Opacity(0)), }, new Triangle { From 2453e2fd00b3b484ccc9f14e1555c87fc0f6beee Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 12 Jul 2024 18:11:23 +0900 Subject: [PATCH 1482/2493] Fix nullability issue --- .../Edit/Components/Timelines/Summary/Parts/BreakPart.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/BreakPart.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/BreakPart.cs index c20738bbd9..17e0d47676 100644 --- a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/BreakPart.cs +++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/BreakPart.cs @@ -51,7 +51,7 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts private partial class BreakVisualisation : PoolableDrawable, IHasTooltip { - private BreakPeriod breakPeriod; + private BreakPeriod breakPeriod = null!; public BreakPeriod BreakPeriod { From 217b01d3031b550e5e35f9a39142dbe9da27fd84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 12 Jul 2024 11:10:52 +0200 Subject: [PATCH 1483/2493] Apply tooltip to bookmark pieces too Bookmarks don't show on real beatmaps, but they do show in test scenes (namely `TestSceneEditorSummaryTimeline`). Also does some more changes to adjust the markers to the latest updates to other markers. --- .../Components/Timelines/Summary/Parts/BookmarkPart.cs | 8 ++++++-- .../Edit/Components/Timelines/Summary/SummaryTimeline.cs | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/BookmarkPart.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/BookmarkPart.cs index ea71f24e9c..189cb4ba4a 100644 --- a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/BookmarkPart.cs +++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/BookmarkPart.cs @@ -2,6 +2,9 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; +using osu.Framework.Graphics.Cursor; +using osu.Framework.Localisation; +using osu.Game.Extensions; using osu.Game.Graphics; using osu.Game.Screens.Edit.Components.Timelines.Summary.Visualisations; @@ -19,16 +22,17 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts Add(new BookmarkVisualisation(bookmark)); } - private partial class BookmarkVisualisation : PointVisualisation + private partial class BookmarkVisualisation : PointVisualisation, IHasTooltip { public BookmarkVisualisation(double startTime) : base(startTime) { - Width = 2; } [BackgroundDependencyLoader] private void load(OsuColour colours) => Colour = colours.Blue; + + public LocalisableString TooltipText => $"{StartTime.ToEditorFormattedString()} bookmark"; } } } diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/SummaryTimeline.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/SummaryTimeline.cs index 49110ccee3..a495442c1d 100644 --- a/osu.Game/Screens/Edit/Components/Timelines/Summary/SummaryTimeline.cs +++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/SummaryTimeline.cs @@ -71,7 +71,7 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary Anchor = Anchor.Centre, Origin = Anchor.TopCentre, RelativeSizeAxes = Axes.Both, - Height = 0.35f + Height = 0.4f }, new BreakPart { From ad2b354d9c2543d4bedc1316b9401b455703e3c3 Mon Sep 17 00:00:00 2001 From: Jamie Taylor Date: Thu, 11 Jul 2024 17:01:07 +0900 Subject: [PATCH 1484/2493] Update sample looping behaviour to better suit new sample --- osu.Game/Overlays/Dialog/PopupDialogDangerousButton.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Overlays/Dialog/PopupDialogDangerousButton.cs b/osu.Game/Overlays/Dialog/PopupDialogDangerousButton.cs index fec36fa7fa..d84e1d760d 100644 --- a/osu.Game/Overlays/Dialog/PopupDialogDangerousButton.cs +++ b/osu.Game/Overlays/Dialog/PopupDialogDangerousButton.cs @@ -67,7 +67,7 @@ namespace osu.Game.Overlays.Dialog protected override void LoadComplete() { base.LoadComplete(); - Progress.BindValueChanged(progressChanged, true); + Progress.BindValueChanged(progressChanged); } protected override void Confirm() @@ -114,13 +114,13 @@ namespace osu.Game.Overlays.Dialog if (progress.NewValue < progress.OldValue) return; - if (Clock.CurrentTime - lastTickPlaybackTime < 30) + if (Clock.CurrentTime - lastTickPlaybackTime < 40) return; var channel = tickSample.GetChannel(); - channel.Frequency.Value = 1 + progress.NewValue * 0.5f; - channel.Volume.Value = 0.5f + progress.NewValue / 2f; + channel.Frequency.Value = 1 + progress.NewValue; + channel.Volume.Value = 0.1f + progress.NewValue / 2f; channel.Play(); From 320df7da2b564b53820860439ac39f64499b56a5 Mon Sep 17 00:00:00 2001 From: Jamie Taylor Date: Thu, 11 Jul 2024 18:22:27 +0900 Subject: [PATCH 1485/2493] Use separate samples for scrolling to top and scrolling to previous --- .../Graphics/UserInterface/HoverSampleSet.cs | 6 ------ osu.Game/Overlays/OverlayScrollContainer.cs | 19 +++++++++++++++++-- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/osu.Game/Graphics/UserInterface/HoverSampleSet.cs b/osu.Game/Graphics/UserInterface/HoverSampleSet.cs index 72d50eb042..5b0fbc693e 100644 --- a/osu.Game/Graphics/UserInterface/HoverSampleSet.cs +++ b/osu.Game/Graphics/UserInterface/HoverSampleSet.cs @@ -16,15 +16,9 @@ namespace osu.Game.Graphics.UserInterface [Description("button-sidebar")] ButtonSidebar, - [Description("toolbar")] - Toolbar, - [Description("tabselect")] TabSelect, - [Description("scrolltotop")] - ScrollToTop, - [Description("dialog-cancel")] DialogCancel, diff --git a/osu.Game/Overlays/OverlayScrollContainer.cs b/osu.Game/Overlays/OverlayScrollContainer.cs index a99cf08abb..4328977a8d 100644 --- a/osu.Game/Overlays/OverlayScrollContainer.cs +++ b/osu.Game/Overlays/OverlayScrollContainer.cs @@ -5,6 +5,8 @@ using System.Collections.Generic; using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Audio.Sample; using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; @@ -112,8 +114,12 @@ namespace osu.Game.Overlays public Bindable LastScrollTarget = new Bindable(); + protected override HoverSounds CreateHoverSounds(HoverSampleSet sampleSet) => new HoverSounds(); + + private Sample scrollToTopSample; + private Sample scrollToPreviousSample; + public ScrollBackButton() - : base(HoverSampleSet.ScrollToTop) { Size = new Vector2(50); Alpha = 0; @@ -150,11 +156,14 @@ namespace osu.Game.Overlays } [BackgroundDependencyLoader] - private void load(OverlayColourProvider colourProvider) + private void load(OverlayColourProvider colourProvider, AudioManager audio) { IdleColour = colourProvider.Background6; HoverColour = colourProvider.Background5; flashColour = colourProvider.Light1; + + scrollToTopSample = audio.Samples.Get(@"UI/scroll-to-top"); + scrollToPreviousSample = audio.Samples.Get(@"UI/scroll-to-previous"); } protected override void LoadComplete() @@ -171,6 +180,12 @@ namespace osu.Game.Overlays protected override bool OnClick(ClickEvent e) { background.FlashColour(flashColour, 800, Easing.OutQuint); + + if (LastScrollTarget.Value == null) + scrollToTopSample?.Play(); + else + scrollToPreviousSample?.Play(); + return base.OnClick(e); } From e8f7213b3b2907c1c2aee8d0d4be92aeee23f0c3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 12 Jul 2024 19:20:41 +0900 Subject: [PATCH 1486/2493] Move logo depth to a forward passing --- osu.Game/OsuGame.cs | 3 +-- osu.Game/Screens/Footer/ScreenFooter.cs | 6 ++++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index dfd9f41e1a..2580e44e73 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -297,8 +297,6 @@ namespace osu.Game if (hideToolbar) Toolbar.Hide(); } - public void ChangeLogoDepth(bool inFrontOfFooter) => ScreenContainer.ChangeChildDepth(logoContainer, inFrontOfFooter ? float.MinValue : 0); - protected override UserInputManager CreateUserInputManager() { var userInputManager = base.CreateUserInputManager(); @@ -996,6 +994,7 @@ namespace osu.Game RelativeSizeAxes = Axes.Both, Child = ScreenFooter = new ScreenFooter(backReceptor) { + RequestLogoInFront = inFront => ScreenContainer.ChangeChildDepth(logoContainer, inFront ? float.MinValue : 0), OnBack = () => { if (!(ScreenStack.CurrentScreen is IOsuScreen currentScreen)) diff --git a/osu.Game/Screens/Footer/ScreenFooter.cs b/osu.Game/Screens/Footer/ScreenFooter.cs index 4c020fc95e..6a1efcf87a 100644 --- a/osu.Game/Screens/Footer/ScreenFooter.cs +++ b/osu.Game/Screens/Footer/ScreenFooter.cs @@ -44,6 +44,8 @@ namespace osu.Game.Screens.Footer public ScreenBackButton BackButton { get; private set; } = null!; + public Action? RequestLogoInFront { get; set; } + public Action? OnBack; public ScreenFooter(BackReceptor? receptor = null) @@ -114,7 +116,7 @@ namespace osu.Game.Screens.Footer changeLogoDepthDelegate = null; logoTrackingContainer.StartTracking(logo, duration, easing); - game?.ChangeLogoDepth(inFrontOfFooter: true); + RequestLogoInFront?.Invoke(true); } public void StopTrackingLogo() @@ -122,7 +124,7 @@ namespace osu.Game.Screens.Footer logoTrackingContainer.StopTracking(); if (game != null) - changeLogoDepthDelegate = Scheduler.AddDelayed(() => game.ChangeLogoDepth(inFrontOfFooter: false), transition_duration); + changeLogoDepthDelegate = Scheduler.AddDelayed(() => RequestLogoInFront?.Invoke(false), transition_duration); } protected override void PopIn() From 9a1939a922c29657e60745348469f024c00ff99f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 12 Jul 2024 21:25:21 +0900 Subject: [PATCH 1487/2493] Move `logoContainer` local again --- osu.Game/OsuGame.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 2580e44e73..388a98d947 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -84,7 +84,7 @@ namespace osu.Game public partial class OsuGame : OsuGameBase, IKeyBindingHandler, ILocalUserPlayInfo, IPerformFromScreenRunner, IOverlayManager, ILinkHandler { #if DEBUG - // Different port allows runnning release and debug builds alongside each other. + // Different port allows running release and debug builds alongside each other. public const int IPC_PORT = 44824; #else public const int IPC_PORT = 44823; @@ -137,8 +137,6 @@ namespace osu.Game protected ScalingContainer ScreenContainer { get; private set; } - private Container logoContainer; - protected Container ScreenOffsetContainer { get; private set; } private Container overlayOffsetContainer; @@ -954,6 +952,8 @@ namespace osu.Game Add(sessionIdleTracker); + Container logoContainer; + AddRange(new Drawable[] { new VolumeControlReceptor From 3eaac11b4426de652087eb1f18d9e4496eb941d8 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 13 Jul 2024 11:26:45 +0300 Subject: [PATCH 1488/2493] Add profile hue attribute to API model --- osu.Game/Online/API/Requests/Responses/APIUser.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/Online/API/Requests/Responses/APIUser.cs b/osu.Game/Online/API/Requests/Responses/APIUser.cs index 4a31718f28..1c07b38667 100644 --- a/osu.Game/Online/API/Requests/Responses/APIUser.cs +++ b/osu.Game/Online/API/Requests/Responses/APIUser.cs @@ -201,6 +201,10 @@ namespace osu.Game.Online.API.Requests.Responses [JsonProperty(@"playmode")] public string PlayMode; + [JsonProperty(@"profile_hue")] + [CanBeNull] + public int? ProfileHue; + [JsonProperty(@"profile_order")] public string[] ProfileOrder; From 933626a64b855fced565cc2f4ccc516abac73da5 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 13 Jul 2024 11:46:39 +0300 Subject: [PATCH 1489/2493] Support using custom hue in `OverlayColourProvider` --- osu.Game/Overlays/OverlayColourProvider.cs | 57 +++++----------------- 1 file changed, 11 insertions(+), 46 deletions(-) diff --git a/osu.Game/Overlays/OverlayColourProvider.cs b/osu.Game/Overlays/OverlayColourProvider.cs index 06b42eafc0..3b0af77365 100644 --- a/osu.Game/Overlays/OverlayColourProvider.cs +++ b/osu.Game/Overlays/OverlayColourProvider.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 osuTK; using osuTK.Graphics; @@ -59,54 +58,20 @@ namespace osu.Game.Overlays private Color4 getColour(float saturation, float lightness) => Color4.FromHsl(new Vector4(getBaseHue(ColourScheme), saturation, lightness, 1)); - // See https://github.com/ppy/osu-web/blob/5a536d217a21582aad999db50a981003d3ad5659/app/helpers.php#L1620-L1628 - private static float getBaseHue(OverlayColourScheme colourScheme) - { - switch (colourScheme) - { - default: - throw new ArgumentException($@"{colourScheme} colour scheme does not provide a hue value in {nameof(getBaseHue)}."); - - case OverlayColourScheme.Red: - return 0; - - case OverlayColourScheme.Pink: - return 333 / 360f; - - case OverlayColourScheme.Orange: - return 45 / 360f; - - case OverlayColourScheme.Lime: - return 90 / 360f; - - case OverlayColourScheme.Green: - return 125 / 360f; - - case OverlayColourScheme.Aquamarine: - return 160 / 360f; - - case OverlayColourScheme.Purple: - return 255 / 360f; - - case OverlayColourScheme.Blue: - return 200 / 360f; - - case OverlayColourScheme.Plum: - return 320 / 360f; - } - } + private static float getBaseHue(OverlayColourScheme colourScheme) => (int)colourScheme / 360f; } + // See https://github.com/ppy/osu-web/blob/5a536d217a21582aad999db50a981003d3ad5659/app/helpers.php#L1620-L1628 public enum OverlayColourScheme { - Red, - Pink, - Orange, - Lime, - Green, - Purple, - Blue, - Plum, - Aquamarine + Red = 0, + Orange = 45, + Lime = 90, + Green = 125, + Aquamarine = 160, + Blue = 200, + Purple = 255, + Plum = 320, + Pink = 333, } } From b292bf383205fa5d5994b95355a22abfa1d87f07 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 13 Jul 2024 11:47:36 +0300 Subject: [PATCH 1490/2493] Support using custom hue in user profile overlay --- osu.Game/Overlays/FullscreenOverlay.cs | 29 ++++-- osu.Game/Overlays/UserProfileOverlay.cs | 117 +++++++++++++----------- 2 files changed, 87 insertions(+), 59 deletions(-) diff --git a/osu.Game/Overlays/FullscreenOverlay.cs b/osu.Game/Overlays/FullscreenOverlay.cs index 6ddf1eecf0..2a09147c76 100644 --- a/osu.Game/Overlays/FullscreenOverlay.cs +++ b/osu.Game/Overlays/FullscreenOverlay.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.Diagnostics.CodeAnalysis; using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; @@ -22,7 +23,7 @@ namespace osu.Game.Overlays public virtual LocalisableString Title => Header.Title.Title; public virtual LocalisableString Description => Header.Title.Description; - public T Header { get; } + public T Header { get; private set; } protected virtual Color4 BackgroundColour => ColourProvider.Background5; @@ -34,11 +35,12 @@ namespace osu.Game.Overlays protected override Container Content => content; + private readonly Box background; private readonly Container content; protected FullscreenOverlay(OverlayColourScheme colourScheme) { - Header = CreateHeader(); + RecreateHeader(); ColourProvider = new OverlayColourProvider(colourScheme); @@ -60,10 +62,9 @@ namespace osu.Game.Overlays base.Content.AddRange(new Drawable[] { - new Box + background = new Box { RelativeSizeAxes = Axes.Both, - Colour = BackgroundColour }, content = new Container { @@ -75,14 +76,17 @@ namespace osu.Game.Overlays [BackgroundDependencyLoader] private void load() { - Waves.FirstWaveColour = ColourProvider.Light4; - Waves.SecondWaveColour = ColourProvider.Light3; - Waves.ThirdWaveColour = ColourProvider.Dark4; - Waves.FourthWaveColour = ColourProvider.Dark3; + UpdateColours(); } protected abstract T CreateHeader(); + [MemberNotNull(nameof(Header))] + protected void RecreateHeader() + { + Header = CreateHeader(); + } + public override void Show() { if (State.Value == Visibility.Visible) @@ -96,6 +100,15 @@ namespace osu.Game.Overlays } } + public void UpdateColours() + { + Waves.FirstWaveColour = ColourProvider.Light4; + Waves.SecondWaveColour = ColourProvider.Light3; + Waves.ThirdWaveColour = ColourProvider.Dark4; + Waves.FourthWaveColour = ColourProvider.Dark3; + background.Colour = BackgroundColour; + } + protected override void PopIn() { base.PopIn(); diff --git a/osu.Game/Overlays/UserProfileOverlay.cs b/osu.Game/Overlays/UserProfileOverlay.cs index 9840551d9f..c8b64bf2f1 100644 --- a/osu.Game/Overlays/UserProfileOverlay.cs +++ b/osu.Game/Overlays/UserProfileOverlay.cs @@ -99,11 +99,11 @@ namespace osu.Game.Overlays if (user.OnlineID == Header.User.Value?.User.Id && ruleset?.MatchesOnlineID(Header.User.Value?.Ruleset) == true) return; - if (sectionsContainer != null) - sectionsContainer.ExpandableHeader = null; + sectionsContainer?.ScrollToTop(); + sectionsContainer?.Clear(); + tabs?.Clear(); userReq?.Cancel(); - Clear(); lastSection = null; sections = !user.IsBot @@ -119,20 +119,74 @@ namespace osu.Game.Overlays } : Array.Empty(); - tabs = new ProfileSectionTabControl - { - RelativeSizeAxes = Axes.X, - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - }; + setupBaseContent(OverlayColourScheme.Pink); - Add(new OsuContextMenuContainer + if (API.State.Value != APIState.Offline) + { + userReq = user.OnlineID > 1 ? new GetUserRequest(user.OnlineID, ruleset) : new GetUserRequest(user.Username, ruleset); + userReq.Success += u => userLoadComplete(u, ruleset); + + API.Queue(userReq); + loadingLayer.Show(); + } + } + + private void userLoadComplete(APIUser loadedUser, IRulesetInfo? userRuleset) + { + Debug.Assert(sections != null && sectionsContainer != null && tabs != null); + + // reuse header and content if same colour scheme, otherwise recreate both. + var profileScheme = (OverlayColourScheme?)loadedUser.ProfileHue ?? OverlayColourScheme.Pink; + if (profileScheme != ColourProvider.ColourScheme) + setupBaseContent(profileScheme); + + var actualRuleset = rulesets.GetRuleset(userRuleset?.ShortName ?? loadedUser.PlayMode).AsNonNull(); + + var userProfile = new UserProfileData(loadedUser, actualRuleset); + Header.User.Value = userProfile; + + if (loadedUser.ProfileOrder != null) + { + foreach (string id in loadedUser.ProfileOrder) + { + var sec = sections.FirstOrDefault(s => s.Identifier == id); + + if (sec != null) + { + sec.User.Value = userProfile; + + sectionsContainer.Add(sec); + tabs.AddItem(sec); + } + } + } + + loadingLayer.Hide(); + } + + private void setupBaseContent(OverlayColourScheme colourScheme) + { + var previousColourScheme = ColourProvider.ColourScheme; + ColourProvider.ChangeColourScheme(colourScheme); + + if (sectionsContainer != null && colourScheme == previousColourScheme) + return; + + RecreateHeader(); + UpdateColours(); + + Child = new OsuContextMenuContainer { RelativeSizeAxes = Axes.Both, Child = sectionsContainer = new ProfileSectionsContainer { ExpandableHeader = Header, - FixedHeader = tabs, + FixedHeader = tabs = new ProfileSectionTabControl + { + RelativeSizeAxes = Axes.X, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + }, HeaderBackground = new Box { // this is only visible as the ProfileTabControl background @@ -140,7 +194,7 @@ namespace osu.Game.Overlays RelativeSizeAxes = Axes.Both }, } - }); + }; sectionsContainer.SelectedSection.ValueChanged += section => { @@ -167,45 +221,6 @@ namespace osu.Game.Overlays sectionsContainer.ScrollTo(lastSection); } }; - - sectionsContainer.ScrollToTop(); - - if (API.State.Value != APIState.Offline) - { - userReq = user.OnlineID > 1 ? new GetUserRequest(user.OnlineID, ruleset) : new GetUserRequest(user.Username, ruleset); - userReq.Success += u => userLoadComplete(u, ruleset); - - API.Queue(userReq); - loadingLayer.Show(); - } - } - - private void userLoadComplete(APIUser loadedUser, IRulesetInfo? userRuleset) - { - Debug.Assert(sections != null && sectionsContainer != null && tabs != null); - - var actualRuleset = rulesets.GetRuleset(userRuleset?.ShortName ?? loadedUser.PlayMode).AsNonNull(); - - var userProfile = new UserProfileData(loadedUser, actualRuleset); - Header.User.Value = userProfile; - - if (loadedUser.ProfileOrder != null) - { - foreach (string id in loadedUser.ProfileOrder) - { - var sec = sections.FirstOrDefault(s => s.Identifier == id); - - if (sec != null) - { - sec.User.Value = userProfile; - - sectionsContainer.Add(sec); - tabs.AddItem(sec); - } - } - } - - loadingLayer.Hide(); } private partial class ProfileSectionTabControl : OsuTabControl From be1d3c0ea4e0fc3527c0d660e6432481bc0f9c3c Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 13 Jul 2024 11:47:48 +0300 Subject: [PATCH 1491/2493] Add test coverage --- .../Online/TestSceneUserProfileOverlay.cs | 81 +++++++++++++++++++ 1 file changed, 81 insertions(+) diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs index fa68c931d8..8dbd493920 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs @@ -111,6 +111,87 @@ namespace osu.Game.Tests.Visual.Online AddStep("complete request", () => pendingRequest.TriggerSuccess(TEST_USER)); } + [Test] + public void TestCustomColourScheme() + { + int hue = 0; + + AddSliderStep("hue", 0, 360, 222, h => hue = h); + + AddStep("set up request handling", () => + { + dummyAPI.HandleRequest = req => + { + if (req is GetUserRequest getUserRequest) + { + getUserRequest.TriggerSuccess(new APIUser + { + Username = $"Colorful #{hue}", + Id = 1, + CountryCode = CountryCode.JP, + CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c2.jpg", + ProfileHue = hue, + }); + return true; + } + + return false; + }; + }); + + AddStep("show user", () => profile.ShowUser(new APIUser { Id = 1 })); + } + + [Test] + public void TestCustomColourSchemeWithReload() + { + int hue = 0; + GetUserRequest pendingRequest = null!; + + AddSliderStep("hue", 0, 360, 222, h => hue = h); + + AddStep("set up request handling", () => + { + dummyAPI.HandleRequest = req => + { + if (req is GetUserRequest getUserRequest) + { + pendingRequest = getUserRequest; + return true; + } + + return false; + }; + }); + + AddStep("show user", () => profile.ShowUser(new APIUser { Id = 1 })); + + AddWaitStep("wait some", 3); + AddStep("complete request", () => pendingRequest.TriggerSuccess(new APIUser + { + Username = $"Colorful #{hue}", + Id = 1, + CountryCode = CountryCode.JP, + CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c2.jpg", + ProfileHue = hue, + })); + + int hue2 = 0; + + AddSliderStep("hue 2", 0, 360, 50, h => hue2 = h); + AddStep("show user", () => profile.ShowUser(new APIUser { Id = 1 })); + AddWaitStep("wait some", 3); + + AddStep("complete request", () => pendingRequest.TriggerSuccess(new APIUser + { + Username = $"Colorful #{hue2}", + Id = 1, + CountryCode = CountryCode.JP, + CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c2.jpg", + ProfileHue = hue2, + })); + } + public static readonly APIUser TEST_USER = new APIUser { Username = @"Somebody", From 9ed97d03a8143e74064ddb76f07f6dcd61af04ad Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 13 Jul 2024 20:22:52 +0900 Subject: [PATCH 1492/2493] 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 447c783b29..4d9e7dea33 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -36,7 +36,7 @@ - + From b12790c684bd0474d3f36c18cb71dd21c2922c92 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 13 Jul 2024 18:29:03 +0300 Subject: [PATCH 1493/2493] Fix hue number 360 giving off a gray colour scheme They say it's not a bug, it's a feature...I dunno maybe later. --- osu.Game/Overlays/OverlayColourProvider.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/OverlayColourProvider.cs b/osu.Game/Overlays/OverlayColourProvider.cs index 3b0af77365..5b6579e6cf 100644 --- a/osu.Game/Overlays/OverlayColourProvider.cs +++ b/osu.Game/Overlays/OverlayColourProvider.cs @@ -58,7 +58,12 @@ namespace osu.Game.Overlays private Color4 getColour(float saturation, float lightness) => Color4.FromHsl(new Vector4(getBaseHue(ColourScheme), saturation, lightness, 1)); - private static float getBaseHue(OverlayColourScheme colourScheme) => (int)colourScheme / 360f; + private static float getBaseHue(OverlayColourScheme colourScheme) + { + // intentionally round hue number back to zero when it's 360, because that number apparently gives off a nice-looking gray colour scheme but is totally against expectation (maybe we can use this one day). + int hueNumber = (int)colourScheme % 360; + return hueNumber / 360f; + } } // See https://github.com/ppy/osu-web/blob/5a536d217a21582aad999db50a981003d3ad5659/app/helpers.php#L1620-L1628 From 43d08f702aa550aa2e41117c8970ae2b6fc0865d Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 13 Jul 2024 18:43:46 +0300 Subject: [PATCH 1494/2493] Or just use `Colour4` where we have that fixed --- osu.Game/Overlays/OverlayColourProvider.cs | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/osu.Game/Overlays/OverlayColourProvider.cs b/osu.Game/Overlays/OverlayColourProvider.cs index 5b6579e6cf..9613bc2857 100644 --- a/osu.Game/Overlays/OverlayColourProvider.cs +++ b/osu.Game/Overlays/OverlayColourProvider.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 osuTK; using osuTK.Graphics; namespace osu.Game.Overlays @@ -56,14 +55,9 @@ namespace osu.Game.Overlays ColourScheme = colourScheme; } - private Color4 getColour(float saturation, float lightness) => Color4.FromHsl(new Vector4(getBaseHue(ColourScheme), saturation, lightness, 1)); + private Color4 getColour(float saturation, float lightness) => Framework.Graphics.Colour4.FromHSL(getBaseHue(ColourScheme), saturation, lightness); - private static float getBaseHue(OverlayColourScheme colourScheme) - { - // intentionally round hue number back to zero when it's 360, because that number apparently gives off a nice-looking gray colour scheme but is totally against expectation (maybe we can use this one day). - int hueNumber = (int)colourScheme % 360; - return hueNumber / 360f; - } + private static float getBaseHue(OverlayColourScheme colourScheme) => (int)colourScheme / 360f; } // See https://github.com/ppy/osu-web/blob/5a536d217a21582aad999db50a981003d3ad5659/app/helpers.php#L1620-L1628 From 2c102fc9d01726f84d185f0da1373e38f5c0163b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 13 Jul 2024 23:54:10 +0900 Subject: [PATCH 1495/2493] Fix test failure in `TestMetadataTransferred` --- osu.Game.Tests/Database/BeatmapImporterUpdateTests.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Database/BeatmapImporterUpdateTests.cs b/osu.Game.Tests/Database/BeatmapImporterUpdateTests.cs index 16e66cb2c5..a47da4d505 100644 --- a/osu.Game.Tests/Database/BeatmapImporterUpdateTests.cs +++ b/osu.Game.Tests/Database/BeatmapImporterUpdateTests.cs @@ -479,6 +479,7 @@ namespace osu.Game.Tests.Database using var rulesets = new RealmRulesetStore(realm, storage); using var __ = getBeatmapArchive(out string pathOriginal); + using var _ = getBeatmapArchiveWithModifications(out string pathMissingOneBeatmap, directory => { // arbitrary beatmap removal @@ -496,7 +497,7 @@ namespace osu.Game.Tests.Database Debug.Assert(importAfterUpdate != null); Assert.That(importBeforeUpdate.ID, Is.Not.EqualTo(importAfterUpdate.ID)); - Assert.That(importBeforeUpdate.Value.DateAdded, Is.EqualTo(importAfterUpdate.Value.DateAdded)); + Assert.That(importBeforeUpdate.Value.DateAdded, Is.EqualTo(importAfterUpdate.Value.DateAdded).Within(TimeSpan.FromSeconds(1))); }); } From adb803c7a9b41d60e27f9965e470ee83921bae70 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sun, 14 Jul 2024 15:19:26 +0300 Subject: [PATCH 1496/2493] Force recreating sections container when loading new user to avoid weird UX when scrolled away --- osu.Game/Overlays/UserProfileOverlay.cs | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/osu.Game/Overlays/UserProfileOverlay.cs b/osu.Game/Overlays/UserProfileOverlay.cs index c8b64bf2f1..8c750b5d83 100644 --- a/osu.Game/Overlays/UserProfileOverlay.cs +++ b/osu.Game/Overlays/UserProfileOverlay.cs @@ -99,9 +99,8 @@ namespace osu.Game.Overlays if (user.OnlineID == Header.User.Value?.User.Id && ruleset?.MatchesOnlineID(Header.User.Value?.Ruleset) == true) return; - sectionsContainer?.ScrollToTop(); - sectionsContainer?.Clear(); - tabs?.Clear(); + if (sectionsContainer != null) + sectionsContainer.ExpandableHeader = null; userReq?.Cancel(); lastSection = null; @@ -119,7 +118,7 @@ namespace osu.Game.Overlays } : Array.Empty(); - setupBaseContent(OverlayColourScheme.Pink); + setupBaseContent(OverlayColourScheme.Pink, forceContentRecreation: true); if (API.State.Value != APIState.Offline) { @@ -138,7 +137,7 @@ namespace osu.Game.Overlays // reuse header and content if same colour scheme, otherwise recreate both. var profileScheme = (OverlayColourScheme?)loadedUser.ProfileHue ?? OverlayColourScheme.Pink; if (profileScheme != ColourProvider.ColourScheme) - setupBaseContent(profileScheme); + setupBaseContent(profileScheme, forceContentRecreation: false); var actualRuleset = rulesets.GetRuleset(userRuleset?.ShortName ?? loadedUser.PlayMode).AsNonNull(); @@ -164,17 +163,19 @@ namespace osu.Game.Overlays loadingLayer.Hide(); } - private void setupBaseContent(OverlayColourScheme colourScheme) + private void setupBaseContent(OverlayColourScheme colourScheme, bool forceContentRecreation) { var previousColourScheme = ColourProvider.ColourScheme; ColourProvider.ChangeColourScheme(colourScheme); - if (sectionsContainer != null && colourScheme == previousColourScheme) + if (colourScheme != previousColourScheme) + { + RecreateHeader(); + UpdateColours(); + } + else if (!forceContentRecreation) return; - RecreateHeader(); - UpdateColours(); - Child = new OsuContextMenuContainer { RelativeSizeAxes = Axes.Both, From ae380027772867b45baf80acc910c98cb39fac46 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Sun, 14 Jul 2024 15:46:40 +0200 Subject: [PATCH 1497/2493] Revert "fix incorrect rotated bound checking" This reverts commit 4165ded8134d05f4d6b934255a5678a6a7d74bca. --- .../Edit/OsuSelectionScaleHandler.cs | 53 ++++--------------- osu.Game/Utils/GeometryUtils.cs | 20 +++---- 2 files changed, 17 insertions(+), 56 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs index d336261499..8b87246456 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs @@ -80,32 +80,12 @@ namespace osu.Game.Rulesets.Osu.Edit changeHandler?.BeginChange(); objectsInScale = selectedMovableObjects.ToDictionary(ho => ho, ho => new OriginalHitObjectState(ho)); - OriginalSurroundingQuad = getOriginalSurroundingQuad()!; + OriginalSurroundingQuad = objectsInScale.Count == 1 && objectsInScale.First().Key is Slider slider + ? GeometryUtils.GetSurroundingQuad(slider.Path.ControlPoints.Select(p => slider.Position + p.Position)) + : GeometryUtils.GetSurroundingQuad(objectsInScale.Keys); defaultOrigin = OriginalSurroundingQuad.Value.Centre; } - private Quad? getOriginalSurroundingQuad(float axisRotation = 0) - { - if (objectsInScale == null) - return null; - - return objectsInScale.Count == 1 && objectsInScale.First().Value.PathControlPointPositions != null - ? GeometryUtils.GetSurroundingQuad(objectsInScale.First().Value.PathControlPointPositions!.Select(p => objectsInScale.First().Value.Position + p), axisRotation) - : GeometryUtils.GetSurroundingQuad(objectsInScale.Values.SelectMany(s => - { - if (s.EndPosition.HasValue) - { - return new[] - { - s.Position, - s.Position + s.EndPosition.Value - }; - } - - return new[] { s.Position }; - }), axisRotation); - } - public override void Update(Vector2 scale, Vector2? origin = null, Axes adjustAxis = Axes.Both, float axisRotation = 0) { if (!OperationInProgress.Value) @@ -233,23 +213,10 @@ namespace osu.Game.Rulesets.Osu.Edit scale = clampScaleToAdjustAxis(scale, adjustAxis); Vector2 actualOrigin = origin ?? defaultOrigin.Value; - var selectionQuad = axisRotation == 0 ? OriginalSurroundingQuad.Value : getOriginalSurroundingQuad(axisRotation)!.Value; - var points = new[] - { - selectionQuad.TopLeft, - selectionQuad.TopRight, - selectionQuad.BottomLeft, - selectionQuad.BottomRight - }; + var selectionQuad = OriginalSurroundingQuad.Value; - float cos = MathF.Cos(float.DegreesToRadians(-axisRotation)); - float sin = MathF.Sin(float.DegreesToRadians(-axisRotation)); - - foreach (var point in points) - { - scale = clampToBound(scale, point, Vector2.Zero); - scale = clampToBound(scale, point, OsuPlayfield.BASE_SIZE); - } + scale = clampToBound(scale, selectionQuad.BottomRight, OsuPlayfield.BASE_SIZE); + scale = clampToBound(scale, selectionQuad.TopLeft, Vector2.Zero); return Vector2.ComponentMax(scale, new Vector2(Precision.FLOAT_EPSILON)); @@ -259,17 +226,19 @@ namespace osu.Game.Rulesets.Osu.Edit { p -= actualOrigin; bound -= actualOrigin; + float cos = MathF.Cos(float.DegreesToRadians(-axisRotation)); + float sin = MathF.Sin(float.DegreesToRadians(-axisRotation)); var a = new Vector2(cos * cos * p.X - sin * cos * p.Y, -sin * cos * p.X + sin * sin * p.Y); var b = new Vector2(sin * sin * p.X + sin * cos * p.Y, sin * cos * p.X + cos * cos * p.Y); switch (adjustAxis) { case Axes.X: - s.X = MathF.Min(s.X, minPositiveComponent(Vector2.Divide(bound - b, a))); + s.X = MathF.Min(scale.X, minPositiveComponent(Vector2.Divide(bound - b, a))); break; case Axes.Y: - s.Y = MathF.Min(s.Y, minPositiveComponent(Vector2.Divide(bound - a, b))); + s.Y = MathF.Min(scale.Y, minPositiveComponent(Vector2.Divide(bound - a, b))); break; case Axes.Both: @@ -306,14 +275,12 @@ namespace osu.Game.Rulesets.Osu.Edit public Vector2 Position { get; } public Vector2[]? PathControlPointPositions { get; } public PathType?[]? PathControlPointTypes { get; } - public Vector2? EndPosition { get; } public OriginalHitObjectState(OsuHitObject hitObject) { Position = hitObject.Position; PathControlPointPositions = (hitObject as IHasPath)?.Path.ControlPoints.Select(p => p.Position).ToArray(); PathControlPointTypes = (hitObject as IHasPath)?.Path.ControlPoints.Select(p => p.Type).ToArray(); - EndPosition = (hitObject as IHasPath)?.Path.PositionAt(1); } } } diff --git a/osu.Game/Utils/GeometryUtils.cs b/osu.Game/Utils/GeometryUtils.cs index 23c25cfffa..f6e7e81007 100644 --- a/osu.Game/Utils/GeometryUtils.cs +++ b/osu.Game/Utils/GeometryUtils.cs @@ -113,8 +113,7 @@ namespace osu.Game.Utils /// Returns a quad surrounding the provided points. /// /// The points to calculate a quad for. - /// The rotation in degrees of the axis to align the quad to. - public static Quad GetSurroundingQuad(IEnumerable points, float axisRotation = 0) + public static Quad GetSurroundingQuad(IEnumerable points) { if (!points.Any()) return new Quad(); @@ -125,25 +124,20 @@ namespace osu.Game.Utils // Go through all hitobjects to make sure they would remain in the bounds of the editor after movement, before any movement is attempted foreach (var p in points) { - var pr = RotateVector(p, axisRotation); - minPosition = Vector2.ComponentMin(minPosition, pr); - maxPosition = Vector2.ComponentMax(maxPosition, pr); + minPosition = Vector2.ComponentMin(minPosition, p); + maxPosition = Vector2.ComponentMax(maxPosition, p); } - var p1 = RotateVector(minPosition, -axisRotation); - var p2 = RotateVector(new Vector2(minPosition.X, maxPosition.Y), -axisRotation); - var p3 = RotateVector(maxPosition, -axisRotation); - var p4 = RotateVector(new Vector2(maxPosition.X, minPosition.Y), -axisRotation); + Vector2 size = maxPosition - minPosition; - return new Quad(p1, p2, p3, p4); + return new Quad(minPosition.X, minPosition.Y, size.X, size.Y); } /// /// Returns a gamefield-space quad surrounding the provided hit objects. /// /// The hit objects to calculate a quad for. - /// The rotation in degrees of the axis to align the quad to. - public static Quad GetSurroundingQuad(IEnumerable hitObjects, float axisRotation = 0) => + public static Quad GetSurroundingQuad(IEnumerable hitObjects) => GetSurroundingQuad(hitObjects.SelectMany(h => { if (h is IHasPath path) @@ -157,6 +151,6 @@ namespace osu.Game.Utils } return new[] { h.Position }; - }), axisRotation); + })); } } From 58eb7f6fe174ba72f06304d0334b0668dde14c74 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Sun, 14 Jul 2024 16:58:05 +0200 Subject: [PATCH 1498/2493] fix rotated scale bounds again --- .../Edit/OsuSelectionScaleHandler.cs | 31 ++++++++++-- osu.Game/Utils/GeometryUtils.cs | 49 ++++++++++++++++++- 2 files changed, 73 insertions(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs index 8b87246456..56c3ba9315 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs @@ -69,6 +69,7 @@ namespace osu.Game.Rulesets.Osu.Edit private Dictionary? objectsInScale; private Vector2? defaultOrigin; + private List? originalConvexHull; public override void Begin() { @@ -84,6 +85,9 @@ namespace osu.Game.Rulesets.Osu.Edit ? GeometryUtils.GetSurroundingQuad(slider.Path.ControlPoints.Select(p => slider.Position + p.Position)) : GeometryUtils.GetSurroundingQuad(objectsInScale.Keys); defaultOrigin = OriginalSurroundingQuad.Value.Centre; + originalConvexHull = objectsInScale.Count == 1 && objectsInScale.First().Key is Slider slider2 + ? GeometryUtils.GetConvexHull(slider2.Path.ControlPoints.Select(p => slider2.Position + p.Position)) + : GeometryUtils.GetConvexHull(objectsInScale.Keys); } public override void Update(Vector2 scale, Vector2? origin = null, Axes adjustAxis = Axes.Both, float axisRotation = 0) @@ -211,12 +215,31 @@ namespace osu.Game.Rulesets.Osu.Edit if (objectsInScale.Count == 1 && objectsInScale.First().Key is Slider slider) origin = slider.Position; + float cos = MathF.Cos(float.DegreesToRadians(-axisRotation)); + float sin = MathF.Sin(float.DegreesToRadians(-axisRotation)); scale = clampScaleToAdjustAxis(scale, adjustAxis); Vector2 actualOrigin = origin ?? defaultOrigin.Value; - var selectionQuad = OriginalSurroundingQuad.Value; + IEnumerable points; - scale = clampToBound(scale, selectionQuad.BottomRight, OsuPlayfield.BASE_SIZE); - scale = clampToBound(scale, selectionQuad.TopLeft, Vector2.Zero); + if (axisRotation == 0) + { + var selectionQuad = OriginalSurroundingQuad.Value; + points = new[] + { + selectionQuad.TopLeft, + selectionQuad.TopRight, + selectionQuad.BottomLeft, + selectionQuad.BottomRight + }; + } + else + points = originalConvexHull!; + + foreach (var point in points) + { + scale = clampToBound(scale, point, Vector2.Zero); + scale = clampToBound(scale, point, OsuPlayfield.BASE_SIZE); + } return Vector2.ComponentMax(scale, new Vector2(Precision.FLOAT_EPSILON)); @@ -226,8 +249,6 @@ namespace osu.Game.Rulesets.Osu.Edit { p -= actualOrigin; bound -= actualOrigin; - float cos = MathF.Cos(float.DegreesToRadians(-axisRotation)); - float sin = MathF.Sin(float.DegreesToRadians(-axisRotation)); var a = new Vector2(cos * cos * p.X - sin * cos * p.Y, -sin * cos * p.X + sin * sin * p.Y); var b = new Vector2(sin * sin * p.X + sin * cos * p.Y, sin * cos * p.X + cos * cos * p.Y); diff --git a/osu.Game/Utils/GeometryUtils.cs b/osu.Game/Utils/GeometryUtils.cs index f6e7e81007..5a8ca9722e 100644 --- a/osu.Game/Utils/GeometryUtils.cs +++ b/osu.Game/Utils/GeometryUtils.cs @@ -138,7 +138,52 @@ namespace osu.Game.Utils /// /// The hit objects to calculate a quad for. public static Quad GetSurroundingQuad(IEnumerable hitObjects) => - GetSurroundingQuad(hitObjects.SelectMany(h => + GetSurroundingQuad(enumerateStartAndEndPositions(hitObjects)); + + /// + /// Returns the points that make up the convex hull of the provided points. + /// + /// The points to calculate a convex hull. + public static List GetConvexHull(IEnumerable points) + { + List p = points.ToList(); + + if (p.Count <= 1) + return p; + + int n = p.Count, k = 0; + List hull = new List(new Vector2[2 * n]); + + p.Sort((a, b) => a.X == b.X ? a.Y.CompareTo(b.Y) : a.X.CompareTo(b.X)); + + // Build lower hull + for (int i = 0; i < n; ++i) + { + while (k >= 2 && cross(hull[k - 2], hull[k - 1], p[i]) <= 0) + k--; + hull[k] = p[i]; + k++; + } + + // Build upper hull + for (int i = n - 2, t = k + 1; i >= 0; i--) + { + while (k >= t && cross(hull[k - 2], hull[k - 1], p[i]) <= 0) + k--; + hull[k] = p[i]; + k++; + } + + return hull.Take(k - 1).ToList(); + + float cross(Vector2 o, Vector2 a, Vector2 b) => (a.X - o.X) * (b.Y - o.Y) - (a.Y - o.Y) * (b.X - o.X); + } + + public static List GetConvexHull(IEnumerable hitObjects) => + GetConvexHull(enumerateStartAndEndPositions(hitObjects)); + + private static IEnumerable enumerateStartAndEndPositions(IEnumerable hitObjects) => + hitObjects.SelectMany(h => { if (h is IHasPath path) { @@ -151,6 +196,6 @@ namespace osu.Game.Utils } return new[] { h.Position }; - })); + }); } } From 7a319a6d74ee29fbf3e7b5dbc2b7c6c9ca8e4990 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Sun, 14 Jul 2024 17:03:17 +0200 Subject: [PATCH 1499/2493] dont rotate scale when in selection origin mode --- osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs b/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs index a1907a2fd5..0f04efcfa5 100644 --- a/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs +++ b/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs @@ -131,7 +131,7 @@ namespace osu.Game.Rulesets.Osu.Edit scaleInfo.BindValueChanged(scale => { var newScale = new Vector2(scale.NewValue.Scale, scale.NewValue.Scale); - scaleHandler.Update(newScale, getOriginPosition(scale.NewValue), getAdjustAxis(scale.NewValue), gridToolbox.GridLinesRotation.Value); + scaleHandler.Update(newScale, getOriginPosition(scale.NewValue), getAdjustAxis(scale.NewValue), getRotation(scale.NewValue)); }); } @@ -164,7 +164,7 @@ namespace osu.Game.Rulesets.Osu.Edit return; const float max_scale = 10; - var scale = scaleHandler.ClampScaleToPlayfieldBounds(new Vector2(max_scale), getOriginPosition(scaleInfo.Value), getAdjustAxis(scaleInfo.Value), gridToolbox.GridLinesRotation.Value); + var scale = scaleHandler.ClampScaleToPlayfieldBounds(new Vector2(max_scale), getOriginPosition(scaleInfo.Value), getAdjustAxis(scaleInfo.Value), getRotation(scaleInfo.Value)); if (!scaleInfo.Value.XAxis) scale.X = max_scale; @@ -185,6 +185,8 @@ namespace osu.Game.Rulesets.Osu.Edit private Axes getAdjustAxis(PreciseScaleInfo scale) => scale.XAxis ? scale.YAxis ? Axes.Both : Axes.X : Axes.Y; + private float getRotation(PreciseScaleInfo scale) => scale.Origin == ScaleOrigin.PlayfieldCentre ? gridToolbox.GridLinesRotation.Value : 0; + private void setAxis(bool x, bool y) { scaleInfo.Value = scaleInfo.Value with { XAxis = x, YAxis = y }; From 9e5d099b1b1113304294c17155f42ab4a8ea76cd Mon Sep 17 00:00:00 2001 From: OliBomby Date: Sun, 14 Jul 2024 17:13:22 +0200 Subject: [PATCH 1500/2493] rename playfield centre origin to grid centre --- .../Edit/PreciseRotationPopover.cs | 12 ++++++------ .../Edit/PreciseScalePopover.cs | 16 ++++++++-------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs b/osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs index 6a3e326c2b..4a1ccc4b61 100644 --- a/osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs +++ b/osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs @@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Osu.Edit private readonly OsuGridToolboxGroup gridToolbox; - private readonly Bindable rotationInfo = new Bindable(new PreciseRotationInfo(0, RotationOrigin.PlayfieldCentre)); + private readonly Bindable rotationInfo = new Bindable(new PreciseRotationInfo(0, RotationOrigin.GridCentre)); private SliderWithTextBoxInput angleInput = null!; private EditorRadioButtonCollection rotationOrigin = null!; @@ -60,9 +60,9 @@ namespace osu.Game.Rulesets.Osu.Edit RelativeSizeAxes = Axes.X, Items = new[] { - new RadioButton("Playfield centre", - () => rotationInfo.Value = rotationInfo.Value with { Origin = RotationOrigin.PlayfieldCentre }, - () => new SpriteIcon { Icon = FontAwesome.Regular.Square }), + new RadioButton("Grid centre", + () => rotationInfo.Value = rotationInfo.Value with { Origin = RotationOrigin.GridCentre }, + () => new SpriteIcon { Icon = FontAwesome.Regular.PlusSquare }), selectionCentreButton = new RadioButton("Selection centre", () => rotationInfo.Value = rotationInfo.Value with { Origin = RotationOrigin.SelectionCentre }, () => new SpriteIcon { Icon = FontAwesome.Solid.VectorSquare }) @@ -95,7 +95,7 @@ namespace osu.Game.Rulesets.Osu.Edit rotationInfo.BindValueChanged(rotation => { - rotationHandler.Update(rotation.NewValue.Degrees, rotation.NewValue.Origin == RotationOrigin.PlayfieldCentre ? gridToolbox.StartPosition.Value : null); + rotationHandler.Update(rotation.NewValue.Degrees, rotation.NewValue.Origin == RotationOrigin.GridCentre ? gridToolbox.StartPosition.Value : null); }); } @@ -116,7 +116,7 @@ namespace osu.Game.Rulesets.Osu.Edit public enum RotationOrigin { - PlayfieldCentre, + GridCentre, SelectionCentre } diff --git a/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs b/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs index 0f04efcfa5..15ed4c59c3 100644 --- a/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs +++ b/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs @@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Osu.Edit private readonly OsuGridToolboxGroup gridToolbox; - private readonly Bindable scaleInfo = new Bindable(new PreciseScaleInfo(1, ScaleOrigin.PlayfieldCentre, true, true)); + private readonly Bindable scaleInfo = new Bindable(new PreciseScaleInfo(1, ScaleOrigin.GridCentre, true, true)); private SliderWithTextBoxInput scaleInput = null!; private BindableNumber scaleInputBindable = null!; @@ -68,9 +68,9 @@ namespace osu.Game.Rulesets.Osu.Edit RelativeSizeAxes = Axes.X, Items = new[] { - playfieldCentreButton = new RadioButton("Playfield centre", - () => setOrigin(ScaleOrigin.PlayfieldCentre), - () => new SpriteIcon { Icon = FontAwesome.Regular.Square }), + playfieldCentreButton = new RadioButton("Grid centre", + () => setOrigin(ScaleOrigin.GridCentre), + () => new SpriteIcon { Icon = FontAwesome.Regular.PlusSquare }), selectionCentreButton = new RadioButton("Selection centre", () => setOrigin(ScaleOrigin.SelectionCentre), () => new SpriteIcon { Icon = FontAwesome.Solid.VectorSquare }) @@ -137,7 +137,7 @@ namespace osu.Game.Rulesets.Osu.Edit private void updateAxisCheckBoxesEnabled() { - if (scaleInfo.Value.Origin == ScaleOrigin.PlayfieldCentre) + if (scaleInfo.Value.Origin == ScaleOrigin.GridCentre) { toggleAxisAvailable(xCheckBox.Current, true); toggleAxisAvailable(yCheckBox.Current, true); @@ -181,11 +181,11 @@ namespace osu.Game.Rulesets.Osu.Edit updateAxisCheckBoxesEnabled(); } - private Vector2? getOriginPosition(PreciseScaleInfo scale) => scale.Origin == ScaleOrigin.PlayfieldCentre ? gridToolbox.StartPosition.Value : null; + private Vector2? getOriginPosition(PreciseScaleInfo scale) => scale.Origin == ScaleOrigin.GridCentre ? gridToolbox.StartPosition.Value : null; private Axes getAdjustAxis(PreciseScaleInfo scale) => scale.XAxis ? scale.YAxis ? Axes.Both : Axes.X : Axes.Y; - private float getRotation(PreciseScaleInfo scale) => scale.Origin == ScaleOrigin.PlayfieldCentre ? gridToolbox.GridLinesRotation.Value : 0; + private float getRotation(PreciseScaleInfo scale) => scale.Origin == ScaleOrigin.GridCentre ? gridToolbox.GridLinesRotation.Value : 0; private void setAxis(bool x, bool y) { @@ -210,7 +210,7 @@ namespace osu.Game.Rulesets.Osu.Edit public enum ScaleOrigin { - PlayfieldCentre, + GridCentre, SelectionCentre } From a80e3337860773790c88010a7592219984a31570 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Sun, 14 Jul 2024 17:27:04 +0200 Subject: [PATCH 1501/2493] add playfield origin as third origin option --- .../Edit/PreciseRotationPopover.cs | 17 ++++++++++++- .../Edit/PreciseScalePopover.cs | 24 ++++++++++++++++--- 2 files changed, 37 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs b/osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs index 4a1ccc4b61..352debf500 100644 --- a/osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs +++ b/osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.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.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -8,6 +9,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; using osu.Game.Graphics.UserInterfaceV2; +using osu.Game.Rulesets.Osu.UI; using osu.Game.Screens.Edit.Components.RadioButtons; using osu.Game.Screens.Edit.Compose.Components; using osuTK; @@ -63,6 +65,9 @@ namespace osu.Game.Rulesets.Osu.Edit new RadioButton("Grid centre", () => rotationInfo.Value = rotationInfo.Value with { Origin = RotationOrigin.GridCentre }, () => new SpriteIcon { Icon = FontAwesome.Regular.PlusSquare }), + new RadioButton("Playfield centre", + () => rotationInfo.Value = rotationInfo.Value with { Origin = RotationOrigin.PlayfieldCentre }, + () => new SpriteIcon { Icon = FontAwesome.Regular.Square }), selectionCentreButton = new RadioButton("Selection centre", () => rotationInfo.Value = rotationInfo.Value with { Origin = RotationOrigin.SelectionCentre }, () => new SpriteIcon { Icon = FontAwesome.Solid.VectorSquare }) @@ -95,10 +100,19 @@ namespace osu.Game.Rulesets.Osu.Edit rotationInfo.BindValueChanged(rotation => { - rotationHandler.Update(rotation.NewValue.Degrees, rotation.NewValue.Origin == RotationOrigin.GridCentre ? gridToolbox.StartPosition.Value : null); + rotationHandler.Update(rotation.NewValue.Degrees, getOriginPosition(rotation.NewValue)); }); } + private Vector2? getOriginPosition(PreciseRotationInfo rotation) => + rotation.Origin switch + { + RotationOrigin.GridCentre => gridToolbox.StartPosition.Value, + RotationOrigin.PlayfieldCentre => OsuPlayfield.BASE_SIZE / 2, + RotationOrigin.SelectionCentre => null, + _ => throw new ArgumentOutOfRangeException(nameof(rotation)) + }; + protected override void PopIn() { base.PopIn(); @@ -117,6 +131,7 @@ namespace osu.Game.Rulesets.Osu.Edit public enum RotationOrigin { GridCentre, + PlayfieldCentre, SelectionCentre } diff --git a/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs b/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs index 15ed4c59c3..dff370d259 100644 --- a/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs +++ b/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs @@ -10,6 +10,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterfaceV2; +using osu.Game.Rulesets.Osu.UI; using osu.Game.Screens.Edit.Components.RadioButtons; using osuTK; @@ -27,6 +28,7 @@ namespace osu.Game.Rulesets.Osu.Edit private BindableNumber scaleInputBindable = null!; private EditorRadioButtonCollection scaleOrigin = null!; + private RadioButton gridCentreButton = null!; private RadioButton playfieldCentreButton = null!; private RadioButton selectionCentreButton = null!; @@ -68,9 +70,12 @@ namespace osu.Game.Rulesets.Osu.Edit RelativeSizeAxes = Axes.X, Items = new[] { - playfieldCentreButton = new RadioButton("Grid centre", + gridCentreButton = new RadioButton("Grid centre", () => setOrigin(ScaleOrigin.GridCentre), () => new SpriteIcon { Icon = FontAwesome.Regular.PlusSquare }), + playfieldCentreButton = new RadioButton("Playfield centre", + () => setOrigin(ScaleOrigin.PlayfieldCentre), + () => new SpriteIcon { Icon = FontAwesome.Regular.Square }), selectionCentreButton = new RadioButton("Selection centre", () => setOrigin(ScaleOrigin.SelectionCentre), () => new SpriteIcon { Icon = FontAwesome.Solid.VectorSquare }) @@ -99,6 +104,10 @@ namespace osu.Game.Rulesets.Osu.Edit }, } }; + gridCentreButton.Selected.DisabledChanged += isDisabled => + { + gridCentreButton.TooltipText = isDisabled ? "The current selection cannot be scaled relative to grid centre." : string.Empty; + }; playfieldCentreButton.Selected.DisabledChanged += isDisabled => { playfieldCentreButton.TooltipText = isDisabled ? "The current selection cannot be scaled relative to playfield centre." : string.Empty; @@ -125,6 +134,7 @@ namespace osu.Game.Rulesets.Osu.Edit selectionCentreButton.Selected.Disabled = !(scaleHandler.CanScaleX.Value || scaleHandler.CanScaleY.Value); playfieldCentreButton.Selected.Disabled = scaleHandler.IsScalingSlider.Value && !selectionCentreButton.Selected.Disabled; + gridCentreButton.Selected.Disabled = playfieldCentreButton.Selected.Disabled; scaleOrigin.Items.First(b => !b.Selected.Disabled).Select(); @@ -137,7 +147,7 @@ namespace osu.Game.Rulesets.Osu.Edit private void updateAxisCheckBoxesEnabled() { - if (scaleInfo.Value.Origin == ScaleOrigin.GridCentre) + if (scaleInfo.Value.Origin != ScaleOrigin.SelectionCentre) { toggleAxisAvailable(xCheckBox.Current, true); toggleAxisAvailable(yCheckBox.Current, true); @@ -181,7 +191,14 @@ namespace osu.Game.Rulesets.Osu.Edit updateAxisCheckBoxesEnabled(); } - private Vector2? getOriginPosition(PreciseScaleInfo scale) => scale.Origin == ScaleOrigin.GridCentre ? gridToolbox.StartPosition.Value : null; + private Vector2? getOriginPosition(PreciseScaleInfo scale) => + scale.Origin switch + { + ScaleOrigin.GridCentre => gridToolbox.StartPosition.Value, + ScaleOrigin.PlayfieldCentre => OsuPlayfield.BASE_SIZE / 2, + ScaleOrigin.SelectionCentre => null, + _ => throw new ArgumentOutOfRangeException(nameof(scale)) + }; private Axes getAdjustAxis(PreciseScaleInfo scale) => scale.XAxis ? scale.YAxis ? Axes.Both : Axes.X : Axes.Y; @@ -211,6 +228,7 @@ namespace osu.Game.Rulesets.Osu.Edit public enum ScaleOrigin { GridCentre, + PlayfieldCentre, SelectionCentre } From 2bbaa8e43ccce0a1bf42f2c5790ad469a81a195e Mon Sep 17 00:00:00 2001 From: OliBomby Date: Sun, 14 Jul 2024 18:12:55 +0200 Subject: [PATCH 1502/2493] make flips grid-type aware --- .../Edit/OsuSelectionHandler.cs | 26 ++++++++++++++++--- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs index 7d6ef66909..1334dbdbec 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs @@ -107,10 +107,28 @@ namespace osu.Game.Rulesets.Osu.Edit // If we're flipping over the origin, we take the grid origin position from the grid toolbox. var flipQuad = flipOverOrigin ? new Quad(gridToolbox.StartPositionX.Value, gridToolbox.StartPositionY.Value, 0, 0) : GeometryUtils.GetSurroundingQuad(hitObjects); - // If we're flipping over the origin, we take the grid rotation from the grid toolbox. - // We want to normalize the rotation angle to -45 to 45 degrees, so horizontal vs vertical flip is not mixed up by the rotation and it stays intuitive to use. - var flipAxis = flipOverOrigin ? GeometryUtils.RotateVector(Vector2.UnitX, -((gridToolbox.GridLinesRotation.Value + 405) % 90 - 45)) : Vector2.UnitX; - flipAxis = direction == Direction.Vertical ? flipAxis.PerpendicularLeft : flipAxis; + Vector2 flipAxis = direction == Direction.Vertical ? Vector2.UnitY : Vector2.UnitX; + + if (flipOverOrigin) + { + // If we're flipping over the origin, we take one of the axes of the grid. + // Take the axis closest to the direction we want to flip over. + switch (gridToolbox.GridType.Value) + { + case PositionSnapGridType.Square: + flipAxis = GeometryUtils.RotateVector(Vector2.UnitX, -((gridToolbox.GridLinesRotation.Value + 405) % 90 - 45)); + flipAxis = direction == Direction.Vertical ? flipAxis.PerpendicularLeft : flipAxis; + break; + + case PositionSnapGridType.Triangle: + // Hex grid has 3 axes, so you can not directly flip over one of the axes, + // however it's still possible to achieve that flip by combining multiple flips over the other axes. + flipAxis = direction == Direction.Vertical + ? GeometryUtils.RotateVector(Vector2.UnitX, -((gridToolbox.GridLinesRotation.Value + 390) % 60 + 60)) + : GeometryUtils.RotateVector(Vector2.UnitX, -((gridToolbox.GridLinesRotation.Value + 390) % 60 - 60)); + break; + } + } var controlPointFlipQuad = new Quad(); From e25642b48472789aaffa5254106d763a4cee9cad Mon Sep 17 00:00:00 2001 From: StanR Date: Mon, 15 Jul 2024 14:45:31 +0500 Subject: [PATCH 1503/2493] Implement a bunch of rhythm difficulty calculation fixes --- .../Difficulty/Evaluators/RhythmEvaluator.cs | 103 ++++++++++++++---- .../Preprocessing/OsuDifficultyHitObject.cs | 11 +- 2 files changed, 90 insertions(+), 24 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs index f2218a89a7..39c7572c85 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs @@ -2,6 +2,8 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; +using System.Linq; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Osu.Difficulty.Preprocessing; using osu.Game.Rulesets.Osu.Objects; @@ -10,21 +12,65 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators { public static class RhythmEvaluator { - private const int history_time_max = 5000; // 5 seconds of calculatingRhythmBonus max. - private const double rhythm_multiplier = 0.75; + private readonly struct Island : IEquatable + { + public Island() + { + } + + public Island(int firstDelta, double epsilon) + { + AddDelta(firstDelta, epsilon); + } + + public List Deltas { get; } = new List(); + + public void AddDelta(int delta, double epsilon) + { + int existingDelta = Deltas.FirstOrDefault(x => Math.Abs(x - delta) >= epsilon); + + Deltas.Add(existingDelta == default ? delta : existingDelta); + } + + public double AverageDelta() => Math.Max(Deltas.Average(), OsuDifficultyHitObject.MIN_DELTA_TIME); + + public override int GetHashCode() + { + // we need to compare all deltas and they must be in the exact same order we added them + string joinedDeltas = string.Join(string.Empty, Deltas); + return joinedDeltas.GetHashCode(); + } + + public bool Equals(Island other) + { + return other.GetHashCode() == GetHashCode(); + } + + public override bool Equals(object? obj) + { + return obj?.GetHashCode() == GetHashCode(); + } + } + + private const int history_time_max = 5 * 1000; // 5 seconds of calculatingRhythmBonus max. + private const double rhythm_multiplier = 1.14; + private const int max_island_size = 7; /// /// Calculates a rhythm multiplier for the difficulty of the tap associated with historic data of the current . /// public static double EvaluateDifficultyOf(DifficultyHitObject current) { + Dictionary islandCounts = new Dictionary(); + if (current.BaseObject is Spinner) return 0; - int previousIslandSize = 0; - double rhythmComplexitySum = 0; - int islandSize = 1; + + var island = new Island(); + var previousIsland = new Island(); + double startRatio = 0; // store the ratio of the current start of an island to buff for tighter rhythms bool firstDeltaSwitch = false; @@ -50,6 +96,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators double currDelta = currObj.StrainTime; double prevDelta = prevObj.StrainTime; double lastDelta = lastObj.StrainTime; + double currRatio = 1.0 + 6.0 * Math.Min(0.5, Math.Pow(Math.Sin(Math.PI / (Math.Min(prevDelta, currDelta) / Math.Max(prevDelta, currDelta))), 2)); // fancy function to calculate rhythmbonuses. double windowPenalty = Math.Min(1, Math.Max(0, Math.Abs(prevDelta - currDelta) - currObj.HitWindowGreat * 0.3) / (currObj.HitWindowGreat * 0.3)); @@ -58,12 +105,17 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators double effectiveRatio = windowPenalty * currRatio; + double deltaDifferenceEpsilon = currObj.HitWindowGreat * 0.3; + if (firstDeltaSwitch) { - if (!(prevDelta > 1.25 * currDelta || prevDelta * 1.25 < currDelta)) + if (!(Math.Abs(prevDelta - currDelta) > deltaDifferenceEpsilon)) { - if (islandSize < 7) - islandSize++; // island is still progressing, count size. + if (island.Deltas.Count < max_island_size) + { + // island is still progressing + island.AddDelta((int)currDelta, deltaDifferenceEpsilon); + } } else { @@ -73,33 +125,44 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators if (prevObj.BaseObject is Slider) // bpm change was from a slider, this is easier typically than circle -> circle effectiveRatio *= 0.25; - if (previousIslandSize == islandSize) // repeated island size (ex: triplet -> triplet) - effectiveRatio *= 0.25; - - if (previousIslandSize % 2 == islandSize % 2) // repeated island polartiy (2 -> 4, 3 -> 5) + if (previousIsland.Deltas.Count % 2 == island.Deltas.Count % 2) // repeated island polartiy (2 -> 4, 3 -> 5) effectiveRatio *= 0.50; - if (lastDelta > prevDelta + 10 && prevDelta > currDelta + 10) // previous increase happened a note ago, 1/1->1/2-1/4, dont want to buff this. + if (lastDelta > prevDelta + deltaDifferenceEpsilon && prevDelta > currDelta + deltaDifferenceEpsilon) // previous increase happened a note ago, 1/1->1/2-1/4, dont want to buff this. effectiveRatio *= 0.125; - rhythmComplexitySum += Math.Sqrt(effectiveRatio * startRatio) * currHistoricalDecay * Math.Sqrt(4 + islandSize) / 2 * Math.Sqrt(4 + previousIslandSize) / 2; + if (islandCounts.ContainsKey(island)) + { + islandCounts[island]++; + + // repeated island (ex: triplet -> triplet) + double power = Math.Max(0.75, logistic(island.AverageDelta(), 3, 0.15, 9)); + effectiveRatio *= Math.Pow(1.0 / islandCounts[island], power); + } + else + { + islandCounts.Add(island, 1); + } + + rhythmComplexitySum += Math.Sqrt(effectiveRatio * startRatio) * currHistoricalDecay; startRatio = effectiveRatio; - previousIslandSize = islandSize; // log the last island size. + previousIsland = island; - if (prevDelta * 1.25 < currDelta) // we're slowing down, stop counting + if (prevDelta + deltaDifferenceEpsilon < currDelta) // we're slowing down, stop counting firstDeltaSwitch = false; // if we're speeding up, this stays true and we keep counting island size. - islandSize = 1; + island = new Island((int)currDelta, deltaDifferenceEpsilon); } } - else if (prevDelta > 1.25 * currDelta) // we want to be speeding up. + else if (prevDelta > currDelta + deltaDifferenceEpsilon) // we want to be speeding up. { // Begin counting island until we change speed again. firstDeltaSwitch = true; startRatio = effectiveRatio; - islandSize = 1; + + island = new Island((int)currDelta, deltaDifferenceEpsilon); } lastObj = prevObj; @@ -108,5 +171,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators return Math.Sqrt(4 + rhythmComplexitySum * rhythm_multiplier) / 2; //produces multiplier that can be applied to strain. range [1, infinity) (not really though) } + + private static double logistic(double x, double maxValue, double multiplier, double offset) => (maxValue / (1 + Math.Pow(Math.E, offset - multiplier * x))); } } diff --git a/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs b/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs index 0e537632b1..95535274c1 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs @@ -20,7 +20,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing /// public const int NORMALISED_RADIUS = 50; // Change radius to 50 to make 100 the diameter. Easier for mental maths. - private const int min_delta_time = 25; + public const int MIN_DELTA_TIME = 25; + private const float maximum_slider_radius = NORMALISED_RADIUS * 2.4f; private const float assumed_slider_radius = NORMALISED_RADIUS * 1.8f; @@ -93,7 +94,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing this.lastObject = (OsuHitObject)lastObject; // Capped to 25ms to prevent difficulty calculation breaking from simultaneous objects. - StrainTime = Math.Max(DeltaTime, min_delta_time); + StrainTime = Math.Max(DeltaTime, MIN_DELTA_TIME); if (BaseObject is Slider sliderObject) { @@ -143,7 +144,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing computeSliderCursorPosition(currentSlider); // Bonus for repeat sliders until a better per nested object strain system can be achieved. TravelDistance = currentSlider.LazyTravelDistance * (float)Math.Pow(1 + currentSlider.RepeatCount / 2.5, 1.0 / 2.5); - TravelTime = Math.Max(currentSlider.LazyTravelTime / clockRate, min_delta_time); + TravelTime = Math.Max(currentSlider.LazyTravelTime / clockRate, MIN_DELTA_TIME); } // We don't need to calculate either angle or distance when one of the last->curr objects is a spinner @@ -167,8 +168,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing if (lastObject is Slider lastSlider) { - double lastTravelTime = Math.Max(lastSlider.LazyTravelTime / clockRate, min_delta_time); - MinimumJumpTime = Math.Max(StrainTime - lastTravelTime, min_delta_time); + double lastTravelTime = Math.Max(lastSlider.LazyTravelTime / clockRate, MIN_DELTA_TIME); + MinimumJumpTime = Math.Max(StrainTime - lastTravelTime, MIN_DELTA_TIME); // // There are two types of slider-to-object patterns to consider in order to better approximate the real movement a player will take to jump between the hitobjects. From 6cdcd6136d157f91b17a4a7a6ff579a516705ac4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 15 Jul 2024 21:02:54 +0900 Subject: [PATCH 1504/2493] Fix editor toolboxes being incorrectly chopped --- osu.Game/Rulesets/Edit/ExpandingToolboxContainer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Edit/ExpandingToolboxContainer.cs b/osu.Game/Rulesets/Edit/ExpandingToolboxContainer.cs index 36cbf49885..c2ab5a6eb9 100644 --- a/osu.Game/Rulesets/Edit/ExpandingToolboxContainer.cs +++ b/osu.Game/Rulesets/Edit/ExpandingToolboxContainer.cs @@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Edit RelativeSizeAxes = Axes.Y; FillFlow.Spacing = new Vector2(5); - Padding = new MarginPadding { Vertical = 5 }; + FillFlow.Padding = new MarginPadding { Vertical = 5 }; } protected override bool ReceivePositionalInputAtSubTree(Vector2 screenSpacePos) => base.ReceivePositionalInputAtSubTree(screenSpacePos) && anyToolboxHovered(screenSpacePos); From 67cb4a2d02e47a2c8bfd493c4def3c19538057e8 Mon Sep 17 00:00:00 2001 From: StanR Date: Mon, 15 Jul 2024 22:54:25 +0500 Subject: [PATCH 1505/2493] InspectCode --- osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs index 39c7572c85..60bc53e7a1 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs @@ -172,6 +172,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators return Math.Sqrt(4 + rhythmComplexitySum * rhythm_multiplier) / 2; //produces multiplier that can be applied to strain. range [1, infinity) (not really though) } - private static double logistic(double x, double maxValue, double multiplier, double offset) => (maxValue / (1 + Math.Pow(Math.E, offset - multiplier * x))); + private static double logistic(double x, double maxValue, double multiplier, double offset) => (maxValue / (1 + Math.Pow(Math.E, offset - (multiplier * x)))); } } From 1083e71ce6aadd9b9921b7dfbc8f1502390ccfd2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 16 Jul 2024 03:02:04 +0900 Subject: [PATCH 1506/2493] Fix potential crash when exiting daily challenge screen Without the schedule this will potentially run after disposal of the local drawable hierarchy. Closes https://github.com/ppy/osu/issues/28875. --- .../OnlinePlay/DailyChallenge/DailyChallengeLeaderboard.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeLeaderboard.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeLeaderboard.cs index 4d4ae755fc..2b2c3a5e1f 100644 --- a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeLeaderboard.cs +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeLeaderboard.cs @@ -122,7 +122,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge { var request = new IndexPlaylistScoresRequest(room.RoomID.Value!.Value, playlistItem.ID); - request.Success += req => + request.Success += req => Schedule(() => { var best = req.Scores.Select(s => s.CreateScoreInfo(scoreManager, rulesets, playlistItem, beatmap.Value.BeatmapInfo)).ToArray(); var userBest = req.UserScore?.CreateScoreInfo(scoreManager, rulesets, playlistItem, beatmap.Value.BeatmapInfo); @@ -165,7 +165,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge } userBestHeader.FadeTo(userBest == null ? 0 : 1); - }; + }); loadingLayer.Show(); scoreFlow.FadeTo(0.5f, 400, Easing.OutQuint); From fcc8e7be8a711c9356f0f6df725a1aad6aa86162 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 16 Jul 2024 12:09:09 +0900 Subject: [PATCH 1507/2493] Invert condition to reduce number of brain flips required --- osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index 7b54d0ae54..0aec0a9d4f 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -192,7 +192,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty // This percentage only considers HitCircles of any value - in this part of the calculation we focus on hitting the timing hit window. double betterAccuracyPercentage; int amountHitObjectsWithAccuracy = attributes.HitCircleCount; - if (!score.Mods.Any(h => h is OsuModClassic cl && cl.NoSliderHeadAccuracy.Value)) + if (score.Mods.All(h => h is not OsuModClassic cl || !cl.NoSliderHeadAccuracy.Value)) amountHitObjectsWithAccuracy += attributes.SliderCount; if (amountHitObjectsWithAccuracy > 0) From ced11e69496eb4cdd6790d8bcf4689420a24c7e1 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 16 Jul 2024 12:23:46 +0900 Subject: [PATCH 1508/2493] Even better readability --- osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index 0aec0a9d4f..a4ebcd15a4 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -192,7 +192,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty // This percentage only considers HitCircles of any value - in this part of the calculation we focus on hitting the timing hit window. double betterAccuracyPercentage; int amountHitObjectsWithAccuracy = attributes.HitCircleCount; - if (score.Mods.All(h => h is not OsuModClassic cl || !cl.NoSliderHeadAccuracy.Value)) + if (score.Mods.OfType().All(m => !m.NoSliderHeadAccuracy.Value)) amountHitObjectsWithAccuracy += attributes.SliderCount; if (amountHitObjectsWithAccuracy > 0) From bd4f3e28d90127b9b3a99d9594bb9056c7dea20f Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 16 Jul 2024 17:32:59 +0900 Subject: [PATCH 1509/2493] Fix judgement animation getting cut early --- osu.Game/Rulesets/Judgements/DrawableJudgement.cs | 5 +++++ osu.Game/Rulesets/UI/JudgementContainer.cs | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Judgements/DrawableJudgement.cs b/osu.Game/Rulesets/Judgements/DrawableJudgement.cs index 37a9766b71..189be44033 100644 --- a/osu.Game/Rulesets/Judgements/DrawableJudgement.cs +++ b/osu.Game/Rulesets/Judgements/DrawableJudgement.cs @@ -7,6 +7,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Pooling; +using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Scoring; using osu.Game.Skinning; @@ -25,6 +26,8 @@ namespace osu.Game.Rulesets.Judgements public DrawableHitObject? JudgedObject { get; private set; } + public HitObject? JudgedHitObject { get; private set; } + public override bool RemoveCompletedTransforms => false; protected SkinnableDrawable? JudgementBody { get; private set; } @@ -98,6 +101,7 @@ namespace osu.Game.Rulesets.Judgements { Result = result; JudgedObject = judgedObject; + JudgedHitObject = judgedObject?.HitObject; } protected override void FreeAfterUse() @@ -105,6 +109,7 @@ namespace osu.Game.Rulesets.Judgements base.FreeAfterUse(); JudgedObject = null; + JudgedHitObject = null; } protected override void PrepareForUse() diff --git a/osu.Game/Rulesets/UI/JudgementContainer.cs b/osu.Game/Rulesets/UI/JudgementContainer.cs index 886dd34fc7..86ab213ca1 100644 --- a/osu.Game/Rulesets/UI/JudgementContainer.cs +++ b/osu.Game/Rulesets/UI/JudgementContainer.cs @@ -16,7 +16,7 @@ namespace osu.Game.Rulesets.UI // remove any existing judgements for the judged object. // this can be the case when rewinding. - RemoveAll(c => c.JudgedObject == judgement.JudgedObject, false); + RemoveAll(c => c.JudgedHitObject == judgement.JudgedHitObject, false); base.Add(judgement); } From 063377f47cd96bfff5a27a7c5e4c10220badc5ac Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 16 Jul 2024 17:45:25 +0900 Subject: [PATCH 1510/2493] 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 9fd0df3036..fe0a452e92 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -10,7 +10,7 @@ true - + diff --git a/osu.iOS.props b/osu.iOS.props index 48d9c2564a..acfcae7c93 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -23,6 +23,6 @@ iossimulator-x64 - + From c18814817b7d5b1f907574b6cca89f4fdc0efa53 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Tue, 16 Jul 2024 11:17:54 +0200 Subject: [PATCH 1511/2493] fix test --- osu.Game.Rulesets.Osu.Tests/Editor/TestScenePreciseRotation.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePreciseRotation.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePreciseRotation.cs index 30e0dbbf2e..d14bd1fc87 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePreciseRotation.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePreciseRotation.cs @@ -116,7 +116,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor () => EditorBeatmap.HitObjects.OfType().ElementAt(1).Position, () => Is.EqualTo(OsuPlayfield.BASE_SIZE - new Vector2(200))); - AddStep("change rotation origin", () => getPopover().ChildrenOfType().ElementAt(1).TriggerClick()); + AddStep("change rotation origin", () => getPopover().ChildrenOfType().ElementAt(2).TriggerClick()); AddAssert("first object rotated 90deg around selection centre", () => EditorBeatmap.HitObjects.OfType().ElementAt(0).Position, () => Is.EqualTo(new Vector2(200, 200))); AddAssert("second object rotated 90deg around selection centre", From d4ea604ad081788028352a55327e8fef72be2d91 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 16 Jul 2024 18:14:37 +0900 Subject: [PATCH 1512/2493] Add test --- .../Gameplay/TestSceneJudgementContainer.cs | 98 +++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 osu.Game.Tests/Visual/Gameplay/TestSceneJudgementContainer.cs diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneJudgementContainer.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneJudgementContainer.cs new file mode 100644 index 0000000000..508877859c --- /dev/null +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneJudgementContainer.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 NUnit.Framework; +using osu.Framework.Graphics; +using osu.Framework.Testing; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Osu.Judgements; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Osu.Objects.Drawables; +using osu.Game.Rulesets.Scoring; +using osu.Game.Rulesets.UI; + +namespace osu.Game.Tests.Visual.Gameplay +{ + public class TestSceneJudgementContainer : OsuTestScene + { + private JudgementContainer judgementContainer = null!; + + [SetUpSteps] + public void SetUp() + { + AddStep("create judgement container", () => Child = judgementContainer = new JudgementContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre + }); + } + + [Test] + public void TestJudgementFromSameHitObjectIsRemoved() + { + DrawableHitCircle drawableHitCircle1 = null!; + DrawableHitCircle drawableHitCircle2 = null!; + + AddStep("create hit circles", () => + { + Add(drawableHitCircle1 = new DrawableHitCircle(createHitCircle())); + Add(drawableHitCircle2 = new DrawableHitCircle(createHitCircle())); + }); + + int judgementCount = 0; + + AddStep("judge the same hitobject twice via different drawables", () => + { + addDrawableJudgement(drawableHitCircle1); + drawableHitCircle2.Apply(drawableHitCircle1.HitObject); + addDrawableJudgement(drawableHitCircle2); + judgementCount = judgementContainer.Count; + }); + + AddAssert("one judgement in container", () => judgementCount, () => Is.EqualTo(1)); + } + + [Test] + public void TestJudgementFromDifferentHitObjectIsNotRemoved() + { + DrawableHitCircle drawableHitCircle = null!; + + AddStep("create hit circle", () => Add(drawableHitCircle = new DrawableHitCircle(createHitCircle()))); + + int judgementCount = 0; + + AddStep("judge two hitobjects via the same drawable", () => + { + addDrawableJudgement(drawableHitCircle); + drawableHitCircle.Apply(createHitCircle()); + addDrawableJudgement(drawableHitCircle); + judgementCount = judgementContainer.Count; + }); + + AddAssert("two judgements in container", () => judgementCount, () => Is.EqualTo(2)); + } + + private void addDrawableJudgement(DrawableHitObject drawableHitObject) + { + var judgement = new DrawableOsuJudgement(); + + judgement.Apply(new JudgementResult(drawableHitObject.HitObject, new OsuJudgement()) + { + Type = HitResult.Great, + TimeOffset = Time.Current + }, drawableHitObject); + + judgementContainer.Add(judgement); + } + + private HitCircle createHitCircle() + { + var circle = new HitCircle(); + circle.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); + return circle; + } + } +} From 4ad7d900c17ed3de6e405f6f25d29f2b5f40bb5c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 16 Jul 2024 18:20:33 +0900 Subject: [PATCH 1513/2493] Fix incorrect editor screen padding --- osu.Game/Screens/Edit/Editor.cs | 2 +- osu.Game/Screens/Edit/Timing/ControlPointTable.cs | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index cab0ba9bcb..d40db329ec 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -328,7 +328,7 @@ namespace osu.Game.Screens.Edit { Name = "Screen container", RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding { Top = 40, Bottom = 40 }, + Padding = new MarginPadding { Top = 40, Bottom = 50 }, Child = screenContainer = new Container { RelativeSizeAxes = Axes.Both, diff --git a/osu.Game/Screens/Edit/Timing/ControlPointTable.cs b/osu.Game/Screens/Edit/Timing/ControlPointTable.cs index 75b86650af..2204fabf57 100644 --- a/osu.Game/Screens/Edit/Timing/ControlPointTable.cs +++ b/osu.Game/Screens/Edit/Timing/ControlPointTable.cs @@ -104,10 +104,11 @@ namespace osu.Game.Screens.Edit.Timing // child items valid coordinates from the start, so ballpark something similar // using estimated row height. var row = Items.FlowingChildren.SingleOrDefault(item => item.Row.Equals(val.NewValue)); + if (row == null) return; - float minPos = Items.GetLayoutPosition(row) * row_height; + float minPos = row.Y; float maxPos = minPos + row_height; if (minPos < Scroll.Current) From 76d016df348025562fe028a249945d072a90b1ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 16 Jul 2024 11:31:16 +0200 Subject: [PATCH 1514/2493] Fix code inspection --- osu.Game.Tests/Visual/Gameplay/TestSceneJudgementContainer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneJudgementContainer.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneJudgementContainer.cs index 508877859c..0ba67c0bb0 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneJudgementContainer.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneJudgementContainer.cs @@ -16,7 +16,7 @@ using osu.Game.Rulesets.UI; namespace osu.Game.Tests.Visual.Gameplay { - public class TestSceneJudgementContainer : OsuTestScene + public partial class TestSceneJudgementContainer : OsuTestScene { private JudgementContainer judgementContainer = null!; From f1325386f071adcf60b5666c2638e16cf07de671 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 16 Jul 2024 18:32:54 +0900 Subject: [PATCH 1515/2493] Fix summary timeline timing points having x position applied twice --- .../Components/Timelines/Summary/Parts/GroupVisualisation.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/GroupVisualisation.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/GroupVisualisation.cs index e01900b129..b872c3725c 100644 --- a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/GroupVisualisation.cs +++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/GroupVisualisation.cs @@ -41,6 +41,8 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts case TimingControlPoint: AddInternal(new ControlPointVisualisation(point) { + // importantly, override the x position being set since we do that above. + X = 0, Y = -0.4f, }); break; From ae5b0aa54b463ace43b0a95e02576b33d27f651d Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 16 Jul 2024 19:59:13 +0900 Subject: [PATCH 1516/2493] Fix BackgroundDataStoreProcessor test failure --- osu.Game.Tests/Database/BackgroundDataStoreProcessorTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Database/BackgroundDataStoreProcessorTests.cs b/osu.Game.Tests/Database/BackgroundDataStoreProcessorTests.cs index e960995c45..70b6e32363 100644 --- a/osu.Game.Tests/Database/BackgroundDataStoreProcessorTests.cs +++ b/osu.Game.Tests/Database/BackgroundDataStoreProcessorTests.cs @@ -172,7 +172,7 @@ namespace osu.Game.Tests.Database Ruleset = r.All().First(), }) { - TotalScoreVersion = 30000002, + TotalScoreVersion = 30000013, IsLegacyScore = true, }); }); @@ -181,7 +181,7 @@ namespace osu.Game.Tests.Database AddStep("Run background processor", () => Add(new TestBackgroundDataStoreProcessor())); AddUntilStep("Score marked as failed", () => Realm.Run(r => r.Find(scoreInfo.ID)!.BackgroundReprocessingFailed), () => Is.True); - AddAssert("Score version not upgraded", () => Realm.Run(r => r.Find(scoreInfo.ID)!.TotalScoreVersion), () => Is.EqualTo(30000002)); + AddAssert("Score version not upgraded", () => Realm.Run(r => r.Find(scoreInfo.ID)!.TotalScoreVersion), () => Is.EqualTo(30000013)); } [Test] From 7dc006f9bab4f22e326c5692a40a2afc5bfdc566 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Tue, 16 Jul 2024 13:19:01 +0200 Subject: [PATCH 1517/2493] fix horizontal flip rotation --- osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs index 1334dbdbec..2dc43deee1 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs @@ -125,7 +125,7 @@ namespace osu.Game.Rulesets.Osu.Edit // however it's still possible to achieve that flip by combining multiple flips over the other axes. flipAxis = direction == Direction.Vertical ? GeometryUtils.RotateVector(Vector2.UnitX, -((gridToolbox.GridLinesRotation.Value + 390) % 60 + 60)) - : GeometryUtils.RotateVector(Vector2.UnitX, -((gridToolbox.GridLinesRotation.Value + 390) % 60 - 60)); + : GeometryUtils.RotateVector(Vector2.UnitX, -((gridToolbox.GridLinesRotation.Value + 360) % 60 - 30)); break; } } From 6db135279fa27cf8c2abfac1c6bb27a2688a1269 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 16 Jul 2024 14:03:33 +0200 Subject: [PATCH 1518/2493] Restore test coverage of original fail case --- .../Database/BackgroundDataStoreProcessorTests.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Database/BackgroundDataStoreProcessorTests.cs b/osu.Game.Tests/Database/BackgroundDataStoreProcessorTests.cs index 70b6e32363..65a8bcd3c2 100644 --- a/osu.Game.Tests/Database/BackgroundDataStoreProcessorTests.cs +++ b/osu.Game.Tests/Database/BackgroundDataStoreProcessorTests.cs @@ -157,8 +157,9 @@ namespace osu.Game.Tests.Database AddAssert("Score not marked as failed", () => Realm.Run(r => r.Find(scoreInfo.ID)!.BackgroundReprocessingFailed), () => Is.False); } - [Test] - public void TestScoreUpgradeFailed() + [TestCase(30000002)] + [TestCase(30000013)] + public void TestScoreUpgradeFailed(int scoreVersion) { ScoreInfo scoreInfo = null!; @@ -172,7 +173,7 @@ namespace osu.Game.Tests.Database Ruleset = r.All().First(), }) { - TotalScoreVersion = 30000013, + TotalScoreVersion = scoreVersion, IsLegacyScore = true, }); }); @@ -181,7 +182,7 @@ namespace osu.Game.Tests.Database AddStep("Run background processor", () => Add(new TestBackgroundDataStoreProcessor())); AddUntilStep("Score marked as failed", () => Realm.Run(r => r.Find(scoreInfo.ID)!.BackgroundReprocessingFailed), () => Is.True); - AddAssert("Score version not upgraded", () => Realm.Run(r => r.Find(scoreInfo.ID)!.TotalScoreVersion), () => Is.EqualTo(30000013)); + AddAssert("Score version not upgraded", () => Realm.Run(r => r.Find(scoreInfo.ID)!.TotalScoreVersion), () => Is.EqualTo(scoreVersion)); } [Test] From 53b6f9e3854db7933ec06a28164078ddc566fcbb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 16 Jul 2024 14:01:33 +0200 Subject: [PATCH 1519/2493] Fix test not waiting properly for background processing to complete --- osu.Game.Tests/Database/BackgroundDataStoreProcessorTests.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Database/BackgroundDataStoreProcessorTests.cs b/osu.Game.Tests/Database/BackgroundDataStoreProcessorTests.cs index 65a8bcd3c2..f9f9fa2622 100644 --- a/osu.Game.Tests/Database/BackgroundDataStoreProcessorTests.cs +++ b/osu.Game.Tests/Database/BackgroundDataStoreProcessorTests.cs @@ -179,7 +179,9 @@ namespace osu.Game.Tests.Database }); }); - AddStep("Run background processor", () => Add(new TestBackgroundDataStoreProcessor())); + TestBackgroundDataStoreProcessor processor = null!; + AddStep("Run background processor", () => Add(processor = new TestBackgroundDataStoreProcessor())); + AddUntilStep("Wait for completion", () => processor.Completed); AddUntilStep("Score marked as failed", () => Realm.Run(r => r.Find(scoreInfo.ID)!.BackgroundReprocessingFailed), () => Is.True); AddAssert("Score version not upgraded", () => Realm.Run(r => r.Find(scoreInfo.ID)!.TotalScoreVersion), () => Is.EqualTo(scoreVersion)); From 7ba1f142e573f8ca8173a8f64f139b4fd240be52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 16 Jul 2024 14:01:50 +0200 Subject: [PATCH 1520/2493] Fix rank upgrade path upgrading scores that failed background reprocessing earlier --- osu.Game/Database/BackgroundDataStoreProcessor.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Database/BackgroundDataStoreProcessor.cs b/osu.Game/Database/BackgroundDataStoreProcessor.cs index 7074c89b84..16ff766ea4 100644 --- a/osu.Game/Database/BackgroundDataStoreProcessor.cs +++ b/osu.Game/Database/BackgroundDataStoreProcessor.cs @@ -389,7 +389,7 @@ namespace osu.Game.Database HashSet scoreIds = realmAccess.Run(r => new HashSet( r.All() - .Where(s => s.TotalScoreVersion < 30000013) // last total score version with a significant change to ranks + .Where(s => s.TotalScoreVersion < 30000013 && !s.BackgroundReprocessingFailed) // last total score version with a significant change to ranks .AsEnumerable() // must be done after materialisation, as realm doesn't support // filtering on nested property predicates or projection via `.Select()` From 4c1f902969f82ecc4e974e869110281601079d2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 17 Jul 2024 11:46:17 +0200 Subject: [PATCH 1521/2493] Do not allow working beatmap to switch to protected beatmap in song select Principal fix to https://github.com/ppy/osu/issues/28880. --- osu.Game/Screens/Select/SongSelect.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index ecf8210002..14c4a34d14 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -505,6 +505,13 @@ namespace osu.Game.Screens.Select var beatmap = e?.NewValue ?? Beatmap.Value; if (beatmap is DummyWorkingBeatmap || !this.IsCurrentScreen()) return; + if (beatmap.BeatmapSetInfo.Protected && e != null) + { + Logger.Log($"Denying working beatmap switch to protected beatmap {beatmap}"); + Beatmap.Value = e.OldValue; + return; + } + Logger.Log($"Song select working beatmap updated to {beatmap}"); if (!Carousel.SelectBeatmap(beatmap.BeatmapInfo, false)) From 1ffc34b6518f55079728cd61aa47c5cab2fbc4e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 17 Jul 2024 11:46:59 +0200 Subject: [PATCH 1522/2493] Do not show protected beatmaps in playlist overlay Secondary fix to https://github.com/ppy/osu/issues/28880. --- osu.Game/Overlays/Music/PlaylistOverlay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Music/PlaylistOverlay.cs b/osu.Game/Overlays/Music/PlaylistOverlay.cs index 2d03a4a26d..b49c794aa3 100644 --- a/osu.Game/Overlays/Music/PlaylistOverlay.cs +++ b/osu.Game/Overlays/Music/PlaylistOverlay.cs @@ -102,7 +102,7 @@ namespace osu.Game.Overlays.Music { base.LoadComplete(); - beatmapSubscription = realm.RegisterForNotifications(r => r.All().Where(s => !s.DeletePending), beatmapsChanged); + beatmapSubscription = realm.RegisterForNotifications(r => r.All().Where(s => !s.DeletePending && !s.Protected), beatmapsChanged); list.Items.BindTo(beatmapSets); beatmap.BindValueChanged(working => list.SelectedSet.Value = working.NewValue.BeatmapSetInfo.ToLive(realm), true); From e4ff6b5c8b5f2a384f636e26f917c9f1e529ff2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 17 Jul 2024 12:02:42 +0200 Subject: [PATCH 1523/2493] Add flags allowing excluding protected beatmaps from consideration in music controller This means that the protected beatmap can not be skipped forward/back to. Incidentally closes https://github.com/ppy/osu/issues/23199. --- osu.Game/Overlays/MusicController.cs | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/osu.Game/Overlays/MusicController.cs b/osu.Game/Overlays/MusicController.cs index 116e60a014..b6553779bc 100644 --- a/osu.Game/Overlays/MusicController.cs +++ b/osu.Game/Overlays/MusicController.cs @@ -133,7 +133,7 @@ namespace osu.Game.Overlays return; Logger.Log($"{nameof(MusicController)} skipping next track to {nameof(EnsurePlayingSomething)}"); - NextTrack(); + NextTrack(allowProtectedTracks: true); } else if (!IsPlaying) { @@ -207,9 +207,10 @@ namespace osu.Game.Overlays /// Play the previous track or restart the current track if it's current time below . /// /// Invoked when the operation has been performed successfully. - public void PreviousTrack(Action? onSuccess = null) => Schedule(() => + /// Whether to include beatmap sets when navigating. + public void PreviousTrack(Action? onSuccess = null, bool allowProtectedTracks = false) => Schedule(() => { - PreviousTrackResult res = prev(); + PreviousTrackResult res = prev(allowProtectedTracks); if (res != PreviousTrackResult.None) onSuccess?.Invoke(res); }); @@ -217,8 +218,9 @@ namespace osu.Game.Overlays /// /// Play the previous track or restart the current track if it's current time below . /// + /// Whether to include beatmap sets when navigating. /// The that indicate the decided action. - private PreviousTrackResult prev() + private PreviousTrackResult prev(bool allowProtectedTracks) { if (beatmap.Disabled || !AllowTrackControl.Value) return PreviousTrackResult.None; @@ -233,8 +235,8 @@ namespace osu.Game.Overlays queuedDirection = TrackChangeDirection.Prev; - var playableSet = getBeatmapSets().AsEnumerable().TakeWhile(i => !i.Equals(current?.BeatmapSetInfo)).LastOrDefault() - ?? getBeatmapSets().LastOrDefault(); + var playableSet = getBeatmapSets().AsEnumerable().TakeWhile(i => !i.Equals(current?.BeatmapSetInfo)).LastOrDefault(s => !s.Protected || allowProtectedTracks) + ?? getBeatmapSets().AsEnumerable().LastOrDefault(s => !s.Protected || allowProtectedTracks); if (playableSet != null) { @@ -250,10 +252,11 @@ namespace osu.Game.Overlays /// Play the next random or playlist track. /// /// Invoked when the operation has been performed successfully. + /// Whether to include beatmap sets when navigating. /// A of the operation. - public void NextTrack(Action? onSuccess = null) => Schedule(() => + public void NextTrack(Action? onSuccess = null, bool allowProtectedTracks = false) => Schedule(() => { - bool res = next(); + bool res = next(allowProtectedTracks); if (res) onSuccess?.Invoke(); }); @@ -306,15 +309,15 @@ namespace osu.Game.Overlays Scheduler.AddDelayed(() => duckOperation.Dispose(), delayUntilRestore); } - private bool next() + private bool next(bool allowProtectedTracks) { if (beatmap.Disabled || !AllowTrackControl.Value) return false; queuedDirection = TrackChangeDirection.Next; - var playableSet = getBeatmapSets().AsEnumerable().SkipWhile(i => !i.Equals(current?.BeatmapSetInfo)).ElementAtOrDefault(1) - ?? getBeatmapSets().FirstOrDefault(); + var playableSet = getBeatmapSets().AsEnumerable().SkipWhile(i => !i.Equals(current?.BeatmapSetInfo) && (!i.Protected || allowProtectedTracks)).ElementAtOrDefault(1) + ?? getBeatmapSets().AsEnumerable().FirstOrDefault(i => !i.Protected || allowProtectedTracks); var playableBeatmap = playableSet?.Beatmaps.FirstOrDefault(); @@ -432,7 +435,7 @@ namespace osu.Game.Overlays private void onTrackCompleted() { if (!CurrentTrack.Looping && !beatmap.Disabled && AllowTrackControl.Value) - NextTrack(); + NextTrack(allowProtectedTracks: true); } private bool applyModTrackAdjustments; From c4141fff07a4378a0dc8c8b94fd4f64715367511 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 17 Jul 2024 14:47:15 +0300 Subject: [PATCH 1524/2493] Fix storyboard sprites leaving gaps on edges when resolving from an atlas --- osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs index c5d70ddecc..e25c915d8b 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs @@ -100,14 +100,15 @@ namespace osu.Game.Storyboards.Drawables skinSourceChanged(); } else - Texture = textureStore.Get(Sprite.Path); + Texture = textureStore.Get(Sprite.Path, WrapMode.ClampToEdge, WrapMode.ClampToEdge); Sprite.ApplyTransforms(this); } private void skinSourceChanged() { - Texture = skin.GetTexture(Sprite.Path) ?? textureStore.Get(Sprite.Path); + Texture = skin.GetTexture(Sprite.Path, WrapMode.ClampToEdge, WrapMode.ClampToEdge) ?? + textureStore.Get(Sprite.Path, WrapMode.ClampToEdge, WrapMode.ClampToEdge); // Setting texture will only update the size if it's zero. // So let's force an explicit update. From 3006bae0d8ea9d42ed887862dcb6e56e0b9be081 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 17 Jul 2024 14:11:35 +0200 Subject: [PATCH 1525/2493] Send client-generated session GUID for identification purposes This is the first half of a change that *may* fix https://github.com/ppy/osu/issues/26338 (it definitely fixes *one case* where the issue happens, but I'm not sure if it will cover all of them). As described in the issue thread, using the `jti` claim from the JWT used for authorisation seemed like a decent idea. However, upon closer inspection the scheme falls over badly in a specific scenario where: 1. A client instance connects to spectator server using JWT A. 2. At some point, JWT A expires, and is silently rotated by the game in exchange for JWT B. The spectator server knows nothing of this, and continues to only track JWT A, including the old `jti` claim in said JWT. 3. At some later point, the client's connection to one of the spectator server hubs drops out. A reconnection is automatically attempted, *but* it is attempted using JWT B. The spectator server was not aware of JWT B until now, and said JWT has a different `jti` claim than the old one, so to the spectator server, it looks like a completely different client connecting, which boots the user out of their account. This PR adds a per-session GUID which is sent in a HTTP header on every connection attempt to spectator server. This GUID will be used instead of the `jti` claim in JWTs as a persistent identifier of a single user's single lazer session, which bypasses the failure scenario described above. I don't think any stronger primitive than this is required. As far as I can tell this is as strong a protection as the JWT was (which is to say, not *very* strong), and doing this removes a lot of weird complexity that would be otherwise incurred by attempting to have client ferry all of its newly issued JWTs to the server so that it can be aware of them. --- osu.Game/Online/API/APIAccess.cs | 2 ++ osu.Game/Online/API/DummyAPIAccess.cs | 2 ++ osu.Game/Online/API/IAPIProvider.cs | 6 ++++++ osu.Game/Online/HubClientConnector.cs | 8 ++++++-- 4 files changed, 16 insertions(+), 2 deletions(-) diff --git a/osu.Game/Online/API/APIAccess.cs b/osu.Game/Online/API/APIAccess.cs index 923f841bd8..0cf344ecaf 100644 --- a/osu.Game/Online/API/APIAccess.cs +++ b/osu.Game/Online/API/APIAccess.cs @@ -164,6 +164,8 @@ namespace osu.Game.Online.API public string AccessToken => authentication.RequestAccessToken(); + public Guid SessionIdentifier { get; } = Guid.NewGuid(); + /// /// Number of consecutive requests which failed due to network issues. /// diff --git a/osu.Game/Online/API/DummyAPIAccess.cs b/osu.Game/Online/API/DummyAPIAccess.cs index 960941fc05..0af76537cd 100644 --- a/osu.Game/Online/API/DummyAPIAccess.cs +++ b/osu.Game/Online/API/DummyAPIAccess.cs @@ -39,6 +39,8 @@ namespace osu.Game.Online.API public string AccessToken => "token"; + public Guid SessionIdentifier { get; } = Guid.NewGuid(); + /// public bool IsLoggedIn => State.Value > APIState.Offline; diff --git a/osu.Game/Online/API/IAPIProvider.cs b/osu.Game/Online/API/IAPIProvider.cs index 7b95b68ec3..d8194dc32b 100644 --- a/osu.Game/Online/API/IAPIProvider.cs +++ b/osu.Game/Online/API/IAPIProvider.cs @@ -44,6 +44,12 @@ namespace osu.Game.Online.API ///
string AccessToken { get; } + /// + /// Used as an identifier of a single local lazer session. + /// Sent across the wire for the purposes of concurrency control to spectator server. + /// + Guid SessionIdentifier { get; } + /// /// Returns whether the local user is logged in. /// diff --git a/osu.Game/Online/HubClientConnector.cs b/osu.Game/Online/HubClientConnector.cs index 9d414deade..dc9ed7cc2e 100644 --- a/osu.Game/Online/HubClientConnector.cs +++ b/osu.Game/Online/HubClientConnector.cs @@ -19,6 +19,9 @@ namespace osu.Game.Online { public const string SERVER_SHUTDOWN_MESSAGE = "Server is shutting down."; + public const string VERSION_HASH_HEADER = @"OsuVersionHash"; + public const string CLIENT_SESSION_ID_HEADER = @"X-Client-Session-ID"; + /// /// Invoked whenever a new hub connection is built, to configure it before it's started. /// @@ -68,8 +71,9 @@ namespace osu.Game.Online options.Proxy.Credentials = CredentialCache.DefaultCredentials; } - options.Headers.Add("Authorization", $"Bearer {API.AccessToken}"); - options.Headers.Add("OsuVersionHash", versionHash); + options.Headers.Add(@"Authorization", @$"Bearer {API.AccessToken}"); + options.Headers.Add(VERSION_HASH_HEADER, versionHash); + options.Headers.Add(CLIENT_SESSION_ID_HEADER, API.SessionIdentifier.ToString()); }); if (RuntimeFeature.IsDynamicCodeCompiled && preferMessagePack) From 2a601ce9617d0ef55bcb36b4de4e87278179b72e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 17 Jul 2024 16:21:33 +0200 Subject: [PATCH 1526/2493] Also send version hash header under more accepted convention of name --- osu.Game/Online/HubClientConnector.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Online/HubClientConnector.cs b/osu.Game/Online/HubClientConnector.cs index dc9ed7cc2e..9288a32052 100644 --- a/osu.Game/Online/HubClientConnector.cs +++ b/osu.Game/Online/HubClientConnector.cs @@ -19,7 +19,7 @@ namespace osu.Game.Online { public const string SERVER_SHUTDOWN_MESSAGE = "Server is shutting down."; - public const string VERSION_HASH_HEADER = @"OsuVersionHash"; + public const string VERSION_HASH_HEADER = @"X-Osu-Version-Hash"; public const string CLIENT_SESSION_ID_HEADER = @"X-Client-Session-ID"; /// @@ -72,6 +72,8 @@ namespace osu.Game.Online } options.Headers.Add(@"Authorization", @$"Bearer {API.AccessToken}"); + // non-standard header name kept for backwards compatibility, can be removed after server side has migrated to `VERSION_HASH_HEADER` + options.Headers.Add(@"OsuVersionHash", versionHash); options.Headers.Add(VERSION_HASH_HEADER, versionHash); options.Headers.Add(CLIENT_SESSION_ID_HEADER, API.SessionIdentifier.ToString()); }); From 0bc14ba646e8fc7adfa9d8e3b53d0bf1341239e0 Mon Sep 17 00:00:00 2001 From: Layendan Date: Wed, 17 Jul 2024 12:45:20 -0700 Subject: [PATCH 1527/2493] Add favourite button to results screen --- osu.Game/Screens/Ranking/FavouriteButton.cs | 145 ++++++++++++++++++++ osu.Game/Screens/Ranking/ResultsScreen.cs | 25 +++- 2 files changed, 169 insertions(+), 1 deletion(-) create mode 100644 osu.Game/Screens/Ranking/FavouriteButton.cs diff --git a/osu.Game/Screens/Ranking/FavouriteButton.cs b/osu.Game/Screens/Ranking/FavouriteButton.cs new file mode 100644 index 0000000000..ee093d343e --- /dev/null +++ b/osu.Game/Screens/Ranking/FavouriteButton.cs @@ -0,0 +1,145 @@ +// 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.Graphics; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Logging; +using osu.Game.Beatmaps.Drawables.Cards; +using osu.Game.Graphics; +using osu.Game.Graphics.UserInterface; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Resources.Localisation.Web; +using osuTK; + +namespace osu.Game.Screens.Ranking +{ + public partial class FavouriteButton : OsuAnimatedButton + { + private readonly Box background; + private readonly SpriteIcon icon; + private readonly BindableWithCurrent current; + + public Bindable Current + { + get => current.Current; + set => current.Current = value; + } + + private readonly APIBeatmapSet beatmapSet; + + private PostBeatmapFavouriteRequest favouriteRequest; + private LoadingLayer loading; + + private readonly IBindable localUser = new Bindable(); + + [Resolved] + private IAPIProvider api { get; set; } + + [Resolved] + private OsuColour colours { get; set; } + + public FavouriteButton(APIBeatmapSet beatmapSet) + { + this.beatmapSet = beatmapSet; + current = new BindableWithCurrent(new BeatmapSetFavouriteState(this.beatmapSet.HasFavourited, this.beatmapSet.FavouriteCount)); + + Size = new Vector2(50, 30); + + Children = new Drawable[] + { + background = new Box + { + RelativeSizeAxes = Axes.Both, + Depth = float.MaxValue + }, + icon = new SpriteIcon + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(13), + Icon = FontAwesome.Regular.Heart, + }, + loading = new LoadingLayer(true, false), + }; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours, IAPIProvider api) + { + this.api = api; + + updateState(); + + localUser.BindTo(api.LocalUser); + localUser.BindValueChanged(_ => updateEnabled()); + + Action = () => toggleFavouriteStatus(); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + Action = toggleFavouriteStatus; + current.BindValueChanged(_ => updateState(), true); + } + + private void toggleFavouriteStatus() + { + + Enabled.Value = false; + loading.Show(); + + var actionType = current.Value.Favourited ? BeatmapFavouriteAction.UnFavourite : BeatmapFavouriteAction.Favourite; + + favouriteRequest?.Cancel(); + favouriteRequest = new PostBeatmapFavouriteRequest(beatmapSet.OnlineID, actionType); + + favouriteRequest.Success += () => + { + bool favourited = actionType == BeatmapFavouriteAction.Favourite; + + current.Value = new BeatmapSetFavouriteState(favourited, current.Value.FavouriteCount + (favourited ? 1 : -1)); + + Enabled.Value = true; + loading.Hide(); + }; + favouriteRequest.Failure += e => + { + Logger.Error(e, $"Failed to {actionType.ToString().ToLowerInvariant()} beatmap: {e.Message}"); + Enabled.Value = true; + loading.Hide(); + }; + + api.Queue(favouriteRequest); + } + + private void updateEnabled() => Enabled.Value = !(localUser.Value is GuestUser) && beatmapSet.OnlineID > 0; + + private void updateState() + { + if (current?.Value == null) + return; + + if (current.Value.Favourited) + { + background.Colour = colours.Green; + icon.Icon = FontAwesome.Solid.Heart; + TooltipText = BeatmapsetsStrings.ShowDetailsUnfavourite; + } + else + { + background.Colour = colours.Gray4; + icon.Icon = FontAwesome.Regular.Heart; + TooltipText = BeatmapsetsStrings.ShowDetailsFavourite; + } + } + } +} diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs index 44b270db53..e96265be3d 100644 --- a/osu.Game/Screens/Ranking/ResultsScreen.cs +++ b/osu.Game/Screens/Ranking/ResultsScreen.cs @@ -15,6 +15,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; +using osu.Framework.Logging; using osu.Framework.Screens; using osu.Game.Graphics; using osu.Game.Graphics.Containers; @@ -22,6 +23,7 @@ using osu.Game.Graphics.UserInterface; using osu.Game.Input.Bindings; using osu.Game.Localisation; using osu.Game.Online.API; +using osu.Game.Online.API.Requests; using osu.Game.Online.Placeholders; using osu.Game.Overlays; using osu.Game.Scoring; @@ -76,7 +78,7 @@ namespace osu.Game.Screens.Ranking /// /// Whether the user's personal statistics should be shown on the extended statistics panel - /// after clicking the score panel associated with the being presented. + /// after clicking the score panel associated with the being presented. /// Requires to be present. /// public bool ShowUserStatistics { get; init; } @@ -202,6 +204,27 @@ namespace osu.Game.Screens.Ranking }, }); } + + // Do not render if user is not logged in or the mapset does not have a valid online ID. + if (api.IsLoggedIn && Score?.BeatmapInfo?.BeatmapSet != null && Score.BeatmapInfo.BeatmapSet.OnlineID > 0) + { + GetBeatmapSetRequest beatmapSetRequest; + beatmapSetRequest = new GetBeatmapSetRequest(Score.BeatmapInfo.BeatmapSet.OnlineID); + + beatmapSetRequest.Success += (beatmapSet) => + { + buttons.Add(new FavouriteButton(beatmapSet) + { + Width = 75 + }); + }; + beatmapSetRequest.Failure += e => + { + Logger.Error(e, $"Failed to fetch beatmap info: {e.Message}"); + }; + + api.Queue(beatmapSetRequest); + } } protected override void LoadComplete() From 102da0f98c783fecb55736c574ee14e639fa9b6c Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 17 Jul 2024 23:58:38 +0300 Subject: [PATCH 1528/2493] Remove incorrect `[CanBeNull]` attribute --- osu.Game/Online/API/Requests/Responses/APIUser.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Online/API/Requests/Responses/APIUser.cs b/osu.Game/Online/API/Requests/Responses/APIUser.cs index 1c07b38667..a2836476c5 100644 --- a/osu.Game/Online/API/Requests/Responses/APIUser.cs +++ b/osu.Game/Online/API/Requests/Responses/APIUser.cs @@ -202,7 +202,6 @@ namespace osu.Game.Online.API.Requests.Responses public string PlayMode; [JsonProperty(@"profile_hue")] - [CanBeNull] public int? ProfileHue; [JsonProperty(@"profile_order")] From 4eb4d35e2f4c5b6edca8c10692165a22915195af Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 17 Jul 2024 23:58:47 +0300 Subject: [PATCH 1529/2493] Make `UpdateColours` method protected --- osu.Game/Overlays/FullscreenOverlay.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/FullscreenOverlay.cs b/osu.Game/Overlays/FullscreenOverlay.cs index 2a09147c76..c2ecb55814 100644 --- a/osu.Game/Overlays/FullscreenOverlay.cs +++ b/osu.Game/Overlays/FullscreenOverlay.cs @@ -100,7 +100,10 @@ namespace osu.Game.Overlays } } - public void UpdateColours() + /// + /// Updates the colours of the background and the top waves with the latest colour shades provided by . + /// + protected void UpdateColours() { Waves.FirstWaveColour = ColourProvider.Light4; Waves.SecondWaveColour = ColourProvider.Light3; From d61a72b8fbda78a475428549090e90ee9e840a97 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 18 Jul 2024 00:05:44 +0300 Subject: [PATCH 1530/2493] Explicitly define `Hue` rather than implicitly provide it by enum value --- osu.Game/Overlays/OverlayColourProvider.cs | 42 +++++++-------- osu.Game/Overlays/OverlayColourScheme.cs | 60 ++++++++++++++++++++++ osu.Game/Overlays/UserProfileOverlay.cs | 16 +++--- osu.Game/Screens/Footer/ScreenFooter.cs | 8 +-- 4 files changed, 91 insertions(+), 35 deletions(-) create mode 100644 osu.Game/Overlays/OverlayColourScheme.cs diff --git a/osu.Game/Overlays/OverlayColourProvider.cs b/osu.Game/Overlays/OverlayColourProvider.cs index 9613bc2857..9f5583cf73 100644 --- a/osu.Game/Overlays/OverlayColourProvider.cs +++ b/osu.Game/Overlays/OverlayColourProvider.cs @@ -7,11 +7,19 @@ namespace osu.Game.Overlays { public class OverlayColourProvider { - public OverlayColourScheme ColourScheme { get; private set; } + /// + /// The hue degree associated with the colour shades provided by this . + /// + public int Hue { get; private set; } public OverlayColourProvider(OverlayColourScheme colourScheme) + : this(colourScheme.GetHue()) { - ColourScheme = colourScheme; + } + + public OverlayColourProvider(int hue) + { + Hue = hue; } // Note that the following five colours are also defined in `OsuColour` as `{colourScheme}{0,1,2,3,4}`. @@ -46,31 +54,19 @@ namespace osu.Game.Overlays public Color4 Background6 => getColour(0.1f, 0.1f); /// - /// Changes the value of to a different colour scheme. + /// Changes the to a different degree. /// Note that this does not trigger any kind of signal to any drawable that received colours from here, all drawables need to be updated manually. /// /// The proposed colour scheme. - public void ChangeColourScheme(OverlayColourScheme colourScheme) - { - ColourScheme = colourScheme; - } + public void ChangeColourScheme(OverlayColourScheme colourScheme) => ChangeColourScheme(colourScheme.GetHue()); - private Color4 getColour(float saturation, float lightness) => Framework.Graphics.Colour4.FromHSL(getBaseHue(ColourScheme), saturation, lightness); + /// + /// Changes the to a different degree. + /// Note that this does not trigger any kind of signal to any drawable that received colours from here, all drawables need to be updated manually. + /// + /// The proposed hue degree. + public void ChangeColourScheme(int hue) => Hue = hue; - private static float getBaseHue(OverlayColourScheme colourScheme) => (int)colourScheme / 360f; - } - - // See https://github.com/ppy/osu-web/blob/5a536d217a21582aad999db50a981003d3ad5659/app/helpers.php#L1620-L1628 - public enum OverlayColourScheme - { - Red = 0, - Orange = 45, - Lime = 90, - Green = 125, - Aquamarine = 160, - Blue = 200, - Purple = 255, - Plum = 320, - Pink = 333, + private Color4 getColour(float saturation, float lightness) => Framework.Graphics.Colour4.FromHSL(Hue / 360f, saturation, lightness); } } diff --git a/osu.Game/Overlays/OverlayColourScheme.cs b/osu.Game/Overlays/OverlayColourScheme.cs new file mode 100644 index 0000000000..0126f9060f --- /dev/null +++ b/osu.Game/Overlays/OverlayColourScheme.cs @@ -0,0 +1,60 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; + +namespace osu.Game.Overlays +{ + public enum OverlayColourScheme + { + Red, + Orange, + Lime, + Green, + Aquamarine, + Blue, + Purple, + Plum, + Pink, + } + + public static class OverlayColourSchemeExtensions + { + public static int GetHue(this OverlayColourScheme colourScheme) + { + // See https://github.com/ppy/osu-web/blob/5a536d217a21582aad999db50a981003d3ad5659/app/helpers.php#L1620-L1628 + switch (colourScheme) + { + default: + throw new ArgumentOutOfRangeException(nameof(colourScheme)); + + case OverlayColourScheme.Red: + return 0; + + case OverlayColourScheme.Orange: + return 45; + + case OverlayColourScheme.Lime: + return 90; + + case OverlayColourScheme.Green: + return 125; + + case OverlayColourScheme.Aquamarine: + return 160; + + case OverlayColourScheme.Blue: + return 200; + + case OverlayColourScheme.Purple: + return 255; + + case OverlayColourScheme.Plum: + return 320; + + case OverlayColourScheme.Pink: + return 333; + } + } + } +} diff --git a/osu.Game/Overlays/UserProfileOverlay.cs b/osu.Game/Overlays/UserProfileOverlay.cs index 8c750b5d83..815f4b545f 100644 --- a/osu.Game/Overlays/UserProfileOverlay.cs +++ b/osu.Game/Overlays/UserProfileOverlay.cs @@ -118,7 +118,7 @@ namespace osu.Game.Overlays } : Array.Empty(); - setupBaseContent(OverlayColourScheme.Pink, forceContentRecreation: true); + setupBaseContent(OverlayColourScheme.Pink.GetHue(), forceContentRecreation: true); if (API.State.Value != APIState.Offline) { @@ -135,9 +135,9 @@ namespace osu.Game.Overlays Debug.Assert(sections != null && sectionsContainer != null && tabs != null); // reuse header and content if same colour scheme, otherwise recreate both. - var profileScheme = (OverlayColourScheme?)loadedUser.ProfileHue ?? OverlayColourScheme.Pink; - if (profileScheme != ColourProvider.ColourScheme) - setupBaseContent(profileScheme, forceContentRecreation: false); + int profileHue = loadedUser.ProfileHue ?? OverlayColourScheme.Pink.GetHue(); + if (profileHue != ColourProvider.Hue) + setupBaseContent(profileHue, forceContentRecreation: false); var actualRuleset = rulesets.GetRuleset(userRuleset?.ShortName ?? loadedUser.PlayMode).AsNonNull(); @@ -163,12 +163,12 @@ namespace osu.Game.Overlays loadingLayer.Hide(); } - private void setupBaseContent(OverlayColourScheme colourScheme, bool forceContentRecreation) + private void setupBaseContent(int hue, bool forceContentRecreation) { - var previousColourScheme = ColourProvider.ColourScheme; - ColourProvider.ChangeColourScheme(colourScheme); + int previousHue = ColourProvider.Hue; + ColourProvider.ChangeColourScheme(hue); - if (colourScheme != previousColourScheme) + if (hue != previousHue) { RecreateHeader(); UpdateColours(); diff --git a/osu.Game/Screens/Footer/ScreenFooter.cs b/osu.Game/Screens/Footer/ScreenFooter.cs index 6a1efcf87a..ea32507ca0 100644 --- a/osu.Game/Screens/Footer/ScreenFooter.cs +++ b/osu.Game/Screens/Footer/ScreenFooter.cs @@ -219,7 +219,7 @@ namespace osu.Game.Screens.Footer var targetPosition = targetButton?.ToSpaceOfOtherDrawable(targetButton.LayoutRectangle.TopRight, this) ?? fallbackPosition; - updateColourScheme(overlay.ColourProvider.ColourScheme); + updateColourScheme(overlay.ColourProvider.Hue); footerContent = overlay.CreateFooterContent(); @@ -256,16 +256,16 @@ namespace osu.Game.Screens.Footer temporarilyHiddenButtons.Clear(); - updateColourScheme(OverlayColourScheme.Aquamarine); + updateColourScheme(OverlayColourScheme.Aquamarine.GetHue()); contentContainer.Delay(timeUntilRun).Expire(); contentContainer = null; activeOverlay = null; } - private void updateColourScheme(OverlayColourScheme colourScheme) + private void updateColourScheme(int hue) { - colourProvider.ChangeColourScheme(colourScheme); + colourProvider.ChangeColourScheme(hue); background.FadeColour(colourProvider.Background5, 150, Easing.OutQuint); From 5317086171ee7da13154b434620b856994d013a6 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 18 Jul 2024 00:26:37 +0300 Subject: [PATCH 1531/2493] Split content recreation methods --- osu.Game/Overlays/UserProfileOverlay.cs | 33 ++++++++++++++----------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/osu.Game/Overlays/UserProfileOverlay.cs b/osu.Game/Overlays/UserProfileOverlay.cs index 815f4b545f..ac1fc44cd6 100644 --- a/osu.Game/Overlays/UserProfileOverlay.cs +++ b/osu.Game/Overlays/UserProfileOverlay.cs @@ -118,7 +118,8 @@ namespace osu.Game.Overlays } : Array.Empty(); - setupBaseContent(OverlayColourScheme.Pink.GetHue(), forceContentRecreation: true); + changeOverlayColours(OverlayColourScheme.Pink.GetHue()); + recreateBaseContent(); if (API.State.Value != APIState.Offline) { @@ -136,8 +137,9 @@ namespace osu.Game.Overlays // reuse header and content if same colour scheme, otherwise recreate both. int profileHue = loadedUser.ProfileHue ?? OverlayColourScheme.Pink.GetHue(); - if (profileHue != ColourProvider.Hue) - setupBaseContent(profileHue, forceContentRecreation: false); + + if (changeOverlayColours(profileHue)) + recreateBaseContent(); var actualRuleset = rulesets.GetRuleset(userRuleset?.ShortName ?? loadedUser.PlayMode).AsNonNull(); @@ -163,19 +165,8 @@ namespace osu.Game.Overlays loadingLayer.Hide(); } - private void setupBaseContent(int hue, bool forceContentRecreation) + private void recreateBaseContent() { - int previousHue = ColourProvider.Hue; - ColourProvider.ChangeColourScheme(hue); - - if (hue != previousHue) - { - RecreateHeader(); - UpdateColours(); - } - else if (!forceContentRecreation) - return; - Child = new OsuContextMenuContainer { RelativeSizeAxes = Axes.Both, @@ -224,6 +215,18 @@ namespace osu.Game.Overlays }; } + private bool changeOverlayColours(int hue) + { + if (hue == ColourProvider.Hue) + return false; + + ColourProvider.ChangeColourScheme(hue); + + RecreateHeader(); + UpdateColours(); + return true; + } + private partial class ProfileSectionTabControl : OsuTabControl { public ProfileSectionTabControl() From 7a394350170d804d2744706cf0d03b815f47a9cf Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 18 Jul 2024 01:11:39 +0300 Subject: [PATCH 1532/2493] Fix intermitent test failure in `TestSceneArgonHealthDisplay` --- osu.Game.Tests/Visual/Gameplay/TestSceneArgonHealthDisplay.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneArgonHealthDisplay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneArgonHealthDisplay.cs index 5d2921107e..319efee1a7 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneArgonHealthDisplay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneArgonHealthDisplay.cs @@ -99,6 +99,7 @@ namespace osu.Game.Tests.Visual.Gameplay Scheduler.AddDelayed(applyMiss, 500 + 30); }); + AddUntilStep("wait for sequence", () => !Scheduler.HasPendingTasks); } [Test] @@ -120,6 +121,7 @@ namespace osu.Game.Tests.Visual.Gameplay } } }); + AddUntilStep("wait for sequence", () => !Scheduler.HasPendingTasks); } [Test] From 1906c2f72537fde9386f03313afa3e95ba9b1663 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 18 Jul 2024 15:57:57 +0900 Subject: [PATCH 1533/2493] Fix TestTouchScreenDetectionAtSongSelect test failure https://github.com/ppy/osu/actions/runs/9985890747/job/27597501295 In this case, the settings overlay is taking a very long time to load (on a background thread), and pops in when it finishes loading because it's been requested to open. The opens the settings overlay, closes it (by pressing escape, this does not actually close it because it's not loaded yet), and then enters song select by pressing 'P' 3 times. The settings overlay finishes loading at just the right opportune moment to eat one of the 'P' key presses. --- osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs index e81c6d2e86..3ae1d9786d 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs @@ -952,6 +952,8 @@ namespace osu.Game.Tests.Visual.Navigation [Test] public void TestTouchScreenDetectionAtSongSelect() { + AddUntilStep("wait for settings", () => Game.Settings.IsLoaded); + AddStep("touch logo", () => { var button = Game.ChildrenOfType().Single(); From 7bb680a8a445cd7a87ea206627f925a08dd0a337 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 18 Jul 2024 16:01:09 +0900 Subject: [PATCH 1534/2493] Raise workflow timeout time https://github.com/ppy/osu/actions/runs/9985890747/job/27597500883 --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1ea4654563..dc1cb6c186 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -67,7 +67,7 @@ jobs: - { prettyname: macOS, fullname: macos-latest } - { prettyname: Linux, fullname: ubuntu-latest } threadingMode: ['SingleThread', 'MultiThreaded'] - timeout-minutes: 60 + timeout-minutes: 120 steps: - name: Checkout uses: actions/checkout@v4 From f3cd3d7d3b8c7d56746b6d93a103d71ef6de991e Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 18 Jul 2024 16:22:39 +0900 Subject: [PATCH 1535/2493] Fix TestAllSamplesStopDuringSeek test failure https://github.com/smoogipoo/osu/actions/runs/9986761756/job/27599851263 This is a bit of a workaround, likely timing related. I don't foresee an until step in this case to cause false-passes. --- .../Visual/Gameplay/TestSceneGameplaySamplePlayback.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySamplePlayback.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySamplePlayback.cs index ad3fe7cb7e..21c83d521c 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySamplePlayback.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySamplePlayback.cs @@ -48,7 +48,7 @@ namespace osu.Game.Tests.Visual.Gameplay return true; }); - AddAssert("sample playback disabled", () => sampleDisabler.SamplePlaybackDisabled.Value); + AddUntilStep("sample playback disabled", () => sampleDisabler.SamplePlaybackDisabled.Value); // because we are in frame stable context, it's quite likely that not all samples are "played" at this point. // the important thing is that at least one started, and that sample has since stopped. From 00ed7a7a2f19f9770c0772bc9af0093d0dfd07c5 Mon Sep 17 00:00:00 2001 From: Nathan Du Date: Thu, 18 Jul 2024 16:08:30 +0800 Subject: [PATCH 1536/2493] Fix hold note light lingering with No Release Turns out endHold() is not called in the Tail.IsHit branch of the hold notes' CheckForResult method. --- .../Objects/Drawables/DrawableHoldNote.cs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs index 2b55e81788..9c56f0473c 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs @@ -268,11 +268,14 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables ApplyMaxResult(); else MissForcefully(); - } - // Make sure that the hold note is fully judged by giving the body a judgement. - if (Tail.AllJudged && !Body.AllJudged) - Body.TriggerResult(Tail.IsHit); + // Make sure that the hold note is fully judged by giving the body a judgement. + if (!Body.AllJudged) + Body.TriggerResult(Tail.IsHit); + + // Important that this is always called when a result is applied. + endHold(); + } } public override void MissForcefully() From 33a81d818107b80c73c4dd7ad483a9c59abb474a Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 18 Jul 2024 18:34:08 +0900 Subject: [PATCH 1537/2493] Use constraint to improve assertion message --- .../Visual/Navigation/TestSceneBeatmapEditorNavigation.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneBeatmapEditorNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneBeatmapEditorNavigation.cs index b5dfa9a87f..99d1ff93c5 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneBeatmapEditorNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneBeatmapEditorNavigation.cs @@ -209,7 +209,7 @@ namespace osu.Game.Tests.Visual.Navigation AddStep("Close editor while loading", () => Game.ScreenStack.CurrentScreen.Exit()); AddUntilStep("Wait for menu", () => Game.ScreenStack.CurrentScreen is MainMenu); - AddAssert("Check no new beatmaps were made", () => allBeatmapSets().SequenceEqual(beatmapSets)); + AddAssert("Check no new beatmaps were made", allBeatmapSets, () => Is.EquivalentTo(beatmapSets)); BeatmapSetInfo[] allBeatmapSets() => Game.Realm.Run(realm => realm.All().Where(x => !x.DeletePending).ToArray()); } From c9517aeebf0c9a726436d2d1776e7c69266c7df4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 18 Jul 2024 18:37:07 +0900 Subject: [PATCH 1538/2493] Fix tab extension dropdown having dead non-clickable hover area Closes https://github.com/ppy/osu/issues/28899. --- osu.Game/Graphics/UserInterface/OsuTabDropdown.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Graphics/UserInterface/OsuTabDropdown.cs b/osu.Game/Graphics/UserInterface/OsuTabDropdown.cs index 6272f95510..5924ee005a 100644 --- a/osu.Game/Graphics/UserInterface/OsuTabDropdown.cs +++ b/osu.Game/Graphics/UserInterface/OsuTabDropdown.cs @@ -123,7 +123,7 @@ namespace osu.Game.Graphics.UserInterface } }; - Padding = new MarginPadding { Left = 5, Right = 5 }; + Margin = new MarginPadding { Left = 5, Right = 5 }; } protected override bool OnHover(HoverEvent e) From 70985d3b2234d746275bc9e1f45891cc41ea0ab9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 18 Jul 2024 19:01:52 +0900 Subject: [PATCH 1539/2493] Remove margin completely --- osu.Game/Graphics/UserInterface/OsuTabDropdown.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game/Graphics/UserInterface/OsuTabDropdown.cs b/osu.Game/Graphics/UserInterface/OsuTabDropdown.cs index 5924ee005a..7a17be57a8 100644 --- a/osu.Game/Graphics/UserInterface/OsuTabDropdown.cs +++ b/osu.Game/Graphics/UserInterface/OsuTabDropdown.cs @@ -122,8 +122,6 @@ namespace osu.Game.Graphics.UserInterface Anchor = Anchor.Centre, } }; - - Margin = new MarginPadding { Left = 5, Right = 5 }; } protected override bool OnHover(HoverEvent e) From a7e110f6693beca6f6e6a20efb69a6913d58550e Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 18 Jul 2024 19:07:02 +0900 Subject: [PATCH 1540/2493] Don't rely on single-use properties --- .../Objects/Drawables/DrawableOsuJudgement.cs | 57 ++++++++++--------- .../Objects/Drawables/SkinnableLighting.cs | 18 +++--- .../Rulesets/Judgements/DrawableJudgement.cs | 6 +- 3 files changed, 39 insertions(+), 42 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs index 0630ecfbb5..8b3fcb23cd 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs @@ -5,19 +5,23 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Game.Configuration; using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Scoring; using osuTK; +using osuTK.Graphics; namespace osu.Game.Rulesets.Osu.Objects.Drawables { public partial class DrawableOsuJudgement : DrawableJudgement { + internal Color4 AccentColour { get; private set; } + internal SkinnableLighting Lighting { get; private set; } = null!; [Resolved] private OsuConfigManager config { get; set; } = null!; - private bool positionTransferred; + private Vector2 screenSpacePosition; [BackgroundDependencyLoader] private void load() @@ -32,37 +36,36 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables }); } + public override void Apply(JudgementResult result, DrawableHitObject? judgedObject) + { + base.Apply(result, judgedObject); + + if (judgedObject is not DrawableOsuHitObject osuObject) + return; + + AccentColour = osuObject.AccentColour.Value; + + switch (osuObject) + { + case DrawableSlider slider: + screenSpacePosition = slider.TailCircle.ToScreenSpace(slider.TailCircle.OriginPosition); + break; + + default: + screenSpacePosition = osuObject.ToScreenSpace(osuObject.OriginPosition); + break; + } + + Scale = new Vector2(osuObject.HitObject.Scale); + } + protected override void PrepareForUse() { base.PrepareForUse(); Lighting.ResetAnimation(); - Lighting.SetColourFrom(JudgedObject, Result); - - positionTransferred = false; - } - - protected override void Update() - { - base.Update(); - - if (!positionTransferred && JudgedObject is DrawableOsuHitObject osuObject && JudgedObject.IsInUse) - { - switch (osuObject) - { - case DrawableSlider slider: - Position = slider.TailCircle.ToSpaceOfOtherDrawable(slider.TailCircle.OriginPosition, Parent!); - break; - - default: - Position = osuObject.ToSpaceOfOtherDrawable(osuObject.OriginPosition, Parent!); - break; - } - - positionTransferred = true; - - Scale = new Vector2(osuObject.HitObject.Scale); - } + Lighting.SetColourFrom(this, Result); + Position = Parent!.ToLocalSpace(screenSpacePosition); } protected override void ApplyHitAnimations() diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/SkinnableLighting.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/SkinnableLighting.cs index b39b9c4c54..3776201626 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/SkinnableLighting.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/SkinnableLighting.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.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Skinning; @@ -12,8 +10,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { internal partial class SkinnableLighting : SkinnableSprite { - private DrawableHitObject targetObject; - private JudgementResult targetResult; + private DrawableOsuJudgement? targetJudgement; + private JudgementResult? targetResult; public SkinnableLighting() : base("lighting") @@ -29,11 +27,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables /// /// Updates the lighting colour from a given hitobject and result. /// - /// The that's been judged. - /// The that was judged with. - public void SetColourFrom(DrawableHitObject targetObject, JudgementResult targetResult) + /// The that's been judged. + /// The that was judged with. + public void SetColourFrom(DrawableOsuJudgement targetJudgement, JudgementResult? targetResult) { - this.targetObject = targetObject; + this.targetJudgement = targetJudgement; this.targetResult = targetResult; updateColour(); @@ -41,10 +39,10 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables private void updateColour() { - if (targetObject == null || targetResult == null) + if (targetJudgement == null || targetResult == null) Colour = Color4.White; else - Colour = targetResult.IsHit ? targetObject.AccentColour.Value : Color4.Transparent; + Colour = targetResult.IsHit ? targetJudgement.AccentColour : Color4.Transparent; } } } diff --git a/osu.Game/Rulesets/Judgements/DrawableJudgement.cs b/osu.Game/Rulesets/Judgements/DrawableJudgement.cs index 189be44033..bdeadfd201 100644 --- a/osu.Game/Rulesets/Judgements/DrawableJudgement.cs +++ b/osu.Game/Rulesets/Judgements/DrawableJudgement.cs @@ -24,8 +24,6 @@ namespace osu.Game.Rulesets.Judgements public JudgementResult? Result { get; private set; } - public DrawableHitObject? JudgedObject { get; private set; } - public HitObject? JudgedHitObject { get; private set; } public override bool RemoveCompletedTransforms => false; @@ -97,10 +95,9 @@ namespace osu.Game.Rulesets.Judgements /// /// The applicable judgement. /// The drawable object. - public void Apply(JudgementResult result, DrawableHitObject? judgedObject) + public virtual void Apply(JudgementResult result, DrawableHitObject? judgedObject) { Result = result; - JudgedObject = judgedObject; JudgedHitObject = judgedObject?.HitObject; } @@ -108,7 +105,6 @@ namespace osu.Game.Rulesets.Judgements { base.FreeAfterUse(); - JudgedObject = null; JudgedHitObject = null; } From 3f4e56be3ce3532b6c7b75f530a1a2b7c45a99ca Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 18 Jul 2024 20:53:59 +0900 Subject: [PATCH 1541/2493] Fix TestPostAsOwner test failure https://github.com/smoogipoo/osu/actions/runs/9990112749/job/27610257309 Comments are loaded asynchronously, both from the initial request and the following message-post request. By sheer timing luck, these could be out of order and the assertion on the posted message could fail. --- osu.Game.Tests/Visual/Online/TestSceneCommentsContainer.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game.Tests/Visual/Online/TestSceneCommentsContainer.cs b/osu.Game.Tests/Visual/Online/TestSceneCommentsContainer.cs index fd3552f675..acc3c9b8b4 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneCommentsContainer.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneCommentsContainer.cs @@ -157,6 +157,7 @@ namespace osu.Game.Tests.Visual.Online { setUpCommentsResponse(getExampleComments()); AddStep("show comments", () => commentsContainer.ShowComments(CommentableType.Beatmapset, 123)); + AddUntilStep("comments shown", () => commentsContainer.ChildrenOfType().Any()); setUpPostResponse(); AddStep("enter text", () => editorTextBox.Current.Value = "comm"); @@ -175,6 +176,7 @@ namespace osu.Game.Tests.Visual.Online { setUpCommentsResponse(getExampleComments()); AddStep("show comments", () => commentsContainer.ShowComments(CommentableType.Beatmapset, 123)); + AddUntilStep("comments shown", () => commentsContainer.ChildrenOfType().Any()); setUpPostResponse(true); AddStep("enter text", () => editorTextBox.Current.Value = "comm"); From a570949459b3e66fab91fd87834df01547ace2b8 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 19 Jul 2024 03:00:44 +0300 Subject: [PATCH 1542/2493] Fix selection box initialy visible despite no items selected --- osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs index b68d5cd540..16d11ccd1a 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs @@ -85,10 +85,7 @@ namespace osu.Game.Screens.Edit.Compose.Components SelectionBox = CreateSelectionBox(), }); - SelectedItems.CollectionChanged += (_, _) => - { - Scheduler.AddOnce(updateVisibility); - }; + SelectedItems.BindCollectionChanged((_, _) => Scheduler.AddOnce(updateVisibility), true); } public SelectionBox CreateSelectionBox() From dd2454ba10532798d9e5c59136123507efd098a6 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 19 Jul 2024 03:17:50 +0300 Subject: [PATCH 1543/2493] Disable trailing comma inspections entirely --- osu.sln.DotSettings | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.sln.DotSettings b/osu.sln.DotSettings index 25bbc4beb5..0c52f8d82a 100644 --- a/osu.sln.DotSettings +++ b/osu.sln.DotSettings @@ -20,6 +20,7 @@ WARNING WARNING True + DO_NOT_SHOW WARNING WARNING HINT From 2ad8eeb918b7129f13df5f985c6f0739ab8ff5d5 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 19 Jul 2024 03:25:12 +0300 Subject: [PATCH 1544/2493] Fix beatmap attributes display in mod select recreating star difficulty bindable every setting change --- .../Overlays/Mods/BeatmapAttributesDisplay.cs | 37 +++++++++++-------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/osu.Game/Overlays/Mods/BeatmapAttributesDisplay.cs b/osu.Game/Overlays/Mods/BeatmapAttributesDisplay.cs index 1f4e007f47..2670c20d26 100644 --- a/osu.Game/Overlays/Mods/BeatmapAttributesDisplay.cs +++ b/osu.Game/Overlays/Mods/BeatmapAttributesDisplay.cs @@ -108,8 +108,6 @@ namespace osu.Game.Overlays.Mods updateValues(); }, true); - BeatmapInfo.BindValueChanged(_ => updateValues()); - Collapsed.BindValueChanged(_ => { // Only start autosize animations on first collapse toggle. This avoids an ugly initial presentation. @@ -120,12 +118,32 @@ namespace osu.Game.Overlays.Mods GameRuleset = game.Ruleset.GetBoundCopy(); GameRuleset.BindValueChanged(_ => updateValues()); - BeatmapInfo.BindValueChanged(_ => updateValues()); + BeatmapInfo.BindValueChanged(_ => + { + updateStarDifficultyBindable(); + updateValues(); + }, true); - updateValues(); updateCollapsedState(); } + private void updateStarDifficultyBindable() + { + cancellationSource?.Cancel(); + + if (BeatmapInfo.Value == null) + return; + + starDifficulty = difficultyCache.GetBindableDifficulty(BeatmapInfo.Value, (cancellationSource = new CancellationTokenSource()).Token); + starDifficulty.BindValueChanged(s => + { + starRatingDisplay.Current.Value = s.NewValue ?? default; + + if (!starRatingDisplay.IsPresent) + starRatingDisplay.FinishTransforms(true); + }); + } + protected override bool OnHover(HoverEvent e) { startAnimating(); @@ -154,17 +172,6 @@ namespace osu.Game.Overlays.Mods if (BeatmapInfo.Value == null) return; - cancellationSource?.Cancel(); - - starDifficulty = difficultyCache.GetBindableDifficulty(BeatmapInfo.Value, (cancellationSource = new CancellationTokenSource()).Token); - starDifficulty.BindValueChanged(s => - { - starRatingDisplay.Current.Value = s.NewValue ?? default; - - if (!starRatingDisplay.IsPresent) - starRatingDisplay.FinishTransforms(true); - }); - double rate = ModUtils.CalculateRateWithMods(Mods.Value); bpmDisplay.Current.Value = FormatUtils.RoundBPM(BeatmapInfo.Value.BPM, rate); From bae9625b0b4785b83124bfd38ccd8ff85d74947b Mon Sep 17 00:00:00 2001 From: StanR Date: Fri, 19 Jul 2024 10:13:50 +0500 Subject: [PATCH 1545/2493] Make repetition nerf harsher, buff initial rhythm ratio, small refactoring --- .../Difficulty/Evaluators/RhythmEvaluator.cs | 28 ++++++++++--------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs index 60bc53e7a1..9c48af80a7 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs @@ -32,7 +32,14 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators Deltas.Add(existingDelta == default ? delta : existingDelta); } - public double AverageDelta() => Math.Max(Deltas.Average(), OsuDifficultyHitObject.MIN_DELTA_TIME); + public double AverageDelta() => Deltas.Count > 0 ? Math.Max(Deltas.Average(), OsuDifficultyHitObject.MIN_DELTA_TIME) : 0; + + public bool IsSimilarPolarity(Island other, double epsilon) + { + // consider islands to be of similar polarity only if they're having the same average delta (we don't want to consider 3 singletaps similar to a triple) + return Math.Abs(AverageDelta() - other.AverageDelta()) < epsilon && + Deltas.Count % 2 == other.Deltas.Count % 2; + } public override int GetHashCode() { @@ -53,7 +60,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators } private const int history_time_max = 5 * 1000; // 5 seconds of calculatingRhythmBonus max. - private const double rhythm_multiplier = 1.14; + private const double rhythm_multiplier = 1.05; private const int max_island_size = 7; /// @@ -61,8 +68,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators /// public static double EvaluateDifficultyOf(DifficultyHitObject current) { - Dictionary islandCounts = new Dictionary(); - if (current.BaseObject is Spinner) return 0; @@ -70,6 +75,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators var island = new Island(); var previousIsland = new Island(); + Dictionary islandCounts = new Dictionary(); double startRatio = 0; // store the ratio of the current start of an island to buff for tighter rhythms @@ -97,7 +103,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators double prevDelta = prevObj.StrainTime; double lastDelta = lastObj.StrainTime; - double currRatio = 1.0 + 6.0 * Math.Min(0.5, Math.Pow(Math.Sin(Math.PI / (Math.Min(prevDelta, currDelta) / Math.Max(prevDelta, currDelta))), 2)); // fancy function to calculate rhythmbonuses. + double currRatio = 1.0 + 10.0 * Math.Min(0.5, Math.Pow(Math.Sin(Math.PI / (Math.Min(prevDelta, currDelta) / Math.Max(prevDelta, currDelta))), 2)); // fancy function to calculate rhythmbonuses. double windowPenalty = Math.Min(1, Math.Max(0, Math.Abs(prevDelta - currDelta) - currObj.HitWindowGreat * 0.3) / (currObj.HitWindowGreat * 0.3)); @@ -125,23 +131,19 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators if (prevObj.BaseObject is Slider) // bpm change was from a slider, this is easier typically than circle -> circle effectiveRatio *= 0.25; - if (previousIsland.Deltas.Count % 2 == island.Deltas.Count % 2) // repeated island polartiy (2 -> 4, 3 -> 5) + if (island.IsSimilarPolarity(previousIsland, deltaDifferenceEpsilon)) // repeated island polartiy (2 -> 4, 3 -> 5) effectiveRatio *= 0.50; if (lastDelta > prevDelta + deltaDifferenceEpsilon && prevDelta > currDelta + deltaDifferenceEpsilon) // previous increase happened a note ago, 1/1->1/2-1/4, dont want to buff this. effectiveRatio *= 0.125; - if (islandCounts.ContainsKey(island)) + if (!islandCounts.TryAdd(island, 1)) { islandCounts[island]++; // repeated island (ex: triplet -> triplet) - double power = Math.Max(0.75, logistic(island.AverageDelta(), 3, 0.15, 9)); - effectiveRatio *= Math.Pow(1.0 / islandCounts[island], power); - } - else - { - islandCounts.Add(island, 1); + double power = logistic(island.AverageDelta(), 4, 0.165, 10); + effectiveRatio *= Math.Min(1.0 / islandCounts[island], Math.Pow(1.0 / islandCounts[island], power)); } rhythmComplexitySum += Math.Sqrt(effectiveRatio * startRatio) * currHistoricalDecay; From 73edb324403c4ea1e1716547e2756f6fd5df001a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 19 Jul 2024 07:30:55 +0200 Subject: [PATCH 1546/2493] Add failing test coverage --- .../Visual/Menus/TestSceneMusicActionHandling.cs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Menus/TestSceneMusicActionHandling.cs b/osu.Game.Tests/Visual/Menus/TestSceneMusicActionHandling.cs index f17433244b..9936b24a06 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneMusicActionHandling.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneMusicActionHandling.cs @@ -62,14 +62,22 @@ namespace osu.Game.Tests.Visual.Menus AddUntilStep("track restarted", () => Game.MusicController.CurrentTrack.CurrentTime < 5000); AddStep("press previous", () => globalActionContainer.TriggerPressed(GlobalAction.MusicPrev)); - AddAssert("track changed to previous", () => + AddUntilStep("track changed to previous", () => trackChangeQueue.Count == 1 && trackChangeQueue.Dequeue().changeDirection == TrackChangeDirection.Prev); AddStep("press next", () => globalActionContainer.TriggerPressed(GlobalAction.MusicNext)); - AddAssert("track changed to next", () => + AddUntilStep("track changed to next", () => trackChangeQueue.Count == 1 && - trackChangeQueue.Dequeue().changeDirection == TrackChangeDirection.Next); + trackChangeQueue.Peek().changeDirection == TrackChangeDirection.Next); + + AddUntilStep("wait until track switches", () => trackChangeQueue.Count == 2); + + AddStep("press next", () => globalActionContainer.TriggerPressed(GlobalAction.MusicNext)); + AddUntilStep("track changed to next", () => + trackChangeQueue.Count == 3 && + trackChangeQueue.Peek().changeDirection == TrackChangeDirection.Next); + AddAssert("track actually changed", () => !trackChangeQueue.First().working.BeatmapInfo.Equals(trackChangeQueue.Last().working.BeatmapInfo)); } } } From 9fe6354afc5517bb4527397777e2299e2cf8e7c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 19 Jul 2024 07:32:29 +0200 Subject: [PATCH 1547/2493] Fix backwards conditional --- osu.Game/Overlays/MusicController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/MusicController.cs b/osu.Game/Overlays/MusicController.cs index b6553779bc..d9bb92b4b7 100644 --- a/osu.Game/Overlays/MusicController.cs +++ b/osu.Game/Overlays/MusicController.cs @@ -316,7 +316,7 @@ namespace osu.Game.Overlays queuedDirection = TrackChangeDirection.Next; - var playableSet = getBeatmapSets().AsEnumerable().SkipWhile(i => !i.Equals(current?.BeatmapSetInfo) && (!i.Protected || allowProtectedTracks)).ElementAtOrDefault(1) + var playableSet = getBeatmapSets().AsEnumerable().SkipWhile(i => !i.Equals(current?.BeatmapSetInfo) || (i.Protected && !allowProtectedTracks)).ElementAtOrDefault(1) ?? getBeatmapSets().AsEnumerable().FirstOrDefault(i => !i.Protected || allowProtectedTracks); var playableBeatmap = playableSet?.Beatmaps.FirstOrDefault(); From 79cf644b8def9d767348a643d610933400c25c91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 19 Jul 2024 07:33:58 +0200 Subject: [PATCH 1548/2493] Enable NRT while we're here --- osu.Game.Tests/Visual/Menus/TestSceneMusicActionHandling.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Menus/TestSceneMusicActionHandling.cs b/osu.Game.Tests/Visual/Menus/TestSceneMusicActionHandling.cs index 9936b24a06..03b3b94bd8 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneMusicActionHandling.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneMusicActionHandling.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.Collections.Generic; using System.Linq; using NUnit.Framework; @@ -34,7 +32,7 @@ namespace osu.Game.Tests.Visual.Menus [Test] public void TestMusicNavigationActions() { - Queue<(IWorkingBeatmap working, TrackChangeDirection changeDirection)> trackChangeQueue = null; + Queue<(IWorkingBeatmap working, TrackChangeDirection changeDirection)> trackChangeQueue = null!; // ensure we have at least two beatmaps available to identify the direction the music controller navigated to. AddRepeatStep("import beatmap", () => Game.BeatmapManager.Import(TestResources.CreateTestBeatmapSetInfo()), 5); From c1532bcb570ae36d7a73d5b42e451b8c7325b111 Mon Sep 17 00:00:00 2001 From: StanR Date: Fri, 19 Jul 2024 11:01:42 +0500 Subject: [PATCH 1549/2493] Reduce base ratio a bit --- osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs index 9c48af80a7..ba77bc6a61 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs @@ -103,7 +103,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators double prevDelta = prevObj.StrainTime; double lastDelta = lastObj.StrainTime; - double currRatio = 1.0 + 10.0 * Math.Min(0.5, Math.Pow(Math.Sin(Math.PI / (Math.Min(prevDelta, currDelta) / Math.Max(prevDelta, currDelta))), 2)); // fancy function to calculate rhythmbonuses. + double currRatio = 1.0 + 8.0 * Math.Min(0.5, Math.Pow(Math.Sin(Math.PI / (Math.Min(prevDelta, currDelta) / Math.Max(prevDelta, currDelta))), 2)); // fancy function to calculate rhythmbonuses. double windowPenalty = Math.Min(1, Math.Max(0, Math.Abs(prevDelta - currDelta) - currObj.HitWindowGreat * 0.3) / (currObj.HitWindowGreat * 0.3)); From d7ae9505b2c3fe826d769c64279468671de94d82 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 19 Jul 2024 14:08:05 +0900 Subject: [PATCH 1550/2493] Fix TestCancelNavigationToEditor test failure https://github.com/ppy/osu/actions/runs/10002179087/job/27648253709 The editor could be pushed before the exit actually occurs. --- .../TestSceneBeatmapEditorNavigation.cs | 38 ++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneBeatmapEditorNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneBeatmapEditorNavigation.cs index 99d1ff93c5..5640682d06 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneBeatmapEditorNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneBeatmapEditorNavigation.cs @@ -1,12 +1,16 @@ // 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.Threading; using NUnit.Framework; +using osu.Framework.Allocation; using osu.Framework.Extensions; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Extensions.ObjectExtensions; +using osu.Framework.Graphics; using osu.Framework.Graphics.UserInterface; using osu.Framework.Input; using osu.Framework.Screens; @@ -204,9 +208,13 @@ namespace osu.Game.Tests.Visual.Navigation AddStep("Set current beatmap to default", () => Game.Beatmap.SetDefault()); - AddStep("Push editor loader", () => Game.ScreenStack.Push(new EditorLoader())); + DelayedLoadEditorLoader loader = null!; + AddStep("Push editor loader", () => Game.ScreenStack.Push(loader = new DelayedLoadEditorLoader())); AddUntilStep("Wait for loader current", () => Game.ScreenStack.CurrentScreen is EditorLoader); + AddUntilStep("wait for editor load start", () => loader.Editor != null); AddStep("Close editor while loading", () => Game.ScreenStack.CurrentScreen.Exit()); + AddStep("allow editor load", () => loader.AllowLoad.Set()); + AddUntilStep("wait for editor ready", () => loader.Editor!.LoadState >= LoadState.Ready); AddUntilStep("Wait for menu", () => Game.ScreenStack.CurrentScreen is MainMenu); AddAssert("Check no new beatmaps were made", allBeatmapSets, () => Is.EquivalentTo(beatmapSets)); @@ -356,5 +364,33 @@ namespace osu.Game.Tests.Visual.Navigation private EditorBeatmap getEditorBeatmap() => getEditor().ChildrenOfType().Single(); private Editor getEditor() => (Editor)Game.ScreenStack.CurrentScreen; + + private partial class DelayedLoadEditorLoader : EditorLoader + { + public readonly ManualResetEventSlim AllowLoad = new ManualResetEventSlim(); + public Editor? Editor { get; private set; } + + protected override Editor CreateEditor() => Editor = new DelayedLoadEditor(this); + } + + private partial class DelayedLoadEditor : Editor + { + private readonly DelayedLoadEditorLoader loader; + + public DelayedLoadEditor(DelayedLoadEditorLoader loader) + : base(loader) + { + this.loader = loader; + } + + protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) + { + // Importantly, this occurs before base.load(). + if (!loader.AllowLoad.Wait(TimeSpan.FromSeconds(10))) + throw new TimeoutException(); + + return base.CreateChildDependencies(parent); + } + } } } From 4dd225fdc8184f062292ff15c258076f29e0bfa5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 19 Jul 2024 08:26:53 +0200 Subject: [PATCH 1551/2493] Fix compose blueprint container not unsubscribing from event Closes https://github.com/ppy/osu/issues/28938. This is related to reloading the composer on timing point changes in scrolling rulesets. The lack of unsubscription from this would cause blueprints to be created for disposed composers via the `hitObjectAdded()` flow. The following line looks as if a sync load should be forced on a newly created placement blueprint: https://github.com/ppy/osu/blob/da4d37c4aded5e10d0a65ff44a08a886e3897e19/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs#L364 however, it is not the case if the parent (`placementBlueprintContainer`) is disposed, which it would be in this case. Therefore, the blueprint stays `NotLoaded` rather than `Ready`, therefore it never receives its DI dependencies, therefore it dies on an `EditorBeatmap` nullref. --- .../Edit/Compose/Components/ComposeBlueprintContainer.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs index fc8bce4c96..f1294ccc3c 100644 --- a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs @@ -9,6 +9,7 @@ using Humanizer; using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; @@ -405,5 +406,13 @@ namespace osu.Game.Screens.Edit.Compose.Components CommitIfPlacementActive(); } } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (Beatmap.IsNotNull()) + Beatmap.HitObjectAdded -= hitObjectAdded; + } } } From 0560214d5b7fd9f71cb1abdd4f32d878c511373b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 19 Jul 2024 15:15:49 +0900 Subject: [PATCH 1552/2493] Fix beatmap carousel performance regression with large databases --- osu.Game/Screens/Select/BeatmapCarousel.cs | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 3f9e676068..c76dbf9502 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -278,13 +278,27 @@ namespace osu.Game.Screens.Select if (changes == null) { + // Usually we'd handle the initial load case here, but that's already done in load() as an optimisation. + // So the only thing we need to handle here is the edge case where realm blocks-resumes all operations. realmBeatmapSets.Clear(); realmBeatmapSets.AddRange(sender.Select(r => r.ID)); + // Do a full two-way check on missing beatmaps. + // Let's assume that the worst that can happen is deletions or additions. setsRequiringRemoval.Clear(); setsRequiringUpdate.Clear(); - loadBeatmapSets(sender); + foreach (Guid id in realmBeatmapSets) + { + if (!root.BeatmapSetsByID.ContainsKey(id)) + setsRequiringUpdate.Add(id); + } + + foreach (Guid id in root.BeatmapSetsByID.Keys) + { + if (!realmBeatmapSets.Contains(id)) + setsRequiringRemoval.Add(id); + } } else { From 0f29ed618a691e419926de8b5b95df53af2aba47 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 19 Jul 2024 17:39:43 +0900 Subject: [PATCH 1553/2493] Don't attempt to clear the carousel during realm blocking operation --- osu.Game/Screens/Select/BeatmapCarousel.cs | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index c76dbf9502..cd0d2eea2c 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -278,12 +278,21 @@ namespace osu.Game.Screens.Select if (changes == null) { - // Usually we'd handle the initial load case here, but that's already done in load() as an optimisation. - // So the only thing we need to handle here is the edge case where realm blocks-resumes all operations. realmBeatmapSets.Clear(); realmBeatmapSets.AddRange(sender.Select(r => r.ID)); - // Do a full two-way check on missing beatmaps. + if (originalBeatmapSetsDetached.Count > 0 && sender.Count == 0) + { + // Usually we'd reset stuff here, but doing so triggers a silly flow which ends up deadlocking realm. + // Additionally, user should not be at song select when realm is blocking all operations in the first place. + // + // Note that due to the catch-up logic below, once operations are restored we will still be in a roughly + // correct state. The only things that this return will change is the carousel will not empty *during* the blocking + // operation. + return; + } + + // Do a full two-way check for missing (or incorrectly present) beatmaps. // Let's assume that the worst that can happen is deletions or additions. setsRequiringRemoval.Clear(); setsRequiringUpdate.Clear(); From 7a4758d8ccbf10e8118f4e71ff0075b4107b3c4d Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 19 Jul 2024 17:49:13 +0900 Subject: [PATCH 1554/2493] Attempt to fix TestSelectableMouseHandling test failure https://github.com/ppy/osu/pull/28900/checks?check_run_id=27652166871 This is an attempt. Going frame-by-frame I noticed that there's one frame in which the text is loaded but the FillFlowContainer/GridContainer haven't properly validated so the text is not positioned correctly (it's overflowing the panel to the left). If the cursor is moved at this exact time, then it may not be properly positioned for the following assertion, even though it is _somewhere_ on the panel. If the above is the case, then this is a known o!f issue, but not a simple one to solve. I haven't reproed this locally. --- .../TestSceneDrawableRoomPlaylist.cs | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs index bd62a8b131..2ef56bd54e 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs @@ -12,6 +12,7 @@ using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; using osu.Framework.Platform; using osu.Framework.Testing; using osu.Game.Beatmaps; @@ -319,16 +320,17 @@ namespace osu.Game.Tests.Visual.Multiplayer }); AddUntilStep("wait for load", () => playlist.ChildrenOfType().Any() && playlist.ChildrenOfType().First().DrawWidth > 0); - AddStep("move mouse to first item title", () => - { - var drawQuad = playlist.ChildrenOfType().First().ScreenSpaceDrawQuad; - var location = (drawQuad.TopLeft + drawQuad.BottomLeft) / 2 + new Vector2(drawQuad.Width * 0.2f, 0); - InputManager.MoveMouseTo(location); - }); + + AddStep("move mouse to first item title", () => InputManager.MoveMouseTo(playlist.ChildrenOfType().First().ChildrenOfType().First())); AddAssert("first item title not hovered", () => playlist.ChildrenOfType().First().IsHovered, () => Is.False); - AddStep("click left mouse", () => InputManager.Click(MouseButton.Left)); + + AddStep("click title", () => + { + InputManager.MoveMouseTo(playlist.ChildrenOfType().First().ChildrenOfType().First()); + InputManager.Click(MouseButton.Left); + }); + AddUntilStep("first item selected", () => playlist.ChildrenOfType().First().IsSelectedItem, () => Is.True); - // implies being clickable. AddUntilStep("first item title hovered", () => playlist.ChildrenOfType().First().IsHovered, () => Is.True); AddStep("move mouse to second item results button", () => InputManager.MoveMouseTo(playlist.ChildrenOfType().ElementAt(5))); From 5af39aad00878d577fb57fcd020ffd8340cfffe9 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 19 Jul 2024 19:02:41 +0900 Subject: [PATCH 1555/2493] Add beatmap name to log string Makes it easy to compare this line versus the one in OsuGame.PresentBeatmap(). At the moment it's just GUID which is... not useful! --- osu.Game/Screens/Select/SongSelect.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index 14c4a34d14..307043a312 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -609,7 +609,7 @@ namespace osu.Game.Screens.Select // clear pending task immediately to track any potential nested debounce operation. selectionChangedDebounce = null; - Logger.Log($"Song select updating selection with beatmap:{beatmap?.ID.ToString() ?? "null"} ruleset:{ruleset?.ShortName ?? "null"}"); + Logger.Log($"Song select updating selection with beatmap: {beatmap} {beatmap?.ID.ToString() ?? "null"} ruleset:{ruleset?.ShortName ?? "null"}"); if (transferRulesetValue()) { From f11f01f9b70fb3548f8e86470ed27289a3c66560 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 19 Jul 2024 19:16:37 +0900 Subject: [PATCH 1556/2493] Fix various visuals of playlist beatmap panels Supersedes https://github.com/ppy/osu/pull/28907. - Fix border being fat - Fix thumbnail not masking correctly - Fix background layer not being correctly fit to the panel - Dim the main background on hover - Minor tweaks to dimming --- .../Drawables/Cards/BeatmapCardThumbnail.cs | 2 +- .../OnlinePlay/DrawableRoomPlaylistItem.cs | 234 ++++++++++-------- 2 files changed, 130 insertions(+), 106 deletions(-) diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardThumbnail.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardThumbnail.cs index 7b668d7dc4..976f797760 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardThumbnail.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardThumbnail.cs @@ -101,7 +101,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards bool shouldDim = Dimmed.Value || playButton.Playing.Value; playButton.FadeTo(shouldDim ? 1 : 0, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); - background.FadeColour(colourProvider.Background6.Opacity(shouldDim ? 0.8f : 0f), BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); + background.FadeColour(colourProvider.Background6.Opacity(shouldDim ? 0.6f : 0f), BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); } } } diff --git a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs index ab32ca2558..43ffaf947e 100644 --- a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs +++ b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs @@ -49,6 +49,8 @@ namespace osu.Game.Screens.OnlinePlay private const float icon_height = 34; + private const float border_thickness = 3; + /// /// Invoked when this item requests to be deleted. /// @@ -81,7 +83,7 @@ namespace osu.Game.Screens.OnlinePlay private IRulesetInfo ruleset; private Mod[] requiredMods = Array.Empty(); - private Container maskingContainer; + private Container borderContainer; private FillFlowContainer difficultyIconContainer; private LinkFlowContainer beatmapText; private LinkFlowContainer authorText; @@ -134,7 +136,7 @@ namespace osu.Game.Screens.OnlinePlay [BackgroundDependencyLoader] private void load() { - maskingContainer.BorderColour = colours.Yellow; + borderContainer.BorderColour = colours.Yellow; ruleset = rulesets.GetRuleset(Item.RulesetID); var rulesetInstance = ruleset?.CreateInstance(); @@ -161,7 +163,7 @@ namespace osu.Game.Screens.OnlinePlay return; } - maskingContainer.BorderThickness = IsSelectedItem ? 5 : 0; + borderContainer.BorderThickness = IsSelectedItem ? border_thickness : 0; }, true); valid.BindValueChanged(_ => Scheduler.AddOnce(refresh)); @@ -278,8 +280,8 @@ namespace osu.Game.Screens.OnlinePlay { if (!valid.Value) { - maskingContainer.BorderThickness = 5; - maskingContainer.BorderColour = colours.Red; + borderContainer.BorderThickness = border_thickness; + borderContainer.BorderColour = colours.Red; } if (beatmap != null) @@ -291,12 +293,14 @@ namespace osu.Game.Screens.OnlinePlay Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, Width = 60, + Masking = true, + CornerRadius = 10, RelativeSizeAxes = Axes.Y, Dimmed = { Value = IsHovered } }, new DifficultyIcon(beatmap, ruleset, requiredMods) { - Size = new Vector2(icon_height), + Size = new Vector2(24), TooltipType = DifficultyIconTooltipType.Extended, Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, @@ -346,136 +350,153 @@ namespace osu.Game.Screens.OnlinePlay { Action fontParameters = s => s.Font = OsuFont.Default.With(size: 14, weight: FontWeight.SemiBold); - return maskingContainer = new Container + return new Container { RelativeSizeAxes = Axes.X, Height = HEIGHT, - Masking = true, - CornerRadius = 10, Children = new Drawable[] { - new Box // A transparent box that forces the border to be drawn if the panel background is opaque + new Container { RelativeSizeAxes = Axes.Both, - Alpha = 0, - AlwaysPresent = true - }, - onScreenLoader, - panelBackground = new PanelBackground - { - RelativeSizeAxes = Axes.Both, - }, - new GridContainer - { - RelativeSizeAxes = Axes.Both, - ColumnDimensions = new[] + Masking = true, + CornerRadius = 10, + Children = new Drawable[] { - new Dimension(GridSizeMode.AutoSize), - new Dimension(), - new Dimension(GridSizeMode.AutoSize), - new Dimension(GridSizeMode.AutoSize) - }, - Content = new[] - { - new Drawable[] + onScreenLoader, + panelBackground = new PanelBackground { - difficultyIconContainer = new FillFlowContainer + RelativeSizeAxes = Axes.Both, + }, + new GridContainer + { + RelativeSizeAxes = Axes.Both, + ColumnDimensions = new[] { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - AutoSizeAxes = Axes.X, - RelativeSizeAxes = Axes.Y, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(4), - Margin = new MarginPadding { Right = 4 }, + new Dimension(GridSizeMode.AutoSize), + new Dimension(), + new Dimension(GridSizeMode.AutoSize), + new Dimension(GridSizeMode.AutoSize) }, - mainFillFlow = new MainFlow(() => SelectedItem.Value == Model || !AllowSelection) + Content = new[] { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - AutoSizeAxes = Axes.Y, - RelativeSizeAxes = Axes.X, - Direction = FillDirection.Vertical, - Children = new Drawable[] + new Drawable[] { - beatmapText = new LinkFlowContainer(fontParameters) + difficultyIconContainer = new FillFlowContainer { - RelativeSizeAxes = Axes.X, - // workaround to ensure only the first line of text shows, emulating truncation (but without ellipsis at the end). - // TODO: remove when text/link flow can support truncation with ellipsis natively. - Height = OsuFont.DEFAULT_FONT_SIZE, - Masking = true - }, - new FillFlowContainer - { - AutoSizeAxes = Axes.Both, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + AutoSizeAxes = Axes.X, + RelativeSizeAxes = Axes.Y, Direction = FillDirection.Horizontal, - Spacing = new Vector2(10f, 0), + Spacing = new Vector2(4), + Margin = new MarginPadding { Right = 4 }, + }, + mainFillFlow = new MainFlow(() => SelectedItem.Value == Model || !AllowSelection) + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + AutoSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.X, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0, -2), Children = new Drawable[] { + beatmapText = new LinkFlowContainer(fontParameters) + { + RelativeSizeAxes = Axes.X, + // workaround to ensure only the first line of text shows, emulating truncation (but without ellipsis at the end). + // TODO: remove when text/link flow can support truncation with ellipsis natively. + Height = OsuFont.DEFAULT_FONT_SIZE, + Masking = true + }, new FillFlowContainer { AutoSizeAxes = Axes.Both, - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, Direction = FillDirection.Horizontal, Spacing = new Vector2(10f, 0), Children = new Drawable[] { - authorText = new LinkFlowContainer(fontParameters) { AutoSizeAxes = Axes.Both }, - explicitContent = new ExplicitContentBeatmapBadge + new FillFlowContainer { - Alpha = 0f, + AutoSizeAxes = Axes.Both, Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, - Margin = new MarginPadding { Top = 3f }, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(10f, 0), + Children = new Drawable[] + { + authorText = new LinkFlowContainer(fontParameters) { AutoSizeAxes = Axes.Both }, + explicitContent = new ExplicitContentBeatmapBadge + { + Alpha = 0f, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Margin = new MarginPadding { Top = 3f }, + } + }, + }, + new Container + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + AutoSizeAxes = Axes.Both, + Child = modDisplay = new ModDisplay + { + Scale = new Vector2(0.4f), + ExpansionMode = ExpansionMode.AlwaysExpanded, + Margin = new MarginPadding { Vertical = -6 }, + } } - }, - }, - new Container - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - AutoSizeAxes = Axes.Both, - Child = modDisplay = new ModDisplay - { - Scale = new Vector2(0.4f), - ExpansionMode = ExpansionMode.AlwaysExpanded, - Margin = new MarginPadding { Vertical = -6 }, } } } - } + }, + buttonsFlow = new FillFlowContainer + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + Direction = FillDirection.Horizontal, + Margin = new MarginPadding { Horizontal = 8 }, + AutoSizeAxes = Axes.Both, + Spacing = new Vector2(5), + ChildrenEnumerable = createButtons().Select(button => button.With(b => + { + b.Anchor = Anchor.Centre; + b.Origin = Anchor.Centre; + })) + }, + ownerAvatar = new OwnerAvatar + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(icon_height), + Margin = new MarginPadding { Right = 8 }, + Masking = true, + CornerRadius = 4, + Alpha = ShowItemOwner ? 1 : 0 + }, } - }, - buttonsFlow = new FillFlowContainer - { - Anchor = Anchor.CentreRight, - Origin = Anchor.CentreRight, - Direction = FillDirection.Horizontal, - Margin = new MarginPadding { Horizontal = 8 }, - AutoSizeAxes = Axes.Both, - Spacing = new Vector2(5), - ChildrenEnumerable = createButtons().Select(button => button.With(b => - { - b.Anchor = Anchor.Centre; - b.Origin = Anchor.Centre; - })) - }, - ownerAvatar = new OwnerAvatar - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Size = new Vector2(icon_height), - Margin = new MarginPadding { Right = 8 }, - Masking = true, - CornerRadius = 4, - Alpha = ShowItemOwner ? 1 : 0 - }, - } - } + } + }, + }, }, - }, + borderContainer = new Container + { + RelativeSizeAxes = Axes.Both, + Masking = true, + CornerRadius = 10, + Children = new Drawable[] + { + new Box // A transparent box that forces the border to be drawn if the panel background is opaque + { + RelativeSizeAxes = Axes.Both, + Alpha = 0, + AlwaysPresent = true + }, + } + } + } }; } @@ -509,6 +530,8 @@ namespace osu.Game.Screens.OnlinePlay { if (thumbnail != null) thumbnail.Dimmed.Value = true; + + panelBackground.FadeColour(OsuColour.Gray(0.7f), BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); return base.OnHover(e); } @@ -516,6 +539,8 @@ namespace osu.Game.Screens.OnlinePlay { if (thumbnail != null) thumbnail.Dimmed.Value = false; + + panelBackground.FadeColour(OsuColour.Gray(1f), BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); base.OnHoverLost(e); } @@ -642,7 +667,6 @@ namespace osu.Game.Screens.OnlinePlay backgroundSprite = new UpdateableBeatmapBackgroundSprite { RelativeSizeAxes = Axes.Both, - FillMode = FillMode.Fill, }, new FillFlowContainer { @@ -651,7 +675,7 @@ namespace osu.Game.Screens.OnlinePlay Direction = FillDirection.Horizontal, // This makes the gradient not be perfectly horizontal, but diagonal at a ~40° angle Shear = new Vector2(0.8f, 0), - Alpha = 0.5f, + Alpha = 0.6f, Children = new[] { // The left half with no gradient applied From 5ee645ac8f9eb630d58951a7843f3b688e2b885c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 19 Jul 2024 19:50:21 +0900 Subject: [PATCH 1557/2493] Increase opacity of control points slightly --- .../Timelines/Summary/Parts/ControlPointVisualisation.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/ControlPointVisualisation.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/ControlPointVisualisation.cs index 977aadd6c3..17c98003b0 100644 --- a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/ControlPointVisualisation.cs +++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/ControlPointVisualisation.cs @@ -21,7 +21,7 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts : base(point.Time) { Point = point; - Alpha = 0.3f; + Alpha = 0.5f; Blending = BlendingParameters.Additive; } From c4de2bbb60b7052a2ccad915cc10148d00df51f0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 19 Jul 2024 19:50:40 +0900 Subject: [PATCH 1558/2493] Ignore "too many ticks" in timeline (triggers in normal cases) --- .../Components/Timeline/TimelineTickDisplay.cs | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs index def528d9e5..4796c08809 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs @@ -2,12 +2,10 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Diagnostics; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Caching; using osu.Framework.Graphics; -using osu.Framework.Logging; using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.Graphics; @@ -161,20 +159,6 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline } } - if (Children.Count > 512) - { - // There should always be a sanely small number of ticks rendered. - // If this assertion triggers, either the zoom logic is broken or a beatmap is - // probably doing weird things... - // - // Let's hope the latter never happens. - // If it does, we can choose to either fix it or ignore it as an outlier. - string message = $"Timeline is rendering many ticks ({Children.Count})"; - - Logger.Log(message); - Debug.Fail(message); - } - int usedDrawables = drawableIndex; // save a few drawables beyond the currently used for edge cases. From c2cc85e6f023bb8089c421ce9f9aa6e676953003 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 19 Jul 2024 19:59:38 +0900 Subject: [PATCH 1559/2493] Use purple again for kiai time specifically --- .../Timelines/Summary/Parts/EffectPointVisualisation.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/EffectPointVisualisation.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/EffectPointVisualisation.cs index f1e2b52ad8..17fedb933a 100644 --- a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/EffectPointVisualisation.cs +++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/EffectPointVisualisation.cs @@ -100,7 +100,7 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts Origin = Anchor.CentreLeft, Height = 0.4f, Depth = float.MaxValue, - Colour = effect.GetRepresentingColour(colours), + Colour = colours.Purple1, }); } } From f500abd4f74f4a5a6f5f717a1e865d9956d1427d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 19 Jul 2024 20:02:17 +0900 Subject: [PATCH 1560/2493] Make "Hold Off" and "No Release" mod incompatible --- osu.Game.Rulesets.Mania/Mods/ManiaModHoldOff.cs | 2 +- osu.Game.Rulesets.Mania/Mods/ManiaModNoRelease.cs | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModHoldOff.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModHoldOff.cs index 4e6cc4f1d6..eba0b2effe 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModHoldOff.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModHoldOff.cs @@ -27,7 +27,7 @@ namespace osu.Game.Rulesets.Mania.Mods public override ModType Type => ModType.Conversion; - public override Type[] IncompatibleMods => new[] { typeof(ManiaModInvert) }; + public override Type[] IncompatibleMods => new[] { typeof(ManiaModInvert), typeof(ManiaModNoRelease) }; public void ApplyToBeatmap(IBeatmap beatmap) { diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModNoRelease.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModNoRelease.cs index 8cb2e821e6..b5490aa950 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModNoRelease.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModNoRelease.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.Linq; using System.Threading; using osu.Framework.Localisation; @@ -27,6 +28,8 @@ namespace osu.Game.Rulesets.Mania.Mods public override ModType Type => ModType.DifficultyReduction; + public override Type[] IncompatibleMods => new[] { typeof(ManiaModHoldOff) }; + public void ApplyToBeatmap(IBeatmap beatmap) { var maniaBeatmap = (ManiaBeatmap)beatmap; From d7651ef38728b31a2ae011ea4b428483cdc24cc7 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 19 Jul 2024 16:52:10 +0300 Subject: [PATCH 1561/2493] Add extensive test cases for correct input handling while paused in osu! & non-osu! --- .../Gameplay/TestScenePauseInputHandling.cs | 267 ++++++++++++++++++ osu.Game/Screens/Play/HUD/KeyCounter.cs | 8 +- osu.Game/Screens/Play/Player.cs | 4 +- 3 files changed, 274 insertions(+), 5 deletions(-) create mode 100644 osu.Game.Tests/Visual/Gameplay/TestScenePauseInputHandling.cs diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePauseInputHandling.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePauseInputHandling.cs new file mode 100644 index 0000000000..d778f2e991 --- /dev/null +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePauseInputHandling.cs @@ -0,0 +1,267 @@ +// 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 NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Testing; +using osu.Framework.Timing; +using osu.Game.Beatmaps; +using osu.Game.Configuration; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Mania; +using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.Osu.UI; +using osu.Game.Screens.Play.HUD; +using osu.Game.Skinning; +using osu.Game.Storyboards; +using osuTK.Input; + +namespace osu.Game.Tests.Visual.Gameplay +{ + public partial class TestScenePauseInputHandling : PlayerTestScene + { + private Ruleset currentRuleset = new OsuRuleset(); + + protected override Ruleset CreatePlayerRuleset() => currentRuleset; + + protected override bool HasCustomSteps => true; + + [Resolved] + private AudioManager audioManager { get; set; } = null!; + + protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard? storyboard = null) => + new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audioManager); + + [SetUp] + public void SetUp() => Schedule(() => + { + foreach (var key in InputManager.CurrentState.Keyboard.Keys) + InputManager.ReleaseKey(key); + + InputManager.MoveMouseTo(Content); + LocalConfig.SetValue(OsuSetting.KeyOverlay, true); + }); + + [Test] + public void TestOsuInputNotReceivedWhilePaused() + { + KeyCounter counter = null!; + + loadPlayer(() => new OsuRuleset()); + AddStep("get key counter", () => counter = this.ChildrenOfType().Single(k => k.Trigger is KeyCounterActionTrigger actionTrigger && actionTrigger.Action == OsuAction.LeftButton)); + checkKey(() => counter, 0, false); + + AddStep("press Z", () => InputManager.PressKey(Key.Z)); + checkKey(() => counter, 1, true); + + AddStep("release Z", () => InputManager.ReleaseKey(Key.Z)); + checkKey(() => counter, 1, false); + + AddStep("pause", () => Player.Pause()); + AddStep("press Z", () => InputManager.PressKey(Key.Z)); + checkKey(() => counter, 1, false); + + AddStep("release Z", () => InputManager.ReleaseKey(Key.Z)); + checkKey(() => counter, 1, false); + + AddStep("resume", () => Player.Resume()); + AddStep("go to resume cursor", () => InputManager.MoveMouseTo(this.ChildrenOfType().Single())); + AddStep("press Z to resume", () => InputManager.PressKey(Key.Z)); + + // Z key was released before pause, resuming should not trigger it + checkKey(() => counter, 1, false); + + AddStep("release Z", () => InputManager.ReleaseKey(Key.Z)); + checkKey(() => counter, 1, false); + + AddStep("press Z", () => InputManager.PressKey(Key.Z)); + checkKey(() => counter, 2, true); + + AddStep("release Z", () => InputManager.ReleaseKey(Key.Z)); + checkKey(() => counter, 2, false); + } + + [Test] + public void TestManiaInputNotReceivedWhilePaused() + { + KeyCounter counter = null!; + + loadPlayer(() => new ManiaRuleset()); + AddStep("get key counter", () => counter = this.ChildrenOfType().Single(k => k.Trigger is KeyCounterActionTrigger actionTrigger && actionTrigger.Action == ManiaAction.Key1)); + checkKey(() => counter, 0, false); + + AddStep("press D", () => InputManager.PressKey(Key.D)); + checkKey(() => counter, 1, true); + + AddStep("release D", () => InputManager.ReleaseKey(Key.D)); + checkKey(() => counter, 1, false); + + AddStep("pause", () => Player.Pause()); + AddStep("press D", () => InputManager.PressKey(Key.D)); + checkKey(() => counter, 1, false); + + AddStep("release D", () => InputManager.ReleaseKey(Key.D)); + checkKey(() => counter, 1, false); + + AddStep("resume", () => Player.Resume()); + AddUntilStep("wait for resume", () => Player.GameplayClockContainer.IsRunning); + checkKey(() => counter, 1, false); + + AddStep("press D", () => InputManager.PressKey(Key.D)); + checkKey(() => counter, 2, true); + + AddStep("release D", () => InputManager.ReleaseKey(Key.D)); + checkKey(() => counter, 2, false); + } + + [Test] + public void TestOsuPreviouslyHeldInputReleaseOnResume() + { + KeyCounter counterZ = null!; + KeyCounter counterX = null!; + + loadPlayer(() => new OsuRuleset()); + AddStep("get key counter Z", () => counterZ = this.ChildrenOfType().Single(k => k.Trigger is KeyCounterActionTrigger actionTrigger && actionTrigger.Action == OsuAction.LeftButton)); + AddStep("get key counter X", () => counterX = this.ChildrenOfType().Single(k => k.Trigger is KeyCounterActionTrigger actionTrigger && actionTrigger.Action == OsuAction.RightButton)); + + AddStep("press Z", () => InputManager.PressKey(Key.Z)); + AddStep("pause", () => Player.Pause()); + + AddStep("release Z", () => InputManager.ReleaseKey(Key.Z)); + + AddStep("resume", () => Player.Resume()); + AddStep("go to resume cursor", () => InputManager.MoveMouseTo(this.ChildrenOfType().Single())); + AddStep("press and release Z", () => InputManager.Key(Key.Z)); + checkKey(() => counterZ, 1, false); + + AddStep("press X", () => InputManager.PressKey(Key.X)); + AddStep("pause", () => Player.Pause()); + AddStep("release X", () => InputManager.ReleaseKey(Key.X)); + checkKey(() => counterX, 1, true); + + AddStep("resume", () => Player.Resume()); + AddStep("go to resume cursor", () => InputManager.MoveMouseTo(this.ChildrenOfType().Single())); + AddStep("press Z to resume", () => InputManager.PressKey(Key.Z)); + checkKey(() => counterZ, 1, false); + checkKey(() => counterX, 1, false); + } + + [Test] + public void TestManiaPreviouslyHeldInputReleaseOnResume() + { + KeyCounter counter = null!; + + loadPlayer(() => new ManiaRuleset()); + AddStep("get key counter", () => counter = this.ChildrenOfType().Single(k => k.Trigger is KeyCounterActionTrigger actionTrigger && actionTrigger.Action == ManiaAction.Key1)); + + AddStep("press D", () => InputManager.PressKey(Key.D)); + AddStep("pause", () => Player.Pause()); + + AddStep("release D", () => InputManager.ReleaseKey(Key.D)); + checkKey(() => counter, 1, true); + + AddStep("resume", () => Player.Resume()); + AddUntilStep("wait for resume", () => Player.GameplayClockContainer.IsRunning); + checkKey(() => counter, 1, false); + } + + [Test] + public void TestOsuHeldInputRemainHeldAfterResume() + { + KeyCounter counterZ = null!; + KeyCounter counterX = null!; + + loadPlayer(() => new OsuRuleset()); + AddStep("get key counter Z", () => counterZ = this.ChildrenOfType().Single(k => k.Trigger is KeyCounterActionTrigger actionTrigger && actionTrigger.Action == OsuAction.LeftButton)); + AddStep("get key counter X", () => counterX = this.ChildrenOfType().Single(k => k.Trigger is KeyCounterActionTrigger actionTrigger && actionTrigger.Action == OsuAction.RightButton)); + + AddStep("press Z", () => InputManager.PressKey(Key.Z)); + AddStep("pause", () => Player.Pause()); + + AddStep("release Z", () => InputManager.ReleaseKey(Key.Z)); + + AddStep("resume", () => Player.Resume()); + AddStep("go to resume cursor", () => InputManager.MoveMouseTo(this.ChildrenOfType().Single())); + AddStep("press Z to resume", () => InputManager.PressKey(Key.Z)); + checkKey(() => counterZ, 1, true); + + AddStep("release Z", () => InputManager.ReleaseKey(Key.Z)); + checkKey(() => counterZ, 1, false); + + AddStep("press X", () => InputManager.PressKey(Key.X)); + checkKey(() => counterX, 1, true); + + AddStep("pause", () => Player.Pause()); + + AddStep("release X", () => InputManager.ReleaseKey(Key.X)); + AddStep("press X", () => InputManager.PressKey(Key.X)); + + AddStep("resume", () => Player.Resume()); + AddStep("go to resume cursor", () => InputManager.MoveMouseTo(this.ChildrenOfType().Single())); + AddStep("press Z to resume", () => InputManager.PressKey(Key.Z)); + checkKey(() => counterZ, 1, false); + checkKey(() => counterX, 1, true); + + AddStep("release X", () => InputManager.ReleaseKey(Key.X)); + checkKey(() => counterZ, 1, false); + checkKey(() => counterX, 1, false); + } + + [Test] + public void TestManiaHeldInputRemainHeldAfterResume() + { + KeyCounter counter = null!; + + loadPlayer(() => new ManiaRuleset()); + AddStep("get key counter", () => counter = this.ChildrenOfType().Single(k => k.Trigger is KeyCounterActionTrigger actionTrigger && actionTrigger.Action == ManiaAction.Key1)); + + AddStep("press D", () => InputManager.PressKey(Key.D)); + checkKey(() => counter, 1, true); + + AddStep("pause", () => Player.Pause()); + + AddStep("release D", () => InputManager.ReleaseKey(Key.D)); + AddStep("press D", () => InputManager.PressKey(Key.D)); + + AddStep("resume", () => Player.Resume()); + AddUntilStep("wait for resume", () => Player.GameplayClockContainer.IsRunning); + checkKey(() => counter, 1, true); + + AddStep("release D", () => InputManager.ReleaseKey(Key.D)); + checkKey(() => counter, 1, false); + } + + private void loadPlayer(Func createRuleset) + { + AddStep("set ruleset", () => currentRuleset = createRuleset()); + AddStep("load player", LoadPlayer); + AddUntilStep("player loaded", () => Player.IsLoaded && Player.Alpha == 1); + AddUntilStep("wait for hud", () => Player.HUDOverlay.ChildrenOfType().All(s => s.ComponentsLoaded)); + + AddStep("seek to gameplay", () => Player.GameplayClockContainer.Seek(20000)); + AddUntilStep("wait for seek to finish", () => Player.DrawableRuleset.FrameStableClock.CurrentTime, () => Is.EqualTo(20000).Within(500)); + AddAssert("not in break", () => !Player.IsBreakTime.Value); + } + + private void checkKey(Func counter, int count, bool active) + { + AddAssert($"key count = {count}", () => counter().CountPresses.Value, () => Is.EqualTo(count)); + AddAssert($"key active = {active}", () => counter().IsActive.Value, () => Is.EqualTo(active)); + } + + protected override TestPlayer CreatePlayer(Ruleset ruleset) => new PausePlayer(); + + private partial class PausePlayer : TestPlayer + { + protected override double PauseCooldownDuration => 0; + + public PausePlayer() + : base(allowPause: true, showResults: false) + { + } + } + } +} diff --git a/osu.Game/Screens/Play/HUD/KeyCounter.cs b/osu.Game/Screens/Play/HUD/KeyCounter.cs index f12d2166fc..66f9dfd6f2 100644 --- a/osu.Game/Screens/Play/HUD/KeyCounter.cs +++ b/osu.Game/Screens/Play/HUD/KeyCounter.cs @@ -24,7 +24,9 @@ namespace osu.Game.Screens.Play.HUD /// /// Whether this is currently in the "activated" state because the associated key is currently pressed. /// - protected readonly Bindable IsActive = new BindableBool(); + public IBindable IsActive => isActive; + + private readonly Bindable isActive = new BindableBool(); protected KeyCounter(InputTrigger trigger) { @@ -36,12 +38,12 @@ namespace osu.Game.Screens.Play.HUD protected virtual void Activate(bool forwardPlayback = true) { - IsActive.Value = true; + isActive.Value = true; } protected virtual void Deactivate(bool forwardPlayback = true) { - IsActive.Value = false; + isActive.Value = false; } protected override void Dispose(bool isDisposing) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 3a08d3be24..4a419e1431 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -987,14 +987,14 @@ namespace osu.Game.Screens.Play /// /// The amount of gameplay time after which a second pause is allowed. /// - private const double pause_cooldown = 1000; + protected virtual double PauseCooldownDuration => 1000; protected PauseOverlay PauseOverlay { get; private set; } private double? lastPauseActionTime; protected bool PauseCooldownActive => - lastPauseActionTime.HasValue && GameplayClockContainer.CurrentTime < lastPauseActionTime + pause_cooldown; + lastPauseActionTime.HasValue && GameplayClockContainer.CurrentTime < lastPauseActionTime + PauseCooldownDuration; /// /// A set of conditionals which defines whether the current game state and configuration allows for From 4f6c7fe7c3c6941db20abc28cf321c03fa58646c Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 19 Jul 2024 16:52:48 +0300 Subject: [PATCH 1562/2493] Schedule resume operation by one frame to ensure the triggered key down event does not cause a gameplay press --- osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs b/osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs index a04ea80640..8a137e6665 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs @@ -116,7 +116,7 @@ namespace osu.Game.Rulesets.Osu.UI scaleTransitionContainer.ScaleTo(2, TRANSITION_TIME, Easing.OutQuint); - ResumeRequested?.Invoke(); + Schedule(() => ResumeRequested?.Invoke()); return true; } From 818b60a3d80aa5e3c702fadf9cd83397e56e2a66 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 19 Jul 2024 18:18:37 +0300 Subject: [PATCH 1563/2493] Fix pause overlay hiding input from ruleset input manager If a key is pressed while the pause overlay is visible, the ruleset input manager will not see it, therefore if the user resumes while the key is held then releases the key, the ruleset input manager will not receive the key up event. --- osu.Game/Screens/Play/GameplayMenuOverlay.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game/Screens/Play/GameplayMenuOverlay.cs b/osu.Game/Screens/Play/GameplayMenuOverlay.cs index da239d585e..2b961278d5 100644 --- a/osu.Game/Screens/Play/GameplayMenuOverlay.cs +++ b/osu.Game/Screens/Play/GameplayMenuOverlay.cs @@ -32,8 +32,6 @@ namespace osu.Game.Screens.Play private const int button_height = 70; private const float background_alpha = 0.75f; - protected override bool BlockNonPositionalInput => true; - protected override bool BlockScrollInput => false; public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; From e539670df1d6d73c077b5a3792ea27c3318e464e Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 19 Jul 2024 19:19:36 +0300 Subject: [PATCH 1564/2493] Add explanatory note --- osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs b/osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs index 8a137e6665..d809f2b318 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs @@ -116,6 +116,8 @@ namespace osu.Game.Rulesets.Osu.UI scaleTransitionContainer.ScaleTo(2, TRANSITION_TIME, Easing.OutQuint); + // When resuming with a button, we do not want the osu! input manager to see this button press and include it in the score. + // To ensure that this works correctly, schedule the resume operation one frame forward, since the resume operation enables the input manager to see input events. Schedule(() => ResumeRequested?.Invoke()); return true; } From d914b990f3ee62b5aacb48c5893340aff396c73c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 20 Jul 2024 14:08:00 +0900 Subject: [PATCH 1565/2493] 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 fe0a452e92..7785cb3c94 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -10,7 +10,7 @@ true - + diff --git a/osu.iOS.props b/osu.iOS.props index acfcae7c93..dceb88c6f7 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -23,6 +23,6 @@ iossimulator-x64 - + From 3296beb00362a55e012f6b809d901da02174552b Mon Sep 17 00:00:00 2001 From: Layendan Date: Sat, 20 Jul 2024 11:49:46 -0700 Subject: [PATCH 1566/2493] Added collection button to result screen --- osu.Game/Screens/Ranking/CollectionButton.cs | 64 ++++++++++ osu.Game/Screens/Ranking/CollectionPopover.cs | 70 +++++++++++ osu.Game/Screens/Ranking/FavouriteButton.cs | 7 +- osu.Game/Screens/Ranking/ResultsScreen.cs | 118 ++++++++++-------- 4 files changed, 201 insertions(+), 58 deletions(-) create mode 100644 osu.Game/Screens/Ranking/CollectionButton.cs create mode 100644 osu.Game/Screens/Ranking/CollectionPopover.cs diff --git a/osu.Game/Screens/Ranking/CollectionButton.cs b/osu.Game/Screens/Ranking/CollectionButton.cs new file mode 100644 index 0000000000..99a51e03d9 --- /dev/null +++ b/osu.Game/Screens/Ranking/CollectionButton.cs @@ -0,0 +1,64 @@ +// 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.Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Cursor; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.UserInterface; +using osu.Game.Beatmaps; +using osu.Game.Graphics; +using osu.Game.Graphics.UserInterface; +using osuTK; + +namespace osu.Game.Screens.Ranking +{ + public partial class CollectionButton : OsuAnimatedButton, IHasPopover + { + private readonly Box background; + + private readonly BeatmapInfo beatmapInfo; + + public CollectionButton(BeatmapInfo beatmapInfo) + { + this.beatmapInfo = beatmapInfo; + + Size = new Vector2(50, 30); + + Children = new Drawable[] + { + background = new Box + { + RelativeSizeAxes = Axes.Both, + Depth = float.MaxValue + }, + new SpriteIcon + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(13), + Icon = FontAwesome.Solid.Book, + }, + }; + + TooltipText = "collections"; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + background.Colour = colours.Green; + + Action = this.ShowPopover; + } + + // use Content for tracking input as some buttons might be temporarily hidden with DisappearToBottom, and they become hidden by moving Content away from screen. + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => Content.ReceivePositionalInputAt(screenSpacePos); + + public Popover GetPopover() => new CollectionPopover(beatmapInfo); + } +} diff --git a/osu.Game/Screens/Ranking/CollectionPopover.cs b/osu.Game/Screens/Ranking/CollectionPopover.cs new file mode 100644 index 0000000000..926745d4d9 --- /dev/null +++ b/osu.Game/Screens/Ranking/CollectionPopover.cs @@ -0,0 +1,70 @@ +// 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.Linq; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Input.Events; +using osu.Game.Beatmaps; +using osu.Game.Collections; +using osu.Game.Database; +using osu.Game.Graphics.UserInterface; +using osu.Game.Graphics.UserInterfaceV2; + +namespace osu.Game.Screens.Ranking +{ + public partial class CollectionPopover : OsuPopover + { + private OsuMenu menu; + private readonly BeatmapInfo beatmapInfo; + + [Resolved] + private RealmAccess realm { get; set; } = null!; + [Resolved] + private ManageCollectionsDialog? manageCollectionsDialog { get; set; } + + public CollectionPopover(BeatmapInfo beatmapInfo) : base(false) + { + this.beatmapInfo = beatmapInfo; + } + + [BackgroundDependencyLoader] + private void load() + { + Margin = new MarginPadding(5); + Body.CornerRadius = 4; + + Children = new[] + { + menu = new OsuMenu(Direction.Vertical, true) + { + Items = items, + }, + }; + } + + protected override void OnFocusLost(FocusLostEvent e) + { + base.OnFocusLost(e); + Hide(); + } + + private OsuMenuItem[] items + { + get + { + var collectionItems = realm.Realm.All() + .OrderBy(c => c.Name) + .AsEnumerable() + .Select(c => new CollectionToggleMenuItem(c.ToLive(realm), beatmapInfo)).Cast().ToList(); + + if (manageCollectionsDialog != null) + collectionItems.Add(new OsuMenuItem("Manage...", MenuItemType.Standard, manageCollectionsDialog.Show)); + + return collectionItems.ToArray(); + } + } + } +} diff --git a/osu.Game/Screens/Ranking/FavouriteButton.cs b/osu.Game/Screens/Ranking/FavouriteButton.cs index ee093d343e..6014929242 100644 --- a/osu.Game/Screens/Ranking/FavouriteButton.cs +++ b/osu.Game/Screens/Ranking/FavouriteButton.cs @@ -71,23 +71,20 @@ namespace osu.Game.Screens.Ranking } [BackgroundDependencyLoader] - private void load(OsuColour colours, IAPIProvider api) + private void load() { - this.api = api; - updateState(); localUser.BindTo(api.LocalUser); localUser.BindValueChanged(_ => updateEnabled()); - Action = () => toggleFavouriteStatus(); + Action = toggleFavouriteStatus; } protected override void LoadComplete() { base.LoadComplete(); - Action = toggleFavouriteStatus; current.BindValueChanged(_ => updateState(), true); } diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs index e96265be3d..b88a3cd2f8 100644 --- a/osu.Game/Screens/Ranking/ResultsScreen.cs +++ b/osu.Game/Screens/Ranking/ResultsScreen.cs @@ -12,6 +12,7 @@ using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; @@ -99,73 +100,79 @@ namespace osu.Game.Screens.Ranking popInSample = audio.Samples.Get(@"UI/overlay-pop-in"); - InternalChild = new GridContainer + InternalChild = new PopoverContainer { + Depth = -1, RelativeSizeAxes = Axes.Both, - Content = new[] + Padding = new MarginPadding(0), + Child = new GridContainer { - new Drawable[] + RelativeSizeAxes = Axes.Both, + Content = new[] { - VerticalScrollContent = new VerticalScrollContainer + new Drawable[] { - RelativeSizeAxes = Axes.Both, - ScrollbarVisible = false, - Child = new Container + VerticalScrollContent = new VerticalScrollContainer { RelativeSizeAxes = Axes.Both, - Children = new Drawable[] - { - StatisticsPanel = createStatisticsPanel().With(panel => - { - panel.RelativeSizeAxes = Axes.Both; - panel.Score.BindTarget = SelectedScore; - }), - ScorePanelList = new ScorePanelList - { - RelativeSizeAxes = Axes.Both, - SelectedScore = { BindTarget = SelectedScore }, - PostExpandAction = () => StatisticsPanel.ToggleVisibility() - }, - detachedPanelContainer = new Container - { - RelativeSizeAxes = Axes.Both - }, - } - } - }, - }, - new[] - { - bottomPanel = new Container - { - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - RelativeSizeAxes = Axes.X, - Height = TwoLayerButton.SIZE_EXTENDED.Y, - Alpha = 0, - Children = new Drawable[] - { - new Box + ScrollbarVisible = false, + Child = new Container { RelativeSizeAxes = Axes.Both, - Colour = Color4Extensions.FromHex("#333") - }, - buttons = new FillFlowContainer + Children = new Drawable[] + { + StatisticsPanel = createStatisticsPanel().With(panel => + { + panel.RelativeSizeAxes = Axes.Both; + panel.Score.BindTarget = SelectedScore; + }), + ScorePanelList = new ScorePanelList + { + RelativeSizeAxes = Axes.Both, + SelectedScore = { BindTarget = SelectedScore }, + PostExpandAction = () => StatisticsPanel.ToggleVisibility() + }, + detachedPanelContainer = new Container + { + RelativeSizeAxes = Axes.Both + }, + } + } + }, + }, + new[] + { + bottomPanel = new Container + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + RelativeSizeAxes = Axes.X, + Height = TwoLayerButton.SIZE_EXTENDED.Y, + Alpha = 0, + Children = new Drawable[] { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - AutoSizeAxes = Axes.Both, - Spacing = new Vector2(5), - Direction = FillDirection.Horizontal + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4Extensions.FromHex("#333") + }, + buttons = new FillFlowContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + AutoSizeAxes = Axes.Both, + Spacing = new Vector2(5), + Direction = FillDirection.Horizontal + }, } } } + }, + RowDimensions = new[] + { + new Dimension(), + new Dimension(GridSizeMode.AutoSize) } - }, - RowDimensions = new[] - { - new Dimension(), - new Dimension(GridSizeMode.AutoSize) } }; @@ -205,6 +212,11 @@ namespace osu.Game.Screens.Ranking }); } + if (Score?.BeatmapInfo != null) + { + buttons.Add(new CollectionButton(Score.BeatmapInfo) { Width = 75 }); + } + // Do not render if user is not logged in or the mapset does not have a valid online ID. if (api.IsLoggedIn && Score?.BeatmapInfo?.BeatmapSet != null && Score.BeatmapInfo.BeatmapSet.OnlineID > 0) { From c16b7c5c707f62be237d280fd477101532dbf8a8 Mon Sep 17 00:00:00 2001 From: Layendan Date: Sun, 21 Jul 2024 10:01:06 -0700 Subject: [PATCH 1567/2493] Update favorite button --- osu.Game/Screens/Ranking/FavouriteButton.cs | 62 ++++++++++++++------- osu.Game/Screens/Ranking/ResultsScreen.cs | 23 ++------ 2 files changed, 47 insertions(+), 38 deletions(-) diff --git a/osu.Game/Screens/Ranking/FavouriteButton.cs b/osu.Game/Screens/Ranking/FavouriteButton.cs index 6014929242..5a8cd51c65 100644 --- a/osu.Game/Screens/Ranking/FavouriteButton.cs +++ b/osu.Game/Screens/Ranking/FavouriteButton.cs @@ -9,6 +9,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Logging; +using osu.Game.Beatmaps; using osu.Game.Beatmaps.Drawables.Cards; using osu.Game.Graphics; using osu.Game.Graphics.UserInterface; @@ -24,15 +25,10 @@ namespace osu.Game.Screens.Ranking { private readonly Box background; private readonly SpriteIcon icon; - private readonly BindableWithCurrent current; - public Bindable Current - { - get => current.Current; - set => current.Current = value; - } - - private readonly APIBeatmapSet beatmapSet; + private readonly BeatmapSetInfo beatmapSetInfo; + private APIBeatmapSet beatmapSet; + private Bindable current; private PostBeatmapFavouriteRequest favouriteRequest; private LoadingLayer loading; @@ -45,10 +41,9 @@ namespace osu.Game.Screens.Ranking [Resolved] private OsuColour colours { get; set; } - public FavouriteButton(APIBeatmapSet beatmapSet) + public FavouriteButton(BeatmapSetInfo beatmapSetInfo) { - this.beatmapSet = beatmapSet; - current = new BindableWithCurrent(new BeatmapSetFavouriteState(this.beatmapSet.HasFavourited, this.beatmapSet.FavouriteCount)); + this.beatmapSetInfo = beatmapSetInfo; Size = new Vector2(50, 30); @@ -68,24 +63,42 @@ namespace osu.Game.Screens.Ranking }, loading = new LoadingLayer(true, false), }; + + Action = toggleFavouriteStatus; } [BackgroundDependencyLoader] private void load() { - updateState(); + current = new BindableWithCurrent(new BeatmapSetFavouriteState(false, 0)); + current.BindValueChanged(_ => updateState(), true); localUser.BindTo(api.LocalUser); - localUser.BindValueChanged(_ => updateEnabled()); - - Action = toggleFavouriteStatus; + localUser.BindValueChanged(_ => updateUser(), true); } - protected override void LoadComplete() + private void getBeatmapSet() { - base.LoadComplete(); + GetBeatmapSetRequest beatmapSetRequest; + beatmapSetRequest = new GetBeatmapSetRequest(beatmapSetInfo.OnlineID); - current.BindValueChanged(_ => updateState(), true); + loading.Show(); + beatmapSetRequest.Success += beatmapSet => + { + this.beatmapSet = beatmapSet; + current.Value = new BeatmapSetFavouriteState(this.beatmapSet.HasFavourited, this.beatmapSet.FavouriteCount); + + loading.Hide(); + Enabled.Value = true; + }; + beatmapSetRequest.Failure += e => + { + Logger.Error(e, $"Failed to fetch beatmap info: {e.Message}"); + + loading.Hide(); + Enabled.Value = false; + }; + api.Queue(beatmapSetRequest); } private void toggleFavouriteStatus() @@ -118,7 +131,18 @@ namespace osu.Game.Screens.Ranking api.Queue(favouriteRequest); } - private void updateEnabled() => Enabled.Value = !(localUser.Value is GuestUser) && beatmapSet.OnlineID > 0; + private void updateUser() + { + if (!(localUser.Value is GuestUser) && beatmapSetInfo.OnlineID > 0) + getBeatmapSet(); + else + { + Enabled.Value = false; + current.Value = new BeatmapSetFavouriteState(false, 0); + updateState(); + TooltipText = BeatmapsetsStrings.ShowDetailsFavouriteLogin; + } + } private void updateState() { diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs index b88a3cd2f8..befd024ccb 100644 --- a/osu.Game/Screens/Ranking/ResultsScreen.cs +++ b/osu.Game/Screens/Ranking/ResultsScreen.cs @@ -16,7 +16,6 @@ using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; -using osu.Framework.Logging; using osu.Framework.Screens; using osu.Game.Graphics; using osu.Game.Graphics.Containers; @@ -24,7 +23,6 @@ using osu.Game.Graphics.UserInterface; using osu.Game.Input.Bindings; using osu.Game.Localisation; using osu.Game.Online.API; -using osu.Game.Online.API.Requests; using osu.Game.Online.Placeholders; using osu.Game.Overlays; using osu.Game.Scoring; @@ -217,25 +215,12 @@ namespace osu.Game.Screens.Ranking buttons.Add(new CollectionButton(Score.BeatmapInfo) { Width = 75 }); } - // Do not render if user is not logged in or the mapset does not have a valid online ID. - if (api.IsLoggedIn && Score?.BeatmapInfo?.BeatmapSet != null && Score.BeatmapInfo.BeatmapSet.OnlineID > 0) + if (Score?.BeatmapInfo?.BeatmapSet != null && Score.BeatmapInfo.BeatmapSet.OnlineID > 0) { - GetBeatmapSetRequest beatmapSetRequest; - beatmapSetRequest = new GetBeatmapSetRequest(Score.BeatmapInfo.BeatmapSet.OnlineID); - - beatmapSetRequest.Success += (beatmapSet) => + buttons.Add(new FavouriteButton(Score.BeatmapInfo.BeatmapSet) { - buttons.Add(new FavouriteButton(beatmapSet) - { - Width = 75 - }); - }; - beatmapSetRequest.Failure += e => - { - Logger.Error(e, $"Failed to fetch beatmap info: {e.Message}"); - }; - - api.Queue(beatmapSetRequest); + Width = 75 + }); } } From a575566638fc65abba4f7baa91f57917b9b604e6 Mon Sep 17 00:00:00 2001 From: Layendan Date: Sun, 21 Jul 2024 16:14:26 -0700 Subject: [PATCH 1568/2493] Add tests --- .../Ranking/TestSceneCollectionButton.cs | 71 +++++++++++++++ .../Ranking/TestSceneFavouriteButton.cs | 90 +++++++++++++++++++ osu.Game/Screens/Ranking/FavouriteButton.cs | 14 +-- 3 files changed, 168 insertions(+), 7 deletions(-) create mode 100644 osu.Game.Tests/Visual/Ranking/TestSceneCollectionButton.cs create mode 100644 osu.Game.Tests/Visual/Ranking/TestSceneFavouriteButton.cs diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneCollectionButton.cs b/osu.Game.Tests/Visual/Ranking/TestSceneCollectionButton.cs new file mode 100644 index 0000000000..7bc2964cdf --- /dev/null +++ b/osu.Game.Tests/Visual/Ranking/TestSceneCollectionButton.cs @@ -0,0 +1,71 @@ +// 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.Linq; +using NUnit.Framework; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; +using osu.Framework.Testing; +using osu.Game.Beatmaps; +using osu.Game.Screens.Ranking; +using osuTK; +using osuTK.Input; + +namespace osu.Game.Tests.Visual.Ranking +{ + public partial class TestSceneCollectionButton : OsuManualInputManagerTestScene + { + private CollectionButton collectionButton; + private BeatmapInfo beatmapInfo = new BeatmapInfo { OnlineID = 88 }; + + [SetUpSteps] + public void SetUpSteps() + { + AddStep("create button", () => Child = new PopoverContainer + { + Depth = -1, + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Child = collectionButton = new CollectionButton(beatmapInfo) + { + RelativeSizeAxes = Axes.None, + Size = new Vector2(50), + } + }); + } + + [Test] + public void TestCollectionButton() + { + AddStep("click collection button", () => + { + InputManager.MoveMouseTo(collectionButton); + InputManager.Click(MouseButton.Left); + }); + + AddAssert("collection popover is visible", () => this.ChildrenOfType().Single().State.Value == Visibility.Visible); + + AddStep("click outside popover", () => + { + InputManager.MoveMouseTo(ScreenSpaceDrawQuad.TopLeft); + InputManager.Click(MouseButton.Left); + }); + + AddAssert("collection popover is hidden", () => this.ChildrenOfType().Single().State.Value == Visibility.Hidden); + + AddStep("click collection button", () => + { + InputManager.MoveMouseTo(collectionButton); + InputManager.Click(MouseButton.Left); + }); + + AddStep("press escape", () => InputManager.Key(Key.Escape)); + + AddAssert("collection popover is hidden", () => this.ChildrenOfType().Single().State.Value == Visibility.Hidden); + } + } +} diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneFavouriteButton.cs b/osu.Game.Tests/Visual/Ranking/TestSceneFavouriteButton.cs new file mode 100644 index 0000000000..6ce9fdb87e --- /dev/null +++ b/osu.Game.Tests/Visual/Ranking/TestSceneFavouriteButton.cs @@ -0,0 +1,90 @@ +// 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.Graphics; +using osu.Framework.Testing; +using osu.Game.Beatmaps; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Screens.Ranking; +using osuTK; + +namespace osu.Game.Tests.Visual.Ranking +{ + public partial class TestSceneFavouriteButton : OsuTestScene + { + private FavouriteButton favourite; + + private readonly BeatmapSetInfo beatmapSetInfo = new BeatmapSetInfo { OnlineID = 88 }; + private readonly BeatmapSetInfo invalidBeatmapSetInfo = new BeatmapSetInfo(); + + private DummyAPIAccess dummyAPI => (DummyAPIAccess)API; + + + [SetUpSteps] + public void SetUpSteps() + { + AddStep("create button", () => Child = favourite = new FavouriteButton(beatmapSetInfo) + { + RelativeSizeAxes = Axes.None, + Size = new Vector2(50), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }); + + AddStep("register request handling", () => dummyAPI.HandleRequest = request => + { + if (!(request is GetBeatmapSetRequest beatmapSetRequest)) return false; + + beatmapSetRequest.TriggerSuccess(new APIBeatmapSet + { + OnlineID = beatmapSetRequest.ID, + HasFavourited = false, + FavouriteCount = 0, + }); + + return true; + }); + } + + [Test] + public void TestLoggedOutIn() + { + AddStep("log out", () => API.Logout()); + checkEnabled(false); + AddStep("log in", () => + { + API.Login("test", "test"); + ((DummyAPIAccess)API).AuthenticateSecondFactor("abcdefgh"); + }); + checkEnabled(true); + } + + [Test] + public void TestInvalidBeatmap() + { + AddStep("make beatmap invalid", () => Child = favourite = new FavouriteButton(invalidBeatmapSetInfo) + { + RelativeSizeAxes = Axes.None, + Size = new Vector2(50), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }); + AddStep("log in", () => + { + API.Login("test", "test"); + ((DummyAPIAccess)API).AuthenticateSecondFactor("abcdefgh"); + }); + checkEnabled(false); + } + + private void checkEnabled(bool expected) + { + AddAssert("is " + (expected ? "enabled" : "disabled"), () => favourite.Enabled.Value == expected); + } + } +} diff --git a/osu.Game/Screens/Ranking/FavouriteButton.cs b/osu.Game/Screens/Ranking/FavouriteButton.cs index 5a8cd51c65..2f2da8ae40 100644 --- a/osu.Game/Screens/Ranking/FavouriteButton.cs +++ b/osu.Game/Screens/Ranking/FavouriteButton.cs @@ -26,12 +26,12 @@ namespace osu.Game.Screens.Ranking private readonly Box background; private readonly SpriteIcon icon; - private readonly BeatmapSetInfo beatmapSetInfo; + public readonly BeatmapSetInfo BeatmapSetInfo; private APIBeatmapSet beatmapSet; - private Bindable current; + private readonly Bindable current; private PostBeatmapFavouriteRequest favouriteRequest; - private LoadingLayer loading; + private readonly LoadingLayer loading; private readonly IBindable localUser = new Bindable(); @@ -43,7 +43,8 @@ namespace osu.Game.Screens.Ranking public FavouriteButton(BeatmapSetInfo beatmapSetInfo) { - this.beatmapSetInfo = beatmapSetInfo; + BeatmapSetInfo = beatmapSetInfo; + current = new BindableWithCurrent(new BeatmapSetFavouriteState(false, 0)); Size = new Vector2(50, 30); @@ -70,7 +71,6 @@ namespace osu.Game.Screens.Ranking [BackgroundDependencyLoader] private void load() { - current = new BindableWithCurrent(new BeatmapSetFavouriteState(false, 0)); current.BindValueChanged(_ => updateState(), true); localUser.BindTo(api.LocalUser); @@ -80,7 +80,7 @@ namespace osu.Game.Screens.Ranking private void getBeatmapSet() { GetBeatmapSetRequest beatmapSetRequest; - beatmapSetRequest = new GetBeatmapSetRequest(beatmapSetInfo.OnlineID); + beatmapSetRequest = new GetBeatmapSetRequest(BeatmapSetInfo.OnlineID); loading.Show(); beatmapSetRequest.Success += beatmapSet => @@ -133,7 +133,7 @@ namespace osu.Game.Screens.Ranking private void updateUser() { - if (!(localUser.Value is GuestUser) && beatmapSetInfo.OnlineID > 0) + if (!(localUser.Value is GuestUser) && BeatmapSetInfo.OnlineID > 0) getBeatmapSet(); else { From e4cccb5e319ed2f83d445a169250cebe0b085845 Mon Sep 17 00:00:00 2001 From: Layendan Date: Sun, 21 Jul 2024 17:32:48 -0700 Subject: [PATCH 1569/2493] Fix lint errors --- .../Visual/Ranking/TestSceneCollectionButton.cs | 2 +- .../Visual/Ranking/TestSceneFavouriteButton.cs | 1 - osu.Game/Screens/Ranking/CollectionPopover.cs | 17 +++++++++-------- osu.Game/Screens/Ranking/FavouriteButton.cs | 4 +--- 4 files changed, 11 insertions(+), 13 deletions(-) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneCollectionButton.cs b/osu.Game.Tests/Visual/Ranking/TestSceneCollectionButton.cs index 7bc2964cdf..2cd75f6cef 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneCollectionButton.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneCollectionButton.cs @@ -19,7 +19,7 @@ namespace osu.Game.Tests.Visual.Ranking public partial class TestSceneCollectionButton : OsuManualInputManagerTestScene { private CollectionButton collectionButton; - private BeatmapInfo beatmapInfo = new BeatmapInfo { OnlineID = 88 }; + private readonly BeatmapInfo beatmapInfo = new BeatmapInfo { OnlineID = 88 }; [SetUpSteps] public void SetUpSteps() diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneFavouriteButton.cs b/osu.Game.Tests/Visual/Ranking/TestSceneFavouriteButton.cs index 6ce9fdb87e..b281fc1bbf 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneFavouriteButton.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneFavouriteButton.cs @@ -24,7 +24,6 @@ namespace osu.Game.Tests.Visual.Ranking private DummyAPIAccess dummyAPI => (DummyAPIAccess)API; - [SetUpSteps] public void SetUpSteps() { diff --git a/osu.Game/Screens/Ranking/CollectionPopover.cs b/osu.Game/Screens/Ranking/CollectionPopover.cs index 926745d4d9..98a5de597e 100644 --- a/osu.Game/Screens/Ranking/CollectionPopover.cs +++ b/osu.Game/Screens/Ranking/CollectionPopover.cs @@ -17,15 +17,16 @@ namespace osu.Game.Screens.Ranking { public partial class CollectionPopover : OsuPopover { - private OsuMenu menu; private readonly BeatmapInfo beatmapInfo; [Resolved] private RealmAccess realm { get; set; } = null!; - [Resolved] - private ManageCollectionsDialog? manageCollectionsDialog { get; set; } - public CollectionPopover(BeatmapInfo beatmapInfo) : base(false) + [Resolved] + private ManageCollectionsDialog manageCollectionsDialog { get; set; } + + public CollectionPopover(BeatmapInfo beatmapInfo) + : base(false) { this.beatmapInfo = beatmapInfo; } @@ -38,7 +39,7 @@ namespace osu.Game.Screens.Ranking Children = new[] { - menu = new OsuMenu(Direction.Vertical, true) + new OsuMenu(Direction.Vertical, true) { Items = items, }, @@ -56,9 +57,9 @@ namespace osu.Game.Screens.Ranking get { var collectionItems = realm.Realm.All() - .OrderBy(c => c.Name) - .AsEnumerable() - .Select(c => new CollectionToggleMenuItem(c.ToLive(realm), beatmapInfo)).Cast().ToList(); + .OrderBy(c => c.Name) + .AsEnumerable() + .Select(c => new CollectionToggleMenuItem(c.ToLive(realm), beatmapInfo)).Cast().ToList(); if (manageCollectionsDialog != null) collectionItems.Add(new OsuMenuItem("Manage...", MenuItemType.Standard, manageCollectionsDialog.Show)); diff --git a/osu.Game/Screens/Ranking/FavouriteButton.cs b/osu.Game/Screens/Ranking/FavouriteButton.cs index 2f2da8ae40..95e1fdf985 100644 --- a/osu.Game/Screens/Ranking/FavouriteButton.cs +++ b/osu.Game/Screens/Ranking/FavouriteButton.cs @@ -79,8 +79,7 @@ namespace osu.Game.Screens.Ranking private void getBeatmapSet() { - GetBeatmapSetRequest beatmapSetRequest; - beatmapSetRequest = new GetBeatmapSetRequest(BeatmapSetInfo.OnlineID); + GetBeatmapSetRequest beatmapSetRequest = new GetBeatmapSetRequest(BeatmapSetInfo.OnlineID); loading.Show(); beatmapSetRequest.Success += beatmapSet => @@ -103,7 +102,6 @@ namespace osu.Game.Screens.Ranking private void toggleFavouriteStatus() { - Enabled.Value = false; loading.Show(); From 6bb562db14ccd84ac27c45fe14623e9a443da7e4 Mon Sep 17 00:00:00 2001 From: Layendan Date: Sun, 21 Jul 2024 17:51:30 -0700 Subject: [PATCH 1570/2493] Fix collection popover --- osu.Game/Screens/Ranking/CollectionPopover.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/osu.Game/Screens/Ranking/CollectionPopover.cs b/osu.Game/Screens/Ranking/CollectionPopover.cs index 98a5de597e..2411ab99d8 100644 --- a/osu.Game/Screens/Ranking/CollectionPopover.cs +++ b/osu.Game/Screens/Ranking/CollectionPopover.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.Linq; using osu.Framework.Allocation; using osu.Framework.Graphics; @@ -23,7 +21,7 @@ namespace osu.Game.Screens.Ranking private RealmAccess realm { get; set; } = null!; [Resolved] - private ManageCollectionsDialog manageCollectionsDialog { get; set; } + private ManageCollectionsDialog? manageCollectionsDialog { get; set; } public CollectionPopover(BeatmapInfo beatmapInfo) : base(false) From 6a4872faa8323f9bf3abcc059f2895ea430963c7 Mon Sep 17 00:00:00 2001 From: Layendan Date: Sun, 21 Jul 2024 23:46:04 -0700 Subject: [PATCH 1571/2493] Remove nullable disable --- .../Visual/Ranking/TestSceneCollectionButton.cs | 8 +++----- .../Visual/Ranking/TestSceneFavouriteButton.cs | 6 ++---- osu.Game/Screens/Ranking/CollectionButton.cs | 2 -- osu.Game/Screens/Ranking/FavouriteButton.cs | 16 +++++++--------- 4 files changed, 12 insertions(+), 20 deletions(-) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneCollectionButton.cs b/osu.Game.Tests/Visual/Ranking/TestSceneCollectionButton.cs index 2cd75f6cef..4449aae257 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneCollectionButton.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneCollectionButton.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.Linq; using NUnit.Framework; using osu.Framework.Graphics; @@ -18,7 +16,7 @@ namespace osu.Game.Tests.Visual.Ranking { public partial class TestSceneCollectionButton : OsuManualInputManagerTestScene { - private CollectionButton collectionButton; + private CollectionButton? collectionButton; private readonly BeatmapInfo beatmapInfo = new BeatmapInfo { OnlineID = 88 }; [SetUpSteps] @@ -43,7 +41,7 @@ namespace osu.Game.Tests.Visual.Ranking { AddStep("click collection button", () => { - InputManager.MoveMouseTo(collectionButton); + InputManager.MoveMouseTo(collectionButton!); InputManager.Click(MouseButton.Left); }); @@ -59,7 +57,7 @@ namespace osu.Game.Tests.Visual.Ranking AddStep("click collection button", () => { - InputManager.MoveMouseTo(collectionButton); + InputManager.MoveMouseTo(collectionButton!); InputManager.Click(MouseButton.Left); }); diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneFavouriteButton.cs b/osu.Game.Tests/Visual/Ranking/TestSceneFavouriteButton.cs index b281fc1bbf..a90fbc0c84 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneFavouriteButton.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneFavouriteButton.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 NUnit.Framework; using osu.Framework.Graphics; using osu.Framework.Testing; @@ -17,7 +15,7 @@ namespace osu.Game.Tests.Visual.Ranking { public partial class TestSceneFavouriteButton : OsuTestScene { - private FavouriteButton favourite; + private FavouriteButton? favourite; private readonly BeatmapSetInfo beatmapSetInfo = new BeatmapSetInfo { OnlineID = 88 }; private readonly BeatmapSetInfo invalidBeatmapSetInfo = new BeatmapSetInfo(); @@ -83,7 +81,7 @@ namespace osu.Game.Tests.Visual.Ranking private void checkEnabled(bool expected) { - AddAssert("is " + (expected ? "enabled" : "disabled"), () => favourite.Enabled.Value == expected); + AddAssert("is " + (expected ? "enabled" : "disabled"), () => favourite!.Enabled.Value == expected); } } } diff --git a/osu.Game/Screens/Ranking/CollectionButton.cs b/osu.Game/Screens/Ranking/CollectionButton.cs index 99a51e03d9..a3e2864c7e 100644 --- a/osu.Game/Screens/Ranking/CollectionButton.cs +++ b/osu.Game/Screens/Ranking/CollectionButton.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.Extensions; using osu.Framework.Graphics; diff --git a/osu.Game/Screens/Ranking/FavouriteButton.cs b/osu.Game/Screens/Ranking/FavouriteButton.cs index 95e1fdf985..caa0eddb55 100644 --- a/osu.Game/Screens/Ranking/FavouriteButton.cs +++ b/osu.Game/Screens/Ranking/FavouriteButton.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.Bindables; using osu.Framework.Graphics; @@ -27,19 +25,19 @@ namespace osu.Game.Screens.Ranking private readonly SpriteIcon icon; public readonly BeatmapSetInfo BeatmapSetInfo; - private APIBeatmapSet beatmapSet; + private APIBeatmapSet? beatmapSet; private readonly Bindable current; - private PostBeatmapFavouriteRequest favouriteRequest; + private PostBeatmapFavouriteRequest? favouriteRequest; private readonly LoadingLayer loading; private readonly IBindable localUser = new Bindable(); [Resolved] - private IAPIProvider api { get; set; } + private IAPIProvider api { get; set; } = null!; [Resolved] - private OsuColour colours { get; set; } + private OsuColour colours { get; set; } = null!; public FavouriteButton(BeatmapSetInfo beatmapSetInfo) { @@ -102,6 +100,9 @@ namespace osu.Game.Screens.Ranking private void toggleFavouriteStatus() { + if (beatmapSet == null) + return; + Enabled.Value = false; loading.Show(); @@ -144,9 +145,6 @@ namespace osu.Game.Screens.Ranking private void updateState() { - if (current?.Value == null) - return; - if (current.Value.Favourited) { background.Colour = colours.Green; From e2fe1935a92943c5e505bf82cf76e4e59ab93298 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 22 Jul 2024 11:22:36 +0200 Subject: [PATCH 1572/2493] Add failing test case --- .../Editor/TestSceneEditorTestGameplay.cs | 70 +++++++++++++++++++ .../osu.Game.Rulesets.Taiko.Tests.csproj | 1 + 2 files changed, 71 insertions(+) create mode 100644 osu.Game.Rulesets.Taiko.Tests/Editor/TestSceneEditorTestGameplay.cs diff --git a/osu.Game.Rulesets.Taiko.Tests/Editor/TestSceneEditorTestGameplay.cs b/osu.Game.Rulesets.Taiko.Tests/Editor/TestSceneEditorTestGameplay.cs new file mode 100644 index 0000000000..2422e62571 --- /dev/null +++ b/osu.Game.Rulesets.Taiko.Tests/Editor/TestSceneEditorTestGameplay.cs @@ -0,0 +1,70 @@ +// 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.Allocation; +using osu.Framework.Extensions; +using osu.Framework.Testing; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Taiko.Objects; +using osu.Game.Screens.Edit; +using osu.Game.Screens.Edit.Components.Timelines.Summary; +using osu.Game.Screens.Edit.GameplayTest; +using osu.Game.Storyboards; +using osu.Game.Tests.Beatmaps.IO; +using osu.Game.Tests.Visual; +using osuTK.Input; + +namespace osu.Game.Rulesets.Taiko.Tests.Editor +{ + public partial class TestSceneTaikoEditorTestGameplay : EditorTestScene + { + protected override bool IsolateSavingFromDatabase => false; + + protected override Ruleset CreateEditorRuleset() => new TaikoRuleset(); + + [Resolved] + private OsuGameBase game { get; set; } = null!; + + [Resolved] + private BeatmapManager beatmaps { get; set; } = null!; + + private BeatmapSetInfo importedBeatmapSet = null!; + + public override void SetUpSteps() + { + AddStep("import test beatmap", () => importedBeatmapSet = BeatmapImportHelper.LoadOszIntoOsu(game).GetResultSafely()); + base.SetUpSteps(); + } + + protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard? storyboard = null) + => beatmaps.GetWorkingBeatmap(importedBeatmapSet.Beatmaps.First(b => b.Ruleset.OnlineID == 1)); + + [Test] + public void TestBasicGameplayTest() + { + AddStep("add objects", () => + { + EditorBeatmap.Clear(); + EditorBeatmap.Add(new Swell { StartTime = 500, EndTime = 1500 }); + EditorBeatmap.Add(new Hit { StartTime = 3000 }); + }); + AddStep("seek to 250", () => EditorClock.Seek(250)); + AddUntilStep("wait for seek", () => EditorClock.CurrentTime, () => Is.EqualTo(250)); + + AddStep("click test gameplay button", () => + { + var button = Editor.ChildrenOfType().Single(); + + InputManager.MoveMouseTo(button); + InputManager.Click(MouseButton.Left); + }); + AddUntilStep("save prompt shown", () => DialogOverlay.CurrentDialog is SaveRequiredPopupDialog); + + AddStep("save changes", () => DialogOverlay.CurrentDialog!.PerformOkAction()); + AddUntilStep("player pushed", () => Stack.CurrentScreen is EditorPlayer); + AddUntilStep("wait for return to editor", () => Stack.CurrentScreen is Screens.Edit.Editor); + } + } +} diff --git a/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj b/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj index 26afd42445..a2420fc679 100644 --- a/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj +++ b/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj @@ -11,5 +11,6 @@ + From 157cc884f4c48c3e130f655fe60343a3dfda1cf4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 22 Jul 2024 11:23:28 +0200 Subject: [PATCH 1573/2493] Fix swells not being correctly treated in editor gameplay test Closes https://github.com/ppy/osu/issues/28989. Because swell ticks are judged manually by their parenting objects, swell ticks were not given a start time (with the thinking that there isn't really one *to* give). This tripped up the "judge past objects" logic in `EditorPlayer`, since it would enumerate all objects (regardless of nesting) that are prior to current time and mark them as judged. With all swell ticks having the default start time of 0 they would get judged more often than not, leading to behaviour weirdness. To resolve, give swell ticks a *relatively* sane start time equal to the start time of the swell itself. --- osu.Game.Rulesets.Taiko/Objects/Swell.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game.Rulesets.Taiko/Objects/Swell.cs b/osu.Game.Rulesets.Taiko/Objects/Swell.cs index a8db8df021..d9e8c77ea7 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Swell.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Swell.cs @@ -33,6 +33,7 @@ namespace osu.Game.Rulesets.Taiko.Objects cancellationToken.ThrowIfCancellationRequested(); AddNested(new SwellTick { + StartTime = StartTime, Samples = Samples }); } From 636e965868866283b3848e74281ab6df897a9e53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 22 Jul 2024 11:26:04 +0200 Subject: [PATCH 1574/2493] Remove no-longer-valid test remark & adjust test --- .../TestSceneDrumSampleTriggerSource.cs | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneDrumSampleTriggerSource.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneDrumSampleTriggerSource.cs index 6c925f566b..b47f02afa3 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TestSceneDrumSampleTriggerSource.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneDrumSampleTriggerSource.cs @@ -315,10 +315,7 @@ namespace osu.Game.Rulesets.Taiko.Tests hitObjectContainer.Add(drawableSwell); }); - // You might think that this should be a SwellTick since we're before the swell, but SwellTicks get no StartTime (ie. they are zero). - // 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); + AddAssert("most valid object is swell tick", () => triggerSource.GetMostValidObject(), Is.InstanceOf); checkSamples(HitType.Centre, false, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK); checkSamples(HitType.Rim, false, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK); @@ -352,10 +349,7 @@ namespace osu.Game.Rulesets.Taiko.Tests hitObjectContainer.Add(drawableSwell); }); - // You might think that this should be a SwellTick since we're before the swell, but SwellTicks get no StartTime (ie. they are zero). - // 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); + AddAssert("most valid object is swell tick", () => triggerSource.GetMostValidObject(), Is.InstanceOf); checkSamples(HitType.Centre, false, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_DRUM); checkSamples(HitType.Rim, false, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_DRUM); From 9fb9a54a4d611eb4f23e47f02668d962f4a22d43 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Mon, 22 Jul 2024 11:34:07 +0200 Subject: [PATCH 1575/2493] hold shift to adjust velocity instead of duration --- .../Sliders/SliderSelectionBlueprint.cs | 47 +++++++++++++++---- 1 file changed, 38 insertions(+), 9 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs index 2f73dedc64..339ca55cb2 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs @@ -227,10 +227,19 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders } private Vector2 lengthAdjustMouseOffset; + private double oldDuration; + private double oldVelocity; + private double desiredDistance; + private bool isAdjustingLength; + private bool adjustVelocityMomentary; private void startAdjustingLength(DragStartEvent e) { + isAdjustingLength = true; + adjustVelocityMomentary = e.ShiftPressed; lengthAdjustMouseOffset = ToLocalSpace(e.ScreenSpaceMouseDownPosition) - HitObject.Position - HitObject.Path.PositionAt(1); + oldDuration = HitObject.Path.Distance / HitObject.SliderVelocityMultiplier; + oldVelocity = HitObject.SliderVelocityMultiplier; changeHandler?.BeginChange(); } @@ -238,22 +247,27 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders { trimExcessControlPoints(HitObject.Path); changeHandler?.EndChange(); + isAdjustingLength = false; } - private void adjustLength(MouseEvent e) + private void adjustLength(MouseEvent e) => adjustLength(findClosestPathDistance(e), e.ShiftPressed); + + private void adjustLength(double proposedDistance, bool adjustVelocity) { - double oldDistance = HitObject.Path.Distance; - double proposedDistance = findClosestPathDistance(e); + desiredDistance = proposedDistance; + proposedDistance = MathHelper.Clamp(proposedDistance, 1, HitObject.Path.CalculatedDistance); + double proposedVelocity = oldVelocity; - proposedDistance = MathHelper.Clamp(proposedDistance, 0, HitObject.Path.CalculatedDistance); - proposedDistance = MathHelper.Clamp(proposedDistance, - 0.1 * oldDistance / HitObject.SliderVelocityMultiplier, - 10 * oldDistance / HitObject.SliderVelocityMultiplier); + if (adjustVelocity) + { + proposedDistance = MathHelper.Clamp(proposedDistance, 0.1 * oldDuration, 10 * oldDuration); + proposedVelocity = proposedDistance / oldDuration; + } - if (Precision.AlmostEquals(proposedDistance, oldDistance)) + if (Precision.AlmostEquals(proposedDistance, HitObject.Path.Distance) && Precision.AlmostEquals(proposedVelocity, HitObject.SliderVelocityMultiplier)) return; - HitObject.SliderVelocityMultiplier *= proposedDistance / oldDistance; + HitObject.SliderVelocityMultiplier = proposedVelocity; HitObject.Path.ExpectedDistance.Value = proposedDistance; editorBeatmap?.Update(HitObject); } @@ -374,9 +388,24 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders return true; } + if (isAdjustingLength && e.ShiftPressed != adjustVelocityMomentary) + { + adjustVelocityMomentary = e.ShiftPressed; + adjustLength(desiredDistance, adjustVelocityMomentary); + return true; + } + return false; } + protected override void OnKeyUp(KeyUpEvent e) + { + if (!IsSelected || !isAdjustingLength || e.ShiftPressed == adjustVelocityMomentary) return; + + adjustVelocityMomentary = e.ShiftPressed; + adjustLength(desiredDistance, adjustVelocityMomentary); + } + private PathControlPoint addControlPoint(Vector2 position) { position -= HitObject.Position; From 57fa502786c2c7a609b8b9428a3b4fd082fd546d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 22 Jul 2024 11:57:46 +0200 Subject: [PATCH 1576/2493] Fix editor UI dimming when hovering over expanded part of toolboxes Closes https://github.com/ppy/osu/issues/28969. --- osu.Game/Rulesets/Edit/HitObjectComposer.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs index 3c38a7258e..c2a7bec9f9 100644 --- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs @@ -305,7 +305,9 @@ namespace osu.Game.Rulesets.Edit PlayfieldContentContainer.X = TOOLBOX_CONTRACTED_SIZE_LEFT; } - composerFocusMode.Value = PlayfieldContentContainer.Contains(InputManager.CurrentState.Mouse.Position); + composerFocusMode.Value = PlayfieldContentContainer.Contains(InputManager.CurrentState.Mouse.Position) + && !LeftToolbox.Contains(InputManager.CurrentState.Mouse.Position) + && !RightToolbox.Contains(InputManager.CurrentState.Mouse.Position); } public override Playfield Playfield => drawableRulesetWrapper.Playfield; From c57232c2201f9e989cebd918d3fbf65745e06b01 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Mon, 22 Jul 2024 11:58:53 +0200 Subject: [PATCH 1577/2493] enforce minimum duration based on snap --- .../Blueprints/Sliders/SliderSelectionBlueprint.cs | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs index 339ca55cb2..785febab4b 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs @@ -228,7 +228,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders private Vector2 lengthAdjustMouseOffset; private double oldDuration; - private double oldVelocity; + private double oldVelocityMultiplier; private double desiredDistance; private bool isAdjustingLength; private bool adjustVelocityMomentary; @@ -239,7 +239,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders adjustVelocityMomentary = e.ShiftPressed; lengthAdjustMouseOffset = ToLocalSpace(e.ScreenSpaceMouseDownPosition) - HitObject.Position - HitObject.Path.PositionAt(1); oldDuration = HitObject.Path.Distance / HitObject.SliderVelocityMultiplier; - oldVelocity = HitObject.SliderVelocityMultiplier; + oldVelocityMultiplier = HitObject.SliderVelocityMultiplier; changeHandler?.BeginChange(); } @@ -255,13 +255,17 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders private void adjustLength(double proposedDistance, bool adjustVelocity) { desiredDistance = proposedDistance; - proposedDistance = MathHelper.Clamp(proposedDistance, 1, HitObject.Path.CalculatedDistance); - double proposedVelocity = oldVelocity; + double proposedVelocity = oldVelocityMultiplier; if (adjustVelocity) { - proposedDistance = MathHelper.Clamp(proposedDistance, 0.1 * oldDuration, 10 * oldDuration); proposedVelocity = proposedDistance / oldDuration; + proposedDistance = MathHelper.Clamp(proposedDistance, 0.1 * oldDuration, 10 * oldDuration); + } + else + { + double minDistance = distanceSnapProvider?.GetBeatSnapDistanceAt(HitObject, false) * oldVelocityMultiplier ?? 1; + proposedDistance = MathHelper.Clamp(proposedDistance, minDistance, HitObject.Path.CalculatedDistance); } if (Precision.AlmostEquals(proposedDistance, HitObject.Path.Distance) && Precision.AlmostEquals(proposedVelocity, HitObject.SliderVelocityMultiplier)) From 64381d4087994086ee9b0bdada5e06e23a0f3f19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 22 Jul 2024 12:18:53 +0200 Subject: [PATCH 1578/2493] Fix catch juice stream vertex add operation not undoing --- .../Edit/Blueprints/Components/SelectionEditablePath.cs | 9 ++++++++- .../Edit/Blueprints/JuiceStreamSelectionBlueprint.cs | 2 +- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/SelectionEditablePath.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/SelectionEditablePath.cs index c7a26ca15a..c4e906d5dc 100644 --- a/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/SelectionEditablePath.cs +++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/SelectionEditablePath.cs @@ -9,6 +9,7 @@ using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; using osu.Game.Graphics.UserInterface; +using osu.Game.Rulesets.Catch.Objects; using osu.Game.Screens.Edit; using osuTK; using osuTK.Input; @@ -19,22 +20,28 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components { public MenuItem[] ContextMenuItems => getContextMenuItems().ToArray(); + private readonly JuiceStream juiceStream; + // To handle when the editor is scrolled while dragging. private Vector2 dragStartPosition; [Resolved] private IEditorChangeHandler? changeHandler { get; set; } - public SelectionEditablePath(Func positionToTime) + public SelectionEditablePath(JuiceStream juiceStream, Func positionToTime) : base(positionToTime) { + this.juiceStream = juiceStream; } public void AddVertex(Vector2 relativePosition) { + changeHandler?.BeginChange(); double time = Math.Max(0, PositionToTime(relativePosition.Y)); int index = AddVertex(time, relativePosition.X); + UpdateHitObjectFromPath(juiceStream); selectOnly(index); + changeHandler?.EndChange(); } public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => InternalChildren.Any(d => d.ReceivePositionalInputAt(screenSpacePos)); diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/JuiceStreamSelectionBlueprint.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/JuiceStreamSelectionBlueprint.cs index 49d778ad08..a492920d3a 100644 --- a/osu.Game.Rulesets.Catch/Edit/Blueprints/JuiceStreamSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/JuiceStreamSelectionBlueprint.cs @@ -60,7 +60,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints { scrollingPath = new ScrollingPath(), nestedOutlineContainer = new NestedOutlineContainer(), - editablePath = new SelectionEditablePath(positionToTime) + editablePath = new SelectionEditablePath(hitObject, positionToTime) }; } From 47964f33d77aca5dd4305313aac0f7216648a000 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 22 Jul 2024 13:21:49 +0200 Subject: [PATCH 1579/2493] Fix catch juice stream vertex remove operation not undoing --- .../Blueprints/Components/SelectionEditablePath.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/SelectionEditablePath.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/SelectionEditablePath.cs index c4e906d5dc..904d7a2579 100644 --- a/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/SelectionEditablePath.cs +++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/SelectionEditablePath.cs @@ -54,7 +54,11 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components if (e.Button == MouseButton.Left && e.ShiftPressed) { + changeHandler?.BeginChange(); RemoveVertex(index); + UpdateHitObjectFromPath(juiceStream); + changeHandler?.EndChange(); + return true; } @@ -125,11 +129,17 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components private void deleteSelectedVertices() { + changeHandler?.BeginChange(); + for (int i = VertexCount - 1; i >= 0; i--) { if (VertexStates[i].IsSelected) RemoveVertex(i); } + + UpdateHitObjectFromPath(juiceStream); + + changeHandler?.EndChange(); } } } From 6b3c1f4e47abc22a60bddaa6dbc0c8a43d13bff5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 22 Jul 2024 13:26:16 +0200 Subject: [PATCH 1580/2493] Unify juice stream piece UX with osu! control point pieces - Use same hover state - Use shift-right click for quick delete rather than shift-left click --- .../Components/SelectionEditablePath.cs | 2 +- .../Edit/Blueprints/Components/VertexPiece.cs | 30 ++++++++++++++++++- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/SelectionEditablePath.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/SelectionEditablePath.cs index 904d7a2579..6a4e35b1f9 100644 --- a/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/SelectionEditablePath.cs +++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/SelectionEditablePath.cs @@ -52,7 +52,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components if (index == -1 || VertexStates[index].IsFixed) return false; - if (e.Button == MouseButton.Left && e.ShiftPressed) + if (e.Button == MouseButton.Right && e.ShiftPressed) { changeHandler?.BeginChange(); RemoveVertex(index); diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/VertexPiece.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/VertexPiece.cs index 07d7c72698..a3f8e85278 100644 --- a/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/VertexPiece.cs +++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/VertexPiece.cs @@ -5,6 +5,7 @@ using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Shapes; +using osu.Framework.Input.Events; using osu.Game.Graphics; using osuTK; @@ -12,6 +13,8 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components { public partial class VertexPiece : Circle { + private VertexState state = new VertexState(); + [Resolved] private OsuColour osuColour { get; set; } = null!; @@ -24,7 +27,32 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components public void UpdateFrom(VertexState state) { - Colour = state.IsSelected ? osuColour.Yellow.Lighten(1) : osuColour.Yellow; + this.state = state; + updateMarkerDisplay(); + } + + protected override bool OnHover(HoverEvent e) + { + updateMarkerDisplay(); + return false; + } + + protected override void OnHoverLost(HoverLostEvent e) + { + updateMarkerDisplay(); + } + + /// + /// Updates the state of the circular control point marker. + /// + private void updateMarkerDisplay() + { + var colour = osuColour.Yellow; + + if (IsHovered || state.IsSelected) + colour = colour.Lighten(1); + + Colour = colour; Alpha = state.IsFixed ? 0.5f : 1; } } From 1d91201c4303e1cb32470b86cb2ff1d8f306b0de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 22 Jul 2024 13:33:51 +0200 Subject: [PATCH 1581/2493] Fix tests --- .../Editor/TestSceneJuiceStreamSelectionBlueprint.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneJuiceStreamSelectionBlueprint.cs b/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneJuiceStreamSelectionBlueprint.cs index c96f32d87c..10cf294a36 100644 --- a/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneJuiceStreamSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneJuiceStreamSelectionBlueprint.cs @@ -158,14 +158,14 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor float[] positions = { 200, 200 }; addBlueprintStep(times, positions, 0.2); - addAddVertexSteps(500, 150); - addVertexCheckStep(3, 1, 500, 150); + addAddVertexSteps(500, 180); + addVertexCheckStep(3, 1, 500, 180); addAddVertexSteps(90, 200); addVertexCheckStep(4, 1, times[0], positions[0]); - addAddVertexSteps(750, 180); - addVertexCheckStep(5, 4, 750, 180); + addAddVertexSteps(750, 200); + addVertexCheckStep(5, 4, 750, 200); AddAssert("duration is changed", () => Precision.AlmostEquals(hitObject.Duration, 800 - times[0], 1e-3)); } @@ -265,7 +265,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor AddStep("delete vertex", () => { InputManager.PressKey(Key.ShiftLeft); - InputManager.Click(MouseButton.Left); + InputManager.Click(MouseButton.Right); InputManager.ReleaseKey(Key.ShiftLeft); }); } From f86ab1a64e8137bf89d6c082eefe4d3d72ed7466 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 22 Jul 2024 13:49:52 +0200 Subject: [PATCH 1582/2493] Fix filename --- ...eEditorTestGameplay.cs => TestSceneTaikoEditorTestGameplay.cs} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename osu.Game.Rulesets.Taiko.Tests/Editor/{TestSceneEditorTestGameplay.cs => TestSceneTaikoEditorTestGameplay.cs} (100%) diff --git a/osu.Game.Rulesets.Taiko.Tests/Editor/TestSceneEditorTestGameplay.cs b/osu.Game.Rulesets.Taiko.Tests/Editor/TestSceneTaikoEditorTestGameplay.cs similarity index 100% rename from osu.Game.Rulesets.Taiko.Tests/Editor/TestSceneEditorTestGameplay.cs rename to osu.Game.Rulesets.Taiko.Tests/Editor/TestSceneTaikoEditorTestGameplay.cs From 56af009e7759d364b6ded4d57d0f9c32c1f6dbd0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 22 Jul 2024 14:47:33 +0200 Subject: [PATCH 1583/2493] Fix `EditablePath.UpdateHitObjectFromPath()` not automatically updating object This is important because the editable path conversions heavily depend on the value of `JuiceStream.Velocity` being correct. The value is only guaranteed to be correct after an `ApplyDefaults()` call, which is triggered by updating the object via `EditorBeatmap`. --- .../Blueprints/Components/EditablePath.cs | 5 ++++ .../Components/SelectionEditablePath.cs | 23 ++++++++----------- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/EditablePath.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/EditablePath.cs index 86f92d16ca..857c00cd41 100644 --- a/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/EditablePath.cs +++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/EditablePath.cs @@ -13,6 +13,7 @@ using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.UI.Scrolling; +using osu.Game.Screens.Edit; using osuTK; namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components @@ -42,6 +43,9 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components [Resolved] private IBeatSnapProvider? beatSnapProvider { get; set; } + [Resolved] + protected EditorBeatmap? EditorBeatmap { get; private set; } + protected EditablePath(Func positionToTime) { PositionToTime = positionToTime; @@ -112,6 +116,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components double endTime = hitObject.StartTime + path.Duration; double snappedEndTime = beatSnapProvider.SnapTime(endTime, hitObject.StartTime); hitObject.Path.ExpectedDistance.Value = (snappedEndTime - hitObject.StartTime) * hitObject.Velocity; + EditorBeatmap?.Update(hitObject); } public Vector2 ToRelativePosition(Vector2 screenSpacePosition) diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/SelectionEditablePath.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/SelectionEditablePath.cs index 6a4e35b1f9..b2ee43ba16 100644 --- a/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/SelectionEditablePath.cs +++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/SelectionEditablePath.cs @@ -4,13 +4,11 @@ using System; using System.Collections.Generic; using System.Linq; -using osu.Framework.Allocation; using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets.Catch.Objects; -using osu.Game.Screens.Edit; using osuTK; using osuTK.Input; @@ -25,9 +23,6 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components // To handle when the editor is scrolled while dragging. private Vector2 dragStartPosition; - [Resolved] - private IEditorChangeHandler? changeHandler { get; set; } - public SelectionEditablePath(JuiceStream juiceStream, Func positionToTime) : base(positionToTime) { @@ -36,12 +31,14 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components public void AddVertex(Vector2 relativePosition) { - changeHandler?.BeginChange(); + EditorBeatmap?.BeginChange(); + double time = Math.Max(0, PositionToTime(relativePosition.Y)); int index = AddVertex(time, relativePosition.X); UpdateHitObjectFromPath(juiceStream); selectOnly(index); - changeHandler?.EndChange(); + + EditorBeatmap?.EndChange(); } public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => InternalChildren.Any(d => d.ReceivePositionalInputAt(screenSpacePos)); @@ -54,10 +51,10 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components if (e.Button == MouseButton.Right && e.ShiftPressed) { - changeHandler?.BeginChange(); + EditorBeatmap?.BeginChange(); RemoveVertex(index); UpdateHitObjectFromPath(juiceStream); - changeHandler?.EndChange(); + EditorBeatmap?.EndChange(); return true; } @@ -85,7 +82,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components for (int i = 0; i < VertexCount; i++) VertexStates[i].VertexBeforeChange = Vertices[i]; - changeHandler?.BeginChange(); + EditorBeatmap?.BeginChange(); return true; } @@ -99,7 +96,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components protected override void OnDragEnd(DragEndEvent e) { - changeHandler?.EndChange(); + EditorBeatmap?.EndChange(); } private int getMouseTargetVertex(Vector2 screenSpacePosition) @@ -129,7 +126,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components private void deleteSelectedVertices() { - changeHandler?.BeginChange(); + EditorBeatmap?.BeginChange(); for (int i = VertexCount - 1; i >= 0; i--) { @@ -139,7 +136,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components UpdateHitObjectFromPath(juiceStream); - changeHandler?.EndChange(); + EditorBeatmap?.EndChange(); } } } From f3617eadad1f997d5cd4eea45eb28c22467c25f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 22 Jul 2024 14:51:45 +0200 Subject: [PATCH 1584/2493] Fix editing juice stream path sometimes changing its duration I'm not *super* sure why this works, but it appears to, and my educated guess as to why is that it counteracts the effects of a change in the SV of the juice stream by artificially increasing or decreasing the velocity when running the appropriate path conversions and expected distance calculations. The actual SV change takes effect on the next default application, which is triggered by the `Update()` call at the end of the method. --- .../Edit/Blueprints/Components/EditablePath.cs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/EditablePath.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/EditablePath.cs index 857c00cd41..e626392234 100644 --- a/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/EditablePath.cs +++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/EditablePath.cs @@ -107,15 +107,22 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components // // The value is clamped here by the bindable min and max values. // In case the required velocity is too large, the path is not preserved. + double previousVelocity = svBindable.Value; svBindable.Value = Math.Ceiling(requiredVelocity / svToVelocityFactor); - path.ConvertToSliderPath(hitObject.Path, hitObject.LegacyConvertedY, hitObject.Velocity); + // adjust velocity locally, so that once the SV change is applied by applying defaults + // (triggered by `EditorBeatmap.Update()` call at end of method), + // it results in the outcome desired by the user. + double relativeChange = svBindable.Value / previousVelocity; + double localVelocity = hitObject.Velocity * relativeChange; + path.ConvertToSliderPath(hitObject.Path, hitObject.LegacyConvertedY, localVelocity); if (beatSnapProvider == null) return; double endTime = hitObject.StartTime + path.Duration; double snappedEndTime = beatSnapProvider.SnapTime(endTime, hitObject.StartTime); - hitObject.Path.ExpectedDistance.Value = (snappedEndTime - hitObject.StartTime) * hitObject.Velocity; + hitObject.Path.ExpectedDistance.Value = (snappedEndTime - hitObject.StartTime) * localVelocity; + EditorBeatmap?.Update(hitObject); } From 6100f5269d757177d557af714a17a8c116530dbd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 22 Jul 2024 14:57:36 +0200 Subject: [PATCH 1585/2493] Fix tests --- .../TestSceneJuiceStreamSelectionBlueprint.cs | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneJuiceStreamSelectionBlueprint.cs b/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneJuiceStreamSelectionBlueprint.cs index 10cf294a36..7b665b1ff9 100644 --- a/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneJuiceStreamSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneJuiceStreamSelectionBlueprint.cs @@ -82,6 +82,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor AddMouseMoveStep(-100, 100); addVertexCheckStep(3, 1, times[0], positions[0]); + addDragEndStep(); } [Test] @@ -100,6 +101,9 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor AddMouseMoveStep(times[2] - 50, positions[2] - 50); addVertexCheckStep(4, 1, times[1] - 50, positions[1] - 50); addVertexCheckStep(4, 2, times[2] - 50, positions[2] - 50); + + AddStep("release control", () => InputManager.ReleaseKey(Key.ControlLeft)); + addDragEndStep(); } [Test] @@ -113,6 +117,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor addDragStartStep(times[1], positions[1]); AddMouseMoveStep(times[1], 400); AddAssert("slider velocity changed", () => !hitObject.SliderVelocityMultiplierBindable.IsDefault); + addDragEndStep(); } [Test] @@ -129,6 +134,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor AddStep("scroll playfield", () => manualClock.CurrentTime += 200); AddMouseMoveStep(times[1] + 200, positions[1] + 100); addVertexCheckStep(2, 1, times[1] + 200, positions[1] + 100); + addDragEndStep(); } [Test] @@ -158,21 +164,21 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor float[] positions = { 200, 200 }; addBlueprintStep(times, positions, 0.2); - addAddVertexSteps(500, 180); - addVertexCheckStep(3, 1, 500, 180); + addAddVertexSteps(500, 150); + addVertexCheckStep(3, 1, 500, 150); - addAddVertexSteps(90, 200); - addVertexCheckStep(4, 1, times[0], positions[0]); + addAddVertexSteps(160, 200); + addVertexCheckStep(4, 1, 160, 200); - addAddVertexSteps(750, 200); - addVertexCheckStep(5, 4, 750, 200); + addAddVertexSteps(750, 180); + addVertexCheckStep(5, 4, 800, 160); AddAssert("duration is changed", () => Precision.AlmostEquals(hitObject.Duration, 800 - times[0], 1e-3)); } [Test] public void TestDeleteVertex() { - double[] times = { 100, 300, 500 }; + double[] times = { 100, 300, 400 }; float[] positions = { 100, 200, 150 }; addBlueprintStep(times, positions); From 38fc6f70f63a1dbff8b1b1848ea61c9c6b9bd0a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 23 Jul 2024 11:05:53 +0200 Subject: [PATCH 1586/2493] Add tolerance when drag-scrolling editor timeline Closes https://github.com/ppy/osu/issues/28983. While the direct cause of this is most likely mouse confine in full-screen, it shouldn't/can't really be disabled just for this, and I also get this on linux in *windowed* mode. In checking other apps, adding some tolerance to this sort of drag-scroll behaviour seems like a sane UX improvement anyways. --- .../Timeline/TimelineBlueprintContainer.cs | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs index 9a8fdc3dac..62c15996e0 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs @@ -198,11 +198,20 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline var timelineQuad = timeline.ScreenSpaceDrawQuad; float mouseX = InputManager.CurrentState.Mouse.Position.X; + // for better UX do not require the user to drag all the way to the edge and beyond to initiate a drag-scroll. + // this is especially important in scenarios like fullscreen, where mouse confine will usually be on + // and the user physically *won't be able to* drag beyond the edge of the timeline + // (since its left edge is co-incident with the window edge). + const float scroll_tolerance = 20; + + float leftBound = timelineQuad.TopLeft.X + scroll_tolerance; + float rightBound = timelineQuad.TopRight.X - scroll_tolerance; + // scroll if in a drag and dragging outside visible extents - if (mouseX > timelineQuad.TopRight.X) - timeline.ScrollBy((float)((mouseX - timelineQuad.TopRight.X) / 10 * Clock.ElapsedFrameTime)); - else if (mouseX < timelineQuad.TopLeft.X) - timeline.ScrollBy((float)((mouseX - timelineQuad.TopLeft.X) / 10 * Clock.ElapsedFrameTime)); + if (mouseX > rightBound) + timeline.ScrollBy((float)((mouseX - rightBound) / 10 * Clock.ElapsedFrameTime)); + else if (mouseX < leftBound) + timeline.ScrollBy((float)((mouseX - leftBound) / 10 * Clock.ElapsedFrameTime)); } private partial class SelectableAreaBackground : CompositeDrawable From cc4ed0ff3f9892052128adfe5b46ded90e918f95 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 23 Jul 2024 18:59:22 +0900 Subject: [PATCH 1587/2493] Use non-screen-space coordinates and add time-based drag ramping for better control --- .../Timeline/TimelineBlueprintContainer.cs | 42 ++++++++++++++----- 1 file changed, 32 insertions(+), 10 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs index 62c15996e0..ca23e3e88f 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs @@ -3,6 +3,7 @@ #nullable disable +using System; using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; @@ -100,10 +101,14 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline return base.OnDragStart(e); } + private float dragTimeAccumulated; + protected override void Update() { if (IsDragged || hitObjectDragged) handleScrollViaDrag(); + else + dragTimeAccumulated = 0; if (Composer != null && timeline != null) { @@ -193,25 +198,42 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline private void handleScrollViaDrag() { + // The amount of time dragging before we reach maximum drag speed. + const float time_ramp_multiplier = 5000; + + // A maximum drag speed to ensure things don't get out of hand. + const float max_velocity = 10; + if (timeline == null) return; - var timelineQuad = timeline.ScreenSpaceDrawQuad; - float mouseX = InputManager.CurrentState.Mouse.Position.X; + var mousePos = timeline.ToLocalSpace(InputManager.CurrentState.Mouse.Position); // for better UX do not require the user to drag all the way to the edge and beyond to initiate a drag-scroll. // this is especially important in scenarios like fullscreen, where mouse confine will usually be on // and the user physically *won't be able to* drag beyond the edge of the timeline // (since its left edge is co-incident with the window edge). - const float scroll_tolerance = 20; + const float scroll_tolerance = 40; - float leftBound = timelineQuad.TopLeft.X + scroll_tolerance; - float rightBound = timelineQuad.TopRight.X - scroll_tolerance; + float leftBound = timeline.BoundingBox.TopLeft.X + scroll_tolerance; + float rightBound = timeline.BoundingBox.TopRight.X - scroll_tolerance; - // scroll if in a drag and dragging outside visible extents - if (mouseX > rightBound) - timeline.ScrollBy((float)((mouseX - rightBound) / 10 * Clock.ElapsedFrameTime)); - else if (mouseX < leftBound) - timeline.ScrollBy((float)((mouseX - leftBound) / 10 * Clock.ElapsedFrameTime)); + float amount = 0; + + if (mousePos.X > rightBound) + amount = mousePos.X - rightBound; + else if (mousePos.X < leftBound) + amount = mousePos.X - leftBound; + + if (amount == 0) + { + dragTimeAccumulated = 0; + return; + } + + amount = Math.Sign(amount) * Math.Min(max_velocity, Math.Abs(MathF.Pow(amount, 2) / (MathF.Pow(scroll_tolerance, 2)))); + dragTimeAccumulated += (float)Clock.ElapsedFrameTime; + + timeline.ScrollBy(amount * (float)Clock.ElapsedFrameTime * Math.Min(1, dragTimeAccumulated / time_ramp_multiplier)); } private partial class SelectableAreaBackground : CompositeDrawable From ad1a86ebdcfc8f05380331c2aae3c0f46b084496 Mon Sep 17 00:00:00 2001 From: normalid Date: Tue, 23 Jul 2024 19:05:14 +0800 Subject: [PATCH 1588/2493] Implement the overlay --- osu.Game/Skinning/LegacyKeyCounter.cs | 101 +++++++++++++++++++ osu.Game/Skinning/LegacyKeyCounterDisplay.cs | 79 +++++++++++++++ osu.Game/Skinning/LegacySkin.cs | 20 ++-- 3 files changed, 191 insertions(+), 9 deletions(-) create mode 100644 osu.Game/Skinning/LegacyKeyCounter.cs create mode 100644 osu.Game/Skinning/LegacyKeyCounterDisplay.cs diff --git a/osu.Game/Skinning/LegacyKeyCounter.cs b/osu.Game/Skinning/LegacyKeyCounter.cs new file mode 100644 index 0000000000..73534a8e51 --- /dev/null +++ b/osu.Game/Skinning/LegacyKeyCounter.cs @@ -0,0 +1,101 @@ +// 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.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Screens.Play.HUD; + +namespace osu.Game.Skinning +{ + public partial class LegacyKeyCounter : KeyCounter + { + public bool UsesFixedAnchor { get; set; } + + public float TransitionDuration { get; set; } = 150f; + + public Colour4 KeyTextColour { get; set; } = Colour4.White; + + public Colour4 KeyDownBackgroundColour { get; set; } = Colour4.Yellow; + + public Colour4 KeyUpBackgroundColour { get; set; } = Colour4.White; + + private Container keyContainer = null!; + + private SkinnableSprite overlayKey = null!; + + private OsuSpriteText overlayKeyText = null!; + + public LegacyKeyCounter(InputTrigger trigger) + : base(trigger) + { + Origin = Anchor.Centre; + Anchor = Anchor.Centre; + Child = keyContainer = new Container + { + RelativeSizeAxes = Axes.Both, + Origin = Anchor.Centre, + Anchor = Anchor.Centre, + Children = new Drawable[] + { + overlayKey = new SkinnableSprite + { + Blending = Multiplicative, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + BypassAutoSizeAxes = Axes.Both, + SpriteName = { Value = "inputoverlay-key" }, + Rotation = -90, + }, + overlayKeyText = new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Text = trigger.Name, + Colour = KeyTextColour, + Font = OsuFont.Default.With(fixedWidth: true), + }, + } + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + Width = Math.Max(overlayKey.Width, 43 * 1.05f); + Height = Math.Max(overlayKey.Height, 43 * 1.05f); + } + + protected override void Activate(bool forwardPlayback = true) + { + base.Activate(forwardPlayback); + keyContainer.ScaleTo(0.75f, TransitionDuration); + keyContainer.FadeColour(KeyDownBackgroundColour, TransitionDuration); + overlayKeyText.Text = CountPresses.Value.ToString(); + } + + protected override void Deactivate(bool forwardPlayback = true) + { + base.Deactivate(forwardPlayback); + keyContainer.ScaleTo(1f, TransitionDuration); + keyContainer.FadeColour(KeyUpBackgroundColour, TransitionDuration); + } + + public static BlendingParameters Multiplicative + { + get + { + BlendingParameters result = default(BlendingParameters); + result.Source = BlendingType.SrcAlpha; + result.Destination = BlendingType.OneMinusSrcAlpha; + result.SourceAlpha = BlendingType.One; + result.DestinationAlpha = BlendingType.One; + result.RGBEquation = BlendingEquation.Add; + result.AlphaEquation = BlendingEquation.Add; + return result; + } + } + } +} diff --git a/osu.Game/Skinning/LegacyKeyCounterDisplay.cs b/osu.Game/Skinning/LegacyKeyCounterDisplay.cs new file mode 100644 index 0000000000..ace582af5c --- /dev/null +++ b/osu.Game/Skinning/LegacyKeyCounterDisplay.cs @@ -0,0 +1,79 @@ +// 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.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Screens.Play.HUD; +using osuTK.Graphics; +using osu.Framework.Allocation; + +namespace osu.Game.Skinning +{ + public partial class LegacyKeyCounterDisplay : KeyCounterDisplay + { + private const float key_transition_time = 50; + + protected override FillFlowContainer KeyFlow { get; } = null!; + + private SkinnableSprite overlayBackground = null!; + + public LegacyKeyCounterDisplay() + { + AutoSizeAxes = Axes.Both; + + AddRangeInternal(new Drawable[] + { + overlayBackground = new SkinnableSprite + { + Anchor = Anchor.TopLeft, + Origin = Anchor.TopLeft, + //BypassAutoSizeAxes = Axes.Both, + SpriteName = { Value= "inputoverlay-background" }, + }, + KeyFlow = new FillFlowContainer + { + Padding = new MarginPadding + { + Horizontal = 7f * 1.05f, + }, + Anchor = Anchor.TopLeft, + Origin = Anchor.TopLeft, + Direction = FillDirection.Horizontal, + AutoSizeAxes = Axes.Both, + }, + }); + } + + [BackgroundDependencyLoader] + private void load(ISkinSource source) + { + source.GetConfig("InputOverlayText")?.BindValueChanged(v => + { + KeyTextColor = v.NewValue; + }, true); + } + + protected override KeyCounter CreateCounter(InputTrigger trigger) => new LegacyKeyCounter(trigger) + { + TransitionDuration = key_transition_time, + KeyTextColour = keyTextColor, + }; + + private Color4 keyTextColor = Color4.White; + + public Color4 KeyTextColor + { + get => keyTextColor; + set + { + if (value != keyTextColor) + { + keyTextColor = value; + foreach (var child in KeyFlow.Cast()) + child.KeyTextColour = value; + } + } + } + } +} diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index 816cfc0a2d..a890c5ffab 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -15,6 +15,7 @@ using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Textures; using osu.Framework.IO.Stores; +using osu.Framework.Logging; using osu.Game.Audio; using osu.Game.Beatmaps.Formats; using osu.Game.Extensions; @@ -381,22 +382,22 @@ namespace osu.Game.Skinning } var hitError = container.OfType().FirstOrDefault(); - var keyCounter = container.OfType().FirstOrDefault(); if (hitError != null) { hitError.Anchor = Anchor.BottomCentre; hitError.Origin = Anchor.CentreLeft; hitError.Rotation = -90; + } - if (keyCounter != null) - { - const float padding = 10; + var keyCounter = container.OfType().FirstOrDefault(); - keyCounter.Anchor = Anchor.BottomRight; - keyCounter.Origin = Anchor.BottomRight; - keyCounter.Position = new Vector2(-padding, -(padding + hitError.Width)); - } + if (keyCounter != null) + { + keyCounter.Rotation = 90f; + keyCounter.Anchor = Anchor.CentreRight; + keyCounter.Origin = Anchor.TopCentre; + keyCounter.Position = new Vector2(0); } }) { @@ -408,7 +409,8 @@ namespace osu.Game.Skinning new LegacySongProgress(), new LegacyHealthDisplay(), new BarHitErrorMeter(), - new DefaultKeyCounterDisplay() + //new DefaultKeyCounterDisplay(), + new LegacyKeyCounterDisplay(), } }; } From 25d63ac6a59ae4c4ada282eaf7c5d0b20376331e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 23 Jul 2024 13:25:38 +0200 Subject: [PATCH 1589/2493] Move editor beatmap processor test cases off of `OsuHitObject`s Most of them are about to become obsolete once consideration for `TimePreempt` is re-added. --- .../TestSceneEditorBeatmapProcessor.cs | 55 +++++++++---------- 1 file changed, 27 insertions(+), 28 deletions(-) diff --git a/osu.Game.Tests/Editing/TestSceneEditorBeatmapProcessor.cs b/osu.Game.Tests/Editing/TestSceneEditorBeatmapProcessor.cs index 251099c0e2..c4a61177a9 100644 --- a/osu.Game.Tests/Editing/TestSceneEditorBeatmapProcessor.cs +++ b/osu.Game.Tests/Editing/TestSceneEditorBeatmapProcessor.cs @@ -8,7 +8,6 @@ using osu.Game.Beatmaps.Timing; using osu.Game.Rulesets.Mania; using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Osu; -using osu.Game.Rulesets.Osu.Objects; using osu.Game.Screens.Edit; namespace osu.Game.Tests.Editing @@ -45,7 +44,7 @@ namespace osu.Game.Tests.Editing BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo }, HitObjects = { - new HitCircle { StartTime = 1000 }, + new Note { StartTime = 1000 }, } }); @@ -67,8 +66,8 @@ namespace osu.Game.Tests.Editing BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo }, HitObjects = { - new HitCircle { StartTime = 1000 }, - new HitCircle { StartTime = 2000 }, + new Note { StartTime = 1000 }, + new Note { StartTime = 2000 }, } }); @@ -136,8 +135,8 @@ namespace osu.Game.Tests.Editing BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo }, HitObjects = { - new HitCircle { StartTime = 1000 }, - new HitCircle { StartTime = 5000 }, + new Note { StartTime = 1000 }, + new Note { StartTime = 5000 }, } }); @@ -164,8 +163,8 @@ namespace osu.Game.Tests.Editing BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo }, HitObjects = { - new HitCircle { StartTime = 1000 }, - new HitCircle { StartTime = 9000 }, + new Note { StartTime = 1000 }, + new Note { StartTime = 9000 }, }, Breaks = { @@ -197,9 +196,9 @@ namespace osu.Game.Tests.Editing BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo }, HitObjects = { - new HitCircle { StartTime = 1000 }, - new HitCircle { StartTime = 5000 }, - new HitCircle { StartTime = 9000 }, + new Note { StartTime = 1000 }, + new Note { StartTime = 5000 }, + new Note { StartTime = 9000 }, }, Breaks = { @@ -232,8 +231,8 @@ namespace osu.Game.Tests.Editing BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo }, HitObjects = { - new HitCircle { StartTime = 1100 }, - new HitCircle { StartTime = 9000 }, + new Note { StartTime = 1100 }, + new Note { StartTime = 9000 }, }, Breaks = { @@ -264,8 +263,8 @@ namespace osu.Game.Tests.Editing BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo }, HitObjects = { - new HitCircle { StartTime = 1000 }, - new HitCircle { StartTime = 9000 }, + new Note { StartTime = 1000 }, + new Note { StartTime = 9000 }, }, Breaks = { @@ -299,9 +298,9 @@ namespace osu.Game.Tests.Editing BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo }, HitObjects = { - new HitCircle { StartTime = 1000 }, - new HitCircle { StartTime = 5000 }, - new HitCircle { StartTime = 9000 }, + new Note { StartTime = 1000 }, + new Note { StartTime = 5000 }, + new Note { StartTime = 9000 }, }, Breaks = { @@ -334,8 +333,8 @@ namespace osu.Game.Tests.Editing BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo }, HitObjects = { - new HitCircle { StartTime = 1000 }, - new HitCircle { StartTime = 9000 }, + new Note { StartTime = 1000 }, + new Note { StartTime = 9000 }, }, Breaks = { @@ -366,8 +365,8 @@ namespace osu.Game.Tests.Editing BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo }, HitObjects = { - new HitCircle { StartTime = 1000 }, - new HitCircle { StartTime = 2000 }, + new Note { StartTime = 1000 }, + new Note { StartTime = 2000 }, }, Breaks = { @@ -393,8 +392,8 @@ namespace osu.Game.Tests.Editing BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo }, HitObjects = { - new HitCircle { StartTime = 1000 }, - new HitCircle { StartTime = 2000 }, + new Note { StartTime = 1000 }, + new Note { StartTime = 2000 }, }, Breaks = { @@ -447,8 +446,8 @@ namespace osu.Game.Tests.Editing BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo }, HitObjects = { - new HitCircle { StartTime = 10000 }, - new HitCircle { StartTime = 11000 }, + new Note { StartTime = 10000 }, + new Note { StartTime = 11000 }, }, Breaks = { @@ -474,8 +473,8 @@ namespace osu.Game.Tests.Editing BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo }, HitObjects = { - new HitCircle { StartTime = 10000 }, - new HitCircle { StartTime = 11000 }, + new Note { StartTime = 10000 }, + new Note { StartTime = 11000 }, }, Breaks = { From 088e8ad0a27415fd0b93e79e0126a18051191d0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 23 Jul 2024 13:30:10 +0200 Subject: [PATCH 1590/2493] Respect pre-empt time when auto-generating breaks Closes https://github.com/ppy/osu/issues/28703. --- .../Objects/CatchHitObject.cs | 2 +- osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs | 4 ++-- .../Rulesets/Objects/Types/IHasTimePreempt.cs | 13 +++++++++++++ .../Screens/Edit/EditorBeatmapProcessor.cs | 18 +++++++++++++----- 4 files changed, 29 insertions(+), 8 deletions(-) create mode 100644 osu.Game/Rulesets/Objects/Types/IHasTimePreempt.cs diff --git a/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs index 52c42dfddb..329055b3dd 100644 --- a/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs +++ b/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs @@ -15,7 +15,7 @@ using osuTK; namespace osu.Game.Rulesets.Catch.Objects { - public abstract class CatchHitObject : HitObject, IHasPosition, IHasComboInformation + public abstract class CatchHitObject : HitObject, IHasPosition, IHasComboInformation, IHasTimePreempt { public const float OBJECT_RADIUS = 64; diff --git a/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs b/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs index 6c77d9189c..1b0993b698 100644 --- a/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs +++ b/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs @@ -14,7 +14,7 @@ using osuTK; namespace osu.Game.Rulesets.Osu.Objects { - public abstract class OsuHitObject : HitObject, IHasComboInformation, IHasPosition + public abstract class OsuHitObject : HitObject, IHasComboInformation, IHasPosition, IHasTimePreempt { /// /// The radius of hit objects (ie. the radius of a ). @@ -46,7 +46,7 @@ namespace osu.Game.Rulesets.Osu.Objects /// public const double PREEMPT_MAX = 1800; - public double TimePreempt = 600; + public double TimePreempt { get; set; } = 600; public double TimeFadeIn = 400; private HitObjectProperty position; diff --git a/osu.Game/Rulesets/Objects/Types/IHasTimePreempt.cs b/osu.Game/Rulesets/Objects/Types/IHasTimePreempt.cs new file mode 100644 index 0000000000..e7239515f6 --- /dev/null +++ b/osu.Game/Rulesets/Objects/Types/IHasTimePreempt.cs @@ -0,0 +1,13 @@ +// 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.Rulesets.Objects.Types +{ + /// + /// A that appears on screen at a fixed time interval before its . + /// + public interface IHasTimePreempt + { + double TimePreempt { get; } + } +} diff --git a/osu.Game/Screens/Edit/EditorBeatmapProcessor.cs b/osu.Game/Screens/Edit/EditorBeatmapProcessor.cs index 9b6d956a4c..5c435e771d 100644 --- a/osu.Game/Screens/Edit/EditorBeatmapProcessor.cs +++ b/osu.Game/Screens/Edit/EditorBeatmapProcessor.cs @@ -8,6 +8,7 @@ using osu.Game.Beatmaps; using osu.Game.Beatmaps.Timing; using osu.Game.Rulesets; using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; namespace osu.Game.Screens.Edit { @@ -67,19 +68,26 @@ namespace osu.Game.Screens.Edit for (int i = 1; i < Beatmap.HitObjects.Count; ++i) { + var previousObject = Beatmap.HitObjects[i - 1]; + var nextObject = Beatmap.HitObjects[i]; + // Keep track of the maximum end time encountered thus far. // This handles cases like osu!mania's hold notes, which could have concurrent other objects after their start time. // Note that we're relying on the implicit assumption that objects are sorted by start time, // which is why similar tracking is not done for start time. - currentMaxEndTime = Math.Max(currentMaxEndTime, Beatmap.HitObjects[i - 1].GetEndTime()); + currentMaxEndTime = Math.Max(currentMaxEndTime, previousObject.GetEndTime()); - double nextObjectStartTime = Beatmap.HitObjects[i].StartTime; - - if (nextObjectStartTime - currentMaxEndTime < BreakPeriod.MIN_GAP_DURATION) + if (nextObject.StartTime - currentMaxEndTime < BreakPeriod.MIN_GAP_DURATION) continue; double breakStartTime = currentMaxEndTime + BreakPeriod.GAP_BEFORE_BREAK; - double breakEndTime = nextObjectStartTime - Math.Max(BreakPeriod.GAP_AFTER_BREAK, Beatmap.ControlPointInfo.TimingPointAt(nextObjectStartTime).BeatLength * 2); + + double breakEndTime = nextObject.StartTime; + + if (nextObject is IHasTimePreempt hasTimePreempt) + breakEndTime -= hasTimePreempt.TimePreempt; + else + breakEndTime -= Math.Max(BreakPeriod.GAP_AFTER_BREAK, Beatmap.ControlPointInfo.TimingPointAt(nextObject.StartTime).BeatLength * 2); if (breakEndTime - breakStartTime < BreakPeriod.MIN_BREAK_DURATION) continue; From c2fa30bf81b46316c6043944c73d4519db4a73cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 23 Jul 2024 13:38:25 +0200 Subject: [PATCH 1591/2493] Add test coverage for break generation respecting pre-empt time --- .../TestSceneEditorBeatmapProcessor.cs | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/osu.Game.Tests/Editing/TestSceneEditorBeatmapProcessor.cs b/osu.Game.Tests/Editing/TestSceneEditorBeatmapProcessor.cs index c4a61177a9..bbcf6aac2c 100644 --- a/osu.Game.Tests/Editing/TestSceneEditorBeatmapProcessor.cs +++ b/osu.Game.Tests/Editing/TestSceneEditorBeatmapProcessor.cs @@ -8,6 +8,7 @@ using osu.Game.Beatmaps.Timing; using osu.Game.Rulesets.Mania; using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.Osu.Objects; using osu.Game.Screens.Edit; namespace osu.Game.Tests.Editing @@ -488,5 +489,55 @@ namespace osu.Game.Tests.Editing Assert.That(beatmap.Breaks, Is.Empty); } + + [Test] + public void TestTimePreemptIsRespected() + { + var controlPoints = new ControlPointInfo(); + controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 }); + var beatmap = new EditorBeatmap(new Beatmap + { + ControlPointInfo = controlPoints, + BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo }, + Difficulty = + { + ApproachRate = 10, + }, + HitObjects = + { + new HitCircle { StartTime = 1000 }, + new HitCircle { StartTime = 5000 }, + } + }); + + foreach (var ho in beatmap.HitObjects) + ho.ApplyDefaults(beatmap.ControlPointInfo, beatmap.Difficulty); + + var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new OsuRuleset()); + beatmapProcessor.PreProcess(); + beatmapProcessor.PostProcess(); + + Assert.Multiple(() => + { + Assert.That(beatmap.Breaks, Has.Count.EqualTo(1)); + Assert.That(beatmap.Breaks[0].StartTime, Is.EqualTo(1200)); + Assert.That(beatmap.Breaks[0].EndTime, Is.EqualTo(5000 - OsuHitObject.PREEMPT_MIN)); + }); + + beatmap.Difficulty.ApproachRate = 0; + + foreach (var ho in beatmap.HitObjects) + ho.ApplyDefaults(beatmap.ControlPointInfo, beatmap.Difficulty); + + beatmapProcessor.PreProcess(); + beatmapProcessor.PostProcess(); + + Assert.Multiple(() => + { + Assert.That(beatmap.Breaks, Has.Count.EqualTo(1)); + Assert.That(beatmap.Breaks[0].StartTime, Is.EqualTo(1200)); + Assert.That(beatmap.Breaks[0].EndTime, Is.EqualTo(5000 - OsuHitObject.PREEMPT_MAX)); + }); + } } } From c3062f96eee5d3a7a92a5f105bf4c62d024d3572 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 23 Jul 2024 13:38:50 +0200 Subject: [PATCH 1592/2493] Fix autogenerated breaks not invalidating on change to pre-empt time --- osu.Game/Screens/Edit/EditorBeatmapProcessor.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/EditorBeatmapProcessor.cs b/osu.Game/Screens/Edit/EditorBeatmapProcessor.cs index 5c435e771d..4fe431498f 100644 --- a/osu.Game/Screens/Edit/EditorBeatmapProcessor.cs +++ b/osu.Game/Screens/Edit/EditorBeatmapProcessor.cs @@ -45,7 +45,7 @@ namespace osu.Game.Screens.Edit private void autoGenerateBreaks() { - var objectDuration = Beatmap.HitObjects.Select(ho => (ho.StartTime, ho.GetEndTime())).ToHashSet(); + var objectDuration = Beatmap.HitObjects.Select(ho => (ho.StartTime - ((ho as IHasTimePreempt)?.TimePreempt ?? 0), ho.GetEndTime())).ToHashSet(); if (objectDuration.SetEquals(objectDurationCache)) return; From 777a0deb0fcc814b0b4f59a1f2a67011462d0556 Mon Sep 17 00:00:00 2001 From: normalid Date: Tue, 23 Jul 2024 19:45:53 +0800 Subject: [PATCH 1593/2493] Update the offset formula --- osu.Game/Skinning/LegacyKeyCounter.cs | 4 ++-- osu.Game/Skinning/LegacyKeyCounterDisplay.cs | 9 ++++----- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/osu.Game/Skinning/LegacyKeyCounter.cs b/osu.Game/Skinning/LegacyKeyCounter.cs index 73534a8e51..c3125c3eda 100644 --- a/osu.Game/Skinning/LegacyKeyCounter.cs +++ b/osu.Game/Skinning/LegacyKeyCounter.cs @@ -64,8 +64,8 @@ namespace osu.Game.Skinning protected override void LoadComplete() { base.LoadComplete(); - Width = Math.Max(overlayKey.Width, 43 * 1.05f); - Height = Math.Max(overlayKey.Height, 43 * 1.05f); + Width = Math.Max(overlayKey.Width, 48 * 0.95f); + Height = Math.Max(overlayKey.Height, 48 * 0.95f); } protected override void Activate(bool forwardPlayback = true) diff --git a/osu.Game/Skinning/LegacyKeyCounterDisplay.cs b/osu.Game/Skinning/LegacyKeyCounterDisplay.cs index ace582af5c..e395aefe8a 100644 --- a/osu.Game/Skinning/LegacyKeyCounterDisplay.cs +++ b/osu.Game/Skinning/LegacyKeyCounterDisplay.cs @@ -28,15 +28,14 @@ namespace osu.Game.Skinning { Anchor = Anchor.TopLeft, Origin = Anchor.TopLeft, - //BypassAutoSizeAxes = Axes.Both, SpriteName = { Value= "inputoverlay-background" }, }, KeyFlow = new FillFlowContainer { - Padding = new MarginPadding - { - Horizontal = 7f * 1.05f, - }, + // https://osu.ppy.sh/wiki/en/Skinning/Interface#input-overlay + // 24px away from the container, there're 4 counter in legacy, so divide by 4 + // "inputoverlay-background.png" are 1.05x in-game. so *1.05f to the X coordinate + X = (24 / 4) * 1.05f, Anchor = Anchor.TopLeft, Origin = Anchor.TopLeft, Direction = FillDirection.Horizontal, From aed7ba9508b636a00a7e38c7e6519fe2bbb87af3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 23 Jul 2024 20:56:21 +0900 Subject: [PATCH 1594/2493] Change order of application to avoid bias to side with more room to drag MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bartłomiej Dach --- .../Compose/Components/Timeline/TimelineBlueprintContainer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs index ca23e3e88f..740f0b6aac 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs @@ -230,7 +230,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline return; } - amount = Math.Sign(amount) * Math.Min(max_velocity, Math.Abs(MathF.Pow(amount, 2) / (MathF.Pow(scroll_tolerance, 2)))); + amount = Math.Sign(amount) * Math.Min(max_velocity, MathF.Pow(Math.Clamp(Math.Abs(amount), 0, scroll_tolerance), 2)); dragTimeAccumulated += (float)Clock.ElapsedFrameTime; timeline.ScrollBy(amount * (float)Clock.ElapsedFrameTime * Math.Min(1, dragTimeAccumulated / time_ramp_multiplier)); From 5dcc8b7a8f403dcfa6e2f1e6cfba409224cf844c Mon Sep 17 00:00:00 2001 From: normalid Date: Tue, 23 Jul 2024 19:56:43 +0800 Subject: [PATCH 1595/2493] Make the text are always horizontal --- osu.Game/Skinning/LegacyKeyCounter.cs | 14 +++++++++++++- osu.Game/Skinning/LegacyKeyCounterDisplay.cs | 9 +++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/osu.Game/Skinning/LegacyKeyCounter.cs b/osu.Game/Skinning/LegacyKeyCounter.cs index c3125c3eda..bb1532d75d 100644 --- a/osu.Game/Skinning/LegacyKeyCounter.cs +++ b/osu.Game/Skinning/LegacyKeyCounter.cs @@ -20,6 +20,17 @@ namespace osu.Game.Skinning public Colour4 KeyDownBackgroundColour { get; set; } = Colour4.Yellow; + private float keyTextRotation = 0f; + public float KeyTextRotation + { + get => keyTextRotation; + set + { + keyTextRotation = value; + overlayKeyText.Rotation = value; + } + } + public Colour4 KeyUpBackgroundColour { get; set; } = Colour4.White; private Container keyContainer = null!; @@ -56,6 +67,7 @@ namespace osu.Game.Skinning Text = trigger.Name, Colour = KeyTextColour, Font = OsuFont.Default.With(fixedWidth: true), + Rotation = KeyTextRotation }, } }; @@ -65,7 +77,7 @@ namespace osu.Game.Skinning { base.LoadComplete(); Width = Math.Max(overlayKey.Width, 48 * 0.95f); - Height = Math.Max(overlayKey.Height, 48 * 0.95f); + Height = Math.Max(overlayKey.Height, 46 * 0.95f); } protected override void Activate(bool forwardPlayback = true) diff --git a/osu.Game/Skinning/LegacyKeyCounterDisplay.cs b/osu.Game/Skinning/LegacyKeyCounterDisplay.cs index e395aefe8a..1847effb3b 100644 --- a/osu.Game/Skinning/LegacyKeyCounterDisplay.cs +++ b/osu.Game/Skinning/LegacyKeyCounterDisplay.cs @@ -57,8 +57,17 @@ namespace osu.Game.Skinning { TransitionDuration = key_transition_time, KeyTextColour = keyTextColor, + KeyTextRotation = -Rotation, }; + protected override void Update() + { + base.Update(); + // keep the text are always horizontal + foreach (var child in KeyFlow.Cast()) + child.KeyTextRotation = -Rotation; + } + private Color4 keyTextColor = Color4.White; public Color4 KeyTextColor From f7dc0b65dacd8e98733eb87873f70adb97f2bf56 Mon Sep 17 00:00:00 2001 From: normalid Date: Tue, 23 Jul 2024 20:47:49 +0800 Subject: [PATCH 1596/2493] Clean up the code --- osu.Game/Skinning/LegacyKeyCounter.cs | 13 ++++--------- osu.Game/Skinning/LegacyKeyCounterDisplay.cs | 4 +--- 2 files changed, 5 insertions(+), 12 deletions(-) diff --git a/osu.Game/Skinning/LegacyKeyCounter.cs b/osu.Game/Skinning/LegacyKeyCounter.cs index bb1532d75d..333e120024 100644 --- a/osu.Game/Skinning/LegacyKeyCounter.cs +++ b/osu.Game/Skinning/LegacyKeyCounter.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 osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Graphics; @@ -20,6 +19,8 @@ namespace osu.Game.Skinning public Colour4 KeyDownBackgroundColour { get; set; } = Colour4.Yellow; + public Colour4 KeyUpBackgroundColour { get; set; } = Colour4.White; + private float keyTextRotation = 0f; public float KeyTextRotation { @@ -31,8 +32,6 @@ namespace osu.Game.Skinning } } - public Colour4 KeyUpBackgroundColour { get; set; } = Colour4.White; - private Container keyContainer = null!; private SkinnableSprite overlayKey = null!; @@ -71,13 +70,9 @@ namespace osu.Game.Skinning }, } }; - } - protected override void LoadComplete() - { - base.LoadComplete(); - Width = Math.Max(overlayKey.Width, 48 * 0.95f); - Height = Math.Max(overlayKey.Height, 46 * 0.95f); + // Legacy key counter size + Height = Width = 48 * 0.95f; } protected override void Activate(bool forwardPlayback = true) diff --git a/osu.Game/Skinning/LegacyKeyCounterDisplay.cs b/osu.Game/Skinning/LegacyKeyCounterDisplay.cs index 1847effb3b..091691657b 100644 --- a/osu.Game/Skinning/LegacyKeyCounterDisplay.cs +++ b/osu.Game/Skinning/LegacyKeyCounterDisplay.cs @@ -16,15 +16,13 @@ namespace osu.Game.Skinning protected override FillFlowContainer KeyFlow { get; } = null!; - private SkinnableSprite overlayBackground = null!; - public LegacyKeyCounterDisplay() { AutoSizeAxes = Axes.Both; AddRangeInternal(new Drawable[] { - overlayBackground = new SkinnableSprite + new SkinnableSprite { Anchor = Anchor.TopLeft, Origin = Anchor.TopLeft, From dce894108ab0679106b940b46a7cd40dd166674b Mon Sep 17 00:00:00 2001 From: normalid Date: Tue, 23 Jul 2024 20:50:08 +0800 Subject: [PATCH 1597/2493] Remove unused blending mode --- osu.Game/Skinning/LegacyKeyCounter.cs | 20 +------------------- 1 file changed, 1 insertion(+), 19 deletions(-) diff --git a/osu.Game/Skinning/LegacyKeyCounter.cs b/osu.Game/Skinning/LegacyKeyCounter.cs index 333e120024..ce757e25e1 100644 --- a/osu.Game/Skinning/LegacyKeyCounter.cs +++ b/osu.Game/Skinning/LegacyKeyCounter.cs @@ -34,8 +34,6 @@ namespace osu.Game.Skinning private Container keyContainer = null!; - private SkinnableSprite overlayKey = null!; - private OsuSpriteText overlayKeyText = null!; public LegacyKeyCounter(InputTrigger trigger) @@ -50,9 +48,8 @@ namespace osu.Game.Skinning Anchor = Anchor.Centre, Children = new Drawable[] { - overlayKey = new SkinnableSprite + new SkinnableSprite { - Blending = Multiplicative, Anchor = Anchor.Centre, Origin = Anchor.Centre, BypassAutoSizeAxes = Axes.Both, @@ -89,20 +86,5 @@ namespace osu.Game.Skinning keyContainer.ScaleTo(1f, TransitionDuration); keyContainer.FadeColour(KeyUpBackgroundColour, TransitionDuration); } - - public static BlendingParameters Multiplicative - { - get - { - BlendingParameters result = default(BlendingParameters); - result.Source = BlendingType.SrcAlpha; - result.Destination = BlendingType.OneMinusSrcAlpha; - result.SourceAlpha = BlendingType.One; - result.DestinationAlpha = BlendingType.One; - result.RGBEquation = BlendingEquation.Add; - result.AlphaEquation = BlendingEquation.Add; - return result; - } - } } } From a015fde014aebee865a0ab57736ab340e3fceea9 Mon Sep 17 00:00:00 2001 From: normalid Date: Tue, 23 Jul 2024 20:53:06 +0800 Subject: [PATCH 1598/2493] Change the default height to match the stable --- osu.Game/Skinning/LegacySkin.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index a890c5ffab..281a9c6053 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -395,9 +395,9 @@ namespace osu.Game.Skinning if (keyCounter != null) { keyCounter.Rotation = 90f; - keyCounter.Anchor = Anchor.CentreRight; + keyCounter.Anchor = Anchor.BottomRight; keyCounter.Origin = Anchor.TopCentre; - keyCounter.Position = new Vector2(0); + keyCounter.Position = new Vector2(0, -340); } }) { @@ -409,7 +409,6 @@ namespace osu.Game.Skinning new LegacySongProgress(), new LegacyHealthDisplay(), new BarHitErrorMeter(), - //new DefaultKeyCounterDisplay(), new LegacyKeyCounterDisplay(), } }; From 9fe369b7f41952dba6b9ddb779f9d792d5e2a867 Mon Sep 17 00:00:00 2001 From: normalid Date: Tue, 23 Jul 2024 21:08:08 +0800 Subject: [PATCH 1599/2493] Replace `SkinnableSprite` with `Sprite` --- osu.Game/Skinning/LegacyKeyCounter.cs | 18 ++++++++++++++++-- osu.Game/Skinning/LegacyKeyCounterDisplay.cs | 12 ++++++++++-- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/osu.Game/Skinning/LegacyKeyCounter.cs b/osu.Game/Skinning/LegacyKeyCounter.cs index ce757e25e1..08b03b5550 100644 --- a/osu.Game/Skinning/LegacyKeyCounter.cs +++ b/osu.Game/Skinning/LegacyKeyCounter.cs @@ -1,8 +1,11 @@ // 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.Sprites; +using osu.Framework.Graphics.Textures; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Screens.Play.HUD; @@ -22,6 +25,7 @@ namespace osu.Game.Skinning public Colour4 KeyUpBackgroundColour { get; set; } = Colour4.White; private float keyTextRotation = 0f; + public float KeyTextRotation { get => keyTextRotation; @@ -36,6 +40,8 @@ namespace osu.Game.Skinning private OsuSpriteText overlayKeyText = null!; + private Sprite keySprite = null!; + public LegacyKeyCounter(InputTrigger trigger) : base(trigger) { @@ -48,12 +54,11 @@ namespace osu.Game.Skinning Anchor = Anchor.Centre, Children = new Drawable[] { - new SkinnableSprite + keySprite = new Sprite { Anchor = Anchor.Centre, Origin = Anchor.Centre, BypassAutoSizeAxes = Axes.Both, - SpriteName = { Value = "inputoverlay-key" }, Rotation = -90, }, overlayKeyText = new OsuSpriteText @@ -72,6 +77,15 @@ namespace osu.Game.Skinning Height = Width = 48 * 0.95f; } + [BackgroundDependencyLoader] + private void load(ISkinSource source) + { + Texture? keyTexture = source.GetTexture($"inputoverlay-key"); + + if (keyTexture != null) + keySprite.Texture = keyTexture; + } + protected override void Activate(bool forwardPlayback = true) { base.Activate(forwardPlayback); diff --git a/osu.Game/Skinning/LegacyKeyCounterDisplay.cs b/osu.Game/Skinning/LegacyKeyCounterDisplay.cs index 091691657b..d26228c1d6 100644 --- a/osu.Game/Skinning/LegacyKeyCounterDisplay.cs +++ b/osu.Game/Skinning/LegacyKeyCounterDisplay.cs @@ -7,6 +7,8 @@ using osu.Framework.Graphics.Containers; using osu.Game.Screens.Play.HUD; using osuTK.Graphics; using osu.Framework.Allocation; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Textures; namespace osu.Game.Skinning { @@ -16,17 +18,18 @@ namespace osu.Game.Skinning protected override FillFlowContainer KeyFlow { get; } = null!; + private Sprite backgroundSprite = null!; + public LegacyKeyCounterDisplay() { AutoSizeAxes = Axes.Both; AddRangeInternal(new Drawable[] { - new SkinnableSprite + backgroundSprite = new Sprite { Anchor = Anchor.TopLeft, Origin = Anchor.TopLeft, - SpriteName = { Value= "inputoverlay-background" }, }, KeyFlow = new FillFlowContainer { @@ -49,6 +52,11 @@ namespace osu.Game.Skinning { KeyTextColor = v.NewValue; }, true); + + Texture? backgroundTexture = source.GetTexture($"inputoverlay-background"); + + if (backgroundTexture != null) + backgroundSprite.Texture = backgroundTexture; } protected override KeyCounter CreateCounter(InputTrigger trigger) => new LegacyKeyCounter(trigger) From 989ac56cbbee2c75231aa95c743d3acaf0442f42 Mon Sep 17 00:00:00 2001 From: normalid Date: Tue, 23 Jul 2024 21:12:55 +0800 Subject: [PATCH 1600/2493] Fix the return button being squshed --- osu.Game/Skinning/LegacySkin.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index 281a9c6053..6e447242da 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -395,9 +395,11 @@ namespace osu.Game.Skinning if (keyCounter != null) { keyCounter.Rotation = 90f; - keyCounter.Anchor = Anchor.BottomRight; + // set the anchor to top right so that it won't squash to the return button to the top + keyCounter.Anchor = Anchor.TopRight; keyCounter.Origin = Anchor.TopCentre; - keyCounter.Position = new Vector2(0, -340); + keyCounter.X = 0; + keyCounter.Y = container.ToLocalSpace(container.ScreenSpaceDrawQuad.BottomRight).Y - 340; } }) { From c7b110a471f94bb30b4d697124ce8cabdaf90b1e Mon Sep 17 00:00:00 2001 From: normalid Date: Tue, 23 Jul 2024 22:11:28 +0800 Subject: [PATCH 1601/2493] * Fix the default position * Make the font match stable style --- osu.Game/Skinning/LegacyKeyCounter.cs | 2 +- osu.Game/Skinning/LegacySkin.cs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Skinning/LegacyKeyCounter.cs b/osu.Game/Skinning/LegacyKeyCounter.cs index 08b03b5550..5640e14dbf 100644 --- a/osu.Game/Skinning/LegacyKeyCounter.cs +++ b/osu.Game/Skinning/LegacyKeyCounter.cs @@ -67,7 +67,7 @@ namespace osu.Game.Skinning Origin = Anchor.Centre, Text = trigger.Name, Colour = KeyTextColour, - Font = OsuFont.Default.With(fixedWidth: true), + Font = OsuFont.GetFont(size: 20), Rotation = KeyTextRotation }, } diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index 6e447242da..cfa7eb7872 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -15,7 +15,6 @@ using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Textures; using osu.Framework.IO.Stores; -using osu.Framework.Logging; using osu.Game.Audio; using osu.Game.Beatmaps.Formats; using osu.Game.Extensions; @@ -396,10 +395,11 @@ namespace osu.Game.Skinning { keyCounter.Rotation = 90f; // set the anchor to top right so that it won't squash to the return button to the top - keyCounter.Anchor = Anchor.TopRight; + keyCounter.Anchor = Anchor.CentreRight; keyCounter.Origin = Anchor.TopCentre; keyCounter.X = 0; - keyCounter.Y = container.ToLocalSpace(container.ScreenSpaceDrawQuad.BottomRight).Y - 340; + // 340px is the default height inherit from stable + keyCounter.Y = container.ToLocalSpace(new Vector2(0, container.ScreenSpaceDrawQuad.Centre.Y - 340f)).Y; } }) { From c52a993607d50363acabc85b0fcc275a091265f5 Mon Sep 17 00:00:00 2001 From: normalid Date: Tue, 23 Jul 2024 23:35:25 +0800 Subject: [PATCH 1602/2493] Support custom input overlay color --- osu.Game/Beatmaps/Formats/LegacyDecoder.cs | 27 +++++++++++++++----- osu.Game/Skinning/LegacyKeyCounter.cs | 22 +++++++++------- osu.Game/Skinning/LegacyKeyCounterDisplay.cs | 15 ++++++----- osu.Game/Skinning/LegacySkin.cs | 4 ++- osu.Game/Skinning/SkinConfiguration.cs | 4 +++ 5 files changed, 49 insertions(+), 23 deletions(-) diff --git a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs index 93af9cf41c..331b84cbf2 100644 --- a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs @@ -9,6 +9,7 @@ using osu.Game.Audio; using osu.Game.Beatmaps.ControlPoints; using osu.Game.IO; using osu.Game.Rulesets.Objects.Legacy; +using osu.Game.Skinning; using osuTK.Graphics; namespace osu.Game.Beatmaps.Formats @@ -93,14 +94,8 @@ namespace osu.Game.Beatmaps.Formats return line; } - protected void HandleColours(TModel output, string line, bool allowAlpha) + private Color4 convertSettingStringToColor4(string[] split, bool allowAlpha, KeyValuePair pair) { - var pair = SplitKeyVal(line); - - bool isCombo = pair.Key.StartsWith(@"Combo", StringComparison.Ordinal); - - string[] split = pair.Value.Split(','); - if (split.Length != 3 && split.Length != 4) throw new InvalidOperationException($@"Color specified in incorrect format (should be R,G,B or R,G,B,A): {pair.Value}"); @@ -115,6 +110,17 @@ namespace osu.Game.Beatmaps.Formats { throw new InvalidOperationException(@"Color must be specified with 8-bit integer components"); } + return colour; + } + + protected void HandleColours(TModel output, string line, bool allowAlpha) + { + var pair = SplitKeyVal(line); + + string[] split = pair.Value.Split(','); + Color4 colour = convertSettingStringToColor4(split, allowAlpha, pair); + + bool isCombo = pair.Key.StartsWith(@"Combo", StringComparison.Ordinal); if (isCombo) { @@ -128,6 +134,13 @@ namespace osu.Game.Beatmaps.Formats tHasCustomColours.CustomColours[pair.Key] = colour; } + bool isInputOverlayText = pair.Key.StartsWith(@"InputOverlayText"); + + if (isInputOverlayText) + { + if (!(output is SkinConfiguration tSkinConfiguration)) return; + tSkinConfiguration.InputOverlayText = colour; + } } protected KeyValuePair SplitKeyVal(string line, char separator = ':', bool shouldTrim = true) diff --git a/osu.Game/Skinning/LegacyKeyCounter.cs b/osu.Game/Skinning/LegacyKeyCounter.cs index 5640e14dbf..85d5a897fb 100644 --- a/osu.Game/Skinning/LegacyKeyCounter.cs +++ b/osu.Game/Skinning/LegacyKeyCounter.cs @@ -16,13 +16,19 @@ namespace osu.Game.Skinning { public bool UsesFixedAnchor { get; set; } - public float TransitionDuration { get; set; } = 150f; + public float TransitionDuration { get; set; } = 50f; - public Colour4 KeyTextColour { get; set; } = Colour4.White; + public Colour4 KeyTextColour + { + get => keyTextColour; + set + { + keyTextColour = value; + overlayKeyText.Colour = value; + } + } - public Colour4 KeyDownBackgroundColour { get; set; } = Colour4.Yellow; - - public Colour4 KeyUpBackgroundColour { get; set; } = Colour4.White; + private Colour4 keyTextColour = Colour4.White; private float keyTextRotation = 0f; @@ -66,7 +72,7 @@ namespace osu.Game.Skinning Anchor = Anchor.Centre, Origin = Anchor.Centre, Text = trigger.Name, - Colour = KeyTextColour, + Colour = keyTextColour, Font = OsuFont.GetFont(size: 20), Rotation = KeyTextRotation }, @@ -89,8 +95,7 @@ namespace osu.Game.Skinning protected override void Activate(bool forwardPlayback = true) { base.Activate(forwardPlayback); - keyContainer.ScaleTo(0.75f, TransitionDuration); - keyContainer.FadeColour(KeyDownBackgroundColour, TransitionDuration); + keyContainer.ScaleTo(0.8f, TransitionDuration); overlayKeyText.Text = CountPresses.Value.ToString(); } @@ -98,7 +103,6 @@ namespace osu.Game.Skinning { base.Deactivate(forwardPlayback); keyContainer.ScaleTo(1f, TransitionDuration); - keyContainer.FadeColour(KeyUpBackgroundColour, TransitionDuration); } } } diff --git a/osu.Game/Skinning/LegacyKeyCounterDisplay.cs b/osu.Game/Skinning/LegacyKeyCounterDisplay.cs index d26228c1d6..a585d93c5a 100644 --- a/osu.Game/Skinning/LegacyKeyCounterDisplay.cs +++ b/osu.Game/Skinning/LegacyKeyCounterDisplay.cs @@ -5,7 +5,6 @@ using System.Linq; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Screens.Play.HUD; -using osuTK.Graphics; using osu.Framework.Allocation; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; @@ -45,10 +44,13 @@ namespace osu.Game.Skinning }); } - [BackgroundDependencyLoader] - private void load(ISkinSource source) + [Resolved] + private ISkinSource source { get; set; } = null!; + + protected override void LoadComplete() { - source.GetConfig("InputOverlayText")?.BindValueChanged(v => + base.LoadComplete(); + source.GetConfig(SkinConfiguration.LegacySetting.InputOverlayText)?.BindValueChanged(v => { KeyTextColor = v.NewValue; }, true); @@ -69,14 +71,15 @@ namespace osu.Game.Skinning protected override void Update() { base.Update(); + // keep the text are always horizontal foreach (var child in KeyFlow.Cast()) child.KeyTextRotation = -Rotation; } - private Color4 keyTextColor = Color4.White; + private Colour4 keyTextColor = Colour4.White; - public Color4 KeyTextColor + public Colour4 KeyTextColor { get => keyTextColor; set diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index cfa7eb7872..fa83837214 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -15,6 +15,7 @@ using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Textures; using osu.Framework.IO.Stores; +using osu.Framework.Logging; using osu.Game.Audio; using osu.Game.Beatmaps.Formats; using osu.Game.Extensions; @@ -309,7 +310,8 @@ namespace osu.Game.Skinning { case SkinConfiguration.LegacySetting.Version: return SkinUtils.As(new Bindable(Configuration.LegacyVersion ?? SkinConfiguration.LATEST_VERSION)); - + case SkinConfiguration.LegacySetting.InputOverlayText: + return SkinUtils.As(new Bindable(Configuration.InputOverlayText ?? Colour4.White)); default: return genericLookup(legacySetting); } diff --git a/osu.Game/Skinning/SkinConfiguration.cs b/osu.Game/Skinning/SkinConfiguration.cs index 937cca0aeb..abfcaff1d8 100644 --- a/osu.Game/Skinning/SkinConfiguration.cs +++ b/osu.Game/Skinning/SkinConfiguration.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; +using osu.Framework.Graphics; using osu.Game.Beatmaps.Formats; using osuTK.Graphics; @@ -38,8 +39,11 @@ namespace osu.Game.Skinning AnimationFramerate, LayeredHitSounds, AllowSliderBallTint, + InputOverlayText, } + public Colour4? InputOverlayText { get; internal set; } + public static List DefaultComboColours { get; } = new List { new Color4(255, 192, 0, 255), From 661f58a39747ed03ec94bd4df12168ac055d5e2f Mon Sep 17 00:00:00 2001 From: normalid Date: Wed, 24 Jul 2024 12:18:05 +0800 Subject: [PATCH 1603/2493] Add test coverage --- .../Archives/modified-classic-20240724.osk | Bin 0 -> 518 bytes osu.Game.Tests/Skins/SkinDeserialisationTest.cs | 4 +++- 2 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 osu.Game.Tests/Resources/Archives/modified-classic-20240724.osk diff --git a/osu.Game.Tests/Resources/Archives/modified-classic-20240724.osk b/osu.Game.Tests/Resources/Archives/modified-classic-20240724.osk new file mode 100644 index 0000000000000000000000000000000000000000..ed8f3fd3fdd58672e43d31fdf7ab9b0f48e4cac9 GIT binary patch literal 518 zcmWIWW@Zs#U|`^2_?h@4!u!p6tu;X2aj*yjLveOyo?d2NrfSc|yh8>euI1kU+M}1b ztP)zl9JI|ZXKs|(m4|}+8(RO$ZIX5UX~t&P8J`ibU3u=}&wK1|iQN}Jwm|X4lczm# zOE-x4OmR%U=yCMB$RVDhK3P9wlm7gI*%ul3l?a!!L@9MX{Vt)GOx9F<3Ri6VFcfI=a;HK`5m1}>>oOTUvm*3gtmvpu< z_?wpOwzKDE*$1Hbs4sTW-zK1MW&trT5QBV_nU|KYmsOmfxBA)Bpn!l+-e-J6Hw6Wp z($@$z^pmlLBigjx*dKw zectr>)9KF(gyvNqRqd=)o!O~6^JnF}%FLeui@)A!y!L`MJ^k6!)Mray?_9D>b=tEj zKb9<0R|{`)DHTcRno_Ydi!s2Pkx7IBcLV@k3;~TG3Kkvcy3jod)x*HhcnPWt?&Sb) RRyL3{6A%^x>3T*G4*(EG#>4;s literal 0 HcmV?d00001 diff --git a/osu.Game.Tests/Skins/SkinDeserialisationTest.cs b/osu.Game.Tests/Skins/SkinDeserialisationTest.cs index f2547b4f5d..039e85bbce 100644 --- a/osu.Game.Tests/Skins/SkinDeserialisationTest.cs +++ b/osu.Game.Tests/Skins/SkinDeserialisationTest.cs @@ -65,7 +65,9 @@ namespace osu.Game.Tests.Skins // Covers default rank display "Archives/modified-default-20230809.osk", // Covers legacy rank display - "Archives/modified-classic-20230809.osk" + "Archives/modified-classic-20230809.osk", + // Covcers legacy key counter + "Archives/modified-classic-20240724.osk" }; /// From 95f287104eb85c0165727bcd44e65cf841b6d006 Mon Sep 17 00:00:00 2001 From: normalid Date: Wed, 24 Jul 2024 12:24:58 +0800 Subject: [PATCH 1604/2493] Add visual test seane --- .../Visual/Gameplay/TestSceneKeyCounter.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneKeyCounter.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneKeyCounter.cs index 2d2b6c3bed..57bfb5fddf 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneKeyCounter.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneKeyCounter.cs @@ -9,6 +9,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Utils; using osu.Game.Screens.Play; using osu.Game.Screens.Play.HUD; +using osu.Game.Skinning; using osuTK; using osuTK.Input; @@ -56,6 +57,11 @@ namespace osu.Game.Tests.Visual.Gameplay Anchor = Anchor.Centre, Scale = new Vector2(1, -1) }, + new LegacyKeyCounterDisplay + { + Origin = Anchor.Centre, + Anchor = Anchor.Centre, + }, new FillFlowContainer { AutoSizeAxes = Axes.Both, @@ -89,6 +95,12 @@ namespace osu.Game.Tests.Visual.Gameplay Anchor = Anchor.Centre, Rotation = 90, }, + new LegacyKeyCounterDisplay + { + Origin = Anchor.Centre, + Anchor = Anchor.Centre, + Rotation = 90, + }, } }, } From 395f8424b5aed3837a5aba63d6cb79eb426c3d81 Mon Sep 17 00:00:00 2001 From: normalid Date: Wed, 24 Jul 2024 12:30:08 +0800 Subject: [PATCH 1605/2493] Match the stable animation --- osu.Game/Skinning/LegacyKeyCounter.cs | 2 +- osu.Game/Skinning/LegacyKeyCounterDisplay.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Skinning/LegacyKeyCounter.cs b/osu.Game/Skinning/LegacyKeyCounter.cs index 85d5a897fb..0637118bd1 100644 --- a/osu.Game/Skinning/LegacyKeyCounter.cs +++ b/osu.Game/Skinning/LegacyKeyCounter.cs @@ -95,7 +95,7 @@ namespace osu.Game.Skinning protected override void Activate(bool forwardPlayback = true) { base.Activate(forwardPlayback); - keyContainer.ScaleTo(0.8f, TransitionDuration); + keyContainer.ScaleTo(0.75f, TransitionDuration, Easing.OutQuad); overlayKeyText.Text = CountPresses.Value.ToString(); } diff --git a/osu.Game/Skinning/LegacyKeyCounterDisplay.cs b/osu.Game/Skinning/LegacyKeyCounterDisplay.cs index a585d93c5a..adc5d87973 100644 --- a/osu.Game/Skinning/LegacyKeyCounterDisplay.cs +++ b/osu.Game/Skinning/LegacyKeyCounterDisplay.cs @@ -13,7 +13,7 @@ namespace osu.Game.Skinning { public partial class LegacyKeyCounterDisplay : KeyCounterDisplay { - private const float key_transition_time = 50; + private const float key_transition_time = 100; protected override FillFlowContainer KeyFlow { get; } = null!; From e2beacb3ddd5e83ab7f7bb2487d13f548f9f5252 Mon Sep 17 00:00:00 2001 From: normalid Date: Wed, 24 Jul 2024 12:31:01 +0800 Subject: [PATCH 1606/2493] Remove logging --- osu.Game/Skinning/LegacySkin.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index fa83837214..f754fee077 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -15,7 +15,6 @@ using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Textures; using osu.Framework.IO.Stores; -using osu.Framework.Logging; using osu.Game.Audio; using osu.Game.Beatmaps.Formats; using osu.Game.Extensions; From 56143de2c6ade3e8ffe705bc4dedc6df4acfcb00 Mon Sep 17 00:00:00 2001 From: normalid Date: Wed, 24 Jul 2024 12:39:36 +0800 Subject: [PATCH 1607/2493] Update offset factor --- osu.Game/Skinning/LegacyKeyCounterDisplay.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Skinning/LegacyKeyCounterDisplay.cs b/osu.Game/Skinning/LegacyKeyCounterDisplay.cs index adc5d87973..ccb1c21402 100644 --- a/osu.Game/Skinning/LegacyKeyCounterDisplay.cs +++ b/osu.Game/Skinning/LegacyKeyCounterDisplay.cs @@ -8,6 +8,7 @@ using osu.Game.Screens.Play.HUD; using osu.Framework.Allocation; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; +using osuTK; namespace osu.Game.Skinning { @@ -35,7 +36,7 @@ namespace osu.Game.Skinning // https://osu.ppy.sh/wiki/en/Skinning/Interface#input-overlay // 24px away from the container, there're 4 counter in legacy, so divide by 4 // "inputoverlay-background.png" are 1.05x in-game. so *1.05f to the X coordinate - X = (24 / 4) * 1.05f, + X = (24 / 4) * 1f, Anchor = Anchor.TopLeft, Origin = Anchor.TopLeft, Direction = FillDirection.Horizontal, From b24be96d047d02a258a1ea40569efdd65fe4dae6 Mon Sep 17 00:00:00 2001 From: normalid Date: Wed, 24 Jul 2024 12:57:30 +0800 Subject: [PATCH 1608/2493] Fix code quality --- osu.Game/Beatmaps/Formats/LegacyDecoder.cs | 5 ++- osu.Game/Skinning/LegacyKeyCounter.cs | 10 ++--- osu.Game/Skinning/LegacyKeyCounterDisplay.cs | 39 ++++++++++---------- osu.Game/Skinning/LegacySkin.cs | 2 + 4 files changed, 30 insertions(+), 26 deletions(-) diff --git a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs index 331b84cbf2..4f11666392 100644 --- a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs @@ -110,6 +110,7 @@ namespace osu.Game.Beatmaps.Formats { throw new InvalidOperationException(@"Color must be specified with 8-bit integer components"); } + return colour; } @@ -134,11 +135,13 @@ namespace osu.Game.Beatmaps.Formats tHasCustomColours.CustomColours[pair.Key] = colour; } - bool isInputOverlayText = pair.Key.StartsWith(@"InputOverlayText"); + + bool isInputOverlayText = pair.Key == @"InputOverlayText"; if (isInputOverlayText) { if (!(output is SkinConfiguration tSkinConfiguration)) return; + tSkinConfiguration.InputOverlayText = colour; } } diff --git a/osu.Game/Skinning/LegacyKeyCounter.cs b/osu.Game/Skinning/LegacyKeyCounter.cs index 0637118bd1..88ca86c63b 100644 --- a/osu.Game/Skinning/LegacyKeyCounter.cs +++ b/osu.Game/Skinning/LegacyKeyCounter.cs @@ -30,7 +30,7 @@ namespace osu.Game.Skinning private Colour4 keyTextColour = Colour4.White; - private float keyTextRotation = 0f; + private float keyTextRotation; public float KeyTextRotation { @@ -42,11 +42,11 @@ namespace osu.Game.Skinning } } - private Container keyContainer = null!; + private readonly Container keyContainer; - private OsuSpriteText overlayKeyText = null!; + private readonly OsuSpriteText overlayKeyText; - private Sprite keySprite = null!; + private readonly Sprite keySprite; public LegacyKeyCounter(InputTrigger trigger) : base(trigger) @@ -86,7 +86,7 @@ namespace osu.Game.Skinning [BackgroundDependencyLoader] private void load(ISkinSource source) { - Texture? keyTexture = source.GetTexture($"inputoverlay-key"); + Texture? keyTexture = source.GetTexture(@"inputoverlay-key"); if (keyTexture != null) keySprite.Texture = keyTexture; diff --git a/osu.Game/Skinning/LegacyKeyCounterDisplay.cs b/osu.Game/Skinning/LegacyKeyCounterDisplay.cs index ccb1c21402..651afb788a 100644 --- a/osu.Game/Skinning/LegacyKeyCounterDisplay.cs +++ b/osu.Game/Skinning/LegacyKeyCounterDisplay.cs @@ -8,7 +8,6 @@ using osu.Game.Screens.Play.HUD; using osu.Framework.Allocation; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; -using osuTK; namespace osu.Game.Skinning { @@ -16,9 +15,9 @@ namespace osu.Game.Skinning { private const float key_transition_time = 100; - protected override FillFlowContainer KeyFlow { get; } = null!; + protected override FillFlowContainer KeyFlow { get; } - private Sprite backgroundSprite = null!; + private readonly Sprite backgroundSprite; public LegacyKeyCounterDisplay() { @@ -26,22 +25,22 @@ namespace osu.Game.Skinning AddRangeInternal(new Drawable[] { - backgroundSprite = new Sprite - { - Anchor = Anchor.TopLeft, - Origin = Anchor.TopLeft, - }, - KeyFlow = new FillFlowContainer - { - // https://osu.ppy.sh/wiki/en/Skinning/Interface#input-overlay - // 24px away from the container, there're 4 counter in legacy, so divide by 4 - // "inputoverlay-background.png" are 1.05x in-game. so *1.05f to the X coordinate - X = (24 / 4) * 1f, - Anchor = Anchor.TopLeft, - Origin = Anchor.TopLeft, - Direction = FillDirection.Horizontal, - AutoSizeAxes = Axes.Both, - }, + backgroundSprite = new Sprite + { + Anchor = Anchor.TopLeft, + Origin = Anchor.TopLeft, + }, + KeyFlow = new FillFlowContainer + { + // https://osu.ppy.sh/wiki/en/Skinning/Interface#input-overlay + // 24px away from the container, there're 4 counter in legacy, so divide by 4 + // "inputoverlay-background.png" are 1.05x in-game. so *1.05f to the X coordinate + X = 24f / 4f, + Anchor = Anchor.TopLeft, + Origin = Anchor.TopLeft, + Direction = FillDirection.Horizontal, + AutoSizeAxes = Axes.Both, + }, }); } @@ -56,7 +55,7 @@ namespace osu.Game.Skinning KeyTextColor = v.NewValue; }, true); - Texture? backgroundTexture = source.GetTexture($"inputoverlay-background"); + Texture? backgroundTexture = source.GetTexture(@"inputoverlay-background"); if (backgroundTexture != null) backgroundSprite.Texture = backgroundTexture; diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index f754fee077..7a3cc2d785 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -309,8 +309,10 @@ namespace osu.Game.Skinning { case SkinConfiguration.LegacySetting.Version: return SkinUtils.As(new Bindable(Configuration.LegacyVersion ?? SkinConfiguration.LATEST_VERSION)); + case SkinConfiguration.LegacySetting.InputOverlayText: return SkinUtils.As(new Bindable(Configuration.InputOverlayText ?? Colour4.White)); + default: return genericLookup(legacySetting); } From fede6b3657e75391051bc2b8259194999769331a Mon Sep 17 00:00:00 2001 From: normalid Date: Wed, 24 Jul 2024 13:09:21 +0800 Subject: [PATCH 1609/2493] Fix indent problems --- osu.Game/Skinning/LegacyKeyCounterDisplay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Skinning/LegacyKeyCounterDisplay.cs b/osu.Game/Skinning/LegacyKeyCounterDisplay.cs index 651afb788a..44aab407a5 100644 --- a/osu.Game/Skinning/LegacyKeyCounterDisplay.cs +++ b/osu.Game/Skinning/LegacyKeyCounterDisplay.cs @@ -28,7 +28,7 @@ namespace osu.Game.Skinning backgroundSprite = new Sprite { Anchor = Anchor.TopLeft, - Origin = Anchor.TopLeft, + Origin = Anchor.TopLeft, }, KeyFlow = new FillFlowContainer { From 0306ef4096aeb8aafd1eb49c8afefcd338a62f05 Mon Sep 17 00:00:00 2001 From: normalid Date: Wed, 24 Jul 2024 14:13:45 +0800 Subject: [PATCH 1610/2493] Update test assets --- .../Archives/modified-classic-20240724.osk | Bin 518 -> 1446 bytes .../Skins/SkinDeserialisationTest.cs | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Resources/Archives/modified-classic-20240724.osk b/osu.Game.Tests/Resources/Archives/modified-classic-20240724.osk index ed8f3fd3fdd58672e43d31fdf7ab9b0f48e4cac9..29a06abf1d00fae7cb73a2cfb868f96d2be2bcca 100644 GIT binary patch literal 1446 zcmWIWW@Zs#U|`^2Xes;=Ay6+@eI3YK!Uz&!U?|Sc%+t%v%e>le=*Mg*;!^Fs@32a! z(#D%-g)}dfg_MLi2rvB+`CnCRTYXCP)>DEznB5&7F^J4zi&}TU&EYufrL!6H=9$Fj zzfbMFZM#_bdCG=|jB8uYC?+ppd9}IX)Hc=UeG17p63Sba)G=K?E}Zw*F=^j{zDpKf z4(YP&pL#y2n94k~oV?J7d#%}Z8H-7`vr3JP&h}^Bvhq5)E%?ltz+^=dFJBG$-lFRg;y_OHoLh~!*rv{evXuijJUG6x=Oi_L&ocOs2HwSG2WW~n&aAAQS^2U`Z)IlZQcG34l`lI_PE`n$Wu2j<8n-Tr0o5PoiSe~TObiTy{0t0Y zK!5lqX6AW>x;W?O7Ubup=9LtKJ#;rLHvh7LNUi%Ldzacg-Q_1Yv99D_%-Y2ec#&6b znP$+Vt)I$6u0jGk!Ik@M`-cG`F~Reg3(VTjk^S7tOiGa8tIMwPuR> zs;|c?H5|B?J010jc#y?vA)R(IPABhRk?zvsPmO0AdX~p{=>>n144S6%D@$zkzEevX zbF62t*!EdApjfqfiq8CPm(RDJ6s!^|YWCSOH&BjyUQN@{)>%z&PR4YERWJYKk(rm% z^)~GKk-e^6Q&bB!GHt){IB#A4sSTy;KApN|k;tUwy<UYBLWhWtNNo>#})T#J}&#g8QGVXV~cOPIPi!DQR%bYjJm-SM=-j7$1SI zCE?elYotG_H}wm*eu(Gt^OOj=%F%p__4^*LgQp9eS?v8M$xN@WQa1Ts`-p8yS+?lE zCs&p&xw3jv{o%bF!c)GTzjn%1cwIhg^tR#!-B#=^lU{O!b}jfSc=l4^-(aWRaXZ#0 zKA61OV8Oij*DnJe`hI_4v+BNT9kcsUff)uj6P|9Xx7B#Hk5|U?{f3}-8w9h|ITTDK zBTG+yC~|1Kv&T8)2kV=I3rcuHT>SGbO;%&G2x>In(}~50+)zN#J|xF?Z$ejAhTZr5t(IpwoJ4#}P5N zjx?U-4HM0}pMEbp?bviwVcV0>^P{g-zSchXkv-zL^1k{z|^M;#C$*;kds)MmYJH9f|Q+# zG`)2EeNK6PJ>z{kd{fX0HcqD2&)wdQdtO8cY@arLwHPpEGct)V;4Ywm217t2h=LVY z=(^BzHdGG-L*rklE_gmi*NUE*5L%A{%Sh~53*8L#pg@@6&4L~<0p6@^AbA!b{0OAy J1643E008f1L+Jnj literal 518 zcmWIWW@Zs#U|`^2_?h@4!u!p6tu;X2aj*yjLveOyo?d2NrfSc|yh8>euI1kU+M}1b ztP)zl9JI|ZXKs|(m4|}+8(RO$ZIX5UX~t&P8J`ibU3u=}&wK1|iQN}Jwm|X4lczm# zOE-x4OmR%U=yCMB$RVDhK3P9wlm7gI*%ul3l?a!!L@9MX{Vt)GOx9F<3Ri6VFcfI=a;HK`5m1}>>oOTUvm*3gtmvpu< z_?wpOwzKDE*$1Hbs4sTW-zK1MW&trT5QBV_nU|KYmsOmfxBA)Bpn!l+-e-J6Hw6Wp z($@$z^pmlLBigjx*dKw zectr>)9KF(gyvNqRqd=)o!O~6^JnF}%FLeui@)A!y!L`MJ^k6!)Mray?_9D>b=tEj zKb9<0R|{`)DHTcRno_Ydi!s2Pkx7IBcLV@k3;~TG3Kkvcy3jod)x*HhcnPWt?&Sb) RRyL3{6A%^x>3T*G4*(EG#>4;s diff --git a/osu.Game.Tests/Skins/SkinDeserialisationTest.cs b/osu.Game.Tests/Skins/SkinDeserialisationTest.cs index 039e85bbce..534d47d617 100644 --- a/osu.Game.Tests/Skins/SkinDeserialisationTest.cs +++ b/osu.Game.Tests/Skins/SkinDeserialisationTest.cs @@ -66,7 +66,7 @@ namespace osu.Game.Tests.Skins "Archives/modified-default-20230809.osk", // Covers legacy rank display "Archives/modified-classic-20230809.osk", - // Covcers legacy key counter + // Covers legacy key counter "Archives/modified-classic-20240724.osk" }; From bf4bf4d39e126e67d3125a4e03a04c7fda2af677 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 24 Jul 2024 08:54:30 +0200 Subject: [PATCH 1611/2493] Fill daily challenge top 50 position numbers client-side Only doing this client-side, because doing this server-side is expensive: https://github.com/ppy/osu-web/pull/11354#discussion_r1689224285 --- .../OnlinePlay/DailyChallenge/DailyChallengeLeaderboard.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeLeaderboard.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeLeaderboard.cs index 2b2c3a5e1f..8ba490b14d 100644 --- a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeLeaderboard.cs +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeLeaderboard.cs @@ -138,9 +138,9 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge } else { - LoadComponentsAsync(best.Select(s => new LeaderboardScoreV2(s, sheared: false) + LoadComponentsAsync(best.Select((s, index) => new LeaderboardScoreV2(s, sheared: false) { - Rank = s.Position, + Rank = index + 1, IsPersonalBest = s.UserID == api.LocalUser.Value.Id, Action = () => PresentScore?.Invoke(s.OnlineID), }), loaded => From 788b70469d855f46d08adce40bd5ab983d381bcb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 24 Jul 2024 09:15:32 +0200 Subject: [PATCH 1612/2493] Exit daily challenge screen when going offline This sort of thing is bound to happen when rewriting screens from scratch without invoking abstract eldritch entities sometimes. Damned if you do, damned if you don't... --- .../DailyChallenge/DailyChallenge.cs | 28 +++++++++++++++++++ .../Screens/OnlinePlay/OnlinePlayScreen.cs | 1 + 2 files changed, 29 insertions(+) diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs index 2d58b3b82c..2101444728 100644 --- a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs @@ -21,6 +21,7 @@ using osu.Game.Database; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osu.Game.Localisation; +using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Metadata; using osu.Game.Online.Multiplayer; @@ -48,6 +49,8 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge /// private readonly Bindable> userMods = new Bindable>(Array.Empty()); + private readonly IBindable apiState = new Bindable(); + private OnlinePlayScreenWaveContainer waves = null!; private DailyChallengeLeaderboard leaderboard = null!; private RoomModSelectOverlay userModsSelectOverlay = null!; @@ -84,6 +87,9 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge [Resolved] private UserLookupCache userLookupCache { get; set; } = null!; + [Resolved] + protected IAPIProvider API { get; private set; } = null!; + public override bool DisallowExternalBeatmapRulesetChanges => true; public DailyChallenge(Room room) @@ -358,6 +364,9 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge userModsSelectOverlayRegistration = overlayManager?.RegisterBlockingOverlay(userModsSelectOverlay); userModsSelectOverlay.SelectedItem.Value = playlistItem; userMods.BindValueChanged(_ => Scheduler.AddOnce(updateMods), true); + + apiState.BindTo(API.State); + apiState.BindValueChanged(onlineStateChanged, true); } private void trySetDailyChallengeBeatmap() @@ -368,6 +377,25 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge applyLoopingToTrack(); } + private void onlineStateChanged(ValueChangedEvent state) => Schedule(() => + { + if (state.NewValue != APIState.Online) + Schedule(forcefullyExit); + }); + + private void forcefullyExit() + { + Logger.Log($"{this} forcefully exiting due to loss of API connection"); + + // This is temporary since we don't currently have a way to force screens to be exited + // See also: `OnlinePlayScreen.forcefullyExit()` + if (this.IsCurrentScreen()) + { + while (this.IsCurrentScreen()) + this.Exit(); + } + } + public override void OnEntering(ScreenTransitionEvent e) { base.OnEntering(e); diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs b/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs index 9b6284fb89..1b7041c9bb 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs @@ -99,6 +99,7 @@ namespace osu.Game.Screens.OnlinePlay Logger.Log($"{this} forcefully exiting due to loss of API connection"); // This is temporary since we don't currently have a way to force screens to be exited + // See also: `DailyChallenge.forcefullyExit()` if (this.IsCurrentScreen()) { while (this.IsCurrentScreen()) From 55382a4ba6edea3f1d26227bd125f8db7f3c8c09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 24 Jul 2024 12:08:12 +0200 Subject: [PATCH 1613/2493] Add test coverage for expected sample popover behaviour --- .../TestSceneHitObjectSampleAdjustments.cs | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneHitObjectSampleAdjustments.cs b/osu.Game.Tests/Visual/Editing/TestSceneHitObjectSampleAdjustments.cs index 558d8dce94..75a68237c8 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneHitObjectSampleAdjustments.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneHitObjectSampleAdjustments.cs @@ -402,6 +402,70 @@ namespace osu.Game.Tests.Visual.Editing void checkPlacementSample(string expected) => AddAssert($"Placement sample is {expected}", () => EditorBeatmap.PlacementObject.Value.Samples.First().Bank, () => Is.EqualTo(expected)); } + [Test] + public void PopoverForMultipleSelectionChangesAllSamples() + { + AddStep("add slider", () => + { + EditorBeatmap.Add(new Slider + { + Position = new Vector2(256, 256), + StartTime = 1000, + Path = new SliderPath(new[] { new PathControlPoint(Vector2.Zero), new PathControlPoint(new Vector2(250, 0)) }), + Samples = + { + new HitSampleInfo(HitSampleInfo.HIT_NORMAL) + }, + NodeSamples = new List> + { + new List + { + new HitSampleInfo(HitSampleInfo.HIT_NORMAL, bank: HitSampleInfo.BANK_DRUM), + new HitSampleInfo(HitSampleInfo.HIT_CLAP, bank: HitSampleInfo.BANK_DRUM), + }, + new List + { + new HitSampleInfo(HitSampleInfo.HIT_NORMAL, bank: HitSampleInfo.BANK_SOFT), + new HitSampleInfo(HitSampleInfo.HIT_WHISTLE, bank: HitSampleInfo.BANK_SOFT), + }, + } + }); + }); + AddStep("select everything", () => EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects)); + + clickSamplePiece(0); + + setBankViaPopover(HitSampleInfo.BANK_DRUM); + samplePopoverHasSingleBank(HitSampleInfo.BANK_DRUM); + hitObjectHasSampleNormalBank(0, HitSampleInfo.BANK_DRUM); + hitObjectHasSampleNormalBank(1, HitSampleInfo.BANK_DRUM); + hitObjectHasSampleNormalBank(2, HitSampleInfo.BANK_DRUM); + hitObjectNodeHasSampleNormalBank(2, 0, HitSampleInfo.BANK_DRUM); + hitObjectNodeHasSampleNormalBank(2, 1, HitSampleInfo.BANK_DRUM); + + setVolumeViaPopover(30); + samplePopoverHasSingleVolume(30); + hitObjectHasSampleVolume(0, 30); + hitObjectHasSampleVolume(1, 30); + hitObjectHasSampleVolume(2, 30); + hitObjectNodeHasSampleVolume(2, 0, 30); + hitObjectNodeHasSampleVolume(2, 1, 30); + + toggleAdditionViaPopover(0); + hitObjectHasSamples(0, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_WHISTLE); + hitObjectHasSamples(1, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_WHISTLE); + hitObjectHasSamples(2, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_WHISTLE); + hitObjectNodeHasSamples(2, 0, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_CLAP, HitSampleInfo.HIT_WHISTLE); + hitObjectNodeHasSamples(2, 1, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_WHISTLE); + + setAdditionBankViaPopover(HitSampleInfo.BANK_SOFT); + hitObjectHasSampleAdditionBank(0, HitSampleInfo.BANK_SOFT); + hitObjectHasSampleAdditionBank(1, HitSampleInfo.BANK_SOFT); + hitObjectHasSampleAdditionBank(2, HitSampleInfo.BANK_SOFT); + hitObjectNodeHasSampleAdditionBank(2, 0, HitSampleInfo.BANK_SOFT); + hitObjectNodeHasSampleAdditionBank(2, 1, HitSampleInfo.BANK_SOFT); + } + [Test] public void TestHotkeysAffectNodeSamples() { From 1ed7e4b075bafe9ccf2e33ef252e272c61bb7d4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 23 Jul 2024 14:34:48 +0200 Subject: [PATCH 1614/2493] Make sample popover change properties of all samples in multiple selection Closes https://github.com/ppy/osu/issues/28916. The previous behaviour *may* have been intended, but it was honestly quite baffling. This seems like a saner variant. --- .../Timeline/NodeSamplePointPiece.cs | 8 +-- .../Components/Timeline/SamplePointPiece.cs | 51 +++++++++++++++---- 2 files changed, 44 insertions(+), 15 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/NodeSamplePointPiece.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/NodeSamplePointPiece.cs index ae3838bc41..9cc1268db7 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/NodeSamplePointPiece.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/NodeSamplePointPiece.cs @@ -34,12 +34,12 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline { private readonly int nodeIndex; - protected override IList GetRelevantSamples(HitObject ho) + protected override IEnumerable<(HitObject hitObject, IList samples)> GetRelevantSamples(HitObject[] hitObjects) { - if (ho is not IHasRepeats hasRepeats) - return ho.Samples; + if (hitObjects.Length > 1 || hitObjects[0] is not IHasRepeats hasRepeats) + return base.GetRelevantSamples(hitObjects); - return nodeIndex < hasRepeats.NodeSamples.Count ? hasRepeats.NodeSamples[nodeIndex] : ho.Samples; + return [(hitObjects[0], nodeIndex < hasRepeats.NodeSamples.Count ? hasRepeats.NodeSamples[nodeIndex] : hitObjects[0].Samples)]; } public NodeSampleEditPopover(HitObject hitObject, int nodeIndex) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs index 8c7603021a..8bfb0a3358 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs @@ -21,6 +21,7 @@ using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Rulesets.Objects; using osu.Game.Screens.Edit.Components.TernaryButtons; using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Objects.Types; using osu.Game.Screens.Edit.Timing; using osuTK; using osuTK.Graphics; @@ -106,15 +107,34 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline private FillFlowContainer togglesCollection = null!; private HitObject[] relevantObjects = null!; - private IList[] allRelevantSamples = null!; + private (HitObject hitObject, IList samples)[] allRelevantSamples = null!; /// /// Gets the sub-set of samples relevant to this sample point piece. /// For example, to edit node samples this should return the samples at the index of the node. /// - /// The hit object to get the relevant samples from. + /// The hit objects to get the relevant samples from. /// The relevant list of samples. - protected virtual IList GetRelevantSamples(HitObject ho) => ho.Samples; + protected virtual IEnumerable<(HitObject hitObject, IList samples)> GetRelevantSamples(HitObject[] hitObjects) + { + if (hitObjects.Length == 1) + { + yield return (hitObjects[0], hitObjects[0].Samples); + + yield break; + } + + foreach (var ho in hitObjects) + { + yield return (ho, ho.Samples); + + if (ho is IHasRepeats hasRepeats) + { + foreach (var node in hasRepeats.NodeSamples) + yield return (ho, node); + } + } + } [Resolved(canBeNull: true)] private EditorBeatmap beatmap { get; set; } = null!; @@ -172,7 +192,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline // if the piece belongs to a currently selected object, assume that the user wants to change all selected objects. // if the piece belongs to an unselected object, operate on that object alone, independently of the selection. relevantObjects = (beatmap.SelectedHitObjects.Contains(hitObject) ? beatmap.SelectedHitObjects : hitObject.Yield()).ToArray(); - allRelevantSamples = relevantObjects.Select(GetRelevantSamples).ToArray(); + allRelevantSamples = GetRelevantSamples(relevantObjects).ToArray(); // even if there are multiple objects selected, we can still display sample volume or bank if they all have the same value. int? commonVolume = getCommonVolume(); @@ -214,9 +234,19 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline togglesCollection.AddRange(createTernaryButtons().Select(b => new DrawableTernaryButton(b) { RelativeSizeAxes = Axes.None, Size = new Vector2(40, 40) })); } - private string? getCommonBank() => allRelevantSamples.Select(GetBankValue).Distinct().Count() == 1 ? GetBankValue(allRelevantSamples.First()) : null; - private string? getCommonAdditionBank() => allRelevantSamples.Select(GetAdditionBankValue).Where(o => o is not null).Distinct().Count() == 1 ? GetAdditionBankValue(allRelevantSamples.First()) : null; - private int? getCommonVolume() => allRelevantSamples.Select(GetVolumeValue).Distinct().Count() == 1 ? GetVolumeValue(allRelevantSamples.First()) : null; + private string? getCommonBank() => allRelevantSamples.Select(h => GetBankValue(h.samples)).Distinct().Count() == 1 + ? GetBankValue(allRelevantSamples.First().samples) + : null; + + private string? getCommonAdditionBank() + { + string[] additionBanks = allRelevantSamples.Select(h => GetAdditionBankValue(h.samples)).Where(o => o is not null).Cast().Distinct().ToArray(); + return additionBanks.Length == 1 ? additionBanks[0] : null; + } + + private int? getCommonVolume() => allRelevantSamples.Select(h => GetVolumeValue(h.samples)).Distinct().Count() == 1 + ? GetVolumeValue(allRelevantSamples.First().samples) + : null; private void updatePrimaryBankState() { @@ -231,7 +261,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline additionBank.PlaceholderText = string.IsNullOrEmpty(commonAdditionBank) ? "(multiple)" : string.Empty; additionBank.Current.Value = commonAdditionBank; - bool anyAdditions = allRelevantSamples.Any(o => o.Any(s => s.Name != HitSampleInfo.HIT_NORMAL)); + bool anyAdditions = allRelevantSamples.Any(o => o.samples.Any(s => s.Name != HitSampleInfo.HIT_NORMAL)); if (anyAdditions) additionBank.Show(); else @@ -247,9 +277,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline { beatmap.BeginChange(); - foreach (var relevantHitObject in relevantObjects) + foreach (var (relevantHitObject, relevantSamples) in GetRelevantSamples(relevantObjects)) { - var relevantSamples = GetRelevantSamples(relevantHitObject); updateAction(relevantHitObject, relevantSamples); beatmap.Update(relevantHitObject); } @@ -333,7 +362,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline { foreach ((string sampleName, var bindable) in selectionSampleStates) { - bindable.Value = SelectionHandler.GetStateFromSelection(relevantObjects, h => GetRelevantSamples(h).Any(s => s.Name == sampleName)); + bindable.Value = SelectionHandler.GetStateFromSelection(GetRelevantSamples(relevantObjects), h => h.samples.Any(s => s.Name == sampleName)); } } From ace5071d888eb644371b9255ea5f70e265a820c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 24 Jul 2024 14:25:51 +0200 Subject: [PATCH 1615/2493] Add better test scene --- .../Gameplay/TestSceneSkinnableKeyCounter.cs | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableKeyCounter.cs diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableKeyCounter.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableKeyCounter.cs new file mode 100644 index 0000000000..07c39793d2 --- /dev/null +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableKeyCounter.cs @@ -0,0 +1,42 @@ +// 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.Game.Rulesets; +using osu.Game.Rulesets.Osu; +using osu.Game.Screens.Play; +using osu.Game.Screens.Play.HUD; +using osu.Game.Skinning; +using osuTK.Input; + +namespace osu.Game.Tests.Visual.Gameplay +{ + public partial class TestSceneSkinnableKeyCounter : SkinnableHUDComponentTestScene + { + [Cached] + private readonly InputCountController controller = new InputCountController(); + + public override void SetUpSteps() + { + AddStep("create dependencies", () => + { + Add(controller); + controller.Add(new KeyCounterKeyboardTrigger(Key.Z)); + controller.Add(new KeyCounterKeyboardTrigger(Key.X)); + controller.Add(new KeyCounterKeyboardTrigger(Key.C)); + controller.Add(new KeyCounterKeyboardTrigger(Key.V)); + + foreach (var trigger in controller.Triggers) + Add(trigger); + }); + base.SetUpSteps(); + } + + protected override Ruleset CreateRulesetForSkinProvider() => new OsuRuleset(); + + protected override Drawable CreateDefaultImplementation() => new ArgonKeyCounterDisplay(); + + protected override Drawable CreateLegacyImplementation() => new LegacyKeyCounterDisplay(); //{ Rotation = 90, }; + } +} From 087dd759be973612cd4c734cb3948ecc115465cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 24 Jul 2024 14:26:16 +0200 Subject: [PATCH 1616/2493] Adjust layout to ballpark-match stable I dunno what the wiki is claiming with the "24px" figure or why but I'm not playing conversion games either. Dimensions ballparked via screenshots captured at x768 resolution. Also removes a weird homebrew method to keep the text upright. There is one canonical way to do this, namely `UprightAspectMaintainingContainer`. And the other key counters were already using it. --- .../Gameplay/TestSceneSkinnableKeyCounter.cs | 2 +- osu.Game/Skinning/LegacyKeyCounter.cs | 36 ++++++++----------- osu.Game/Skinning/LegacyKeyCounterDisplay.cs | 28 ++++++--------- osu.Game/Skinning/LegacySkin.cs | 3 +- 4 files changed, 26 insertions(+), 43 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableKeyCounter.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableKeyCounter.cs index 07c39793d2..098f8e3246 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableKeyCounter.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableKeyCounter.cs @@ -37,6 +37,6 @@ namespace osu.Game.Tests.Visual.Gameplay protected override Drawable CreateDefaultImplementation() => new ArgonKeyCounterDisplay(); - protected override Drawable CreateLegacyImplementation() => new LegacyKeyCounterDisplay(); //{ Rotation = 90, }; + protected override Drawable CreateLegacyImplementation() => new LegacyKeyCounterDisplay(); } } diff --git a/osu.Game/Skinning/LegacyKeyCounter.cs b/osu.Game/Skinning/LegacyKeyCounter.cs index 88ca86c63b..d5ba14e484 100644 --- a/osu.Game/Skinning/LegacyKeyCounter.cs +++ b/osu.Game/Skinning/LegacyKeyCounter.cs @@ -7,6 +7,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; using osu.Game.Graphics; +using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Screens.Play.HUD; @@ -30,18 +31,6 @@ namespace osu.Game.Skinning private Colour4 keyTextColour = Colour4.White; - private float keyTextRotation; - - public float KeyTextRotation - { - get => keyTextRotation; - set - { - keyTextRotation = value; - overlayKeyText.Rotation = value; - } - } - private readonly Container keyContainer; private readonly OsuSpriteText overlayKeyText; @@ -55,7 +44,7 @@ namespace osu.Game.Skinning Anchor = Anchor.Centre; Child = keyContainer = new Container { - RelativeSizeAxes = Axes.Both, + AutoSizeAxes = Axes.Both, Origin = Anchor.Centre, Anchor = Anchor.Centre, Children = new Drawable[] @@ -64,23 +53,26 @@ namespace osu.Game.Skinning { Anchor = Anchor.Centre, Origin = Anchor.Centre, - BypassAutoSizeAxes = Axes.Both, - Rotation = -90, }, - overlayKeyText = new OsuSpriteText + new UprightAspectMaintainingContainer { + AutoSizeAxes = Axes.Both, Anchor = Anchor.Centre, Origin = Anchor.Centre, - Text = trigger.Name, - Colour = keyTextColour, - Font = OsuFont.GetFont(size: 20), - Rotation = KeyTextRotation + Child = overlayKeyText = new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Text = trigger.Name, + Colour = keyTextColour, + Font = OsuFont.GetFont(size: 20), + }, }, } }; - // Legacy key counter size - Height = Width = 48 * 0.95f; + // matches longest dimension of default skin asset + Height = Width = 46; } [BackgroundDependencyLoader] diff --git a/osu.Game/Skinning/LegacyKeyCounterDisplay.cs b/osu.Game/Skinning/LegacyKeyCounterDisplay.cs index 44aab407a5..abfe607aab 100644 --- a/osu.Game/Skinning/LegacyKeyCounterDisplay.cs +++ b/osu.Game/Skinning/LegacyKeyCounterDisplay.cs @@ -8,6 +8,7 @@ using osu.Game.Screens.Play.HUD; using osu.Framework.Allocation; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; +using osuTK; namespace osu.Game.Skinning { @@ -27,18 +28,19 @@ namespace osu.Game.Skinning { backgroundSprite = new Sprite { - Anchor = Anchor.TopLeft, + Anchor = Anchor.TopRight, Origin = Anchor.TopLeft, + Scale = new Vector2(1.05f, 1), + Rotation = 90, }, KeyFlow = new FillFlowContainer { - // https://osu.ppy.sh/wiki/en/Skinning/Interface#input-overlay - // 24px away from the container, there're 4 counter in legacy, so divide by 4 - // "inputoverlay-background.png" are 1.05x in-game. so *1.05f to the X coordinate - X = 24f / 4f, - Anchor = Anchor.TopLeft, - Origin = Anchor.TopLeft, - Direction = FillDirection.Horizontal, + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + X = -1.5f, + Y = 7, + Spacing = new Vector2(1.8f), + Direction = FillDirection.Vertical, AutoSizeAxes = Axes.Both, }, }); @@ -65,18 +67,8 @@ namespace osu.Game.Skinning { TransitionDuration = key_transition_time, KeyTextColour = keyTextColor, - KeyTextRotation = -Rotation, }; - protected override void Update() - { - base.Update(); - - // keep the text are always horizontal - foreach (var child in KeyFlow.Cast()) - child.KeyTextRotation = -Rotation; - } - private Colour4 keyTextColor = Colour4.White; public Colour4 KeyTextColor diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index 7a3cc2d785..191ca04153 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -396,10 +396,9 @@ namespace osu.Game.Skinning if (keyCounter != null) { - keyCounter.Rotation = 90f; // set the anchor to top right so that it won't squash to the return button to the top keyCounter.Anchor = Anchor.CentreRight; - keyCounter.Origin = Anchor.TopCentre; + keyCounter.Origin = Anchor.CentreRight; keyCounter.X = 0; // 340px is the default height inherit from stable keyCounter.Y = container.ToLocalSpace(new Vector2(0, container.ScreenSpaceDrawQuad.Centre.Y - 340f)).Y; From 3c28c116ca76e153503da8ed8465727b92ee383f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 24 Jul 2024 14:49:23 +0200 Subject: [PATCH 1617/2493] Simplify input overlay text colour decode (and fix incorrect default) --- osu.Game/Beatmaps/Formats/LegacyDecoder.cs | 10 ---------- osu.Game/Skinning/LegacySkin.cs | 2 +- osu.Game/Skinning/SkinConfiguration.cs | 3 --- 3 files changed, 1 insertion(+), 14 deletions(-) diff --git a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs index 4f11666392..30a78a16ed 100644 --- a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs @@ -9,7 +9,6 @@ using osu.Game.Audio; using osu.Game.Beatmaps.ControlPoints; using osu.Game.IO; using osu.Game.Rulesets.Objects.Legacy; -using osu.Game.Skinning; using osuTK.Graphics; namespace osu.Game.Beatmaps.Formats @@ -135,15 +134,6 @@ namespace osu.Game.Beatmaps.Formats tHasCustomColours.CustomColours[pair.Key] = colour; } - - bool isInputOverlayText = pair.Key == @"InputOverlayText"; - - if (isInputOverlayText) - { - if (!(output is SkinConfiguration tSkinConfiguration)) return; - - tSkinConfiguration.InputOverlayText = colour; - } } protected KeyValuePair SplitKeyVal(string line, char separator = ':', bool shouldTrim = true) diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index 191ca04153..64965874a5 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -311,7 +311,7 @@ namespace osu.Game.Skinning return SkinUtils.As(new Bindable(Configuration.LegacyVersion ?? SkinConfiguration.LATEST_VERSION)); case SkinConfiguration.LegacySetting.InputOverlayText: - return SkinUtils.As(new Bindable(Configuration.InputOverlayText ?? Colour4.White)); + return SkinUtils.As(new Bindable(Configuration.CustomColours.TryGetValue(@"InputOverlayText", out var colour) ? colour : Colour4.Black)); default: return genericLookup(legacySetting); diff --git a/osu.Game/Skinning/SkinConfiguration.cs b/osu.Game/Skinning/SkinConfiguration.cs index abfcaff1d8..a657a667eb 100644 --- a/osu.Game/Skinning/SkinConfiguration.cs +++ b/osu.Game/Skinning/SkinConfiguration.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; -using osu.Framework.Graphics; using osu.Game.Beatmaps.Formats; using osuTK.Graphics; @@ -42,8 +41,6 @@ namespace osu.Game.Skinning InputOverlayText, } - public Colour4? InputOverlayText { get; internal set; } - public static List DefaultComboColours { get; } = new List { new Color4(255, 192, 0, 255), From 26395bd443dd5ca13f641c9486180a0d8cc74d32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 24 Jul 2024 15:07:15 +0200 Subject: [PATCH 1618/2493] Adjust animations further to match stable --- osu.Game/Skinning/LegacyKeyCounter.cs | 25 ++++++++++---------- osu.Game/Skinning/LegacyKeyCounterDisplay.cs | 13 ++++++---- 2 files changed, 22 insertions(+), 16 deletions(-) diff --git a/osu.Game/Skinning/LegacyKeyCounter.cs b/osu.Game/Skinning/LegacyKeyCounter.cs index d5ba14e484..8a182de9b7 100644 --- a/osu.Game/Skinning/LegacyKeyCounter.cs +++ b/osu.Game/Skinning/LegacyKeyCounter.cs @@ -15,26 +15,24 @@ namespace osu.Game.Skinning { public partial class LegacyKeyCounter : KeyCounter { - public bool UsesFixedAnchor { get; set; } + private const float transition_duration = 160; - public float TransitionDuration { get; set; } = 50f; + public Colour4 ActiveColour { get; set; } - public Colour4 KeyTextColour + private Colour4 textColour; + + public Colour4 TextColour { - get => keyTextColour; + get => textColour; set { - keyTextColour = value; + textColour = value; overlayKeyText.Colour = value; } } - private Colour4 keyTextColour = Colour4.White; - private readonly Container keyContainer; - private readonly OsuSpriteText overlayKeyText; - private readonly Sprite keySprite; public LegacyKeyCounter(InputTrigger trigger) @@ -64,7 +62,7 @@ namespace osu.Game.Skinning Anchor = Anchor.Centre, Origin = Anchor.Centre, Text = trigger.Name, - Colour = keyTextColour, + Colour = textColour, Font = OsuFont.GetFont(size: 20), }, }, @@ -87,14 +85,17 @@ namespace osu.Game.Skinning protected override void Activate(bool forwardPlayback = true) { base.Activate(forwardPlayback); - keyContainer.ScaleTo(0.75f, TransitionDuration, Easing.OutQuad); + keyContainer.ScaleTo(0.75f, transition_duration, Easing.Out); + keySprite.Colour = ActiveColour; overlayKeyText.Text = CountPresses.Value.ToString(); + overlayKeyText.Font = overlayKeyText.Font.With(weight: FontWeight.Bold); } protected override void Deactivate(bool forwardPlayback = true) { base.Deactivate(forwardPlayback); - keyContainer.ScaleTo(1f, TransitionDuration); + keyContainer.ScaleTo(1f, transition_duration, Easing.Out); + keySprite.Colour = Colour4.White; } } } diff --git a/osu.Game/Skinning/LegacyKeyCounterDisplay.cs b/osu.Game/Skinning/LegacyKeyCounterDisplay.cs index abfe607aab..8c652085e4 100644 --- a/osu.Game/Skinning/LegacyKeyCounterDisplay.cs +++ b/osu.Game/Skinning/LegacyKeyCounterDisplay.cs @@ -14,7 +14,8 @@ namespace osu.Game.Skinning { public partial class LegacyKeyCounterDisplay : KeyCounterDisplay { - private const float key_transition_time = 100; + private static readonly Colour4 active_colour_top = Colour4.FromHex(@"#ffde00"); + private static readonly Colour4 active_colour_bottom = Colour4.FromHex(@"#f8009e"); protected override FillFlowContainer KeyFlow { get; } @@ -61,12 +62,16 @@ namespace osu.Game.Skinning if (backgroundTexture != null) backgroundSprite.Texture = backgroundTexture; + + for (int i = 0; i < KeyFlow.Count; ++i) + { + ((LegacyKeyCounter)KeyFlow[i]).ActiveColour = i < 2 ? active_colour_top : active_colour_bottom; + } } protected override KeyCounter CreateCounter(InputTrigger trigger) => new LegacyKeyCounter(trigger) { - TransitionDuration = key_transition_time, - KeyTextColour = keyTextColor, + TextColour = keyTextColor, }; private Colour4 keyTextColor = Colour4.White; @@ -80,7 +85,7 @@ namespace osu.Game.Skinning { keyTextColor = value; foreach (var child in KeyFlow.Cast()) - child.KeyTextColour = value; + child.TextColour = value; } } } From c3dae81935a18eb62ebbfa7fdc2c36af5fd9fe2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 24 Jul 2024 15:41:20 +0200 Subject: [PATCH 1619/2493] Only add legacy key overlay to osu! and catch HUD layers --- osu.Game/Skinning/LegacySkin.cs | 138 +++++++++++++++++++------------- 1 file changed, 81 insertions(+), 57 deletions(-) diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index 64965874a5..4ca0e3cac0 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -356,70 +356,16 @@ namespace osu.Game.Skinning switch (lookup) { case SkinComponentsContainerLookup containerLookup: - // Only handle global level defaults for now. - if (containerLookup.Ruleset != null) - return null; switch (containerLookup.Target) { case SkinComponentsContainerLookup.TargetArea.MainHUDComponents: - return new DefaultSkinComponentsContainer(container => - { - var score = container.OfType().FirstOrDefault(); - var accuracy = container.OfType().FirstOrDefault(); + return createDefaultHUDComponents(containerLookup); - if (score != null && accuracy != null) - { - accuracy.Y = container.ToLocalSpace(score.ScreenSpaceDrawQuad.BottomRight).Y; - } - - var songProgress = container.OfType().FirstOrDefault(); - - if (songProgress != null && accuracy != null) - { - songProgress.Anchor = Anchor.TopRight; - songProgress.Origin = Anchor.CentreRight; - songProgress.X = -accuracy.ScreenSpaceDeltaToParentSpace(accuracy.ScreenSpaceDrawQuad.Size).X - 18; - songProgress.Y = container.ToLocalSpace(accuracy.ScreenSpaceDrawQuad.TopLeft).Y + (accuracy.ScreenSpaceDeltaToParentSpace(accuracy.ScreenSpaceDrawQuad.Size).Y / 2); - } - - var hitError = container.OfType().FirstOrDefault(); - - if (hitError != null) - { - hitError.Anchor = Anchor.BottomCentre; - hitError.Origin = Anchor.CentreLeft; - hitError.Rotation = -90; - } - - var keyCounter = container.OfType().FirstOrDefault(); - - if (keyCounter != null) - { - // set the anchor to top right so that it won't squash to the return button to the top - keyCounter.Anchor = Anchor.CentreRight; - keyCounter.Origin = Anchor.CentreRight; - keyCounter.X = 0; - // 340px is the default height inherit from stable - keyCounter.Y = container.ToLocalSpace(new Vector2(0, container.ScreenSpaceDrawQuad.Centre.Y - 340f)).Y; - } - }) - { - Children = new Drawable[] - { - new LegacyComboCounter(), - new LegacyScoreCounter(), - new LegacyAccuracyCounter(), - new LegacySongProgress(), - new LegacyHealthDisplay(), - new BarHitErrorMeter(), - new LegacyKeyCounterDisplay(), - } - }; + default: + return null; } - return null; - case GameplaySkinComponentLookup resultComponent: // kind of wasteful that we throw this away, but should do for now. @@ -442,6 +388,84 @@ namespace osu.Game.Skinning return null; } + private static DefaultSkinComponentsContainer? createDefaultHUDComponents(SkinComponentsContainerLookup containerLookup) + { + switch (containerLookup.Ruleset?.ShortName) + { + case null: + { + return new DefaultSkinComponentsContainer(container => + { + var score = container.OfType().FirstOrDefault(); + var accuracy = container.OfType().FirstOrDefault(); + + if (score != null && accuracy != null) + { + accuracy.Y = container.ToLocalSpace(score.ScreenSpaceDrawQuad.BottomRight).Y; + } + + var songProgress = container.OfType().FirstOrDefault(); + + if (songProgress != null && accuracy != null) + { + songProgress.Anchor = Anchor.TopRight; + songProgress.Origin = Anchor.CentreRight; + songProgress.X = -accuracy.ScreenSpaceDeltaToParentSpace(accuracy.ScreenSpaceDrawQuad.Size).X - 18; + songProgress.Y = container.ToLocalSpace(accuracy.ScreenSpaceDrawQuad.TopLeft).Y + (accuracy.ScreenSpaceDeltaToParentSpace(accuracy.ScreenSpaceDrawQuad.Size).Y / 2); + } + + var hitError = container.OfType().FirstOrDefault(); + + if (hitError != null) + { + hitError.Anchor = Anchor.BottomCentre; + hitError.Origin = Anchor.CentreLeft; + hitError.Rotation = -90; + } + }) + { + Children = new Drawable[] + { + new LegacyComboCounter(), + new LegacyScoreCounter(), + new LegacyAccuracyCounter(), + new LegacySongProgress(), + new LegacyHealthDisplay(), + new BarHitErrorMeter(), + } + }; + } + + case @"osu": + case @"fruits": + { + return new DefaultSkinComponentsContainer(container => + { + var keyCounter = container.OfType().FirstOrDefault(); + + if (keyCounter != null) + { + // set the anchor to top right so that it won't squash to the return button to the top + keyCounter.Anchor = Anchor.CentreRight; + keyCounter.Origin = Anchor.CentreRight; + keyCounter.X = 0; + // 340px is the default height inherit from stable + keyCounter.Y = container.ToLocalSpace(new Vector2(0, container.ScreenSpaceDrawQuad.Centre.Y - 340f)).Y; + } + }) + { + Children = new Drawable[] + { + new LegacyKeyCounterDisplay(), + } + }; + } + + default: + return null; + } + } + private Texture? getParticleTexture(HitResult result) { switch (result) From 12a9086aa31127b4b81f5a02cbcfe190b4159b5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 24 Jul 2024 18:30:18 +0200 Subject: [PATCH 1620/2493] Fix test failure After the legacy key counter was moved to ruleset-specific component containers, `TestSceneSkinnableHUDOverlay` no longer had a key counter, because it wasn't creating a ruleset-specific HUD component container due to https://github.com/ppy/osu/blob/4983e5f33ed11ba3777e53face6271066ba01ab9/osu.Game/Screens/Play/HUDOverlay.cs#L131-L133 Therefore, to fix, do just enough persuading to make it create one. --- .../Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs index 4cb0d5c0ff..d1e224a910 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs @@ -16,12 +16,13 @@ using osu.Framework.Testing; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.Osu.Beatmaps; +using osu.Game.Rulesets.Osu.UI; using osu.Game.Rulesets.Scoring; using osu.Game.Screens.Play; using osu.Game.Screens.Play.HUD; using osu.Game.Skinning; using osu.Game.Tests.Gameplay; -using osuTK.Input; namespace osu.Game.Tests.Visual.Gameplay { @@ -91,10 +92,7 @@ namespace osu.Game.Tests.Visual.Gameplay { SetContents(_ => { - hudOverlay = new HUDOverlay(null, Array.Empty()); - - // Add any key just to display the key counter visually. - hudOverlay.InputCountController.Add(new KeyCounterKeyboardTrigger(Key.Space)); + hudOverlay = new HUDOverlay(new DrawableOsuRuleset(new OsuRuleset(), new OsuBeatmap()), Array.Empty()); action?.Invoke(hudOverlay); From 6645dac71d418499b1d758e06fcf859b4329005f Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 24 Jul 2024 23:19:04 +0300 Subject: [PATCH 1621/2493] Fix dragging number boxes overwritten by select-all-on-focus feature --- .../UserInterface/TestSceneOsuTextBox.cs | 18 ++++++++++++++++++ osu.Game/Graphics/UserInterface/OsuTextBox.cs | 3 ++- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneOsuTextBox.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneOsuTextBox.cs index 921c5bbbfa..435fe2f7a2 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneOsuTextBox.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneOsuTextBox.cs @@ -76,6 +76,24 @@ namespace osu.Game.Tests.Visual.UserInterface InputManager.Click(MouseButton.Left); }); AddAssert("text selected", () => numberBoxes.First().SelectedText == "987654321"); + + AddStep("click away", () => + { + InputManager.MoveMouseTo(Vector2.Zero); + InputManager.Click(MouseButton.Left); + }); + + Drawable textContainer = null!; + + AddStep("move mouse to end of text", () => + { + textContainer = numberBoxes.First().ChildrenOfType().ElementAt(1); + InputManager.MoveMouseTo(textContainer.ScreenSpaceDrawQuad.TopRight); + }); + AddStep("hold mouse", () => InputManager.PressButton(MouseButton.Left)); + AddStep("drag to half", () => InputManager.MoveMouseTo(textContainer.ScreenSpaceDrawQuad.BottomRight - new Vector2(textContainer.ScreenSpaceDrawQuad.Width / 2, 0))); + AddStep("release mouse", () => InputManager.ReleaseButton(MouseButton.Left)); + AddAssert("half text selected", () => numberBoxes.First().SelectedText == "54321"); } private void clearTextboxes(IEnumerable textBoxes) => AddStep("clear textbox", () => textBoxes.ForEach(textBox => textBox.Text = null)); diff --git a/osu.Game/Graphics/UserInterface/OsuTextBox.cs b/osu.Game/Graphics/UserInterface/OsuTextBox.cs index 90a000d441..6388f56f61 100644 --- a/osu.Game/Graphics/UserInterface/OsuTextBox.cs +++ b/osu.Game/Graphics/UserInterface/OsuTextBox.cs @@ -261,7 +261,8 @@ namespace osu.Game.Graphics.UserInterface base.OnFocus(e); - if (SelectAllOnFocus) + // we may become focused from an ongoing drag operation, we don't want to overwrite selection in that case. + if (SelectAllOnFocus && string.IsNullOrEmpty(SelectedText)) SelectAll(); } From b3e3bf7ceca3b67663b38036ba3002cc4e6dc68b Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 24 Jul 2024 23:26:23 +0300 Subject: [PATCH 1622/2493] Add lenience to avoid floating point errors --- osu.Game.Tests/Visual/UserInterface/TestSceneOsuTextBox.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneOsuTextBox.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneOsuTextBox.cs index 435fe2f7a2..abad7e775c 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneOsuTextBox.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneOsuTextBox.cs @@ -91,7 +91,7 @@ namespace osu.Game.Tests.Visual.UserInterface InputManager.MoveMouseTo(textContainer.ScreenSpaceDrawQuad.TopRight); }); AddStep("hold mouse", () => InputManager.PressButton(MouseButton.Left)); - AddStep("drag to half", () => InputManager.MoveMouseTo(textContainer.ScreenSpaceDrawQuad.BottomRight - new Vector2(textContainer.ScreenSpaceDrawQuad.Width / 2, 0))); + AddStep("drag to half", () => InputManager.MoveMouseTo(textContainer.ScreenSpaceDrawQuad.BottomRight - new Vector2(textContainer.ScreenSpaceDrawQuad.Width / 2 + 1f, 0))); AddStep("release mouse", () => InputManager.ReleaseButton(MouseButton.Left)); AddAssert("half text selected", () => numberBoxes.First().SelectedText == "54321"); } From 4cc07badbd7f3c68b39456d51bfd98d0dd093b1a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 25 Jul 2024 09:05:58 +0900 Subject: [PATCH 1623/2493] Disable macOS test runs for now We are seeing update frames run as little as [once per second](https://github.com/ppy/osu/blob/aa4d16bdb873d7296b899cee7b7491ffdf5cd6ab/osu.Game/Overlays/BeatmapListingOverlay.cs#L141). Until we can ascertain why this is happening, let's reduce developer stress by not running macOS tests for now. --- .github/workflows/ci.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dc1cb6c186..ba65cfa33a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -64,7 +64,8 @@ jobs: matrix: os: - { prettyname: Windows, fullname: windows-latest } - - { prettyname: macOS, fullname: macos-latest } + # macOS runner performance has gotten unbearably slow so let's turn them off temporarily. + # - { prettyname: macOS, fullname: macos-latest } - { prettyname: Linux, fullname: ubuntu-latest } threadingMode: ['SingleThread', 'MultiThreaded'] timeout-minutes: 120 From d63335082ec4be44a40cc4386852bf876dba501c Mon Sep 17 00:00:00 2001 From: Cyrus Yip Date: Wed, 24 Jul 2024 18:24:52 -0700 Subject: [PATCH 1624/2493] fix link to good first issues --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0fe6b6fb4d..ebe1e08074 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -55,7 +55,7 @@ When in doubt, it's probably best to start with a discussion first. We will esca While pull requests from unaffiliated contributors are welcome, please note that due to significant community interest and limited review throughput, the core team's primary focus is on the issues which are currently [on the roadmap](https://github.com/orgs/ppy/projects/7/views/6). Reviewing PRs that fall outside of the scope of the roadmap is done on a best-effort basis, so please be aware that it may take a while before a core maintainer gets around to review your change. -The [issue tracker](https://github.com/ppy/osu/issues) should provide plenty of issues to start with. We also have a [`good-first-issue`](https://github.com/ppy/osu/issues?q=is%3Aissue+is%3Aopen+label%3Agood-first-issue) label, although from experience it is not used very often, as it is relatively rare that we can spot an issue that will definitively be a good first issue for a new contributor regardless of their programming experience. +The [issue tracker](https://github.com/ppy/osu/issues) should provide plenty of issues to start with. We also have a [`good first issue`](https://github.com/ppy/osu/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) label, although from experience it is not used very often, as it is relatively rare that we can spot an issue that will definitively be a good first issue for a new contributor regardless of their programming experience. In the case of simple issues, a direct PR is okay. However, if you decide to work on an existing issue which doesn't seem trivial, **please ask us first**. This way we can try to estimate if it is a good fit for you and provide the correct direction on how to address it. In addition, note that while we do not rule out external contributors from working on roadmapped issues, we will generally prefer to handle them ourselves unless they're not very time sensitive. From a696e3c2612bd079ba061e6b7715a1757dd0fbe2 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 25 Jul 2024 10:44:44 +0900 Subject: [PATCH 1625/2493] Add reference to android project --- .../osu.Game.Rulesets.Taiko.Tests.Android.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game.Rulesets.Taiko.Tests.Android/osu.Game.Rulesets.Taiko.Tests.Android.csproj b/osu.Game.Rulesets.Taiko.Tests.Android/osu.Game.Rulesets.Taiko.Tests.Android.csproj index ee973e8544..88aa137797 100644 --- a/osu.Game.Rulesets.Taiko.Tests.Android/osu.Game.Rulesets.Taiko.Tests.Android.csproj +++ b/osu.Game.Rulesets.Taiko.Tests.Android/osu.Game.Rulesets.Taiko.Tests.Android.csproj @@ -19,6 +19,7 @@ + From 9ec687caab0178cf2cb17ea9872a548bbcda70d5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 25 Jul 2024 12:55:45 +0900 Subject: [PATCH 1626/2493] Avoid reloading the daily challenge leaderboard when already requested --- .../OnlinePlay/DailyChallenge/DailyChallengeLeaderboard.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeLeaderboard.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeLeaderboard.cs index 8ba490b14d..4c4622bba3 100644 --- a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeLeaderboard.cs +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeLeaderboard.cs @@ -118,9 +118,14 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge RefetchScores(); } + private IndexPlaylistScoresRequest? request; + public void RefetchScores() { - var request = new IndexPlaylistScoresRequest(room.RoomID.Value!.Value, playlistItem.ID); + if (request?.CompletionState == APIRequestCompletionState.Waiting) + return; + + request = new IndexPlaylistScoresRequest(room.RoomID.Value!.Value, playlistItem.ID); request.Success += req => Schedule(() => { From aac98ab6b25fd5a6fe5ccf57e219eb5b4dc3431f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 25 Jul 2024 12:58:32 +0900 Subject: [PATCH 1627/2493] Debounce leaderboard refetches to stop excessive operations after returning from gameplay --- osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs index 2101444728..aab0458275 100644 --- a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs @@ -349,7 +349,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge feed.AddNewScore(ev); if (e.NewRank <= 50) - Schedule(() => leaderboard.RefetchScores()); + Scheduler.AddOnce(() => leaderboard.RefetchScores()); }); }); } @@ -486,7 +486,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge sampleStart?.Play(); this.Push(new PlayerLoader(() => new PlaylistsPlayer(room, playlistItem) { - Exited = () => leaderboard.RefetchScores() + Exited = () => Scheduler.AddOnce(() => leaderboard.RefetchScores()) })); } From 0182f3d7c3e52fd63f9267148498ee069d5644f0 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 25 Jul 2024 07:39:58 +0300 Subject: [PATCH 1628/2493] Add failing test case --- .../DailyChallenge/TestSceneDailyChallenge.cs | 29 +++++++++++++++---- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallenge.cs b/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallenge.cs index cd09a1d20f..0e0927a678 100644 --- a/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallenge.cs +++ b/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallenge.cs @@ -4,9 +4,12 @@ using System; using System.Linq; using NUnit.Framework; +using osu.Framework.Testing; using osu.Game.Online.API; using osu.Game.Online.Rooms; using osu.Game.Rulesets.Osu.Mods; +using osu.Game.Screens.Ranking; +using osu.Game.Screens.SelectV2.Leaderboards; using osu.Game.Tests.Resources; using osu.Game.Tests.Visual.OnlinePlay; @@ -14,10 +17,14 @@ namespace osu.Game.Tests.Visual.DailyChallenge { public partial class TestSceneDailyChallenge : OnlinePlayTestScene { - [Test] - public void TestDailyChallenge() + [SetUpSteps] + public override void SetUpSteps() { - var room = new Room + base.SetUpSteps(); + + Room room = null!; + + AddStep("add room", () => API.Perform(new CreateRoomRequest(room = new Room { RoomID = { Value = 1234 }, Name = { Value = "Daily Challenge: June 4, 2024" }, @@ -31,10 +38,22 @@ namespace osu.Game.Tests.Visual.DailyChallenge }, EndDate = { Value = DateTimeOffset.Now.AddHours(12) }, Category = { Value = RoomCategory.DailyChallenge } - }; + }))); - AddStep("add room", () => API.Perform(new CreateRoomRequest(room))); AddStep("push screen", () => LoadScreen(new Screens.OnlinePlay.DailyChallenge.DailyChallenge(room))); } + + [Test] + public void TestDailyChallenge() + { + } + + [Test] + public void TestScoreNavigation() + { + AddStep("click on score", () => this.ChildrenOfType().First().TriggerClick()); + AddUntilStep("wait for load", () => Stack.CurrentScreen is ResultsScreen results && results.IsLoaded); + AddAssert("replay download button exists", () => this.ChildrenOfType().Count(), () => Is.EqualTo(1)); + } } } From dad8e28446fe82940d0fd5cbb8e99f63d083c5de Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 25 Jul 2024 07:40:17 +0300 Subject: [PATCH 1629/2493] Fix replay download button not added when no score is selected initially --- osu.Game/Screens/Ranking/ReplayDownloadButton.cs | 10 +++++----- osu.Game/Screens/Ranking/ResultsScreen.cs | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/osu.Game/Screens/Ranking/ReplayDownloadButton.cs b/osu.Game/Screens/Ranking/ReplayDownloadButton.cs index df5f9c7a8a..aac29ad269 100644 --- a/osu.Game/Screens/Ranking/ReplayDownloadButton.cs +++ b/osu.Game/Screens/Ranking/ReplayDownloadButton.cs @@ -18,7 +18,7 @@ namespace osu.Game.Screens.Ranking { public partial class ReplayDownloadButton : CompositeDrawable, IKeyBindingHandler { - public readonly Bindable Score = new Bindable(); + public readonly Bindable Score = new Bindable(); protected readonly Bindable State = new Bindable(); @@ -44,7 +44,7 @@ namespace osu.Game.Screens.Ranking } } - public ReplayDownloadButton(ScoreInfo score) + public ReplayDownloadButton(ScoreInfo? score) { Score.Value = score; Size = new Vector2(50, 30); @@ -67,11 +67,11 @@ namespace osu.Game.Screens.Ranking switch (State.Value) { case DownloadState.LocallyAvailable: - game?.PresentScore(Score.Value, ScorePresentType.Gameplay); + game?.PresentScore(Score.Value!, ScorePresentType.Gameplay); break; case DownloadState.NotDownloaded: - scoreDownloader.Download(Score.Value); + scoreDownloader.Download(Score.Value!); break; case DownloadState.Importing: @@ -147,7 +147,7 @@ namespace osu.Game.Screens.Ranking { if (state.NewValue != DownloadState.LocallyAvailable) return; - scoreManager.Export(Score.Value); + scoreManager.Export(Score.Value!); State.ValueChanged -= exportWhenReady; } diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs index 44b270db53..0793697833 100644 --- a/osu.Game/Screens/Ranking/ResultsScreen.cs +++ b/osu.Game/Screens/Ranking/ResultsScreen.cs @@ -179,11 +179,11 @@ namespace osu.Game.Screens.Ranking Scheduler.AddDelayed(() => OverlayActivationMode.Value = OverlayActivation.All, shouldFlair ? AccuracyCircle.TOTAL_DURATION + 1000 : 0); } - if (SelectedScore.Value != null && AllowWatchingReplay) + if (AllowWatchingReplay) { buttons.Add(new ReplayDownloadButton(SelectedScore.Value) { - Score = { BindTarget = SelectedScore! }, + Score = { BindTarget = SelectedScore }, Width = 300 }); } From bba151a776bb910c0db8514aa66564130e4a6ead Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 25 Jul 2024 14:24:04 +0900 Subject: [PATCH 1630/2493] Make event feed test show more realistic content automatically --- .../TestSceneDailyChallengeEventFeed.cs | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeEventFeed.cs b/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeEventFeed.cs index d9a8ccd510..77ae5b653a 100644 --- a/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeEventFeed.cs +++ b/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeEventFeed.cs @@ -35,6 +35,7 @@ namespace osu.Game.Tests.Visual.DailyChallenge feed = new DailyChallengeEventFeed { RelativeSizeAxes = Axes.Both, + Height = 0.3f, Anchor = Anchor.Centre, Origin = Anchor.Centre, } @@ -44,13 +45,13 @@ namespace osu.Game.Tests.Visual.DailyChallenge if (feed.IsNotNull()) feed.Width = width; }); - AddSliderStep("adjust height", 0.1f, 1, 1, height => + AddSliderStep("adjust height", 0.1f, 1, 0.3f, height => { if (feed.IsNotNull()) feed.Height = height; }); - AddStep("add normal score", () => + AddRepeatStep("add normal score", () => { var ev = new NewScoreEvent(1, new APIUser { @@ -60,9 +61,9 @@ namespace osu.Game.Tests.Visual.DailyChallenge }, RNG.Next(1_000_000), null); feed.AddNewScore(ev); - }); + }, 50); - AddStep("add new user best", () => + AddRepeatStep("add new user best", () => { var ev = new NewScoreEvent(1, new APIUser { @@ -75,9 +76,9 @@ namespace osu.Game.Tests.Visual.DailyChallenge testScore.TotalScore = RNG.Next(1_000_000); feed.AddNewScore(ev); - }); + }, 50); - AddStep("add top 10 score", () => + AddRepeatStep("add top 10 score", () => { var ev = new NewScoreEvent(1, new APIUser { @@ -87,7 +88,7 @@ namespace osu.Game.Tests.Visual.DailyChallenge }, RNG.Next(1_000_000), RNG.Next(1, 10)); feed.AddNewScore(ev); - }); + }, 50); } } } From c90d345ff98ec5ba15ffcc1cc2c0a46cf193b352 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 25 Jul 2024 14:20:17 +0900 Subject: [PATCH 1631/2493] Scroll content forever rather than aggressively fading --- .../OnlinePlay/DailyChallenge/DailyChallengeEventFeed.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeEventFeed.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeEventFeed.cs index e76238abad..24e530223e 100644 --- a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeEventFeed.cs +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeEventFeed.cs @@ -54,7 +54,6 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge PresentScore = PresentScore, }; flow.Add(row); - row.Delay(15000).Then().FadeOut(300, Easing.OutQuint).Expire(); } protected override void Update() @@ -65,6 +64,8 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge { var row = flow[i]; + row.Alpha = Math.Max(0, (row.Y + flow.DrawHeight) / flow.DrawHeight); + if (row.Y < -flow.DrawHeight) { row.RemoveAndDisposeImmediately(); From f1dda4ab1ed5c040569c4224389abfe4b30290b4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 25 Jul 2024 14:28:09 +0900 Subject: [PATCH 1632/2493] Fix too many event rows displaying after spending a long time in gameplay/results --- .../TestSceneDailyChallengeEventFeed.cs | 33 ++++++++++++++++--- .../DailyChallenge/DailyChallengeEventFeed.cs | 24 ++++++++++---- 2 files changed, 46 insertions(+), 11 deletions(-) diff --git a/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeEventFeed.cs b/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeEventFeed.cs index 77ae5b653a..4b784f661d 100644 --- a/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeEventFeed.cs +++ b/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeEventFeed.cs @@ -6,6 +6,7 @@ using osu.Framework.Allocation; using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Shapes; +using osu.Framework.Testing; using osu.Framework.Utils; using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays; @@ -17,14 +18,14 @@ namespace osu.Game.Tests.Visual.DailyChallenge { public partial class TestSceneDailyChallengeEventFeed : OsuTestScene { + private DailyChallengeEventFeed feed = null!; + [Cached] private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Plum); - [Test] - public void TestBasicAppearance() + [SetUpSteps] + public void SetUpSteps() { - DailyChallengeEventFeed feed = null!; - AddStep("create content", () => Children = new Drawable[] { new Box @@ -40,6 +41,7 @@ namespace osu.Game.Tests.Visual.DailyChallenge Origin = Anchor.Centre, } }); + AddSliderStep("adjust width", 0.1f, 1, 1, width => { if (feed.IsNotNull()) @@ -50,7 +52,11 @@ namespace osu.Game.Tests.Visual.DailyChallenge if (feed.IsNotNull()) feed.Height = height; }); + } + [Test] + public void TestBasicAppearance() + { AddRepeatStep("add normal score", () => { var ev = new NewScoreEvent(1, new APIUser @@ -90,5 +96,24 @@ namespace osu.Game.Tests.Visual.DailyChallenge feed.AddNewScore(ev); }, 50); } + + [Test] + public void TestMassAdd() + { + AddStep("add 1000 scores at once", () => + { + for (int i = 0; i < 1000; i++) + { + var ev = new NewScoreEvent(1, new APIUser + { + Id = 2, + Username = "peppy", + CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", + }, RNG.Next(1_000_000), null); + + feed.AddNewScore(ev); + } + }); + } } } diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeEventFeed.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeEventFeed.cs index 24e530223e..c23deec8ac 100644 --- a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeEventFeed.cs +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeEventFeed.cs @@ -22,6 +22,8 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge public Action? PresentScore { get; init; } + private readonly Queue newScores = new Queue(); + [BackgroundDependencyLoader] private void load() { @@ -47,19 +49,27 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge public void AddNewScore(NewScoreEvent newScoreEvent) { - var row = new NewScoreEventRow(newScoreEvent) - { - Anchor = Anchor.BottomCentre, - Origin = Anchor.BottomCentre, - PresentScore = PresentScore, - }; - flow.Add(row); + newScores.Enqueue(newScoreEvent); + + // ensure things don't get too out-of-hand. + if (newScores.Count > 25) + newScores.Dequeue(); } protected override void Update() { base.Update(); + while (newScores.TryDequeue(out var newScore)) + { + flow.Add(new NewScoreEventRow(newScore) + { + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + PresentScore = PresentScore, + }); + } + for (int i = 0; i < flow.Count; ++i) { var row = flow[i]; From e1ccf688019cd871e02aed858e07e0439243879d Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 25 Jul 2024 08:51:48 +0300 Subject: [PATCH 1633/2493] Revert "Add failing test case" This reverts commit 0182f3d7c3e52fd63f9267148498ee069d5644f0. --- .../DailyChallenge/TestSceneDailyChallenge.cs | 29 ++++--------------- 1 file changed, 5 insertions(+), 24 deletions(-) diff --git a/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallenge.cs b/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallenge.cs index 0e0927a678..cd09a1d20f 100644 --- a/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallenge.cs +++ b/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallenge.cs @@ -4,12 +4,9 @@ using System; using System.Linq; using NUnit.Framework; -using osu.Framework.Testing; using osu.Game.Online.API; using osu.Game.Online.Rooms; using osu.Game.Rulesets.Osu.Mods; -using osu.Game.Screens.Ranking; -using osu.Game.Screens.SelectV2.Leaderboards; using osu.Game.Tests.Resources; using osu.Game.Tests.Visual.OnlinePlay; @@ -17,14 +14,10 @@ namespace osu.Game.Tests.Visual.DailyChallenge { public partial class TestSceneDailyChallenge : OnlinePlayTestScene { - [SetUpSteps] - public override void SetUpSteps() + [Test] + public void TestDailyChallenge() { - base.SetUpSteps(); - - Room room = null!; - - AddStep("add room", () => API.Perform(new CreateRoomRequest(room = new Room + var room = new Room { RoomID = { Value = 1234 }, Name = { Value = "Daily Challenge: June 4, 2024" }, @@ -38,22 +31,10 @@ namespace osu.Game.Tests.Visual.DailyChallenge }, EndDate = { Value = DateTimeOffset.Now.AddHours(12) }, Category = { Value = RoomCategory.DailyChallenge } - }))); + }; + AddStep("add room", () => API.Perform(new CreateRoomRequest(room))); AddStep("push screen", () => LoadScreen(new Screens.OnlinePlay.DailyChallenge.DailyChallenge(room))); } - - [Test] - public void TestDailyChallenge() - { - } - - [Test] - public void TestScoreNavigation() - { - AddStep("click on score", () => this.ChildrenOfType().First().TriggerClick()); - AddUntilStep("wait for load", () => Stack.CurrentScreen is ResultsScreen results && results.IsLoaded); - AddAssert("replay download button exists", () => this.ChildrenOfType().Count(), () => Is.EqualTo(1)); - } } } From 9d5fbb8b4fa1739dd07b16eab84663a826eacc95 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 25 Jul 2024 07:59:33 +0300 Subject: [PATCH 1634/2493] Fix target score selection abruptly discarded after opening results screen --- .../OnlinePlay/Playlists/PlaylistItemScoreResultsScreen.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemScoreResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemScoreResultsScreen.cs index 831b6538a7..32be7f21b0 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemScoreResultsScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemScoreResultsScreen.cs @@ -29,7 +29,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists { var scoreInfos = base.PerformSuccessCallback(callback, scores, pivot); - Schedule(() => SelectedScore.Value = scoreInfos.SingleOrDefault(score => score.OnlineID == scoreId)); + Schedule(() => SelectedScore.Value ??= scoreInfos.SingleOrDefault(score => score.OnlineID == scoreId)); return scoreInfos; } From f3dd1facf15935fcbd93b1b5383e5a69997170b4 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 25 Jul 2024 08:38:20 +0300 Subject: [PATCH 1635/2493] Add failing test case --- .../Visual/Playlists/TestScenePlaylistsResultsScreen.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs index a52d29a120..7527647b9c 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs @@ -135,7 +135,7 @@ namespace osu.Game.Tests.Visual.Playlists AddAssert("right loading spinner shown", () => resultsScreen.RightSpinner.State.Value == Visibility.Visible); waitForDisplay(); - AddAssert($"count increased by {scores_per_result}", () => this.ChildrenOfType().Count() >= beforePanelCount + scores_per_result); + AddAssert($"count increased by {scores_per_result}", () => this.ChildrenOfType().Count() == beforePanelCount + scores_per_result); AddAssert("right loading spinner hidden", () => resultsScreen.RightSpinner.State.Value == Visibility.Hidden); } } @@ -156,7 +156,7 @@ namespace osu.Game.Tests.Visual.Playlists AddAssert("right loading spinner shown", () => resultsScreen.RightSpinner.State.Value == Visibility.Visible); waitForDisplay(); - AddAssert($"count increased by {scores_per_result}", () => this.ChildrenOfType().Count() >= beforePanelCount + scores_per_result); + AddAssert($"count increased by {scores_per_result}", () => this.ChildrenOfType().Count() == beforePanelCount + scores_per_result); AddAssert("right loading spinner hidden", () => resultsScreen.RightSpinner.State.Value == Visibility.Hidden); AddStep("get panel count", () => beforePanelCount = this.ChildrenOfType().Count()); @@ -191,7 +191,7 @@ namespace osu.Game.Tests.Visual.Playlists AddAssert("left loading spinner shown", () => resultsScreen.LeftSpinner.State.Value == Visibility.Visible); waitForDisplay(); - AddAssert($"count increased by {scores_per_result}", () => this.ChildrenOfType().Count() >= beforePanelCount + scores_per_result); + AddAssert($"count increased by {scores_per_result}", () => this.ChildrenOfType().Count() == beforePanelCount + scores_per_result); AddAssert("left loading spinner hidden", () => resultsScreen.LeftSpinner.State.Value == Visibility.Hidden); } } From 615f07d54cdb6db9ae7c58fddbb48c1ff3ed63ae Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 25 Jul 2024 09:09:53 +0300 Subject: [PATCH 1636/2493] Fix results screen fetching more scores twice --- osu.Game/Screens/Ranking/ResultsScreen.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs index 44b270db53..4fdfc2beb8 100644 --- a/osu.Game/Screens/Ranking/ResultsScreen.cs +++ b/osu.Game/Screens/Ranking/ResultsScreen.cs @@ -267,7 +267,8 @@ namespace osu.Game.Screens.Ranking foreach (var s in scores) addScore(s); - lastFetchCompleted = true; + // allow a frame for scroll container to adjust its dimensions with the added scores before fetching again. + Schedule(() => lastFetchCompleted = true); if (ScorePanelList.IsEmpty) { From 8d89557ab88b0bb0508626d91fd69276b0ef0eff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 25 Jul 2024 11:11:54 +0200 Subject: [PATCH 1637/2493] Fix not being able to send chat reports on daily challenge screen Something something some people cannot be trusted with a textbox. --- .../DailyChallenge/DailyChallenge.cs | 271 +++++++++--------- 1 file changed, 138 insertions(+), 133 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs index aab0458275..235361dfaa 100644 --- a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs @@ -13,6 +13,7 @@ using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Shapes; using osu.Framework.Logging; using osu.Framework.Screens; @@ -126,169 +127,173 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge RelativeSizeAxes = Axes.Both, }, new Header(ButtonSystemStrings.DailyChallenge.ToSentence(), null), - new GridContainer + new PopoverContainer { RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding + Child = new GridContainer { - Horizontal = WaveOverlayContainer.WIDTH_PADDING, - Top = Header.HEIGHT, - }, - RowDimensions = - [ - new Dimension(GridSizeMode.AutoSize), - new Dimension(GridSizeMode.Absolute, 10), - new Dimension(), - new Dimension(GridSizeMode.Absolute, 30), - new Dimension(GridSizeMode.Absolute, 50) - ], - Content = new[] - { - new Drawable[] + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { - new DrawableRoomPlaylistItem(playlistItem) - { - RelativeSizeAxes = Axes.X, - AllowReordering = false, - Scale = new Vector2(1.4f), - Width = 1 / 1.4f, - } + Horizontal = WaveOverlayContainer.WIDTH_PADDING, + Top = Header.HEIGHT, }, - null, + RowDimensions = [ - new Container + new Dimension(GridSizeMode.AutoSize), + new Dimension(GridSizeMode.Absolute, 10), + new Dimension(), + new Dimension(GridSizeMode.Absolute, 30), + new Dimension(GridSizeMode.Absolute, 50) + ], + Content = new[] + { + new Drawable[] { - RelativeSizeAxes = Axes.Both, - Masking = true, - CornerRadius = 10, - Children = new Drawable[] + new DrawableRoomPlaylistItem(playlistItem) { - new Box + RelativeSizeAxes = Axes.X, + AllowReordering = false, + Scale = new Vector2(1.4f), + Width = 1 / 1.4f, + } + }, + null, + [ + new Container + { + RelativeSizeAxes = Axes.Both, + Masking = true, + CornerRadius = 10, + Children = new Drawable[] { - RelativeSizeAxes = Axes.Both, - Colour = colourProvider.Background4, - }, - new GridContainer - { - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding(10), - ColumnDimensions = - [ - new Dimension(), - new Dimension(GridSizeMode.Absolute, 10), - new Dimension(), - new Dimension(GridSizeMode.Absolute, 10), - new Dimension() - ], - Content = new[] + new Box { - new Drawable?[] + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background4, + }, + new GridContainer + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding(10), + ColumnDimensions = + [ + new Dimension(), + new Dimension(GridSizeMode.Absolute, 10), + new Dimension(), + new Dimension(GridSizeMode.Absolute, 10), + new Dimension() + ], + Content = new[] { - new GridContainer + new Drawable?[] { - RelativeSizeAxes = Axes.Both, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RowDimensions = - [ - new Dimension(), - new Dimension() - ], - Content = new[] + new GridContainer { - new Drawable[] - { - new DailyChallengeCarousel - { - RelativeSizeAxes = Axes.Both, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Children = new Drawable[] - { - new DailyChallengeTimeRemainingRing(), - breakdown = new DailyChallengeScoreBreakdown(), - } - } - }, + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RowDimensions = [ - feed = new DailyChallengeEventFeed - { - RelativeSizeAxes = Axes.Both, - PresentScore = presentScore - } + new Dimension(), + new Dimension() ], - }, - }, - null, - // Middle column (leaderboard) - leaderboard = new DailyChallengeLeaderboard(room, playlistItem) - { - RelativeSizeAxes = Axes.Both, - PresentScore = presentScore, - }, - // Spacer - null, - // Main right column - new GridContainer - { - RelativeSizeAxes = Axes.Both, - Content = new[] - { - new Drawable[] + Content = new[] { - new SectionHeader("Chat") + new Drawable[] + { + new DailyChallengeCarousel + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Children = new Drawable[] + { + new DailyChallengeTimeRemainingRing(), + breakdown = new DailyChallengeScoreBreakdown(), + } + } + }, + [ + feed = new DailyChallengeEventFeed + { + RelativeSizeAxes = Axes.Both, + PresentScore = presentScore + } + ], }, - [new MatchChatDisplay(room) { RelativeSizeAxes = Axes.Both }] }, - RowDimensions = - [ - new Dimension(GridSizeMode.AutoSize), - new Dimension() - ] - }, + null, + // Middle column (leaderboard) + leaderboard = new DailyChallengeLeaderboard(room, playlistItem) + { + RelativeSizeAxes = Axes.Both, + PresentScore = presentScore, + }, + // Spacer + null, + // Main right column + new GridContainer + { + RelativeSizeAxes = Axes.Both, + Content = new[] + { + new Drawable[] + { + new SectionHeader("Chat") + }, + [new MatchChatDisplay(room) { RelativeSizeAxes = Axes.Both }] + }, + RowDimensions = + [ + new Dimension(GridSizeMode.AutoSize), + new Dimension() + ] + }, + } } } } } - } - ], - null, - [ - new Container - { - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding + ], + null, + [ + new Container { - Horizontal = -WaveOverlayContainer.WIDTH_PADDING, - }, - Children = new Drawable[] - { - new Box + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { - RelativeSizeAxes = Axes.Both, - Colour = colourProvider.Background5, + Horizontal = -WaveOverlayContainer.WIDTH_PADDING, }, - footerButtons = new FillFlowContainer + Children = new Drawable[] { - RelativeSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Padding = new MarginPadding(5), - Spacing = new Vector2(10), - Children = new Drawable[] + new Box { - new PlaylistsReadyButton + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background5, + }, + footerButtons = new FillFlowContainer + { + RelativeSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Padding = new MarginPadding(5), + Spacing = new Vector2(10), + Children = new Drawable[] { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Y, - Size = new Vector2(250, 1), - Action = startPlay + new PlaylistsReadyButton + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Y, + Size = new Vector2(250, 1), + Action = startPlay + } } - } - }, + }, + } } - } - ], + ], + } } } } From 8dbd4d70ff36734c1af64471eed86932dfb55ea7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 25 Jul 2024 11:42:56 +0200 Subject: [PATCH 1638/2493] Fix crash when toggling extended statistics visibility during results load Closes https://github.com/ppy/osu/issues/29066. Initially I fixed this at where the assert is right now: https://github.com/ppy/osu/blob/9790c5a574b782c41c8c6da99ad8c42dfadc9de8/osu.Game/Screens/Ranking/ResultsScreen.cs#L333 but because of the weird way that visible state management is done in this screen that made it possible for the extended statistics to be visible *behind* the score panels, without the score panels making way for it. So this is in a way safer, because it prevents the visibility state of the extended statistics from changing in the first place if there is no score selected (yet). This can be also seen in playlists, at least. --- osu.Game/Screens/Ranking/ResultsScreen.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs index 44b270db53..283a74e35f 100644 --- a/osu.Game/Screens/Ranking/ResultsScreen.cs +++ b/osu.Game/Screens/Ranking/ResultsScreen.cs @@ -398,7 +398,8 @@ namespace osu.Game.Screens.Ranking break; case GlobalAction.Select: - StatisticsPanel.ToggleVisibility(); + if (SelectedScore.Value != null) + StatisticsPanel.ToggleVisibility(); return true; } From e564b1dc9e470cc77af1ba38eacb8b3c08966409 Mon Sep 17 00:00:00 2001 From: Caiyi Shyu Date: Thu, 25 Jul 2024 18:23:01 +0800 Subject: [PATCH 1639/2493] Fix cursor trail alignment issue with UI panels - Convert cursor trail coordinates to local space before storing. - Apply necessary transformations to align with other UI elements. - Ensure cursor trail remains connected during UI panel movements. --- osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs b/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs index 30a77db5a1..49e4ee18c1 100644 --- a/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs +++ b/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs @@ -193,7 +193,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor private void addPart(Vector2 screenSpacePosition) { - parts[currentIndex].Position = screenSpacePosition; + parts[currentIndex].Position = ToLocalSpace(screenSpacePosition); parts[currentIndex].Time = time + 1; ++parts[currentIndex].InvalidationID; @@ -285,9 +285,11 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor if (time - part.Time >= 1) continue; + Vector2 screenSpacePos = Source.ToScreenSpace(part.Position); + vertexBatch.Add(new TexturedTrailVertex { - Position = new Vector2(part.Position.X - size.X * originPosition.X, part.Position.Y + size.Y * (1 - originPosition.Y)), + Position = new Vector2(screenSpacePos.X - size.X * originPosition.X, screenSpacePos.Y + size.Y * (1 - originPosition.Y)), TexturePosition = textureRect.BottomLeft, TextureRect = new Vector4(0, 0, 1, 1), Colour = DrawColourInfo.Colour.BottomLeft.Linear, @@ -296,7 +298,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor vertexBatch.Add(new TexturedTrailVertex { - Position = new Vector2(part.Position.X + size.X * (1 - originPosition.X), part.Position.Y + size.Y * (1 - originPosition.Y)), + Position = new Vector2(screenSpacePos.X + size.X * (1 - originPosition.X), screenSpacePos.Y + size.Y * (1 - originPosition.Y)), TexturePosition = textureRect.BottomRight, TextureRect = new Vector4(0, 0, 1, 1), Colour = DrawColourInfo.Colour.BottomRight.Linear, @@ -305,7 +307,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor vertexBatch.Add(new TexturedTrailVertex { - Position = new Vector2(part.Position.X + size.X * (1 - originPosition.X), part.Position.Y - size.Y * originPosition.Y), + Position = new Vector2(screenSpacePos.X + size.X * (1 - originPosition.X), screenSpacePos.Y - size.Y * originPosition.Y), TexturePosition = textureRect.TopRight, TextureRect = new Vector4(0, 0, 1, 1), Colour = DrawColourInfo.Colour.TopRight.Linear, @@ -314,7 +316,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor vertexBatch.Add(new TexturedTrailVertex { - Position = new Vector2(part.Position.X - size.X * originPosition.X, part.Position.Y - size.Y * originPosition.Y), + Position = new Vector2(screenSpacePos.X - size.X * originPosition.X, screenSpacePos.Y - size.Y * originPosition.Y), TexturePosition = textureRect.TopLeft, TextureRect = new Vector4(0, 0, 1, 1), Colour = DrawColourInfo.Colour.TopLeft.Linear, From 3bb30d7ff93edbf655e67dd6c25edb0516d300c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 25 Jul 2024 13:06:18 +0200 Subject: [PATCH 1640/2493] Fix several missing properties on `MultiplayerScore` You wouldn't think this would be an actual thing that can happen to us, but it is. The most important one by far is `MaximumStatistics`; that is the root cause behind why stuff like spinner ticks or slider tails wasn't showing. On a better day we should probably do cleanup to unify these models better, but today is not that day. --- osu.Game/Online/Rooms/MultiplayerScore.cs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/osu.Game/Online/Rooms/MultiplayerScore.cs b/osu.Game/Online/Rooms/MultiplayerScore.cs index f1b9584d57..faa66c571d 100644 --- a/osu.Game/Online/Rooms/MultiplayerScore.cs +++ b/osu.Game/Online/Rooms/MultiplayerScore.cs @@ -46,6 +46,9 @@ namespace osu.Game.Online.Rooms [JsonProperty("statistics")] public Dictionary Statistics = new Dictionary(); + [JsonProperty("maximum_statistics")] + public Dictionary MaximumStatistics = new Dictionary(); + [JsonProperty("passed")] public bool Passed { get; set; } @@ -58,9 +61,15 @@ namespace osu.Game.Online.Rooms [JsonProperty("position")] public int? Position { get; set; } + [JsonProperty("pp")] + public double? PP { get; set; } + [JsonProperty("has_replay")] public bool HasReplay { get; set; } + [JsonProperty("ranked")] + public bool Ranked { get; set; } + /// /// Any scores in the room around this score. /// @@ -83,13 +92,17 @@ namespace osu.Game.Online.Rooms MaxCombo = MaxCombo, BeatmapInfo = beatmap, Ruleset = rulesets.GetRuleset(playlistItem.RulesetID) ?? throw new InvalidOperationException($"Ruleset with ID of {playlistItem.RulesetID} not found locally"), + Passed = Passed, Statistics = Statistics, + MaximumStatistics = MaximumStatistics, User = User, Accuracy = Accuracy, Date = EndedAt, HasOnlineReplay = HasReplay, Rank = Rank, Mods = Mods?.Select(m => m.ToMod(rulesetInstance)).ToArray() ?? Array.Empty(), + PP = PP, + Ranked = Ranked, Position = Position, }; From 3e8917cadb46c051bc2b399fababf491cd08c298 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 26 Jul 2024 05:08:13 +0300 Subject: [PATCH 1641/2493] Add test case against resetting score in download button --- .../Visual/Gameplay/TestSceneReplayDownloadButton.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayDownloadButton.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayDownloadButton.cs index 5b32f380b9..061e8ea7e1 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayDownloadButton.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayDownloadButton.cs @@ -117,6 +117,9 @@ namespace osu.Game.Tests.Visual.Gameplay AddAssert("state entered downloading", () => downloadStarted); AddUntilStep("state left downloading", () => downloadFinished); + + AddStep("change score to null", () => downloadButton.Score.Value = null); + AddUntilStep("state changed to unknown", () => downloadButton.State.Value, () => Is.EqualTo(DownloadState.Unknown)); } [Test] From c558dfdf138523482dfed8640d5f276e648af970 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 26 Jul 2024 05:11:54 +0300 Subject: [PATCH 1642/2493] Reset download state when score is changed --- osu.Game/Screens/Ranking/ReplayDownloadButton.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Screens/Ranking/ReplayDownloadButton.cs b/osu.Game/Screens/Ranking/ReplayDownloadButton.cs index aac29ad269..5e2161c251 100644 --- a/osu.Game/Screens/Ranking/ReplayDownloadButton.cs +++ b/osu.Game/Screens/Ranking/ReplayDownloadButton.cs @@ -88,6 +88,8 @@ namespace osu.Game.Screens.Ranking State.ValueChanged -= exportWhenReady; downloadTracker?.RemoveAndDisposeImmediately(); + downloadTracker = null; + State.SetDefault(); if (score.NewValue != null) { From c17cabd98136f94ffd3b3854cc9c85d191b47ac3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 26 Jul 2024 07:44:02 +0200 Subject: [PATCH 1643/2493] Adjust alpha for rows for better visibility --- .../OnlinePlay/DailyChallenge/DailyChallengeEventFeed.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeEventFeed.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeEventFeed.cs index c23deec8ac..160ad83c8a 100644 --- a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeEventFeed.cs +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeEventFeed.cs @@ -7,6 +7,7 @@ using System.Linq; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Utils; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; @@ -74,7 +75,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge { var row = flow[i]; - row.Alpha = Math.Max(0, (row.Y + flow.DrawHeight) / flow.DrawHeight); + row.Alpha = Interpolation.ValueAt(Math.Clamp(row.Y + flow.DrawHeight, 0, flow.DrawHeight), 0f, 1f, 0, flow.DrawHeight, Easing.Out); if (row.Y < -flow.DrawHeight) { From 662e9eab8c77bd49be3d557d437ac497464a8d31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 26 Jul 2024 08:07:27 +0200 Subject: [PATCH 1644/2493] Don't force exit to main menu when presenting scores from within online screens Struck me as weird when reviewing https://github.com/ppy/osu/pull/29057. Like sure, that PR adds the replay button, but it's a bit terrible that clicking the button quits the daily challenge screen and you're back at main menu when done watching...? Also extended to cover playlists and multiplayer, which have the same issue. --- osu.Game/OsuGame.cs | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 388a98d947..2195576be1 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -63,6 +63,8 @@ using osu.Game.Screens; using osu.Game.Screens.Edit; using osu.Game.Screens.Footer; using osu.Game.Screens.Menu; +using osu.Game.Screens.OnlinePlay; +using osu.Game.Screens.OnlinePlay.DailyChallenge; using osu.Game.Screens.OnlinePlay.Multiplayer; using osu.Game.Screens.Play; using osu.Game.Screens.Ranking; @@ -749,23 +751,34 @@ namespace osu.Game return; } - // This should be able to be performed from song select, but that is disabled for now + // This should be able to be performed from song select always, but that is disabled for now // due to the weird decoupled ruleset logic (which can cause a crash in certain filter scenarios). // // As a special case, if the beatmap and ruleset already match, allow immediately displaying the score from song select. // This is guaranteed to not crash, and feels better from a user's perspective (ie. if they are clicking a score in the // song select leaderboard). + // Similar exemptions are made here for online flows where there are good chances that beatmap and ruleset match + // (playlists / multiplayer / daily challenge). IEnumerable validScreens = Beatmap.Value.BeatmapInfo.Equals(databasedBeatmap) && Ruleset.Value.Equals(databasedScore.ScoreInfo.Ruleset) - ? new[] { typeof(SongSelect) } + ? new[] { typeof(SongSelect), typeof(OnlinePlayScreen), typeof(DailyChallenge) } : Array.Empty(); PerformFromScreen(screen => { Logger.Log($"{nameof(PresentScore)} updating beatmap ({databasedBeatmap}) and ruleset ({databasedScore.ScoreInfo.Ruleset}) to match score"); - Ruleset.Value = databasedScore.ScoreInfo.Ruleset; - Beatmap.Value = BeatmapManager.GetWorkingBeatmap(databasedBeatmap); + // some screens (mostly online) disable the ruleset/beatmap bindable. + // attempting to set the ruleset/beatmap in that state will crash. + // however, the `validScreens` pre-check above should ensure that we actually never come from one of those screens + // while simultaneously having mismatched ruleset/beatmap. + // therefore this is just a safety against touching the possibly-disabled bindables if we don't actually have to touch them. + // if it ever fails, then this probably *should* crash anyhow (so that we can fix it). + if (!Ruleset.Value.Equals(databasedScore.ScoreInfo.Ruleset)) + Ruleset.Value = databasedScore.ScoreInfo.Ruleset; + + if (!Beatmap.Value.BeatmapInfo.Equals(databasedBeatmap)) + Beatmap.Value = BeatmapManager.GetWorkingBeatmap(databasedBeatmap); switch (presentType) { From 174dc91f4ba0f34f9bfe5772157896eb75affb2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 26 Jul 2024 09:49:36 +0200 Subject: [PATCH 1645/2493] Implement component for displaying running totals in daily challenge Total pass count and cumulative total score, to be more precise. --- .../TestSceneDailyChallengeTotalsDisplay.cs | 86 ++++++++++++ .../DailyChallengeTotalsDisplay.cs | 126 ++++++++++++++++++ 2 files changed, 212 insertions(+) create mode 100644 osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeTotalsDisplay.cs create mode 100644 osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeTotalsDisplay.cs diff --git a/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeTotalsDisplay.cs b/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeTotalsDisplay.cs new file mode 100644 index 0000000000..ba5a0989d4 --- /dev/null +++ b/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeTotalsDisplay.cs @@ -0,0 +1,86 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Extensions.ObjectExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Utils; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Overlays; +using osu.Game.Screens.OnlinePlay.DailyChallenge; +using osu.Game.Screens.OnlinePlay.DailyChallenge.Events; +using osu.Game.Tests.Resources; + +namespace osu.Game.Tests.Visual.DailyChallenge +{ + public partial class TestSceneDailyChallengeTotalsDisplay : OsuTestScene + { + [Cached] + private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Plum); + + [Test] + public void TestBasicAppearance() + { + DailyChallengeTotalsDisplay totals = null!; + + AddStep("create content", () => Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background4, + }, + totals = new DailyChallengeTotalsDisplay + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + } + }); + AddSliderStep("adjust width", 0.1f, 1, 1, width => + { + if (totals.IsNotNull()) + totals.Width = width; + }); + AddSliderStep("adjust height", 0.1f, 1, 1, height => + { + if (totals.IsNotNull()) + totals.Height = height; + }); + + AddStep("set counts", () => totals.SetInitialCounts(totalPassCount: 9650, cumulativeTotalScore: 10_000_000_000)); + + AddStep("add normal score", () => + { + var ev = new NewScoreEvent(1, new APIUser + { + Id = 2, + Username = "peppy", + CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", + }, RNG.Next(1_000_000), null); + + totals.AddNewScore(ev); + }); + + AddStep("spam scores", () => + { + for (int i = 0; i < 1000; ++i) + { + var ev = new NewScoreEvent(1, new APIUser + { + Id = 2, + Username = "peppy", + CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", + }, RNG.Next(1_000_000), RNG.Next(11, 1000)); + + var testScore = TestResources.CreateTestScoreInfo(); + testScore.TotalScore = RNG.Next(1_000_000); + + totals.AddNewScore(ev); + } + }); + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeTotalsDisplay.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeTotalsDisplay.cs new file mode 100644 index 0000000000..464022639f --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeTotalsDisplay.cs @@ -0,0 +1,126 @@ +// 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.Extensions.LocalisationExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Localisation; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osu.Game.Screens.OnlinePlay.DailyChallenge.Events; +using osuTK; + +namespace osu.Game.Screens.OnlinePlay.DailyChallenge +{ + public partial class DailyChallengeTotalsDisplay : CompositeDrawable + { + private Container passCountContainer = null!; + private TotalRollingCounter passCounter = null!; + private Container totalScoreContainer = null!; + private TotalRollingCounter totalScoreCounter = null!; + + private long totalPassCountInstantaneous; + private long cumulativeTotalScoreInstantaneous; + + [BackgroundDependencyLoader] + private void load() + { + InternalChild = new GridContainer + { + RelativeSizeAxes = Axes.Both, + RowDimensions = + [ + new Dimension(GridSizeMode.AutoSize), + new Dimension(), + new Dimension(GridSizeMode.AutoSize), + new Dimension(), + ], + Content = new[] + { + new Drawable[] + { + new SectionHeader("Total pass count") + }, + new Drawable[] + { + passCountContainer = new Container + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Child = passCounter = new TotalRollingCounter + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + } + } + }, + new Drawable[] + { + new SectionHeader("Cumulative total score") + }, + new Drawable[] + { + totalScoreContainer = new Container + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Child = totalScoreCounter = new TotalRollingCounter + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + } + } + }, + } + }; + } + + public void SetInitialCounts(long totalPassCount, long cumulativeTotalScore) + { + totalPassCountInstantaneous = totalPassCount; + cumulativeTotalScoreInstantaneous = cumulativeTotalScore; + } + + public void AddNewScore(NewScoreEvent ev) + { + totalPassCountInstantaneous += 1; + cumulativeTotalScoreInstantaneous += ev.TotalScore; + } + + protected override void Update() + { + base.Update(); + + passCounter.Current.Value = totalPassCountInstantaneous; + totalScoreCounter.Current.Value = cumulativeTotalScoreInstantaneous; + } + + protected override void UpdateAfterChildren() + { + base.UpdateAfterChildren(); + + var totalPassCountProportionOfParent = Vector2.Divide(passCountContainer.DrawSize, passCounter.DrawSize); + passCounter.Scale = new Vector2(Math.Min(Math.Min(totalPassCountProportionOfParent.X, totalPassCountProportionOfParent.Y) * 0.8f, 1)); + + var totalScoreTextProportionOfParent = Vector2.Divide(totalScoreContainer.DrawSize, totalScoreCounter.DrawSize); + totalScoreCounter.Scale = new Vector2(Math.Min(Math.Min(totalScoreTextProportionOfParent.X, totalScoreTextProportionOfParent.Y) * 0.8f, 1)); + } + + private partial class TotalRollingCounter : RollingCounter + { + protected override double RollingDuration => 400; + + protected override OsuSpriteText CreateSpriteText() => new OsuSpriteText + { + Font = OsuFont.Default.With(size: 80f, fixedWidth: true), + }; + + protected override LocalisableString FormatCount(long count) => count.ToLocalisableString(@"N0"); + } + } +} From a8851950bccaf8abbe11f05ce1329ebde256ceb6 Mon Sep 17 00:00:00 2001 From: Cameron Brown Date: Fri, 26 Jul 2024 18:10:11 +1000 Subject: [PATCH 1646/2493] Update the beatmap of Daily Challenge's mods overlay when beatmap is set - #29094 --- osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs index 235361dfaa..057bbd6be4 100644 --- a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs @@ -379,6 +379,9 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge var beatmap = beatmapManager.QueryBeatmap(b => b.OnlineID == playlistItem.Beatmap.OnlineID); Beatmap.Value = beatmapManager.GetWorkingBeatmap(beatmap); // this will gracefully fall back to dummy beatmap if missing locally. Ruleset.Value = rulesets.GetRuleset(playlistItem.RulesetID); + + userModsSelectOverlay.Beatmap.Value = Beatmap.Value; + applyLoopingToTrack(); } From 17f00ec0a6e87e184ae895df4ec05f85c11f6cec Mon Sep 17 00:00:00 2001 From: Cameron Brown Date: Fri, 26 Jul 2024 18:29:50 +1000 Subject: [PATCH 1647/2493] Bind the mod select overlay's Beatmap to OsuScreen.Beatmap in constructor Suggested by @bdach! --- osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs index 057bbd6be4..a4b251bf5b 100644 --- a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs @@ -301,6 +301,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge LoadComponent(userModsSelectOverlay = new RoomModSelectOverlay { + Beatmap = { BindTarget = Beatmap }, SelectedMods = { BindTarget = userMods }, IsValidMod = _ => false }); @@ -380,8 +381,6 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge Beatmap.Value = beatmapManager.GetWorkingBeatmap(beatmap); // this will gracefully fall back to dummy beatmap if missing locally. Ruleset.Value = rulesets.GetRuleset(playlistItem.RulesetID); - userModsSelectOverlay.Beatmap.Value = Beatmap.Value; - applyLoopingToTrack(); } From f9cfc7d96cfbf42a9e40172b55c14ef92a2e9827 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 26 Jul 2024 10:53:12 +0200 Subject: [PATCH 1648/2493] Fix preview tracks not stopping playback when suspending/exiting daily challenge screen Closes https://github.com/ppy/osu/issues/29083. --- .../Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs index 235361dfaa..6ff0eb2452 100644 --- a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs @@ -17,6 +17,7 @@ using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Shapes; using osu.Framework.Logging; using osu.Framework.Screens; +using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Database; using osu.Game.Graphics.Containers; @@ -40,7 +41,8 @@ using osuTK; namespace osu.Game.Screens.OnlinePlay.DailyChallenge { - public partial class DailyChallenge : OsuScreen + [Cached(typeof(IPreviewTrackOwner))] + public partial class DailyChallenge : OsuScreen, IPreviewTrackOwner { private readonly Room room; private readonly PlaylistItem playlistItem; @@ -91,6 +93,9 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge [Resolved] protected IAPIProvider API { get; private set; } = null!; + [Resolved] + private PreviewTrackManager previewTrackManager { get; set; } = null!; + public override bool DisallowExternalBeatmapRulesetChanges => true; public DailyChallenge(Room room) @@ -441,6 +446,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge userModsSelectOverlay.Hide(); cancelTrackLooping(); + previewTrackManager.StopAnyPlaying(this); } public override bool OnExiting(ScreenExitEvent e) @@ -448,6 +454,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge waves.Hide(); userModsSelectOverlay.Hide(); cancelTrackLooping(); + previewTrackManager.StopAnyPlaying(this); this.Delay(WaveContainer.DISAPPEAR_DURATION).FadeOut(); roomManager.PartRoom(); From 1abcf16231eebe0d24dd0b7dab1a9ebea86ad7f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 26 Jul 2024 11:50:43 +0200 Subject: [PATCH 1649/2493] Fix daily challenge screen not applying track adjustments from mods Closes https://github.com/ppy/osu/issues/29093. --- osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs index a4b251bf5b..56398090d0 100644 --- a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs @@ -93,6 +93,8 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge public override bool DisallowExternalBeatmapRulesetChanges => true; + public override bool? ApplyModTrackAdjustments => true; + public DailyChallenge(Room room) { this.room = room; From 1ad0b31217d8df8e29ed02165aa83bd3a665a788 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 26 Jul 2024 19:04:04 +0900 Subject: [PATCH 1650/2493] Add required pieces to `MultiplayerPlaylistItemStats` for total score tracking --- osu.Game/Online/Metadata/MultiplayerPlaylistItemStats.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/osu.Game/Online/Metadata/MultiplayerPlaylistItemStats.cs b/osu.Game/Online/Metadata/MultiplayerPlaylistItemStats.cs index d13705bf5b..6e50242556 100644 --- a/osu.Game/Online/Metadata/MultiplayerPlaylistItemStats.cs +++ b/osu.Game/Online/Metadata/MultiplayerPlaylistItemStats.cs @@ -25,5 +25,14 @@ namespace osu.Game.Online.Metadata /// [Key(1)] public long[] TotalScoreDistribution { get; set; } = new long[TOTAL_SCORE_DISTRIBUTION_BINS]; + + /// + /// The cumulative total of all passing scores (across all users) in the playlist so far. + /// + [Key(2)] + public long TotalPlaylistScore { get; set; } + + [Key(3)] + public ulong LastProcessedScoreID { get; set; } } } From 2e37f3b5de2be1e94bc3a29f4f608f021aeadb2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 26 Jul 2024 12:34:23 +0200 Subject: [PATCH 1651/2493] Hook up score totals display to daily challenge screen --- .../Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs index 235361dfaa..17241a5fd6 100644 --- a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs @@ -59,6 +59,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge private IDisposable? userModsSelectOverlayRegistration; private DailyChallengeScoreBreakdown breakdown = null!; + private DailyChallengeTotalsDisplay totals = null!; private DailyChallengeEventFeed feed = null!; [Cached] @@ -211,6 +212,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge { new DailyChallengeTimeRemainingRing(), breakdown = new DailyChallengeScoreBreakdown(), + totals = new DailyChallengeTotalsDisplay(), } } }, @@ -351,6 +353,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge Schedule(() => { breakdown.AddNewScore(ev); + totals.AddNewScore(ev); feed.AddNewScore(ev); if (e.NewRank <= 50) @@ -421,7 +424,11 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge var itemStats = stats.SingleOrDefault(item => item.PlaylistItemID == playlistItem.ID); if (itemStats == null) return; - Schedule(() => breakdown.SetInitialCounts(itemStats.TotalScoreDistribution)); + Schedule(() => + { + breakdown.SetInitialCounts(itemStats.TotalScoreDistribution); + totals.SetInitialCounts(itemStats.TotalScoreDistribution.Sum(c => c), itemStats.TotalPlaylistScore); + }); }); beatmapAvailabilityTracker.SelectedItem.Value = playlistItem; From 19affa7062bfd7f82e1a33a61e6427a74a1f2463 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 26 Jul 2024 20:42:01 +0900 Subject: [PATCH 1652/2493] Rename new property to match true usage (per item) Also document a bit more. --- osu.Game/Online/Metadata/MultiplayerPlaylistItemStats.cs | 7 +++++-- .../Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/osu.Game/Online/Metadata/MultiplayerPlaylistItemStats.cs b/osu.Game/Online/Metadata/MultiplayerPlaylistItemStats.cs index 6e50242556..19a2bde497 100644 --- a/osu.Game/Online/Metadata/MultiplayerPlaylistItemStats.cs +++ b/osu.Game/Online/Metadata/MultiplayerPlaylistItemStats.cs @@ -27,11 +27,14 @@ namespace osu.Game.Online.Metadata public long[] TotalScoreDistribution { get; set; } = new long[TOTAL_SCORE_DISTRIBUTION_BINS]; /// - /// The cumulative total of all passing scores (across all users) in the playlist so far. + /// The cumulative total of all passing scores (across all users) for the playlist item so far. /// [Key(2)] - public long TotalPlaylistScore { get; set; } + public long CumulativeScore { get; set; } + /// + /// The last score to have been processed into provided statistics. Generally only for server-side accounting purposes. + /// [Key(3)] public ulong LastProcessedScoreID { get; set; } } diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs index 17241a5fd6..ff37d7c970 100644 --- a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs @@ -427,7 +427,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge Schedule(() => { breakdown.SetInitialCounts(itemStats.TotalScoreDistribution); - totals.SetInitialCounts(itemStats.TotalScoreDistribution.Sum(c => c), itemStats.TotalPlaylistScore); + totals.SetInitialCounts(itemStats.TotalScoreDistribution.Sum(c => c), itemStats.CumulativeScore); }); }); From 2caaebb6705bf9df5f0fd8572c1a013230cf6ced Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 26 Jul 2024 13:47:41 +0200 Subject: [PATCH 1653/2493] Add tooltip with counts to daily challenge score breakdown chart --- .../DailyChallengeScoreBreakdown.cs | 49 ++++++++++--------- 1 file changed, 27 insertions(+), 22 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeScoreBreakdown.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeScoreBreakdown.cs index 0c7202f7cf..45bda9f185 100644 --- a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeScoreBreakdown.cs +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeScoreBreakdown.cs @@ -6,6 +6,7 @@ using System.Linq; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Shapes; using osu.Framework.Localisation; using osu.Game.Graphics; @@ -44,23 +45,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge for (int i = 0; i < bin_count; ++i) { - LocalisableString? label = null; - - switch (i) - { - case 2: - case 4: - case 6: - case 8: - label = @$"{100 * i}k"; - break; - - case 10: - label = @"1M"; - break; - } - - barsContainer.Add(new Bar(label) + barsContainer.Add(new Bar(100_000 * i, 100_000 * (i + 1) - 1) { Width = 1f / bin_count, }); @@ -113,18 +98,20 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge barsContainer[i].UpdateCounts(bins[i], max); } - private partial class Bar : CompositeDrawable + private partial class Bar : CompositeDrawable, IHasTooltip { - private readonly LocalisableString? label; + private readonly int binStart; + private readonly int binEnd; private long count; private long max; public Container CircularBar { get; private set; } = null!; - public Bar(LocalisableString? label = null) + public Bar(int binStart, int binEnd) { - this.label = label; + this.binStart = binStart; + this.binEnd = binEnd; } [BackgroundDependencyLoader] @@ -159,13 +146,29 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge } }); + string? label = null; + + switch (binStart) + { + case 200_000: + case 400_000: + case 600_000: + case 800_000: + label = @$"{binStart / 1000}k"; + break; + + case 1_000_000: + label = @"1M"; + break; + } + if (label != null) { AddInternal(new OsuSpriteText { Anchor = Anchor.BottomLeft, Origin = Anchor.BottomCentre, - Text = label.Value, + Text = label, Colour = colourProvider.Content2, }); } @@ -189,6 +192,8 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge if (isIncrement) CircularBar.FlashColour(Colour4.White, 600, Easing.OutQuint); } + + public LocalisableString TooltipText => LocalisableString.Format("{0:N0} passes in {1:N0} - {2:N0} range", count, binStart, binEnd); } } } From fc0ade2c61405b80b3c15acc042d678fa545d720 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 26 Jul 2024 14:31:21 +0200 Subject: [PATCH 1654/2493] Highlight where local user's best is on the breakdown --- .../TestSceneDailyChallengeScoreBreakdown.cs | 3 + .../DailyChallenge/DailyChallenge.cs | 2 + .../DailyChallengeLeaderboard.cs | 7 +- .../DailyChallengeScoreBreakdown.cs | 101 ++++++++++++++---- 4 files changed, 92 insertions(+), 21 deletions(-) diff --git a/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeScoreBreakdown.cs b/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeScoreBreakdown.cs index 81ec95d8d2..631aafb58f 100644 --- a/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeScoreBreakdown.cs +++ b/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeScoreBreakdown.cs @@ -8,6 +8,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Shapes; using osu.Framework.Utils; using osu.Game.Online.API.Requests.Responses; +using osu.Game.Online.Rooms; using osu.Game.Overlays; using osu.Game.Screens.OnlinePlay.DailyChallenge; using osu.Game.Screens.OnlinePlay.DailyChallenge.Events; @@ -61,6 +62,8 @@ namespace osu.Game.Tests.Visual.DailyChallenge breakdown.AddNewScore(ev); }); + AddStep("set user score", () => breakdown.UserBestScore.Value = new MultiplayerScore { TotalScore = RNG.Next(1_000_000) }); + AddStep("unset user score", () => breakdown.UserBestScore.Value = null); } } } diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs index a4b251bf5b..44c47e18d6 100644 --- a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs @@ -324,6 +324,8 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge } metadataClient.MultiplayerRoomScoreSet += onRoomScoreSet; + + ((IBindable)breakdown.UserBestScore).BindTo(leaderboard.UserBestScore); } private void presentScore(long id) diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeLeaderboard.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeLeaderboard.cs index 4c4622bba3..d87a34405d 100644 --- a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeLeaderboard.cs +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeLeaderboard.cs @@ -22,6 +22,9 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge { public partial class DailyChallengeLeaderboard : CompositeDrawable { + public IBindable UserBestScore => userBestScore; + private Bindable userBestScore = new Bindable(); + public Action? PresentScore { get; init; } private readonly Room room; @@ -130,7 +133,9 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge request.Success += req => Schedule(() => { var best = req.Scores.Select(s => s.CreateScoreInfo(scoreManager, rulesets, playlistItem, beatmap.Value.BeatmapInfo)).ToArray(); - var userBest = req.UserScore?.CreateScoreInfo(scoreManager, rulesets, playlistItem, beatmap.Value.BeatmapInfo); + + userBestScore.Value = req.UserScore; + var userBest = userBestScore.Value?.CreateScoreInfo(scoreManager, rulesets, playlistItem, beatmap.Value.BeatmapInfo); cancellationTokenSource?.Cancel(); cancellationTokenSource = null; diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeScoreBreakdown.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeScoreBreakdown.cs index 45bda9f185..fce4f0452b 100644 --- a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeScoreBreakdown.cs +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeScoreBreakdown.cs @@ -4,6 +4,7 @@ using System; using System.Linq; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; @@ -13,6 +14,7 @@ using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Online.Metadata; +using osu.Game.Online.Rooms; using osu.Game.Overlays; using osu.Game.Screens.OnlinePlay.DailyChallenge.Events; using osuTK; @@ -21,6 +23,8 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge { public partial class DailyChallengeScoreBreakdown : CompositeDrawable { + public Bindable UserBestScore { get; } = new Bindable(); + private FillFlowContainer barsContainer = null!; private const int bin_count = MultiplayerPlaylistItemStats.TOTAL_SCORE_DISTRIBUTION_BINS; @@ -52,6 +56,17 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge } } + protected override void LoadComplete() + { + base.LoadComplete(); + + UserBestScore.BindValueChanged(_ => + { + foreach (var bar in barsContainer) + bar.ContainsLocalUser.Value = UserBestScore.Value is not null && bar.BinStart <= UserBestScore.Value.TotalScore && UserBestScore.Value.TotalScore <= bar.BinEnd; + }); + } + public void AddNewScore(NewScoreEvent newScoreEvent) { int targetBin = (int)Math.Clamp(Math.Floor((float)newScoreEvent.TotalScore / 100000), 0, bin_count - 1); @@ -100,20 +115,32 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge private partial class Bar : CompositeDrawable, IHasTooltip { - private readonly int binStart; - private readonly int binEnd; + public BindableBool ContainsLocalUser { get; } = new BindableBool(); + + public readonly int BinStart; + public readonly int BinEnd; private long count; private long max; public Container CircularBar { get; private set; } = null!; + private Box fill = null!; + private Box flashLayer = null!; + private OsuSpriteText userIndicator = null!; + public Bar(int binStart, int binEnd) { - this.binStart = binStart; - this.binEnd = binEnd; + this.BinStart = binStart; + this.BinEnd = binEnd; } + [Resolved] + private OverlayColourProvider colourProvider { get; set; } = null!; + + [Resolved] + private OsuColour colours { get; set; } = null!; + [BackgroundDependencyLoader] private void load(OverlayColourProvider colourProvider) { @@ -129,32 +156,52 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge }, Anchor = Anchor.BottomCentre, Origin = Anchor.BottomCentre, - Masking = true, - Child = CircularBar = new Container + Children = new Drawable[] { - RelativeSizeAxes = Axes.Both, - Anchor = Anchor.BottomCentre, - Origin = Anchor.BottomCentre, - Height = 0.01f, - Masking = true, - CornerRadius = 10, - Colour = colourProvider.Highlight1, - Child = new Box + CircularBar = new Container { RelativeSizeAxes = Axes.Both, + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + Height = 0.01f, + Masking = true, + CornerRadius = 10, + Children = new Drawable[] + { + fill = new Box + { + RelativeSizeAxes = Axes.Both, + }, + flashLayer = new Box + { + RelativeSizeAxes = Axes.Both, + Alpha = 0, + }, + } + }, + userIndicator = new OsuSpriteText + { + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + Colour = colours.Orange1, + Text = "You", + Font = OsuFont.Default.With(weight: FontWeight.Bold), + Alpha = 0, + RelativePositionAxes = Axes.Y, + Margin = new MarginPadding { Bottom = 5, }, } - } + }, }); string? label = null; - switch (binStart) + switch (BinStart) { case 200_000: case 400_000: case 600_000: case 800_000: - label = @$"{binStart / 1000}k"; + label = @$"{BinStart / 1000}k"; break; case 1_000_000: @@ -174,6 +221,18 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge } } + protected override void LoadComplete() + { + base.LoadComplete(); + + ContainsLocalUser.BindValueChanged(_ => + { + fill.FadeColour(ContainsLocalUser.Value ? colours.Orange1 : colourProvider.Highlight1, 300, Easing.OutQuint); + userIndicator.FadeTo(ContainsLocalUser.Value ? 1 : 0, 300, Easing.OutQuint); + }, true); + FinishTransforms(true); + } + protected override void Update() { base.Update(); @@ -188,12 +247,14 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge count = newCount; max = newMax; - CircularBar.ResizeHeightTo(0.01f + 0.99f * count / max, 300, Easing.OutQuint); + float height = 0.01f + 0.99f * count / max; + CircularBar.ResizeHeightTo(height, 300, Easing.OutQuint); + userIndicator.MoveToY(-height, 300, Easing.OutQuint); if (isIncrement) - CircularBar.FlashColour(Colour4.White, 600, Easing.OutQuint); + flashLayer.FadeOutFromOne(600, Easing.OutQuint); } - public LocalisableString TooltipText => LocalisableString.Format("{0:N0} passes in {1:N0} - {2:N0} range", count, binStart, binEnd); + public LocalisableString TooltipText => LocalisableString.Format("{0:N0} passes in {1:N0} - {2:N0} range", count, BinStart, BinEnd); } } } From a870722ea691297c216283833215cf8316cb2f3f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 26 Jul 2024 21:43:23 +0900 Subject: [PATCH 1655/2493] Adjust easings and reduce character spacing slightly --- .../DailyChallengeTotalsDisplay.cs | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeTotalsDisplay.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeTotalsDisplay.cs index 464022639f..cf8a60d4a2 100644 --- a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeTotalsDisplay.cs +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeTotalsDisplay.cs @@ -113,11 +113,26 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge private partial class TotalRollingCounter : RollingCounter { - protected override double RollingDuration => 400; + protected override double RollingDuration => 1000; + + protected override Easing RollingEasing => Easing.OutPow10; + + protected override bool IsRollingProportional => true; + + protected override double GetProportionalDuration(long currentValue, long newValue) + { + long change = Math.Abs(newValue - currentValue); + + if (change < 10) + return 0; + + return Math.Min(6000, RollingDuration * Math.Sqrt(change) / 100); + } protected override OsuSpriteText CreateSpriteText() => new OsuSpriteText { Font = OsuFont.Default.With(size: 80f, fixedWidth: true), + Spacing = new Vector2(-2, 0) }; protected override LocalisableString FormatCount(long count) => count.ToLocalisableString(@"N0"); From 0996f9b0b51dbc7d2308c812385e603b6a05036d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 26 Jul 2024 14:45:39 +0200 Subject: [PATCH 1656/2493] Fix code quality --- .../OnlinePlay/DailyChallenge/DailyChallengeLeaderboard.cs | 2 +- .../OnlinePlay/DailyChallenge/DailyChallengeScoreBreakdown.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeLeaderboard.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeLeaderboard.cs index d87a34405d..5efb656cea 100644 --- a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeLeaderboard.cs +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeLeaderboard.cs @@ -23,7 +23,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge public partial class DailyChallengeLeaderboard : CompositeDrawable { public IBindable UserBestScore => userBestScore; - private Bindable userBestScore = new Bindable(); + private readonly Bindable userBestScore = new Bindable(); public Action? PresentScore { get; init; } diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeScoreBreakdown.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeScoreBreakdown.cs index fce4f0452b..cfec170cf6 100644 --- a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeScoreBreakdown.cs +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeScoreBreakdown.cs @@ -131,8 +131,8 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge public Bar(int binStart, int binEnd) { - this.BinStart = binStart; - this.BinEnd = binEnd; + BinStart = binStart; + BinEnd = binEnd; } [Resolved] From 96049807c4392ee68d85134418865cdb9946c296 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 26 Jul 2024 23:20:32 +0900 Subject: [PATCH 1657/2493] Adjust weight and text in event feed output Just some minor adjustments. --- .../DailyChallenge/DailyChallengeEventFeed.cs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeEventFeed.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeEventFeed.cs index 160ad83c8a..044c599ae9 100644 --- a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeEventFeed.cs +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeEventFeed.cs @@ -121,7 +121,14 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge }, text = new LinkFlowContainer(t => { - t.Font = OsuFont.Default.With(weight: newScore.NewRank == null ? FontWeight.Medium : FontWeight.Bold); + FontWeight fontWeight = FontWeight.Medium; + + if (newScore.NewRank < 100) + fontWeight = FontWeight.Bold; + else if (newScore.NewRank < 1000) + fontWeight = FontWeight.SemiBold; + + t.Font = OsuFont.Default.With(weight: fontWeight); t.Colour = newScore.NewRank < 10 ? colours.Orange1 : Colour4.White; }) { @@ -132,8 +139,8 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge }; text.AddUserLink(newScore.User); - text.AddText(" got "); - text.AddLink($"{newScore.TotalScore:N0} points", () => PresentScore?.Invoke(newScore.ScoreID)); + text.AddText(" scored "); + text.AddLink($"{newScore.TotalScore:N0}", () => PresentScore?.Invoke(newScore.ScoreID)); if (newScore.NewRank != null) text.AddText($" and achieved rank #{newScore.NewRank.Value:N0}"); From 0421e1e9d0bf16e6a947cc122694fff19f4d42c3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 26 Jul 2024 23:21:44 +0900 Subject: [PATCH 1658/2493] Reduce number spacing a bit more --- .../OnlinePlay/DailyChallenge/DailyChallengeTotalsDisplay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeTotalsDisplay.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeTotalsDisplay.cs index cf8a60d4a2..e2535ed806 100644 --- a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeTotalsDisplay.cs +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeTotalsDisplay.cs @@ -132,7 +132,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge protected override OsuSpriteText CreateSpriteText() => new OsuSpriteText { Font = OsuFont.Default.With(size: 80f, fixedWidth: true), - Spacing = new Vector2(-2, 0) + Spacing = new Vector2(-4, 0) }; protected override LocalisableString FormatCount(long count) => count.ToLocalisableString(@"N0"); From 0cc6818b21f8bb88746269570e22e9ce399ab74a Mon Sep 17 00:00:00 2001 From: Caiyi Shyu Date: Fri, 26 Jul 2024 22:44:50 +0800 Subject: [PATCH 1659/2493] allow hover to expand `ModCustomisationPanel` --- .../Overlays/Mods/ModCustomisationHeader.cs | 13 ++++++++++ .../Overlays/Mods/ModCustomisationPanel.cs | 24 +++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/osu.Game/Overlays/Mods/ModCustomisationHeader.cs b/osu.Game/Overlays/Mods/ModCustomisationHeader.cs index bf10e13515..fbdce7be6d 100644 --- a/osu.Game/Overlays/Mods/ModCustomisationHeader.cs +++ b/osu.Game/Overlays/Mods/ModCustomisationHeader.cs @@ -8,6 +8,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; +using osu.Framework.Input.Events; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; @@ -28,6 +29,8 @@ namespace osu.Game.Overlays.Mods public readonly BindableBool Expanded = new BindableBool(); + protected new ModCustomisationPanel Parent => (ModCustomisationPanel)base.Parent; + public ModCustomisationHeader() { Action = Expanded.Toggle; @@ -91,5 +94,15 @@ namespace osu.Game.Overlays.Mods icon.ScaleTo(v.NewValue ? new Vector2(1, -1) : Vector2.One, 300, Easing.OutQuint); }, true); } + + protected override bool OnHover(HoverEvent e) + { + if (Enabled.Value) + { + Parent.UpdateHoverExpansion(true); + } + + return base.OnHover(e); + } } } diff --git a/osu.Game/Overlays/Mods/ModCustomisationPanel.cs b/osu.Game/Overlays/Mods/ModCustomisationPanel.cs index a1e64e8c49..a82e279d01 100644 --- a/osu.Game/Overlays/Mods/ModCustomisationPanel.cs +++ b/osu.Game/Overlays/Mods/ModCustomisationPanel.cs @@ -175,6 +175,22 @@ namespace osu.Game.Overlays.Mods content.ResizeHeightTo(header_height, 400, Easing.OutQuint); content.FadeOut(400, Easing.OutSine); } + + expandedByHovering = false; + } + + private bool expandedByHovering = false; + public void UpdateHoverExpansion(bool hovered) + { + if (hovered && !Expanded.Value) + { + Expanded.Value = true; + expandedByHovering = true; + } + else if (!hovered && expandedByHovering) + { + Expanded.Value = false; + } } private void updateMods() @@ -206,6 +222,14 @@ namespace osu.Game.Overlays.Mods public override bool RequestsFocus => Expanded.Value; public override bool AcceptsFocus => Expanded.Value; + + public new ModCustomisationPanel Parent => (ModCustomisationPanel)base.Parent; + + protected override void OnHoverLost(HoverLostEvent e) + { + Parent.UpdateHoverExpansion(false); + base.OnHoverLost(e); + } } } } From a3576a55c229f16e3d4e251d566a8b2443fb0ebb Mon Sep 17 00:00:00 2001 From: Caiyi Shyu Date: Fri, 26 Jul 2024 22:45:12 +0800 Subject: [PATCH 1660/2493] add test for hovering `ModCustomisationPanel` --- .../TestSceneModCustomisationPanel.cs | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModCustomisationPanel.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModCustomisationPanel.cs index 9c0d185892..64ef7891c8 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModCustomisationPanel.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModCustomisationPanel.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; @@ -10,6 +11,7 @@ using osu.Game.Overlays; using osu.Game.Overlays.Mods; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.Mods; +using osuTK; namespace osu.Game.Tests.Visual.UserInterface { @@ -19,6 +21,8 @@ namespace osu.Game.Tests.Visual.UserInterface private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine); private ModCustomisationPanel panel = null!; + private ModCustomisationHeader header = null!; + private Container content = null!; [SetUp] public void SetUp() => Schedule(() => @@ -36,6 +40,9 @@ namespace osu.Game.Tests.Visual.UserInterface SelectedMods = { BindTarget = SelectedMods }, } }; + + header = panel.Children.OfType().First(); + content = panel.Children.OfType().First(); }); [Test] @@ -62,5 +69,68 @@ namespace osu.Game.Tests.Visual.UserInterface panel.Enabled.Value = panel.Expanded.Value = false; }); } + + [Test] + public void TestHoverExpand() + { + // Can not expand by hovering when no supported mod + { + AddStep("hover header", () => InputManager.MoveMouseTo(header)); + + AddAssert("not expanded", () => !panel.Expanded.Value); + + AddStep("hover content", () => InputManager.MoveMouseTo(content)); + + AddAssert("neither expanded", () => !panel.Expanded.Value); + + AddStep("left from content", () => InputManager.MoveMouseTo(Vector2.One)); + } + + AddStep("add customisable mod", () => + { + SelectedMods.Value = new[] { new OsuModDoubleTime() }; + panel.Enabled.Value = true; + }); + + // Can expand by hovering when supported mod + { + AddStep("hover header", () => InputManager.MoveMouseTo(header)); + + AddAssert("expanded", () => panel.Expanded.Value); + + AddStep("hover content", () => InputManager.MoveMouseTo(content)); + + AddAssert("still expanded", () => panel.Expanded.Value); + } + + // Will collapse when mouse left from content + { + AddStep("left from content", () => InputManager.MoveMouseTo(Vector2.One)); + + AddAssert("not expanded", () => !panel.Expanded.Value); + } + + // Will collapse when mouse left from header + { + AddStep("hover header", () => InputManager.MoveMouseTo(header)); + + AddAssert("expanded", () => panel.Expanded.Value); + + AddStep("left from header", () => InputManager.MoveMouseTo(Vector2.One)); + + AddAssert("not expanded", () => !panel.Expanded.Value); + } + + // Not collapse when mouse left if not expanded by hovering + { + AddStep("expand not by hovering", () => panel.Expanded.Value = true); + + AddStep("hover content", () => InputManager.MoveMouseTo(content)); + + AddStep("moust left", () => InputManager.MoveMouseTo(Vector2.One)); + + AddAssert("still expanded", () => panel.Expanded.Value); + } + } } } From aed81d97584353d06431fb7767690fa22ecc3f19 Mon Sep 17 00:00:00 2001 From: Caiyi Shyu Date: Fri, 26 Jul 2024 22:56:07 +0800 Subject: [PATCH 1661/2493] make code inspector happy --- osu.Game/Overlays/Mods/ModCustomisationHeader.cs | 4 ++-- osu.Game/Overlays/Mods/ModCustomisationPanel.cs | 7 ++++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModCustomisationHeader.cs b/osu.Game/Overlays/Mods/ModCustomisationHeader.cs index fbdce7be6d..5a9e6099e6 100644 --- a/osu.Game/Overlays/Mods/ModCustomisationHeader.cs +++ b/osu.Game/Overlays/Mods/ModCustomisationHeader.cs @@ -29,7 +29,7 @@ namespace osu.Game.Overlays.Mods public readonly BindableBool Expanded = new BindableBool(); - protected new ModCustomisationPanel Parent => (ModCustomisationPanel)base.Parent; + protected new ModCustomisationPanel? Parent => (ModCustomisationPanel?)base.Parent; public ModCustomisationHeader() { @@ -99,7 +99,7 @@ namespace osu.Game.Overlays.Mods { if (Enabled.Value) { - Parent.UpdateHoverExpansion(true); + Parent?.UpdateHoverExpansion(true); } return base.OnHover(e); diff --git a/osu.Game/Overlays/Mods/ModCustomisationPanel.cs b/osu.Game/Overlays/Mods/ModCustomisationPanel.cs index a82e279d01..ee8232b79a 100644 --- a/osu.Game/Overlays/Mods/ModCustomisationPanel.cs +++ b/osu.Game/Overlays/Mods/ModCustomisationPanel.cs @@ -179,7 +179,8 @@ namespace osu.Game.Overlays.Mods expandedByHovering = false; } - private bool expandedByHovering = false; + private bool expandedByHovering; + public void UpdateHoverExpansion(bool hovered) { if (hovered && !Expanded.Value) @@ -223,11 +224,11 @@ namespace osu.Game.Overlays.Mods public override bool RequestsFocus => Expanded.Value; public override bool AcceptsFocus => Expanded.Value; - public new ModCustomisationPanel Parent => (ModCustomisationPanel)base.Parent; + public new ModCustomisationPanel? Parent => (ModCustomisationPanel?)base.Parent; protected override void OnHoverLost(HoverLostEvent e) { - Parent.UpdateHoverExpansion(false); + Parent?.UpdateHoverExpansion(false); base.OnHoverLost(e); } } From bd017aea38f48746bb5148d7315e33dd9463f2ee Mon Sep 17 00:00:00 2001 From: Caiyi Shyu Date: Fri, 26 Jul 2024 23:58:52 +0800 Subject: [PATCH 1662/2493] fix `TestPreexistingSelection` failing --- osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs index e4622ffcf9..77909d6936 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs @@ -57,6 +57,7 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep("reset ruleset", () => Ruleset.Value = rulesetStore.GetRuleset(0)); AddStep("reset mods", () => SelectedMods.SetDefault()); AddStep("reset config", () => configManager.SetValue(OsuSetting.ModSelectTextSearchStartsActive, true)); + AddStep("reset mouse", () => InputManager.MoveMouseTo(Vector2.One)); AddStep("set beatmap", () => Beatmap.Value = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo)); AddStep("set up presets", () => { From 9323f89357f0fd070f3aa12e7559d8ee9572d925 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 27 Jul 2024 02:06:56 +0900 Subject: [PATCH 1663/2493] Fix "Beatmap not downloaded" tooltip hint not showing in daily challenge --- osu.Game/Screens/OnlinePlay/Components/ReadyButton.cs | 4 ++-- osu.Game/Screens/OnlinePlay/Playlists/PlaylistsReadyButton.cs | 3 --- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Components/ReadyButton.cs b/osu.Game/Screens/OnlinePlay/Components/ReadyButton.cs index 813e243449..2e669fd1b2 100644 --- a/osu.Game/Screens/OnlinePlay/Components/ReadyButton.cs +++ b/osu.Game/Screens/OnlinePlay/Components/ReadyButton.cs @@ -21,8 +21,8 @@ namespace osu.Game.Screens.OnlinePlay.Components private void load(OnlinePlayBeatmapAvailabilityTracker beatmapTracker) { availability.BindTo(beatmapTracker.Availability); - availability.BindValueChanged(_ => updateState()); + Enabled.BindValueChanged(_ => updateState(), true); } @@ -33,7 +33,7 @@ namespace osu.Game.Screens.OnlinePlay.Components { get { - if (Enabled.Value) + if (base.Enabled.Value) return string.Empty; if (availability.Value.State != DownloadState.LocallyAvailable) diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsReadyButton.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsReadyButton.cs index 91a3edbea3..4b00678b01 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsReadyButton.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsReadyButton.cs @@ -68,9 +68,6 @@ namespace osu.Game.Screens.OnlinePlay.Playlists { get { - if (Enabled.Value) - return string.Empty; - if (!enoughTimeLeft) return "No time left!"; From d55e861b906f8b049f0af5248ad78e9a36b99dd7 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Fri, 26 Jul 2024 16:55:15 -0700 Subject: [PATCH 1664/2493] Fix daily challenge background clipping when settings/notifications is opened --- osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs index 8538cbbb59..c8e1434e37 100644 --- a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs @@ -106,6 +106,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge this.room = room; playlistItem = room.Playlist.Single(); roomManager = new RoomManager(); + Padding = new MarginPadding { Horizontal = -HORIZONTAL_OVERFLOW_PADDING }; } protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) From b97d96fcb0189b736984ec1ecb0fdebc0c55d07d Mon Sep 17 00:00:00 2001 From: Caiyi Shyu Date: Sat, 27 Jul 2024 15:15:14 +0800 Subject: [PATCH 1665/2493] Fix panel collapse when hovering dropdown menu --- .../Overlays/Mods/ModCustomisationPanel.cs | 33 ++++++++++++++++--- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModCustomisationPanel.cs b/osu.Game/Overlays/Mods/ModCustomisationPanel.cs index ee8232b79a..d906e704e0 100644 --- a/osu.Game/Overlays/Mods/ModCustomisationPanel.cs +++ b/osu.Game/Overlays/Mods/ModCustomisationPanel.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -11,6 +12,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Shapes; +using osu.Framework.Input; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Game.Configuration; @@ -81,6 +83,7 @@ namespace osu.Game.Overlays.Mods Colour = Color4.Black.Opacity(0.25f), }, Expanded = { BindTarget = Expanded }, + ExpandedByHovering = { BindTarget = ExpandedByHovering }, Children = new Drawable[] { new Box @@ -176,20 +179,22 @@ namespace osu.Game.Overlays.Mods content.FadeOut(400, Easing.OutSine); } - expandedByHovering = false; + ExpandedByHovering.Value = false; } - private bool expandedByHovering; + public readonly BindableBool ExpandedByHovering = new BindableBool(); public void UpdateHoverExpansion(bool hovered) { if (hovered && !Expanded.Value) { Expanded.Value = true; - expandedByHovering = true; + ExpandedByHovering.Value = true; } - else if (!hovered && expandedByHovering) + else if (!hovered && ExpandedByHovering.Value) { + Debug.Assert(Expanded.Value); + Expanded.Value = false; } } @@ -220,17 +225,35 @@ namespace osu.Game.Overlays.Mods private partial class FocusGrabbingContainer : InputBlockingContainer { public IBindable Expanded { get; } = new BindableBool(); + public IBindable ExpandedByHovering { get; } = new BindableBool(); public override bool RequestsFocus => Expanded.Value; public override bool AcceptsFocus => Expanded.Value; public new ModCustomisationPanel? Parent => (ModCustomisationPanel?)base.Parent; + private InputManager inputManager = null!; + + protected override void LoadComplete() + { + inputManager = GetContainingInputManager(); + } + protected override void OnHoverLost(HoverLostEvent e) { - Parent?.UpdateHoverExpansion(false); + if (ExpandedByHovering.Value && !hasHoveredchild()) + Parent?.UpdateHoverExpansion(false); + base.OnHoverLost(e); } + + private bool hasHoveredchild() + { + return inputManager.HoveredDrawables.Any(parentIsThis); + + bool parentIsThis(Drawable d) + => d is not null && (d == this || parentIsThis(d.Parent)); + } } } } From fc842868a98132c2cedb5952b26cb55feb9b2a7e Mon Sep 17 00:00:00 2001 From: Caiyi Shyu Date: Sat, 27 Jul 2024 16:24:20 +0800 Subject: [PATCH 1666/2493] fix code quality --- osu.Game/Overlays/Mods/ModCustomisationPanel.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModCustomisationPanel.cs b/osu.Game/Overlays/Mods/ModCustomisationPanel.cs index d906e704e0..f17d2f39e6 100644 --- a/osu.Game/Overlays/Mods/ModCustomisationPanel.cs +++ b/osu.Game/Overlays/Mods/ModCustomisationPanel.cs @@ -236,7 +236,8 @@ namespace osu.Game.Overlays.Mods protected override void LoadComplete() { - inputManager = GetContainingInputManager(); + base.LoadComplete(); + inputManager = GetContainingInputManager()!; } protected override void OnHoverLost(HoverLostEvent e) @@ -251,7 +252,7 @@ namespace osu.Game.Overlays.Mods { return inputManager.HoveredDrawables.Any(parentIsThis); - bool parentIsThis(Drawable d) + bool parentIsThis(Drawable? d) => d is not null && (d == this || parentIsThis(d.Parent)); } } From c2711d0c4e3b5e33982f31d1344075a62fc5814a Mon Sep 17 00:00:00 2001 From: normalid Date: Sat, 27 Jul 2024 17:25:44 +0800 Subject: [PATCH 1667/2493] Implement chatline background altering --- osu.Game/Overlays/Chat/ChatLine.cs | 23 +++++++++++++++++++++++ osu.Game/Overlays/Chat/DrawableChannel.cs | 8 ++++++++ 2 files changed, 31 insertions(+) diff --git a/osu.Game/Overlays/Chat/ChatLine.cs b/osu.Game/Overlays/Chat/ChatLine.cs index 9bcca3ac9d..922d040d54 100644 --- a/osu.Game/Overlays/Chat/ChatLine.cs +++ b/osu.Game/Overlays/Chat/ChatLine.cs @@ -69,6 +69,29 @@ namespace osu.Game.Overlays.Chat private Container? highlight; + private Drawable? background; + + private bool alteringBackground; + + public bool AlteringBackground + { + get => alteringBackground; + set + { + alteringBackground = value; + + if (background == null) + AddInternal(background = new Box + { + BypassAutoSizeAxes = Axes.Both, + RelativeSizeAxes = Axes.Both, + Colour = Color4.White, + }); + + background.Alpha = value ? 0.04f : 0f; + } + } + /// /// The colour used to paint the author's username. /// diff --git a/osu.Game/Overlays/Chat/DrawableChannel.cs b/osu.Game/Overlays/Chat/DrawableChannel.cs index aa17df4907..c817417a44 100644 --- a/osu.Game/Overlays/Chat/DrawableChannel.cs +++ b/osu.Game/Overlays/Chat/DrawableChannel.cs @@ -104,6 +104,13 @@ namespace osu.Game.Overlays.Chat highlightedMessage.Value = null; }); + private void processMessageBackgroundAltering() + { + for (int i = 0; i < ChatLineFlow.Count(); i++) + if (ChatLineFlow[i] is ChatLine chatline) + chatline.AlteringBackground = i % 2 == 0; + } + protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); @@ -158,6 +165,7 @@ namespace osu.Game.Overlays.Chat scroll.ScrollToEnd(); processMessageHighlighting(); + processMessageBackgroundAltering(); }); private void pendingMessageResolved(Message existing, Message updated) => Schedule(() => From 77d64e0c3d593d4b912a6fc8d2f1e16a9e46e9b8 Mon Sep 17 00:00:00 2001 From: Caiyi Shyu Date: Sat, 27 Jul 2024 17:59:38 +0800 Subject: [PATCH 1668/2493] replace with `ReceivePositionalInputAt` --- .../Overlays/Mods/ModCustomisationPanel.cs | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModCustomisationPanel.cs b/osu.Game/Overlays/Mods/ModCustomisationPanel.cs index f17d2f39e6..9795b61762 100644 --- a/osu.Game/Overlays/Mods/ModCustomisationPanel.cs +++ b/osu.Game/Overlays/Mods/ModCustomisationPanel.cs @@ -12,7 +12,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Shapes; -using osu.Framework.Input; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Game.Configuration; @@ -232,29 +231,13 @@ namespace osu.Game.Overlays.Mods public new ModCustomisationPanel? Parent => (ModCustomisationPanel?)base.Parent; - private InputManager inputManager = null!; - - protected override void LoadComplete() - { - base.LoadComplete(); - inputManager = GetContainingInputManager()!; - } - protected override void OnHoverLost(HoverLostEvent e) { - if (ExpandedByHovering.Value && !hasHoveredchild()) + if (ExpandedByHovering.Value && !ReceivePositionalInputAt(e.ScreenSpaceMousePosition)) Parent?.UpdateHoverExpansion(false); base.OnHoverLost(e); } - - private bool hasHoveredchild() - { - return inputManager.HoveredDrawables.Any(parentIsThis); - - bool parentIsThis(Drawable? d) - => d is not null && (d == this || parentIsThis(d.Parent)); - } } } } From 7f4bfb25a9991a3a4a6145e29c0951a35f97bd99 Mon Sep 17 00:00:00 2001 From: normalid Date: Sat, 27 Jul 2024 18:24:32 +0800 Subject: [PATCH 1669/2493] Implement unit test --- .../Visual/Online/TestSceneDrawableChannel.cs | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/osu.Game.Tests/Visual/Online/TestSceneDrawableChannel.cs b/osu.Game.Tests/Visual/Online/TestSceneDrawableChannel.cs index 4830c7b856..798bf48175 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneDrawableChannel.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneDrawableChannel.cs @@ -4,10 +4,15 @@ using System; using System.Linq; using NUnit.Framework; +using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Logging; using osu.Framework.Testing; +using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Chat; +using osu.Game.Overlays; using osu.Game.Overlays.Chat; namespace osu.Game.Tests.Visual.Online @@ -30,6 +35,8 @@ namespace osu.Game.Tests.Visual.Online { RelativeSizeAxes = Axes.Both }); + Logger.Log("v.dwadwaawddwa"); + } [Test] @@ -83,5 +90,43 @@ namespace osu.Game.Tests.Visual.Online AddUntilStep("three day separators present", () => drawableChannel.ChildrenOfType().Count() == 3); AddAssert("last day separator is from correct day", () => drawableChannel.ChildrenOfType().Last().Date.Date == new DateTime(2022, 11, 22)); } + + [Test] + public void TestBackgroundAltering() + { + var localUser = new APIUser + { + Id = 3, + Username = "LocalUser" + }; + + string uuid = Guid.NewGuid().ToString(); + + int messageCount = 1; + + AddRepeatStep($"add messages", () => + { + channel.AddNewMessages(new Message(messageCount) + { + Sender = localUser, + Content = "Hi there all!", + Timestamp = new DateTimeOffset(2022, 11, 21, 20, 11, 13, TimeSpan.Zero), + Uuid = uuid, + }); + messageCount++; + }, 10); + + + AddUntilStep("10 message present", () => drawableChannel.ChildrenOfType().Count() == 10); + + int checkCount = 0; + + AddRepeatStep("check background", () => + { + // +1 because the day separator take one index + Assert.AreEqual((checkCount + 1) % 2 == 0, drawableChannel.ChildrenOfType().ToList()[checkCount].AlteringBackground); + checkCount++; + }, 10); + } } } From 73a98b45e94d0c289efecc7fd08f44c87953e358 Mon Sep 17 00:00:00 2001 From: normalid Date: Sat, 27 Jul 2024 18:48:45 +0800 Subject: [PATCH 1670/2493] FIx code quality --- osu.Game.Tests/Visual/Online/TestSceneDrawableChannel.cs | 8 ++------ osu.Game/Overlays/Chat/ChatLine.cs | 2 ++ osu.Game/Overlays/Chat/DrawableChannel.cs | 4 ++++ 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneDrawableChannel.cs b/osu.Game.Tests/Visual/Online/TestSceneDrawableChannel.cs index 798bf48175..19f88826a7 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneDrawableChannel.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneDrawableChannel.cs @@ -4,15 +4,11 @@ using System; using System.Linq; using NUnit.Framework; -using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; using osu.Framework.Logging; using osu.Framework.Testing; -using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Chat; -using osu.Game.Overlays; using osu.Game.Overlays.Chat; namespace osu.Game.Tests.Visual.Online @@ -47,6 +43,7 @@ namespace osu.Game.Tests.Visual.Online Id = 3, Username = "LocalUser" }; + string uuid = Guid.NewGuid().ToString(); AddStep("add local echo message", () => channel.AddLocalEcho(new LocalEchoMessage { @@ -104,7 +101,7 @@ namespace osu.Game.Tests.Visual.Online int messageCount = 1; - AddRepeatStep($"add messages", () => + AddRepeatStep("add messages", () => { channel.AddNewMessages(new Message(messageCount) { @@ -116,7 +113,6 @@ namespace osu.Game.Tests.Visual.Online messageCount++; }, 10); - AddUntilStep("10 message present", () => drawableChannel.ChildrenOfType().Count() == 10); int checkCount = 0; diff --git a/osu.Game/Overlays/Chat/ChatLine.cs b/osu.Game/Overlays/Chat/ChatLine.cs index 922d040d54..fc43e38239 100644 --- a/osu.Game/Overlays/Chat/ChatLine.cs +++ b/osu.Game/Overlays/Chat/ChatLine.cs @@ -81,12 +81,14 @@ namespace osu.Game.Overlays.Chat alteringBackground = value; if (background == null) + { AddInternal(background = new Box { BypassAutoSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both, Colour = Color4.White, }); + } background.Alpha = value ? 0.04f : 0f; } diff --git a/osu.Game/Overlays/Chat/DrawableChannel.cs b/osu.Game/Overlays/Chat/DrawableChannel.cs index c817417a44..8e353bfebd 100644 --- a/osu.Game/Overlays/Chat/DrawableChannel.cs +++ b/osu.Game/Overlays/Chat/DrawableChannel.cs @@ -107,8 +107,12 @@ namespace osu.Game.Overlays.Chat private void processMessageBackgroundAltering() { for (int i = 0; i < ChatLineFlow.Count(); i++) + { if (ChatLineFlow[i] is ChatLine chatline) + { chatline.AlteringBackground = i % 2 == 0; + } + } } protected override void Dispose(bool isDisposing) From 4e44a6e7f8fbb6f60cb6a67fc57f711fa4335b3c Mon Sep 17 00:00:00 2001 From: normalid Date: Sat, 27 Jul 2024 18:55:17 +0800 Subject: [PATCH 1671/2493] Clean up code --- osu.Game.Tests/Visual/Online/TestSceneDrawableChannel.cs | 3 --- osu.Game/Overlays/Chat/DrawableChannel.cs | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneDrawableChannel.cs b/osu.Game.Tests/Visual/Online/TestSceneDrawableChannel.cs index 19f88826a7..d0f9a8c69e 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneDrawableChannel.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneDrawableChannel.cs @@ -5,7 +5,6 @@ using System; using System.Linq; using NUnit.Framework; using osu.Framework.Graphics; -using osu.Framework.Logging; using osu.Framework.Testing; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Chat; @@ -31,8 +30,6 @@ namespace osu.Game.Tests.Visual.Online { RelativeSizeAxes = Axes.Both }); - Logger.Log("v.dwadwaawddwa"); - } [Test] diff --git a/osu.Game/Overlays/Chat/DrawableChannel.cs b/osu.Game/Overlays/Chat/DrawableChannel.cs index 8e353bfebd..21af8d7305 100644 --- a/osu.Game/Overlays/Chat/DrawableChannel.cs +++ b/osu.Game/Overlays/Chat/DrawableChannel.cs @@ -106,7 +106,7 @@ namespace osu.Game.Overlays.Chat private void processMessageBackgroundAltering() { - for (int i = 0; i < ChatLineFlow.Count(); i++) + for (int i = 0; i < ChatLineFlow.Count; i++) { if (ChatLineFlow[i] is ChatLine chatline) { From aed2b3c7c681235cc365d01cc8282630558985cb Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Sat, 27 Jul 2024 17:20:22 -0700 Subject: [PATCH 1672/2493] Inherit `GrayButton` instead Also fixes hover highlight. --- osu.Game/Screens/Ranking/CollectionButton.cs | 25 ++----------- osu.Game/Screens/Ranking/FavouriteButton.cs | 37 +++++--------------- 2 files changed, 12 insertions(+), 50 deletions(-) diff --git a/osu.Game/Screens/Ranking/CollectionButton.cs b/osu.Game/Screens/Ranking/CollectionButton.cs index a3e2864c7e..8343266771 100644 --- a/osu.Game/Screens/Ranking/CollectionButton.cs +++ b/osu.Game/Screens/Ranking/CollectionButton.cs @@ -3,9 +3,7 @@ using osu.Framework.Allocation; using osu.Framework.Extensions; -using osu.Framework.Graphics; using osu.Framework.Graphics.Cursor; -using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.UserInterface; using osu.Game.Beatmaps; @@ -15,41 +13,24 @@ using osuTK; namespace osu.Game.Screens.Ranking { - public partial class CollectionButton : OsuAnimatedButton, IHasPopover + public partial class CollectionButton : GrayButton, IHasPopover { - private readonly Box background; - private readonly BeatmapInfo beatmapInfo; public CollectionButton(BeatmapInfo beatmapInfo) + : base(FontAwesome.Solid.Book) { this.beatmapInfo = beatmapInfo; Size = new Vector2(50, 30); - Children = new Drawable[] - { - background = new Box - { - RelativeSizeAxes = Axes.Both, - Depth = float.MaxValue - }, - new SpriteIcon - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Size = new Vector2(13), - Icon = FontAwesome.Solid.Book, - }, - }; - TooltipText = "collections"; } [BackgroundDependencyLoader] private void load(OsuColour colours) { - background.Colour = colours.Green; + Background.Colour = colours.Green; Action = this.ShowPopover; } diff --git a/osu.Game/Screens/Ranking/FavouriteButton.cs b/osu.Game/Screens/Ranking/FavouriteButton.cs index caa0eddb55..5f21291854 100644 --- a/osu.Game/Screens/Ranking/FavouriteButton.cs +++ b/osu.Game/Screens/Ranking/FavouriteButton.cs @@ -3,8 +3,6 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Logging; using osu.Game.Beatmaps; @@ -19,17 +17,14 @@ using osuTK; namespace osu.Game.Screens.Ranking { - public partial class FavouriteButton : OsuAnimatedButton + public partial class FavouriteButton : GrayButton { - private readonly Box background; - private readonly SpriteIcon icon; - public readonly BeatmapSetInfo BeatmapSetInfo; private APIBeatmapSet? beatmapSet; private readonly Bindable current; private PostBeatmapFavouriteRequest? favouriteRequest; - private readonly LoadingLayer loading; + private LoadingLayer loading = null!; private readonly IBindable localUser = new Bindable(); @@ -40,35 +35,21 @@ namespace osu.Game.Screens.Ranking private OsuColour colours { get; set; } = null!; public FavouriteButton(BeatmapSetInfo beatmapSetInfo) + : base(FontAwesome.Regular.Heart) { BeatmapSetInfo = beatmapSetInfo; current = new BindableWithCurrent(new BeatmapSetFavouriteState(false, 0)); Size = new Vector2(50, 30); - Children = new Drawable[] - { - background = new Box - { - RelativeSizeAxes = Axes.Both, - Depth = float.MaxValue - }, - icon = new SpriteIcon - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Size = new Vector2(13), - Icon = FontAwesome.Regular.Heart, - }, - loading = new LoadingLayer(true, false), - }; - Action = toggleFavouriteStatus; } [BackgroundDependencyLoader] private void load() { + Add(loading = new LoadingLayer(true, false)); + current.BindValueChanged(_ => updateState(), true); localUser.BindTo(api.LocalUser); @@ -147,14 +128,14 @@ namespace osu.Game.Screens.Ranking { if (current.Value.Favourited) { - background.Colour = colours.Green; - icon.Icon = FontAwesome.Solid.Heart; + Background.Colour = colours.Green; + Icon.Icon = FontAwesome.Solid.Heart; TooltipText = BeatmapsetsStrings.ShowDetailsUnfavourite; } else { - background.Colour = colours.Gray4; - icon.Icon = FontAwesome.Regular.Heart; + Background.Colour = colours.Gray4; + Icon.Icon = FontAwesome.Regular.Heart; TooltipText = BeatmapsetsStrings.ShowDetailsFavourite; } } From b5ff2dab432f7d32200e5f8c53e6d970fcd63e9a Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Sat, 27 Jul 2024 17:26:31 -0700 Subject: [PATCH 1673/2493] Move some properties/bindables around --- osu.Game/Screens/Ranking/CollectionPopover.cs | 6 +++--- osu.Game/Screens/Ranking/FavouriteButton.cs | 9 +++++++-- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/Ranking/CollectionPopover.cs b/osu.Game/Screens/Ranking/CollectionPopover.cs index 2411ab99d8..214b8fa8a9 100644 --- a/osu.Game/Screens/Ranking/CollectionPopover.cs +++ b/osu.Game/Screens/Ranking/CollectionPopover.cs @@ -27,14 +27,14 @@ namespace osu.Game.Screens.Ranking : base(false) { this.beatmapInfo = beatmapInfo; + + Margin = new MarginPadding(5); + Body.CornerRadius = 4; } [BackgroundDependencyLoader] private void load() { - Margin = new MarginPadding(5); - Body.CornerRadius = 4; - Children = new[] { new OsuMenu(Direction.Vertical, true) diff --git a/osu.Game/Screens/Ranking/FavouriteButton.cs b/osu.Game/Screens/Ranking/FavouriteButton.cs index 5f21291854..09c41e4e23 100644 --- a/osu.Game/Screens/Ranking/FavouriteButton.cs +++ b/osu.Game/Screens/Ranking/FavouriteButton.cs @@ -41,8 +41,6 @@ namespace osu.Game.Screens.Ranking current = new BindableWithCurrent(new BeatmapSetFavouriteState(false, 0)); Size = new Vector2(50, 30); - - Action = toggleFavouriteStatus; } [BackgroundDependencyLoader] @@ -50,6 +48,13 @@ namespace osu.Game.Screens.Ranking { Add(loading = new LoadingLayer(true, false)); + Action = toggleFavouriteStatus; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + current.BindValueChanged(_ => updateState(), true); localUser.BindTo(api.LocalUser); From b4ca07300ac2de20aa8f364668cfa6ce613599ae Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Sat, 27 Jul 2024 18:32:35 -0700 Subject: [PATCH 1674/2493] Use same size button for everything --- .../Visual/Ranking/TestSceneCollectionButton.cs | 7 +++---- .../Visual/Ranking/TestSceneFavouriteButton.cs | 5 ----- osu.Game/Screens/Ranking/CollectionButton.cs | 2 +- osu.Game/Screens/Ranking/FavouriteButton.cs | 2 +- osu.Game/Screens/Ranking/ResultsScreen.cs | 11 ++--------- 5 files changed, 7 insertions(+), 20 deletions(-) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneCollectionButton.cs b/osu.Game.Tests/Visual/Ranking/TestSceneCollectionButton.cs index 4449aae257..8bfa74bbce 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneCollectionButton.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneCollectionButton.cs @@ -9,7 +9,6 @@ using osu.Framework.Graphics.Cursor; using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Screens.Ranking; -using osuTK; using osuTK.Input; namespace osu.Game.Tests.Visual.Ranking @@ -30,9 +29,9 @@ namespace osu.Game.Tests.Visual.Ranking Origin = Anchor.Centre, Child = collectionButton = new CollectionButton(beatmapInfo) { - RelativeSizeAxes = Axes.None, - Size = new Vector2(50), - } + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }, }); } diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneFavouriteButton.cs b/osu.Game.Tests/Visual/Ranking/TestSceneFavouriteButton.cs index a90fbc0c84..77a63a3995 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneFavouriteButton.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneFavouriteButton.cs @@ -9,7 +9,6 @@ using osu.Game.Online.API; using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests.Responses; using osu.Game.Screens.Ranking; -using osuTK; namespace osu.Game.Tests.Visual.Ranking { @@ -27,8 +26,6 @@ namespace osu.Game.Tests.Visual.Ranking { AddStep("create button", () => Child = favourite = new FavouriteButton(beatmapSetInfo) { - RelativeSizeAxes = Axes.None, - Size = new Vector2(50), Anchor = Anchor.Centre, Origin = Anchor.Centre, }); @@ -66,8 +63,6 @@ namespace osu.Game.Tests.Visual.Ranking { AddStep("make beatmap invalid", () => Child = favourite = new FavouriteButton(invalidBeatmapSetInfo) { - RelativeSizeAxes = Axes.None, - Size = new Vector2(50), Anchor = Anchor.Centre, Origin = Anchor.Centre, }); diff --git a/osu.Game/Screens/Ranking/CollectionButton.cs b/osu.Game/Screens/Ranking/CollectionButton.cs index 8343266771..980a919a2e 100644 --- a/osu.Game/Screens/Ranking/CollectionButton.cs +++ b/osu.Game/Screens/Ranking/CollectionButton.cs @@ -22,7 +22,7 @@ namespace osu.Game.Screens.Ranking { this.beatmapInfo = beatmapInfo; - Size = new Vector2(50, 30); + Size = new Vector2(75, 30); TooltipText = "collections"; } diff --git a/osu.Game/Screens/Ranking/FavouriteButton.cs b/osu.Game/Screens/Ranking/FavouriteButton.cs index 09c41e4e23..daa6312020 100644 --- a/osu.Game/Screens/Ranking/FavouriteButton.cs +++ b/osu.Game/Screens/Ranking/FavouriteButton.cs @@ -40,7 +40,7 @@ namespace osu.Game.Screens.Ranking BeatmapSetInfo = beatmapSetInfo; current = new BindableWithCurrent(new BeatmapSetFavouriteState(false, 0)); - Size = new Vector2(50, 30); + Size = new Vector2(75, 30); } [BackgroundDependencyLoader] diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs index befd024ccb..da7a4b1e6b 100644 --- a/osu.Game/Screens/Ranking/ResultsScreen.cs +++ b/osu.Game/Screens/Ranking/ResultsScreen.cs @@ -211,17 +211,10 @@ namespace osu.Game.Screens.Ranking } if (Score?.BeatmapInfo != null) - { - buttons.Add(new CollectionButton(Score.BeatmapInfo) { Width = 75 }); - } + buttons.Add(new CollectionButton(Score.BeatmapInfo)); if (Score?.BeatmapInfo?.BeatmapSet != null && Score.BeatmapInfo.BeatmapSet.OnlineID > 0) - { - buttons.Add(new FavouriteButton(Score.BeatmapInfo.BeatmapSet) - { - Width = 75 - }); - } + buttons.Add(new FavouriteButton(Score.BeatmapInfo.BeatmapSet)); } protected override void LoadComplete() From 04b15d0d38ad1e1587493f281a14f4053cd7fe4e Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Sat, 27 Jul 2024 18:35:53 -0700 Subject: [PATCH 1675/2493] Remove unnecessary `ReceivePositionalInputAt` Results is not even using the new footer. --- osu.Game/Screens/Ranking/CollectionButton.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/osu.Game/Screens/Ranking/CollectionButton.cs b/osu.Game/Screens/Ranking/CollectionButton.cs index 980a919a2e..4d53125005 100644 --- a/osu.Game/Screens/Ranking/CollectionButton.cs +++ b/osu.Game/Screens/Ranking/CollectionButton.cs @@ -35,9 +35,6 @@ namespace osu.Game.Screens.Ranking Action = this.ShowPopover; } - // use Content for tracking input as some buttons might be temporarily hidden with DisappearToBottom, and they become hidden by moving Content away from screen. - public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => Content.ReceivePositionalInputAt(screenSpacePos); - public Popover GetPopover() => new CollectionPopover(beatmapInfo); } } From 334f5fda2d4677fd28696e528461a4b19e7b5e7e Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Sat, 27 Jul 2024 19:02:57 -0700 Subject: [PATCH 1676/2493] Remove direct margin set in popover that was causing positioning offset --- osu.Game/Screens/Ranking/CollectionPopover.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Screens/Ranking/CollectionPopover.cs b/osu.Game/Screens/Ranking/CollectionPopover.cs index 214b8fa8a9..e285c80056 100644 --- a/osu.Game/Screens/Ranking/CollectionPopover.cs +++ b/osu.Game/Screens/Ranking/CollectionPopover.cs @@ -28,7 +28,6 @@ namespace osu.Game.Screens.Ranking { this.beatmapInfo = beatmapInfo; - Margin = new MarginPadding(5); Body.CornerRadius = 4; } From bc25e5d706f86381069a00344796b7fc20446710 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Sat, 27 Jul 2024 19:13:11 -0700 Subject: [PATCH 1677/2493] Remove unnecessary depth and padding set --- osu.Game.Tests/Visual/Ranking/TestSceneCollectionButton.cs | 1 - osu.Game/Screens/Ranking/ResultsScreen.cs | 2 -- 2 files changed, 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneCollectionButton.cs b/osu.Game.Tests/Visual/Ranking/TestSceneCollectionButton.cs index 8bfa74bbce..5b6721bc0f 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneCollectionButton.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneCollectionButton.cs @@ -23,7 +23,6 @@ namespace osu.Game.Tests.Visual.Ranking { AddStep("create button", () => Child = new PopoverContainer { - Depth = -1, RelativeSizeAxes = Axes.Both, Anchor = Anchor.Centre, Origin = Anchor.Centre, diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs index 8b9606d468..4481b5f16e 100644 --- a/osu.Game/Screens/Ranking/ResultsScreen.cs +++ b/osu.Game/Screens/Ranking/ResultsScreen.cs @@ -100,9 +100,7 @@ namespace osu.Game.Screens.Ranking InternalChild = new PopoverContainer { - Depth = -1, RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding(0), Child = new GridContainer { RelativeSizeAxes = Axes.Both, From 0c89210bd7f1e578476ce9e7d2c1d2f3df7f107c Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sun, 28 Jul 2024 05:24:05 +0300 Subject: [PATCH 1678/2493] Add API models for daily challenge statistics --- .../Online/API/Requests/Responses/APIUser.cs | 3 ++ .../APIUserDailyChallengeStatistics.cs | 41 +++++++++++++++++++ 2 files changed, 44 insertions(+) create mode 100644 osu.Game/Online/API/Requests/Responses/APIUserDailyChallengeStatistics.cs diff --git a/osu.Game/Online/API/Requests/Responses/APIUser.cs b/osu.Game/Online/API/Requests/Responses/APIUser.cs index a2836476c5..c69e45b3fd 100644 --- a/osu.Game/Online/API/Requests/Responses/APIUser.cs +++ b/osu.Game/Online/API/Requests/Responses/APIUser.cs @@ -272,6 +272,9 @@ namespace osu.Game.Online.API.Requests.Responses [JsonProperty("groups")] public APIUserGroup[] Groups; + [JsonProperty("daily_challenge_user_stats")] + public APIUserDailyChallengeStatistics DailyChallengeStatistics = new APIUserDailyChallengeStatistics(); + public override string ToString() => Username; /// diff --git a/osu.Game/Online/API/Requests/Responses/APIUserDailyChallengeStatistics.cs b/osu.Game/Online/API/Requests/Responses/APIUserDailyChallengeStatistics.cs new file mode 100644 index 0000000000..e77f2b8f68 --- /dev/null +++ b/osu.Game/Online/API/Requests/Responses/APIUserDailyChallengeStatistics.cs @@ -0,0 +1,41 @@ +// 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 Newtonsoft.Json; + +namespace osu.Game.Online.API.Requests.Responses +{ + public class APIUserDailyChallengeStatistics + { + [JsonProperty("user_id")] + public int UserID; + + [JsonProperty("daily_streak_best")] + public int DailyStreakBest; + + [JsonProperty("daily_streak_current")] + public int DailyStreakCurrent; + + [JsonProperty("weekly_streak_best")] + public int WeeklyStreakBest; + + [JsonProperty("weekly_streak_current")] + public int WeeklyStreakCurrent; + + [JsonProperty("top_10p_placements")] + public int Top10PercentPlacements; + + [JsonProperty("top_50p_placements")] + public int Top50PercentPlacements; + + [JsonProperty("playcount")] + public int PlayCount; + + [JsonProperty("last_update")] + public DateTimeOffset? LastUpdate; + + [JsonProperty("last_weekly_streak")] + public DateTimeOffset? LastWeeklyStreak; + } +} From 17f5d58be2fcf5c2ead8d8b76a7b2ae5281d1197 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sun, 28 Jul 2024 05:24:29 +0300 Subject: [PATCH 1679/2493] Add daily challenge streak display and tooltip --- ...tSceneUserProfileDailyChallengeOverview.cs | 63 +++++ .../Components/DailyChallengeStreakDisplay.cs | 112 ++++++++ .../Components/DailyChallengeStreakTooltip.cs | 243 ++++++++++++++++++ 3 files changed, 418 insertions(+) create mode 100644 osu.Game.Tests/Visual/Online/TestSceneUserProfileDailyChallengeOverview.cs create mode 100644 osu.Game/Overlays/Profile/Header/Components/DailyChallengeStreakDisplay.cs create mode 100644 osu.Game/Overlays/Profile/Header/Components/DailyChallengeStreakTooltip.cs diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserProfileDailyChallengeOverview.cs b/osu.Game.Tests/Visual/Online/TestSceneUserProfileDailyChallengeOverview.cs new file mode 100644 index 0000000000..e2d26f222c --- /dev/null +++ b/osu.Game.Tests/Visual/Online/TestSceneUserProfileDailyChallengeOverview.cs @@ -0,0 +1,63 @@ +// 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; +using osu.Framework.Graphics.Shapes; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Overlays; +using osu.Game.Overlays.Profile; +using osu.Game.Overlays.Profile.Header.Components; +using osu.Game.Rulesets.Osu; +using osuTK; + +namespace osu.Game.Tests.Visual.Online +{ + public partial class TestSceneUserProfileDailyChallenge : OsuManualInputManagerTestScene + { + [Cached] + public readonly Bindable User = new Bindable(new UserProfileData(new APIUser(), new OsuRuleset().RulesetInfo)); + + [Cached] + private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Pink); + + protected override void LoadComplete() + { + base.LoadComplete(); + + DailyChallengeStreakDisplay display = null!; + + AddSliderStep("daily", 0, 999, 2, v => update(s => s.DailyStreakCurrent = v)); + AddSliderStep("daily best", 0, 999, 2, v => update(s => s.DailyStreakBest = v)); + AddSliderStep("weekly", 0, 250, 1, v => update(s => s.WeeklyStreakCurrent = v)); + AddSliderStep("weekly best", 0, 250, 1, v => update(s => s.WeeklyStreakBest = v)); + AddSliderStep("top 10%", 0, 999, 0, v => update(s => s.Top10PercentPlacements = v)); + AddSliderStep("top 50%", 0, 999, 0, v => update(s => s.Top50PercentPlacements = v)); + AddStep("create", () => + { + Clear(); + Add(new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background2, + }); + Add(display = new DailyChallengeStreakDisplay + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Scale = new Vector2(1f), + User = { BindTarget = User }, + }); + }); + AddStep("hover", () => InputManager.MoveMouseTo(display)); + } + + private void update(Action change) + { + change.Invoke(User.Value!.User.DailyChallengeStatistics); + User.Value = new UserProfileData(User.Value.User, User.Value.Ruleset); + } + } +} diff --git a/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStreakDisplay.cs b/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStreakDisplay.cs new file mode 100644 index 0000000000..2d9b107367 --- /dev/null +++ b/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStreakDisplay.cs @@ -0,0 +1,112 @@ +// 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.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; +using osu.Framework.Graphics.Shapes; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Sprites; +using osu.Game.Online.API.Requests.Responses; + +namespace osu.Game.Overlays.Profile.Header.Components +{ + public partial class DailyChallengeStreakDisplay : CompositeDrawable, IHasCustomTooltip + { + public readonly Bindable User = new Bindable(); + + public APIUserDailyChallengeStatistics? TooltipContent { get; private set; } + + private OsuSpriteText dailyStreak = null!; + + [Resolved] + private OverlayColourProvider colourProvider { get; set; } = null!; + + [Resolved] + private OsuColour colours { get; set; } = null!; + + [BackgroundDependencyLoader] + private void load() + { + AutoSizeAxes = Axes.Both; + CornerRadius = 5; + Masking = true; + + InternalChildren = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background4, + }, + new FillFlowContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Padding = new MarginPadding(5f), + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Children = new Drawable[] + { + new OsuTextFlowContainer(s => s.Font = OsuFont.GetFont(size: 12)) + { + AutoSizeAxes = Axes.Both, + // Text = UsersStrings.ShowDailyChallengeTitle + Text = "Daily\nChallenge", + Margin = new MarginPadding { Horizontal = 5f, Bottom = 2f }, + }, + new Container + { + AutoSizeAxes = Axes.X, + RelativeSizeAxes = Axes.Y, + CornerRadius = 5f, + Masking = true, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background6, + }, + dailyStreak = new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + UseFullGlyphHeight = false, + Colour = colourProvider.Content2, + Margin = new MarginPadding { Horizontal = 10f, Vertical = 5f }, + }, + } + }, + } + }, + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + User.BindValueChanged(_ => updateDisplay(), true); + } + + private void updateDisplay() + { + if (User.Value == null) + { + dailyStreak.Text = "-"; + return; + } + + var statistics = User.Value.User.DailyChallengeStatistics; + // dailyStreak.Text = UsersStrings.ShowDailyChallengeUnitDay(statistics.DailyStreakCurrent); + dailyStreak.Text = $"{statistics.DailyStreakCurrent}d"; + dailyStreak.Colour = colours.ForRankingTier(DailyChallengeStreakTooltip.TierForDaily(statistics.DailyStreakCurrent)); + TooltipContent = statistics; + } + + public ITooltip GetCustomTooltip() => new DailyChallengeStreakTooltip(colourProvider); + } +} diff --git a/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStreakTooltip.cs b/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStreakTooltip.cs new file mode 100644 index 0000000000..a95de8cefd --- /dev/null +++ b/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStreakTooltip.cs @@ -0,0 +1,243 @@ +// 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.Extensions.LocalisationExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Localisation; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Scoring; +using osuTK; + +namespace osu.Game.Overlays.Profile.Header.Components +{ + public partial class DailyChallengeStreakTooltip : VisibilityContainer, ITooltip + { + [Cached] + private readonly OverlayColourProvider colourProvider; + + private StreakPiece currentDaily = null!; + private StreakPiece currentWeekly = null!; + private StatisticsPiece bestDaily = null!; + private StatisticsPiece bestWeekly = null!; + private StatisticsPiece topTen = null!; + private StatisticsPiece topFifty = null!; + + [Resolved] + private OsuColour colours { get; set; } = null!; + + public DailyChallengeStreakTooltip(OverlayColourProvider colourProvider) + { + this.colourProvider = colourProvider; + } + + [BackgroundDependencyLoader] + private void load() + { + AutoSizeAxes = Axes.Both; + CornerRadius = 20f; + Masking = true; + + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background4, + }, + new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Children = new Drawable[] + { + new Container + { + AutoSizeAxes = Axes.Both, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background5, + }, + new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Padding = new MarginPadding(15f), + Spacing = new Vector2(30f), + Children = new[] + { + // currentDaily = new StreakPiece(UsersStrings.ShowDailyChallengeDailyStreakCurrent), + // currentWeekly = new StreakPiece(UsersStrings.ShowDailyChallengeWeeklyStreakCurrent), + currentDaily = new StreakPiece("Current Daily Streak"), + currentWeekly = new StreakPiece("Current Weekly Streak"), + } + }, + } + }, + new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding(15f), + Spacing = new Vector2(10f), + Children = new[] + { + // bestDaily = new StatisticsPiece(UsersStrings.ShowDailyChallengeDailyStreakBest), + // bestWeekly = new StatisticsPiece(UsersStrings.ShowDailyChallengeWeeklyStreakBest), + // topTen = new StatisticsPiece(UsersStrings.ShowDailyChallengeTop10pPlacements), + // topFifty = new StatisticsPiece(UsersStrings.ShowDailyChallengeTop50pPlacements), + bestDaily = new StatisticsPiece("Best Daily Streak"), + bestWeekly = new StatisticsPiece("Best Weekly Streak"), + topTen = new StatisticsPiece("Top 10% Placements"), + topFifty = new StatisticsPiece("Top 50% Placements"), + } + }, + } + } + }; + } + + public void SetContent(APIUserDailyChallengeStatistics content) + { + // currentDaily.Value = UsersStrings.ShowDailyChallengeUnitDay(content.DailyStreakCurrent.ToLocalisableString(@"N0")); + currentDaily.Value = $"{content.DailyStreakCurrent:N0}d"; + currentDaily.ValueColour = colours.ForRankingTier(TierForDaily(content.DailyStreakCurrent)); + + // currentWeekly.Value = UsersStrings.ShowDailyChallengeUnitWeek(content.WeeklyStreakCurrent.ToLocalisableString(@"N0")); + currentWeekly.Value = $"{content.WeeklyStreakCurrent:N0}w"; + currentWeekly.ValueColour = colours.ForRankingTier(TierForWeekly(content.WeeklyStreakCurrent)); + + // bestDaily.Value = UsersStrings.ShowDailyChallengeUnitDay(content.DailyStreakBest.ToLocalisableString(@"N0")); + bestDaily.Value = $"{content.DailyStreakBest:N0}d"; + bestDaily.ValueColour = colours.ForRankingTier(TierForDaily(content.DailyStreakBest)); + + // bestWeekly.Value = UsersStrings.ShowDailyChallengeUnitWeek(content.WeeklyStreakBest.ToLocalisableString(@"N0")); + bestWeekly.Value = $"{content.WeeklyStreakBest:N0}w"; + bestWeekly.ValueColour = colours.ForRankingTier(TierForWeekly(content.WeeklyStreakBest)); + + topTen.Value = content.Top10PercentPlacements.ToLocalisableString(@"N0"); + topFifty.Value = content.Top50PercentPlacements.ToLocalisableString(@"N0"); + } + + // reference: https://github.com/ppy/osu-web/blob/8206e0e91eeea80ccf92f0586561346dd40e085e/resources/js/profile-page/daily-challenge.tsx#L13-L43 + public static RankingTier TierForDaily(int daily) + { + if (daily > 360) + return RankingTier.Lustrous; + + if (daily > 240) + return RankingTier.Radiant; + + if (daily > 120) + return RankingTier.Rhodium; + + if (daily > 60) + return RankingTier.Platinum; + + if (daily > 30) + return RankingTier.Gold; + + if (daily > 10) + return RankingTier.Silver; + + if (daily > 5) + return RankingTier.Bronze; + + return RankingTier.Iron; + } + + public static RankingTier TierForWeekly(int weekly) => TierForDaily((weekly - 1) * 7); + + protected override void PopIn() => this.FadeIn(200, Easing.OutQuint); + + protected override void PopOut() => this.FadeOut(200, Easing.OutQuint); + + public void Move(Vector2 pos) => Position = pos; + + private partial class StreakPiece : FillFlowContainer + { + private readonly OsuSpriteText valueText; + + public LocalisableString Value + { + set => valueText.Text = value; + } + + public ColourInfo ValueColour + { + set => valueText.Colour = value; + } + + public StreakPiece(LocalisableString title) + { + AutoSizeAxes = Axes.Both; + Direction = FillDirection.Vertical; + + Children = new Drawable[] + { + new OsuSpriteText + { + Font = OsuFont.GetFont(size: 12), + Text = title, + }, + valueText = new OsuSpriteText + { + // Colour = colour + Font = OsuFont.GetFont(size: 40, weight: FontWeight.Light), + } + }; + } + } + + private partial class StatisticsPiece : CompositeDrawable + { + private readonly OsuSpriteText valueText; + + public LocalisableString Value + { + set => valueText.Text = value; + } + + public ColourInfo ValueColour + { + set => valueText.Colour = value; + } + + public StatisticsPiece(LocalisableString title) + { + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + + InternalChildren = new Drawable[] + { + new OsuSpriteText + { + Font = OsuFont.GetFont(size: 12), + Text = title, + }, + valueText = new OsuSpriteText + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + Font = OsuFont.GetFont(size: 12), + } + }; + } + + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider) + { + valueText.Colour = colourProvider.Content2; + } + } + } +} From e82c54a31cf06fe64ff4a6fad31f7d9eff6aff19 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sun, 28 Jul 2024 05:57:30 +0300 Subject: [PATCH 1680/2493] Integrate daily challenge streak display with user profile overlay --- .../Online/TestSceneUserProfileOverlay.cs | 9 ++++ .../Profile/Header/Components/MainDetails.cs | 41 ++++++++++++++----- 2 files changed, 39 insertions(+), 11 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs index 8dbd493920..937e08cb97 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs @@ -282,6 +282,15 @@ namespace osu.Game.Tests.Visual.Online ImageUrlLowRes = "https://assets.ppy.sh/profile-badges/contributor.png", }, }, + DailyChallengeStatistics = new APIUserDailyChallengeStatistics + { + DailyStreakCurrent = 231, + WeeklyStreakCurrent = 18, + DailyStreakBest = 370, + WeeklyStreakBest = 51, + Top10PercentPlacements = 345, + Top50PercentPlacements = 427, + }, Title = "osu!volunteer", Colour = "ff0000", Achievements = Array.Empty(), diff --git a/osu.Game/Overlays/Profile/Header/Components/MainDetails.cs b/osu.Game/Overlays/Profile/Header/Components/MainDetails.cs index 2505c1bc8c..f9a4267ed9 100644 --- a/osu.Game/Overlays/Profile/Header/Components/MainDetails.cs +++ b/osu.Game/Overlays/Profile/Header/Components/MainDetails.cs @@ -44,22 +44,41 @@ namespace osu.Game.Overlays.Profile.Header.Components Spacing = new Vector2(0, 15), Children = new Drawable[] { - new FillFlowContainer + new GridContainer { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(20), - Children = new Drawable[] + ColumnDimensions = new[] { - detailGlobalRank = new ProfileValueDisplay(true) + new Dimension(GridSizeMode.AutoSize), + new Dimension(GridSizeMode.Absolute, 20), + new Dimension(), + new Dimension(GridSizeMode.AutoSize), + }, + RowDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize), + }, + Content = new[] + { + new[] { - Title = UsersStrings.ShowRankGlobalSimple, - }, - detailCountryRank = new ProfileValueDisplay(true) - { - Title = UsersStrings.ShowRankCountrySimple, - }, + detailGlobalRank = new ProfileValueDisplay(true) + { + Title = UsersStrings.ShowRankGlobalSimple, + }, + Empty(), + detailCountryRank = new ProfileValueDisplay(true) + { + Title = UsersStrings.ShowRankCountrySimple, + }, + new DailyChallengeStreakDisplay + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + User = { BindTarget = User }, + } + } } }, new Container From 31787757efefd9c2d0bc9f9f2dfb92942783f4da Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sun, 28 Jul 2024 06:21:21 +0300 Subject: [PATCH 1681/2493] Provide colour scheme as part of tooltip data to handle reusing tooltip with different profile hues --- .../Components/DailyChallengeStreakDisplay.cs | 9 ++- .../Components/DailyChallengeStreakTooltip.cs | 64 +++++++++---------- 2 files changed, 35 insertions(+), 38 deletions(-) diff --git a/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStreakDisplay.cs b/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStreakDisplay.cs index 2d9b107367..87f833d165 100644 --- a/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStreakDisplay.cs +++ b/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStreakDisplay.cs @@ -10,15 +10,14 @@ using osu.Framework.Graphics.Shapes; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; -using osu.Game.Online.API.Requests.Responses; namespace osu.Game.Overlays.Profile.Header.Components { - public partial class DailyChallengeStreakDisplay : CompositeDrawable, IHasCustomTooltip + public partial class DailyChallengeStreakDisplay : CompositeDrawable, IHasCustomTooltip { public readonly Bindable User = new Bindable(); - public APIUserDailyChallengeStatistics? TooltipContent { get; private set; } + public DailyChallengeStreakTooltipData? TooltipContent { get; private set; } private OsuSpriteText dailyStreak = null!; @@ -104,9 +103,9 @@ namespace osu.Game.Overlays.Profile.Header.Components // dailyStreak.Text = UsersStrings.ShowDailyChallengeUnitDay(statistics.DailyStreakCurrent); dailyStreak.Text = $"{statistics.DailyStreakCurrent}d"; dailyStreak.Colour = colours.ForRankingTier(DailyChallengeStreakTooltip.TierForDaily(statistics.DailyStreakCurrent)); - TooltipContent = statistics; + TooltipContent = new DailyChallengeStreakTooltipData(colourProvider, statistics); } - public ITooltip GetCustomTooltip() => new DailyChallengeStreakTooltip(colourProvider); + public ITooltip GetCustomTooltip() => new DailyChallengeStreakTooltip(); } } diff --git a/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStreakTooltip.cs b/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStreakTooltip.cs index a95de8cefd..9dc4dfcb9c 100644 --- a/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStreakTooltip.cs +++ b/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStreakTooltip.cs @@ -17,11 +17,8 @@ using osuTK; namespace osu.Game.Overlays.Profile.Header.Components { - public partial class DailyChallengeStreakTooltip : VisibilityContainer, ITooltip + public partial class DailyChallengeStreakTooltip : VisibilityContainer, ITooltip { - [Cached] - private readonly OverlayColourProvider colourProvider; - private StreakPiece currentDaily = null!; private StreakPiece currentWeekly = null!; private StatisticsPiece bestDaily = null!; @@ -29,14 +26,12 @@ namespace osu.Game.Overlays.Profile.Header.Components private StatisticsPiece topTen = null!; private StatisticsPiece topFifty = null!; + private Box topBackground = null!; + private Box background = null!; + [Resolved] private OsuColour colours { get; set; } = null!; - public DailyChallengeStreakTooltip(OverlayColourProvider colourProvider) - { - this.colourProvider = colourProvider; - } - [BackgroundDependencyLoader] private void load() { @@ -46,10 +41,9 @@ namespace osu.Game.Overlays.Profile.Header.Components Children = new Drawable[] { - new Box + background = new Box { RelativeSizeAxes = Axes.Both, - Colour = colourProvider.Background4, }, new FillFlowContainer { @@ -62,10 +56,9 @@ namespace osu.Game.Overlays.Profile.Header.Components AutoSizeAxes = Axes.Both, Children = new Drawable[] { - new Box + topBackground = new Box { RelativeSizeAxes = Axes.Both, - Colour = colourProvider.Background5, }, new FillFlowContainer { @@ -106,26 +99,35 @@ namespace osu.Game.Overlays.Profile.Header.Components }; } - public void SetContent(APIUserDailyChallengeStatistics content) + public void SetContent(DailyChallengeStreakTooltipData content) { + var statistics = content.Statistics; + var colourProvider = content.ColourProvider; + + background.Colour = colourProvider.Background4; + topBackground.Colour = colourProvider.Background5; + // currentDaily.Value = UsersStrings.ShowDailyChallengeUnitDay(content.DailyStreakCurrent.ToLocalisableString(@"N0")); - currentDaily.Value = $"{content.DailyStreakCurrent:N0}d"; - currentDaily.ValueColour = colours.ForRankingTier(TierForDaily(content.DailyStreakCurrent)); + currentDaily.Value = $"{statistics.DailyStreakCurrent:N0}d"; + currentDaily.ValueColour = colours.ForRankingTier(TierForDaily(statistics.DailyStreakCurrent)); - // currentWeekly.Value = UsersStrings.ShowDailyChallengeUnitWeek(content.WeeklyStreakCurrent.ToLocalisableString(@"N0")); - currentWeekly.Value = $"{content.WeeklyStreakCurrent:N0}w"; - currentWeekly.ValueColour = colours.ForRankingTier(TierForWeekly(content.WeeklyStreakCurrent)); + // currentWeekly.Value = UsersStrings.ShowDailyChallengeUnitWeek(statistics.WeeklyStreakCurrent.ToLocalisableString(@"N0")); + currentWeekly.Value = $"{statistics.WeeklyStreakCurrent:N0}w"; + currentWeekly.ValueColour = colours.ForRankingTier(TierForWeekly(statistics.WeeklyStreakCurrent)); - // bestDaily.Value = UsersStrings.ShowDailyChallengeUnitDay(content.DailyStreakBest.ToLocalisableString(@"N0")); - bestDaily.Value = $"{content.DailyStreakBest:N0}d"; - bestDaily.ValueColour = colours.ForRankingTier(TierForDaily(content.DailyStreakBest)); + // bestDaily.Value = UsersStrings.ShowDailyChallengeUnitDay(statistics.DailyStreakBest.ToLocalisableString(@"N0")); + bestDaily.Value = $"{statistics.DailyStreakBest:N0}d"; + bestDaily.ValueColour = colours.ForRankingTier(TierForDaily(statistics.DailyStreakBest)); - // bestWeekly.Value = UsersStrings.ShowDailyChallengeUnitWeek(content.WeeklyStreakBest.ToLocalisableString(@"N0")); - bestWeekly.Value = $"{content.WeeklyStreakBest:N0}w"; - bestWeekly.ValueColour = colours.ForRankingTier(TierForWeekly(content.WeeklyStreakBest)); + // bestWeekly.Value = UsersStrings.ShowDailyChallengeUnitWeek(statistics.WeeklyStreakBest.ToLocalisableString(@"N0")); + bestWeekly.Value = $"{statistics.WeeklyStreakBest:N0}w"; + bestWeekly.ValueColour = colours.ForRankingTier(TierForWeekly(statistics.WeeklyStreakBest)); - topTen.Value = content.Top10PercentPlacements.ToLocalisableString(@"N0"); - topFifty.Value = content.Top50PercentPlacements.ToLocalisableString(@"N0"); + topTen.Value = statistics.Top10PercentPlacements.ToLocalisableString(@"N0"); + topTen.ValueColour = colourProvider.Content2; + + topFifty.Value = statistics.Top50PercentPlacements.ToLocalisableString(@"N0"); + topFifty.ValueColour = colourProvider.Content2; } // reference: https://github.com/ppy/osu-web/blob/8206e0e91eeea80ccf92f0586561346dd40e085e/resources/js/profile-page/daily-challenge.tsx#L13-L43 @@ -232,12 +234,8 @@ namespace osu.Game.Overlays.Profile.Header.Components } }; } - - [BackgroundDependencyLoader] - private void load(OverlayColourProvider colourProvider) - { - valueText.Colour = colourProvider.Content2; - } } } + + public record DailyChallengeStreakTooltipData(OverlayColourProvider ColourProvider, APIUserDailyChallengeStatistics Statistics); } From 6bdb1107c157a8feb8ded4a383dced04e05026e9 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sun, 28 Jul 2024 06:34:59 +0300 Subject: [PATCH 1682/2493] Add shadow over tooltip --- .../Header/Components/DailyChallengeStreakTooltip.cs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStreakTooltip.cs b/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStreakTooltip.cs index 9dc4dfcb9c..02be0f2c99 100644 --- a/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStreakTooltip.cs +++ b/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStreakTooltip.cs @@ -2,18 +2,21 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; +using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; -using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Effects; using osu.Framework.Localisation; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Online.API.Requests.Responses; using osu.Game.Scoring; using osuTK; +using Box = osu.Framework.Graphics.Shapes.Box; +using Color4 = osuTK.Graphics.Color4; namespace osu.Game.Overlays.Profile.Header.Components { @@ -39,6 +42,13 @@ namespace osu.Game.Overlays.Profile.Header.Components CornerRadius = 20f; Masking = true; + EdgeEffect = new EdgeEffectParameters + { + Type = EdgeEffectType.Shadow, + Colour = Color4.Black.Opacity(0.5f), + Radius = 30f, + }; + Children = new Drawable[] { background = new Box From 82fbd5b045b2487c70e69fa0bf3fdd956b81967b Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sun, 28 Jul 2024 06:40:16 +0300 Subject: [PATCH 1683/2493] Rename file --- ...ChallengeOverview.cs => TestSceneUserProfileDailyChallenge.cs} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename osu.Game.Tests/Visual/Online/{TestSceneUserProfileDailyChallengeOverview.cs => TestSceneUserProfileDailyChallenge.cs} (100%) diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserProfileDailyChallengeOverview.cs b/osu.Game.Tests/Visual/Online/TestSceneUserProfileDailyChallenge.cs similarity index 100% rename from osu.Game.Tests/Visual/Online/TestSceneUserProfileDailyChallengeOverview.cs rename to osu.Game.Tests/Visual/Online/TestSceneUserProfileDailyChallenge.cs From 7fedfd368c83767846e947372e9fba03e07f6ceb Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sun, 28 Jul 2024 07:22:58 +0300 Subject: [PATCH 1684/2493] Fix score breakdown tooltips appearing in other feeds --- .../OnlinePlay/DailyChallenge/DailyChallengeScoreBreakdown.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeScoreBreakdown.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeScoreBreakdown.cs index cfec170cf6..12401061a3 100644 --- a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeScoreBreakdown.cs +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeScoreBreakdown.cs @@ -27,6 +27,9 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge private FillFlowContainer barsContainer = null!; + // we're always present so that we can update while hidden, but we don't want tooltips to be displayed, therefore directly use alpha comparison here. + public override bool PropagatePositionalInputSubTree => base.PropagatePositionalInputSubTree && Alpha > 0; + private const int bin_count = MultiplayerPlaylistItemStats.TOTAL_SCORE_DISTRIBUTION_BINS; private long[] bins = new long[bin_count]; From f6eb9037df1d1f2bfd3d2285c20752923403f3d1 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Sat, 27 Jul 2024 23:50:52 -0700 Subject: [PATCH 1685/2493] Add ability to copy leaderboard mods in daily challenge --- .../DailyChallenge/DailyChallenge.cs | 6 +++-- .../DailyChallengeLeaderboard.cs | 20 +++++++++++++++++ .../Leaderboards/LeaderboardScoreV2.cs | 22 ++++++++++++++----- 3 files changed, 41 insertions(+), 7 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs index 8538cbbb59..e1f78129a4 100644 --- a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs @@ -21,6 +21,7 @@ using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Database; using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Cursor; using osu.Game.Graphics.UserInterface; using osu.Game.Localisation; using osu.Game.Online.API; @@ -168,7 +169,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge }, null, [ - new Container + new OsuContextMenuContainer { RelativeSizeAxes = Axes.Both, Masking = true, @@ -238,6 +239,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge { RelativeSizeAxes = Axes.Both, PresentScore = presentScore, + SelectedMods = { BindTarget = userMods }, }, // Spacer null, @@ -329,7 +331,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge var rulesetInstance = rulesets.GetRuleset(playlistItem.RulesetID)!.CreateInstance(); var allowedMods = playlistItem.AllowedMods.Select(m => m.ToMod(rulesetInstance)); - userModsSelectOverlay.IsValidMod = m => allowedMods.Any(a => a.GetType() == m.GetType()); + userModsSelectOverlay.IsValidMod = leaderboard.IsValidMod = m => allowedMods.Any(a => a.GetType() == m.GetType()); } metadataClient.MultiplayerRoomScoreSet += onRoomScoreSet; diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeLeaderboard.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeLeaderboard.cs index 5efb656cea..f332e717c1 100644 --- a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeLeaderboard.cs +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeLeaderboard.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; using System.Linq; using System.Threading; using osu.Framework.Allocation; @@ -14,6 +15,7 @@ using osu.Game.Graphics.UserInterface; using osu.Game.Online.API; using osu.Game.Online.Rooms; using osu.Game.Rulesets; +using osu.Game.Rulesets.Mods; using osu.Game.Scoring; using osu.Game.Screens.SelectV2.Leaderboards; using osuTK; @@ -24,6 +26,20 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge { public IBindable UserBestScore => userBestScore; private readonly Bindable userBestScore = new Bindable(); + public Bindable> SelectedMods = new Bindable>(); + + private Func isValidMod = _ => true; + + /// + /// A function determining whether each mod in the score can be selected. + /// A return value of means that the mod can be selected in the current context. + /// A return value of means that the mod cannot be selected in the current context. + /// + public Func IsValidMod + { + get => isValidMod; + set => isValidMod = value ?? throw new ArgumentNullException(nameof(value)); + } public Action? PresentScore { get; init; } @@ -153,6 +169,8 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge Rank = index + 1, IsPersonalBest = s.UserID == api.LocalUser.Value.Id, Action = () => PresentScore?.Invoke(s.OnlineID), + SelectedMods = { BindTarget = SelectedMods }, + IsValidMod = isValidMod, }), loaded => { scoreFlow.Clear(); @@ -171,6 +189,8 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge Rank = userBest.Position, IsPersonalBest = true, Action = () => PresentScore?.Invoke(userBest.OnlineID), + SelectedMods = { BindTarget = SelectedMods }, + IsValidMod = isValidMod, }); } diff --git a/osu.Game/Screens/SelectV2/Leaderboards/LeaderboardScoreV2.cs b/osu.Game/Screens/SelectV2/Leaderboards/LeaderboardScoreV2.cs index 700f889d7f..6066bc7739 100644 --- a/osu.Game/Screens/SelectV2/Leaderboards/LeaderboardScoreV2.cs +++ b/osu.Game/Screens/SelectV2/Leaderboards/LeaderboardScoreV2.cs @@ -43,6 +43,21 @@ namespace osu.Game.Screens.SelectV2.Leaderboards { public partial class LeaderboardScoreV2 : OsuClickableContainer, IHasContextMenu, IHasCustomTooltip { + public Bindable> SelectedMods = new Bindable>(); + + private Func isValidMod = _ => true; + + /// + /// A function determining whether each mod in the score can be selected. + /// A return value of means that the mod can be selected in the current context. + /// A return value of means that the mod cannot be selected in the current context. + /// + public Func IsValidMod + { + get => isValidMod; + set => isValidMod = value ?? throw new ArgumentNullException(nameof(value)); + } + public int? Rank { get; init; } public bool IsPersonalBest { get; init; } @@ -68,9 +83,6 @@ namespace osu.Game.Screens.SelectV2.Leaderboards [Resolved] private OverlayColourProvider colourProvider { get; set; } = null!; - [Resolved] - private SongSelect? songSelect { get; set; } - [Resolved] private IDialogOverlay? dialogOverlay { get; set; } @@ -738,8 +750,8 @@ namespace osu.Game.Screens.SelectV2.Leaderboards { List items = new List(); - if (score.Mods.Length > 0 && songSelect != null) - items.Add(new OsuMenuItem("Use these mods", MenuItemType.Highlighted, () => songSelect.Mods.Value = score.Mods)); + if (score.Mods.Length > 0) + items.Add(new OsuMenuItem("Use these mods", MenuItemType.Highlighted, () => SelectedMods.Value = score.Mods.Where(m => isValidMod.Invoke(m)).ToArray())); if (score.Files.Count <= 0) return items.ToArray(); From e58bdbb8a944b0c73623d3ab0867b51f484fab04 Mon Sep 17 00:00:00 2001 From: normalid Date: Sun, 28 Jul 2024 15:08:36 +0800 Subject: [PATCH 1686/2493] Improve unit test --- osu.Game.Tests/Visual/Online/TestSceneDrawableChannel.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneDrawableChannel.cs b/osu.Game.Tests/Visual/Online/TestSceneDrawableChannel.cs index d0f9a8c69e..795d49adad 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneDrawableChannel.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneDrawableChannel.cs @@ -94,8 +94,6 @@ namespace osu.Game.Tests.Visual.Online Username = "LocalUser" }; - string uuid = Guid.NewGuid().ToString(); - int messageCount = 1; AddRepeatStep("add messages", () => @@ -104,8 +102,8 @@ namespace osu.Game.Tests.Visual.Online { Sender = localUser, Content = "Hi there all!", - Timestamp = new DateTimeOffset(2022, 11, 21, 20, 11, 13, TimeSpan.Zero), - Uuid = uuid, + Timestamp = new DateTimeOffset(2022, 11, 21, 20, messageCount, 13, TimeSpan.Zero), + Uuid = Guid.NewGuid().ToString(), }); messageCount++; }, 10); From 5db0e3640436eaf5fe58f21d98f5ea6d19e3b335 Mon Sep 17 00:00:00 2001 From: normalid Date: Sun, 28 Jul 2024 16:18:43 +0800 Subject: [PATCH 1687/2493] Use the `TruncatingSpriteText` in `ModPresetTooltip` --- osu.Game/Overlays/Mods/ModPresetTooltip.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Mods/ModPresetTooltip.cs b/osu.Game/Overlays/Mods/ModPresetTooltip.cs index ec81aa7ceb..8f4efa7667 100644 --- a/osu.Game/Overlays/Mods/ModPresetTooltip.cs +++ b/osu.Game/Overlays/Mods/ModPresetTooltip.cs @@ -44,10 +44,12 @@ namespace osu.Game.Overlays.Mods Spacing = new Vector2(7), Children = new[] { - descriptionText = new OsuSpriteText + descriptionText = new TruncatingSpriteText { + RelativeSizeAxes = Axes.X, Font = OsuFont.GetFont(weight: FontWeight.Regular), Colour = colourProvider.Content1, + AllowMultiline = true, }, } } From 4e65944609d7c907fa10c8f9af7e376115dcefe2 Mon Sep 17 00:00:00 2001 From: normalid Date: Sun, 28 Jul 2024 16:26:18 +0800 Subject: [PATCH 1688/2493] Make the tooltips width be dyanmic with the content, so the long text wont occurs wierd line break --- osu.Game/Overlays/Mods/ModPresetRow.cs | 3 +-- osu.Game/Overlays/Mods/ModPresetTooltip.cs | 17 +++++++++-------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModPresetRow.cs b/osu.Game/Overlays/Mods/ModPresetRow.cs index 4829e93b87..8614806085 100644 --- a/osu.Game/Overlays/Mods/ModPresetRow.cs +++ b/osu.Game/Overlays/Mods/ModPresetRow.cs @@ -24,8 +24,7 @@ namespace osu.Game.Overlays.Mods { new FillFlowContainer { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, + AutoSizeAxes = Axes.Both, Direction = FillDirection.Horizontal, Spacing = new Vector2(7), Children = new Drawable[] diff --git a/osu.Game/Overlays/Mods/ModPresetTooltip.cs b/osu.Game/Overlays/Mods/ModPresetTooltip.cs index 8f4efa7667..768feb0756 100644 --- a/osu.Game/Overlays/Mods/ModPresetTooltip.cs +++ b/osu.Game/Overlays/Mods/ModPresetTooltip.cs @@ -23,8 +23,7 @@ namespace osu.Game.Overlays.Mods public ModPresetTooltip(OverlayColourProvider colourProvider) { - Width = 250; - AutoSizeAxes = Axes.Y; + AutoSizeAxes = Axes.Both; Masking = true; CornerRadius = 7; @@ -38,18 +37,16 @@ namespace osu.Game.Overlays.Mods }, Content = new FillFlowContainer { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, Padding = new MarginPadding { Left = 10, Right = 10, Top = 5, Bottom = 5 }, Spacing = new Vector2(7), Children = new[] { - descriptionText = new TruncatingSpriteText + descriptionText = new OsuSpriteText { - RelativeSizeAxes = Axes.X, Font = OsuFont.GetFont(weight: FontWeight.Regular), Colour = colourProvider.Content1, - AllowMultiline = true, }, } } @@ -68,7 +65,11 @@ namespace osu.Game.Overlays.Mods lastPreset = preset; Content.RemoveAll(d => d is ModPresetRow, true); - Content.AddRange(preset.Mods.AsOrdered().Select(mod => new ModPresetRow(mod))); + Content.AddRange(preset.Mods.AsOrdered().Select(mod => new ModPresetRow(mod) + { + RelativeSizeAxes = Axes.None, + AutoSizeAxes = Axes.Both, + })); } protected override void PopIn() => this.FadeIn(transition_duration, Easing.OutQuint); From 1c9c3c92fdf539b485bc91173e931d08396de904 Mon Sep 17 00:00:00 2001 From: Shreyas Kadambi Date: Sun, 28 Jul 2024 11:30:42 -0400 Subject: [PATCH 1689/2493] Add tests for expected timestamp format --- osu.Game.Tests/Editing/EditorTimestampParserTest.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Editing/EditorTimestampParserTest.cs b/osu.Game.Tests/Editing/EditorTimestampParserTest.cs index 9c7fae0eaf..cb9bd3dafe 100644 --- a/osu.Game.Tests/Editing/EditorTimestampParserTest.cs +++ b/osu.Game.Tests/Editing/EditorTimestampParserTest.cs @@ -16,7 +16,6 @@ namespace osu.Game.Tests.Editing new object?[] { "1", true, TimeSpan.FromMilliseconds(1), null }, new object?[] { "99", true, TimeSpan.FromMilliseconds(99), null }, new object?[] { "320000", true, TimeSpan.FromMilliseconds(320000), null }, - new object?[] { "1:2", true, new TimeSpan(0, 0, 1, 2), null }, new object?[] { "1:02", true, new TimeSpan(0, 0, 1, 2), null }, new object?[] { "1:92", false, null, null }, new object?[] { "1:002", false, null, null }, @@ -25,6 +24,9 @@ namespace osu.Game.Tests.Editing new object?[] { "1:02:3000", false, null, null }, new object?[] { "1:02:300 ()", false, null, null }, new object?[] { "1:02:300 (1,2,3)", true, new TimeSpan(0, 0, 1, 2, 300), "1,2,3" }, + new object?[] { "1:02:300 (1,2,3) - ", true, new TimeSpan(0, 0, 1, 2, 300), "1,2,3" }, + new object?[] { "1:02:300 (1,2,3) - following mod", true, new TimeSpan(0, 0, 1, 2, 300), "1,2,3" }, + new object?[] { "1:02:300 (1,2,3) - following mod\nwith newlines", true, new TimeSpan(0, 0, 1, 2, 300), "1,2,3" }, }; [TestCaseSource(nameof(test_cases))] From dec6b190f249677f9a9e37a477547f8a6474dff9 Mon Sep 17 00:00:00 2001 From: Shreyas Kadambi Date: Sun, 28 Jul 2024 11:31:36 -0400 Subject: [PATCH 1690/2493] Add optional 'suffix' to timestamp --- osu.Game/Rulesets/Edit/EditorTimestampParser.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/Edit/EditorTimestampParser.cs b/osu.Game/Rulesets/Edit/EditorTimestampParser.cs index 92a692b94e..9e637e55bc 100644 --- a/osu.Game/Rulesets/Edit/EditorTimestampParser.cs +++ b/osu.Game/Rulesets/Edit/EditorTimestampParser.cs @@ -11,7 +11,8 @@ namespace osu.Game.Rulesets.Edit { /// /// Used for parsing in contexts where we don't want e.g. normal times of day to be parsed as timestamps (e.g. chat) - /// Original osu-web regex: https://github.com/ppy/osu-web/blob/3b1698639244cfdaf0b41c68bfd651ea729ec2e3/resources/js/utils/beatmapset-discussion-helper.ts#L78 + /// Original osu-web regex: + /// https://github.com/ppy/osu-web/blob/3b1698639244cfdaf0b41c68bfd651ea729ec2e3/resources/js/utils/beatmapset-discussion-helper.ts#L78 /// /// /// 00:00:000 (...) - test @@ -32,7 +33,10 @@ namespace osu.Game.Rulesets.Edit /// 1:02:300 (1,2,3) - parses to 01:02:300 with selection /// /// - private static readonly Regex time_regex_lenient = new Regex(@"^(((?\d{1,3}):(?([0-5]?\d))([:.](?\d{0,3}))?)(?\s\([^)]+\))?)$", RegexOptions.Compiled); + private static readonly Regex time_regex_lenient = new Regex( + @"^(((?\d{1,3}):(?([0-5]?\d))([:.](?\d{0,3}))?)(?\s\([^)]+\))?)(?\s-.*)?$", + RegexOptions.Compiled | RegexOptions.Singleline + ); public static bool TryParse(string timestamp, [NotNullWhen(true)] out TimeSpan? parsedTime, out string? parsedSelection) { From ae61df0abe507f675282be5d54146c9e1736a27b Mon Sep 17 00:00:00 2001 From: Shreyas Kadambi Date: Sun, 28 Jul 2024 11:47:00 -0400 Subject: [PATCH 1691/2493] Add back accidentally removed test --- osu.Game.Tests/Editing/EditorTimestampParserTest.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game.Tests/Editing/EditorTimestampParserTest.cs b/osu.Game.Tests/Editing/EditorTimestampParserTest.cs index cb9bd3dafe..49154f1cbb 100644 --- a/osu.Game.Tests/Editing/EditorTimestampParserTest.cs +++ b/osu.Game.Tests/Editing/EditorTimestampParserTest.cs @@ -16,6 +16,7 @@ namespace osu.Game.Tests.Editing new object?[] { "1", true, TimeSpan.FromMilliseconds(1), null }, new object?[] { "99", true, TimeSpan.FromMilliseconds(99), null }, new object?[] { "320000", true, TimeSpan.FromMilliseconds(320000), null }, + new object?[] { "1:2", true, new TimeSpan(0, 0, 1, 2), null }, new object?[] { "1:02", true, new TimeSpan(0, 0, 1, 2), null }, new object?[] { "1:92", false, null, null }, new object?[] { "1:002", false, null, null }, From 63757a77a54a108c56d9538c229daa15c81c1b7b Mon Sep 17 00:00:00 2001 From: jkh675 Date: Mon, 29 Jul 2024 13:39:08 +0800 Subject: [PATCH 1692/2493] Extract update background method --- osu.Game/Overlays/Chat/ChatLine.cs | 28 +++++++++++++---------- osu.Game/Overlays/Chat/DrawableChannel.cs | 4 ++-- 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/osu.Game/Overlays/Chat/ChatLine.cs b/osu.Game/Overlays/Chat/ChatLine.cs index fc43e38239..81b63d3380 100644 --- a/osu.Game/Overlays/Chat/ChatLine.cs +++ b/osu.Game/Overlays/Chat/ChatLine.cs @@ -79,18 +79,7 @@ namespace osu.Game.Overlays.Chat set { alteringBackground = value; - - if (background == null) - { - AddInternal(background = new Box - { - BypassAutoSizeAxes = Axes.Both, - RelativeSizeAxes = Axes.Both, - Colour = Color4.White, - }); - } - - background.Alpha = value ? 0.04f : 0f; + updateBackground(); } } @@ -283,5 +272,20 @@ namespace osu.Game.Overlays.Chat Color4Extensions.FromHex("812a96"), Color4Extensions.FromHex("992861"), }; + + private void updateBackground() + { + if (background == null) + { + AddInternal(background = new Box + { + BypassAutoSizeAxes = Axes.Both, + RelativeSizeAxes = Axes.Both, + Colour = Color4.White, + }); + } + + background.Alpha = alteringBackground ? 0.04f : 0f; + } } } diff --git a/osu.Game/Overlays/Chat/DrawableChannel.cs b/osu.Game/Overlays/Chat/DrawableChannel.cs index 21af8d7305..b6b89c4201 100644 --- a/osu.Game/Overlays/Chat/DrawableChannel.cs +++ b/osu.Game/Overlays/Chat/DrawableChannel.cs @@ -104,7 +104,7 @@ namespace osu.Game.Overlays.Chat highlightedMessage.Value = null; }); - private void processMessageBackgroundAltering() + private void processChatlineBackgroundAltering() { for (int i = 0; i < ChatLineFlow.Count; i++) { @@ -169,7 +169,7 @@ namespace osu.Game.Overlays.Chat scroll.ScrollToEnd(); processMessageHighlighting(); - processMessageBackgroundAltering(); + processChatlineBackgroundAltering(); }); private void pendingMessageResolved(Message existing, Message updated) => Schedule(() => From 54c904d439ae2b6b37b97d29768ad8e04994d054 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 29 Jul 2024 10:40:29 +0200 Subject: [PATCH 1693/2493] Convert into auto-property --- .../SelectV2/Leaderboards/LeaderboardScoreV2.cs | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/osu.Game/Screens/SelectV2/Leaderboards/LeaderboardScoreV2.cs b/osu.Game/Screens/SelectV2/Leaderboards/LeaderboardScoreV2.cs index 6066bc7739..c9584b057b 100644 --- a/osu.Game/Screens/SelectV2/Leaderboards/LeaderboardScoreV2.cs +++ b/osu.Game/Screens/SelectV2/Leaderboards/LeaderboardScoreV2.cs @@ -45,18 +45,12 @@ namespace osu.Game.Screens.SelectV2.Leaderboards { public Bindable> SelectedMods = new Bindable>(); - private Func isValidMod = _ => true; - /// /// A function determining whether each mod in the score can be selected. /// A return value of means that the mod can be selected in the current context. /// A return value of means that the mod cannot be selected in the current context. /// - public Func IsValidMod - { - get => isValidMod; - set => isValidMod = value ?? throw new ArgumentNullException(nameof(value)); - } + public Func IsValidMod { get; set; } = _ => true; public int? Rank { get; init; } public bool IsPersonalBest { get; init; } @@ -751,7 +745,7 @@ namespace osu.Game.Screens.SelectV2.Leaderboards List items = new List(); if (score.Mods.Length > 0) - items.Add(new OsuMenuItem("Use these mods", MenuItemType.Highlighted, () => SelectedMods.Value = score.Mods.Where(m => isValidMod.Invoke(m)).ToArray())); + items.Add(new OsuMenuItem("Use these mods", MenuItemType.Highlighted, () => SelectedMods.Value = score.Mods.Where(m => IsValidMod.Invoke(m)).ToArray())); if (score.Files.Count <= 0) return items.ToArray(); From 861b5465628cff64ca1efed3883d0978725bb61c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 29 Jul 2024 10:45:03 +0200 Subject: [PATCH 1694/2493] Add vague test coverage --- osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs b/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs index 36e256b920..91df38feb9 100644 --- a/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs +++ b/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs @@ -115,6 +115,7 @@ namespace osu.Game.Tests.Visual.OnlinePlay MaxCombo = 1000, TotalScore = 1000000, User = new APIUser { Username = "best user" }, + Mods = [new APIMod { Acronym = @"DT" }], Statistics = new Dictionary() }, new MultiplayerScore From 2ff0a89b4fda97b4fb4b6a634376b5ac4b629b2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 29 Jul 2024 10:59:21 +0200 Subject: [PATCH 1695/2493] Convert into auto-property even more --- .../DailyChallenge/DailyChallengeLeaderboard.cs | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeLeaderboard.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeLeaderboard.cs index f332e717c1..c9152393e7 100644 --- a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeLeaderboard.cs +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeLeaderboard.cs @@ -28,18 +28,12 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge private readonly Bindable userBestScore = new Bindable(); public Bindable> SelectedMods = new Bindable>(); - private Func isValidMod = _ => true; - /// /// A function determining whether each mod in the score can be selected. /// A return value of means that the mod can be selected in the current context. /// A return value of means that the mod cannot be selected in the current context. /// - public Func IsValidMod - { - get => isValidMod; - set => isValidMod = value ?? throw new ArgumentNullException(nameof(value)); - } + public Func IsValidMod { get; set; } = _ => true; public Action? PresentScore { get; init; } @@ -170,7 +164,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge IsPersonalBest = s.UserID == api.LocalUser.Value.Id, Action = () => PresentScore?.Invoke(s.OnlineID), SelectedMods = { BindTarget = SelectedMods }, - IsValidMod = isValidMod, + IsValidMod = IsValidMod, }), loaded => { scoreFlow.Clear(); @@ -190,7 +184,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge IsPersonalBest = true, Action = () => PresentScore?.Invoke(userBest.OnlineID), SelectedMods = { BindTarget = SelectedMods }, - IsValidMod = isValidMod, + IsValidMod = IsValidMod, }); } From 5ec46a79b4d002f78b14f549998ae7fef447bc85 Mon Sep 17 00:00:00 2001 From: jkh675 Date: Mon, 29 Jul 2024 17:50:23 +0800 Subject: [PATCH 1696/2493] Only create a new drawable object when the background is needed --- osu.Game/Overlays/Chat/ChatLine.cs | 28 +++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/osu.Game/Overlays/Chat/ChatLine.cs b/osu.Game/Overlays/Chat/ChatLine.cs index 81b63d3380..486beb58b7 100644 --- a/osu.Game/Overlays/Chat/ChatLine.cs +++ b/osu.Game/Overlays/Chat/ChatLine.cs @@ -21,7 +21,6 @@ using osu.Game.Graphics.Sprites; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Chat; using osuTK.Graphics; -using Message = osu.Game.Online.Chat.Message; namespace osu.Game.Overlays.Chat { @@ -118,6 +117,11 @@ namespace osu.Game.Overlays.Chat InternalChild = new GridContainer { + Margin = new MarginPadding + { + Horizontal = 10, + Vertical = 1, + }, RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize) }, @@ -275,17 +279,23 @@ namespace osu.Game.Overlays.Chat private void updateBackground() { - if (background == null) + if (alteringBackground) { - AddInternal(background = new Box + if (background?.IsAlive != true) { - BypassAutoSizeAxes = Axes.Both, - RelativeSizeAxes = Axes.Both, - Colour = Color4.White, - }); - } + AddInternal(background = new Circle + { + MaskingSmoothness = 2.5f, + Depth = float.MaxValue, + RelativeSizeAxes = Axes.Both, + Colour = Color4.White, + }); + } - background.Alpha = alteringBackground ? 0.04f : 0f; + background.Alpha = 0.04f; + } + else + background?.Expire(); } } } From 9b96bd1d730ac95456650c530ba6d6a4afeb6d87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 29 Jul 2024 11:53:06 +0200 Subject: [PATCH 1697/2493] Force exit to main menu when presenting scores from within playlists / multiplayer - Closes https://github.com/ppy/osu/issues/29152 - Partially reverts https://github.com/ppy/osu/pull/29097 - Reopens https://github.com/ppy/osu/issues/26666 When testing I failed to predict that in multiplayer there can be a different beatmap in the playlist queue. If this is the case, `PresentScore()` will exit out to `Multiplayer`, whose `RoomSubScreen` will update the selected item - and thus, the global beatmap - to the next item in queue, at which point trying to play games with "not touching the global beatmap bindable if we don't need to" fail to work, because the bindable *must* be touched for correct operation, yet it cannot (because `OnlinePlayScreen`s disable it). I'm not sure what the fix is here: - making replay player somehow independent of the global beatmap? - not exiting out to multiplayer, but instead doing the present from the results screen itself? if so, then how to ensure the screen stack can't overflow to infinity? so I'm just reverting the broken part. The daily challenge part is left in because as is it should not cause issues. --- osu.Game/OsuGame.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 2195576be1..53b2fd5904 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -63,7 +63,6 @@ using osu.Game.Screens; using osu.Game.Screens.Edit; using osu.Game.Screens.Footer; using osu.Game.Screens.Menu; -using osu.Game.Screens.OnlinePlay; using osu.Game.Screens.OnlinePlay.DailyChallenge; using osu.Game.Screens.OnlinePlay.Multiplayer; using osu.Game.Screens.Play; @@ -757,11 +756,13 @@ namespace osu.Game // As a special case, if the beatmap and ruleset already match, allow immediately displaying the score from song select. // This is guaranteed to not crash, and feels better from a user's perspective (ie. if they are clicking a score in the // song select leaderboard). - // Similar exemptions are made here for online flows where there are good chances that beatmap and ruleset match - // (playlists / multiplayer / daily challenge). + // Similar exemptions are made here for daily challenge where it is guaranteed that beatmap and ruleset match. + // `OnlinePlayScreen` is excluded because when resuming back to it, + // `RoomSubScreen` changes the global beatmap to the next playlist item on resume, + // which may not match the score, and thus crash. IEnumerable validScreens = Beatmap.Value.BeatmapInfo.Equals(databasedBeatmap) && Ruleset.Value.Equals(databasedScore.ScoreInfo.Ruleset) - ? new[] { typeof(SongSelect), typeof(OnlinePlayScreen), typeof(DailyChallenge) } + ? new[] { typeof(SongSelect), typeof(DailyChallenge) } : Array.Empty(); PerformFromScreen(screen => From 90fdf5599fbb6943ef62b2fd5eafe66d954e7bc2 Mon Sep 17 00:00:00 2001 From: jkh675 Date: Mon, 29 Jul 2024 18:14:07 +0800 Subject: [PATCH 1698/2493] Revert changes --- osu.Game/Overlays/Mods/ModPresetRow.cs | 3 ++- osu.Game/Overlays/Mods/ModPresetTooltip.cs | 13 +++++-------- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModPresetRow.cs b/osu.Game/Overlays/Mods/ModPresetRow.cs index 8614806085..4829e93b87 100644 --- a/osu.Game/Overlays/Mods/ModPresetRow.cs +++ b/osu.Game/Overlays/Mods/ModPresetRow.cs @@ -24,7 +24,8 @@ namespace osu.Game.Overlays.Mods { new FillFlowContainer { - AutoSizeAxes = Axes.Both, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, Direction = FillDirection.Horizontal, Spacing = new Vector2(7), Children = new Drawable[] diff --git a/osu.Game/Overlays/Mods/ModPresetTooltip.cs b/osu.Game/Overlays/Mods/ModPresetTooltip.cs index 768feb0756..ec81aa7ceb 100644 --- a/osu.Game/Overlays/Mods/ModPresetTooltip.cs +++ b/osu.Game/Overlays/Mods/ModPresetTooltip.cs @@ -23,7 +23,8 @@ namespace osu.Game.Overlays.Mods public ModPresetTooltip(OverlayColourProvider colourProvider) { - AutoSizeAxes = Axes.Both; + Width = 250; + AutoSizeAxes = Axes.Y; Masking = true; CornerRadius = 7; @@ -37,8 +38,8 @@ namespace osu.Game.Overlays.Mods }, Content = new FillFlowContainer { - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Vertical, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, Padding = new MarginPadding { Left = 10, Right = 10, Top = 5, Bottom = 5 }, Spacing = new Vector2(7), Children = new[] @@ -65,11 +66,7 @@ namespace osu.Game.Overlays.Mods lastPreset = preset; Content.RemoveAll(d => d is ModPresetRow, true); - Content.AddRange(preset.Mods.AsOrdered().Select(mod => new ModPresetRow(mod) - { - RelativeSizeAxes = Axes.None, - AutoSizeAxes = Axes.Both, - })); + Content.AddRange(preset.Mods.AsOrdered().Select(mod => new ModPresetRow(mod))); } protected override void PopIn() => this.FadeIn(transition_duration, Easing.OutQuint); From 8f8668111077cbb75a29370a4735da60e79cba2e Mon Sep 17 00:00:00 2001 From: jkh675 Date: Mon, 29 Jul 2024 18:29:44 +0800 Subject: [PATCH 1699/2493] Replace `OsuSpriteText` with `TextFlowContainer` --- osu.Game/Overlays/Mods/ModPresetTooltip.cs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModPresetTooltip.cs b/osu.Game/Overlays/Mods/ModPresetTooltip.cs index ec81aa7ceb..6dafe85d89 100644 --- a/osu.Game/Overlays/Mods/ModPresetTooltip.cs +++ b/osu.Game/Overlays/Mods/ModPresetTooltip.cs @@ -7,7 +7,6 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Shapes; using osu.Game.Graphics; -using osu.Game.Graphics.Sprites; using osu.Game.Rulesets.Mods; using osuTK; @@ -19,7 +18,7 @@ namespace osu.Game.Overlays.Mods private const double transition_duration = 200; - private readonly OsuSpriteText descriptionText; + private readonly TextFlowContainer descriptionText; public ModPresetTooltip(OverlayColourProvider colourProvider) { @@ -44,11 +43,11 @@ namespace osu.Game.Overlays.Mods Spacing = new Vector2(7), Children = new[] { - descriptionText = new OsuSpriteText + descriptionText = new TextFlowContainer(f => { - Font = OsuFont.GetFont(weight: FontWeight.Regular), - Colour = colourProvider.Content1, - }, + f.Font = OsuFont.GetFont(weight: FontWeight.Regular); + f.Colour = colourProvider.Content1; + }) } } }; From f1a84a5111748a59ee72973cd379850c199751d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 29 Jul 2024 12:52:11 +0200 Subject: [PATCH 1700/2493] Fix mods persisting after watching replay from daily challenge screen Closes https://github.com/ppy/osu/issues/29133. Hope I can be forgiven for no tests. I had a brief try but writing them is going to take hours. --- osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs index 4b4e4a7a62..322d855cd3 100644 --- a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs @@ -454,6 +454,9 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge { base.OnResuming(e); applyLoopingToTrack(); + // re-apply mods as they may have been changed by a child screen + // (one known instance of this is showing a replay). + updateMods(); } public override void OnSuspending(ScreenTransitionEvent e) From c142adf926795b6714311657953edcba8c7ad9d9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 29 Jul 2024 19:58:19 +0900 Subject: [PATCH 1701/2493] Fix online status not persisting correctly Regressed at some point. I don't see much reason not to link the bindable directly with config. It seems to work as you'd expect. Tested with logout (resets to "Online") and connection failure (persists). Closes https://github.com/ppy/osu/issues/29173. --- osu.Game/Online/API/APIAccess.cs | 7 +++---- osu.Game/Overlays/Login/LoginPanel.cs | 1 + 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Online/API/APIAccess.cs b/osu.Game/Online/API/APIAccess.cs index 0cf344ecaf..c02ca1bf5e 100644 --- a/osu.Game/Online/API/APIAccess.cs +++ b/osu.Game/Online/API/APIAccess.cs @@ -118,12 +118,11 @@ namespace osu.Game.Online.API u.OldValue?.Activity.UnbindFrom(activity); u.NewValue.Activity.BindTo(activity); - if (u.OldValue != null) - localUserStatus.UnbindFrom(u.OldValue.Status); - localUserStatus.BindTo(u.NewValue.Status); + u.OldValue?.Status.UnbindFrom(localUserStatus); + u.NewValue.Status.BindTo(localUserStatus); }, true); - localUserStatus.BindValueChanged(val => configStatus.Value = val.NewValue); + localUserStatus.BindTo(configStatus); var thread = new Thread(run) { diff --git a/osu.Game/Overlays/Login/LoginPanel.cs b/osu.Game/Overlays/Login/LoginPanel.cs index cb642f9b72..84bd0c36b9 100644 --- a/osu.Game/Overlays/Login/LoginPanel.cs +++ b/osu.Game/Overlays/Login/LoginPanel.cs @@ -157,6 +157,7 @@ namespace osu.Game.Overlays.Login }, }; + updateDropdownCurrent(status.Value); dropdown.Current.BindValueChanged(action => { switch (action.NewValue) From 11265538c484fa22a23c49dc994faac96d8a2bca Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 29 Jul 2024 20:02:18 +0900 Subject: [PATCH 1702/2493] Reset online status on logout --- osu.Game/Online/API/APIAccess.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Online/API/APIAccess.cs b/osu.Game/Online/API/APIAccess.cs index c02ca1bf5e..716d1e4466 100644 --- a/osu.Game/Online/API/APIAccess.cs +++ b/osu.Game/Online/API/APIAccess.cs @@ -599,6 +599,7 @@ namespace osu.Game.Online.API password = null; SecondFactorCode = null; authentication.Clear(); + configStatus.Value = UserStatus.Online; // Scheduled prior to state change such that the state changed event is invoked with the correct user and their friends present Schedule(() => From d51a53b051d46c58179f3774d0ea195f4705368c Mon Sep 17 00:00:00 2001 From: jkh675 Date: Mon, 29 Jul 2024 19:08:14 +0800 Subject: [PATCH 1703/2493] Preventing the mod icon being squashed up --- osu.Game/Overlays/Mods/ModPresetTooltip.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/Overlays/Mods/ModPresetTooltip.cs b/osu.Game/Overlays/Mods/ModPresetTooltip.cs index 6dafe85d89..6ffcfca1e0 100644 --- a/osu.Game/Overlays/Mods/ModPresetTooltip.cs +++ b/osu.Game/Overlays/Mods/ModPresetTooltip.cs @@ -48,6 +48,10 @@ namespace osu.Game.Overlays.Mods f.Font = OsuFont.GetFont(weight: FontWeight.Regular); f.Colour = colourProvider.Content1; }) + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + } } } }; From 8b96b0b9e497f93c5860ad1366b1d7db3363c324 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 29 Jul 2024 13:19:01 +0200 Subject: [PATCH 1704/2493] Add logging when starting and stopping watch operations in online metadata client For future use with debugging issues like https://github.com/ppy/osu/issues/29138, hopefully. --- osu.Game/Online/Metadata/OnlineMetadataClient.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/osu.Game/Online/Metadata/OnlineMetadataClient.cs b/osu.Game/Online/Metadata/OnlineMetadataClient.cs index 911b13ecd8..a3041c6753 100644 --- a/osu.Game/Online/Metadata/OnlineMetadataClient.cs +++ b/osu.Game/Online/Metadata/OnlineMetadataClient.cs @@ -215,6 +215,7 @@ namespace osu.Game.Online.Metadata Debug.Assert(connection != null); await connection.InvokeAsync(nameof(IMetadataServer.BeginWatchingUserPresence)).ConfigureAwait(false); Schedule(() => isWatchingUserPresence.Value = true); + Logger.Log($@"{nameof(OnlineMetadataClient)} began watching user presence", LoggingTarget.Network); } public override async Task EndWatchingUserPresence() @@ -228,6 +229,7 @@ namespace osu.Game.Online.Metadata Schedule(() => userStates.Clear()); Debug.Assert(connection != null); await connection.InvokeAsync(nameof(IMetadataServer.EndWatchingUserPresence)).ConfigureAwait(false); + Logger.Log($@"{nameof(OnlineMetadataClient)} stopped watching user presence", LoggingTarget.Network); } finally { @@ -247,7 +249,9 @@ namespace osu.Game.Online.Metadata throw new OperationCanceledException(); Debug.Assert(connection != null); - return await connection.InvokeAsync(nameof(IMetadataServer.BeginWatchingMultiplayerRoom), id).ConfigureAwait(false); + var result = await connection.InvokeAsync(nameof(IMetadataServer.BeginWatchingMultiplayerRoom), id).ConfigureAwait(false); + Logger.Log($@"{nameof(OnlineMetadataClient)} began watching multiplayer room with ID {id}", LoggingTarget.Network); + return result; } public override async Task EndWatchingMultiplayerRoom(long id) @@ -257,6 +261,7 @@ namespace osu.Game.Online.Metadata Debug.Assert(connection != null); await connection.InvokeAsync(nameof(IMetadataServer.EndWatchingMultiplayerRoom), id).ConfigureAwait(false); + Logger.Log($@"{nameof(OnlineMetadataClient)} stopped watching multiplayer room with ID {id}", LoggingTarget.Network); } public override async Task DisconnectRequested() From 997b3eb498ecdd689c4f68205bb90fdbd211a0a1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 29 Jul 2024 20:16:41 +0900 Subject: [PATCH 1705/2493] Fix typos and visuals --- .../Visual/Online/TestSceneDrawableChannel.cs | 2 +- osu.Game/Overlays/Chat/ChatLine.cs | 126 +++++++++--------- osu.Game/Overlays/Chat/DrawableChannel.cs | 6 +- 3 files changed, 68 insertions(+), 66 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneDrawableChannel.cs b/osu.Game.Tests/Visual/Online/TestSceneDrawableChannel.cs index 795d49adad..bb73a458a3 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneDrawableChannel.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneDrawableChannel.cs @@ -115,7 +115,7 @@ namespace osu.Game.Tests.Visual.Online AddRepeatStep("check background", () => { // +1 because the day separator take one index - Assert.AreEqual((checkCount + 1) % 2 == 0, drawableChannel.ChildrenOfType().ToList()[checkCount].AlteringBackground); + Assert.AreEqual((checkCount + 1) % 2 == 0, drawableChannel.ChildrenOfType().ToList()[checkCount].AlternatingBackground); checkCount++; }, 10); } diff --git a/osu.Game/Overlays/Chat/ChatLine.cs b/osu.Game/Overlays/Chat/ChatLine.cs index 486beb58b7..e7be7e7814 100644 --- a/osu.Game/Overlays/Chat/ChatLine.cs +++ b/osu.Game/Overlays/Chat/ChatLine.cs @@ -70,14 +70,14 @@ namespace osu.Game.Overlays.Chat private Drawable? background; - private bool alteringBackground; + private bool alternatingBackground; - public bool AlteringBackground + public bool AlternatingBackground { - get => alteringBackground; + get => alternatingBackground; set { - alteringBackground = value; + alternatingBackground = value; updateBackground(); } } @@ -115,53 +115,70 @@ namespace osu.Game.Overlays.Chat configManager.BindWith(OsuSetting.Prefer24HourTime, prefer24HourTime); prefer24HourTime.BindValueChanged(_ => updateTimestamp()); - InternalChild = new GridContainer + InternalChildren = new[] { - Margin = new MarginPadding + background = new Container { - Horizontal = 10, - Vertical = 1, - }, - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize) }, - ColumnDimensions = new[] - { - new Dimension(GridSizeMode.AutoSize), - new Dimension(GridSizeMode.Absolute, Spacing + UsernameWidth + Spacing), - new Dimension(), - }, - Content = new[] - { - new Drawable[] + Masking = true, + Blending = BlendingParameters.Additive, + CornerRadius = 4, + RelativeSizeAxes = Axes.Both, + Child = new Box { - drawableTimestamp = new OsuSpriteText - { - Shadow = false, - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Font = OsuFont.GetFont(size: FontSize * 0.75f, weight: FontWeight.SemiBold, fixedWidth: true), - AlwaysPresent = true, - }, - drawableUsername = new DrawableChatUsername(message.Sender) - { - Width = UsernameWidth, - FontSize = FontSize, - AutoSizeAxes = Axes.Y, - Origin = Anchor.TopRight, - Anchor = Anchor.TopRight, - Margin = new MarginPadding { Horizontal = Spacing }, - AccentColour = UsernameColour, - Inverted = !string.IsNullOrEmpty(message.Sender.Colour), - }, - drawableContentFlow = new LinkFlowContainer(styleMessageContent) - { - AutoSizeAxes = Axes.Y, - RelativeSizeAxes = Axes.X, - } + Colour = Color4.White, + RelativeSizeAxes = Axes.Both, }, + }, + new GridContainer + { + Margin = new MarginPadding + { + Horizontal = 10, + Vertical = 1, + }, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize) }, + ColumnDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize), + new Dimension(GridSizeMode.Absolute, Spacing + UsernameWidth + Spacing), + new Dimension(), + }, + Content = new[] + { + new Drawable[] + { + drawableTimestamp = new OsuSpriteText + { + Shadow = false, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Font = OsuFont.GetFont(size: FontSize * 0.75f, weight: FontWeight.SemiBold, fixedWidth: true), + AlwaysPresent = true, + }, + drawableUsername = new DrawableChatUsername(message.Sender) + { + Width = UsernameWidth, + FontSize = FontSize, + AutoSizeAxes = Axes.Y, + Origin = Anchor.TopRight, + Anchor = Anchor.TopRight, + Margin = new MarginPadding { Horizontal = Spacing }, + AccentColour = UsernameColour, + Inverted = !string.IsNullOrEmpty(message.Sender.Colour), + }, + drawableContentFlow = new LinkFlowContainer(styleMessageContent) + { + AutoSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.X, + } + }, + } } }; + + updateBackground(); } protected override void LoadComplete() @@ -279,23 +296,8 @@ namespace osu.Game.Overlays.Chat private void updateBackground() { - if (alteringBackground) - { - if (background?.IsAlive != true) - { - AddInternal(background = new Circle - { - MaskingSmoothness = 2.5f, - Depth = float.MaxValue, - RelativeSizeAxes = Axes.Both, - Colour = Color4.White, - }); - } - - background.Alpha = 0.04f; - } - else - background?.Expire(); + if (background != null) + background.Alpha = alternatingBackground ? 0.03f : 0; } } } diff --git a/osu.Game/Overlays/Chat/DrawableChannel.cs b/osu.Game/Overlays/Chat/DrawableChannel.cs index b6b89c4201..f5dd5a24f2 100644 --- a/osu.Game/Overlays/Chat/DrawableChannel.cs +++ b/osu.Game/Overlays/Chat/DrawableChannel.cs @@ -104,13 +104,13 @@ namespace osu.Game.Overlays.Chat highlightedMessage.Value = null; }); - private void processChatlineBackgroundAltering() + private void updateBackgroundAlternating() { for (int i = 0; i < ChatLineFlow.Count; i++) { if (ChatLineFlow[i] is ChatLine chatline) { - chatline.AlteringBackground = i % 2 == 0; + chatline.AlternatingBackground = i % 2 == 0; } } } @@ -169,7 +169,7 @@ namespace osu.Game.Overlays.Chat scroll.ScrollToEnd(); processMessageHighlighting(); - processChatlineBackgroundAltering(); + updateBackgroundAlternating(); }); private void pendingMessageResolved(Message existing, Message updated) => Schedule(() => From 5bc02cc1c69e104dca9a44439218d77a91d93c55 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 29 Jul 2024 20:25:02 +0900 Subject: [PATCH 1706/2493] Fix background alternating not updating on message removal --- osu.Game/Overlays/Chat/ChatLine.cs | 4 ++++ osu.Game/Overlays/Chat/DrawableChannel.cs | 23 +++++++++++------------ 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/osu.Game/Overlays/Chat/ChatLine.cs b/osu.Game/Overlays/Chat/ChatLine.cs index e7be7e7814..87e1f5699b 100644 --- a/osu.Game/Overlays/Chat/ChatLine.cs +++ b/osu.Game/Overlays/Chat/ChatLine.cs @@ -77,6 +77,9 @@ namespace osu.Game.Overlays.Chat get => alternatingBackground; set { + if (alternatingBackground == value) + return; + alternatingBackground = value; updateBackground(); } @@ -122,6 +125,7 @@ namespace osu.Game.Overlays.Chat Masking = true, Blending = BlendingParameters.Additive, CornerRadius = 4, + Alpha = 0, RelativeSizeAxes = Axes.Both, Child = new Box { diff --git a/osu.Game/Overlays/Chat/DrawableChannel.cs b/osu.Game/Overlays/Chat/DrawableChannel.cs index f5dd5a24f2..6b3acaa226 100644 --- a/osu.Game/Overlays/Chat/DrawableChannel.cs +++ b/osu.Game/Overlays/Chat/DrawableChannel.cs @@ -84,6 +84,17 @@ namespace osu.Game.Overlays.Chat highlightedMessage.BindValueChanged(_ => processMessageHighlighting(), true); } + protected override void Update() + { + base.Update(); + + for (int i = 0; i < ChatLineFlow.Count; i++) + { + if (ChatLineFlow[i] is ChatLine chatline) + chatline.AlternatingBackground = i % 2 == 0; + } + } + /// /// Processes any pending message in . /// @@ -104,17 +115,6 @@ namespace osu.Game.Overlays.Chat highlightedMessage.Value = null; }); - private void updateBackgroundAlternating() - { - for (int i = 0; i < ChatLineFlow.Count; i++) - { - if (ChatLineFlow[i] is ChatLine chatline) - { - chatline.AlternatingBackground = i % 2 == 0; - } - } - } - protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); @@ -169,7 +169,6 @@ namespace osu.Game.Overlays.Chat scroll.ScrollToEnd(); processMessageHighlighting(); - updateBackgroundAlternating(); }); private void pendingMessageResolved(Message existing, Message updated) => Schedule(() => From 76cd2df6999866dde279387dfa9efe1ea4f04fc3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 29 Jul 2024 20:43:09 +0900 Subject: [PATCH 1707/2493] Add ability to test daily challenge carousel items when hidden --- .../DailyChallenge/TestSceneDailyChallengeScoreBreakdown.cs | 2 ++ .../DailyChallenge/TestSceneDailyChallengeTimeRemainingRing.cs | 2 ++ .../DailyChallenge/TestSceneDailyChallengeTotalsDisplay.cs | 1 + 3 files changed, 5 insertions(+) diff --git a/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeScoreBreakdown.cs b/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeScoreBreakdown.cs index 631aafb58f..086f3ce174 100644 --- a/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeScoreBreakdown.cs +++ b/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeScoreBreakdown.cs @@ -50,6 +50,8 @@ namespace osu.Game.Tests.Visual.DailyChallenge breakdown.Height = height; }); + AddToggleStep("toggle visible", v => breakdown.Alpha = v ? 1 : 0); + AddStep("set initial data", () => breakdown.SetInitialCounts([1, 4, 9, 16, 25, 36, 49, 36, 25, 16, 9, 4, 1])); AddStep("add new score", () => { diff --git a/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeTimeRemainingRing.cs b/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeTimeRemainingRing.cs index 9e21214c11..baa1eb8318 100644 --- a/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeTimeRemainingRing.cs +++ b/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeTimeRemainingRing.cs @@ -55,6 +55,8 @@ namespace osu.Game.Tests.Visual.DailyChallenge if (ring.IsNotNull()) ring.Height = height; }); + AddToggleStep("toggle visible", v => ring.Alpha = v ? 1 : 0); + AddStep("just started", () => { room.Value.StartDate.Value = DateTimeOffset.Now.AddMinutes(-1); diff --git a/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeTotalsDisplay.cs b/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeTotalsDisplay.cs index ba5a0989d4..ae212f5212 100644 --- a/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeTotalsDisplay.cs +++ b/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeTotalsDisplay.cs @@ -49,6 +49,7 @@ namespace osu.Game.Tests.Visual.DailyChallenge if (totals.IsNotNull()) totals.Height = height; }); + AddToggleStep("toggle visible", v => totals.Alpha = v ? 1 : 0); AddStep("set counts", () => totals.SetInitialCounts(totalPassCount: 9650, cumulativeTotalScore: 10_000_000_000)); From 5a1002c1a07b2108f73dad4731ecd506a82c6445 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 29 Jul 2024 20:43:34 +0900 Subject: [PATCH 1708/2493] Ensure score breakdown doesn't spam scores when not visible --- .../DailyChallengeScoreBreakdown.cs | 46 ++++++++++--------- 1 file changed, 25 insertions(+), 21 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeScoreBreakdown.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeScoreBreakdown.cs index 12401061a3..79ad77831b 100644 --- a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeScoreBreakdown.cs +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeScoreBreakdown.cs @@ -74,30 +74,34 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge { int targetBin = (int)Math.Clamp(Math.Floor((float)newScoreEvent.TotalScore / 100000), 0, bin_count - 1); bins[targetBin] += 1; - updateCounts(); - var text = new OsuSpriteText - { - Text = newScoreEvent.TotalScore.ToString(@"N0"), - Anchor = Anchor.TopCentre, - Origin = Anchor.BottomCentre, - Font = OsuFont.Default.With(size: 30), - RelativePositionAxes = Axes.X, - X = (targetBin + 0.5f) / bin_count - 0.5f, - Alpha = 0, - }; - AddInternal(text); + Scheduler.AddOnce(updateCounts); - Scheduler.AddDelayed(() => + if (Alpha > 0) { - float startY = ToLocalSpace(barsContainer[targetBin].CircularBar.ScreenSpaceDrawQuad.TopLeft).Y; - text.FadeInFromZero() - .ScaleTo(new Vector2(0.8f), 500, Easing.OutElasticHalf) - .MoveToY(startY) - .MoveToOffset(new Vector2(0, -50), 2500, Easing.OutQuint) - .FadeOut(2500, Easing.OutQuint) - .Expire(); - }, 150); + var text = new OsuSpriteText + { + Text = newScoreEvent.TotalScore.ToString(@"N0"), + Anchor = Anchor.TopCentre, + Origin = Anchor.BottomCentre, + Font = OsuFont.Default.With(size: 30), + RelativePositionAxes = Axes.X, + X = (targetBin + 0.5f) / bin_count - 0.5f, + Alpha = 0, + }; + AddInternal(text); + + Scheduler.AddDelayed(() => + { + float startY = ToLocalSpace(barsContainer[targetBin].CircularBar.ScreenSpaceDrawQuad.TopLeft).Y; + text.FadeInFromZero() + .ScaleTo(new Vector2(0.8f), 500, Easing.OutElasticHalf) + .MoveToY(startY) + .MoveToOffset(new Vector2(0, -50), 2500, Easing.OutQuint) + .FadeOut(2500, Easing.OutQuint) + .Expire(); + }, 150); + } } public void SetInitialCounts(long[] counts) From 05056f0e8a107da1b7a54208f3b8aee9139679d4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 29 Jul 2024 20:37:52 +0900 Subject: [PATCH 1709/2493] Remove no longer required `AlwaysPresent` definition This also reverts commit 7fedfd368c83767846e947372e9fba03e07f6ceb as no-longer-necessary. --- .../OnlinePlay/DailyChallenge/DailyChallengeCarousel.cs | 1 - .../OnlinePlay/DailyChallenge/DailyChallengeScoreBreakdown.cs | 3 --- 2 files changed, 4 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeCarousel.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeCarousel.cs index a9f9a5cd78..09c0c3f017 100644 --- a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeCarousel.cs +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeCarousel.cs @@ -50,7 +50,6 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge { drawable.RelativeSizeAxes = Axes.Both; drawable.Size = Vector2.One; - drawable.AlwaysPresent = true; drawable.Alpha = 0; base.Add(drawable); diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeScoreBreakdown.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeScoreBreakdown.cs index 79ad77831b..b35379e126 100644 --- a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeScoreBreakdown.cs +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeScoreBreakdown.cs @@ -27,9 +27,6 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge private FillFlowContainer barsContainer = null!; - // we're always present so that we can update while hidden, but we don't want tooltips to be displayed, therefore directly use alpha comparison here. - public override bool PropagatePositionalInputSubTree => base.PropagatePositionalInputSubTree && Alpha > 0; - private const int bin_count = MultiplayerPlaylistItemStats.TOTAL_SCORE_DISTRIBUTION_BINS; private long[] bins = new long[bin_count]; From 7afcd728723b94996112bb34a6de6a96c7041dc1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 29 Jul 2024 20:58:42 +0900 Subject: [PATCH 1710/2493] Fix potentially too many scores displaying in breakdown while in gameplay --- .../TestSceneDailyChallengeScoreBreakdown.cs | 33 ++++++++++++++-- .../DailyChallengeScoreBreakdown.cs | 39 ++++++++++++++++--- 2 files changed, 62 insertions(+), 10 deletions(-) diff --git a/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeScoreBreakdown.cs b/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeScoreBreakdown.cs index 086f3ce174..b04696aded 100644 --- a/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeScoreBreakdown.cs +++ b/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeScoreBreakdown.cs @@ -6,6 +6,7 @@ using osu.Framework.Allocation; using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Shapes; +using osu.Framework.Testing; using osu.Framework.Utils; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Rooms; @@ -20,11 +21,11 @@ namespace osu.Game.Tests.Visual.DailyChallenge [Cached] private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Plum); - [Test] - public void TestBasicAppearance() - { - DailyChallengeScoreBreakdown breakdown = null!; + private DailyChallengeScoreBreakdown breakdown = null!; + [SetUpSteps] + public void SetUpSteps() + { AddStep("create content", () => Children = new Drawable[] { new Box @@ -53,6 +54,11 @@ namespace osu.Game.Tests.Visual.DailyChallenge AddToggleStep("toggle visible", v => breakdown.Alpha = v ? 1 : 0); AddStep("set initial data", () => breakdown.SetInitialCounts([1, 4, 9, 16, 25, 36, 49, 36, 25, 16, 9, 4, 1])); + } + + [Test] + public void TestBasicAppearance() + { AddStep("add new score", () => { var ev = new NewScoreEvent(1, new APIUser @@ -67,5 +73,24 @@ namespace osu.Game.Tests.Visual.DailyChallenge AddStep("set user score", () => breakdown.UserBestScore.Value = new MultiplayerScore { TotalScore = RNG.Next(1_000_000) }); AddStep("unset user score", () => breakdown.UserBestScore.Value = null); } + + [Test] + public void TestMassAdd() + { + AddStep("add 1000 scores at once", () => + { + for (int i = 0; i < 1000; i++) + { + var ev = new NewScoreEvent(1, new APIUser + { + Id = 2, + Username = "peppy", + CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", + }, RNG.Next(1_000_000), null); + + breakdown.AddNewScore(ev); + } + }); + } } } diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeScoreBreakdown.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeScoreBreakdown.cs index b35379e126..71ab73b535 100644 --- a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeScoreBreakdown.cs +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeScoreBreakdown.cs @@ -2,6 +2,7 @@ // 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; @@ -67,18 +68,39 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge }); } + private readonly Queue newScores = new Queue(); + public void AddNewScore(NewScoreEvent newScoreEvent) { - int targetBin = (int)Math.Clamp(Math.Floor((float)newScoreEvent.TotalScore / 100000), 0, bin_count - 1); - bins[targetBin] += 1; + newScores.Enqueue(newScoreEvent); - Scheduler.AddOnce(updateCounts); - - if (Alpha > 0) + // ensure things don't get too out-of-hand. + if (newScores.Count > 25) { + bins[getTargetBin(newScores.Dequeue())] += 1; + Scheduler.AddOnce(updateCounts); + } + } + + private double lastScoreDisplay; + + protected override void Update() + { + base.Update(); + + if (Time.Current - lastScoreDisplay > 150 && newScores.TryDequeue(out var newScore)) + { + if (lastScoreDisplay < Time.Current) + lastScoreDisplay = Time.Current; + + int targetBin = getTargetBin(newScore); + bins[targetBin] += 1; + + updateCounts(); + var text = new OsuSpriteText { - Text = newScoreEvent.TotalScore.ToString(@"N0"), + Text = newScore.TotalScore.ToString(@"N0"), Anchor = Anchor.TopCentre, Origin = Anchor.BottomCentre, Font = OsuFont.Default.With(size: 30), @@ -98,6 +120,8 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge .FadeOut(2500, Easing.OutQuint) .Expire(); }, 150); + + lastScoreDisplay = Time.Current; } } @@ -110,6 +134,9 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge updateCounts(); } + private static int getTargetBin(NewScoreEvent score) => + (int)Math.Clamp(Math.Floor((float)score.TotalScore / 100000), 0, bin_count - 1); + private void updateCounts() { long max = Math.Max(bins.Max(), 1); From b46f3c97da76f767c455e42dc73a16a0ca73e400 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 29 Jul 2024 14:20:47 +0200 Subject: [PATCH 1711/2493] Add notification on daily challenge conclusion & start of new one Because I wish to stop seeing "DAILY CHALLENGE WHERE" every day on #general. The notifications are constrained to the daily challenge screen only to not spam users who may not care. --- .../DailyChallenge/TestSceneDailyChallenge.cs | 73 +++++++++++++++++++ .../DailyChallenge/DailyChallenge.cs | 33 +++++++++ .../NewDailyChallengeNotification.cs | 44 +++++++++++ .../Visual/Metadata/TestMetadataClient.cs | 2 +- 4 files changed, 151 insertions(+), 1 deletion(-) create mode 100644 osu.Game/Screens/OnlinePlay/DailyChallenge/NewDailyChallengeNotification.cs diff --git a/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallenge.cs b/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallenge.cs index cd09a1d20f..4a5f452ed1 100644 --- a/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallenge.cs +++ b/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallenge.cs @@ -4,16 +4,32 @@ using System; using System.Linq; using NUnit.Framework; +using osu.Framework.Allocation; using osu.Game.Online.API; +using osu.Game.Online.Metadata; using osu.Game.Online.Rooms; +using osu.Game.Overlays; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Tests.Resources; +using osu.Game.Tests.Visual.Metadata; using osu.Game.Tests.Visual.OnlinePlay; namespace osu.Game.Tests.Visual.DailyChallenge { public partial class TestSceneDailyChallenge : OnlinePlayTestScene { + [Cached(typeof(MetadataClient))] + private TestMetadataClient metadataClient = new TestMetadataClient(); + + [Cached(typeof(INotificationOverlay))] + private NotificationOverlay notificationOverlay = new NotificationOverlay(); + + [BackgroundDependencyLoader] + private void load() + { + base.Content.Add(notificationOverlay); + } + [Test] public void TestDailyChallenge() { @@ -36,5 +52,62 @@ namespace osu.Game.Tests.Visual.DailyChallenge AddStep("add room", () => API.Perform(new CreateRoomRequest(room))); AddStep("push screen", () => LoadScreen(new Screens.OnlinePlay.DailyChallenge.DailyChallenge(room))); } + + [Test] + public void TestNotifications() + { + var room = new Room + { + RoomID = { Value = 1234 }, + Name = { Value = "Daily Challenge: June 4, 2024" }, + Playlist = + { + new PlaylistItem(CreateAPIBeatmapSet().Beatmaps.First()) + { + RequiredMods = [new APIMod(new OsuModTraceable())], + AllowedMods = [new APIMod(new OsuModDoubleTime())] + } + }, + EndDate = { Value = DateTimeOffset.Now.AddHours(12) }, + Category = { Value = RoomCategory.DailyChallenge } + }; + + AddStep("add room", () => API.Perform(new CreateRoomRequest(room))); + AddStep("set daily challenge info", () => metadataClient.DailyChallengeInfo.Value = new DailyChallengeInfo { RoomID = 1234 }); + AddStep("push screen", () => LoadScreen(new Screens.OnlinePlay.DailyChallenge.DailyChallenge(room))); + AddStep("daily challenge ended", () => metadataClient.DailyChallengeInfo.Value = null); + AddStep("install custom handler", () => + { + ((DummyAPIAccess)API).HandleRequest = req => + { + switch (req) + { + case GetRoomRequest r: + { + r.TriggerSuccess(new Room + { + RoomID = { Value = 1235, }, + Name = { Value = "Daily Challenge: June 5, 2024" }, + Playlist = + { + new PlaylistItem(CreateAPIBeatmapSet().Beatmaps.First()) + { + RequiredMods = [new APIMod(new OsuModTraceable())], + AllowedMods = [new APIMod(new OsuModDoubleTime())] + } + }, + EndDate = { Value = DateTimeOffset.Now.AddHours(12) }, + Category = { Value = RoomCategory.DailyChallenge } + }); + return true; + } + + default: + return false; + } + }; + }); + AddStep("next daily challenge started", () => metadataClient.DailyChallengeInfo.Value = new DailyChallengeInfo { RoomID = 1235 }); + } } } diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs index 4b4e4a7a62..d176f36162 100644 --- a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs @@ -30,6 +30,7 @@ using osu.Game.Online.Metadata; using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; using osu.Game.Overlays; +using osu.Game.Overlays.Notifications; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Screens.OnlinePlay.Components; @@ -54,6 +55,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge private readonly Bindable> userMods = new Bindable>(Array.Empty()); private readonly IBindable apiState = new Bindable(); + private readonly IBindable dailyChallengeInfo = new Bindable(); private OnlinePlayScreenWaveContainer waves = null!; private DailyChallengeLeaderboard leaderboard = null!; @@ -65,6 +67,8 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge private DailyChallengeTotalsDisplay totals = null!; private DailyChallengeEventFeed feed = null!; + private SimpleNotification? waitForNextChallengeNotification; + [Cached] private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Plum); @@ -98,6 +102,9 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge [Resolved] private PreviewTrackManager previewTrackManager { get; set; } = null!; + [Resolved] + private INotificationOverlay? notificationOverlay { get; set; } + public override bool DisallowExternalBeatmapRulesetChanges => true; public override bool? ApplyModTrackAdjustments => true; @@ -336,6 +343,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge } metadataClient.MultiplayerRoomScoreSet += onRoomScoreSet; + dailyChallengeInfo.BindTo(metadataClient.DailyChallengeInfo); ((IBindable)breakdown.UserBestScore).BindTo(leaderboard.UserBestScore); } @@ -388,6 +396,8 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge apiState.BindTo(API.State); apiState.BindValueChanged(onlineStateChanged, true); + + dailyChallengeInfo.BindValueChanged(dailyChallengeChanged); } private void trySetDailyChallengeBeatmap() @@ -405,6 +415,29 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge Schedule(forcefullyExit); }); + private void dailyChallengeChanged(ValueChangedEvent change) + { + if (change.OldValue?.RoomID == room.RoomID.Value && change.NewValue == null) + { + notificationOverlay?.Post(waitForNextChallengeNotification = new SimpleNotification + { + Text = "Today's daily challenge has concluded. Thanks for playing! The next one should appear in a few minutes." + }); + } + + if (change.NewValue != null && change.NewValue.Value.RoomID != room.RoomID.Value) + { + var roomRequest = new GetRoomRequest(change.NewValue.Value.RoomID); + + roomRequest.Success += room => + { + waitForNextChallengeNotification?.Close(false); + notificationOverlay?.Post(new NewDailyChallengeNotification(room)); + }; + API.Queue(roomRequest); + } + } + private void forcefullyExit() { Logger.Log($"{this} forcefully exiting due to loss of API connection"); diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/NewDailyChallengeNotification.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/NewDailyChallengeNotification.cs new file mode 100644 index 0000000000..36ec8b37a7 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/NewDailyChallengeNotification.cs @@ -0,0 +1,44 @@ +// 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.Framework.Screens; +using osu.Game.Beatmaps.Drawables.Cards; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Online.Rooms; +using osu.Game.Overlays.Notifications; +using osu.Game.Screens.Menu; + +namespace osu.Game.Screens.OnlinePlay.DailyChallenge +{ + public partial class NewDailyChallengeNotification : SimpleNotification + { + private readonly Room room; + + private BeatmapCardNano card = null!; + + public NewDailyChallengeNotification(Room room) + { + this.room = room; + } + + [BackgroundDependencyLoader] + private void load(OsuGame? game) + { + Text = "Today's daily challenge is here! Click here to play."; + Content.Add(card = new BeatmapCardNano((APIBeatmapSet)room.Playlist.Single().Beatmap.BeatmapSet!)); + Activated = () => + { + game?.PerformFromScreen(s => s.Push(new DailyChallenge(room)), [typeof(MainMenu)]); + return true; + }; + } + + protected override void Update() + { + base.Update(); + card.Width = Content.DrawWidth; + } + } +} diff --git a/osu.Game/Tests/Visual/Metadata/TestMetadataClient.cs b/osu.Game/Tests/Visual/Metadata/TestMetadataClient.cs index fa64a83352..2a0af0b10e 100644 --- a/osu.Game/Tests/Visual/Metadata/TestMetadataClient.cs +++ b/osu.Game/Tests/Visual/Metadata/TestMetadataClient.cs @@ -21,7 +21,7 @@ namespace osu.Game.Tests.Visual.Metadata public override IBindableDictionary UserStates => userStates; private readonly BindableDictionary userStates = new BindableDictionary(); - public override IBindable DailyChallengeInfo => dailyChallengeInfo; + public override Bindable DailyChallengeInfo => dailyChallengeInfo; private readonly Bindable dailyChallengeInfo = new Bindable(); [Resolved] From 1daeb7ebd0cb673c6dd63158c5fe70856ff02f61 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 29 Jul 2024 22:19:38 +0900 Subject: [PATCH 1712/2493] Rename typo in test naming --- osu.Game.Tests/Visual/Online/TestSceneDrawableChannel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneDrawableChannel.cs b/osu.Game.Tests/Visual/Online/TestSceneDrawableChannel.cs index bb73a458a3..7b7565b13f 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneDrawableChannel.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneDrawableChannel.cs @@ -86,7 +86,7 @@ namespace osu.Game.Tests.Visual.Online } [Test] - public void TestBackgroundAltering() + public void TestBackgroundAlternating() { var localUser = new APIUser { From b77a10b6db27757ee6e8bdd09a5eebccd7557fc6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 29 Jul 2024 15:28:52 +0200 Subject: [PATCH 1713/2493] Fix tests maybe --- .../Visual/DailyChallenge/TestSceneDailyChallenge.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallenge.cs b/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallenge.cs index 4a5f452ed1..b6dcc82ac1 100644 --- a/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallenge.cs +++ b/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallenge.cs @@ -76,8 +76,12 @@ namespace osu.Game.Tests.Visual.DailyChallenge AddStep("set daily challenge info", () => metadataClient.DailyChallengeInfo.Value = new DailyChallengeInfo { RoomID = 1234 }); AddStep("push screen", () => LoadScreen(new Screens.OnlinePlay.DailyChallenge.DailyChallenge(room))); AddStep("daily challenge ended", () => metadataClient.DailyChallengeInfo.Value = null); + + Func? previousHandler = null; + AddStep("install custom handler", () => { + previousHandler = ((DummyAPIAccess)API).HandleRequest; ((DummyAPIAccess)API).HandleRequest = req => { switch (req) @@ -108,6 +112,8 @@ namespace osu.Game.Tests.Visual.DailyChallenge }; }); AddStep("next daily challenge started", () => metadataClient.DailyChallengeInfo.Value = new DailyChallengeInfo { RoomID = 1235 }); + + AddStep("restore previous handler", () => ((DummyAPIAccess)API).HandleRequest = previousHandler); } } } From 621f4dfece39d1323fc209c48293289c992d5b3e Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 30 Jul 2024 02:45:12 +0300 Subject: [PATCH 1714/2493] Enforce new line between X/Y coordinate in editor position inspector --- osu.Game/Screens/Edit/Compose/Components/HitObjectInspector.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/HitObjectInspector.cs b/osu.Game/Screens/Edit/Compose/Components/HitObjectInspector.cs index 2f19888e9e..de23147e7b 100644 --- a/osu.Game/Screens/Edit/Compose/Components/HitObjectInspector.cs +++ b/osu.Game/Screens/Edit/Compose/Components/HitObjectInspector.cs @@ -58,7 +58,8 @@ namespace osu.Game.Screens.Edit.Compose.Components { case IHasPosition pos: AddHeader("Position"); - AddValue($"x:{pos.X:#,0.##} y:{pos.Y:#,0.##}"); + AddValue($"x:{pos.X:#,0.##}"); + AddValue($"y:{pos.Y:#,0.##}"); break; case IHasXPosition x: From 78417db06d1053cae9288db7afd5a46afa8f5659 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 30 Jul 2024 06:35:09 +0300 Subject: [PATCH 1715/2493] Remove stray line --- .../Profile/Header/Components/DailyChallengeStreakTooltip.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStreakTooltip.cs b/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStreakTooltip.cs index 02be0f2c99..5f28928665 100644 --- a/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStreakTooltip.cs +++ b/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStreakTooltip.cs @@ -203,7 +203,6 @@ namespace osu.Game.Overlays.Profile.Header.Components }, valueText = new OsuSpriteText { - // Colour = colour Font = OsuFont.GetFont(size: 40, weight: FontWeight.Light), } }; From 9868fb4aaa736c6956b549f0317a87e328961dc8 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 30 Jul 2024 06:36:02 +0300 Subject: [PATCH 1716/2493] Remove tier-based colour from the condensed piece to match web --- .../Profile/Header/Components/DailyChallengeStreakDisplay.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStreakDisplay.cs b/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStreakDisplay.cs index 87f833d165..b0f57099f5 100644 --- a/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStreakDisplay.cs +++ b/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStreakDisplay.cs @@ -102,7 +102,6 @@ namespace osu.Game.Overlays.Profile.Header.Components var statistics = User.Value.User.DailyChallengeStatistics; // dailyStreak.Text = UsersStrings.ShowDailyChallengeUnitDay(statistics.DailyStreakCurrent); dailyStreak.Text = $"{statistics.DailyStreakCurrent}d"; - dailyStreak.Colour = colours.ForRankingTier(DailyChallengeStreakTooltip.TierForDaily(statistics.DailyStreakCurrent)); TooltipContent = new DailyChallengeStreakTooltipData(colourProvider, statistics); } From 8b910e59f68ec871ae3f7f1d7543c14c906ab32d Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 30 Jul 2024 06:37:17 +0300 Subject: [PATCH 1717/2493] Reduce tooltip shadow outline --- .../Profile/Header/Components/DailyChallengeStreakTooltip.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStreakTooltip.cs b/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStreakTooltip.cs index 5f28928665..a105659ac7 100644 --- a/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStreakTooltip.cs +++ b/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStreakTooltip.cs @@ -45,7 +45,7 @@ namespace osu.Game.Overlays.Profile.Header.Components EdgeEffect = new EdgeEffectParameters { Type = EdgeEffectType.Shadow, - Colour = Color4.Black.Opacity(0.5f), + Colour = Color4.Black.Opacity(0.25f), Radius = 30f, }; From 33fc6dfaffe694e78ef4fc23c3871ba16eb286a8 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 30 Jul 2024 07:05:31 +0300 Subject: [PATCH 1718/2493] Hide daily challenge display when not selecting osu! Also hide when no user is displayed. --- .../Profile/Header/Components/DailyChallengeStreakDisplay.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStreakDisplay.cs b/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStreakDisplay.cs index b0f57099f5..a4c62d4357 100644 --- a/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStreakDisplay.cs +++ b/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStreakDisplay.cs @@ -93,9 +93,9 @@ namespace osu.Game.Overlays.Profile.Header.Components private void updateDisplay() { - if (User.Value == null) + if (User.Value == null || User.Value.Ruleset.OnlineID != 0) { - dailyStreak.Text = "-"; + Hide(); return; } @@ -103,6 +103,7 @@ namespace osu.Game.Overlays.Profile.Header.Components // dailyStreak.Text = UsersStrings.ShowDailyChallengeUnitDay(statistics.DailyStreakCurrent); dailyStreak.Text = $"{statistics.DailyStreakCurrent}d"; TooltipContent = new DailyChallengeStreakTooltipData(colourProvider, statistics); + Show(); } public ITooltip GetCustomTooltip() => new DailyChallengeStreakTooltip(); From 7c3d592a84451a9c2b2ee5a853e43035ed15d458 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 30 Jul 2024 07:04:52 +0300 Subject: [PATCH 1719/2493] Fix user profile overlay test scene being broke --- .../Visual/Online/TestSceneUserProfileOverlay.cs | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs index 937e08cb97..3bb38f167f 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs @@ -4,6 +4,7 @@ using System; using System.Linq; using NUnit.Framework; +using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Testing; using osu.Game.Online.API; @@ -24,7 +25,17 @@ namespace osu.Game.Tests.Visual.Online [SetUpSteps] public void SetUp() { - AddStep("create profile overlay", () => Child = profile = new UserProfileOverlay()); + AddStep("create profile overlay", () => + { + profile = new UserProfileOverlay(); + + Child = new DependencyProvidingContainer + { + RelativeSizeAxes = Axes.Both, + CachedDependencies = new (Type, object)[] { (typeof(UserProfileOverlay), profile) }, + Child = profile, + }; + }); } [Test] @@ -131,6 +142,7 @@ namespace osu.Game.Tests.Visual.Online CountryCode = CountryCode.JP, CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c2.jpg", ProfileHue = hue, + PlayMode = "osu", }); return true; } @@ -174,6 +186,7 @@ namespace osu.Game.Tests.Visual.Online CountryCode = CountryCode.JP, CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c2.jpg", ProfileHue = hue, + PlayMode = "osu", })); int hue2 = 0; @@ -189,6 +202,7 @@ namespace osu.Game.Tests.Visual.Online CountryCode = CountryCode.JP, CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c2.jpg", ProfileHue = hue2, + PlayMode = "osu", })); } From dca61eb76caca6b063025d39ba4c63d3865d25f6 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 30 Jul 2024 07:07:10 +0300 Subject: [PATCH 1720/2493] Remove no longer used dependency --- .../Profile/Header/Components/DailyChallengeStreakDisplay.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStreakDisplay.cs b/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStreakDisplay.cs index a4c62d4357..a9d0ab4f01 100644 --- a/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStreakDisplay.cs +++ b/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStreakDisplay.cs @@ -24,9 +24,6 @@ namespace osu.Game.Overlays.Profile.Header.Components [Resolved] private OverlayColourProvider colourProvider { get; set; } = null!; - [Resolved] - private OsuColour colours { get; set; } = null!; - [BackgroundDependencyLoader] private void load() { From 91dfe4515bbd74d1b0260788c2c1c12517842428 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 30 Jul 2024 08:12:03 +0300 Subject: [PATCH 1721/2493] Fix daily challenge display showing incorrect statistic --- .../Visual/Online/TestSceneUserProfileDailyChallenge.cs | 1 + .../Profile/Header/Components/DailyChallengeStreakDisplay.cs | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserProfileDailyChallenge.cs b/osu.Game.Tests/Visual/Online/TestSceneUserProfileDailyChallenge.cs index e2d26f222c..c0fb7b49f0 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneUserProfileDailyChallenge.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneUserProfileDailyChallenge.cs @@ -35,6 +35,7 @@ namespace osu.Game.Tests.Visual.Online AddSliderStep("weekly best", 0, 250, 1, v => update(s => s.WeeklyStreakBest = v)); AddSliderStep("top 10%", 0, 999, 0, v => update(s => s.Top10PercentPlacements = v)); AddSliderStep("top 50%", 0, 999, 0, v => update(s => s.Top50PercentPlacements = v)); + AddSliderStep("playcount", 0, 999, 0, v => update(s => s.PlayCount = v)); AddStep("create", () => { Clear(); diff --git a/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStreakDisplay.cs b/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStreakDisplay.cs index a9d0ab4f01..da0e334a4e 100644 --- a/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStreakDisplay.cs +++ b/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStreakDisplay.cs @@ -97,8 +97,8 @@ namespace osu.Game.Overlays.Profile.Header.Components } var statistics = User.Value.User.DailyChallengeStatistics; - // dailyStreak.Text = UsersStrings.ShowDailyChallengeUnitDay(statistics.DailyStreakCurrent); - dailyStreak.Text = $"{statistics.DailyStreakCurrent}d"; + // dailyStreak.Text = UsersStrings.ShowDailyChallengeUnitDay(statistics.PlayCount); + dailyStreak.Text = $"{statistics.PlayCount}d"; TooltipContent = new DailyChallengeStreakTooltipData(colourProvider, statistics); Show(); } From ae38e66036b62ceb9b3a2b2c8687878ffd5ca710 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 30 Jul 2024 08:17:23 +0200 Subject: [PATCH 1722/2493] Add failing test coverage --- .../Mods/ModDifficultyAdjustTest.cs | 47 +++++++++++++++++-- .../TestSceneModDifficultyAdjustSettings.cs | 29 +++++++++++- 2 files changed, 72 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Mods/ModDifficultyAdjustTest.cs b/osu.Game.Tests/Mods/ModDifficultyAdjustTest.cs index 4101652c49..e31a3dbdf0 100644 --- a/osu.Game.Tests/Mods/ModDifficultyAdjustTest.cs +++ b/osu.Game.Tests/Mods/ModDifficultyAdjustTest.cs @@ -8,6 +8,8 @@ using osu.Game.Online.API; using osu.Game.Rulesets; using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.UI; namespace osu.Game.Tests.Mods @@ -105,9 +107,6 @@ namespace osu.Game.Tests.Mods testMod.ResetSettingsToDefaults(); Assert.That(testMod.DrainRate.Value, Is.Null); - - // ReSharper disable once HeuristicUnreachableCode - // see https://youtrack.jetbrains.com/issue/RIDER-70159. Assert.That(testMod.OverallDifficulty.Value, Is.Null); var applied = applyDifficulty(new BeatmapDifficulty @@ -119,6 +118,48 @@ namespace osu.Game.Tests.Mods Assert.That(applied.OverallDifficulty, Is.EqualTo(10)); } + [Test] + public void TestDeserializeIncorrectRange() + { + var apiMod = new APIMod + { + Acronym = @"DA", + Settings = new Dictionary + { + [@"circle_size"] = -727, + [@"approach_rate"] = -727, + } + }; + var ruleset = new OsuRuleset(); + + var mod = (OsuModDifficultyAdjust)apiMod.ToMod(ruleset); + + Assert.Multiple(() => + { + Assert.That(mod.CircleSize.Value, Is.GreaterThanOrEqualTo(0).And.LessThanOrEqualTo(11)); + Assert.That(mod.ApproachRate.Value, Is.GreaterThanOrEqualTo(-10).And.LessThanOrEqualTo(11)); + }); + } + + [Test] + public void TestDeserializeNegativeApproachRate() + { + var apiMod = new APIMod + { + Acronym = @"DA", + Settings = new Dictionary + { + [@"approach_rate"] = -9, + } + }; + var ruleset = new OsuRuleset(); + + var mod = (OsuModDifficultyAdjust)apiMod.ToMod(ruleset); + + Assert.That(mod.ApproachRate.Value, Is.GreaterThanOrEqualTo(-10).And.LessThanOrEqualTo(11)); + Assert.That(mod.ApproachRate.Value, Is.EqualTo(-9)); + } + /// /// Applies a to the mod and returns a new /// representing the result if the mod were applied to a fresh instance. diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModDifficultyAdjustSettings.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModDifficultyAdjustSettings.cs index 307f436f84..b40d0b10d2 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModDifficultyAdjustSettings.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModDifficultyAdjustSettings.cs @@ -70,7 +70,7 @@ namespace osu.Game.Tests.Visual.UserInterface } [Test] - public void TestOutOfRangeValueStillApplied() + public void TestValueAboveRangeStillApplied() { AddStep("set override cs to 11", () => modDifficultyAdjust.CircleSize.Value = 11); @@ -91,6 +91,28 @@ namespace osu.Game.Tests.Visual.UserInterface checkBindableAtValue("Circle Size", 11); } + [Test] + public void TestValueBelowRangeStillApplied() + { + AddStep("set override cs to -5", () => modDifficultyAdjust.ApproachRate.Value = -5); + + checkSliderAtValue("Approach Rate", -5); + checkBindableAtValue("Approach Rate", -5); + + // this is a no-op, just showing that it won't reset the value during deserialisation. + setExtendedLimits(false); + + checkSliderAtValue("Approach Rate", -5); + checkBindableAtValue("Approach Rate", -5); + + // setting extended limits will reset the serialisation exception. + // this should be fine as the goal is to allow, at most, the value of extended limits. + setExtendedLimits(true); + + checkSliderAtValue("Approach Rate", -5); + checkBindableAtValue("Approach Rate", -5); + } + [Test] public void TestExtendedLimits() { @@ -109,6 +131,11 @@ namespace osu.Game.Tests.Visual.UserInterface checkSliderAtValue("Circle Size", 11); checkBindableAtValue("Circle Size", 11); + setSliderValue("Approach Rate", -5); + + checkSliderAtValue("Approach Rate", -5); + checkBindableAtValue("Approach Rate", -5); + setExtendedLimits(false); checkSliderAtValue("Circle Size", 10); From 6813f5ee0a746f4f6782de75c8fc7d46ac16d4e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 30 Jul 2024 08:17:35 +0200 Subject: [PATCH 1723/2493] Fix incorrect `DifficultyBindable` logic --- osu.Game/Rulesets/Mods/DifficultyBindable.cs | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/Mods/DifficultyBindable.cs b/osu.Game/Rulesets/Mods/DifficultyBindable.cs index 5f6fd21860..099806d320 100644 --- a/osu.Game/Rulesets/Mods/DifficultyBindable.cs +++ b/osu.Game/Rulesets/Mods/DifficultyBindable.cs @@ -38,6 +38,7 @@ namespace osu.Game.Rulesets.Mods public float MinValue { + get => minValue; set { if (value == minValue) @@ -52,6 +53,7 @@ namespace osu.Game.Rulesets.Mods public float MaxValue { + get => maxValue; set { if (value == maxValue) @@ -69,6 +71,7 @@ namespace osu.Game.Rulesets.Mods /// public float? ExtendedMinValue { + get => extendedMinValue; set { if (value == extendedMinValue) @@ -86,6 +89,7 @@ namespace osu.Game.Rulesets.Mods /// public float? ExtendedMaxValue { + get => extendedMaxValue; set { if (value == extendedMaxValue) @@ -114,9 +118,14 @@ namespace osu.Game.Rulesets.Mods { // Ensure that in the case serialisation runs in the wrong order (and limit extensions aren't applied yet) the deserialised value is still propagated. if (value != null) - CurrentNumber.MaxValue = MathF.Max(CurrentNumber.MaxValue, value.Value); + { + CurrentNumber.MinValue = Math.Clamp(MathF.Min(CurrentNumber.MinValue, value.Value), ExtendedMinValue ?? MinValue, MinValue); + CurrentNumber.MaxValue = Math.Clamp(MathF.Max(CurrentNumber.MaxValue, value.Value), MaxValue, ExtendedMaxValue ?? MaxValue); - base.Value = value; + base.Value = Math.Clamp(value.Value, CurrentNumber.MinValue, CurrentNumber.MaxValue); + } + else + base.Value = value; } } @@ -138,6 +147,8 @@ namespace osu.Game.Rulesets.Mods // the following max value copies are only safe as long as these values are effectively constants. otherDifficultyBindable.MaxValue = maxValue; otherDifficultyBindable.ExtendedMaxValue = extendedMaxValue; + otherDifficultyBindable.MinValue = minValue; + otherDifficultyBindable.ExtendedMinValue = extendedMinValue; } public override void BindTo(Bindable them) From bf10a910826fa68b65e1eb4f0e22b5a77003bad5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 30 Jul 2024 15:13:07 +0900 Subject: [PATCH 1724/2493] Adjust colouring to work better across multiple usages --- osu.Game/Overlays/Chat/ChatLine.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Overlays/Chat/ChatLine.cs b/osu.Game/Overlays/Chat/ChatLine.cs index 87e1f5699b..e4564724f0 100644 --- a/osu.Game/Overlays/Chat/ChatLine.cs +++ b/osu.Game/Overlays/Chat/ChatLine.cs @@ -123,13 +123,13 @@ namespace osu.Game.Overlays.Chat background = new Container { Masking = true, - Blending = BlendingParameters.Additive, CornerRadius = 4, Alpha = 0, RelativeSizeAxes = Axes.Both, + Blending = BlendingParameters.Additive, Child = new Box { - Colour = Color4.White, + Colour = Colour4.FromHex("#3b3234"), RelativeSizeAxes = Axes.Both, }, }, @@ -301,7 +301,7 @@ namespace osu.Game.Overlays.Chat private void updateBackground() { if (background != null) - background.Alpha = alternatingBackground ? 0.03f : 0; + background.Alpha = alternatingBackground ? 0.2f : 0; } } } From fc78dc9f3890bd068b4e5d62202467e45476a15c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 30 Jul 2024 16:02:49 +0900 Subject: [PATCH 1725/2493] Adjust paddings to avoid scrollbar overlap --- osu.Game/Overlays/Chat/ChatLine.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/osu.Game/Overlays/Chat/ChatLine.cs b/osu.Game/Overlays/Chat/ChatLine.cs index e4564724f0..a233c18115 100644 --- a/osu.Game/Overlays/Chat/ChatLine.cs +++ b/osu.Game/Overlays/Chat/ChatLine.cs @@ -118,6 +118,8 @@ namespace osu.Game.Overlays.Chat configManager.BindWith(OsuSetting.Prefer24HourTime, prefer24HourTime); prefer24HourTime.BindValueChanged(_ => updateTimestamp()); + Padding = new MarginPadding { Right = 5 }; + InternalChildren = new[] { background = new Container @@ -135,10 +137,10 @@ namespace osu.Game.Overlays.Chat }, new GridContainer { - Margin = new MarginPadding + Padding = new MarginPadding { - Horizontal = 10, - Vertical = 1, + Horizontal = 2, + Vertical = 2, }, RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, From a05f8107247bc6896a8716a22139b6143f332621 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 30 Jul 2024 10:07:38 +0200 Subject: [PATCH 1726/2493] Attempt to fix tests more --- .../Visual/DailyChallenge/TestSceneDailyChallenge.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallenge.cs b/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallenge.cs index b6dcc82ac1..b4d0b746a7 100644 --- a/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallenge.cs +++ b/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallenge.cs @@ -5,6 +5,7 @@ using System; using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; +using osu.Framework.Screens; using osu.Game.Online.API; using osu.Game.Online.Metadata; using osu.Game.Online.Rooms; @@ -74,7 +75,10 @@ namespace osu.Game.Tests.Visual.DailyChallenge AddStep("add room", () => API.Perform(new CreateRoomRequest(room))); AddStep("set daily challenge info", () => metadataClient.DailyChallengeInfo.Value = new DailyChallengeInfo { RoomID = 1234 }); - AddStep("push screen", () => LoadScreen(new Screens.OnlinePlay.DailyChallenge.DailyChallenge(room))); + + Screens.OnlinePlay.DailyChallenge.DailyChallenge screen = null!; + AddStep("push screen", () => LoadScreen(screen = new Screens.OnlinePlay.DailyChallenge.DailyChallenge(room))); + AddUntilStep("wait for screen", () => screen.IsCurrentScreen()); AddStep("daily challenge ended", () => metadataClient.DailyChallengeInfo.Value = null); Func? previousHandler = null; From 1b57a2a136c626939669a1a6f6e425b93f823ebe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 30 Jul 2024 10:36:26 +0200 Subject: [PATCH 1727/2493] Show new daily challenge notification globally --- .../DailyChallenge/TestSceneDailyChallenge.cs | 38 ---------------- .../UserInterface/TestSceneMainMenuButton.cs | 44 +++++++++++++++---- osu.Game/Screens/Menu/DailyChallengeButton.cs | 16 +++++-- .../DailyChallenge/DailyChallenge.cs | 12 ----- 4 files changed, 48 insertions(+), 62 deletions(-) diff --git a/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallenge.cs b/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallenge.cs index b4d0b746a7..25b3375f9e 100644 --- a/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallenge.cs +++ b/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallenge.cs @@ -80,44 +80,6 @@ namespace osu.Game.Tests.Visual.DailyChallenge AddStep("push screen", () => LoadScreen(screen = new Screens.OnlinePlay.DailyChallenge.DailyChallenge(room))); AddUntilStep("wait for screen", () => screen.IsCurrentScreen()); AddStep("daily challenge ended", () => metadataClient.DailyChallengeInfo.Value = null); - - Func? previousHandler = null; - - AddStep("install custom handler", () => - { - previousHandler = ((DummyAPIAccess)API).HandleRequest; - ((DummyAPIAccess)API).HandleRequest = req => - { - switch (req) - { - case GetRoomRequest r: - { - r.TriggerSuccess(new Room - { - RoomID = { Value = 1235, }, - Name = { Value = "Daily Challenge: June 5, 2024" }, - Playlist = - { - new PlaylistItem(CreateAPIBeatmapSet().Beatmaps.First()) - { - RequiredMods = [new APIMod(new OsuModTraceable())], - AllowedMods = [new APIMod(new OsuModDoubleTime())] - } - }, - EndDate = { Value = DateTimeOffset.Now.AddHours(12) }, - Category = { Value = RoomCategory.DailyChallenge } - }); - return true; - } - - default: - return false; - } - }; - }); - AddStep("next daily challenge started", () => metadataClient.DailyChallengeInfo.Value = new DailyChallengeInfo { RoomID = 1235 }); - - AddStep("restore previous handler", () => ((DummyAPIAccess)API).HandleRequest = previousHandler); } } } diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneMainMenuButton.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneMainMenuButton.cs index 5914898cb1..af98aa21db 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneMainMenuButton.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneMainMenuButton.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; @@ -10,6 +11,7 @@ using osu.Game.Localisation; using osu.Game.Online.API; using osu.Game.Online.Metadata; using osu.Game.Online.Rooms; +using osu.Game.Overlays; using osu.Game.Screens.Menu; using osuTK.Input; using Color4 = osuTK.Graphics.Color4; @@ -39,8 +41,6 @@ namespace osu.Game.Tests.Visual.UserInterface [Test] public void TestDailyChallengeButton() { - AddStep("beatmap of the day not active", () => metadataClient.DailyChallengeUpdated(null)); - AddStep("set up API", () => dummyAPI.HandleRequest = req => { switch (req) @@ -67,17 +67,45 @@ namespace osu.Game.Tests.Visual.UserInterface } }); - AddStep("add button", () => Child = new DailyChallengeButton(@"button-default-select", new Color4(102, 68, 204, 255), _ => { }, 0, Key.D) - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - ButtonSystemState = ButtonSystemState.TopLevel, - }); + NotificationOverlay notificationOverlay = null!; + DependencyProvidingContainer buttonContainer = null!; AddStep("beatmap of the day active", () => metadataClient.DailyChallengeUpdated(new DailyChallengeInfo { RoomID = 1234, })); + AddStep("add content", () => + { + notificationOverlay = new NotificationOverlay(); + Children = new Drawable[] + { + notificationOverlay, + buttonContainer = new DependencyProvidingContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + AutoSizeAxes = Axes.Both, + CachedDependencies = [(typeof(INotificationOverlay), notificationOverlay)], + Child = new DailyChallengeButton(@"button-default-select", new Color4(102, 68, 204, 255), _ => { }, 0, Key.D) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + ButtonSystemState = ButtonSystemState.TopLevel, + }, + }, + }; + }); + AddAssert("no notification posted", () => notificationOverlay.AllNotifications.Count(), () => Is.Zero); + + AddStep("beatmap of the day not active", () => metadataClient.DailyChallengeUpdated(null)); + AddAssert("no notification posted", () => notificationOverlay.AllNotifications.Count(), () => Is.Zero); + + AddStep("hide button's parent", () => buttonContainer.Hide()); + AddStep("beatmap of the day active", () => metadataClient.DailyChallengeUpdated(new DailyChallengeInfo + { + RoomID = 1234, + })); + AddAssert("notification posted", () => notificationOverlay.AllNotifications.Count(), () => Is.EqualTo(1)); } } } diff --git a/osu.Game/Screens/Menu/DailyChallengeButton.cs b/osu.Game/Screens/Menu/DailyChallengeButton.cs index c365994736..a5616b95a0 100644 --- a/osu.Game/Screens/Menu/DailyChallengeButton.cs +++ b/osu.Game/Screens/Menu/DailyChallengeButton.cs @@ -23,6 +23,7 @@ using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Metadata; using osu.Game.Online.Rooms; using osu.Game.Overlays; +using osu.Game.Screens.OnlinePlay.DailyChallenge; using osuTK; using osuTK.Graphics; using osuTK.Input; @@ -44,6 +45,9 @@ namespace osu.Game.Screens.Menu [Resolved] private IAPIProvider api { get; set; } = null!; + [Resolved] + private INotificationOverlay? notificationOverlay { get; set; } + public DailyChallengeButton(string sampleName, Color4 colour, Action? clickAction = null, params Key[] triggerKeys) : base(ButtonSystemStrings.DailyChallenge, sampleName, OsuIcon.DailyChallenge, colour, clickAction, triggerKeys) { @@ -100,7 +104,8 @@ namespace osu.Game.Screens.Menu { base.LoadComplete(); - info.BindValueChanged(updateDisplay, true); + info.BindValueChanged(_ => dailyChallengeChanged(postNotification: true)); + dailyChallengeChanged(postNotification: false); } protected override void Update() @@ -126,27 +131,30 @@ namespace osu.Game.Screens.Menu } } - private void updateDisplay(ValueChangedEvent info) + private void dailyChallengeChanged(bool postNotification) { UpdateState(); scheduledCountdownUpdate?.Cancel(); scheduledCountdownUpdate = null; - if (info.NewValue == null) + if (info.Value == null) { Room = null; cover.OnlineInfo = TooltipContent = null; } else { - var roomRequest = new GetRoomRequest(info.NewValue.Value.RoomID); + var roomRequest = new GetRoomRequest(info.Value.Value.RoomID); roomRequest.Success += room => { Room = room; cover.OnlineInfo = TooltipContent = room.Playlist.FirstOrDefault()?.Beatmap.BeatmapSet as APIBeatmapSet; + if (postNotification) + notificationOverlay?.Post(new NewDailyChallengeNotification(room)); + updateCountdown(); Scheduler.AddDelayed(updateCountdown, 1000, true); }; diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs index d176f36162..32209bc3b4 100644 --- a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs @@ -424,18 +424,6 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge Text = "Today's daily challenge has concluded. Thanks for playing! The next one should appear in a few minutes." }); } - - if (change.NewValue != null && change.NewValue.Value.RoomID != room.RoomID.Value) - { - var roomRequest = new GetRoomRequest(change.NewValue.Value.RoomID); - - roomRequest.Success += room => - { - waitForNextChallengeNotification?.Close(false); - notificationOverlay?.Post(new NewDailyChallengeNotification(room)); - }; - API.Queue(roomRequest); - } } private void forcefullyExit() From e63080eb2e5932ac97a656a0cfb2c06c7b3f96f0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 30 Jul 2024 16:59:46 +0900 Subject: [PATCH 1728/2493] Don't show seconds in chat timestamps --- osu.Game/Overlays/Chat/ChatLine.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Chat/ChatLine.cs b/osu.Game/Overlays/Chat/ChatLine.cs index a233c18115..6538bcfcf4 100644 --- a/osu.Game/Overlays/Chat/ChatLine.cs +++ b/osu.Game/Overlays/Chat/ChatLine.cs @@ -258,7 +258,7 @@ namespace osu.Game.Overlays.Chat private void updateTimestamp() { - drawableTimestamp.Text = message.Timestamp.LocalDateTime.ToLocalisableString(prefer24HourTime.Value ? @"HH:mm:ss" : @"hh:mm:ss tt"); + drawableTimestamp.Text = message.Timestamp.LocalDateTime.ToLocalisableString(prefer24HourTime.Value ? @"HH:mm" : @"hh:mm tt"); } private static readonly Color4[] default_username_colours = From a2a73232f3bdf6117e2d17bd3ae41423a32cc61b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 30 Jul 2024 16:59:32 +0900 Subject: [PATCH 1729/2493] Avoid showing timestamp in chat line when repeated --- osu.Game/Overlays/Chat/ChatLine.cs | 31 +++++++++++++++++++++-- osu.Game/Overlays/Chat/DrawableChannel.cs | 6 +++++ 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/Chat/ChatLine.cs b/osu.Game/Overlays/Chat/ChatLine.cs index 6538bcfcf4..4d228b2af0 100644 --- a/osu.Game/Overlays/Chat/ChatLine.cs +++ b/osu.Game/Overlays/Chat/ChatLine.cs @@ -71,6 +71,25 @@ namespace osu.Game.Overlays.Chat private Drawable? background; private bool alternatingBackground; + private bool requiresTimestamp = true; + + + public bool RequiresTimestamp + { + get => requiresTimestamp; + set + { + if (requiresTimestamp == value) + return; + + requiresTimestamp = value; + + if (!IsLoaded) + return; + + updateMessageContent(); + } + } public bool AlternatingBackground { @@ -244,9 +263,17 @@ namespace osu.Game.Overlays.Chat private void updateMessageContent() { this.FadeTo(message is LocalEchoMessage ? 0.4f : 1.0f, 500, Easing.OutQuint); - drawableTimestamp.FadeTo(message is LocalEchoMessage ? 0 : 1, 500, Easing.OutQuint); - updateTimestamp(); + if (requiresTimestamp && !(message is LocalEchoMessage)) + { + drawableTimestamp.Show(); + updateTimestamp(); + } + else + { + drawableTimestamp.Hide(); + } + drawableUsername.Text = $@"{message.Sender.Username}"; // remove non-existent channels from the link list diff --git a/osu.Game/Overlays/Chat/DrawableChannel.cs b/osu.Game/Overlays/Chat/DrawableChannel.cs index 6b3acaa226..05d09401a9 100644 --- a/osu.Game/Overlays/Chat/DrawableChannel.cs +++ b/osu.Game/Overlays/Chat/DrawableChannel.cs @@ -88,10 +88,16 @@ namespace osu.Game.Overlays.Chat { base.Update(); + int? minute = null; + for (int i = 0; i < ChatLineFlow.Count; i++) { if (ChatLineFlow[i] is ChatLine chatline) + { chatline.AlternatingBackground = i % 2 == 0; + chatline.RequiresTimestamp = chatline.Message.Timestamp.Minute != minute; + minute = chatline.Message.Timestamp.Minute; + } } } From 71649005bf02f5a158afdabe9d6ffd9273690777 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 30 Jul 2024 17:00:12 +0900 Subject: [PATCH 1730/2493] Elongate usernames in `DrawableChannel` test --- .../Visual/Online/TestSceneDrawableChannel.cs | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneDrawableChannel.cs b/osu.Game.Tests/Visual/Online/TestSceneDrawableChannel.cs index 7b7565b13f..dd12ee34ed 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneDrawableChannel.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneDrawableChannel.cs @@ -6,6 +6,7 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Graphics; using osu.Framework.Testing; +using osu.Framework.Utils; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Chat; using osu.Game.Overlays.Chat; @@ -88,19 +89,17 @@ namespace osu.Game.Tests.Visual.Online [Test] public void TestBackgroundAlternating() { - var localUser = new APIUser - { - Id = 3, - Username = "LocalUser" - }; - int messageCount = 1; AddRepeatStep("add messages", () => { channel.AddNewMessages(new Message(messageCount) { - Sender = localUser, + Sender = new APIUser + { + Id = 3, + Username = "LocalUser " + RNG.Next(0, int.MaxValue - 100).ToString("N") + }, Content = "Hi there all!", Timestamp = new DateTimeOffset(2022, 11, 21, 20, messageCount, 13, TimeSpan.Zero), Uuid = Guid.NewGuid().ToString(), From 4557ad43d5622221471a31d119bb4319d202cea7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 30 Jul 2024 17:00:26 +0900 Subject: [PATCH 1731/2493] Reduce padding on chat lines to give more breathing room --- osu.Game/Overlays/Chat/DrawableChannel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Chat/DrawableChannel.cs b/osu.Game/Overlays/Chat/DrawableChannel.cs index 05d09401a9..d20506ea4c 100644 --- a/osu.Game/Overlays/Chat/DrawableChannel.cs +++ b/osu.Game/Overlays/Chat/DrawableChannel.cs @@ -61,7 +61,7 @@ namespace osu.Game.Overlays.Chat Padding = new MarginPadding { Bottom = 5 }, Child = ChatLineFlow = new FillFlowContainer { - Padding = new MarginPadding { Horizontal = 10 }, + Padding = new MarginPadding { Left = 3, Right = 10 }, RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Direction = FillDirection.Vertical, From 6670f79258dc6fb9ab25505fc38d92df1158f562 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 30 Jul 2024 17:01:16 +0900 Subject: [PATCH 1732/2493] Reduce overall size of chat text --- osu.Game/Online/Chat/StandAloneChatDisplay.cs | 4 ++-- osu.Game/Overlays/Chat/ChatLine.cs | 2 +- osu.Game/Overlays/Chat/DaySeparator.cs | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Online/Chat/StandAloneChatDisplay.cs b/osu.Game/Online/Chat/StandAloneChatDisplay.cs index e3b5037367..440486d6a0 100644 --- a/osu.Game/Online/Chat/StandAloneChatDisplay.cs +++ b/osu.Game/Online/Chat/StandAloneChatDisplay.cs @@ -178,7 +178,7 @@ namespace osu.Game.Online.Chat protected partial class StandAloneDaySeparator : DaySeparator { - protected override float TextSize => 14; + protected override float TextSize => 13; protected override float LineHeight => 1; protected override float Spacing => 5; protected override float DateAlign => 125; @@ -198,7 +198,7 @@ namespace osu.Game.Online.Chat protected partial class StandAloneMessage : ChatLine { - protected override float FontSize => 15; + protected override float FontSize => 13; protected override float Spacing => 5; protected override float UsernameWidth => 75; diff --git a/osu.Game/Overlays/Chat/ChatLine.cs b/osu.Game/Overlays/Chat/ChatLine.cs index 4d228b2af0..adb193af32 100644 --- a/osu.Game/Overlays/Chat/ChatLine.cs +++ b/osu.Game/Overlays/Chat/ChatLine.cs @@ -46,7 +46,7 @@ namespace osu.Game.Overlays.Chat public IReadOnlyCollection DrawableContentFlow => drawableContentFlow; - protected virtual float FontSize => 14; + protected virtual float FontSize => 12; protected virtual float Spacing => 15; diff --git a/osu.Game/Overlays/Chat/DaySeparator.cs b/osu.Game/Overlays/Chat/DaySeparator.cs index e737b787ba..fd6b15c778 100644 --- a/osu.Game/Overlays/Chat/DaySeparator.cs +++ b/osu.Game/Overlays/Chat/DaySeparator.cs @@ -14,7 +14,7 @@ namespace osu.Game.Overlays.Chat { public partial class DaySeparator : Container { - protected virtual float TextSize => 15; + protected virtual float TextSize => 13; protected virtual float LineHeight => 2; From 7229ae83ea8449be6bd1bf290c69b3cb7bf2e494 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 30 Jul 2024 17:16:27 +0900 Subject: [PATCH 1733/2493] Adjust sizing and distribution of timestamp and username --- osu.Game/Online/Chat/StandAloneChatDisplay.cs | 2 +- osu.Game/Overlays/Chat/ChatLine.cs | 13 +++++++------ 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/osu.Game/Online/Chat/StandAloneChatDisplay.cs b/osu.Game/Online/Chat/StandAloneChatDisplay.cs index 440486d6a0..3a094cc074 100644 --- a/osu.Game/Online/Chat/StandAloneChatDisplay.cs +++ b/osu.Game/Online/Chat/StandAloneChatDisplay.cs @@ -200,7 +200,7 @@ namespace osu.Game.Online.Chat { protected override float FontSize => 13; protected override float Spacing => 5; - protected override float UsernameWidth => 75; + protected override float UsernameWidth => 90; public StandAloneMessage(Message message) : base(message) diff --git a/osu.Game/Overlays/Chat/ChatLine.cs b/osu.Game/Overlays/Chat/ChatLine.cs index adb193af32..29c6ec2564 100644 --- a/osu.Game/Overlays/Chat/ChatLine.cs +++ b/osu.Game/Overlays/Chat/ChatLine.cs @@ -20,6 +20,7 @@ using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Chat; +using osuTK; using osuTK.Graphics; namespace osu.Game.Overlays.Chat @@ -50,7 +51,7 @@ namespace osu.Game.Overlays.Chat protected virtual float Spacing => 15; - protected virtual float UsernameWidth => 130; + protected virtual float UsernameWidth => 150; [Resolved] private ChannelManager? chatManager { get; set; } @@ -73,7 +74,6 @@ namespace osu.Game.Overlays.Chat private bool alternatingBackground; private bool requiresTimestamp = true; - public bool RequiresTimestamp { get => requiresTimestamp; @@ -166,7 +166,7 @@ namespace osu.Game.Overlays.Chat RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize) }, ColumnDimensions = new[] { - new Dimension(GridSizeMode.AutoSize), + new Dimension(GridSizeMode.Absolute, 45), new Dimension(GridSizeMode.Absolute, Spacing + UsernameWidth + Spacing), new Dimension(), }, @@ -177,9 +177,10 @@ namespace osu.Game.Overlays.Chat drawableTimestamp = new OsuSpriteText { Shadow = false, - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Font = OsuFont.GetFont(size: FontSize * 0.75f, weight: FontWeight.SemiBold, fixedWidth: true), + Anchor = Anchor.TopLeft, + Origin = Anchor.TopLeft, + Spacing = new Vector2(-1, 0), + Font = OsuFont.GetFont(size: FontSize, weight: FontWeight.SemiBold, fixedWidth: true), AlwaysPresent = true, }, drawableUsername = new DrawableChatUsername(message.Sender) From 25747fdeb3c3a57db92fda84de3189f48c6784b9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 30 Jul 2024 18:06:56 +0900 Subject: [PATCH 1734/2493] Fix edge case where minutes are same but hour is different --- osu.Game/Overlays/Chat/DrawableChannel.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/osu.Game/Overlays/Chat/DrawableChannel.cs b/osu.Game/Overlays/Chat/DrawableChannel.cs index d20506ea4c..97660a34f1 100644 --- a/osu.Game/Overlays/Chat/DrawableChannel.cs +++ b/osu.Game/Overlays/Chat/DrawableChannel.cs @@ -88,15 +88,17 @@ namespace osu.Game.Overlays.Chat { base.Update(); - int? minute = null; + int? lastMinutes = null; for (int i = 0; i < ChatLineFlow.Count; i++) { if (ChatLineFlow[i] is ChatLine chatline) { + int minutes = chatline.Message.Timestamp.TotalOffsetMinutes; + chatline.AlternatingBackground = i % 2 == 0; - chatline.RequiresTimestamp = chatline.Message.Timestamp.Minute != minute; - minute = chatline.Message.Timestamp.Minute; + chatline.RequiresTimestamp = minutes != lastMinutes; + lastMinutes = minutes; } } } From d5f9173288ecd3a4030bd07e47b82e5c897f3af5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 30 Jul 2024 19:04:43 +0900 Subject: [PATCH 1735/2493] Remove unused local variable --- osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs index 32209bc3b4..e98e758ceb 100644 --- a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs @@ -67,8 +67,6 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge private DailyChallengeTotalsDisplay totals = null!; private DailyChallengeEventFeed feed = null!; - private SimpleNotification? waitForNextChallengeNotification; - [Cached] private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Plum); @@ -419,7 +417,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge { if (change.OldValue?.RoomID == room.RoomID.Value && change.NewValue == null) { - notificationOverlay?.Post(waitForNextChallengeNotification = new SimpleNotification + notificationOverlay?.Post(new SimpleNotification { Text = "Today's daily challenge has concluded. Thanks for playing! The next one should appear in a few minutes." }); From ff7815c3c563b2ad607d47da76ccbb9b1f0cc24b Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 30 Jul 2024 20:13:00 +0900 Subject: [PATCH 1736/2493] Submit vertices in local space to avoid cross-thread access --- osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs b/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs index 49e4ee18c1..5e8061bb6a 100644 --- a/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs +++ b/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs @@ -220,7 +220,6 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor private float fadeExponent; private readonly TrailPart[] parts = new TrailPart[max_sprites]; - private Vector2 size; private Vector2 originPosition; private IVertexBatch vertexBatch; @@ -236,7 +235,6 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor shader = Source.shader; texture = Source.texture; - size = Source.partSize; time = Source.time; fadeExponent = Source.FadeExponent; @@ -277,6 +275,8 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor RectangleF textureRect = texture.GetTextureRect(); + renderer.PushLocalMatrix(DrawInfo.Matrix); + foreach (var part in parts) { if (part.InvalidationID == -1) @@ -285,11 +285,9 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor if (time - part.Time >= 1) continue; - Vector2 screenSpacePos = Source.ToScreenSpace(part.Position); - vertexBatch.Add(new TexturedTrailVertex { - Position = new Vector2(screenSpacePos.X - size.X * originPosition.X, screenSpacePos.Y + size.Y * (1 - originPosition.Y)), + Position = new Vector2(part.Position.X - texture.DisplayWidth * originPosition.X, part.Position.Y + texture.DisplayHeight * (1 - originPosition.Y)), TexturePosition = textureRect.BottomLeft, TextureRect = new Vector4(0, 0, 1, 1), Colour = DrawColourInfo.Colour.BottomLeft.Linear, @@ -298,7 +296,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor vertexBatch.Add(new TexturedTrailVertex { - Position = new Vector2(screenSpacePos.X + size.X * (1 - originPosition.X), screenSpacePos.Y + size.Y * (1 - originPosition.Y)), + Position = new Vector2(part.Position.X + texture.DisplayWidth * (1 - originPosition.X), part.Position.Y + texture.DisplayHeight * (1 - originPosition.Y)), TexturePosition = textureRect.BottomRight, TextureRect = new Vector4(0, 0, 1, 1), Colour = DrawColourInfo.Colour.BottomRight.Linear, @@ -307,7 +305,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor vertexBatch.Add(new TexturedTrailVertex { - Position = new Vector2(screenSpacePos.X + size.X * (1 - originPosition.X), screenSpacePos.Y - size.Y * originPosition.Y), + Position = new Vector2(part.Position.X + texture.DisplayWidth * (1 - originPosition.X), part.Position.Y - texture.DisplayHeight * originPosition.Y), TexturePosition = textureRect.TopRight, TextureRect = new Vector4(0, 0, 1, 1), Colour = DrawColourInfo.Colour.TopRight.Linear, @@ -316,7 +314,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor vertexBatch.Add(new TexturedTrailVertex { - Position = new Vector2(screenSpacePos.X - size.X * originPosition.X, screenSpacePos.Y - size.Y * originPosition.Y), + Position = new Vector2(part.Position.X - texture.DisplayWidth * originPosition.X, part.Position.Y - texture.DisplayHeight * originPosition.Y), TexturePosition = textureRect.TopLeft, TextureRect = new Vector4(0, 0, 1, 1), Colour = DrawColourInfo.Colour.TopLeft.Linear, @@ -324,6 +322,8 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor }); } + renderer.PopLocalMatrix(); + vertexBatch.Draw(); shader.Unbind(); } From 7f22ade90da1426924b220f76373f2dcc5414911 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 30 Jul 2024 21:58:54 +0900 Subject: [PATCH 1737/2493] Fix oversight in timekeeping --- osu.Game/Overlays/Chat/DrawableChannel.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/Chat/DrawableChannel.cs b/osu.Game/Overlays/Chat/DrawableChannel.cs index 97660a34f1..41098ef823 100644 --- a/osu.Game/Overlays/Chat/DrawableChannel.cs +++ b/osu.Game/Overlays/Chat/DrawableChannel.cs @@ -88,13 +88,13 @@ namespace osu.Game.Overlays.Chat { base.Update(); - int? lastMinutes = null; + long? lastMinutes = null; for (int i = 0; i < ChatLineFlow.Count; i++) { if (ChatLineFlow[i] is ChatLine chatline) { - int minutes = chatline.Message.Timestamp.TotalOffsetMinutes; + long minutes = chatline.Message.Timestamp.ToUnixTimeSeconds() / 60; chatline.AlternatingBackground = i % 2 == 0; chatline.RequiresTimestamp = minutes != lastMinutes; From 5ebb5ad6707f1ca29c18ced13683c5ecfacedcb1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 31 Jul 2024 02:53:10 +0900 Subject: [PATCH 1738/2493] Fix test failure due to `TestMetadataClient` providing null statistics array --- .../Visual/DailyChallenge/TestSceneDailyChallenge.cs | 3 ++- osu.Game/Tests/Visual/Metadata/TestMetadataClient.cs | 9 ++++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallenge.cs b/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallenge.cs index 25b3375f9e..e10b3f76e6 100644 --- a/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallenge.cs +++ b/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallenge.cs @@ -29,6 +29,7 @@ namespace osu.Game.Tests.Visual.DailyChallenge private void load() { base.Content.Add(notificationOverlay); + base.Content.Add(metadataClient); } [Test] @@ -63,7 +64,7 @@ namespace osu.Game.Tests.Visual.DailyChallenge Name = { Value = "Daily Challenge: June 4, 2024" }, Playlist = { - new PlaylistItem(CreateAPIBeatmapSet().Beatmaps.First()) + new PlaylistItem(TestResources.CreateTestBeatmapSetInfo().Beatmaps.First()) { RequiredMods = [new APIMod(new OsuModTraceable())], AllowedMods = [new APIMod(new OsuModDoubleTime())] diff --git a/osu.Game/Tests/Visual/Metadata/TestMetadataClient.cs b/osu.Game/Tests/Visual/Metadata/TestMetadataClient.cs index 2a0af0b10e..c9f2b183e3 100644 --- a/osu.Game/Tests/Visual/Metadata/TestMetadataClient.cs +++ b/osu.Game/Tests/Visual/Metadata/TestMetadataClient.cs @@ -88,7 +88,14 @@ namespace osu.Game.Tests.Visual.Metadata } public override Task BeginWatchingMultiplayerRoom(long id) - => Task.FromResult(new MultiplayerPlaylistItemStats[MultiplayerPlaylistItemStats.TOTAL_SCORE_DISTRIBUTION_BINS]); + { + var stats = new MultiplayerPlaylistItemStats[MultiplayerPlaylistItemStats.TOTAL_SCORE_DISTRIBUTION_BINS]; + + for (int i = 0; i < stats.Length; i++) + stats[i] = new MultiplayerPlaylistItemStats { PlaylistItemID = i }; + + return Task.FromResult(stats); + } public override Task EndWatchingMultiplayerRoom(long id) => Task.CompletedTask; } From bdc465e1c68c29841ff01dc8efc07817d92f1a51 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 31 Jul 2024 03:06:35 +0900 Subject: [PATCH 1739/2493] Reword notification text slightly --- osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs | 2 +- .../OnlinePlay/DailyChallenge/NewDailyChallengeNotification.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs index db85db2cd3..8c8b6bdbf0 100644 --- a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs @@ -419,7 +419,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge { notificationOverlay?.Post(new SimpleNotification { - Text = "Today's daily challenge has concluded. Thanks for playing! The next one should appear in a few minutes." + Text = "Today's daily challenge has concluded – thanks for playing!\n\nTomorrow's challenge is now being prepared and will appear soon." }); } } diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/NewDailyChallengeNotification.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/NewDailyChallengeNotification.cs index 36ec8b37a7..3f14e63a2d 100644 --- a/osu.Game/Screens/OnlinePlay/DailyChallenge/NewDailyChallengeNotification.cs +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/NewDailyChallengeNotification.cs @@ -26,7 +26,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge [BackgroundDependencyLoader] private void load(OsuGame? game) { - Text = "Today's daily challenge is here! Click here to play."; + Text = "Today's daily challenge is now live! Click here to play."; Content.Add(card = new BeatmapCardNano((APIBeatmapSet)room.Playlist.Single().Beatmap.BeatmapSet!)); Activated = () => { From e77489f2a9d8e6edd6d75e888512e34064e3644f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 31 Jul 2024 03:10:36 +0900 Subject: [PATCH 1740/2493] Allow notification of new strings --- .../Localisation/DailyChallengeStrings.cs | 29 +++++++++++++++++++ .../DailyChallenge/DailyChallenge.cs | 7 ++--- .../NewDailyChallengeNotification.cs | 3 +- 3 files changed, 33 insertions(+), 6 deletions(-) create mode 100644 osu.Game/Localisation/DailyChallengeStrings.cs diff --git a/osu.Game/Localisation/DailyChallengeStrings.cs b/osu.Game/Localisation/DailyChallengeStrings.cs new file mode 100644 index 0000000000..32ff98db06 --- /dev/null +++ b/osu.Game/Localisation/DailyChallengeStrings.cs @@ -0,0 +1,29 @@ +// 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.Localisation +{ + public static class DailyChallengeStrings + { + private const string prefix = @"osu.Game.Resources.Localisation.DailyChallenge"; + + /// + /// "Today's daily challenge has concluded – thanks for playing! + /// + /// Tomorrow's challenge is now being prepared and will appear soon." + /// + public static LocalisableString ChallengeEndedNotification => new TranslatableString(getKey(@"todays_daily_challenge_has_concluded"), + @"Today's daily challenge has concluded – thanks for playing! + +Tomorrow's challenge is now being prepared and will appear soon."); + + /// + /// "Today's daily challenge is now live! Click here to play." + /// + public static LocalisableString ChallengeLiveNotification => new TranslatableString(getKey(@"todays_daily_challenge_is_now"), @"Today's daily challenge is now live! Click here to play."); + + private static string getKey(string key) => $@"{prefix}:{key}"; + } +} diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs index 8c8b6bdbf0..da2d9036c5 100644 --- a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs @@ -417,16 +417,13 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge { if (change.OldValue?.RoomID == room.RoomID.Value && change.NewValue == null) { - notificationOverlay?.Post(new SimpleNotification - { - Text = "Today's daily challenge has concluded – thanks for playing!\n\nTomorrow's challenge is now being prepared and will appear soon." - }); + notificationOverlay?.Post(new SimpleNotification { Text = DailyChallengeStrings.ChallengeEndedNotification }); } } private void forcefullyExit() { - Logger.Log($"{this} forcefully exiting due to loss of API connection"); + Logger.Log(@$"{this} forcefully exiting due to loss of API connection"); // This is temporary since we don't currently have a way to force screens to be exited // See also: `OnlinePlayScreen.forcefullyExit()` diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/NewDailyChallengeNotification.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/NewDailyChallengeNotification.cs index 3f14e63a2d..ea19828a21 100644 --- a/osu.Game/Screens/OnlinePlay/DailyChallenge/NewDailyChallengeNotification.cs +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/NewDailyChallengeNotification.cs @@ -9,6 +9,7 @@ using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Rooms; using osu.Game.Overlays.Notifications; using osu.Game.Screens.Menu; +using osu.Game.Localisation; namespace osu.Game.Screens.OnlinePlay.DailyChallenge { @@ -26,7 +27,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge [BackgroundDependencyLoader] private void load(OsuGame? game) { - Text = "Today's daily challenge is now live! Click here to play."; + Text = DailyChallengeStrings.ChallengeLiveNotification; Content.Add(card = new BeatmapCardNano((APIBeatmapSet)room.Playlist.Single().Beatmap.BeatmapSet!)); Activated = () => { From 36bd83bb80701da00a017af7a44a6f15cb3394bd Mon Sep 17 00:00:00 2001 From: Layendan Date: Tue, 30 Jul 2024 15:22:41 -0700 Subject: [PATCH 1741/2493] Update collection state when users add/remove from collection --- osu.Game/Screens/Ranking/CollectionButton.cs | 47 ++++++++++++++++++-- 1 file changed, 44 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Ranking/CollectionButton.cs b/osu.Game/Screens/Ranking/CollectionButton.cs index 4d53125005..804ffe9f75 100644 --- a/osu.Game/Screens/Ranking/CollectionButton.cs +++ b/osu.Game/Screens/Ranking/CollectionButton.cs @@ -1,26 +1,43 @@ // 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.Bindables; using osu.Framework.Extensions; +using osu.Framework.Graphics; using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.UserInterface; using osu.Game.Beatmaps; +using osu.Game.Collections; +using osu.Game.Database; using osu.Game.Graphics; using osu.Game.Graphics.UserInterface; using osuTK; +using Realms; namespace osu.Game.Screens.Ranking { public partial class CollectionButton : GrayButton, IHasPopover { private readonly BeatmapInfo beatmapInfo; + private readonly Bindable current; + + [Resolved] + private RealmAccess realmAccess { get; set; } = null!; + + private IDisposable? collectionSubscription; + + [Resolved] + private OsuColour colours { get; set; } = null!; public CollectionButton(BeatmapInfo beatmapInfo) : base(FontAwesome.Solid.Book) { this.beatmapInfo = beatmapInfo; + current = new Bindable(false); Size = new Vector2(75, 30); @@ -28,13 +45,37 @@ namespace osu.Game.Screens.Ranking } [BackgroundDependencyLoader] - private void load(OsuColour colours) + private void load() { - Background.Colour = colours.Green; - Action = this.ShowPopover; } + protected override void LoadComplete() + { + base.LoadComplete(); + + collectionSubscription = realmAccess.RegisterForNotifications(r => r.All(), updateRealm); + + current.BindValueChanged(_ => updateState(), true); + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + collectionSubscription?.Dispose(); + } + + private void updateRealm(IRealmCollection sender, ChangeSet? changes) + { + current.Value = sender.AsEnumerable().Any(c => c.BeatmapMD5Hashes.Contains(beatmapInfo.MD5Hash)); + } + + private void updateState() + { + Background.FadeColour(current.Value ? colours.Green : colours.Gray4, 500, Easing.InOutExpo); + } + public Popover GetPopover() => new CollectionPopover(beatmapInfo); } } From 8eeb5ae06b3204aee8dce0541181e191ca7e175a Mon Sep 17 00:00:00 2001 From: Layendan Date: Tue, 30 Jul 2024 17:08:56 -0700 Subject: [PATCH 1742/2493] Fix tests --- osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs | 5 ++--- osu.Game/Tests/Visual/OsuTestScene.cs | 4 +++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs b/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs index 36e256b920..bf29ae9442 100644 --- a/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs +++ b/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs @@ -197,9 +197,8 @@ namespace osu.Game.Tests.Visual.OnlinePlay case GetBeatmapSetRequest getBeatmapSetRequest: { - var baseBeatmap = getBeatmapSetRequest.Type == BeatmapSetLookupType.BeatmapId - ? beatmapManager.QueryBeatmap(b => b.OnlineID == getBeatmapSetRequest.ID) - : beatmapManager.QueryBeatmap(b => b.BeatmapSet.OnlineID == getBeatmapSetRequest.ID); + // Incorrect logic, see https://github.com/ppy/osu/pull/28991#issuecomment-2256721076 for reason why this change + var baseBeatmap = beatmapManager.QueryBeatmap(b => b.OnlineID == getBeatmapSetRequest.ID); if (baseBeatmap == null) { diff --git a/osu.Game/Tests/Visual/OsuTestScene.cs b/osu.Game/Tests/Visual/OsuTestScene.cs index 2b4c64dca8..09cfe5ecad 100644 --- a/osu.Game/Tests/Visual/OsuTestScene.cs +++ b/osu.Game/Tests/Visual/OsuTestScene.cs @@ -306,7 +306,9 @@ namespace osu.Game.Tests.Visual StarRating = original.StarRating, DifficultyName = original.DifficultyName, } - } + }, + HasFavourited = false, + FavouriteCount = 0, }; foreach (var beatmap in result.Beatmaps) From cbfb569ad47d17a6de63dd6484d3c9a15cd2d452 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 31 Jul 2024 14:37:56 +0900 Subject: [PATCH 1743/2493] 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 e7d9d4c022..3d8b643279 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -36,7 +36,7 @@ - + From 9e03dc3b5e8b67f6e8ff27ecbd213c2c15118244 Mon Sep 17 00:00:00 2001 From: jkh675 Date: Wed, 31 Jul 2024 16:52:53 +0800 Subject: [PATCH 1744/2493] Implement maximum width on `CommentTooltip` --- .../Overlays/Comments/CommentAuthorLine.cs | 66 ++++++++++++++++++- 1 file changed, 63 insertions(+), 3 deletions(-) diff --git a/osu.Game/Overlays/Comments/CommentAuthorLine.cs b/osu.Game/Overlays/Comments/CommentAuthorLine.cs index 1f6fef4df3..6c04f332bb 100644 --- a/osu.Game/Overlays/Comments/CommentAuthorLine.cs +++ b/osu.Game/Overlays/Comments/CommentAuthorLine.cs @@ -1,15 +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 System; using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; +using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; +using osu.Framework.Logging; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; @@ -139,11 +142,13 @@ namespace osu.Game.Overlays.Comments } } - private partial class ParentUsername : FillFlowContainer, IHasTooltip + private partial class ParentUsername : FillFlowContainer, IHasCustomTooltip { - public LocalisableString TooltipText => getParentMessage(); + public ITooltip GetCustomTooltip() => new CommentTooltip(); - private readonly Comment? parentComment; + LocalisableString IHasCustomTooltip.TooltipContent => getParentMessage(); + + private Comment? parentComment { get; } public ParentUsername(Comment comment) { @@ -176,5 +181,60 @@ namespace osu.Game.Overlays.Comments return parentComment.HasMessage ? parentComment.Message : parentComment.IsDeleted ? CommentsStrings.Deleted : string.Empty; } } + + private partial class CommentTooltip : VisibilityContainer, ITooltip + { + private const int max_width = 500; + + private TextFlowContainer content { get; set; } = null!; + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + AutoSizeAxes = Axes.Both; + + Masking = true; + CornerRadius = 7; + + Children = new Drawable[] + { + new Box + { + Colour = colours.Gray3, + RelativeSizeAxes = Axes.Both + }, + content = new TextFlowContainer(f => + { + f.Font = OsuFont.Default; + f.Truncate = true; + f.MaxWidth = max_width; + }) + { + Margin = new MarginPadding(3), + AutoSizeAxes = Axes.Both, + MaximumSize = new Vector2(max_width, float.PositiveInfinity), + } + }; + + FinishTransforms(); + } + + private LocalisableString lastPresent; + + public void SetContent(LocalisableString content) + { + if (lastPresent.Equals(content)) + return; + + this.content.Text = content; + lastPresent = content; + } + + public void Move(Vector2 pos) => Position = pos; + + protected override void PopIn() => this.FadeIn(200, Easing.OutQuint); + + protected override void PopOut() => this.FadeOut(200, Easing.OutQuint); + } } } From 5b46597d562ef2526d9f8206ae98af47a5919370 Mon Sep 17 00:00:00 2001 From: Caiyi Shyu Date: Wed, 31 Jul 2024 16:54:32 +0800 Subject: [PATCH 1745/2493] fix click to expand on touch devices --- .../Overlays/Mods/ModCustomisationHeader.cs | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Mods/ModCustomisationHeader.cs b/osu.Game/Overlays/Mods/ModCustomisationHeader.cs index 5a9e6099e6..3d6a23043d 100644 --- a/osu.Game/Overlays/Mods/ModCustomisationHeader.cs +++ b/osu.Game/Overlays/Mods/ModCustomisationHeader.cs @@ -95,11 +95,30 @@ namespace osu.Game.Overlays.Mods }, true); } + private bool touchedThisFrame; + + protected override bool OnTouchDown(TouchDownEvent e) + { + if (Enabled.Value) + { + touchedThisFrame = true; + Schedule(() => touchedThisFrame = false); + } + + return base.OnTouchDown(e); + } + protected override bool OnHover(HoverEvent e) { if (Enabled.Value) { - Parent?.UpdateHoverExpansion(true); + if (!touchedThisFrame) + Parent?.UpdateHoverExpansion(true); + } + if (Enabled.Value) + { + if (!touchedThisFrame) + Parent?.UpdateHoverExpansion(true); } return base.OnHover(e); From 5fb364cad6dff7243d03fe8287c4a79590f80104 Mon Sep 17 00:00:00 2001 From: Caiyi Shyu Date: Wed, 31 Jul 2024 16:56:25 +0800 Subject: [PATCH 1746/2493] remove redundant code added accidentally --- osu.Game/Overlays/Mods/ModCustomisationHeader.cs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModCustomisationHeader.cs b/osu.Game/Overlays/Mods/ModCustomisationHeader.cs index 3d6a23043d..306675a741 100644 --- a/osu.Game/Overlays/Mods/ModCustomisationHeader.cs +++ b/osu.Game/Overlays/Mods/ModCustomisationHeader.cs @@ -115,11 +115,6 @@ namespace osu.Game.Overlays.Mods if (!touchedThisFrame) Parent?.UpdateHoverExpansion(true); } - if (Enabled.Value) - { - if (!touchedThisFrame) - Parent?.UpdateHoverExpansion(true); - } return base.OnHover(e); } From 04ecefe2268a89d7471f207668bcb1aff725db83 Mon Sep 17 00:00:00 2001 From: jkh675 Date: Wed, 31 Jul 2024 17:23:25 +0800 Subject: [PATCH 1747/2493] Remove unused using --- osu.Game/Overlays/Comments/CommentAuthorLine.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/osu.Game/Overlays/Comments/CommentAuthorLine.cs b/osu.Game/Overlays/Comments/CommentAuthorLine.cs index 6c04f332bb..6d70de222d 100644 --- a/osu.Game/Overlays/Comments/CommentAuthorLine.cs +++ b/osu.Game/Overlays/Comments/CommentAuthorLine.cs @@ -1,18 +1,15 @@ // 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.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; -using osu.Framework.Logging; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; From 54e73acecea40d6d19f596aeb2d64e0f9858d7fc Mon Sep 17 00:00:00 2001 From: jkh675 Date: Wed, 31 Jul 2024 17:49:03 +0800 Subject: [PATCH 1748/2493] Cleanup code --- osu.Game/Overlays/Comments/CommentAuthorLine.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game/Overlays/Comments/CommentAuthorLine.cs b/osu.Game/Overlays/Comments/CommentAuthorLine.cs index 6d70de222d..fa58ce06fe 100644 --- a/osu.Game/Overlays/Comments/CommentAuthorLine.cs +++ b/osu.Game/Overlays/Comments/CommentAuthorLine.cs @@ -212,8 +212,6 @@ namespace osu.Game.Overlays.Comments MaximumSize = new Vector2(max_width, float.PositiveInfinity), } }; - - FinishTransforms(); } private LocalisableString lastPresent; From e329427d6e2c66a5c68ee9d6e9858c284abc9d5f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 31 Jul 2024 19:28:30 +0900 Subject: [PATCH 1749/2493] Apply nullability to `Timeline` --- .../Compose/Components/Timeline/Timeline.cs | 40 +++++++++---------- 1 file changed, 19 insertions(+), 21 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs index 6ce5c06801..c1a16e2503 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using osu.Framework.Allocation; using osu.Framework.Audio.Track; @@ -31,10 +29,10 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline private readonly Drawable userContent; [Resolved] - private EditorClock editorClock { get; set; } + private EditorClock editorClock { get; set; } = null!; [Resolved] - private EditorBeatmap editorBeatmap { get; set; } + private EditorBeatmap editorBeatmap { get; set; } = null!; /// /// The timeline's scroll position in the last frame. @@ -61,6 +59,22 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline /// private float defaultTimelineZoom; + private WaveformGraph waveform = null!; + + private TimelineTickDisplay ticks = null!; + + private TimelineControlPointDisplay controlPoints = null!; + + private Container mainContent = null!; + + private Bindable waveformOpacity = null!; + private Bindable controlPointsVisible = null!; + private Bindable ticksVisible = null!; + + private double trackLengthForZoom; + + private readonly IBindable track = new Bindable(); + public Timeline(Drawable userContent) { this.userContent = userContent; @@ -73,22 +87,6 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline ScrollbarVisible = false; } - private WaveformGraph waveform; - - private TimelineTickDisplay ticks; - - private TimelineControlPointDisplay controlPoints; - - private Container mainContent; - - private Bindable waveformOpacity; - private Bindable controlPointsVisible; - private Bindable ticksVisible; - - private double trackLengthForZoom; - - private readonly IBindable track = new Bindable(); - [BackgroundDependencyLoader] private void load(IBindable beatmap, OsuColour colours, OsuConfigManager config) { @@ -318,7 +316,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline } [Resolved] - private IBeatSnapProvider beatSnapProvider { get; set; } + private IBeatSnapProvider beatSnapProvider { get; set; } = null!; /// /// The total amount of time visible on the timeline. From 2d52bab77b7d008e296659e442a0cf2273e1b430 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 31 Jul 2024 19:43:08 +0900 Subject: [PATCH 1750/2493] Always show timing points in timeline when at the timing screen Supersedes https://github.com/ppy/osu/pull/29196. --- .../Compose/Components/Timeline/Timeline.cs | 17 ++++++++++++++++- .../Screens/Edit/EditorScreenWithTimeline.cs | 13 ++++++++++--- osu.Game/Screens/Edit/Timing/TimingScreen.cs | 8 ++++++++ 3 files changed, 34 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs index c1a16e2503..7a28f7bbaa 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs @@ -28,6 +28,21 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline private readonly Drawable userContent; + private bool alwaysShowControlPoints; + + public bool AlwaysShowControlPoints + { + get => alwaysShowControlPoints; + set + { + if (value == alwaysShowControlPoints) + return; + + alwaysShowControlPoints = value; + controlPointsVisible.TriggerChange(); + } + } + [Resolved] private EditorClock editorClock { get; set; } = null!; @@ -176,7 +191,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline controlPointsVisible.BindValueChanged(visible => { - if (visible.NewValue) + if (visible.NewValue || alwaysShowControlPoints) { this.ResizeHeightTo(timeline_expanded_height, 200, Easing.OutQuint); mainContent.MoveToY(15, 200, Easing.OutQuint); diff --git a/osu.Game/Screens/Edit/EditorScreenWithTimeline.cs b/osu.Game/Screens/Edit/EditorScreenWithTimeline.cs index 38d2a1e7e4..01908e45c7 100644 --- a/osu.Game/Screens/Edit/EditorScreenWithTimeline.cs +++ b/osu.Game/Screens/Edit/EditorScreenWithTimeline.cs @@ -5,7 +5,6 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Graphics.UserInterface; -using osu.Game.Overlays; using osu.Game.Screens.Edit.Compose.Components.Timeline; namespace osu.Game.Screens.Edit @@ -26,7 +25,7 @@ namespace osu.Game.Screens.Edit } [BackgroundDependencyLoader(true)] - private void load(OverlayColourProvider colourProvider) + private void load() { // Grid with only two rows. // First is the timeline area, which should be allowed to expand as required. @@ -107,10 +106,18 @@ namespace osu.Game.Screens.Edit MainContent.Add(content); content.FadeInFromZero(300, Easing.OutQuint); - LoadComponentAsync(TimelineArea = new TimelineArea(CreateTimelineContent()), timelineContent.Add); + LoadComponentAsync(TimelineArea = new TimelineArea(CreateTimelineContent()), timeline => + { + ConfigureTimeline(timeline); + timelineContent.Add(timeline); + }); }); } + protected virtual void ConfigureTimeline(TimelineArea timelineArea) + { + } + protected abstract Drawable CreateMainContent(); protected virtual Drawable CreateTimelineContent() => new Container(); diff --git a/osu.Game/Screens/Edit/Timing/TimingScreen.cs b/osu.Game/Screens/Edit/Timing/TimingScreen.cs index 3f911f5067..67d4429be8 100644 --- a/osu.Game/Screens/Edit/Timing/TimingScreen.cs +++ b/osu.Game/Screens/Edit/Timing/TimingScreen.cs @@ -6,6 +6,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Screens.Edit.Compose.Components.Timeline; namespace osu.Game.Screens.Edit.Timing { @@ -53,5 +54,12 @@ namespace osu.Game.Screens.Edit.Timing SelectedGroup.Value = EditorBeatmap.ControlPointInfo.GroupAt(nearestTimingPoint.Time); } } + + protected override void ConfigureTimeline(TimelineArea timelineArea) + { + base.ConfigureTimeline(timelineArea); + + timelineArea.Timeline.AlwaysShowControlPoints = true; + } } } From 5098d637b5c86a36ce2d45be22dadeb39b6022e3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 31 Jul 2024 19:55:20 +0900 Subject: [PATCH 1751/2493] Flash customise button on mod overlay when it becomes available --- .../Overlays/Mods/ModCustomisationHeader.cs | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Mods/ModCustomisationHeader.cs b/osu.Game/Overlays/Mods/ModCustomisationHeader.cs index bf10e13515..fb9e960f41 100644 --- a/osu.Game/Overlays/Mods/ModCustomisationHeader.cs +++ b/osu.Game/Overlays/Mods/ModCustomisationHeader.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; 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; @@ -11,14 +12,16 @@ using osu.Framework.Graphics.Sprites; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; -using osuTK; using osu.Game.Localisation; +using osuTK; +using osuTK.Graphics; namespace osu.Game.Overlays.Mods { public partial class ModCustomisationHeader : OsuHoverContainer { private Box background = null!; + private Box backgroundFlash = null!; private SpriteIcon icon = null!; [Resolved] @@ -46,6 +49,12 @@ namespace osu.Game.Overlays.Mods { RelativeSizeAxes = Axes.Both, }, + backgroundFlash = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.White.Opacity(0.4f), + Blending = BlendingParameters.Additive, + }, new OsuSpriteText { Anchor = Anchor.CentreLeft, @@ -84,6 +93,12 @@ namespace osu.Game.Overlays.Mods TooltipText = e.NewValue ? string.Empty : ModSelectOverlayStrings.CustomisationPanelDisabledReason; + + if (e.NewValue) + { + backgroundFlash.FadeInFromZero(150, Easing.OutQuad).Then() + .FadeOutFromOne(350, Easing.OutQuad); + } }, true); Expanded.BindValueChanged(v => From dab967e6be8603bfb50dc462099dd167c9cda965 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 1 Aug 2024 18:36:33 +0900 Subject: [PATCH 1752/2493] Fix insane transform allocations in new leaderboard display --- .../Leaderboards/LeaderboardScoreV2.cs | 58 +++++++++++++------ 1 file changed, 40 insertions(+), 18 deletions(-) diff --git a/osu.Game/Screens/SelectV2/Leaderboards/LeaderboardScoreV2.cs b/osu.Game/Screens/SelectV2/Leaderboards/LeaderboardScoreV2.cs index c9584b057b..b6508e177a 100644 --- a/osu.Game/Screens/SelectV2/Leaderboards/LeaderboardScoreV2.cs +++ b/osu.Game/Screens/SelectV2/Leaderboards/LeaderboardScoreV2.cs @@ -15,7 +15,6 @@ using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; -using osu.Framework.Layout; using osu.Framework.Localisation; using osu.Game.Configuration; using osu.Game.Extensions; @@ -38,6 +37,7 @@ using osu.Game.Users.Drawables; using osu.Game.Utils; using osuTK; using osuTK.Graphics; +using CommonStrings = osu.Game.Localisation.CommonStrings; namespace osu.Game.Screens.SelectV2.Leaderboards { @@ -61,7 +61,6 @@ namespace osu.Game.Screens.SelectV2.Leaderboards private const float statistics_regular_min_width = 175; private const float statistics_compact_min_width = 100; private const float rank_label_width = 65; - private const float rank_label_visibility_width_cutoff = rank_label_width + height + username_min_width + statistics_regular_min_width + expanded_right_content_width; private readonly ScoreInfo score; private readonly bool sheared; @@ -560,33 +559,34 @@ namespace osu.Game.Screens.SelectV2.Leaderboards background.FadeColour(IsHovered ? backgroundColour.Lighten(0.2f) : backgroundColour, transition_duration, Easing.OutQuint); totalScoreBackground.FadeColour(IsHovered ? lightenedGradient : totalScoreBackgroundGradient, transition_duration, Easing.OutQuint); - if (DrawWidth < rank_label_visibility_width_cutoff && IsHovered) + if (IsHovered && currentMode != DisplayMode.Full) rankLabelOverlay.FadeIn(transition_duration, Easing.OutQuint); else rankLabelOverlay.FadeOut(transition_duration, Easing.OutQuint); } - protected override bool OnInvalidate(Invalidation invalidation, InvalidationSource source) - { - Scheduler.AddOnce(() => - { - // when width decreases - // - hide rank and show rank overlay on avatar when hovered, then - // - compact statistics, then - // - hide statistics + private DisplayMode? currentMode; - if (DrawWidth >= rank_label_visibility_width_cutoff) + protected override void Update() + { + base.Update(); + + DisplayMode mode = getCurrentDisplayMode(); + + if (currentMode != mode) + { + if (mode >= DisplayMode.Full) rankLabel.FadeIn(transition_duration, Easing.OutQuint).MoveToX(0, transition_duration, Easing.OutQuint); else rankLabel.FadeOut(transition_duration, Easing.OutQuint).MoveToX(-rankLabel.DrawWidth, transition_duration, Easing.OutQuint); - if (DrawWidth >= height + username_min_width + statistics_regular_min_width + expanded_right_content_width) + if (mode >= DisplayMode.Regular) { statisticsContainer.FadeIn(transition_duration, Easing.OutQuint).MoveToX(0, transition_duration, Easing.OutQuint); statisticsContainer.Direction = FillDirection.Horizontal; statisticsContainer.ScaleTo(1, transition_duration, Easing.OutQuint); } - else if (DrawWidth >= height + username_min_width + statistics_compact_min_width + expanded_right_content_width) + else if (mode >= DisplayMode.Compact) { statisticsContainer.FadeIn(transition_duration, Easing.OutQuint).MoveToX(0, transition_duration, Easing.OutQuint); statisticsContainer.Direction = FillDirection.Vertical; @@ -594,13 +594,35 @@ namespace osu.Game.Screens.SelectV2.Leaderboards } else statisticsContainer.FadeOut(transition_duration, Easing.OutQuint).MoveToX(statisticsContainer.DrawWidth, transition_duration, Easing.OutQuint); - }); - return base.OnInvalidate(invalidation, source); + currentMode = mode; + } + } + + private DisplayMode getCurrentDisplayMode() + { + if (DrawWidth >= height + username_min_width + statistics_regular_min_width + expanded_right_content_width + rank_label_width) + return DisplayMode.Full; + + if (DrawWidth >= height + username_min_width + statistics_regular_min_width + expanded_right_content_width) + return DisplayMode.Regular; + + if (DrawWidth >= height + username_min_width + statistics_compact_min_width + expanded_right_content_width) + return DisplayMode.Compact; + + return DisplayMode.Minimal; } #region Subclasses + private enum DisplayMode + { + Minimal, + Compact, + Regular, + Full + } + private partial class DateLabel : DrawableDate { public DateLabel(DateTimeOffset date) @@ -749,8 +771,8 @@ namespace osu.Game.Screens.SelectV2.Leaderboards if (score.Files.Count <= 0) return items.ToArray(); - items.Add(new OsuMenuItem(Localisation.CommonStrings.Export, MenuItemType.Standard, () => scoreManager.Export(score))); - items.Add(new OsuMenuItem(CommonStrings.ButtonsDelete, MenuItemType.Destructive, () => dialogOverlay?.Push(new LocalScoreDeleteDialog(score)))); + items.Add(new OsuMenuItem(CommonStrings.Export, MenuItemType.Standard, () => scoreManager.Export(score))); + items.Add(new OsuMenuItem(Resources.Localisation.Web.CommonStrings.ButtonsDelete, MenuItemType.Destructive, () => dialogOverlay?.Push(new LocalScoreDeleteDialog(score)))); return items.ToArray(); } From 19a4cef113fcae9f1807e992c2e746bb0c8c0dad Mon Sep 17 00:00:00 2001 From: Layendan Date: Thu, 1 Aug 2024 02:52:41 -0700 Subject: [PATCH 1753/2493] update var names and test logic --- osu.Game/Screens/Ranking/CollectionButton.cs | 14 +++++++------- .../Visual/OnlinePlay/TestRoomRequestsHandler.cs | 6 ++++-- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/osu.Game/Screens/Ranking/CollectionButton.cs b/osu.Game/Screens/Ranking/CollectionButton.cs index 804ffe9f75..869c6a7ff4 100644 --- a/osu.Game/Screens/Ranking/CollectionButton.cs +++ b/osu.Game/Screens/Ranking/CollectionButton.cs @@ -23,7 +23,7 @@ namespace osu.Game.Screens.Ranking public partial class CollectionButton : GrayButton, IHasPopover { private readonly BeatmapInfo beatmapInfo; - private readonly Bindable current; + private readonly Bindable isInAnyCollection; [Resolved] private RealmAccess realmAccess { get; set; } = null!; @@ -37,7 +37,7 @@ namespace osu.Game.Screens.Ranking : base(FontAwesome.Solid.Book) { this.beatmapInfo = beatmapInfo; - current = new Bindable(false); + isInAnyCollection = new Bindable(false); Size = new Vector2(75, 30); @@ -54,9 +54,9 @@ namespace osu.Game.Screens.Ranking { base.LoadComplete(); - collectionSubscription = realmAccess.RegisterForNotifications(r => r.All(), updateRealm); + collectionSubscription = realmAccess.RegisterForNotifications(r => r.All(), collectionsChanged); - current.BindValueChanged(_ => updateState(), true); + isInAnyCollection.BindValueChanged(_ => updateState(), true); } protected override void Dispose(bool isDisposing) @@ -66,14 +66,14 @@ namespace osu.Game.Screens.Ranking collectionSubscription?.Dispose(); } - private void updateRealm(IRealmCollection sender, ChangeSet? changes) + private void collectionsChanged(IRealmCollection sender, ChangeSet? changes) { - current.Value = sender.AsEnumerable().Any(c => c.BeatmapMD5Hashes.Contains(beatmapInfo.MD5Hash)); + isInAnyCollection.Value = sender.AsEnumerable().Any(c => c.BeatmapMD5Hashes.Contains(beatmapInfo.MD5Hash)); } private void updateState() { - Background.FadeColour(current.Value ? colours.Green : colours.Gray4, 500, Easing.InOutExpo); + Background.FadeColour(isInAnyCollection.Value ? colours.Green : colours.Gray4, 500, Easing.InOutExpo); } public Popover GetPopover() => new CollectionPopover(beatmapInfo); diff --git a/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs b/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs index bf29ae9442..b6ceb61254 100644 --- a/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs +++ b/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs @@ -9,6 +9,7 @@ using System.Diagnostics; using System.Linq; using Newtonsoft.Json; using osu.Game.Beatmaps; +using osu.Game.Database; using osu.Game.Online.API; using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests.Responses; @@ -197,8 +198,9 @@ namespace osu.Game.Tests.Visual.OnlinePlay case GetBeatmapSetRequest getBeatmapSetRequest: { - // Incorrect logic, see https://github.com/ppy/osu/pull/28991#issuecomment-2256721076 for reason why this change - var baseBeatmap = beatmapManager.QueryBeatmap(b => b.OnlineID == getBeatmapSetRequest.ID); + var baseBeatmap = getBeatmapSetRequest.Type == BeatmapSetLookupType.BeatmapId + ? beatmapManager.QueryBeatmap(b => b.OnlineID == getBeatmapSetRequest.ID) + : beatmapManager.QueryBeatmapSet(s => s.OnlineID == getBeatmapSetRequest.ID)?.PerformRead(s => s.Beatmaps.First().Detach()); if (baseBeatmap == null) { From 188ddbcad68fa67a074c9a0f8860304d27d560b9 Mon Sep 17 00:00:00 2001 From: Caiyi Shyu Date: Thu, 1 Aug 2024 18:38:01 +0800 Subject: [PATCH 1754/2493] pass `ModCustomisationPanel` through ctor --- osu.Game/Overlays/Mods/ModCustomisationHeader.cs | 7 ++++--- osu.Game/Overlays/Mods/ModCustomisationPanel.cs | 13 +++++++++---- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModCustomisationHeader.cs b/osu.Game/Overlays/Mods/ModCustomisationHeader.cs index 306675a741..9589c7465f 100644 --- a/osu.Game/Overlays/Mods/ModCustomisationHeader.cs +++ b/osu.Game/Overlays/Mods/ModCustomisationHeader.cs @@ -29,10 +29,11 @@ namespace osu.Game.Overlays.Mods public readonly BindableBool Expanded = new BindableBool(); - protected new ModCustomisationPanel? Parent => (ModCustomisationPanel?)base.Parent; + private readonly ModCustomisationPanel panel; - public ModCustomisationHeader() + public ModCustomisationHeader(ModCustomisationPanel panel) { + this.panel = panel; Action = Expanded.Toggle; Enabled.Value = false; } @@ -113,7 +114,7 @@ namespace osu.Game.Overlays.Mods if (Enabled.Value) { if (!touchedThisFrame) - Parent?.UpdateHoverExpansion(true); + panel.UpdateHoverExpansion(true); } return base.OnHover(e); diff --git a/osu.Game/Overlays/Mods/ModCustomisationPanel.cs b/osu.Game/Overlays/Mods/ModCustomisationPanel.cs index 9795b61762..85991c3a9d 100644 --- a/osu.Game/Overlays/Mods/ModCustomisationPanel.cs +++ b/osu.Game/Overlays/Mods/ModCustomisationPanel.cs @@ -58,7 +58,7 @@ namespace osu.Game.Overlays.Mods InternalChildren = new Drawable[] { - new ModCustomisationHeader + new ModCustomisationHeader(this) { Depth = float.MinValue, RelativeSizeAxes = Axes.X, @@ -66,7 +66,7 @@ namespace osu.Game.Overlays.Mods Enabled = { BindTarget = Enabled }, Expanded = { BindTarget = Expanded }, }, - content = new FocusGrabbingContainer + content = new FocusGrabbingContainer(this) { RelativeSizeAxes = Axes.X, BorderColour = colourProvider.Dark3, @@ -229,12 +229,17 @@ namespace osu.Game.Overlays.Mods public override bool RequestsFocus => Expanded.Value; public override bool AcceptsFocus => Expanded.Value; - public new ModCustomisationPanel? Parent => (ModCustomisationPanel?)base.Parent; + private readonly ModCustomisationPanel panel; + + public FocusGrabbingContainer(ModCustomisationPanel panel) + { + this.panel = panel; + } protected override void OnHoverLost(HoverLostEvent e) { if (ExpandedByHovering.Value && !ReceivePositionalInputAt(e.ScreenSpaceMousePosition)) - Parent?.UpdateHoverExpansion(false); + panel.UpdateHoverExpansion(false); base.OnHoverLost(e); } From 548fd9cbf9e96b93cc69add00509e8b0491fce2d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 1 Aug 2024 19:44:42 +0900 Subject: [PATCH 1755/2493] Show breaks behind objects in timeline Closes https://github.com/ppy/osu/issues/29227. --- .../Screens/Edit/Compose/ComposeScreen.cs | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/ComposeScreen.cs b/osu.Game/Screens/Edit/Compose/ComposeScreen.cs index cc33840929..f7e523db25 100644 --- a/osu.Game/Screens/Edit/Compose/ComposeScreen.cs +++ b/osu.Game/Screens/Edit/Compose/ComposeScreen.cs @@ -69,19 +69,24 @@ namespace osu.Game.Screens.Edit.Compose if (ruleset == null || composer == null) return base.CreateTimelineContent(); + TimelineBreakDisplay breakDisplay = new TimelineBreakDisplay + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Height = 0.75f, + }; + return wrapSkinnableContent(new Container { RelativeSizeAxes = Axes.Both, - Children = new Drawable[] + Children = new[] { + // We want to display this below hitobjects to better expose placement objects visually. + // It needs to be above the blueprint container to handle drags on breaks though. + breakDisplay.CreateProxy(), new TimelineBlueprintContainer(composer), - new TimelineBreakDisplay - { - RelativeSizeAxes = Axes.Both, - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Height = 0.75f, - }, + breakDisplay } }); } From 051d52c23f69db59c3a4270f29c4d7a5fb439838 Mon Sep 17 00:00:00 2001 From: Caiyi Shyu Date: Thu, 1 Aug 2024 19:25:45 +0800 Subject: [PATCH 1756/2493] Update ModCustomisationPanel to use ExpandedState enum --- .../TestSceneFreeModSelectOverlay.cs | 2 +- .../TestSceneModCustomisationPanel.cs | 26 +++---- .../TestSceneModSelectOverlay.cs | 4 +- .../Overlays/Mods/ModCustomisationHeader.cs | 24 +++++-- .../Overlays/Mods/ModCustomisationPanel.cs | 68 ++++++++++--------- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 14 ++-- 6 files changed, 77 insertions(+), 61 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneFreeModSelectOverlay.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneFreeModSelectOverlay.cs index 497faa28d0..3097d24595 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneFreeModSelectOverlay.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneFreeModSelectOverlay.cs @@ -61,7 +61,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("select difficulty adjust", () => freeModSelectOverlay.SelectedMods.Value = new[] { new OsuModDifficultyAdjust() }); AddWaitStep("wait some", 3); - AddAssert("customisation area not expanded", () => !this.ChildrenOfType().Single().Expanded.Value); + AddAssert("customisation area not expanded", () => !this.ChildrenOfType().Single().Expanded); } [Test] diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModCustomisationPanel.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModCustomisationPanel.cs index 64ef7891c8..16c9c2bc14 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModCustomisationPanel.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModCustomisationPanel.cs @@ -51,22 +51,22 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep("set DT", () => { SelectedMods.Value = new[] { new OsuModDoubleTime() }; - panel.Enabled.Value = panel.Expanded.Value = true; + panel.Enabled.Value = panel.Expanded = true; }); AddStep("set DA", () => { SelectedMods.Value = new Mod[] { new OsuModDifficultyAdjust() }; - panel.Enabled.Value = panel.Expanded.Value = true; + panel.Enabled.Value = panel.Expanded = true; }); AddStep("set FL+WU+DA+AD", () => { SelectedMods.Value = new Mod[] { new OsuModFlashlight(), new ModWindUp(), new OsuModDifficultyAdjust(), new OsuModApproachDifferent() }; - panel.Enabled.Value = panel.Expanded.Value = true; + panel.Enabled.Value = panel.Expanded = true; }); AddStep("set empty", () => { SelectedMods.Value = Array.Empty(); - panel.Enabled.Value = panel.Expanded.Value = false; + panel.Enabled.Value = panel.Expanded = false; }); } @@ -77,11 +77,11 @@ namespace osu.Game.Tests.Visual.UserInterface { AddStep("hover header", () => InputManager.MoveMouseTo(header)); - AddAssert("not expanded", () => !panel.Expanded.Value); + AddAssert("not expanded", () => !panel.Expanded); AddStep("hover content", () => InputManager.MoveMouseTo(content)); - AddAssert("neither expanded", () => !panel.Expanded.Value); + AddAssert("neither expanded", () => !panel.Expanded); AddStep("left from content", () => InputManager.MoveMouseTo(Vector2.One)); } @@ -96,40 +96,40 @@ namespace osu.Game.Tests.Visual.UserInterface { AddStep("hover header", () => InputManager.MoveMouseTo(header)); - AddAssert("expanded", () => panel.Expanded.Value); + AddAssert("expanded", () => panel.Expanded); AddStep("hover content", () => InputManager.MoveMouseTo(content)); - AddAssert("still expanded", () => panel.Expanded.Value); + AddAssert("still expanded", () => panel.Expanded); } // Will collapse when mouse left from content { AddStep("left from content", () => InputManager.MoveMouseTo(Vector2.One)); - AddAssert("not expanded", () => !panel.Expanded.Value); + AddAssert("not expanded", () => !panel.Expanded); } // Will collapse when mouse left from header { AddStep("hover header", () => InputManager.MoveMouseTo(header)); - AddAssert("expanded", () => panel.Expanded.Value); + AddAssert("expanded", () => panel.Expanded); AddStep("left from header", () => InputManager.MoveMouseTo(Vector2.One)); - AddAssert("not expanded", () => !panel.Expanded.Value); + AddAssert("not expanded", () => !panel.Expanded); } // Not collapse when mouse left if not expanded by hovering { - AddStep("expand not by hovering", () => panel.Expanded.Value = true); + AddStep("expand not by hovering", () => panel.Expanded = true); AddStep("hover content", () => InputManager.MoveMouseTo(content)); AddStep("moust left", () => InputManager.MoveMouseTo(Vector2.One)); - AddAssert("still expanded", () => panel.Expanded.Value); + AddAssert("still expanded", () => panel.Expanded); } } } diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs index 77909d6936..0057582755 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs @@ -999,7 +999,7 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep("press mouse", () => InputManager.PressButton(MouseButton.Left)); AddAssert("search still not focused", () => !this.ChildrenOfType().Single().HasFocus); AddStep("release mouse", () => InputManager.ReleaseButton(MouseButton.Left)); - AddAssert("customisation panel closed by click", () => !this.ChildrenOfType().Single().Expanded.Value); + AddAssert("customisation panel closed by click", () => !this.ChildrenOfType().Single().Expanded); if (textSearchStartsActive) AddAssert("search focused", () => this.ChildrenOfType().Single().HasFocus); @@ -1022,7 +1022,7 @@ namespace osu.Game.Tests.Visual.UserInterface private void assertCustomisationToggleState(bool disabled, bool active) { AddUntilStep($"customisation panel is {(disabled ? "" : "not ")}disabled", () => modSelectOverlay.ChildrenOfType().Single().Enabled.Value == !disabled); - AddAssert($"customisation panel is {(active ? "" : "not ")}active", () => modSelectOverlay.ChildrenOfType().Single().Expanded.Value == active); + AddAssert($"customisation panel is {(active ? "" : "not ")}active", () => modSelectOverlay.ChildrenOfType().Single().Expanded == active); } private T getSelectedMod() where T : Mod => SelectedMods.Value.OfType().Single(); diff --git a/osu.Game/Overlays/Mods/ModCustomisationHeader.cs b/osu.Game/Overlays/Mods/ModCustomisationHeader.cs index 9589c7465f..76d2a0deb1 100644 --- a/osu.Game/Overlays/Mods/ModCustomisationHeader.cs +++ b/osu.Game/Overlays/Mods/ModCustomisationHeader.cs @@ -14,6 +14,7 @@ using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osuTK; using osu.Game.Localisation; +using static osu.Game.Overlays.Mods.ModCustomisationPanel; namespace osu.Game.Overlays.Mods { @@ -27,14 +28,13 @@ namespace osu.Game.Overlays.Mods protected override IEnumerable EffectTargets => new[] { background }; - public readonly BindableBool Expanded = new BindableBool(); + public readonly Bindable ExpandedState = new Bindable(ModCustomisationPanelState.Collapsed); private readonly ModCustomisationPanel panel; public ModCustomisationHeader(ModCustomisationPanel panel) { this.panel = panel; - Action = Expanded.Toggle; Enabled.Value = false; } @@ -90,12 +90,26 @@ namespace osu.Game.Overlays.Mods : ModSelectOverlayStrings.CustomisationPanelDisabledReason; }, true); - Expanded.BindValueChanged(v => + ExpandedState.BindValueChanged(v => { - icon.ScaleTo(v.NewValue ? new Vector2(1, -1) : Vector2.One, 300, Easing.OutQuint); + icon.ScaleTo(v.NewValue > ModCustomisationPanelState.Collapsed ? new Vector2(1, -1) : Vector2.One, 300, Easing.OutQuint); }, true); } + protected override bool OnClick(ClickEvent e) + { + if (Enabled.Value) + { + ExpandedState.Value = ExpandedState.Value switch + { + ModCustomisationPanelState.Collapsed => ModCustomisationPanelState.Expanded, + _ => ModCustomisationPanelState.Collapsed + }; + } + + return base.OnClick(e); + } + private bool touchedThisFrame; protected override bool OnTouchDown(TouchDownEvent e) @@ -114,7 +128,7 @@ namespace osu.Game.Overlays.Mods if (Enabled.Value) { if (!touchedThisFrame) - panel.UpdateHoverExpansion(true); + panel.UpdateHoverExpansion(ModCustomisationPanelState.ExpandedByHover); } return base.OnHover(e); diff --git a/osu.Game/Overlays/Mods/ModCustomisationPanel.cs b/osu.Game/Overlays/Mods/ModCustomisationPanel.cs index 85991c3a9d..a551081a7b 100644 --- a/osu.Game/Overlays/Mods/ModCustomisationPanel.cs +++ b/osu.Game/Overlays/Mods/ModCustomisationPanel.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -39,7 +38,13 @@ namespace osu.Game.Overlays.Mods public readonly BindableBool Enabled = new BindableBool(); - public readonly BindableBool Expanded = new BindableBool(); + public readonly Bindable ExpandedState = new Bindable(ModCustomisationPanelState.Collapsed); + + public bool Expanded + { + get => ExpandedState.Value > ModCustomisationPanelState.Collapsed; + set => ExpandedState.Value = value ? ModCustomisationPanelState.Expanded : ModCustomisationPanelState.Collapsed; + } public Bindable> SelectedMods { get; } = new Bindable>(Array.Empty()); @@ -48,8 +53,8 @@ namespace osu.Game.Overlays.Mods // Handle{Non}PositionalInput controls whether the panel should act as a blocking layer on the screen. only block when the panel is expanded. // These properties are used because they correctly handle blocking/unblocking hover when mouse is pointing at a drawable outside // (returning Expanded.Value to OnHover or overriding Block{Non}PositionalInput doesn't work). - public override bool HandlePositionalInput => Expanded.Value; - public override bool HandleNonPositionalInput => Expanded.Value; + public override bool HandlePositionalInput => Expanded; + public override bool HandleNonPositionalInput => Expanded; [BackgroundDependencyLoader] private void load() @@ -64,7 +69,7 @@ namespace osu.Game.Overlays.Mods RelativeSizeAxes = Axes.X, Height = header_height, Enabled = { BindTarget = Enabled }, - Expanded = { BindTarget = Expanded }, + ExpandedState = { BindTarget = ExpandedState }, }, content = new FocusGrabbingContainer(this) { @@ -81,8 +86,7 @@ namespace osu.Game.Overlays.Mods Roundness = 5f, Colour = Color4.Black.Opacity(0.25f), }, - Expanded = { BindTarget = Expanded }, - ExpandedByHovering = { BindTarget = ExpandedByHovering }, + ExpandedState = { BindTarget = ExpandedState }, Children = new Drawable[] { new Box @@ -124,7 +128,7 @@ namespace osu.Game.Overlays.Mods this.FadeColour(OsuColour.Gray(e.NewValue ? 1f : 0.6f), 300, Easing.OutQuint); }, true); - Expanded.BindValueChanged(_ => updateDisplay(), true); + ExpandedState.BindValueChanged(_ => updateDisplay(), true); SelectedMods.BindValueChanged(_ => updateMods(), true); FinishTransforms(true); @@ -136,7 +140,7 @@ namespace osu.Game.Overlays.Mods protected override bool OnClick(ClickEvent e) { - Expanded.Value = false; + Expanded = false; return base.OnClick(e); } @@ -149,7 +153,7 @@ namespace osu.Game.Overlays.Mods switch (e.Action) { case GlobalAction.Back: - Expanded.Value = false; + Expanded = false; return true; } @@ -164,7 +168,7 @@ namespace osu.Game.Overlays.Mods { content.ClearTransforms(); - if (Expanded.Value) + if (Expanded) { content.AutoSizeDuration = 400; content.AutoSizeEasing = Easing.OutQuint; @@ -177,30 +181,19 @@ namespace osu.Game.Overlays.Mods content.ResizeHeightTo(header_height, 400, Easing.OutQuint); content.FadeOut(400, Easing.OutSine); } - - ExpandedByHovering.Value = false; } - public readonly BindableBool ExpandedByHovering = new BindableBool(); - - public void UpdateHoverExpansion(bool hovered) + public void UpdateHoverExpansion(ModCustomisationPanelState state) { - if (hovered && !Expanded.Value) - { - Expanded.Value = true; - ExpandedByHovering.Value = true; - } - else if (!hovered && ExpandedByHovering.Value) - { - Debug.Assert(Expanded.Value); + if (state > ModCustomisationPanelState.Collapsed && state <= ExpandedState.Value) + return; - Expanded.Value = false; - } + ExpandedState.Value = state; } private void updateMods() { - Expanded.Value = false; + Expanded = false; sectionsFlow.Clear(); // Importantly, the selected mods bindable is already ordered by the mod select overlay (following the order of mod columns and panels). @@ -223,11 +216,10 @@ namespace osu.Game.Overlays.Mods private partial class FocusGrabbingContainer : InputBlockingContainer { - public IBindable Expanded { get; } = new BindableBool(); - public IBindable ExpandedByHovering { get; } = new BindableBool(); + public readonly IBindable ExpandedState = new Bindable(ModCustomisationPanelState.Collapsed); - public override bool RequestsFocus => Expanded.Value; - public override bool AcceptsFocus => Expanded.Value; + public override bool RequestsFocus => panel.Expanded; + public override bool AcceptsFocus => panel.Expanded; private readonly ModCustomisationPanel panel; @@ -238,11 +230,21 @@ namespace osu.Game.Overlays.Mods protected override void OnHoverLost(HoverLostEvent e) { - if (ExpandedByHovering.Value && !ReceivePositionalInputAt(e.ScreenSpaceMousePosition)) - panel.UpdateHoverExpansion(false); + if (ExpandedState.Value is ModCustomisationPanelState.ExpandedByHover + && !ReceivePositionalInputAt(e.ScreenSpaceMousePosition)) + { + panel.UpdateHoverExpansion(ModCustomisationPanelState.Collapsed); + } base.OnHoverLost(e); } } + + public enum ModCustomisationPanelState + { + Collapsed = 0, + ExpandedByHover = 1, + Expanded = 2, + } } } diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index 7469590895..109d81f779 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -237,7 +237,7 @@ namespace osu.Game.Overlays.Mods ActiveMods.Value = ComputeActiveMods(); }, true); - customisationPanel.Expanded.BindValueChanged(_ => updateCustomisationVisualState(), true); + customisationPanel.ExpandedState.BindValueChanged(_ => updateCustomisationVisualState(), true); SearchTextBox.Current.BindValueChanged(query => { @@ -368,18 +368,18 @@ namespace osu.Game.Overlays.Mods customisationPanel.Enabled.Value = true; if (anyModPendingConfiguration) - customisationPanel.Expanded.Value = true; + customisationPanel.Expanded = true; } else { - customisationPanel.Expanded.Value = false; + customisationPanel.Expanded = false; customisationPanel.Enabled.Value = false; } } private void updateCustomisationVisualState() { - if (customisationPanel.Expanded.Value) + if (customisationPanel.Expanded) { columnScroll.FadeColour(OsuColour.Gray(0.5f), 400, Easing.OutQuint); SearchTextBox.FadeColour(OsuColour.Gray(0.5f), 400, Easing.OutQuint); @@ -544,7 +544,7 @@ namespace osu.Game.Overlays.Mods nonFilteredColumnCount += 1; } - customisationPanel.Expanded.Value = false; + customisationPanel.Expanded = false; } #endregion @@ -571,7 +571,7 @@ namespace osu.Game.Overlays.Mods // wherein activating the binding will both change the contents of the search text box and deselect all mods. case GlobalAction.DeselectAllMods: { - if (!SearchTextBox.HasFocus && !customisationPanel.Expanded.Value) + if (!SearchTextBox.HasFocus && !customisationPanel.Expanded) { DisplayedFooterContent?.DeselectAllModsButton?.TriggerClick(); return true; @@ -637,7 +637,7 @@ namespace osu.Game.Overlays.Mods if (e.Repeat || e.Key != Key.Tab) return false; - if (customisationPanel.Expanded.Value) + if (customisationPanel.Expanded) return true; // TODO: should probably eventually support typical platform search shortcuts (`Ctrl-F`, `/`) From b883ff6c7be16f2ef6d6bd8456fda38da45d5ba7 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Thu, 1 Aug 2024 18:18:00 -0700 Subject: [PATCH 1757/2493] Fix click sounds playing twice on `OsuRearrangeableListItem` --- osu.Game/Graphics/Containers/OsuRearrangeableListItem.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Graphics/Containers/OsuRearrangeableListItem.cs b/osu.Game/Graphics/Containers/OsuRearrangeableListItem.cs index 39a3edb82c..445588d525 100644 --- a/osu.Game/Graphics/Containers/OsuRearrangeableListItem.cs +++ b/osu.Game/Graphics/Containers/OsuRearrangeableListItem.cs @@ -64,6 +64,7 @@ namespace osu.Game.Graphics.Containers { InternalChildren = new Drawable[] { + new HoverClickSounds(), new GridContainer { RelativeSizeAxes = Axes.X, @@ -92,7 +93,6 @@ namespace osu.Game.Graphics.Containers ColumnDimensions = new[] { new Dimension(GridSizeMode.AutoSize) }, RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize) } }, - new HoverClickSounds() }; } From 0fac8148ed987bcc9f4b3340719bdd77fe57b110 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Thu, 1 Aug 2024 18:30:52 -0700 Subject: [PATCH 1758/2493] Fix collection delete button not having hover click sounds --- .../Collections/DrawableCollectionListItem.cs | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/osu.Game/Collections/DrawableCollectionListItem.cs b/osu.Game/Collections/DrawableCollectionListItem.cs index 596bb5d673..3b7649a30c 100644 --- a/osu.Game/Collections/DrawableCollectionListItem.cs +++ b/osu.Game/Collections/DrawableCollectionListItem.cs @@ -132,7 +132,7 @@ namespace osu.Game.Collections } } - public partial class DeleteButton : CompositeDrawable + public partial class DeleteButton : OsuClickableContainer { public Func IsTextBoxHovered = null!; @@ -155,7 +155,7 @@ namespace osu.Game.Collections [BackgroundDependencyLoader] private void load(OsuColour colours) { - InternalChild = fadeContainer = new Container + Child = fadeContainer = new Container { RelativeSizeAxes = Axes.Both, Alpha = 0.1f, @@ -176,6 +176,14 @@ namespace osu.Game.Collections } } }; + + Action = () => + { + if (collection.PerformRead(c => c.BeatmapMD5Hashes.Count) == 0) + deleteCollection(); + else + dialogOverlay?.Push(new DeleteCollectionDialog(collection, deleteCollection)); + }; } public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => base.ReceivePositionalInputAt(screenSpacePos) && !IsTextBoxHovered(screenSpacePos); @@ -195,12 +203,7 @@ namespace osu.Game.Collections { background.FlashColour(Color4.White, 150); - if (collection.PerformRead(c => c.BeatmapMD5Hashes.Count) == 0) - deleteCollection(); - else - dialogOverlay?.Push(new DeleteCollectionDialog(collection, deleteCollection)); - - return true; + return base.OnClick(e); } private void deleteCollection() => collection.PerformWrite(c => c.Realm!.Remove(c)); From 1e38d1fa57c1fc37791ce7a58f9f1c1de05e6162 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Thu, 1 Aug 2024 18:45:47 -0700 Subject: [PATCH 1759/2493] Apply corner radius at a higher level so hover click sounds account for it --- osu.Game/Collections/DrawableCollectionListItem.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/osu.Game/Collections/DrawableCollectionListItem.cs b/osu.Game/Collections/DrawableCollectionListItem.cs index 3b7649a30c..e71368c079 100644 --- a/osu.Game/Collections/DrawableCollectionListItem.cs +++ b/osu.Game/Collections/DrawableCollectionListItem.cs @@ -43,6 +43,9 @@ namespace osu.Game.Collections // // if we want to support user sorting (but changes will need to be made to realm to persist). ShowDragHandle.Value = false; + + Masking = true; + CornerRadius = item_height / 2; } protected override Drawable CreateContent() => new ItemContent(Model); @@ -50,7 +53,7 @@ namespace osu.Game.Collections /// /// The main content of the . /// - private partial class ItemContent : CircularContainer + private partial class ItemContent : CompositeDrawable { private readonly Live collection; @@ -65,13 +68,12 @@ namespace osu.Game.Collections RelativeSizeAxes = Axes.X; Height = item_height; - Masking = true; } [BackgroundDependencyLoader] private void load() { - Children = new[] + InternalChildren = new[] { collection.IsManaged ? new DeleteButton(collection) From 894c6150c8b180e380a5e7d541ef0c306bf05dc3 Mon Sep 17 00:00:00 2001 From: jkh675 Date: Fri, 2 Aug 2024 11:59:28 +0800 Subject: [PATCH 1760/2493] Revert "Update resources" This reverts commit cbfb569ad47d17a6de63dd6484d3c9a15cd2d452. --- 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 3d8b643279..e7d9d4c022 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -36,7 +36,7 @@ - + From 5de37f9cd5fe4ba2784dd233af85044804b3f5b9 Mon Sep 17 00:00:00 2001 From: jkh675 Date: Fri, 2 Aug 2024 12:02:28 +0800 Subject: [PATCH 1761/2493] Revert changes --- .../Overlays/Comments/CommentAuthorLine.cs | 61 +------------------ osu.Game/osu.Game.csproj | 2 +- 2 files changed, 4 insertions(+), 59 deletions(-) diff --git a/osu.Game/Overlays/Comments/CommentAuthorLine.cs b/osu.Game/Overlays/Comments/CommentAuthorLine.cs index fa58ce06fe..1f6fef4df3 100644 --- a/osu.Game/Overlays/Comments/CommentAuthorLine.cs +++ b/osu.Game/Overlays/Comments/CommentAuthorLine.cs @@ -139,13 +139,11 @@ namespace osu.Game.Overlays.Comments } } - private partial class ParentUsername : FillFlowContainer, IHasCustomTooltip + private partial class ParentUsername : FillFlowContainer, IHasTooltip { - public ITooltip GetCustomTooltip() => new CommentTooltip(); + public LocalisableString TooltipText => getParentMessage(); - LocalisableString IHasCustomTooltip.TooltipContent => getParentMessage(); - - private Comment? parentComment { get; } + private readonly Comment? parentComment; public ParentUsername(Comment comment) { @@ -178,58 +176,5 @@ namespace osu.Game.Overlays.Comments return parentComment.HasMessage ? parentComment.Message : parentComment.IsDeleted ? CommentsStrings.Deleted : string.Empty; } } - - private partial class CommentTooltip : VisibilityContainer, ITooltip - { - private const int max_width = 500; - - private TextFlowContainer content { get; set; } = null!; - - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { - AutoSizeAxes = Axes.Both; - - Masking = true; - CornerRadius = 7; - - Children = new Drawable[] - { - new Box - { - Colour = colours.Gray3, - RelativeSizeAxes = Axes.Both - }, - content = new TextFlowContainer(f => - { - f.Font = OsuFont.Default; - f.Truncate = true; - f.MaxWidth = max_width; - }) - { - Margin = new MarginPadding(3), - AutoSizeAxes = Axes.Both, - MaximumSize = new Vector2(max_width, float.PositiveInfinity), - } - }; - } - - private LocalisableString lastPresent; - - public void SetContent(LocalisableString content) - { - if (lastPresent.Equals(content)) - return; - - this.content.Text = content; - lastPresent = content; - } - - public void Move(Vector2 pos) => Position = pos; - - protected override void PopIn() => this.FadeIn(200, Easing.OutQuint); - - protected override void PopOut() => this.FadeOut(200, Easing.OutQuint); - } } } diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index e7d9d4c022..3d8b643279 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -36,7 +36,7 @@ - + From f6ca4b233913916d4ca19a7efee3b61345b90172 Mon Sep 17 00:00:00 2001 From: jkh675 Date: Fri, 2 Aug 2024 12:16:50 +0800 Subject: [PATCH 1762/2493] Replace the `OsuSpriteText` with `TextFlowContainer` in `OsuTooltip` and limit the max width --- .../Graphics/Cursor/OsuTooltipContainer.cs | 25 ++++++++++++++----- 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/osu.Game/Graphics/Cursor/OsuTooltipContainer.cs b/osu.Game/Graphics/Cursor/OsuTooltipContainer.cs index aab5b3ee36..cc95a5bd2b 100644 --- a/osu.Game/Graphics/Cursor/OsuTooltipContainer.cs +++ b/osu.Game/Graphics/Cursor/OsuTooltipContainer.cs @@ -10,7 +10,7 @@ using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Shapes; using osu.Framework.Localisation; -using osu.Game.Graphics.Sprites; +using osu.Framework.Graphics.Containers; namespace osu.Game.Graphics.Cursor { @@ -27,13 +27,18 @@ namespace osu.Game.Graphics.Cursor public partial class OsuTooltip : Tooltip { + private const float max_width = 1024; + private readonly Box background; - private readonly OsuSpriteText text; + private readonly TextFlowContainer text; private bool instantMovement = true; + private LocalisableString lastPresent; + public override void SetContent(LocalisableString contentString) { - if (contentString == text.Text) return; + if (contentString.Equals(lastPresent)) + return; text.Text = contentString; @@ -44,6 +49,8 @@ namespace osu.Game.Graphics.Cursor } else AutoSizeDuration = 0; + + lastPresent = contentString; } public OsuTooltip() @@ -65,10 +72,16 @@ namespace osu.Game.Graphics.Cursor RelativeSizeAxes = Axes.Both, Alpha = 0.9f, }, - text = new OsuSpriteText + text = new TextFlowContainer(f => { - Padding = new MarginPadding(5), - Font = OsuFont.GetFont(weight: FontWeight.Regular) + f.Font = OsuFont.GetFont(weight: FontWeight.Regular); + f.Truncate = true; + f.MaxWidth = max_width; + }) + { + Margin = new MarginPadding(5), + AutoSizeAxes = Axes.Both, + MaximumSize = new Vector2(max_width, float.PositiveInfinity), } }; } From 3c1907ced3b084593fb3bc6e3796b811aa717dda Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 2 Aug 2024 14:48:49 +0900 Subject: [PATCH 1763/2493] Update `LocalisationAnalyser` to latest version --- .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 ace7db82f8..c4ba6e5143 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -21,7 +21,7 @@ ] }, "ppy.localisationanalyser.tools": { - "version": "2024.517.0", + "version": "2024.802.0", "commands": [ "localisation" ] diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 3d8b643279..c25f16f1b0 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -30,7 +30,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive From d9c965c47b4cc2291403ca5fbe7b4619c3541eff Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 2 Aug 2024 15:27:21 +0900 Subject: [PATCH 1764/2493] 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 e7d9d4c022..2ac0864266 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -36,7 +36,7 @@ - + From d76fc34cf8e9f96890ebc3c2bc3e0ea2c224702e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 2 Aug 2024 15:28:54 +0900 Subject: [PATCH 1765/2493] Update to use localiastions --- .../Components/DailyChallengeStreakDisplay.cs | 10 +++--- .../Components/DailyChallengeStreakTooltip.cs | 31 +++++++------------ 2 files changed, 16 insertions(+), 25 deletions(-) diff --git a/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStreakDisplay.cs b/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStreakDisplay.cs index da0e334a4e..289e820e4a 100644 --- a/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStreakDisplay.cs +++ b/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStreakDisplay.cs @@ -3,6 +3,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; @@ -10,6 +11,7 @@ using osu.Framework.Graphics.Shapes; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; +using osu.Game.Resources.Localisation.Web; namespace osu.Game.Overlays.Profile.Header.Components { @@ -50,8 +52,7 @@ namespace osu.Game.Overlays.Profile.Header.Components new OsuTextFlowContainer(s => s.Font = OsuFont.GetFont(size: 12)) { AutoSizeAxes = Axes.Both, - // Text = UsersStrings.ShowDailyChallengeTitle - Text = "Daily\nChallenge", + Text = UsersStrings.ShowDailyChallengeTitle, Margin = new MarginPadding { Horizontal = 5f, Bottom = 2f }, }, new Container @@ -97,9 +98,8 @@ namespace osu.Game.Overlays.Profile.Header.Components } var statistics = User.Value.User.DailyChallengeStatistics; - // dailyStreak.Text = UsersStrings.ShowDailyChallengeUnitDay(statistics.PlayCount); - dailyStreak.Text = $"{statistics.PlayCount}d"; - TooltipContent = new DailyChallengeStreakTooltipData(colourProvider, statistics); + dailyStreak.Text = UsersStrings.ShowDailyChallengeUnitDay(statistics.PlayCount.ToLocalisableString("N0")); + TooltipContent = new DailyChallengeTooltipData(colourProvider, statistics); Show(); } diff --git a/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStreakTooltip.cs b/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStreakTooltip.cs index a105659ac7..d33c7d9504 100644 --- a/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStreakTooltip.cs +++ b/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStreakTooltip.cs @@ -13,6 +13,7 @@ using osu.Framework.Localisation; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Online.API.Requests.Responses; +using osu.Game.Resources.Localisation.Web; using osu.Game.Scoring; using osuTK; using Box = osu.Framework.Graphics.Shapes.Box; @@ -78,10 +79,8 @@ namespace osu.Game.Overlays.Profile.Header.Components Spacing = new Vector2(30f), Children = new[] { - // currentDaily = new StreakPiece(UsersStrings.ShowDailyChallengeDailyStreakCurrent), - // currentWeekly = new StreakPiece(UsersStrings.ShowDailyChallengeWeeklyStreakCurrent), - currentDaily = new StreakPiece("Current Daily Streak"), - currentWeekly = new StreakPiece("Current Weekly Streak"), + currentDaily = new StreakPiece(UsersStrings.ShowDailyChallengeDailyStreakCurrent), + currentWeekly = new StreakPiece(UsersStrings.ShowDailyChallengeWeeklyStreakCurrent), } }, } @@ -94,14 +93,10 @@ namespace osu.Game.Overlays.Profile.Header.Components Spacing = new Vector2(10f), Children = new[] { - // bestDaily = new StatisticsPiece(UsersStrings.ShowDailyChallengeDailyStreakBest), - // bestWeekly = new StatisticsPiece(UsersStrings.ShowDailyChallengeWeeklyStreakBest), - // topTen = new StatisticsPiece(UsersStrings.ShowDailyChallengeTop10pPlacements), - // topFifty = new StatisticsPiece(UsersStrings.ShowDailyChallengeTop50pPlacements), - bestDaily = new StatisticsPiece("Best Daily Streak"), - bestWeekly = new StatisticsPiece("Best Weekly Streak"), - topTen = new StatisticsPiece("Top 10% Placements"), - topFifty = new StatisticsPiece("Top 50% Placements"), + bestDaily = new StatisticsPiece(UsersStrings.ShowDailyChallengeDailyStreakBest), + bestWeekly = new StatisticsPiece(UsersStrings.ShowDailyChallengeWeeklyStreakBest), + topTen = new StatisticsPiece(UsersStrings.ShowDailyChallengeTop10pPlacements), + topFifty = new StatisticsPiece(UsersStrings.ShowDailyChallengeTop50pPlacements), } }, } @@ -117,20 +112,16 @@ namespace osu.Game.Overlays.Profile.Header.Components background.Colour = colourProvider.Background4; topBackground.Colour = colourProvider.Background5; - // currentDaily.Value = UsersStrings.ShowDailyChallengeUnitDay(content.DailyStreakCurrent.ToLocalisableString(@"N0")); - currentDaily.Value = $"{statistics.DailyStreakCurrent:N0}d"; + currentDaily.Value = UsersStrings.ShowDailyChallengeUnitDay(content.Statistics.DailyStreakCurrent.ToLocalisableString(@"N0")); currentDaily.ValueColour = colours.ForRankingTier(TierForDaily(statistics.DailyStreakCurrent)); - // currentWeekly.Value = UsersStrings.ShowDailyChallengeUnitWeek(statistics.WeeklyStreakCurrent.ToLocalisableString(@"N0")); - currentWeekly.Value = $"{statistics.WeeklyStreakCurrent:N0}w"; + currentWeekly.Value = UsersStrings.ShowDailyChallengeUnitWeek(statistics.WeeklyStreakCurrent.ToLocalisableString(@"N0")); currentWeekly.ValueColour = colours.ForRankingTier(TierForWeekly(statistics.WeeklyStreakCurrent)); - // bestDaily.Value = UsersStrings.ShowDailyChallengeUnitDay(statistics.DailyStreakBest.ToLocalisableString(@"N0")); - bestDaily.Value = $"{statistics.DailyStreakBest:N0}d"; + bestDaily.Value = UsersStrings.ShowDailyChallengeUnitDay(statistics.DailyStreakBest.ToLocalisableString(@"N0")); bestDaily.ValueColour = colours.ForRankingTier(TierForDaily(statistics.DailyStreakBest)); - // bestWeekly.Value = UsersStrings.ShowDailyChallengeUnitWeek(statistics.WeeklyStreakBest.ToLocalisableString(@"N0")); - bestWeekly.Value = $"{statistics.WeeklyStreakBest:N0}w"; + bestWeekly.Value = UsersStrings.ShowDailyChallengeUnitWeek(statistics.WeeklyStreakBest.ToLocalisableString(@"N0")); bestWeekly.ValueColour = colours.ForRankingTier(TierForWeekly(statistics.WeeklyStreakBest)); topTen.Value = statistics.Top10PercentPlacements.ToLocalisableString(@"N0"); From 816dee181ab34412940259e89d42eb9cc77230a7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 2 Aug 2024 15:31:22 +0900 Subject: [PATCH 1766/2493] Rename classes to remove "streak" terminology Since the primary display isn't showing a streak. --- .../Visual/Online/TestSceneUserProfileDailyChallenge.cs | 4 ++-- ...llengeStreakDisplay.cs => DailyChallengeStatsDisplay.cs} | 6 +++--- ...llengeStreakTooltip.cs => DailyChallengeStatsTooltip.cs} | 6 +++--- osu.Game/Overlays/Profile/Header/Components/MainDetails.cs | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) rename osu.Game/Overlays/Profile/Header/Components/{DailyChallengeStreakDisplay.cs => DailyChallengeStatsDisplay.cs} (92%) rename osu.Game/Overlays/Profile/Header/Components/{DailyChallengeStreakTooltip.cs => DailyChallengeStatsTooltip.cs} (96%) diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserProfileDailyChallenge.cs b/osu.Game.Tests/Visual/Online/TestSceneUserProfileDailyChallenge.cs index c0fb7b49f0..f2135ec992 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneUserProfileDailyChallenge.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneUserProfileDailyChallenge.cs @@ -27,7 +27,7 @@ namespace osu.Game.Tests.Visual.Online { base.LoadComplete(); - DailyChallengeStreakDisplay display = null!; + DailyChallengeStatsDisplay display = null!; AddSliderStep("daily", 0, 999, 2, v => update(s => s.DailyStreakCurrent = v)); AddSliderStep("daily best", 0, 999, 2, v => update(s => s.DailyStreakBest = v)); @@ -44,7 +44,7 @@ namespace osu.Game.Tests.Visual.Online RelativeSizeAxes = Axes.Both, Colour = colourProvider.Background2, }); - Add(display = new DailyChallengeStreakDisplay + Add(display = new DailyChallengeStatsDisplay { Anchor = Anchor.Centre, Origin = Anchor.Centre, diff --git a/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStreakDisplay.cs b/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsDisplay.cs similarity index 92% rename from osu.Game/Overlays/Profile/Header/Components/DailyChallengeStreakDisplay.cs rename to osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsDisplay.cs index 289e820e4a..e154909139 100644 --- a/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStreakDisplay.cs +++ b/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsDisplay.cs @@ -15,11 +15,11 @@ using osu.Game.Resources.Localisation.Web; namespace osu.Game.Overlays.Profile.Header.Components { - public partial class DailyChallengeStreakDisplay : CompositeDrawable, IHasCustomTooltip + public partial class DailyChallengeStatsDisplay : CompositeDrawable, IHasCustomTooltip { public readonly Bindable User = new Bindable(); - public DailyChallengeStreakTooltipData? TooltipContent { get; private set; } + public DailyChallengeTooltipData? TooltipContent { get; private set; } private OsuSpriteText dailyStreak = null!; @@ -103,6 +103,6 @@ namespace osu.Game.Overlays.Profile.Header.Components Show(); } - public ITooltip GetCustomTooltip() => new DailyChallengeStreakTooltip(); + public ITooltip GetCustomTooltip() => new DailyChallengeStatsTooltip(); } } diff --git a/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStreakTooltip.cs b/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsTooltip.cs similarity index 96% rename from osu.Game/Overlays/Profile/Header/Components/DailyChallengeStreakTooltip.cs rename to osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsTooltip.cs index d33c7d9504..1b54633b8a 100644 --- a/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStreakTooltip.cs +++ b/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsTooltip.cs @@ -21,7 +21,7 @@ using Color4 = osuTK.Graphics.Color4; namespace osu.Game.Overlays.Profile.Header.Components { - public partial class DailyChallengeStreakTooltip : VisibilityContainer, ITooltip + public partial class DailyChallengeStatsTooltip : VisibilityContainer, ITooltip { private StreakPiece currentDaily = null!; private StreakPiece currentWeekly = null!; @@ -104,7 +104,7 @@ namespace osu.Game.Overlays.Profile.Header.Components }; } - public void SetContent(DailyChallengeStreakTooltipData content) + public void SetContent(DailyChallengeTooltipData content) { var statistics = content.Statistics; var colourProvider = content.ColourProvider; @@ -237,5 +237,5 @@ namespace osu.Game.Overlays.Profile.Header.Components } } - public record DailyChallengeStreakTooltipData(OverlayColourProvider ColourProvider, APIUserDailyChallengeStatistics Statistics); + public record DailyChallengeTooltipData(OverlayColourProvider ColourProvider, APIUserDailyChallengeStatistics Statistics); } diff --git a/osu.Game/Overlays/Profile/Header/Components/MainDetails.cs b/osu.Game/Overlays/Profile/Header/Components/MainDetails.cs index f9a4267ed9..3d97082230 100644 --- a/osu.Game/Overlays/Profile/Header/Components/MainDetails.cs +++ b/osu.Game/Overlays/Profile/Header/Components/MainDetails.cs @@ -72,7 +72,7 @@ namespace osu.Game.Overlays.Profile.Header.Components { Title = UsersStrings.ShowRankCountrySimple, }, - new DailyChallengeStreakDisplay + new DailyChallengeStatsDisplay { Anchor = Anchor.TopRight, Origin = Anchor.TopRight, From 729039406bbd32102a2706f207a0c1fcbeba0f12 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 2 Aug 2024 15:37:22 +0900 Subject: [PATCH 1767/2493] Add colouring for play count Matches https://github.com/ppy/osu-web/pull/11381. --- .../Components/DailyChallengeStatsDisplay.cs | 21 ++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsDisplay.cs b/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsDisplay.cs index e154909139..50c9b6e1f9 100644 --- a/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsDisplay.cs +++ b/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsDisplay.cs @@ -11,7 +11,9 @@ using osu.Framework.Graphics.Shapes; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; +using osu.Game.Online.API.Requests.Responses; using osu.Game.Resources.Localisation.Web; +using osu.Game.Scoring; namespace osu.Game.Overlays.Profile.Header.Components { @@ -21,7 +23,10 @@ namespace osu.Game.Overlays.Profile.Header.Components public DailyChallengeTooltipData? TooltipContent { get; private set; } - private OsuSpriteText dailyStreak = null!; + private OsuSpriteText dailyPlayCount = null!; + + [Resolved] + private OsuColour colours { get; set; } = null!; [Resolved] private OverlayColourProvider colourProvider { get; set; } = null!; @@ -68,7 +73,7 @@ namespace osu.Game.Overlays.Profile.Header.Components RelativeSizeAxes = Axes.Both, Colour = colourProvider.Background6, }, - dailyStreak = new OsuSpriteText + dailyPlayCount = new OsuSpriteText { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -97,10 +102,16 @@ namespace osu.Game.Overlays.Profile.Header.Components return; } - var statistics = User.Value.User.DailyChallengeStatistics; - dailyStreak.Text = UsersStrings.ShowDailyChallengeUnitDay(statistics.PlayCount.ToLocalisableString("N0")); - TooltipContent = new DailyChallengeTooltipData(colourProvider, statistics); + APIUserDailyChallengeStatistics stats = User.Value.User.DailyChallengeStatistics; + + dailyPlayCount.Text = UsersStrings.ShowDailyChallengeUnitDay(stats.PlayCount.ToLocalisableString("N0")); + dailyPlayCount.Colour = colours.ForRankingTier(tierForPlayCount(stats.PlayCount)); + + TooltipContent = new DailyChallengeTooltipData(colourProvider, stats); + Show(); + + static RankingTier tierForPlayCount(int playCount) => DailyChallengeStatsTooltip.TierForDaily(playCount / 3); } public ITooltip GetCustomTooltip() => new DailyChallengeStatsTooltip(); From c3b2d81066d9885e263dc150158aed78cfcefb46 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 2 Aug 2024 09:23:25 +0300 Subject: [PATCH 1768/2493] Add failing test case --- .../Gameplay/TestScenePauseInputHandling.cs | 107 +++++++++++++----- 1 file changed, 78 insertions(+), 29 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePauseInputHandling.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePauseInputHandling.cs index d778f2e991..a6e062242c 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePauseInputHandling.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePauseInputHandling.cs @@ -6,6 +6,7 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Audio; +using osu.Framework.Graphics; using osu.Framework.Testing; using osu.Framework.Timing; using osu.Game.Beatmaps; @@ -13,10 +14,12 @@ using osu.Game.Configuration; using osu.Game.Rulesets; using osu.Game.Rulesets.Mania; using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.UI; using osu.Game.Screens.Play.HUD; using osu.Game.Skinning; using osu.Game.Storyboards; + using osuTK.Input; namespace osu.Game.Tests.Visual.Gameplay @@ -32,6 +35,23 @@ namespace osu.Game.Tests.Visual.Gameplay [Resolved] private AudioManager audioManager { get; set; } = null!; + protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new Beatmap + { + HitObjects = + { + new HitCircle + { + Position = OsuPlayfield.BASE_SIZE / 2, + StartTime = 0, + }, + new HitCircle + { + Position = OsuPlayfield.BASE_SIZE / 2, + StartTime = 5000, + } + } + }; + protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard? storyboard = null) => new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audioManager); @@ -70,18 +90,16 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("resume", () => Player.Resume()); AddStep("go to resume cursor", () => InputManager.MoveMouseTo(this.ChildrenOfType().Single())); AddStep("press Z to resume", () => InputManager.PressKey(Key.Z)); - - // Z key was released before pause, resuming should not trigger it - checkKey(() => counter, 1, false); - - AddStep("release Z", () => InputManager.ReleaseKey(Key.Z)); - checkKey(() => counter, 1, false); - - AddStep("press Z", () => InputManager.PressKey(Key.Z)); checkKey(() => counter, 2, true); AddStep("release Z", () => InputManager.ReleaseKey(Key.Z)); checkKey(() => counter, 2, false); + + AddStep("press Z", () => InputManager.PressKey(Key.Z)); + checkKey(() => counter, 3, true); + + AddStep("release Z", () => InputManager.ReleaseKey(Key.Z)); + checkKey(() => counter, 3, false); } [Test] @@ -90,30 +108,29 @@ namespace osu.Game.Tests.Visual.Gameplay KeyCounter counter = null!; loadPlayer(() => new ManiaRuleset()); - AddStep("get key counter", () => counter = this.ChildrenOfType().Single(k => k.Trigger is KeyCounterActionTrigger actionTrigger && actionTrigger.Action == ManiaAction.Key1)); + AddStep("get key counter", () => counter = this.ChildrenOfType().Single(k => k.Trigger is KeyCounterActionTrigger actionTrigger && actionTrigger.Action == ManiaAction.Special1)); checkKey(() => counter, 0, false); - AddStep("press D", () => InputManager.PressKey(Key.D)); + AddStep("press space", () => InputManager.PressKey(Key.Space)); checkKey(() => counter, 1, true); - AddStep("release D", () => InputManager.ReleaseKey(Key.D)); + AddStep("release space", () => InputManager.ReleaseKey(Key.Space)); checkKey(() => counter, 1, false); AddStep("pause", () => Player.Pause()); - AddStep("press D", () => InputManager.PressKey(Key.D)); + AddStep("press space", () => InputManager.PressKey(Key.Space)); checkKey(() => counter, 1, false); - AddStep("release D", () => InputManager.ReleaseKey(Key.D)); + AddStep("release space", () => InputManager.ReleaseKey(Key.Space)); checkKey(() => counter, 1, false); AddStep("resume", () => Player.Resume()); AddUntilStep("wait for resume", () => Player.GameplayClockContainer.IsRunning); - checkKey(() => counter, 1, false); - AddStep("press D", () => InputManager.PressKey(Key.D)); + AddStep("press space", () => InputManager.PressKey(Key.Space)); checkKey(() => counter, 2, true); - AddStep("release D", () => InputManager.ReleaseKey(Key.D)); + AddStep("release space", () => InputManager.ReleaseKey(Key.Space)); checkKey(() => counter, 2, false); } @@ -145,8 +162,11 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("resume", () => Player.Resume()); AddStep("go to resume cursor", () => InputManager.MoveMouseTo(this.ChildrenOfType().Single())); AddStep("press Z to resume", () => InputManager.PressKey(Key.Z)); - checkKey(() => counterZ, 1, false); + checkKey(() => counterZ, 2, true); checkKey(() => counterX, 1, false); + + AddStep("release Z", () => InputManager.ReleaseKey(Key.Z)); + checkKey(() => counterZ, 2, false); } [Test] @@ -155,12 +175,12 @@ namespace osu.Game.Tests.Visual.Gameplay KeyCounter counter = null!; loadPlayer(() => new ManiaRuleset()); - AddStep("get key counter", () => counter = this.ChildrenOfType().Single(k => k.Trigger is KeyCounterActionTrigger actionTrigger && actionTrigger.Action == ManiaAction.Key1)); + AddStep("get key counter", () => counter = this.ChildrenOfType().Single(k => k.Trigger is KeyCounterActionTrigger actionTrigger && actionTrigger.Action == ManiaAction.Special1)); - AddStep("press D", () => InputManager.PressKey(Key.D)); + AddStep("press space", () => InputManager.PressKey(Key.Space)); AddStep("pause", () => Player.Pause()); - AddStep("release D", () => InputManager.ReleaseKey(Key.D)); + AddStep("release space", () => InputManager.ReleaseKey(Key.Space)); checkKey(() => counter, 1, true); AddStep("resume", () => Player.Resume()); @@ -202,12 +222,14 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("resume", () => Player.Resume()); AddStep("go to resume cursor", () => InputManager.MoveMouseTo(this.ChildrenOfType().Single())); AddStep("press Z to resume", () => InputManager.PressKey(Key.Z)); - checkKey(() => counterZ, 1, false); + checkKey(() => counterZ, 2, true); checkKey(() => counterX, 1, true); AddStep("release X", () => InputManager.ReleaseKey(Key.X)); - checkKey(() => counterZ, 1, false); checkKey(() => counterX, 1, false); + + AddStep("release Z", () => InputManager.ReleaseKey(Key.Z)); + checkKey(() => counterZ, 2, false); } [Test] @@ -216,24 +238,50 @@ namespace osu.Game.Tests.Visual.Gameplay KeyCounter counter = null!; loadPlayer(() => new ManiaRuleset()); - AddStep("get key counter", () => counter = this.ChildrenOfType().Single(k => k.Trigger is KeyCounterActionTrigger actionTrigger && actionTrigger.Action == ManiaAction.Key1)); + AddStep("get key counter", () => counter = this.ChildrenOfType().Single(k => k.Trigger is KeyCounterActionTrigger actionTrigger && actionTrigger.Action == ManiaAction.Special1)); - AddStep("press D", () => InputManager.PressKey(Key.D)); + AddStep("press space", () => InputManager.PressKey(Key.Space)); checkKey(() => counter, 1, true); AddStep("pause", () => Player.Pause()); - AddStep("release D", () => InputManager.ReleaseKey(Key.D)); - AddStep("press D", () => InputManager.PressKey(Key.D)); + AddStep("release space", () => InputManager.ReleaseKey(Key.Space)); + AddStep("press space", () => InputManager.PressKey(Key.Space)); AddStep("resume", () => Player.Resume()); AddUntilStep("wait for resume", () => Player.GameplayClockContainer.IsRunning); checkKey(() => counter, 1, true); - AddStep("release D", () => InputManager.ReleaseKey(Key.D)); + AddStep("release space", () => InputManager.ReleaseKey(Key.Space)); checkKey(() => counter, 1, false); } + [Test] + public void TestOsuRegisterInputFromPressingOrangeCursorButPressIsBlocked() + { + KeyCounter counter = null!; + + loadPlayer(() => new OsuRuleset()); + AddStep("get key counter", () => counter = this.ChildrenOfType().Single(k => k.Trigger is KeyCounterActionTrigger actionTrigger && actionTrigger.Action == OsuAction.LeftButton)); + + AddStep("pause", () => Player.Pause()); + AddStep("resume", () => Player.Resume()); + AddStep("go to resume cursor", () => InputManager.MoveMouseTo(this.ChildrenOfType().Single())); + AddStep("press Z to resume", () => InputManager.PressKey(Key.Z)); + + // ensure the input manager receives the Z button press... + checkKey(() => counter, 1, true); + AddAssert("button is pressed in kbc", () => Player.DrawableRuleset.Playfield.FindClosestParent()!.PressedActions.Single() == OsuAction.LeftButton); + + // ...but also ensure the hit circle in front of the cursor isn't hit by checking max combo. + AddAssert("circle not hit", () => Player.ScoreProcessor.HighestCombo.Value, () => Is.EqualTo(0)); + + AddStep("release Z", () => InputManager.ReleaseKey(Key.Z)); + + checkKey(() => counter, 1, false); + AddAssert("button is released in kbc", () => !Player.DrawableRuleset.Playfield.FindClosestParent()!.PressedActions.Any()); + } + private void loadPlayer(Func createRuleset) { AddStep("set ruleset", () => currentRuleset = createRuleset()); @@ -241,9 +289,10 @@ namespace osu.Game.Tests.Visual.Gameplay AddUntilStep("player loaded", () => Player.IsLoaded && Player.Alpha == 1); AddUntilStep("wait for hud", () => Player.HUDOverlay.ChildrenOfType().All(s => s.ComponentsLoaded)); - AddStep("seek to gameplay", () => Player.GameplayClockContainer.Seek(20000)); - AddUntilStep("wait for seek to finish", () => Player.DrawableRuleset.FrameStableClock.CurrentTime, () => Is.EqualTo(20000).Within(500)); + AddStep("seek to gameplay", () => Player.GameplayClockContainer.Seek(0)); + AddUntilStep("wait for seek to finish", () => Player.DrawableRuleset.FrameStableClock.CurrentTime, () => Is.EqualTo(0).Within(500)); AddAssert("not in break", () => !Player.IsBreakTime.Value); + AddStep("move cursor to center", () => InputManager.MoveMouseTo(Player.DrawableRuleset.Playfield)); } private void checkKey(Func counter, int count, bool active) From eafc0f79afcbc64fd1289ebbc8772d1192e5a6b7 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 2 Aug 2024 10:21:44 +0300 Subject: [PATCH 1769/2493] Fix clicking resume cursor not triggering a gameplay press in osu! --- osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs b/osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs index d809f2b318..6970e7db1e 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs @@ -115,10 +115,7 @@ namespace osu.Game.Rulesets.Osu.UI return false; scaleTransitionContainer.ScaleTo(2, TRANSITION_TIME, Easing.OutQuint); - - // When resuming with a button, we do not want the osu! input manager to see this button press and include it in the score. - // To ensure that this works correctly, schedule the resume operation one frame forward, since the resume operation enables the input manager to see input events. - Schedule(() => ResumeRequested?.Invoke()); + ResumeRequested?.Invoke(); return true; } From 5368a43633009f2a158a621d34c2a7fce84bd3e5 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 2 Aug 2024 10:22:01 +0300 Subject: [PATCH 1770/2493] Fix clicking resume overlay hitting underlying hit circle --- osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs | 9 ++++ osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs | 46 +++++++++++++++++++- 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs index df7f279656..dbb63a98c2 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs @@ -206,6 +206,15 @@ namespace osu.Game.Rulesets.Osu.UI public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => HitObjectContainer.ReceivePositionalInputAt(screenSpacePos); + private OsuResumeOverlay.OsuResumeOverlayInputBlocker resumeInputBlocker; + + public void AttachResumeOverlayInputBlocker(OsuResumeOverlay.OsuResumeOverlayInputBlocker resumeInputBlocker) + { + Debug.Assert(this.resumeInputBlocker == null); + this.resumeInputBlocker = resumeInputBlocker; + AddInternal(resumeInputBlocker); + } + private partial class ProxyContainer : LifetimeManagementContainer { public void Add(Drawable proxy) => AddInternal(proxy); diff --git a/osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs b/osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs index 6970e7db1e..39a77d0b42 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs @@ -33,9 +33,26 @@ namespace osu.Game.Rulesets.Osu.UI [BackgroundDependencyLoader] private void load() { + OsuResumeOverlayInputBlocker? inputBlocker = null; + + if (drawableRuleset != null) + { + var osuPlayfield = (OsuPlayfield)drawableRuleset.Playfield; + osuPlayfield.AttachResumeOverlayInputBlocker(inputBlocker = new OsuResumeOverlayInputBlocker()); + } + Add(cursorScaleContainer = new Container { - Child = clickToResumeCursor = new OsuClickToResumeCursor { ResumeRequested = Resume } + Child = clickToResumeCursor = new OsuClickToResumeCursor + { + ResumeRequested = () => + { + if (inputBlocker != null) + inputBlocker.BlockNextPress = true; + + Resume(); + } + } }); } @@ -140,5 +157,32 @@ namespace osu.Game.Rulesets.Osu.UI this.FadeColour(IsHovered ? Color4.White : Color4.Orange, 400, Easing.OutQuint); } } + + public partial class OsuResumeOverlayInputBlocker : Drawable, IKeyBindingHandler + { + public bool BlockNextPress; + + public OsuResumeOverlayInputBlocker() + { + RelativeSizeAxes = Axes.Both; + Depth = float.MinValue; + } + + public bool OnPressed(KeyBindingPressEvent e) + { + try + { + return BlockNextPress; + } + finally + { + BlockNextPress = false; + } + } + + public void OnReleased(KeyBindingReleaseEvent e) + { + } + } } } From 76904272e6e153d2f78514f76de76afe08cceabc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 2 Aug 2024 16:56:34 +0900 Subject: [PATCH 1771/2493] Allow horizontal scrolling on mod select overlay anywhere on the screen Closes https://github.com/ppy/osu/issues/29248. --- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index 7469590895..858992b8ba 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -668,6 +668,8 @@ namespace osu.Game.Overlays.Mods [Cached] internal partial class ColumnScrollContainer : OsuScrollContainer { + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; + public ColumnScrollContainer() : base(Direction.Horizontal) { From f5a3eb56120503d3fc4ae23fb4efcc2c6a711b0e Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 2 Aug 2024 11:01:40 +0300 Subject: [PATCH 1772/2493] Add comment --- osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs b/osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs index 39a77d0b42..44a2be0024 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs @@ -47,6 +47,10 @@ namespace osu.Game.Rulesets.Osu.UI { ResumeRequested = () => { + // since the user had to press a button to tap the resume cursor, + // block that press event from potentially reaching a hit circle that's behind the cursor. + // we cannot do this from OsuClickToResumeCursor directly since we're in a different input manager tree than the gameplay one, + // so we rely on a dedicated input blocking component that's implanted in there to do that for us. if (inputBlocker != null) inputBlocker.BlockNextPress = true; From dc9f6a07cb751845bb871f88d21ccb614256d0b2 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 2 Aug 2024 11:16:32 +0300 Subject: [PATCH 1773/2493] Fix inspections --- osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs | 2 +- osu.Game.Tests/Visual/Gameplay/TestScenePauseInputHandling.cs | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs index dbb63a98c2..7d9f5eb1a8 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs @@ -206,7 +206,7 @@ namespace osu.Game.Rulesets.Osu.UI public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => HitObjectContainer.ReceivePositionalInputAt(screenSpacePos); - private OsuResumeOverlay.OsuResumeOverlayInputBlocker resumeInputBlocker; + private OsuResumeOverlay.OsuResumeOverlayInputBlocker? resumeInputBlocker; public void AttachResumeOverlayInputBlocker(OsuResumeOverlay.OsuResumeOverlayInputBlocker resumeInputBlocker) { diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePauseInputHandling.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePauseInputHandling.cs index a6e062242c..2d03d0cb7c 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePauseInputHandling.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePauseInputHandling.cs @@ -19,7 +19,6 @@ using osu.Game.Rulesets.Osu.UI; using osu.Game.Screens.Play.HUD; using osu.Game.Skinning; using osu.Game.Storyboards; - using osuTK.Input; namespace osu.Game.Tests.Visual.Gameplay From 06af8cb9522fae15daead3c3892301fc5fe1cc46 Mon Sep 17 00:00:00 2001 From: Caiyi Shyu Date: Fri, 2 Aug 2024 16:23:37 +0800 Subject: [PATCH 1774/2493] interpolate parts in local space to avoid broken trails --- .../UI/Cursor/CursorTrail.cs | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs b/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs index 5e8061bb6a..f684bcb58f 100644 --- a/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs +++ b/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs @@ -65,6 +65,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor } AddLayout(partSizeCache); + AddLayout(scaleRatioCache); } [BackgroundDependencyLoader] @@ -154,8 +155,16 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor return base.OnMouseMove(e); } + private readonly LayoutValue scaleRatioCache = new LayoutValue(Invalidation.DrawInfo | Invalidation.RequiredParentSizeToFit | Invalidation.Presence); + + private Vector2 scaleRatio => scaleRatioCache.IsValid + ? scaleRatioCache.Value + : (scaleRatioCache.Value = DrawInfo.MatrixInverse.ExtractScale().Xy); + protected void AddTrail(Vector2 position) { + position = ToLocalSpace(position); + if (InterpolateMovements) { if (!lastPosition.HasValue) @@ -174,10 +183,10 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor float distance = diff.Length; Vector2 direction = diff / distance; - float interval = partSize.X / 2.5f * IntervalMultiplier; - float stopAt = distance - (AvoidDrawingNearCursor ? interval : 0); + Vector2 interval = partSize.X / 2.5f * IntervalMultiplier * scaleRatio; + float stopAt = distance - (AvoidDrawingNearCursor ? interval.Length : 0); - for (float d = interval; d < stopAt; d += interval) + for (Vector2 d = interval; d.Length < stopAt; d += interval) { lastPosition = pos1 + direction * d; addPart(lastPosition.Value); @@ -191,9 +200,9 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor } } - private void addPart(Vector2 screenSpacePosition) + private void addPart(Vector2 localSpacePosition) { - parts[currentIndex].Position = ToLocalSpace(screenSpacePosition); + parts[currentIndex].Position = localSpacePosition; parts[currentIndex].Time = time + 1; ++parts[currentIndex].InvalidationID; From 4b5c163d93d912cd730d07ae9e76eeb7554a04e7 Mon Sep 17 00:00:00 2001 From: Caiyi Shyu Date: Fri, 2 Aug 2024 17:45:05 +0800 Subject: [PATCH 1775/2493] remove unnecessary LayoutValue --- .../UI/Cursor/CursorTrail.cs | 22 +++---------------- 1 file changed, 3 insertions(+), 19 deletions(-) diff --git a/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs b/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs index f684bcb58f..6452444fed 100644 --- a/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs +++ b/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs @@ -16,7 +16,6 @@ using osu.Framework.Graphics.Shaders.Types; using osu.Framework.Graphics.Textures; using osu.Framework.Input; using osu.Framework.Input.Events; -using osu.Framework.Layout; using osu.Framework.Timing; using osuTK; using osuTK.Graphics; @@ -63,9 +62,6 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor // -1 signals that the part is unusable, and should not be drawn parts[i].InvalidationID = -1; } - - AddLayout(partSizeCache); - AddLayout(scaleRatioCache); } [BackgroundDependencyLoader] @@ -96,12 +92,6 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor } } - private readonly LayoutValue partSizeCache = new LayoutValue(Invalidation.DrawInfo | Invalidation.RequiredParentSizeToFit | Invalidation.Presence); - - private Vector2 partSize => partSizeCache.IsValid - ? partSizeCache.Value - : (partSizeCache.Value = new Vector2(Texture.DisplayWidth, Texture.DisplayHeight) * DrawInfo.Matrix.ExtractScale().Xy); - /// /// The amount of time to fade the cursor trail pieces. /// @@ -155,12 +145,6 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor return base.OnMouseMove(e); } - private readonly LayoutValue scaleRatioCache = new LayoutValue(Invalidation.DrawInfo | Invalidation.RequiredParentSizeToFit | Invalidation.Presence); - - private Vector2 scaleRatio => scaleRatioCache.IsValid - ? scaleRatioCache.Value - : (scaleRatioCache.Value = DrawInfo.MatrixInverse.ExtractScale().Xy); - protected void AddTrail(Vector2 position) { position = ToLocalSpace(position); @@ -183,10 +167,10 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor float distance = diff.Length; Vector2 direction = diff / distance; - Vector2 interval = partSize.X / 2.5f * IntervalMultiplier * scaleRatio; - float stopAt = distance - (AvoidDrawingNearCursor ? interval.Length : 0); + float interval = Texture.DisplayWidth / 2.5f * IntervalMultiplier; + float stopAt = distance - (AvoidDrawingNearCursor ? interval : 0); - for (Vector2 d = interval; d.Length < stopAt; d += interval) + for (float d = interval; d < stopAt; d += interval) { lastPosition = pos1 + direction * d; addPart(lastPosition.Value); From 64b7bab4fbd6f08d3848d18673c0b78d8b33ab91 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 2 Aug 2024 18:59:21 +0900 Subject: [PATCH 1776/2493] Fix mod panels overflowing into the column borders --- osu.Game/Overlays/Mods/ModSelectColumn.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Overlays/Mods/ModSelectColumn.cs b/osu.Game/Overlays/Mods/ModSelectColumn.cs index 5ffed24e7a..8a499a391c 100644 --- a/osu.Game/Overlays/Mods/ModSelectColumn.cs +++ b/osu.Game/Overlays/Mods/ModSelectColumn.cs @@ -138,6 +138,7 @@ namespace osu.Game.Overlays.Mods }, new GridContainer { + Padding = new MarginPadding { Top = 1, Bottom = 3 }, RelativeSizeAxes = Axes.Both, RowDimensions = new[] { From 8265e7ce31a6025b12341ef2f9b0df70a103d283 Mon Sep 17 00:00:00 2001 From: jkh675 Date: Fri, 2 Aug 2024 19:44:55 +0800 Subject: [PATCH 1777/2493] Reduce the tooltip max width --- osu.Game/Graphics/Cursor/OsuTooltipContainer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Graphics/Cursor/OsuTooltipContainer.cs b/osu.Game/Graphics/Cursor/OsuTooltipContainer.cs index cc95a5bd2b..5c84f5263f 100644 --- a/osu.Game/Graphics/Cursor/OsuTooltipContainer.cs +++ b/osu.Game/Graphics/Cursor/OsuTooltipContainer.cs @@ -27,7 +27,7 @@ namespace osu.Game.Graphics.Cursor public partial class OsuTooltip : Tooltip { - private const float max_width = 1024; + private const float max_width = 500; private readonly Box background; private readonly TextFlowContainer text; From 531cf64ddbbf2e1673f63e38dfe54423a946779f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 2 Aug 2024 13:15:05 +0900 Subject: [PATCH 1778/2493] Add failing test showing date added changing when importing as update with no change --- .../Database/BeatmapImporterUpdateTests.cs | 46 ++++++++++++++++++- 1 file changed, 44 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Database/BeatmapImporterUpdateTests.cs b/osu.Game.Tests/Database/BeatmapImporterUpdateTests.cs index a47da4d505..3f1bc58147 100644 --- a/osu.Game.Tests/Database/BeatmapImporterUpdateTests.cs +++ b/osu.Game.Tests/Database/BeatmapImporterUpdateTests.cs @@ -259,6 +259,44 @@ namespace osu.Game.Tests.Database }); } + [Test] + public void TestNoChangesAfterDelete() + { + RunTestWithRealmAsync(async (realm, storage) => + { + var importer = new BeatmapImporter(storage, realm); + using var rulesets = new RealmRulesetStore(realm, storage); + + using var __ = getBeatmapArchive(out string pathOriginal); + using var _ = getBeatmapArchive(out string pathOriginalSecond); + + var importBeforeUpdate = await importer.Import(new ImportTask(pathOriginal)); + + importBeforeUpdate!.PerformWrite(s => s.DeletePending = true); + + var dateBefore = importBeforeUpdate.Value.DateAdded; + + Assert.That(importBeforeUpdate, Is.Not.Null); + Debug.Assert(importBeforeUpdate != null); + + var importAfterUpdate = await importer.ImportAsUpdate(new ProgressNotification(), new ImportTask(pathOriginalSecond), importBeforeUpdate.Value); + + realm.Run(r => r.Refresh()); + + Assert.That(importAfterUpdate, Is.Not.Null); + Debug.Assert(importAfterUpdate != null); + + checkCount(realm, 1); + checkCount(realm, count_beatmaps); + checkCount(realm, count_beatmaps); + + Assert.That(importBeforeUpdate.Value.Beatmaps.First().OnlineID, Is.GreaterThan(-1)); + Assert.That(importBeforeUpdate.Value.DateAdded, Is.EqualTo(dateBefore)); + Assert.That(importAfterUpdate.Value.DateAdded, Is.EqualTo(dateBefore)); + Assert.That(importBeforeUpdate.ID, Is.EqualTo(importAfterUpdate.ID)); + }); + } + [Test] public void TestNoChanges() { @@ -272,21 +310,25 @@ namespace osu.Game.Tests.Database var importBeforeUpdate = await importer.Import(new ImportTask(pathOriginal)); + var dateBefore = importBeforeUpdate!.Value.DateAdded; + Assert.That(importBeforeUpdate, Is.Not.Null); Debug.Assert(importBeforeUpdate != null); var importAfterUpdate = await importer.ImportAsUpdate(new ProgressNotification(), new ImportTask(pathOriginalSecond), importBeforeUpdate.Value); + realm.Run(r => r.Refresh()); + Assert.That(importAfterUpdate, Is.Not.Null); Debug.Assert(importAfterUpdate != null); - realm.Run(r => r.Refresh()); - checkCount(realm, 1); checkCount(realm, count_beatmaps); checkCount(realm, count_beatmaps); Assert.That(importBeforeUpdate.Value.Beatmaps.First().OnlineID, Is.GreaterThan(-1)); + Assert.That(importBeforeUpdate.Value.DateAdded, Is.EqualTo(dateBefore)); + Assert.That(importAfterUpdate.Value.DateAdded, Is.EqualTo(dateBefore)); Assert.That(importBeforeUpdate.ID, Is.EqualTo(importAfterUpdate.ID)); }); } From dc73856f76fe71404aeb512ae4233be00309185b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 2 Aug 2024 20:45:57 +0900 Subject: [PATCH 1779/2493] Fix original date not being restored when no changes are made on an import-as-update operation --- osu.Game/Beatmaps/BeatmapImporter.cs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapImporter.cs b/osu.Game/Beatmaps/BeatmapImporter.cs index 71aa5b0333..8acaebd1a8 100644 --- a/osu.Game/Beatmaps/BeatmapImporter.cs +++ b/osu.Game/Beatmaps/BeatmapImporter.cs @@ -43,6 +43,8 @@ namespace osu.Game.Beatmaps public override async Task?> ImportAsUpdate(ProgressNotification notification, ImportTask importTask, BeatmapSetInfo original) { + var originalDateAdded = original.DateAdded; + Guid originalId = original.ID; var imported = await Import(notification, new[] { importTask }).ConfigureAwait(false); @@ -57,8 +59,11 @@ namespace osu.Game.Beatmaps // If there were no changes, ensure we don't accidentally nuke ourselves. if (first.ID == originalId) { - first.PerformRead(s => + first.PerformWrite(s => { + // Transfer local values which should be persisted across a beatmap update. + s.DateAdded = originalDateAdded; + // Re-run processing even in this case. We might have outdated metadata. ProcessBeatmap?.Invoke(s, MetadataLookupScope.OnlineFirst); }); @@ -79,7 +84,7 @@ namespace osu.Game.Beatmaps original.DeletePending = true; // Transfer local values which should be persisted across a beatmap update. - updated.DateAdded = original.DateAdded; + updated.DateAdded = originalDateAdded; transferCollectionReferences(realm, original, updated); @@ -278,6 +283,9 @@ namespace osu.Game.Beatmaps protected override void UndeleteForReuse(BeatmapSetInfo existing) { + if (!existing.DeletePending) + return; + base.UndeleteForReuse(existing); existing.DateAdded = DateTimeOffset.UtcNow; } From c27b35ad14fa50e6f35ac6b8188902e3f189a79d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 2 Aug 2024 20:58:52 +0900 Subject: [PATCH 1780/2493] 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 7785cb3c94..3b3385ecfe 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -10,7 +10,7 @@ true - + diff --git a/osu.iOS.props b/osu.iOS.props index dceb88c6f7..196d5594ad 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -23,6 +23,6 @@ iossimulator-x64 - + From a8141bf15fc1801616828c213858f68926dc8868 Mon Sep 17 00:00:00 2001 From: jkh675 Date: Fri, 2 Aug 2024 21:50:24 +0800 Subject: [PATCH 1781/2493] Only wrap by per word --- osu.Game/Graphics/Cursor/OsuTooltipContainer.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game/Graphics/Cursor/OsuTooltipContainer.cs b/osu.Game/Graphics/Cursor/OsuTooltipContainer.cs index 5c84f5263f..fb5122bb93 100644 --- a/osu.Game/Graphics/Cursor/OsuTooltipContainer.cs +++ b/osu.Game/Graphics/Cursor/OsuTooltipContainer.cs @@ -75,8 +75,6 @@ namespace osu.Game.Graphics.Cursor text = new TextFlowContainer(f => { f.Font = OsuFont.GetFont(weight: FontWeight.Regular); - f.Truncate = true; - f.MaxWidth = max_width; }) { Margin = new MarginPadding(5), From 4ef9f335eef73f607ccd6d2e7ab8bb6d3003775e Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Fri, 2 Aug 2024 10:19:59 -0700 Subject: [PATCH 1782/2493] Fix customise button on mod overlay initially showing flash layer indefinitely --- osu.Game/Overlays/Mods/ModCustomisationHeader.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Overlays/Mods/ModCustomisationHeader.cs b/osu.Game/Overlays/Mods/ModCustomisationHeader.cs index fb9e960f41..540ed8ee94 100644 --- a/osu.Game/Overlays/Mods/ModCustomisationHeader.cs +++ b/osu.Game/Overlays/Mods/ModCustomisationHeader.cs @@ -54,6 +54,7 @@ namespace osu.Game.Overlays.Mods RelativeSizeAxes = Axes.Both, Colour = Color4.White.Opacity(0.4f), Blending = BlendingParameters.Additive, + Alpha = 0, }, new OsuSpriteText { From 040f65432ebcd54bc639d505b94990a986564789 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Sat, 3 Aug 2024 19:39:49 +0900 Subject: [PATCH 1783/2493] Rename variables a bit --- osu.Game/Graphics/Cursor/OsuTooltipContainer.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game/Graphics/Cursor/OsuTooltipContainer.cs b/osu.Game/Graphics/Cursor/OsuTooltipContainer.cs index fb5122bb93..0d36cc1d08 100644 --- a/osu.Game/Graphics/Cursor/OsuTooltipContainer.cs +++ b/osu.Game/Graphics/Cursor/OsuTooltipContainer.cs @@ -33,14 +33,14 @@ namespace osu.Game.Graphics.Cursor private readonly TextFlowContainer text; private bool instantMovement = true; - private LocalisableString lastPresent; + private LocalisableString lastContent; - public override void SetContent(LocalisableString contentString) + public override void SetContent(LocalisableString content) { - if (contentString.Equals(lastPresent)) + if (content.Equals(lastContent)) return; - text.Text = contentString; + text.Text = content; if (IsPresent) { @@ -50,7 +50,7 @@ namespace osu.Game.Graphics.Cursor else AutoSizeDuration = 0; - lastPresent = contentString; + lastContent = content; } public OsuTooltip() From d95d63d7ee4a9c2a925fdbfb3cea6a8f642ec7c9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 3 Aug 2024 22:44:51 +0900 Subject: [PATCH 1784/2493] Undo localisation of Daily Challenge string for now --- .../Profile/Header/Components/DailyChallengeStatsDisplay.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsDisplay.cs b/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsDisplay.cs index 50c9b6e1f9..f55eb595d7 100644 --- a/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsDisplay.cs +++ b/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsDisplay.cs @@ -57,7 +57,9 @@ namespace osu.Game.Overlays.Profile.Header.Components new OsuTextFlowContainer(s => s.Font = OsuFont.GetFont(size: 12)) { AutoSizeAxes = Axes.Both, - Text = UsersStrings.ShowDailyChallengeTitle, + // can't use this because osu-web does weird stuff with \\n. + // Text = UsersStrings.ShowDailyChallengeTitle., + Text = "Daily\nChallenge", Margin = new MarginPadding { Horizontal = 5f, Bottom = 2f }, }, new Container From 2daf1b58f2dcdbd0fbf308547d99c55d6f667f2e Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Sat, 3 Aug 2024 14:48:08 -0700 Subject: [PATCH 1785/2493] Allow searching enum descriptions from `SettingsEnumDropdown`s --- osu.Game/Overlays/Settings/SettingsEnumDropdown.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/osu.Game/Overlays/Settings/SettingsEnumDropdown.cs b/osu.Game/Overlays/Settings/SettingsEnumDropdown.cs index cf6bc30f85..2b74557c1a 100644 --- a/osu.Game/Overlays/Settings/SettingsEnumDropdown.cs +++ b/osu.Game/Overlays/Settings/SettingsEnumDropdown.cs @@ -2,7 +2,11 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; +using System.Linq; +using osu.Framework.Extensions; using osu.Framework.Graphics; +using osu.Framework.Localisation; using osu.Game.Graphics.UserInterface; namespace osu.Game.Overlays.Settings @@ -10,6 +14,8 @@ namespace osu.Game.Overlays.Settings public partial class SettingsEnumDropdown : SettingsDropdown where T : struct, Enum { + public override IEnumerable FilterTerms => base.FilterTerms.Concat(Control.Items.Select(i => i.GetLocalisableDescription())); + protected override OsuDropdown CreateDropdown() => new DropdownControl(); protected new partial class DropdownControl : OsuEnumDropdown From 6f9866d542d14f365294579c44df7564ada34337 Mon Sep 17 00:00:00 2001 From: jkh675 Date: Sun, 4 Aug 2024 18:56:19 +0800 Subject: [PATCH 1786/2493] Add unit test for `OsuTooltip` --- .../UserInterface/TestSceneOsuTooltip.cs | 89 +++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 osu.Game.Tests/Visual/UserInterface/TestSceneOsuTooltip.cs diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneOsuTooltip.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneOsuTooltip.cs new file mode 100644 index 0000000000..1a1db8eaf7 --- /dev/null +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneOsuTooltip.cs @@ -0,0 +1,89 @@ +// 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.Graphics.Containers; +using osu.Framework.Graphics.Cursor; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Localisation; +using osu.Framework.Testing; +using osu.Game.Graphics; +using osu.Game.Graphics.Cursor; +using osu.Game.Graphics.Sprites; +using osuTK; + +namespace osu.Game.Tests.Visual.UserInterface +{ + [TestFixture] + public partial class TestSceneOsuTooltip : OsuManualInputManagerTestScene + { + private TestTooltipContainer container = null!; + + [SetUp] + public void SetUp() + { + Child = new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding(100), + Children = new Drawable[] + { + new Box + { + Colour = Colour4.Red.Opacity(0.5f), + RelativeSizeAxes = Axes.Both, + }, + container = new TestTooltipContainer + { + RelativeSizeAxes = Axes.Both, + Child = new OsuSpriteText + { + Text = "Hover me!", + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Font = OsuFont.GetFont(size: 50) + } + }, + }, + }; + } + + private static readonly string[] test_case_tooltip_string = + [ + "Hello!!", + string.Concat(Enumerable.Repeat("Hello ", 100)), + $"H{new string('e', 500)}llo", + ]; + + [Test] + public void TestTooltipBasic([Values(0, 1, 2)] int index) + { + AddStep("Set tooltip content", () => + { + container.TooltipText = test_case_tooltip_string[index]; + }); + + AddStep("Move to container", () => + { + InputManager.MoveMouseTo(new Vector2(InputManager.ScreenSpaceDrawQuad.Centre.X, InputManager.ScreenSpaceDrawQuad.Centre.Y)); + }); + + OsuTooltipContainer.OsuTooltip? tooltip = null!; + + AddUntilStep("Wait for the tooltip shown", () => + { + tooltip = container.FindClosestParent().ChildrenOfType().FirstOrDefault(); + return tooltip != null && tooltip.Alpha == 1; + }); + + AddAssert("Is tooltip obey 500 width limit", () => tooltip != null && tooltip.Width <= 500); + } + + internal sealed partial class TestTooltipContainer : Container, IHasTooltip + { + public LocalisableString TooltipText { get; set; } + } + } +} From de6d8e7eb71f50d8f6708fa4caca1e028b500ec1 Mon Sep 17 00:00:00 2001 From: jkh675 Date: Sun, 4 Aug 2024 19:07:35 +0800 Subject: [PATCH 1787/2493] Add the custom context menu to handle the key event --- .../Components/EditorContextMenuContainer.cs | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 osu.Game/Screens/Edit/Components/EditorContextMenuContainer.cs diff --git a/osu.Game/Screens/Edit/Components/EditorContextMenuContainer.cs b/osu.Game/Screens/Edit/Components/EditorContextMenuContainer.cs new file mode 100644 index 0000000000..3207cb0849 --- /dev/null +++ b/osu.Game/Screens/Edit/Components/EditorContextMenuContainer.cs @@ -0,0 +1,36 @@ +// 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.Input; +using osu.Framework.Input.Bindings; +using osu.Framework.Input.Events; +using osu.Game.Graphics.Cursor; +using osu.Game.Graphics.UserInterface; + +namespace osu.Game.Screens.Edit.Components +{ + public partial class EditorContextMenuContainer : OsuContextMenuContainer, IKeyBindingHandler + { + public override bool ChangeFocusOnClick => true; + + private OsuContextMenu menu = null!; + + protected override Framework.Graphics.UserInterface.Menu CreateMenu() => menu = new OsuContextMenu(true); + + public bool OnPressed(KeyBindingPressEvent e) + { + switch (e.Action) + { + case PlatformAction.Delete: + menu.Close(); + break; + } + + return false; + } + + public void OnReleased(KeyBindingReleaseEvent e) + { + } + } +} From 83aeb27c7356f0f8e7b561e79608a089181df5ca Mon Sep 17 00:00:00 2001 From: jkh675 Date: Sun, 4 Aug 2024 19:08:31 +0800 Subject: [PATCH 1788/2493] Replace original menu container to the custom one --- osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs | 3 ++- osu.Game/Screens/Edit/Editor.cs | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs index 16d11ccd1a..96b11b4431 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs @@ -213,7 +213,8 @@ namespace osu.Game.Screens.Edit.Compose.Components { case PlatformAction.Delete: DeleteSelected(); - return true; + // Pass to the `EditorContextMenuContainer` to handle the menu close + return false; } return false; diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index d40db329ec..7eed8809a3 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -31,7 +31,6 @@ using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Configuration; using osu.Game.Database; -using osu.Game.Graphics.Cursor; using osu.Game.Graphics.UserInterface; using osu.Game.Input.Bindings; using osu.Game.Localisation; @@ -43,6 +42,7 @@ using osu.Game.Overlays.OSD; using osu.Game.Rulesets; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects; +using osu.Game.Screens.Edit.Components; using osu.Game.Screens.Edit.Components.Menus; using osu.Game.Screens.Edit.Compose; using osu.Game.Screens.Edit.Compose.Components.Timeline; @@ -319,7 +319,7 @@ namespace osu.Game.Screens.Edit editorTimelineShowTimingChanges = config.GetBindable(OsuSetting.EditorTimelineShowTimingChanges); editorTimelineShowTicks = config.GetBindable(OsuSetting.EditorTimelineShowTicks); - AddInternal(new OsuContextMenuContainer + AddInternal(new EditorContextMenuContainer { RelativeSizeAxes = Axes.Both, Children = new Drawable[] From 1ff0c7cb46947597b69e6d0f40fbcab8a39e50f1 Mon Sep 17 00:00:00 2001 From: jkh675 Date: Sun, 4 Aug 2024 19:10:49 +0800 Subject: [PATCH 1789/2493] Replace original menu container with custom one --- osu.Game/Overlays/SkinEditor/SkinEditor.cs | 2 +- osu.Game/Screens/Edit/Editor.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Overlays/SkinEditor/SkinEditor.cs b/osu.Game/Overlays/SkinEditor/SkinEditor.cs index 484af34603..1b5588ef57 100644 --- a/osu.Game/Overlays/SkinEditor/SkinEditor.cs +++ b/osu.Game/Overlays/SkinEditor/SkinEditor.cs @@ -115,7 +115,7 @@ namespace osu.Game.Overlays.SkinEditor { RelativeSizeAxes = Axes.Both; - InternalChild = new OsuContextMenuContainer + InternalChild = new EditorContextMenuContainer { RelativeSizeAxes = Axes.Both, Child = new GridContainer diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 7eed8809a3..d40db329ec 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -31,6 +31,7 @@ using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Configuration; using osu.Game.Database; +using osu.Game.Graphics.Cursor; using osu.Game.Graphics.UserInterface; using osu.Game.Input.Bindings; using osu.Game.Localisation; @@ -42,7 +43,6 @@ using osu.Game.Overlays.OSD; using osu.Game.Rulesets; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects; -using osu.Game.Screens.Edit.Components; using osu.Game.Screens.Edit.Components.Menus; using osu.Game.Screens.Edit.Compose; using osu.Game.Screens.Edit.Compose.Components.Timeline; @@ -319,7 +319,7 @@ namespace osu.Game.Screens.Edit editorTimelineShowTimingChanges = config.GetBindable(OsuSetting.EditorTimelineShowTimingChanges); editorTimelineShowTicks = config.GetBindable(OsuSetting.EditorTimelineShowTicks); - AddInternal(new EditorContextMenuContainer + AddInternal(new OsuContextMenuContainer { RelativeSizeAxes = Axes.Both, Children = new Drawable[] From 5c5fcd7e7ebdb2ed1aac89d1d31ecb90fdbbd824 Mon Sep 17 00:00:00 2001 From: jkh675 Date: Sun, 4 Aug 2024 19:11:21 +0800 Subject: [PATCH 1790/2493] Allow key event pass through selection handler --- osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs index 96b11b4431..16d11ccd1a 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs @@ -213,8 +213,7 @@ namespace osu.Game.Screens.Edit.Compose.Components { case PlatformAction.Delete: DeleteSelected(); - // Pass to the `EditorContextMenuContainer` to handle the menu close - return false; + return true; } return false; From 27d6c4cecb27cfddd87f2d228c4f305cac472777 Mon Sep 17 00:00:00 2001 From: jkh675 Date: Sun, 4 Aug 2024 19:16:14 +0800 Subject: [PATCH 1791/2493] Implement on beatmap editor --- osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs | 2 +- osu.Game/Screens/Edit/Editor.cs | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs index 16d11ccd1a..808e9c71e8 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs @@ -213,7 +213,7 @@ namespace osu.Game.Screens.Edit.Compose.Components { case PlatformAction.Delete: DeleteSelected(); - return true; + return false; } return false; diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index d40db329ec..4e5abf2f82 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -43,6 +43,7 @@ using osu.Game.Overlays.OSD; using osu.Game.Rulesets; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects; +using osu.Game.Screens.Edit.Components; using osu.Game.Screens.Edit.Components.Menus; using osu.Game.Screens.Edit.Compose; using osu.Game.Screens.Edit.Compose.Components.Timeline; @@ -319,7 +320,7 @@ namespace osu.Game.Screens.Edit editorTimelineShowTimingChanges = config.GetBindable(OsuSetting.EditorTimelineShowTimingChanges); editorTimelineShowTicks = config.GetBindable(OsuSetting.EditorTimelineShowTicks); - AddInternal(new OsuContextMenuContainer + AddInternal(new EditorContextMenuContainer { RelativeSizeAxes = Axes.Both, Children = new Drawable[] From 3cc54667742c51bfa18565f92c400c98c5469354 Mon Sep 17 00:00:00 2001 From: jkh675 Date: Sun, 4 Aug 2024 19:39:06 +0800 Subject: [PATCH 1792/2493] Refactor the code to follow IoC principle and more flexible --- osu.Game/Overlays/SkinEditor/SkinEditor.cs | 184 +++++++++--------- .../Components/EditorContextMenuContainer.cs | 19 +- .../Compose/Components/SelectionHandler.cs | 10 +- osu.Game/Screens/Edit/Editor.cs | 173 ++++++++-------- 4 files changed, 196 insertions(+), 190 deletions(-) diff --git a/osu.Game/Overlays/SkinEditor/SkinEditor.cs b/osu.Game/Overlays/SkinEditor/SkinEditor.cs index 1b5588ef57..515ab45f55 100644 --- a/osu.Game/Overlays/SkinEditor/SkinEditor.cs +++ b/osu.Game/Overlays/SkinEditor/SkinEditor.cs @@ -23,7 +23,6 @@ using osu.Framework.Testing; using osu.Game.Database; using osu.Game.Graphics; using osu.Game.Graphics.Containers; -using osu.Game.Graphics.Cursor; using osu.Game.Graphics.UserInterface; using osu.Game.Localisation; using osu.Game.Overlays.Dialog; @@ -101,6 +100,12 @@ namespace osu.Game.Overlays.SkinEditor [Resolved] private IDialogOverlay? dialogOverlay { get; set; } + [Cached] + public EditorContextMenuContainer ContextMenuContainer { get; private set; } = new EditorContextMenuContainer + { + RelativeSizeAxes = Axes.Both, + }; + public SkinEditor() { } @@ -115,114 +120,111 @@ namespace osu.Game.Overlays.SkinEditor { RelativeSizeAxes = Axes.Both; - InternalChild = new EditorContextMenuContainer + ContextMenuContainer.Child = new GridContainer { RelativeSizeAxes = Axes.Both, - Child = new GridContainer + RowDimensions = new[] { - RelativeSizeAxes = Axes.Both, - RowDimensions = new[] + new Dimension(GridSizeMode.AutoSize), + new Dimension(GridSizeMode.AutoSize), + new Dimension(), + }, + Content = new[] + { + new Drawable[] { - new Dimension(GridSizeMode.AutoSize), - new Dimension(GridSizeMode.AutoSize), - new Dimension(), - }, - - Content = new[] - { - new Drawable[] + new Container { - new Container + Name = @"Menu container", + RelativeSizeAxes = Axes.X, + Depth = float.MinValue, + Height = MENU_HEIGHT, + Children = new Drawable[] { - Name = @"Menu container", - RelativeSizeAxes = Axes.X, - Depth = float.MinValue, - Height = MENU_HEIGHT, - Children = new Drawable[] + new EditorMenuBar { - new EditorMenuBar + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + RelativeSizeAxes = Axes.Both, + Items = new[] { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - RelativeSizeAxes = Axes.Both, - Items = new[] + new MenuItem(CommonStrings.MenuBarFile) { - new MenuItem(CommonStrings.MenuBarFile) + Items = new OsuMenuItem[] { - Items = new OsuMenuItem[] - { - new EditorMenuItem(Web.CommonStrings.ButtonsSave, MenuItemType.Standard, () => Save()), - new EditorMenuItem(CommonStrings.Export, MenuItemType.Standard, () => skins.ExportCurrentSkin()) { Action = { Disabled = !RuntimeInfo.IsDesktop } }, - new OsuMenuItemSpacer(), - new EditorMenuItem(CommonStrings.RevertToDefault, MenuItemType.Destructive, () => dialogOverlay?.Push(new RevertConfirmDialog(revert))), - new OsuMenuItemSpacer(), - new EditorMenuItem(CommonStrings.Exit, MenuItemType.Standard, () => skinEditorOverlay?.Hide()), - }, + new EditorMenuItem(Web.CommonStrings.ButtonsSave, MenuItemType.Standard, () => Save()), + new EditorMenuItem(CommonStrings.Export, MenuItemType.Standard, () => skins.ExportCurrentSkin()) { Action = { Disabled = !RuntimeInfo.IsDesktop } }, + new OsuMenuItemSpacer(), + new EditorMenuItem(CommonStrings.RevertToDefault, MenuItemType.Destructive, () => dialogOverlay?.Push(new RevertConfirmDialog(revert))), + new OsuMenuItemSpacer(), + new EditorMenuItem(CommonStrings.Exit, MenuItemType.Standard, () => skinEditorOverlay?.Hide()), }, - new MenuItem(CommonStrings.MenuBarEdit) - { - Items = new OsuMenuItem[] - { - undoMenuItem = new EditorMenuItem(CommonStrings.Undo, MenuItemType.Standard, Undo), - redoMenuItem = new EditorMenuItem(CommonStrings.Redo, MenuItemType.Standard, Redo), - new OsuMenuItemSpacer(), - cutMenuItem = new EditorMenuItem(CommonStrings.Cut, MenuItemType.Standard, Cut), - copyMenuItem = new EditorMenuItem(CommonStrings.Copy, MenuItemType.Standard, Copy), - pasteMenuItem = new EditorMenuItem(CommonStrings.Paste, MenuItemType.Standard, Paste), - cloneMenuItem = new EditorMenuItem(CommonStrings.Clone, MenuItemType.Standard, Clone), - } - }, - } - }, - headerText = new OsuTextFlowContainer - { - TextAnchor = Anchor.TopRight, - Padding = new MarginPadding(5), - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - AutoSizeAxes = Axes.X, - RelativeSizeAxes = Axes.Y, - }, - }, - }, - }, - new Drawable[] - { - new SkinEditorSceneLibrary - { - RelativeSizeAxes = Axes.X, - }, - }, - new Drawable[] - { - new GridContainer - { - RelativeSizeAxes = Axes.Both, - ColumnDimensions = new[] - { - new Dimension(GridSizeMode.AutoSize), - new Dimension(), - new Dimension(GridSizeMode.AutoSize), - }, - Content = new[] - { - new Drawable[] - { - componentsSidebar = new EditorSidebar(), - content = new Container - { - Depth = float.MaxValue, - RelativeSizeAxes = Axes.Both, }, - settingsSidebar = new EditorSidebar(), + new MenuItem(CommonStrings.MenuBarEdit) + { + Items = new OsuMenuItem[] + { + undoMenuItem = new EditorMenuItem(CommonStrings.Undo, MenuItemType.Standard, Undo), + redoMenuItem = new EditorMenuItem(CommonStrings.Redo, MenuItemType.Standard, Redo), + new OsuMenuItemSpacer(), + cutMenuItem = new EditorMenuItem(CommonStrings.Cut, MenuItemType.Standard, Cut), + copyMenuItem = new EditorMenuItem(CommonStrings.Copy, MenuItemType.Standard, Copy), + pasteMenuItem = new EditorMenuItem(CommonStrings.Paste, MenuItemType.Standard, Paste), + cloneMenuItem = new EditorMenuItem(CommonStrings.Clone, MenuItemType.Standard, Clone), + } + }, } + }, + headerText = new OsuTextFlowContainer + { + TextAnchor = Anchor.TopRight, + Padding = new MarginPadding(5), + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + AutoSizeAxes = Axes.X, + RelativeSizeAxes = Axes.Y, + }, + }, + }, + }, + new Drawable[] + { + new SkinEditorSceneLibrary + { + RelativeSizeAxes = Axes.X, + }, + }, + new Drawable[] + { + new GridContainer + { + RelativeSizeAxes = Axes.Both, + ColumnDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize), + new Dimension(), + new Dimension(GridSizeMode.AutoSize), + }, + Content = new[] + { + new Drawable[] + { + componentsSidebar = new EditorSidebar(), + content = new Container + { + Depth = float.MaxValue, + RelativeSizeAxes = Axes.Both, + }, + settingsSidebar = new EditorSidebar(), } } - }, - } + } + }, } }; + AddInternal(ContextMenuContainer); + clipboardContent = clipboard.Content.GetBoundCopy(); } diff --git a/osu.Game/Screens/Edit/Components/EditorContextMenuContainer.cs b/osu.Game/Screens/Edit/Components/EditorContextMenuContainer.cs index 3207cb0849..fa855100d7 100644 --- a/osu.Game/Screens/Edit/Components/EditorContextMenuContainer.cs +++ b/osu.Game/Screens/Edit/Components/EditorContextMenuContainer.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. -using osu.Framework.Input; -using osu.Framework.Input.Bindings; -using osu.Framework.Input.Events; using osu.Game.Graphics.Cursor; using osu.Game.Graphics.UserInterface; namespace osu.Game.Screens.Edit.Components { - public partial class EditorContextMenuContainer : OsuContextMenuContainer, IKeyBindingHandler + public partial class EditorContextMenuContainer : OsuContextMenuContainer { public override bool ChangeFocusOnClick => true; @@ -17,20 +14,14 @@ namespace osu.Game.Screens.Edit.Components protected override Framework.Graphics.UserInterface.Menu CreateMenu() => menu = new OsuContextMenu(true); - public bool OnPressed(KeyBindingPressEvent e) + public void ShowMenu() { - switch (e.Action) - { - case PlatformAction.Delete: - menu.Close(); - break; - } - - return false; + menu.Show(); } - public void OnReleased(KeyBindingReleaseEvent e) + public void CloseMenu() { + menu.Close(); } } } diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs index 808e9c71e8..45aa50afbf 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs @@ -20,6 +20,7 @@ using osu.Game.Graphics.UserInterface; using osu.Game.Input.Bindings; using osu.Game.Resources.Localisation.Web; using osu.Game.Rulesets.Edit; +using osu.Game.Screens.Edit.Components; using osu.Game.Screens.Edit.Compose.Components.Timeline; using osuTK; using osuTK.Input; @@ -59,6 +60,9 @@ namespace osu.Game.Screens.Edit.Compose.Components public SelectionScaleHandler ScaleHandler { get; private set; } + [Resolved] + private EditorContextMenuContainer editorContextMenuContainer { get; set; } + protected SelectionHandler() { selectedBlueprints = new List>(); @@ -230,7 +234,11 @@ namespace osu.Game.Screens.Edit.Compose.Components /// /// Deselect all selected items. /// - protected void DeselectAll() => SelectedItems.Clear(); + protected void DeselectAll() + { + SelectedItems.Clear(); + editorContextMenuContainer.CloseMenu(); + } /// /// Handle a blueprint becoming selected. diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 4e5abf2f82..45c3dfe896 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -77,6 +77,12 @@ namespace osu.Game.Screens.Edit /// public const float WAVEFORM_VISUAL_OFFSET = 20; + [Cached] + public EditorContextMenuContainer ContextMenuContainer { get; private set; } = new EditorContextMenuContainer() + { + RelativeSizeAxes = Axes.Both + }; + public override float BackgroundParallaxAmount => 0.1f; public override bool AllowBackButton => false; @@ -320,109 +326,108 @@ namespace osu.Game.Screens.Edit editorTimelineShowTimingChanges = config.GetBindable(OsuSetting.EditorTimelineShowTimingChanges); editorTimelineShowTicks = config.GetBindable(OsuSetting.EditorTimelineShowTicks); - AddInternal(new EditorContextMenuContainer + ContextMenuContainer.AddRange(new Drawable[] { - RelativeSizeAxes = Axes.Both, - Children = new Drawable[] + new Container { - new Container + Name = "Screen container", + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Top = 40, Bottom = 50 }, + Child = screenContainer = new Container { - Name = "Screen container", RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding { Top = 40, Bottom = 50 }, - Child = screenContainer = new Container - { - RelativeSizeAxes = Axes.Both, - } - }, - new Container + } + }, + new Container + { + Name = "Top bar", + RelativeSizeAxes = Axes.X, + Height = 40, + Children = new Drawable[] { - Name = "Top bar", - RelativeSizeAxes = Axes.X, - Height = 40, - Children = new Drawable[] + new EditorMenuBar { - new EditorMenuBar + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + RelativeSizeAxes = Axes.Both, + MaxHeight = 600, + Items = new[] { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - RelativeSizeAxes = Axes.Both, - MaxHeight = 600, - Items = new[] + new MenuItem(CommonStrings.MenuBarFile) { - new MenuItem(CommonStrings.MenuBarFile) + Items = createFileMenuItems().ToList() + }, + new MenuItem(CommonStrings.MenuBarEdit) + { + Items = new[] { - Items = createFileMenuItems().ToList() - }, - new MenuItem(CommonStrings.MenuBarEdit) + undoMenuItem = new EditorMenuItem(CommonStrings.Undo, MenuItemType.Standard, Undo), + redoMenuItem = new EditorMenuItem(CommonStrings.Redo, MenuItemType.Standard, Redo), + new OsuMenuItemSpacer(), + cutMenuItem = new EditorMenuItem(CommonStrings.Cut, MenuItemType.Standard, Cut), + copyMenuItem = new EditorMenuItem(CommonStrings.Copy, MenuItemType.Standard, Copy), + pasteMenuItem = new EditorMenuItem(CommonStrings.Paste, MenuItemType.Standard, Paste), + cloneMenuItem = new EditorMenuItem(CommonStrings.Clone, MenuItemType.Standard, Clone), + } + }, + new MenuItem(CommonStrings.MenuBarView) + { + Items = new[] { - Items = new[] + new MenuItem(EditorStrings.Timeline) { - undoMenuItem = new EditorMenuItem(CommonStrings.Undo, MenuItemType.Standard, Undo), - redoMenuItem = new EditorMenuItem(CommonStrings.Redo, MenuItemType.Standard, Redo), - new OsuMenuItemSpacer(), - cutMenuItem = new EditorMenuItem(CommonStrings.Cut, MenuItemType.Standard, Cut), - copyMenuItem = new EditorMenuItem(CommonStrings.Copy, MenuItemType.Standard, Copy), - pasteMenuItem = new EditorMenuItem(CommonStrings.Paste, MenuItemType.Standard, Paste), - cloneMenuItem = new EditorMenuItem(CommonStrings.Clone, MenuItemType.Standard, Clone), - } - }, - new MenuItem(CommonStrings.MenuBarView) - { - Items = new[] + Items = + [ + new WaveformOpacityMenuItem(config.GetBindable(OsuSetting.EditorWaveformOpacity)), + new ToggleMenuItem(EditorStrings.TimelineShowTimingChanges) + { + State = { BindTarget = editorTimelineShowTimingChanges } + }, + new ToggleMenuItem(EditorStrings.TimelineShowTicks) + { + State = { BindTarget = editorTimelineShowTicks } + }, + ] + }, + new BackgroundDimMenuItem(editorBackgroundDim), + new ToggleMenuItem(EditorStrings.ShowHitMarkers) { - new MenuItem(EditorStrings.Timeline) - { - Items = - [ - new WaveformOpacityMenuItem(config.GetBindable(OsuSetting.EditorWaveformOpacity)), - new ToggleMenuItem(EditorStrings.TimelineShowTimingChanges) - { - State = { BindTarget = editorTimelineShowTimingChanges } - }, - new ToggleMenuItem(EditorStrings.TimelineShowTicks) - { - State = { BindTarget = editorTimelineShowTicks } - }, - ] - }, - new BackgroundDimMenuItem(editorBackgroundDim), - new ToggleMenuItem(EditorStrings.ShowHitMarkers) - { - State = { BindTarget = editorHitMarkers }, - }, - new ToggleMenuItem(EditorStrings.AutoSeekOnPlacement) - { - State = { BindTarget = editorAutoSeekOnPlacement }, - }, - new ToggleMenuItem(EditorStrings.LimitedDistanceSnap) - { - State = { BindTarget = editorLimitedDistanceSnap }, - } - } - }, - new MenuItem(EditorStrings.Timing) - { - Items = new MenuItem[] + State = { BindTarget = editorHitMarkers }, + }, + new ToggleMenuItem(EditorStrings.AutoSeekOnPlacement) { - new EditorMenuItem(EditorStrings.SetPreviewPointToCurrent, MenuItemType.Standard, SetPreviewPointToCurrentTime) + State = { BindTarget = editorAutoSeekOnPlacement }, + }, + new ToggleMenuItem(EditorStrings.LimitedDistanceSnap) + { + State = { BindTarget = editorLimitedDistanceSnap }, } } + }, + new MenuItem(EditorStrings.Timing) + { + Items = new MenuItem[] + { + new EditorMenuItem(EditorStrings.SetPreviewPointToCurrent, MenuItemType.Standard, SetPreviewPointToCurrentTime) + } } - }, - screenSwitcher = new EditorScreenSwitcherControl - { - Anchor = Anchor.BottomRight, - Origin = Anchor.BottomRight, - X = -10, - Current = Mode, - }, + } + }, + screenSwitcher = new EditorScreenSwitcherControl + { + Anchor = Anchor.BottomRight, + Origin = Anchor.BottomRight, + X = -10, + Current = Mode, }, }, - bottomBar = new BottomBar(), - MutationTracker, - } + }, + bottomBar = new BottomBar(), + MutationTracker, }); + + AddInternal(ContextMenuContainer); + changeHandler?.CanUndo.BindValueChanged(v => undoMenuItem.Action.Disabled = !v.NewValue, true); changeHandler?.CanRedo.BindValueChanged(v => redoMenuItem.Action.Disabled = !v.NewValue, true); From 5d31171fb0ff3d01b034c655c3995489340236ef Mon Sep 17 00:00:00 2001 From: jkh675 Date: Sun, 4 Aug 2024 19:43:43 +0800 Subject: [PATCH 1793/2493] Fix code quality --- osu.Game/Screens/Edit/Editor.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 45c3dfe896..0a944d0627 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -31,7 +31,6 @@ using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Configuration; using osu.Game.Database; -using osu.Game.Graphics.Cursor; using osu.Game.Graphics.UserInterface; using osu.Game.Input.Bindings; using osu.Game.Localisation; @@ -78,7 +77,7 @@ namespace osu.Game.Screens.Edit public const float WAVEFORM_VISUAL_OFFSET = 20; [Cached] - public EditorContextMenuContainer ContextMenuContainer { get; private set; } = new EditorContextMenuContainer() + public EditorContextMenuContainer ContextMenuContainer { get; private set; } = new EditorContextMenuContainer { RelativeSizeAxes = Axes.Both }; From 273bd73a99d79fa3abbe05d14a439a20bccdf869 Mon Sep 17 00:00:00 2001 From: jkh675 Date: Sun, 4 Aug 2024 19:50:57 +0800 Subject: [PATCH 1794/2493] Fix unit test error --- osu.Game.Tests/Visual/UserInterface/TestSceneOsuTooltip.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneOsuTooltip.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneOsuTooltip.cs index 1a1db8eaf7..90545885c7 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneOsuTooltip.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneOsuTooltip.cs @@ -22,7 +22,7 @@ namespace osu.Game.Tests.Visual.UserInterface private TestTooltipContainer container = null!; [SetUp] - public void SetUp() + public void SetUp() => Schedule(() => { Child = new Container { @@ -48,7 +48,7 @@ namespace osu.Game.Tests.Visual.UserInterface }, }, }; - } + }); private static readonly string[] test_case_tooltip_string = [ @@ -57,6 +57,9 @@ namespace osu.Game.Tests.Visual.UserInterface $"H{new string('e', 500)}llo", ]; + //TODO: o!f issue: https://github.com/ppy/osu-framework/issues/5007 + //Enable after o!f fixed + [Ignore("o!f issue https://github.com/ppy/osu-framework/issues/5007")] [Test] public void TestTooltipBasic([Values(0, 1, 2)] int index) { From 7c83d6a883c017204025cb08aae10f54ed04ce2c Mon Sep 17 00:00:00 2001 From: jkh675 Date: Sun, 4 Aug 2024 19:56:41 +0800 Subject: [PATCH 1795/2493] Cleanup code --- osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs index 45aa50afbf..7335096120 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs @@ -217,7 +217,7 @@ namespace osu.Game.Screens.Edit.Compose.Components { case PlatformAction.Delete: DeleteSelected(); - return false; + return true; } return false; From 2145368d17f5740f792c8b721ae3e111823c0f94 Mon Sep 17 00:00:00 2001 From: jkh675 Date: Sun, 4 Aug 2024 21:00:43 +0800 Subject: [PATCH 1796/2493] Merge `EditorContextMenuContainer` into `OsuContextMenuContainer` --- .../Cursor/OsuContextMenuContainer.cs | 13 ++++++--- osu.Game/Overlays/SkinEditor/SkinEditor.cs | 3 ++- .../Components/EditorContextMenuContainer.cs | 27 ------------------- .../Compose/Components/BlueprintContainer.cs | 4 +++ .../Compose/Components/SelectionHandler.cs | 4 +-- osu.Game/Screens/Edit/Editor.cs | 4 +-- 6 files changed, 20 insertions(+), 35 deletions(-) delete mode 100644 osu.Game/Screens/Edit/Components/EditorContextMenuContainer.cs diff --git a/osu.Game/Graphics/Cursor/OsuContextMenuContainer.cs b/osu.Game/Graphics/Cursor/OsuContextMenuContainer.cs index c5bcfcd2df..85b24cb6a3 100644 --- a/osu.Game/Graphics/Cursor/OsuContextMenuContainer.cs +++ b/osu.Game/Graphics/Cursor/OsuContextMenuContainer.cs @@ -13,11 +13,18 @@ namespace osu.Game.Graphics.Cursor [Cached] private OsuContextMenuSamples samples = new OsuContextMenuSamples(); - public OsuContextMenuContainer() + private OsuContextMenu menu = null!; + + protected override Menu CreateMenu() => menu = new OsuContextMenu(true); + + public void ShowMenu() { - AddInternal(samples); + menu.Show(); } - protected override Menu CreateMenu() => new OsuContextMenu(true); + public void CloseMenu() + { + menu.Close(); + } } } diff --git a/osu.Game/Overlays/SkinEditor/SkinEditor.cs b/osu.Game/Overlays/SkinEditor/SkinEditor.cs index 515ab45f55..bb2d93f887 100644 --- a/osu.Game/Overlays/SkinEditor/SkinEditor.cs +++ b/osu.Game/Overlays/SkinEditor/SkinEditor.cs @@ -32,6 +32,7 @@ using osu.Game.Screens.Edit; using osu.Game.Screens.Edit.Components; using osu.Game.Screens.Edit.Components.Menus; using osu.Game.Skinning; +using osu.Game.Graphics.Cursor; namespace osu.Game.Overlays.SkinEditor { @@ -101,7 +102,7 @@ namespace osu.Game.Overlays.SkinEditor private IDialogOverlay? dialogOverlay { get; set; } [Cached] - public EditorContextMenuContainer ContextMenuContainer { get; private set; } = new EditorContextMenuContainer + public OsuContextMenuContainer ContextMenuContainer { get; private set; } = new OsuContextMenuContainer { RelativeSizeAxes = Axes.Both, }; diff --git a/osu.Game/Screens/Edit/Components/EditorContextMenuContainer.cs b/osu.Game/Screens/Edit/Components/EditorContextMenuContainer.cs deleted file mode 100644 index fa855100d7..0000000000 --- a/osu.Game/Screens/Edit/Components/EditorContextMenuContainer.cs +++ /dev/null @@ -1,27 +0,0 @@ -// 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.Graphics.Cursor; -using osu.Game.Graphics.UserInterface; - -namespace osu.Game.Screens.Edit.Components -{ - public partial class EditorContextMenuContainer : OsuContextMenuContainer - { - public override bool ChangeFocusOnClick => true; - - private OsuContextMenu menu = null!; - - protected override Framework.Graphics.UserInterface.Menu CreateMenu() => menu = new OsuContextMenu(true); - - public void ShowMenu() - { - menu.Show(); - } - - public void CloseMenu() - { - menu.Close(); - } - } -} diff --git a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs index c66be90605..e8228872d9 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs @@ -15,6 +15,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Input; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; +using osu.Game.Graphics.Cursor; using osu.Game.Graphics.UserInterface; using osu.Game.Input.Bindings; using osu.Game.Rulesets.Edit; @@ -46,6 +47,9 @@ namespace osu.Game.Screens.Edit.Compose.Components protected readonly BindableList SelectedItems = new BindableList(); + [Resolved(CanBeNull = true)] + private OsuContextMenuContainer contextMenuContainer { get; set; } + protected BlueprintContainer() { RelativeSizeAxes = Axes.Both; diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs index 7335096120..48876278f7 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs @@ -16,11 +16,11 @@ using osu.Framework.Graphics.UserInterface; using osu.Framework.Input; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; +using osu.Game.Graphics.Cursor; using osu.Game.Graphics.UserInterface; using osu.Game.Input.Bindings; using osu.Game.Resources.Localisation.Web; using osu.Game.Rulesets.Edit; -using osu.Game.Screens.Edit.Components; using osu.Game.Screens.Edit.Compose.Components.Timeline; using osuTK; using osuTK.Input; @@ -61,7 +61,7 @@ namespace osu.Game.Screens.Edit.Compose.Components public SelectionScaleHandler ScaleHandler { get; private set; } [Resolved] - private EditorContextMenuContainer editorContextMenuContainer { get; set; } + private OsuContextMenuContainer editorContextMenuContainer { get; set; } protected SelectionHandler() { diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 0a944d0627..b1a066afb7 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -31,6 +31,7 @@ using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Configuration; using osu.Game.Database; +using osu.Game.Graphics.Cursor; using osu.Game.Graphics.UserInterface; using osu.Game.Input.Bindings; using osu.Game.Localisation; @@ -42,7 +43,6 @@ using osu.Game.Overlays.OSD; using osu.Game.Rulesets; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects; -using osu.Game.Screens.Edit.Components; using osu.Game.Screens.Edit.Components.Menus; using osu.Game.Screens.Edit.Compose; using osu.Game.Screens.Edit.Compose.Components.Timeline; @@ -77,7 +77,7 @@ namespace osu.Game.Screens.Edit public const float WAVEFORM_VISUAL_OFFSET = 20; [Cached] - public EditorContextMenuContainer ContextMenuContainer { get; private set; } = new EditorContextMenuContainer + public OsuContextMenuContainer ContextMenuContainer { get; private set; } = new OsuContextMenuContainer { RelativeSizeAxes = Axes.Both }; From 38dacfeaa2e228bf2ab675abefc5e405abd98d10 Mon Sep 17 00:00:00 2001 From: jkh675 Date: Sun, 4 Aug 2024 21:12:09 +0800 Subject: [PATCH 1797/2493] Fix unit test --- .../Visual/Editing/TestSceneComposeScreen.cs | 2 ++ .../Editing/TestSceneHitObjectComposer.cs | 20 ++++++++++++++----- .../Visual/Editing/TimelineTestScene.cs | 9 +++++++-- 3 files changed, 24 insertions(+), 7 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneComposeScreen.cs b/osu.Game.Tests/Visual/Editing/TestSceneComposeScreen.cs index 035092ecb7..7405433e73 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneComposeScreen.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneComposeScreen.cs @@ -10,6 +10,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Testing; using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Graphics.Cursor; using osu.Game.Overlays; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Osu; @@ -52,6 +53,7 @@ namespace osu.Game.Tests.Visual.Editing (typeof(EditorBeatmap), editorBeatmap), (typeof(IBeatSnapProvider), editorBeatmap), (typeof(OverlayColourProvider), new OverlayColourProvider(OverlayColourScheme.Green)), + (typeof(OsuContextMenuContainer), new OsuContextMenuContainer()) }, Children = new Drawable[] { diff --git a/osu.Game.Tests/Visual/Editing/TestSceneHitObjectComposer.cs b/osu.Game.Tests/Visual/Editing/TestSceneHitObjectComposer.cs index f392841ac7..fac47deec9 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneHitObjectComposer.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneHitObjectComposer.cs @@ -3,6 +3,7 @@ #nullable disable +using System; using System.Collections.Generic; using System.Linq; using NUnit.Framework; @@ -12,6 +13,7 @@ using osu.Framework.Graphics.Cursor; using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Graphics.Cursor; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit.Tools; using osu.Game.Rulesets.Objects; @@ -70,13 +72,21 @@ namespace osu.Game.Tests.Visual.Editing AddStep("Create composer", () => { - Child = editorBeatmapContainer = new EditorBeatmapContainer(Beatmap.Value) + Child = new DependencyProvidingContainer { - Child = hitObjectComposer = new OsuHitObjectComposer(new OsuRuleset()) + RelativeSizeAxes = Axes.Both, + CachedDependencies = new (Type, object)[] { - // force the composer to fully overlap the playfield area by setting a 4:3 aspect ratio. - FillMode = FillMode.Fit, - FillAspectRatio = 4 / 3f + (typeof(OsuContextMenuContainer), new OsuContextMenuContainer()) + }, + Child = editorBeatmapContainer = new EditorBeatmapContainer(Beatmap.Value) + { + Child = hitObjectComposer = new OsuHitObjectComposer(new OsuRuleset()) + { + // force the composer to fully overlap the playfield area by setting a 4:3 aspect ratio. + FillMode = FillMode.Fit, + FillAspectRatio = 4 / 3f + } } }; }); diff --git a/osu.Game.Tests/Visual/Editing/TimelineTestScene.cs b/osu.Game.Tests/Visual/Editing/TimelineTestScene.cs index cb45ad5a07..14afd3eac1 100644 --- a/osu.Game.Tests/Visual/Editing/TimelineTestScene.cs +++ b/osu.Game.Tests/Visual/Editing/TimelineTestScene.cs @@ -16,6 +16,7 @@ using osu.Game.Graphics.Cursor; using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets.Edit; using osu.Game.Screens.Edit; +using osu.Game.Screens.Edit.Components; using osu.Game.Screens.Edit.Compose.Components.Timeline; using osu.Game.Storyboards; using osuTK; @@ -51,7 +52,7 @@ namespace osu.Game.Tests.Visual.Editing Composer.Alpha = 0; - Add(new OsuContextMenuContainer + var contextMenuContainer = new OsuContextMenuContainer { RelativeSizeAxes = Axes.Both, Children = new Drawable[] @@ -75,7 +76,11 @@ namespace osu.Game.Tests.Visual.Editing Origin = Anchor.Centre, } } - }); + }; + + Dependencies.Cache(contextMenuContainer); + + Add(contextMenuContainer); } [SetUpSteps] From 7cebf4c3d23cf49c11ddf079512154e1d0fc0618 Mon Sep 17 00:00:00 2001 From: jkh675 Date: Sun, 4 Aug 2024 21:18:03 +0800 Subject: [PATCH 1798/2493] Fix code quality --- osu.Game.Tests/Visual/Editing/TimelineTestScene.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Editing/TimelineTestScene.cs b/osu.Game.Tests/Visual/Editing/TimelineTestScene.cs index 14afd3eac1..2323612e89 100644 --- a/osu.Game.Tests/Visual/Editing/TimelineTestScene.cs +++ b/osu.Game.Tests/Visual/Editing/TimelineTestScene.cs @@ -16,7 +16,6 @@ using osu.Game.Graphics.Cursor; using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets.Edit; using osu.Game.Screens.Edit; -using osu.Game.Screens.Edit.Components; using osu.Game.Screens.Edit.Compose.Components.Timeline; using osu.Game.Storyboards; using osuTK; From b32d97b4c055d9b3ba5618673cda22cf039ca4b3 Mon Sep 17 00:00:00 2001 From: jkh675 Date: Sun, 4 Aug 2024 21:27:25 +0800 Subject: [PATCH 1799/2493] Remove decreapted property --- .../Screens/Edit/Compose/Components/BlueprintContainer.cs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs index e8228872d9..c66be90605 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs @@ -15,7 +15,6 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Input; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; -using osu.Game.Graphics.Cursor; using osu.Game.Graphics.UserInterface; using osu.Game.Input.Bindings; using osu.Game.Rulesets.Edit; @@ -47,9 +46,6 @@ namespace osu.Game.Screens.Edit.Compose.Components protected readonly BindableList SelectedItems = new BindableList(); - [Resolved(CanBeNull = true)] - private OsuContextMenuContainer contextMenuContainer { get; set; } - protected BlueprintContainer() { RelativeSizeAxes = Axes.Both; From 2720bcf285813e53af3f21cbbb220927c6d4f357 Mon Sep 17 00:00:00 2001 From: jkh675 Date: Sun, 4 Aug 2024 22:37:54 +0800 Subject: [PATCH 1800/2493] Fix ruleset unit test --- .../Editor/TestSceneManiaComposeScreen.cs | 2 ++ .../Editor/TestSceneManiaHitObjectComposer.cs | 11 ++++++++++- .../Editor/TestSceneOsuDistanceSnapGrid.cs | 2 ++ .../Editor/TestSceneTaikoHitObjectComposer.cs | 11 ++++++++++- .../TestSceneHitObjectComposerDistanceSnapping.cs | 10 ++++++++-- 5 files changed, 32 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaComposeScreen.cs b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaComposeScreen.cs index 8f623d1fc6..a2f8670774 100644 --- a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaComposeScreen.cs +++ b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaComposeScreen.cs @@ -10,6 +10,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Testing; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Database; +using osu.Game.Graphics.Cursor; using osu.Game.Overlays; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Mania.Beatmaps; @@ -52,6 +53,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor (typeof(EditorBeatmap), editorBeatmap), (typeof(IBeatSnapProvider), editorBeatmap), (typeof(OverlayColourProvider), new OverlayColourProvider(OverlayColourScheme.Green)), + (typeof(OsuContextMenuContainer), new OsuContextMenuContainer()), }, Children = new Drawable[] { diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaHitObjectComposer.cs b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaHitObjectComposer.cs index d88f488582..802b2fe843 100644 --- a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaHitObjectComposer.cs +++ b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaHitObjectComposer.cs @@ -3,6 +3,7 @@ #nullable disable +using System; using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; @@ -11,6 +12,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Testing; using osu.Framework.Utils; +using osu.Game.Graphics.Cursor; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Edit; @@ -37,7 +39,14 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor BeatDivisor.Value = 8; EditorClock.Seek(0); - Child = composer = new TestComposer { RelativeSizeAxes = Axes.Both }; + Child = new DependencyProvidingContainer + { + CachedDependencies = new (Type, object)[] + { + (typeof(OsuContextMenuContainer), new OsuContextMenuContainer()), + }, + Child = composer = new TestComposer { RelativeSizeAxes = Axes.Both }, + }; }); [Test] diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuDistanceSnapGrid.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuDistanceSnapGrid.cs index b70f932913..b57496673b 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuDistanceSnapGrid.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuDistanceSnapGrid.cs @@ -16,6 +16,7 @@ using osu.Framework.Input; using osu.Framework.Testing; using osu.Framework.Utils; using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Graphics.Cursor; using osu.Game.Overlays; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Osu.Beatmaps; @@ -79,6 +80,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor { var dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); dependencies.CacheAs(composer.DistanceSnapProvider); + dependencies.Cache(new OsuContextMenuContainer()); return dependencies; } diff --git a/osu.Game.Rulesets.Taiko.Tests/Editor/TestSceneTaikoHitObjectComposer.cs b/osu.Game.Rulesets.Taiko.Tests/Editor/TestSceneTaikoHitObjectComposer.cs index 64a29ce866..7c379eb43c 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Editor/TestSceneTaikoHitObjectComposer.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Editor/TestSceneTaikoHitObjectComposer.cs @@ -1,10 +1,12 @@ // 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 NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Game.Graphics.Cursor; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Taiko.Beatmaps; using osu.Game.Rulesets.Taiko.Edit; @@ -22,7 +24,14 @@ namespace osu.Game.Rulesets.Taiko.Tests.Editor BeatDivisor.Value = 8; EditorClock.Seek(0); - Child = new TestComposer { RelativeSizeAxes = Axes.Both }; + Child = new DependencyProvidingContainer + { + CachedDependencies = new (Type, object)[] + { + (typeof(OsuContextMenuContainer), new OsuContextMenuContainer()), + }, + Child = new TestComposer { RelativeSizeAxes = Axes.Both }, + }; }); [Test] diff --git a/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs b/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs index 12b7dbbf12..ad6aef6302 100644 --- a/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs +++ b/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.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.Linq; using NUnit.Framework; using osu.Framework.Allocation; @@ -10,6 +11,7 @@ using osu.Framework.Graphics.Cursor; using osu.Framework.Testing; using osu.Framework.Utils; using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Graphics.Cursor; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu; @@ -52,9 +54,13 @@ namespace osu.Game.Tests.Editing [SetUp] public void Setup() => Schedule(() => { - Children = new Drawable[] + Child = new DependencyProvidingContainer { - composer = new TestHitObjectComposer() + CachedDependencies = new (Type, object)[] + { + (typeof(OsuContextMenuContainer), new OsuContextMenuContainer()), + }, + Child = composer = new TestHitObjectComposer(), }; BeatDivisor.Value = 1; From 1b25633e4791c46261614f676c98753ea1b5c2ac Mon Sep 17 00:00:00 2001 From: jkh675 Date: Sun, 4 Aug 2024 23:45:42 +0800 Subject: [PATCH 1801/2493] Fix headless test --- .../Editor/TestSceneManiaHitObjectComposer.cs | 12 ++++-------- .../TestSceneHitObjectComposerDistanceSnapping.cs | 12 ++++-------- 2 files changed, 8 insertions(+), 16 deletions(-) diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaHitObjectComposer.cs b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaHitObjectComposer.cs index 802b2fe843..56ad0a2423 100644 --- a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaHitObjectComposer.cs +++ b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaHitObjectComposer.cs @@ -33,20 +33,16 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor { private TestComposer composer; + [Cached] + public readonly OsuContextMenuContainer ContextMenuContainer = new OsuContextMenuContainer(); + [SetUp] public void Setup() => Schedule(() => { BeatDivisor.Value = 8; EditorClock.Seek(0); - Child = new DependencyProvidingContainer - { - CachedDependencies = new (Type, object)[] - { - (typeof(OsuContextMenuContainer), new OsuContextMenuContainer()), - }, - Child = composer = new TestComposer { RelativeSizeAxes = Axes.Both }, - }; + Child = composer = new TestComposer { RelativeSizeAxes = Axes.Both }; }); [Test] diff --git a/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs b/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs index ad6aef6302..83c660bd4d 100644 --- a/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs +++ b/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs @@ -33,6 +33,9 @@ namespace osu.Game.Tests.Editing [Cached(typeof(IBeatSnapProvider))] private readonly EditorBeatmap editorBeatmap; + [Cached] + public readonly OsuContextMenuContainer ContextMenuContainer = new OsuContextMenuContainer(); + protected override Container Content { get; } = new PopoverContainer { RelativeSizeAxes = Axes.Both }; public TestSceneHitObjectComposerDistanceSnapping() @@ -54,14 +57,7 @@ namespace osu.Game.Tests.Editing [SetUp] public void Setup() => Schedule(() => { - Child = new DependencyProvidingContainer - { - CachedDependencies = new (Type, object)[] - { - (typeof(OsuContextMenuContainer), new OsuContextMenuContainer()), - }, - Child = composer = new TestHitObjectComposer(), - }; + Child = composer = new TestHitObjectComposer(); BeatDivisor.Value = 1; From 2098fb8a9dc07a81102097f789512cd3333c0007 Mon Sep 17 00:00:00 2001 From: jkh675 Date: Mon, 5 Aug 2024 00:08:02 +0800 Subject: [PATCH 1802/2493] Fix code quality --- .../Editor/TestSceneManiaHitObjectComposer.cs | 1 - .../Editing/TestSceneHitObjectComposerDistanceSnapping.cs | 1 - 2 files changed, 2 deletions(-) diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaHitObjectComposer.cs b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaHitObjectComposer.cs index 56ad0a2423..c2364cce1a 100644 --- a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaHitObjectComposer.cs +++ b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaHitObjectComposer.cs @@ -3,7 +3,6 @@ #nullable disable -using System; using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; diff --git a/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs b/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs index 83c660bd4d..e5e7d0f8a7 100644 --- a/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs +++ b/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.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.Linq; using NUnit.Framework; using osu.Framework.Allocation; From b0757a13c20557abc256c9e4937c747dea7d1421 Mon Sep 17 00:00:00 2001 From: kstefanowicz Date: Sun, 4 Aug 2024 12:32:08 -0400 Subject: [PATCH 1803/2493] Add "enter" hint to chatbox placeholder text while in-game --- osu.Game/Localisation/ChatStrings.cs | 5 +++++ .../Screens/OnlinePlay/Multiplayer/GameplayChatDisplay.cs | 2 ++ 2 files changed, 7 insertions(+) diff --git a/osu.Game/Localisation/ChatStrings.cs b/osu.Game/Localisation/ChatStrings.cs index 6b0a6bd8e1..3b1fc6000a 100644 --- a/osu.Game/Localisation/ChatStrings.cs +++ b/osu.Game/Localisation/ChatStrings.cs @@ -24,6 +24,11 @@ namespace osu.Game.Localisation /// public static LocalisableString MentionUser => new TranslatableString(getKey(@"mention_user"), @"Mention"); + /// + /// "press enter to type message..." + /// + public static LocalisableString IngameInputPlaceholder => new TranslatableString(getKey("input.ingameplaceholder"), "press enter to type message..."); + private static string getKey(string key) => $"{prefix}:{key}"; } } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/GameplayChatDisplay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/GameplayChatDisplay.cs index d003110039..cccab46d98 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/GameplayChatDisplay.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/GameplayChatDisplay.cs @@ -10,6 +10,7 @@ using osu.Framework.Graphics; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Game.Input.Bindings; +using osu.Game.Localisation; using osu.Game.Online.Rooms; using osu.Game.Screens.OnlinePlay.Match.Components; using osu.Game.Screens.Play; @@ -42,6 +43,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer Background.Alpha = 0.2f; TextBox.FocusLost = () => expandedFromTextBoxFocus.Value = false; + TextBox.PlaceholderText = ChatStrings.IngameInputPlaceholder; } protected override bool OnHover(HoverEvent e) => true; // use UI mouse cursor. From a5a392e9fc7e9cc121abfe722ab5ad1d8509f7e0 Mon Sep 17 00:00:00 2001 From: AkiraTenchi <34791734+AkiraTenchi@users.noreply.github.com> Date: Sun, 4 Aug 2024 19:48:29 +0200 Subject: [PATCH 1804/2493] Update FilterQueryParser.cs Add sr as an alias for star rating in the search parameters --- osu.Game/Screens/Select/FilterQueryParser.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Screens/Select/FilterQueryParser.cs b/osu.Game/Screens/Select/FilterQueryParser.cs index 4e49495f47..40fd289be6 100644 --- a/osu.Game/Screens/Select/FilterQueryParser.cs +++ b/osu.Game/Screens/Select/FilterQueryParser.cs @@ -40,6 +40,7 @@ namespace osu.Game.Screens.Select { case "star": case "stars": + case "sr": return TryUpdateCriteriaRange(ref criteria.StarDifficulty, op, value, 0.01d / 2); case "ar": From f92e2094c166508e7e27a56a4d3a6ed07b52c120 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 5 Aug 2024 12:29:56 +0900 Subject: [PATCH 1805/2493] Adjust localisation string name + formatting --- osu.Game/Localisation/ChatStrings.cs | 2 +- osu.Game/Screens/OnlinePlay/Multiplayer/GameplayChatDisplay.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Localisation/ChatStrings.cs b/osu.Game/Localisation/ChatStrings.cs index 3b1fc6000a..f7a36d9570 100644 --- a/osu.Game/Localisation/ChatStrings.cs +++ b/osu.Game/Localisation/ChatStrings.cs @@ -27,7 +27,7 @@ namespace osu.Game.Localisation /// /// "press enter to type message..." /// - public static LocalisableString IngameInputPlaceholder => new TranslatableString(getKey("input.ingameplaceholder"), "press enter to type message..."); + public static LocalisableString InGameInputPlaceholder => new TranslatableString(getKey(@"in_game_input_placeholder"), @"press enter to type message..."); private static string getKey(string key) => $"{prefix}:{key}"; } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/GameplayChatDisplay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/GameplayChatDisplay.cs index cccab46d98..656071ad43 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/GameplayChatDisplay.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/GameplayChatDisplay.cs @@ -43,7 +43,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer Background.Alpha = 0.2f; TextBox.FocusLost = () => expandedFromTextBoxFocus.Value = false; - TextBox.PlaceholderText = ChatStrings.IngameInputPlaceholder; + TextBox.PlaceholderText = ChatStrings.InGameInputPlaceholder; } protected override bool OnHover(HoverEvent e) => true; // use UI mouse cursor. From 20b890570e2edaf39d2b6e68a9feac15db23d8c4 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 5 Aug 2024 13:28:42 +0900 Subject: [PATCH 1806/2493] Replace try-finally with return Try-finally has a small overhead that's unnecessary in this case given how small the code block is. --- osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs b/osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs index 44a2be0024..d90d3d26eb 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs @@ -174,14 +174,9 @@ namespace osu.Game.Rulesets.Osu.UI public bool OnPressed(KeyBindingPressEvent e) { - try - { - return BlockNextPress; - } - finally - { - BlockNextPress = false; - } + bool block = BlockNextPress; + BlockNextPress = false; + return block; } public void OnReleased(KeyBindingReleaseEvent e) From 0557b9ab7959deceee5c1ae3ac7bfdf6dc1fe192 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 5 Aug 2024 13:20:44 +0900 Subject: [PATCH 1807/2493] Allow placement deletion with middle mouse This is in addition to Shift + Right-click. I thik middle mouse feels more natural and is a good permanent solution to this issue. Note that this also *allows triggering the context menu from placement mode*. Until now it's done nothing. This may be annoying to users with muscle memory but I want to make the change and harvest feedback. I think showing the context menu is more correct behaviour (although arguably it should return to placement mode on dismiss?). --- osu.Game/Rulesets/Edit/PlacementBlueprint.cs | 4 +--- osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/osu.Game/Rulesets/Edit/PlacementBlueprint.cs b/osu.Game/Rulesets/Edit/PlacementBlueprint.cs index 2817e26abd..60b979da59 100644 --- a/osu.Game/Rulesets/Edit/PlacementBlueprint.cs +++ b/osu.Game/Rulesets/Edit/PlacementBlueprint.cs @@ -209,9 +209,7 @@ namespace osu.Game.Rulesets.Edit case MouseButtonEvent mouse: // placement blueprints should generally block mouse from reaching underlying components (ie. performing clicks on interface buttons). - // for now, the one exception we want to allow is when using a non-main mouse button when shift is pressed, which is used to trigger object deletion - // while in placement mode. - return mouse.Button == MouseButton.Left || !mouse.ShiftPressed; + return mouse.Button == MouseButton.Left || PlacementActive == PlacementState.Active; default: return false; diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs index 16d11ccd1a..eff6629307 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs @@ -263,7 +263,7 @@ namespace osu.Game.Screens.Edit.Compose.Components /// Whether a selection was performed. internal virtual bool MouseDownSelectionRequested(SelectionBlueprint blueprint, MouseButtonEvent e) { - if (e.ShiftPressed && e.Button == MouseButton.Right) + if (e.Button == MouseButton.Middle || (e.ShiftPressed && e.Button == MouseButton.Right)) { handleQuickDeletion(blueprint); return true; From 9673985e2cc03247afd25ecf80d58774a3c7bbcf Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 5 Aug 2024 14:00:56 +0900 Subject: [PATCH 1808/2493] Add test coverage of right/middle click behaviours with placement blueprints --- .../Editing/TestScenePlacementBlueprint.cs | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/osu.Game.Tests/Visual/Editing/TestScenePlacementBlueprint.cs b/osu.Game.Tests/Visual/Editing/TestScenePlacementBlueprint.cs index e9b442f8dd..772a970b5d 100644 --- a/osu.Game.Tests/Visual/Editing/TestScenePlacementBlueprint.cs +++ b/osu.Game.Tests/Visual/Editing/TestScenePlacementBlueprint.cs @@ -28,6 +28,51 @@ namespace osu.Game.Tests.Visual.Editing private GlobalActionContainer globalActionContainer => this.ChildrenOfType().Single(); + [Test] + public void TestDeleteUsingMiddleMouse() + { + AddStep("select circle placement tool", () => InputManager.Key(Key.Number2)); + AddStep("move mouse to center of playfield", () => InputManager.MoveMouseTo(this.ChildrenOfType().Single())); + AddStep("place circle", () => InputManager.Click(MouseButton.Left)); + + AddAssert("one circle added", () => EditorBeatmap.HitObjects, () => Has.One.Items); + AddStep("delete with middle mouse", () => InputManager.Click(MouseButton.Middle)); + AddAssert("circle removed", () => EditorBeatmap.HitObjects, () => Is.Empty); + } + + [Test] + public void TestDeleteUsingShiftRightClick() + { + AddStep("select circle placement tool", () => InputManager.Key(Key.Number2)); + AddStep("move mouse to center of playfield", () => InputManager.MoveMouseTo(this.ChildrenOfType().Single())); + AddStep("place circle", () => InputManager.Click(MouseButton.Left)); + + AddAssert("one circle added", () => EditorBeatmap.HitObjects, () => Has.One.Items); + AddStep("delete with right mouse", () => + { + InputManager.PressKey(Key.ShiftLeft); + InputManager.Click(MouseButton.Right); + InputManager.ReleaseKey(Key.ShiftLeft); + }); + AddAssert("circle removed", () => EditorBeatmap.HitObjects, () => Is.Empty); + } + + [Test] + public void TestContextMenu() + { + AddStep("select circle placement tool", () => InputManager.Key(Key.Number2)); + AddStep("move mouse to center of playfield", () => InputManager.MoveMouseTo(this.ChildrenOfType().Single())); + AddStep("place circle", () => InputManager.Click(MouseButton.Left)); + + AddAssert("one circle added", () => EditorBeatmap.HitObjects, () => Has.One.Items); + AddStep("delete with right mouse", () => + { + InputManager.Click(MouseButton.Right); + }); + AddAssert("circle not removed", () => EditorBeatmap.HitObjects, () => Has.One.Items); + AddAssert("circle selected", () => EditorBeatmap.SelectedHitObjects, () => Has.One.Items); + } + [Test] public void TestCommitPlacementViaGlobalAction() { From c0814c2749a467004fe772d199d591d1d8f60d30 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 5 Aug 2024 14:24:58 +0900 Subject: [PATCH 1809/2493] Add test of existing slider placement behaviour for safety --- .../Editing/TestScenePlacementBlueprint.cs | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/osu.Game.Tests/Visual/Editing/TestScenePlacementBlueprint.cs b/osu.Game.Tests/Visual/Editing/TestScenePlacementBlueprint.cs index 772a970b5d..8173536ba4 100644 --- a/osu.Game.Tests/Visual/Editing/TestScenePlacementBlueprint.cs +++ b/osu.Game.Tests/Visual/Editing/TestScenePlacementBlueprint.cs @@ -73,6 +73,29 @@ namespace osu.Game.Tests.Visual.Editing AddAssert("circle selected", () => EditorBeatmap.SelectedHitObjects, () => Has.One.Items); } + [Test] + [Solo] + public void TestCommitPlacementViaRightClick() + { + Playfield playfield = null!; + + AddStep("select slider placement tool", () => InputManager.Key(Key.Number3)); + AddStep("move mouse to top left of playfield", () => + { + playfield = this.ChildrenOfType().Single(); + var location = (3 * playfield.ScreenSpaceDrawQuad.TopLeft + playfield.ScreenSpaceDrawQuad.BottomRight) / 4; + InputManager.MoveMouseTo(location); + }); + AddStep("begin placement", () => InputManager.Click(MouseButton.Left)); + AddStep("move mouse to bottom right of playfield", () => + { + var location = (playfield.ScreenSpaceDrawQuad.TopLeft + 3 * playfield.ScreenSpaceDrawQuad.BottomRight) / 4; + InputManager.MoveMouseTo(location); + }); + AddStep("confirm via right click", () => InputManager.Click(MouseButton.Right)); + AddAssert("slider placed", () => EditorBeatmap.HitObjects.Count, () => Is.EqualTo(1)); + } + [Test] public void TestCommitPlacementViaGlobalAction() { From 9c5e29b2c9ec46f0849dab7d186326b5d22573cd Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 5 Aug 2024 16:58:00 +0900 Subject: [PATCH 1810/2493] Fix test being disabled for cases which should pass --- .../UserInterface/TestSceneOsuTooltip.cs | 32 ++++++++----------- 1 file changed, 13 insertions(+), 19 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneOsuTooltip.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneOsuTooltip.cs index 90545885c7..49f376c095 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneOsuTooltip.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneOsuTooltip.cs @@ -21,6 +21,16 @@ namespace osu.Game.Tests.Visual.UserInterface { private TestTooltipContainer container = null!; + private static readonly string[] test_case_tooltip_string = + [ + "Hello!!", + string.Concat(Enumerable.Repeat("Hello ", 100)), + + //TODO: o!f issue: https://github.com/ppy/osu-framework/issues/5007 + //Enable after o!f fixed + // $"H{new string('e', 500)}llo", + ]; + [SetUp] public void SetUp() => Schedule(() => { @@ -50,28 +60,12 @@ namespace osu.Game.Tests.Visual.UserInterface }; }); - private static readonly string[] test_case_tooltip_string = - [ - "Hello!!", - string.Concat(Enumerable.Repeat("Hello ", 100)), - $"H{new string('e', 500)}llo", - ]; - - //TODO: o!f issue: https://github.com/ppy/osu-framework/issues/5007 - //Enable after o!f fixed - [Ignore("o!f issue https://github.com/ppy/osu-framework/issues/5007")] [Test] public void TestTooltipBasic([Values(0, 1, 2)] int index) { - AddStep("Set tooltip content", () => - { - container.TooltipText = test_case_tooltip_string[index]; - }); + AddStep("Set tooltip content", () => container.TooltipText = test_case_tooltip_string[index]); - AddStep("Move to container", () => - { - InputManager.MoveMouseTo(new Vector2(InputManager.ScreenSpaceDrawQuad.Centre.X, InputManager.ScreenSpaceDrawQuad.Centre.Y)); - }); + AddStep("Move mouse to container", () => InputManager.MoveMouseTo(new Vector2(InputManager.ScreenSpaceDrawQuad.Centre.X, InputManager.ScreenSpaceDrawQuad.Centre.Y))); OsuTooltipContainer.OsuTooltip? tooltip = null!; @@ -81,7 +75,7 @@ namespace osu.Game.Tests.Visual.UserInterface return tooltip != null && tooltip.Alpha == 1; }); - AddAssert("Is tooltip obey 500 width limit", () => tooltip != null && tooltip.Width <= 500); + AddAssert("Check tooltip is under width limit", () => tooltip != null && tooltip.Width <= 500); } internal sealed partial class TestTooltipContainer : Container, IHasTooltip From 24a0ead62ee6b1172df605dcbca6978f2031678e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 5 Aug 2024 17:01:00 +0900 Subject: [PATCH 1811/2493] Make tests actually show what value they are testing --- osu.Game.Tests/Visual/UserInterface/TestSceneOsuTooltip.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneOsuTooltip.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneOsuTooltip.cs index 49f376c095..a2e88bfbc9 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneOsuTooltip.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneOsuTooltip.cs @@ -60,10 +60,10 @@ namespace osu.Game.Tests.Visual.UserInterface }; }); - [Test] - public void TestTooltipBasic([Values(0, 1, 2)] int index) + [TestCaseSource(nameof(test_case_tooltip_string))] + public void TestTooltipBasic(string text) { - AddStep("Set tooltip content", () => container.TooltipText = test_case_tooltip_string[index]); + AddStep("Set tooltip content", () => container.TooltipText = text); AddStep("Move mouse to container", () => InputManager.MoveMouseTo(new Vector2(InputManager.ScreenSpaceDrawQuad.Centre.X, InputManager.ScreenSpaceDrawQuad.Centre.Y))); From c37f617e1d620e726268f38fce59d4c0cd9dd866 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 5 Aug 2024 17:21:47 +0900 Subject: [PATCH 1812/2493] Adjust song select info icon size slightly Not going to PR this it's just a minor tweak. --- osu.Game/Screens/Select/BeatmapInfoWedge.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Select/BeatmapInfoWedge.cs b/osu.Game/Screens/Select/BeatmapInfoWedge.cs index 02682c1851..3b0fdc3e47 100644 --- a/osu.Game/Screens/Select/BeatmapInfoWedge.cs +++ b/osu.Game/Screens/Select/BeatmapInfoWedge.cs @@ -490,7 +490,7 @@ namespace osu.Game.Screens.Select RelativeSizeAxes = Axes.Both, Colour = Color4Extensions.FromHex(@"f7dd55"), Icon = FontAwesome.Regular.Circle, - Size = new Vector2(0.8f) + Size = new Vector2(0.7f) }, statistic.CreateIcon().With(i => { @@ -498,7 +498,7 @@ namespace osu.Game.Screens.Select i.Origin = Anchor.Centre; i.RelativeSizeAxes = Axes.Both; i.Colour = Color4Extensions.FromHex(@"f7dd55"); - i.Size = new Vector2(0.64f); + i.Size = new Vector2(0.6f); }), } }, From 6d385c6510855f292583ac56db6116865833cc4d Mon Sep 17 00:00:00 2001 From: jkh675 Date: Mon, 5 Aug 2024 16:31:15 +0800 Subject: [PATCH 1813/2493] Remove the meaningless `OpenMenu` method --- osu.Game/Graphics/Cursor/OsuContextMenuContainer.cs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/osu.Game/Graphics/Cursor/OsuContextMenuContainer.cs b/osu.Game/Graphics/Cursor/OsuContextMenuContainer.cs index 85b24cb6a3..c9d89a9206 100644 --- a/osu.Game/Graphics/Cursor/OsuContextMenuContainer.cs +++ b/osu.Game/Graphics/Cursor/OsuContextMenuContainer.cs @@ -17,11 +17,6 @@ namespace osu.Game.Graphics.Cursor protected override Menu CreateMenu() => menu = new OsuContextMenu(true); - public void ShowMenu() - { - menu.Show(); - } - public void CloseMenu() { menu.Close(); From 75c0c6a5f9dd18ced06b4b05dc9288d892c81580 Mon Sep 17 00:00:00 2001 From: jkh675 Date: Mon, 5 Aug 2024 16:32:49 +0800 Subject: [PATCH 1814/2493] Make the `OsuContextMenu` nullable in `SelectionHandler` --- .../Screens/Edit/Compose/Components/SelectionHandler.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs index 48876278f7..21cd2e891f 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs @@ -60,8 +60,8 @@ namespace osu.Game.Screens.Edit.Compose.Components public SelectionScaleHandler ScaleHandler { get; private set; } - [Resolved] - private OsuContextMenuContainer editorContextMenuContainer { get; set; } + [Resolved(CanBeNull = true)] + protected OsuContextMenuContainer ContextMenuContainer { get; private set; } protected SelectionHandler() { @@ -237,7 +237,7 @@ namespace osu.Game.Screens.Edit.Compose.Components protected void DeselectAll() { SelectedItems.Clear(); - editorContextMenuContainer.CloseMenu(); + ContextMenuContainer?.CloseMenu(); } /// From 3c8d0ce59f93dcc9df88479d147aa9f3eefce581 Mon Sep 17 00:00:00 2001 From: jkh675 Date: Mon, 5 Aug 2024 16:40:31 +0800 Subject: [PATCH 1815/2493] Revert the unit test changes --- .../Editor/TestSceneManiaComposeScreen.cs | 2 -- .../Editor/TestSceneManiaHitObjectComposer.cs | 4 ---- .../Editor/TestSceneOsuDistanceSnapGrid.cs | 2 -- .../Editor/TestSceneTaikoHitObjectComposer.cs | 11 +---------- ...stSceneHitObjectComposerDistanceSnapping.cs | 4 ---- .../Visual/Editing/TestSceneComposeScreen.cs | 2 -- .../Editing/TestSceneHitObjectComposer.cs | 18 +++++------------- 7 files changed, 6 insertions(+), 37 deletions(-) diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaComposeScreen.cs b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaComposeScreen.cs index a2f8670774..8f623d1fc6 100644 --- a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaComposeScreen.cs +++ b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaComposeScreen.cs @@ -10,7 +10,6 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Testing; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Database; -using osu.Game.Graphics.Cursor; using osu.Game.Overlays; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Mania.Beatmaps; @@ -53,7 +52,6 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor (typeof(EditorBeatmap), editorBeatmap), (typeof(IBeatSnapProvider), editorBeatmap), (typeof(OverlayColourProvider), new OverlayColourProvider(OverlayColourScheme.Green)), - (typeof(OsuContextMenuContainer), new OsuContextMenuContainer()), }, Children = new Drawable[] { diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaHitObjectComposer.cs b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaHitObjectComposer.cs index c2364cce1a..d88f488582 100644 --- a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaHitObjectComposer.cs +++ b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaHitObjectComposer.cs @@ -11,7 +11,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Testing; using osu.Framework.Utils; -using osu.Game.Graphics.Cursor; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Edit; @@ -32,9 +31,6 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor { private TestComposer composer; - [Cached] - public readonly OsuContextMenuContainer ContextMenuContainer = new OsuContextMenuContainer(); - [SetUp] public void Setup() => Schedule(() => { diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuDistanceSnapGrid.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuDistanceSnapGrid.cs index b57496673b..b70f932913 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuDistanceSnapGrid.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuDistanceSnapGrid.cs @@ -16,7 +16,6 @@ using osu.Framework.Input; using osu.Framework.Testing; using osu.Framework.Utils; using osu.Game.Beatmaps.ControlPoints; -using osu.Game.Graphics.Cursor; using osu.Game.Overlays; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Osu.Beatmaps; @@ -80,7 +79,6 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor { var dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); dependencies.CacheAs(composer.DistanceSnapProvider); - dependencies.Cache(new OsuContextMenuContainer()); return dependencies; } diff --git a/osu.Game.Rulesets.Taiko.Tests/Editor/TestSceneTaikoHitObjectComposer.cs b/osu.Game.Rulesets.Taiko.Tests/Editor/TestSceneTaikoHitObjectComposer.cs index 7c379eb43c..64a29ce866 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Editor/TestSceneTaikoHitObjectComposer.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Editor/TestSceneTaikoHitObjectComposer.cs @@ -1,12 +1,10 @@ // 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 NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Game.Graphics.Cursor; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Taiko.Beatmaps; using osu.Game.Rulesets.Taiko.Edit; @@ -24,14 +22,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Editor BeatDivisor.Value = 8; EditorClock.Seek(0); - Child = new DependencyProvidingContainer - { - CachedDependencies = new (Type, object)[] - { - (typeof(OsuContextMenuContainer), new OsuContextMenuContainer()), - }, - Child = new TestComposer { RelativeSizeAxes = Axes.Both }, - }; + Child = new TestComposer { RelativeSizeAxes = Axes.Both }; }); [Test] diff --git a/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs b/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs index e5e7d0f8a7..cf8c3c6ef1 100644 --- a/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs +++ b/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs @@ -10,7 +10,6 @@ using osu.Framework.Graphics.Cursor; using osu.Framework.Testing; using osu.Framework.Utils; using osu.Game.Beatmaps.ControlPoints; -using osu.Game.Graphics.Cursor; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu; @@ -32,9 +31,6 @@ namespace osu.Game.Tests.Editing [Cached(typeof(IBeatSnapProvider))] private readonly EditorBeatmap editorBeatmap; - [Cached] - public readonly OsuContextMenuContainer ContextMenuContainer = new OsuContextMenuContainer(); - protected override Container Content { get; } = new PopoverContainer { RelativeSizeAxes = Axes.Both }; public TestSceneHitObjectComposerDistanceSnapping() diff --git a/osu.Game.Tests/Visual/Editing/TestSceneComposeScreen.cs b/osu.Game.Tests/Visual/Editing/TestSceneComposeScreen.cs index 7405433e73..035092ecb7 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneComposeScreen.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneComposeScreen.cs @@ -10,7 +10,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Testing; using osu.Game.Beatmaps.ControlPoints; -using osu.Game.Graphics.Cursor; using osu.Game.Overlays; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Osu; @@ -53,7 +52,6 @@ namespace osu.Game.Tests.Visual.Editing (typeof(EditorBeatmap), editorBeatmap), (typeof(IBeatSnapProvider), editorBeatmap), (typeof(OverlayColourProvider), new OverlayColourProvider(OverlayColourScheme.Green)), - (typeof(OsuContextMenuContainer), new OsuContextMenuContainer()) }, Children = new Drawable[] { diff --git a/osu.Game.Tests/Visual/Editing/TestSceneHitObjectComposer.cs b/osu.Game.Tests/Visual/Editing/TestSceneHitObjectComposer.cs index fac47deec9..c14ef5aaeb 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneHitObjectComposer.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneHitObjectComposer.cs @@ -72,21 +72,13 @@ namespace osu.Game.Tests.Visual.Editing AddStep("Create composer", () => { - Child = new DependencyProvidingContainer + Child = editorBeatmapContainer = new EditorBeatmapContainer(Beatmap.Value) { - RelativeSizeAxes = Axes.Both, - CachedDependencies = new (Type, object)[] + Child = hitObjectComposer = new OsuHitObjectComposer(new OsuRuleset()) { - (typeof(OsuContextMenuContainer), new OsuContextMenuContainer()) - }, - Child = editorBeatmapContainer = new EditorBeatmapContainer(Beatmap.Value) - { - Child = hitObjectComposer = new OsuHitObjectComposer(new OsuRuleset()) - { - // force the composer to fully overlap the playfield area by setting a 4:3 aspect ratio. - FillMode = FillMode.Fit, - FillAspectRatio = 4 / 3f - } + // force the composer to fully overlap the playfield area by setting a 4:3 aspect ratio. + FillMode = FillMode.Fit, + FillAspectRatio = 4 / 3f } }; }); From 59ff549b4d886e07fadc194c357a7313570f8c1b Mon Sep 17 00:00:00 2001 From: jkh675 Date: Mon, 5 Aug 2024 16:46:56 +0800 Subject: [PATCH 1816/2493] Remove unused using --- osu.Game.Tests/Visual/Editing/TestSceneHitObjectComposer.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneHitObjectComposer.cs b/osu.Game.Tests/Visual/Editing/TestSceneHitObjectComposer.cs index c14ef5aaeb..f392841ac7 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneHitObjectComposer.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneHitObjectComposer.cs @@ -3,7 +3,6 @@ #nullable disable -using System; using System.Collections.Generic; using System.Linq; using NUnit.Framework; @@ -13,7 +12,6 @@ using osu.Framework.Graphics.Cursor; using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; -using osu.Game.Graphics.Cursor; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit.Tools; using osu.Game.Rulesets.Objects; From 0a9b11d3a76445cbf56cba4f367964340df91e2a Mon Sep 17 00:00:00 2001 From: Givikap120 Date: Mon, 5 Aug 2024 15:57:02 +0300 Subject: [PATCH 1817/2493] removed default difficulty multiplier --- osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs | 2 +- .../Difficulty/Skills/Flashlight.cs | 4 ++-- .../Difficulty/Skills/OsuStrainSkill.cs | 12 +----------- osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs | 3 +-- 4 files changed, 5 insertions(+), 16 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs index 3f6b22bbb1..f0be2440c1 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs @@ -23,7 +23,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills private double currentStrain; - private double skillMultiplier => 23.55; + private double skillMultiplier => 23.55 * 1.06; private double strainDecayBase => 0.15; private double strainDecay(double ms) => Math.Pow(strainDecayBase, ms / 1000); diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs index 3d6d3f99c1..8caaae665a 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs @@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills hasHiddenMod = mods.Any(m => m is OsuModHidden); } - private double skillMultiplier => 0.052; + private double skillMultiplier => 0.052 * 1.06; private double strainDecayBase => 0.15; private double currentStrain; @@ -41,6 +41,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills return currentStrain; } - public override double DifficultyValue() => GetCurrentStrainPeaks().Sum() * OsuStrainSkill.DEFAULT_DIFFICULTY_MULTIPLIER; + public override double DifficultyValue() => GetCurrentStrainPeaks().Sum(); } } diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs index 4a6328010b..d7ceb63d36 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs @@ -12,12 +12,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills { public abstract class OsuStrainSkill : StrainSkill { - /// - /// The default multiplier applied by to the final difficulty value after all other calculations. - /// May be overridden via . - /// - public const double DEFAULT_DIFFICULTY_MULTIPLIER = 1.06; - /// /// The number of sections with the highest strains, which the peak strain reductions will apply to. /// This is done in order to decrease their impact on the overall difficulty of the map for this skill. @@ -29,10 +23,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills /// protected virtual double ReducedStrainBaseline => 0.75; - /// - /// The final multiplier to be applied to after all other calculations. - /// - protected virtual double DifficultyMultiplier => DEFAULT_DIFFICULTY_MULTIPLIER; protected OsuStrainSkill(Mod[] mods) : base(mods) @@ -65,7 +55,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills weight *= DecayWeight; } - return difficulty * DifficultyMultiplier; + return difficulty; } } } diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs index 40aac013ab..f54f135f63 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs @@ -16,14 +16,13 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills /// public class Speed : OsuStrainSkill { - private double skillMultiplier => 1375; + private double skillMultiplier => 1375 * 1.04; private double strainDecayBase => 0.3; private double currentStrain; private double currentRhythm; protected override int ReducedSectionCount => 5; - protected override double DifficultyMultiplier => 1.04; private readonly List objectStrains = new List(); From 251d00939439c5a27a91c1b7508a1fbc057f915c Mon Sep 17 00:00:00 2001 From: Givikap120 Date: Mon, 5 Aug 2024 16:08:30 +0300 Subject: [PATCH 1818/2493] moved conversion formulas to respective classes --- .../Difficulty/OsuDifficultyCalculator.cs | 6 +++--- .../Difficulty/OsuPerformanceCalculator.cs | 7 ++++--- osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs | 2 ++ osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs | 2 ++ 4 files changed, 11 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs index 007cd977e5..e93475ecff 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs @@ -61,12 +61,12 @@ namespace osu.Game.Rulesets.Osu.Difficulty flashlightRating *= 0.7; } - double baseAimPerformance = Math.Pow(5 * Math.Max(1, aimRating / 0.0675) - 4, 3) / 100000; - double baseSpeedPerformance = Math.Pow(5 * Math.Max(1, speedRating / 0.0675) - 4, 3) / 100000; + double baseAimPerformance = OsuStrainSkill.DifficultyToPerformance(aimRating); + double baseSpeedPerformance = OsuStrainSkill.DifficultyToPerformance(speedRating); double baseFlashlightPerformance = 0.0; if (mods.Any(h => h is OsuModFlashlight)) - baseFlashlightPerformance = Math.Pow(flashlightRating, 2.0) * 25.0; + baseFlashlightPerformance = Flashlight.DifficultyToPerformance(flashlightRating); double basePerformance = Math.Pow( diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index 18a4b8be0c..67d88b6b01 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Linq; using osu.Game.Rulesets.Difficulty; +using osu.Game.Rulesets.Osu.Difficulty.Skills; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; @@ -86,7 +87,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty private double computeAimValue(ScoreInfo score, OsuDifficultyAttributes attributes) { - double aimValue = Math.Pow(5.0 * Math.Max(1.0, attributes.AimDifficulty / 0.0675) - 4.0, 3.0) / 100000.0; + double aimValue = OsuStrainSkill.DifficultyToPerformance(attributes.AimDifficulty); double lengthBonus = 0.95 + 0.4 * Math.Min(1.0, totalHits / 2000.0) + (totalHits > 2000 ? Math.Log10(totalHits / 2000.0) * 0.5 : 0.0); @@ -139,7 +140,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty if (score.Mods.Any(h => h is OsuModRelax)) return 0.0; - double speedValue = Math.Pow(5.0 * Math.Max(1.0, attributes.SpeedDifficulty / 0.0675) - 4.0, 3.0) / 100000.0; + double speedValue = OsuStrainSkill.DifficultyToPerformance(attributes.SpeedDifficulty); double lengthBonus = 0.95 + 0.4 * Math.Min(1.0, totalHits / 2000.0) + (totalHits > 2000 ? Math.Log10(totalHits / 2000.0) * 0.5 : 0.0); @@ -226,7 +227,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty if (!score.Mods.Any(h => h is OsuModFlashlight)) return 0.0; - double flashlightValue = Math.Pow(attributes.FlashlightDifficulty, 2.0) * 25.0; + double flashlightValue = Flashlight.DifficultyToPerformance(attributes.FlashlightDifficulty); // Penalize misses by assessing # of misses relative to the total # of objects. Default a 3% reduction for any # of misses. if (effectiveMissCount > 0) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs index 3d6d3f99c1..939641cae9 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs @@ -42,5 +42,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills } public override double DifficultyValue() => GetCurrentStrainPeaks().Sum() * OsuStrainSkill.DEFAULT_DIFFICULTY_MULTIPLIER; + + public static double DifficultyToPerformance(double difficulty) => 25 * Math.Pow(difficulty, 2); } } diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs index 4a6328010b..c4068ef0d7 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs @@ -67,5 +67,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills return difficulty * DifficultyMultiplier; } + + public static double DifficultyToPerformance(double difficulty) => Math.Pow(5.0 * Math.Max(1.0, difficulty / 0.0675) - 4.0, 3.0) / 100000.0; } } From 8431e62c470dbd1e71a27e0e22e27282344b9255 Mon Sep 17 00:00:00 2001 From: Givikap120 Date: Mon, 5 Aug 2024 16:14:32 +0300 Subject: [PATCH 1819/2493] fixed CI --- osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs index d7ceb63d36..c007c1abd2 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs @@ -23,7 +23,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills /// protected virtual double ReducedStrainBaseline => 0.75; - protected OsuStrainSkill(Mod[] mods) : base(mods) { From e6fc4f67668817e041f4800d2dbe5078afd9a427 Mon Sep 17 00:00:00 2001 From: Givikap120 Date: Mon, 5 Aug 2024 16:33:42 +0300 Subject: [PATCH 1820/2493] merged multipliers --- .../Difficulty/CatchDifficultyCalculator.cs | 4 ++-- .../Difficulty/Skills/Movement.cs | 2 +- .../Difficulty/ManiaDifficultyCalculator.cs | 4 ++-- .../Difficulty/ManiaPerformanceCalculator.cs | 6 ++---- .../Difficulty/TaikoDifficultyCalculator.cs | 18 ++++++++---------- 5 files changed, 15 insertions(+), 19 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs index f12c41a415..0899212b6c 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs @@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty { public class CatchDifficultyCalculator : DifficultyCalculator { - private const double star_scaling_factor = 0.153; + private const double difficulty_multiplier = 4.59; private float halfCatcherWidth; @@ -41,7 +41,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty CatchDifficultyAttributes attributes = new CatchDifficultyAttributes { - StarRating = Math.Sqrt(skills[0].DifficultyValue()) * star_scaling_factor, + StarRating = Math.Sqrt(skills[0].DifficultyValue()) * difficulty_multiplier, 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)), diff --git a/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs b/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs index cfb3fe40be..54b85f1745 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs @@ -15,7 +15,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Skills private const float normalized_hitobject_radius = 41.0f; private const double direction_change_bonus = 21.0; - protected override double SkillMultiplier => 900; + protected override double SkillMultiplier => 1; protected override double StrainDecayBase => 0.2; protected override double DecayWeight => 0.94; diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs index 4190e74e51..efe27e8d6b 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs @@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty { public class ManiaDifficultyCalculator : DifficultyCalculator { - private const double star_scaling_factor = 0.018; + private const double difficulty_multiplier = 0.018; private readonly bool isForCurrentRuleset; private readonly double originalOverallDifficulty; @@ -48,7 +48,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty ManiaDifficultyAttributes attributes = new ManiaDifficultyAttributes { - StarRating = skills[0].DifficultyValue() * star_scaling_factor, + StarRating = skills[0].DifficultyValue() * difficulty_multiplier, Mods = mods, // 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. diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs index d9f9479247..9e5b81bf39 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs @@ -38,9 +38,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty countMiss = score.Statistics.GetValueOrDefault(HitResult.Miss); scoreAccuracy = calculateCustomAccuracy(); - // Arbitrary initial value for scaling pp in order to standardize distributions across game modes. - // The specific number has no intrinsic meaning and can be adjusted as needed. - double multiplier = 8.0; + double multiplier = 1.0; if (score.Mods.Any(m => m is ModNoFail)) multiplier *= 0.75; @@ -59,7 +57,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty private double computeDifficultyValue(ManiaDifficultyAttributes attributes) { - double difficultyValue = Math.Pow(Math.Max(attributes.StarRating - 0.15, 0.05), 2.2) // Star rating to pp curve + double difficultyValue = 8.0 * Math.Pow(Math.Max(attributes.StarRating - 0.15, 0.05), 2.2) // Star rating to pp curve * Math.Max(0, 5 * scoreAccuracy - 4) // From 80% accuracy, 1/20th of total pp is awarded per additional 1% accuracy * (1 + 0.1 * Math.Min(1, totalHits / 1500)); // Length bonus, capped at 1500 notes diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs index 9b746d47ea..28323693d0 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs @@ -21,12 +21,10 @@ namespace osu.Game.Rulesets.Taiko.Difficulty { public class TaikoDifficultyCalculator : DifficultyCalculator { - private const double difficulty_multiplier = 1.35; - - private const double final_multiplier = 0.0625; - private const double rhythm_skill_multiplier = 0.2 * final_multiplier; - private const double colour_skill_multiplier = 0.375 * final_multiplier; - private const double stamina_skill_multiplier = 0.375 * final_multiplier; + private const double difficulty_multiplier = 0.084375; + private const double rhythm_skill_multiplier = 0.2 * difficulty_multiplier; + private const double colour_skill_multiplier = 0.375 * difficulty_multiplier; + private const double stamina_skill_multiplier = 0.375 * difficulty_multiplier; public override int Version => 20221107; @@ -83,11 +81,11 @@ namespace osu.Game.Rulesets.Taiko.Difficulty Rhythm rhythm = (Rhythm)skills.First(x => x is Rhythm); Stamina stamina = (Stamina)skills.First(x => x is Stamina); - double colourRating = colour.DifficultyValue() * colour_skill_multiplier * difficulty_multiplier; - double rhythmRating = rhythm.DifficultyValue() * rhythm_skill_multiplier * difficulty_multiplier; - double staminaRating = stamina.DifficultyValue() * stamina_skill_multiplier * difficulty_multiplier; + double colourRating = colour.DifficultyValue() * colour_skill_multiplier; + double rhythmRating = rhythm.DifficultyValue() * rhythm_skill_multiplier; + double staminaRating = stamina.DifficultyValue() * stamina_skill_multiplier; - double combinedRating = combinedDifficultyValue(rhythm, colour, stamina) * difficulty_multiplier; + double combinedRating = combinedDifficultyValue(rhythm, colour, stamina); double starRating = rescale(combinedRating * 1.4); HitWindows hitWindows = new TaikoHitWindows(); From ac57cdd1b32cde5b2ce156101ac178b7d2ef0fb9 Mon Sep 17 00:00:00 2001 From: Givikap120 Date: Mon, 5 Aug 2024 16:50:06 +0300 Subject: [PATCH 1821/2493] speed eval refactoring --- .../Difficulty/Evaluators/SpeedEvaluator.cs | 18 ++++++++++++++---- .../Difficulty/Skills/Speed.cs | 2 +- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/SpeedEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/SpeedEvaluator.cs index 2df383aaa8..ae7a2542bf 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/SpeedEvaluator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/SpeedEvaluator.cs @@ -10,7 +10,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators { public static class SpeedEvaluator { - private const double single_spacing_threshold = 125; + private const double single_spacing_threshold = 125; // 1.25 circles distance between centers private const double min_speed_bonus = 75; // ~200BPM private const double speed_balancing_factor = 40; @@ -50,16 +50,26 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators // 0.93 is derived from making sure 260bpm OD8 streams aren't nerfed harshly, whilst 0.92 limits the effect of the cap. strainTime /= Math.Clamp((strainTime / osuCurrObj.HitWindowGreat) / 0.93, 0.92, 1); - // derive speedBonus for calculation + // speedBonus will be 1.0 for BPM < 200 double speedBonus = 1.0; + // Add additional scaling bonus for streams/bursts higher than 200bpm if (strainTime < min_speed_bonus) speedBonus = 1 + 0.75 * Math.Pow((min_speed_bonus - strainTime) / speed_balancing_factor, 2); double travelDistance = osuPrevObj?.TravelDistance ?? 0; - double distance = Math.Min(single_spacing_threshold, travelDistance + osuCurrObj.MinimumJumpDistance); + double distance = travelDistance + osuCurrObj.MinimumJumpDistance; - return (speedBonus + speedBonus * Math.Pow(distance / single_spacing_threshold, 3.5)) * doubletapness / strainTime; + // Cap distance at single_spacing_threshold + distance = Math.Min(distance, single_spacing_threshold); + + double distanceBonus = 1 + Math.Pow(distance / single_spacing_threshold, 3.5); + + // Base difficulty with all bonuses + double difficulty = speedBonus * distanceBonus * 1000 / strainTime; + + // Apply penalty if there's doubletappable doubles + return difficulty * doubletapness; } } } diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs index 40aac013ab..f7f081b7ea 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs @@ -16,7 +16,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills ///
public class Speed : OsuStrainSkill { - private double skillMultiplier => 1375; + private double skillMultiplier => 1.375; private double strainDecayBase => 0.3; private double currentStrain; From ace1a572429216c7d1e033d33167ceee6248751b Mon Sep 17 00:00:00 2001 From: Givikap120 Date: Mon, 5 Aug 2024 16:53:06 +0300 Subject: [PATCH 1822/2493] Update SpeedEvaluator.cs --- osu.Game.Rulesets.Osu/Difficulty/Evaluators/SpeedEvaluator.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/SpeedEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/SpeedEvaluator.cs index ae7a2542bf..37fd11391c 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/SpeedEvaluator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/SpeedEvaluator.cs @@ -63,6 +63,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators // Cap distance at single_spacing_threshold distance = Math.Min(distance, single_spacing_threshold); + // Max distance bonus is 2 at single_spacing_threshold double distanceBonus = 1 + Math.Pow(distance / single_spacing_threshold, 3.5); // Base difficulty with all bonuses From 174f4d3ab7d86b77337eab8fb33b75081973e812 Mon Sep 17 00:00:00 2001 From: Givikap120 Date: Mon, 5 Aug 2024 17:02:37 +0300 Subject: [PATCH 1823/2493] fixed CI --- .../Difficulty/ManiaPerformanceCalculator.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs index 9e5b81bf39..778d569cf2 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs @@ -58,8 +58,8 @@ namespace osu.Game.Rulesets.Mania.Difficulty private double computeDifficultyValue(ManiaDifficultyAttributes attributes) { double difficultyValue = 8.0 * Math.Pow(Math.Max(attributes.StarRating - 0.15, 0.05), 2.2) // Star rating to pp curve - * Math.Max(0, 5 * scoreAccuracy - 4) // From 80% accuracy, 1/20th of total pp is awarded per additional 1% accuracy - * (1 + 0.1 * Math.Min(1, totalHits / 1500)); // Length bonus, capped at 1500 notes + * Math.Max(0, 5 * scoreAccuracy - 4) // From 80% accuracy, 1/20th of total pp is awarded per additional 1% accuracy + * (1 + 0.1 * Math.Min(1, totalHits / 1500)); // Length bonus, capped at 1500 notes return difficultyValue; } From 54a8f5b3064499c134fa17734dddaf629ed3628d Mon Sep 17 00:00:00 2001 From: kstefanowicz Date: Mon, 5 Aug 2024 11:06:27 -0400 Subject: [PATCH 1824/2493] Shorten TranslatableString --- osu.Game/Localisation/ChatStrings.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Localisation/ChatStrings.cs b/osu.Game/Localisation/ChatStrings.cs index f7a36d9570..4661f9a53e 100644 --- a/osu.Game/Localisation/ChatStrings.cs +++ b/osu.Game/Localisation/ChatStrings.cs @@ -27,7 +27,7 @@ namespace osu.Game.Localisation /// /// "press enter to type message..." /// - public static LocalisableString InGameInputPlaceholder => new TranslatableString(getKey(@"in_game_input_placeholder"), @"press enter to type message..."); + public static LocalisableString InGameInputPlaceholder => new TranslatableString(getKey(@"in_game_input_placeholder"), @"press enter to chat..."); private static string getKey(string key) => $"{prefix}:{key}"; } From 22ab6f577cae8120a59d4b05e5dea591eba067dc Mon Sep 17 00:00:00 2001 From: jkh675 Date: Tue, 6 Aug 2024 12:37:46 +0800 Subject: [PATCH 1825/2493] Add back the sample into `OsuContextMenu` --- osu.Game/Graphics/Cursor/OsuContextMenuContainer.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/osu.Game/Graphics/Cursor/OsuContextMenuContainer.cs b/osu.Game/Graphics/Cursor/OsuContextMenuContainer.cs index c9d89a9206..d15b1c2ee9 100644 --- a/osu.Game/Graphics/Cursor/OsuContextMenuContainer.cs +++ b/osu.Game/Graphics/Cursor/OsuContextMenuContainer.cs @@ -17,6 +17,11 @@ namespace osu.Game.Graphics.Cursor protected override Menu CreateMenu() => menu = new OsuContextMenu(true); + public OsuContextMenuContainer() + { + AddInternal(samples); + } + public void CloseMenu() { menu.Close(); From cb877b76756a8e6c87f97def38673e43a1048b78 Mon Sep 17 00:00:00 2001 From: jkh675 Date: Tue, 6 Aug 2024 13:09:48 +0800 Subject: [PATCH 1826/2493] Close the menu when selecting other object --- osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs index 21cd2e891f..63112edf30 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs @@ -61,7 +61,7 @@ namespace osu.Game.Screens.Edit.Compose.Components public SelectionScaleHandler ScaleHandler { get; private set; } [Resolved(CanBeNull = true)] - protected OsuContextMenuContainer ContextMenuContainer { get; private set; } + protected OsuContextMenuContainer? ContextMenuContainer { get; private set; } protected SelectionHandler() { @@ -251,6 +251,8 @@ namespace osu.Game.Screens.Edit.Compose.Components SelectedItems.Add(blueprint.Item); selectedBlueprints.Add(blueprint); + + ContextMenuContainer?.CloseMenu(); } /// From c4572ec265eb03217430d899515ecdacce4fcb46 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 6 Aug 2024 15:17:43 +0900 Subject: [PATCH 1827/2493] Sanitise font sizes / weights --- osu.Game/Skinning/LegacyKeyCounter.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Skinning/LegacyKeyCounter.cs b/osu.Game/Skinning/LegacyKeyCounter.cs index 8a182de9b7..609e21b9ff 100644 --- a/osu.Game/Skinning/LegacyKeyCounter.cs +++ b/osu.Game/Skinning/LegacyKeyCounter.cs @@ -63,7 +63,7 @@ namespace osu.Game.Skinning Origin = Anchor.Centre, Text = trigger.Name, Colour = textColour, - Font = OsuFont.GetFont(size: 20), + Font = OsuFont.GetFont(weight: FontWeight.SemiBold), }, }, } @@ -88,7 +88,7 @@ namespace osu.Game.Skinning keyContainer.ScaleTo(0.75f, transition_duration, Easing.Out); keySprite.Colour = ActiveColour; overlayKeyText.Text = CountPresses.Value.ToString(); - overlayKeyText.Font = overlayKeyText.Font.With(weight: FontWeight.Bold); + overlayKeyText.Font = overlayKeyText.Font.With(weight: FontWeight.SemiBold); } protected override void Deactivate(bool forwardPlayback = true) From b91461e661798731b5cc6a59a4fe5be6365451f5 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 6 Aug 2024 15:17:52 +0900 Subject: [PATCH 1828/2493] Refactor + CI fixes --- osu.Game/Graphics/Cursor/OsuContextMenuContainer.cs | 4 ++-- .../Edit/Compose/Components/SelectionHandler.cs | 10 ++++------ 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/osu.Game/Graphics/Cursor/OsuContextMenuContainer.cs b/osu.Game/Graphics/Cursor/OsuContextMenuContainer.cs index d15b1c2ee9..33758c618e 100644 --- a/osu.Game/Graphics/Cursor/OsuContextMenuContainer.cs +++ b/osu.Game/Graphics/Cursor/OsuContextMenuContainer.cs @@ -15,13 +15,13 @@ namespace osu.Game.Graphics.Cursor private OsuContextMenu menu = null!; - protected override Menu CreateMenu() => menu = new OsuContextMenu(true); - public OsuContextMenuContainer() { AddInternal(samples); } + protected override Menu CreateMenu() => menu = new OsuContextMenu(true); + public void CloseMenu() { menu.Close(); diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs index 63112edf30..d3461038bf 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.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.Collections.Generic; using System.Linq; @@ -51,14 +49,14 @@ namespace osu.Game.Screens.Edit.Compose.Components private readonly List> selectedBlueprints; - protected SelectionBox SelectionBox { get; private set; } + protected SelectionBox SelectionBox { get; private set; } = null!; [Resolved(CanBeNull = true)] - protected IEditorChangeHandler ChangeHandler { get; private set; } + protected IEditorChangeHandler? ChangeHandler { get; private set; } - public SelectionRotationHandler RotationHandler { get; private set; } + public SelectionRotationHandler RotationHandler { get; private set; } = null!; - public SelectionScaleHandler ScaleHandler { get; private set; } + public SelectionScaleHandler ScaleHandler { get; private set; } = null!; [Resolved(CanBeNull = true)] protected OsuContextMenuContainer? ContextMenuContainer { get; private set; } From 90395aea13cdbf34c63f330961e004cea2c953e6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 6 Aug 2024 15:43:39 +0900 Subject: [PATCH 1829/2493] Fix incorrect colour fallback handling Adds a note about `GetConfig` being stupid. --- osu.Game/Skinning/ISkin.cs | 3 +++ osu.Game/Skinning/LegacyKeyCounterDisplay.cs | 7 +++---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/osu.Game/Skinning/ISkin.cs b/osu.Game/Skinning/ISkin.cs index fa04dda202..2af1eb8dd8 100644 --- a/osu.Game/Skinning/ISkin.cs +++ b/osu.Game/Skinning/ISkin.cs @@ -47,6 +47,9 @@ namespace osu.Game.Skinning /// /// Retrieve a configuration value. /// + /// + /// Note that while this returns a bindable value, it is not actually updated. + /// Until the API is fixed, just use the received bindable's immediately. /// The requested configuration value. /// A matching value boxed in an , or null if unavailable. IBindable? GetConfig(TLookup lookup) diff --git a/osu.Game/Skinning/LegacyKeyCounterDisplay.cs b/osu.Game/Skinning/LegacyKeyCounterDisplay.cs index 8c652085e4..7e0317851d 100644 --- a/osu.Game/Skinning/LegacyKeyCounterDisplay.cs +++ b/osu.Game/Skinning/LegacyKeyCounterDisplay.cs @@ -9,6 +9,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; using osuTK; +using osuTK.Graphics; namespace osu.Game.Skinning { @@ -53,10 +54,8 @@ namespace osu.Game.Skinning protected override void LoadComplete() { base.LoadComplete(); - source.GetConfig(SkinConfiguration.LegacySetting.InputOverlayText)?.BindValueChanged(v => - { - KeyTextColor = v.NewValue; - }, true); + + KeyTextColor = source.GetConfig(new SkinCustomColourLookup(SkinConfiguration.LegacySetting.InputOverlayText))?.Value ?? Color4.Black; Texture? backgroundTexture = source.GetTexture(@"inputoverlay-background"); From c574551ee0c645eecc0585677ce9b0ec172cee66 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 6 Aug 2024 16:02:36 +0900 Subject: [PATCH 1830/2493] Simplify caching --- .../Visual/Editing/TimelineTestScene.cs | 8 +- .../Cursor/OsuContextMenuContainer.cs | 1 + osu.Game/Overlays/SkinEditor/SkinEditor.cs | 186 +++++++++--------- osu.Game/Screens/Edit/Editor.cs | 184 ++++++++--------- 4 files changed, 188 insertions(+), 191 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TimelineTestScene.cs b/osu.Game.Tests/Visual/Editing/TimelineTestScene.cs index 2323612e89..cb45ad5a07 100644 --- a/osu.Game.Tests/Visual/Editing/TimelineTestScene.cs +++ b/osu.Game.Tests/Visual/Editing/TimelineTestScene.cs @@ -51,7 +51,7 @@ namespace osu.Game.Tests.Visual.Editing Composer.Alpha = 0; - var contextMenuContainer = new OsuContextMenuContainer + Add(new OsuContextMenuContainer { RelativeSizeAxes = Axes.Both, Children = new Drawable[] @@ -75,11 +75,7 @@ namespace osu.Game.Tests.Visual.Editing Origin = Anchor.Centre, } } - }; - - Dependencies.Cache(contextMenuContainer); - - Add(contextMenuContainer); + }); } [SetUpSteps] diff --git a/osu.Game/Graphics/Cursor/OsuContextMenuContainer.cs b/osu.Game/Graphics/Cursor/OsuContextMenuContainer.cs index 33758c618e..7b21a413f7 100644 --- a/osu.Game/Graphics/Cursor/OsuContextMenuContainer.cs +++ b/osu.Game/Graphics/Cursor/OsuContextMenuContainer.cs @@ -8,6 +8,7 @@ using osu.Game.Graphics.UserInterface; namespace osu.Game.Graphics.Cursor { + [Cached(typeof(OsuContextMenuContainer))] public partial class OsuContextMenuContainer : ContextMenuContainer { [Cached] diff --git a/osu.Game/Overlays/SkinEditor/SkinEditor.cs b/osu.Game/Overlays/SkinEditor/SkinEditor.cs index bb2d93f887..6ebc52c6b9 100644 --- a/osu.Game/Overlays/SkinEditor/SkinEditor.cs +++ b/osu.Game/Overlays/SkinEditor/SkinEditor.cs @@ -101,12 +101,6 @@ namespace osu.Game.Overlays.SkinEditor [Resolved] private IDialogOverlay? dialogOverlay { get; set; } - [Cached] - public OsuContextMenuContainer ContextMenuContainer { get; private set; } = new OsuContextMenuContainer - { - RelativeSizeAxes = Axes.Both, - }; - public SkinEditor() { } @@ -121,110 +115,112 @@ namespace osu.Game.Overlays.SkinEditor { RelativeSizeAxes = Axes.Both; - ContextMenuContainer.Child = new GridContainer + AddInternal(new OsuContextMenuContainer { RelativeSizeAxes = Axes.Both, - RowDimensions = new[] + Child = new GridContainer { - new Dimension(GridSizeMode.AutoSize), - new Dimension(GridSizeMode.AutoSize), - new Dimension(), - }, - Content = new[] - { - new Drawable[] + RelativeSizeAxes = Axes.Both, + RowDimensions = new[] { - new Container - { - Name = @"Menu container", - RelativeSizeAxes = Axes.X, - Depth = float.MinValue, - Height = MENU_HEIGHT, - Children = new Drawable[] - { - new EditorMenuBar - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - RelativeSizeAxes = Axes.Both, - Items = new[] - { - new MenuItem(CommonStrings.MenuBarFile) - { - Items = new OsuMenuItem[] - { - new EditorMenuItem(Web.CommonStrings.ButtonsSave, MenuItemType.Standard, () => Save()), - new EditorMenuItem(CommonStrings.Export, MenuItemType.Standard, () => skins.ExportCurrentSkin()) { Action = { Disabled = !RuntimeInfo.IsDesktop } }, - new OsuMenuItemSpacer(), - new EditorMenuItem(CommonStrings.RevertToDefault, MenuItemType.Destructive, () => dialogOverlay?.Push(new RevertConfirmDialog(revert))), - new OsuMenuItemSpacer(), - new EditorMenuItem(CommonStrings.Exit, MenuItemType.Standard, () => skinEditorOverlay?.Hide()), - }, - }, - new MenuItem(CommonStrings.MenuBarEdit) - { - Items = new OsuMenuItem[] - { - undoMenuItem = new EditorMenuItem(CommonStrings.Undo, MenuItemType.Standard, Undo), - redoMenuItem = new EditorMenuItem(CommonStrings.Redo, MenuItemType.Standard, Redo), - new OsuMenuItemSpacer(), - cutMenuItem = new EditorMenuItem(CommonStrings.Cut, MenuItemType.Standard, Cut), - copyMenuItem = new EditorMenuItem(CommonStrings.Copy, MenuItemType.Standard, Copy), - pasteMenuItem = new EditorMenuItem(CommonStrings.Paste, MenuItemType.Standard, Paste), - cloneMenuItem = new EditorMenuItem(CommonStrings.Clone, MenuItemType.Standard, Clone), - } - }, - } - }, - headerText = new OsuTextFlowContainer - { - TextAnchor = Anchor.TopRight, - Padding = new MarginPadding(5), - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - AutoSizeAxes = Axes.X, - RelativeSizeAxes = Axes.Y, - }, - }, - }, + new Dimension(GridSizeMode.AutoSize), + new Dimension(GridSizeMode.AutoSize), + new Dimension(), }, - new Drawable[] + Content = new[] { - new SkinEditorSceneLibrary + new Drawable[] { - RelativeSizeAxes = Axes.X, - }, - }, - new Drawable[] - { - new GridContainer - { - RelativeSizeAxes = Axes.Both, - ColumnDimensions = new[] + new Container { - new Dimension(GridSizeMode.AutoSize), - new Dimension(), - new Dimension(GridSizeMode.AutoSize), - }, - Content = new[] - { - new Drawable[] + Name = @"Menu container", + RelativeSizeAxes = Axes.X, + Depth = float.MinValue, + Height = MENU_HEIGHT, + Children = new Drawable[] { - componentsSidebar = new EditorSidebar(), - content = new Container + new EditorMenuBar { - Depth = float.MaxValue, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, RelativeSizeAxes = Axes.Both, + Items = new[] + { + new MenuItem(CommonStrings.MenuBarFile) + { + Items = new OsuMenuItem[] + { + new EditorMenuItem(Web.CommonStrings.ButtonsSave, MenuItemType.Standard, () => Save()), + new EditorMenuItem(CommonStrings.Export, MenuItemType.Standard, () => skins.ExportCurrentSkin()) { Action = { Disabled = !RuntimeInfo.IsDesktop } }, + new OsuMenuItemSpacer(), + new EditorMenuItem(CommonStrings.RevertToDefault, MenuItemType.Destructive, () => dialogOverlay?.Push(new RevertConfirmDialog(revert))), + new OsuMenuItemSpacer(), + new EditorMenuItem(CommonStrings.Exit, MenuItemType.Standard, () => skinEditorOverlay?.Hide()), + }, + }, + new MenuItem(CommonStrings.MenuBarEdit) + { + Items = new OsuMenuItem[] + { + undoMenuItem = new EditorMenuItem(CommonStrings.Undo, MenuItemType.Standard, Undo), + redoMenuItem = new EditorMenuItem(CommonStrings.Redo, MenuItemType.Standard, Redo), + new OsuMenuItemSpacer(), + cutMenuItem = new EditorMenuItem(CommonStrings.Cut, MenuItemType.Standard, Cut), + copyMenuItem = new EditorMenuItem(CommonStrings.Copy, MenuItemType.Standard, Copy), + pasteMenuItem = new EditorMenuItem(CommonStrings.Paste, MenuItemType.Standard, Paste), + cloneMenuItem = new EditorMenuItem(CommonStrings.Clone, MenuItemType.Standard, Clone), + } + }, + } }, - settingsSidebar = new EditorSidebar(), + headerText = new OsuTextFlowContainer + { + TextAnchor = Anchor.TopRight, + Padding = new MarginPadding(5), + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + AutoSizeAxes = Axes.X, + RelativeSizeAxes = Axes.Y, + }, + }, + }, + }, + new Drawable[] + { + new SkinEditorSceneLibrary + { + RelativeSizeAxes = Axes.X, + }, + }, + new Drawable[] + { + new GridContainer + { + RelativeSizeAxes = Axes.Both, + ColumnDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize), + new Dimension(), + new Dimension(GridSizeMode.AutoSize), + }, + Content = new[] + { + new Drawable[] + { + componentsSidebar = new EditorSidebar(), + content = new Container + { + Depth = float.MaxValue, + RelativeSizeAxes = Axes.Both, + }, + settingsSidebar = new EditorSidebar(), + } } } - } - }, + }, + } } - }; - - AddInternal(ContextMenuContainer); + }); clipboardContent = clipboard.Content.GetBoundCopy(); } diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index b1a066afb7..71d4693ac6 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -6,6 +6,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; using JetBrains.Annotations; using osu.Framework; @@ -76,12 +77,6 @@ namespace osu.Game.Screens.Edit /// public const float WAVEFORM_VISUAL_OFFSET = 20; - [Cached] - public OsuContextMenuContainer ContextMenuContainer { get; private set; } = new OsuContextMenuContainer - { - RelativeSizeAxes = Axes.Both - }; - public override float BackgroundParallaxAmount => 0.1f; public override bool AllowBackButton => false; @@ -165,7 +160,7 @@ namespace osu.Game.Screens.Edit private string lastSavedHash; - private Container screenContainer; + private ScreenContainer screenContainer; [CanBeNull] private readonly EditorLoader loader; @@ -325,108 +320,110 @@ namespace osu.Game.Screens.Edit editorTimelineShowTimingChanges = config.GetBindable(OsuSetting.EditorTimelineShowTimingChanges); editorTimelineShowTicks = config.GetBindable(OsuSetting.EditorTimelineShowTicks); - ContextMenuContainer.AddRange(new Drawable[] + AddInternal(new OsuContextMenuContainer { - new Container + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] { - Name = "Screen container", - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding { Top = 40, Bottom = 50 }, - Child = screenContainer = new Container + new Container { + Name = "Screen container", RelativeSizeAxes = Axes.Both, - } - }, - new Container - { - Name = "Top bar", - RelativeSizeAxes = Axes.X, - Height = 40, - Children = new Drawable[] - { - new EditorMenuBar + Padding = new MarginPadding { Top = 40, Bottom = 50 }, + Child = screenContainer = new ScreenContainer { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, RelativeSizeAxes = Axes.Both, - MaxHeight = 600, - Items = new[] + } + }, + new Container + { + Name = "Top bar", + RelativeSizeAxes = Axes.X, + Height = 40, + Children = new Drawable[] + { + new EditorMenuBar { - new MenuItem(CommonStrings.MenuBarFile) + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + RelativeSizeAxes = Axes.Both, + MaxHeight = 600, + Items = new[] { - Items = createFileMenuItems().ToList() - }, - new MenuItem(CommonStrings.MenuBarEdit) - { - Items = new[] + new MenuItem(CommonStrings.MenuBarFile) { - undoMenuItem = new EditorMenuItem(CommonStrings.Undo, MenuItemType.Standard, Undo), - redoMenuItem = new EditorMenuItem(CommonStrings.Redo, MenuItemType.Standard, Redo), - new OsuMenuItemSpacer(), - cutMenuItem = new EditorMenuItem(CommonStrings.Cut, MenuItemType.Standard, Cut), - copyMenuItem = new EditorMenuItem(CommonStrings.Copy, MenuItemType.Standard, Copy), - pasteMenuItem = new EditorMenuItem(CommonStrings.Paste, MenuItemType.Standard, Paste), - cloneMenuItem = new EditorMenuItem(CommonStrings.Clone, MenuItemType.Standard, Clone), - } - }, - new MenuItem(CommonStrings.MenuBarView) - { - Items = new[] + Items = createFileMenuItems().ToList() + }, + new MenuItem(CommonStrings.MenuBarEdit) { - new MenuItem(EditorStrings.Timeline) + Items = new[] { - Items = - [ - new WaveformOpacityMenuItem(config.GetBindable(OsuSetting.EditorWaveformOpacity)), - new ToggleMenuItem(EditorStrings.TimelineShowTimingChanges) - { - State = { BindTarget = editorTimelineShowTimingChanges } - }, - new ToggleMenuItem(EditorStrings.TimelineShowTicks) - { - State = { BindTarget = editorTimelineShowTicks } - }, - ] - }, - new BackgroundDimMenuItem(editorBackgroundDim), - new ToggleMenuItem(EditorStrings.ShowHitMarkers) + undoMenuItem = new EditorMenuItem(CommonStrings.Undo, MenuItemType.Standard, Undo), + redoMenuItem = new EditorMenuItem(CommonStrings.Redo, MenuItemType.Standard, Redo), + new OsuMenuItemSpacer(), + cutMenuItem = new EditorMenuItem(CommonStrings.Cut, MenuItemType.Standard, Cut), + copyMenuItem = new EditorMenuItem(CommonStrings.Copy, MenuItemType.Standard, Copy), + pasteMenuItem = new EditorMenuItem(CommonStrings.Paste, MenuItemType.Standard, Paste), + cloneMenuItem = new EditorMenuItem(CommonStrings.Clone, MenuItemType.Standard, Clone), + } + }, + new MenuItem(CommonStrings.MenuBarView) + { + Items = new[] { - State = { BindTarget = editorHitMarkers }, - }, - new ToggleMenuItem(EditorStrings.AutoSeekOnPlacement) + new MenuItem(EditorStrings.Timeline) + { + Items = + [ + new WaveformOpacityMenuItem(config.GetBindable(OsuSetting.EditorWaveformOpacity)), + new ToggleMenuItem(EditorStrings.TimelineShowTimingChanges) + { + State = { BindTarget = editorTimelineShowTimingChanges } + }, + new ToggleMenuItem(EditorStrings.TimelineShowTicks) + { + State = { BindTarget = editorTimelineShowTicks } + }, + ] + }, + new BackgroundDimMenuItem(editorBackgroundDim), + new ToggleMenuItem(EditorStrings.ShowHitMarkers) + { + State = { BindTarget = editorHitMarkers }, + }, + new ToggleMenuItem(EditorStrings.AutoSeekOnPlacement) + { + State = { BindTarget = editorAutoSeekOnPlacement }, + }, + new ToggleMenuItem(EditorStrings.LimitedDistanceSnap) + { + State = { BindTarget = editorLimitedDistanceSnap }, + } + } + }, + new MenuItem(EditorStrings.Timing) + { + Items = new MenuItem[] { - State = { BindTarget = editorAutoSeekOnPlacement }, - }, - new ToggleMenuItem(EditorStrings.LimitedDistanceSnap) - { - State = { BindTarget = editorLimitedDistanceSnap }, + new EditorMenuItem(EditorStrings.SetPreviewPointToCurrent, MenuItemType.Standard, SetPreviewPointToCurrentTime) } } - }, - new MenuItem(EditorStrings.Timing) - { - Items = new MenuItem[] - { - new EditorMenuItem(EditorStrings.SetPreviewPointToCurrent, MenuItemType.Standard, SetPreviewPointToCurrentTime) - } } - } - }, - screenSwitcher = new EditorScreenSwitcherControl - { - Anchor = Anchor.BottomRight, - Origin = Anchor.BottomRight, - X = -10, - Current = Mode, + }, + screenSwitcher = new EditorScreenSwitcherControl + { + Anchor = Anchor.BottomRight, + Origin = Anchor.BottomRight, + X = -10, + Current = Mode, + }, }, }, - }, - bottomBar = new BottomBar(), - MutationTracker, + bottomBar = new BottomBar(), + MutationTracker, + } }); - AddInternal(ContextMenuContainer); - changeHandler?.CanUndo.BindValueChanged(v => undoMenuItem.Action.Disabled = !v.NewValue, true); changeHandler?.CanRedo.BindValueChanged(v => redoMenuItem.Action.Disabled = !v.NewValue, true); @@ -1012,7 +1009,7 @@ namespace osu.Game.Screens.Edit throw new InvalidOperationException("Editor menu bar switched to an unsupported mode"); } - LoadComponentAsync(currentScreen, newScreen => + screenContainer.LoadComponentAsync(currentScreen, newScreen => { if (newScreen == currentScreen) { @@ -1390,5 +1387,12 @@ namespace osu.Game.Screens.Edit { } } + + private partial class ScreenContainer : Container + { + public new Task LoadComponentAsync([NotNull] TLoadable component, Action onLoaded = null, CancellationToken cancellation = default, Scheduler scheduler = null) + where TLoadable : Drawable + => base.LoadComponentAsync(component, onLoaded, cancellation, scheduler); + } } } From c26a664b849483d9dc12867cf3bcfa2f0fefd829 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 6 Aug 2024 16:08:42 +0900 Subject: [PATCH 1831/2493] Use InternalChild directly --- osu.Game/Overlays/SkinEditor/SkinEditor.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/SkinEditor/SkinEditor.cs b/osu.Game/Overlays/SkinEditor/SkinEditor.cs index 6ebc52c6b9..a6bb8694ab 100644 --- a/osu.Game/Overlays/SkinEditor/SkinEditor.cs +++ b/osu.Game/Overlays/SkinEditor/SkinEditor.cs @@ -115,7 +115,7 @@ namespace osu.Game.Overlays.SkinEditor { RelativeSizeAxes = Axes.Both; - AddInternal(new OsuContextMenuContainer + InternalChild = new OsuContextMenuContainer { RelativeSizeAxes = Axes.Both, Child = new GridContainer @@ -220,7 +220,7 @@ namespace osu.Game.Overlays.SkinEditor }, } } - }); + }; clipboardContent = clipboard.Content.GetBoundCopy(); } From 41d84ea56b1262f356b8c1d657ad8ae211d5ee77 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 6 Aug 2024 16:11:29 +0900 Subject: [PATCH 1832/2493] Revert all SkinEditor changes (none required) --- osu.Game/Overlays/SkinEditor/SkinEditor.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/SkinEditor/SkinEditor.cs b/osu.Game/Overlays/SkinEditor/SkinEditor.cs index a6bb8694ab..484af34603 100644 --- a/osu.Game/Overlays/SkinEditor/SkinEditor.cs +++ b/osu.Game/Overlays/SkinEditor/SkinEditor.cs @@ -23,6 +23,7 @@ using osu.Framework.Testing; using osu.Game.Database; using osu.Game.Graphics; using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Cursor; using osu.Game.Graphics.UserInterface; using osu.Game.Localisation; using osu.Game.Overlays.Dialog; @@ -32,7 +33,6 @@ using osu.Game.Screens.Edit; using osu.Game.Screens.Edit.Components; using osu.Game.Screens.Edit.Components.Menus; using osu.Game.Skinning; -using osu.Game.Graphics.Cursor; namespace osu.Game.Overlays.SkinEditor { @@ -127,6 +127,7 @@ namespace osu.Game.Overlays.SkinEditor new Dimension(GridSizeMode.AutoSize), new Dimension(), }, + Content = new[] { new Drawable[] From 8619bbb9435c82018ffc50ab15a61430ae77146a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 6 Aug 2024 15:52:03 +0900 Subject: [PATCH 1833/2493] Fix legacy key counter's background being visible when intended to be hidden --- .../Visual/Gameplay/TestSceneHUDOverlay.cs | 11 +++--- .../Screens/Play/ArgonKeyCounterDisplay.cs | 3 +- .../Play/HUD/DefaultKeyCounterDisplay.cs | 3 +- .../Screens/Play/HUD/KeyCounterDisplay.cs | 35 ++++++++++++------- osu.Game/Skinning/LegacyKeyCounterDisplay.cs | 2 +- 5 files changed, 31 insertions(+), 23 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs index f8226eb21d..16b2a54a45 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs @@ -9,7 +9,6 @@ using osu.Framework.Allocation; using osu.Framework.Audio.Track; using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; using osu.Framework.Testing; using osu.Game.Configuration; using osu.Game.Graphics.Containers; @@ -45,7 +44,7 @@ namespace osu.Game.Tests.Visual.Gameplay // best way to check without exposing. private Drawable hideTarget => hudOverlay.ChildrenOfType().First(); - private Drawable keyCounterFlow => hudOverlay.ChildrenOfType().First().ChildrenOfType>().Single(); + private Drawable keyCounterContent => hudOverlay.ChildrenOfType().First().ChildrenOfType().Skip(1).First(); public TestSceneHUDOverlay() { @@ -79,7 +78,7 @@ namespace osu.Game.Tests.Visual.Gameplay AddAssert("showhud is set", () => hudOverlay.ShowHud.Value); AddAssert("hidetarget is visible", () => hideTarget.Alpha, () => Is.GreaterThan(0)); - AddAssert("key counter flow is visible", () => keyCounterFlow.IsPresent); + AddAssert("key counter flow is visible", () => keyCounterContent.IsPresent); AddAssert("pause button is visible", () => hudOverlay.HoldToQuit.IsPresent); } @@ -104,7 +103,7 @@ namespace osu.Game.Tests.Visual.Gameplay AddAssert("pause button is still visible", () => hudOverlay.HoldToQuit.IsPresent); // Key counter flow container should not be affected by this, only the key counter display will be hidden as checked above. - AddAssert("key counter flow not affected", () => keyCounterFlow.IsPresent); + AddAssert("key counter flow not affected", () => keyCounterContent.IsPresent); } [Test] @@ -150,11 +149,11 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("set showhud false", () => hudOverlay.ShowHud.Value = false); AddUntilStep("hidetarget is hidden", () => hideTarget.Alpha, () => Is.LessThanOrEqualTo(0)); - AddUntilStep("key counters hidden", () => !keyCounterFlow.IsPresent); + AddUntilStep("key counters hidden", () => !keyCounterContent.IsPresent); AddStep("set showhud true", () => hudOverlay.ShowHud.Value = true); AddUntilStep("hidetarget is visible", () => hideTarget.Alpha, () => Is.GreaterThan(0)); - AddUntilStep("key counters still hidden", () => !keyCounterFlow.IsPresent); + AddUntilStep("key counters still hidden", () => !keyCounterContent.IsPresent); } [Test] diff --git a/osu.Game/Screens/Play/ArgonKeyCounterDisplay.cs b/osu.Game/Screens/Play/ArgonKeyCounterDisplay.cs index 44b90fcad0..d5044b9f06 100644 --- a/osu.Game/Screens/Play/ArgonKeyCounterDisplay.cs +++ b/osu.Game/Screens/Play/ArgonKeyCounterDisplay.cs @@ -14,11 +14,10 @@ namespace osu.Game.Screens.Play public ArgonKeyCounterDisplay() { - InternalChild = KeyFlow = new FillFlowContainer + Child = KeyFlow = new FillFlowContainer { Direction = FillDirection.Horizontal, AutoSizeAxes = Axes.Both, - Alpha = 0, Spacing = new Vector2(2), }; } diff --git a/osu.Game/Screens/Play/HUD/DefaultKeyCounterDisplay.cs b/osu.Game/Screens/Play/HUD/DefaultKeyCounterDisplay.cs index e0f96d32bc..dfb547453e 100644 --- a/osu.Game/Screens/Play/HUD/DefaultKeyCounterDisplay.cs +++ b/osu.Game/Screens/Play/HUD/DefaultKeyCounterDisplay.cs @@ -16,11 +16,10 @@ namespace osu.Game.Screens.Play.HUD public DefaultKeyCounterDisplay() { - InternalChild = KeyFlow = new FillFlowContainer + Child = KeyFlow = new FillFlowContainer { Direction = FillDirection.Horizontal, AutoSizeAxes = Axes.Both, - Alpha = 0, }; } diff --git a/osu.Game/Screens/Play/HUD/KeyCounterDisplay.cs b/osu.Game/Screens/Play/HUD/KeyCounterDisplay.cs index 0a5d6b763e..a1e90687a8 100644 --- a/osu.Game/Screens/Play/HUD/KeyCounterDisplay.cs +++ b/osu.Game/Screens/Play/HUD/KeyCounterDisplay.cs @@ -15,7 +15,7 @@ namespace osu.Game.Screens.Play.HUD /// /// A flowing display of all gameplay keys. Individual keys can be added using implementations. /// - public abstract partial class KeyCounterDisplay : CompositeDrawable, ISerialisableDrawable + public abstract partial class KeyCounterDisplay : Container, ISerialisableDrawable { /// /// Whether the key counter should be visible regardless of the configuration value. @@ -29,25 +29,22 @@ namespace osu.Game.Screens.Play.HUD private readonly IBindableList triggers = new BindableList(); + protected override Container Content { get; } = new Container + { + Alpha = 0, + AutoSizeAxes = Axes.Both, + }; + [Resolved] private InputCountController controller { get; set; } = null!; private const int duration = 100; - protected void UpdateVisibility() + protected KeyCounterDisplay() { - bool visible = AlwaysVisible.Value || ConfigVisibility.Value; - - // Isolate changing visibility of the key counters from fading this component. - KeyFlow.FadeTo(visible ? 1 : 0, duration); - - // Ensure a valid size is immediately obtained even if partially off-screen - // See https://github.com/ppy/osu/issues/14793. - KeyFlow.AlwaysPresent = visible; + AddInternal(Content); } - protected abstract KeyCounter CreateCounter(InputTrigger trigger); - [BackgroundDependencyLoader] private void load(OsuConfigManager config, DrawableRuleset? drawableRuleset) { @@ -70,6 +67,20 @@ namespace osu.Game.Screens.Play.HUD ConfigVisibility.BindValueChanged(_ => UpdateVisibility(), true); } + protected void UpdateVisibility() + { + bool visible = AlwaysVisible.Value || ConfigVisibility.Value; + + // Isolate changing visibility of the key counters from fading this component. + Content.FadeTo(visible ? 1 : 0, duration); + + // Ensure a valid size is immediately obtained even if partially off-screen + // See https://github.com/ppy/osu/issues/14793. + Content.AlwaysPresent = visible; + } + + protected abstract KeyCounter CreateCounter(InputTrigger trigger); + private void triggersChanged(object? sender, NotifyCollectionChangedEventArgs e) { KeyFlow.Clear(); diff --git a/osu.Game/Skinning/LegacyKeyCounterDisplay.cs b/osu.Game/Skinning/LegacyKeyCounterDisplay.cs index 7e0317851d..fdbd3570f5 100644 --- a/osu.Game/Skinning/LegacyKeyCounterDisplay.cs +++ b/osu.Game/Skinning/LegacyKeyCounterDisplay.cs @@ -26,7 +26,7 @@ namespace osu.Game.Skinning { AutoSizeAxes = Axes.Both; - AddRangeInternal(new Drawable[] + AddRange(new Drawable[] { backgroundSprite = new Sprite { From aae49d362f5551ceab9ef032e5c1f67a887486e3 Mon Sep 17 00:00:00 2001 From: jkh675 Date: Tue, 6 Aug 2024 16:34:36 +0800 Subject: [PATCH 1834/2493] Fix unit test code quality --- .../Editor/TestSceneObjectMerging.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectMerging.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectMerging.cs index dfe950c01e..fd711e543c 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectMerging.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectMerging.cs @@ -34,7 +34,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor }); moveMouseToHitObject(1); - AddAssert("merge option available", () => selectionHandler.ContextMenuItems?.Any(o => o.Text.Value == "Merge selection") == true); + AddAssert("merge option available", () => selectionHandler.ContextMenuItems.Any(o => o.Text.Value == "Merge selection")); mergeSelection(); @@ -198,7 +198,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor }); moveMouseToHitObject(1); - AddAssert("merge option not available", () => selectionHandler.ContextMenuItems?.Length > 0 && selectionHandler.ContextMenuItems.All(o => o.Text.Value != "Merge selection")); + AddAssert("merge option not available", () => selectionHandler.ContextMenuItems.Length > 0 && selectionHandler.ContextMenuItems.All(o => o.Text.Value != "Merge selection")); mergeSelection(); AddAssert("circles not merged", () => circle1 is not null && circle2 is not null && EditorBeatmap.HitObjects.Contains(circle1) && EditorBeatmap.HitObjects.Contains(circle2)); @@ -222,7 +222,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor }); moveMouseToHitObject(1); - AddAssert("merge option available", () => selectionHandler.ContextMenuItems?.Any(o => o.Text.Value == "Merge selection") == true); + AddAssert("merge option available", () => selectionHandler.ContextMenuItems.Any(o => o.Text.Value == "Merge selection")); mergeSelection(); From 725dc4de9b80773fe057059eacab9ae244ed21ac Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 6 Aug 2024 18:17:21 +0900 Subject: [PATCH 1835/2493] Use transformers for per-skin key counter implementation --- .../Legacy/CatchLegacySkinTransformer.cs | 121 +++++--- .../Legacy/OsuLegacySkinTransformer.cs | 270 ++++++++++-------- osu.Game/Skinning/LegacySkin.cs | 126 +++----- 3 files changed, 274 insertions(+), 243 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs b/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs index d1ef47cf17..b102ca990c 100644 --- a/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs @@ -6,6 +6,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Skinning; +using osuTK; using osuTK.Graphics; namespace osu.Game.Rulesets.Catch.Skinning.Legacy @@ -28,11 +29,15 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy public override Drawable? GetDrawableComponent(ISkinComponentLookup lookup) { - if (lookup is SkinComponentsContainerLookup containerLookup) + switch (lookup) { - switch (containerLookup.Target) - { - case SkinComponentsContainerLookup.TargetArea.MainHUDComponents: + case SkinComponentsContainerLookup containerLookup: + if (containerLookup.Target != SkinComponentsContainerLookup.TargetArea.MainHUDComponents) + return base.GetDrawableComponent(lookup); + + // Modifications for global components. + if (containerLookup.Ruleset == null) + { var components = base.GetDrawableComponent(lookup) as Container; if (providesComboCounter && components != null) @@ -44,60 +49,84 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy } return components; - } - } + } - if (lookup is CatchSkinComponentLookup catchSkinComponent) - { - switch (catchSkinComponent.Component) - { - case CatchSkinComponents.Fruit: - if (hasPear) - return new LegacyFruitPiece(); + // Skin has configuration. + if (base.GetDrawableComponent(lookup) is Drawable d) + return d; - return null; + // Our own ruleset components default. + return new DefaultSkinComponentsContainer(container => + { + var keyCounter = container.OfType().FirstOrDefault(); - case CatchSkinComponents.Banana: - if (GetTexture("fruit-bananas") != null) - return new LegacyBananaPiece(); - - return null; - - case CatchSkinComponents.Droplet: - if (GetTexture("fruit-drop") != null) - return new LegacyDropletPiece(); - - return null; - - case CatchSkinComponents.Catcher: - decimal version = GetConfig(SkinConfiguration.LegacySetting.Version)?.Value ?? 1; - - if (version < 2.3m) + if (keyCounter != null) { - if (hasOldStyleCatcherSprite()) - return new LegacyCatcherOld(); + // set the anchor to top right so that it won't squash to the return button to the top + keyCounter.Anchor = Anchor.CentreRight; + keyCounter.Origin = Anchor.CentreRight; + keyCounter.X = 0; + // 340px is the default height inherit from stable + keyCounter.Y = container.ToLocalSpace(new Vector2(0, container.ScreenSpaceDrawQuad.Centre.Y - 340f)).Y; } + }) + { + Children = new Drawable[] + { + new LegacyKeyCounterDisplay(), + } + }; - if (hasNewStyleCatcherSprite()) - return new LegacyCatcherNew(); + case CatchSkinComponentLookup catchSkinComponent: + switch (catchSkinComponent.Component) + { + case CatchSkinComponents.Fruit: + if (hasPear) + return new LegacyFruitPiece(); - return null; + return null; - case CatchSkinComponents.CatchComboCounter: - if (providesComboCounter) - return new LegacyCatchComboCounter(); + case CatchSkinComponents.Banana: + if (GetTexture("fruit-bananas") != null) + return new LegacyBananaPiece(); - return null; + return null; - case CatchSkinComponents.HitExplosion: - if (hasOldStyleCatcherSprite() || hasNewStyleCatcherSprite()) - return new LegacyHitExplosion(); + case CatchSkinComponents.Droplet: + if (GetTexture("fruit-drop") != null) + return new LegacyDropletPiece(); - return null; + return null; - default: - throw new UnsupportedSkinComponentException(lookup); - } + case CatchSkinComponents.Catcher: + decimal version = GetConfig(SkinConfiguration.LegacySetting.Version)?.Value ?? 1; + + if (version < 2.3m) + { + if (hasOldStyleCatcherSprite()) + return new LegacyCatcherOld(); + } + + if (hasNewStyleCatcherSprite()) + return new LegacyCatcherNew(); + + return null; + + case CatchSkinComponents.CatchComboCounter: + if (providesComboCounter) + return new LegacyCatchComboCounter(); + + return null; + + case CatchSkinComponents.HitExplosion: + if (hasOldStyleCatcherSprite() || hasNewStyleCatcherSprite()) + return new LegacyHitExplosion(); + + return null; + + default: + throw new UnsupportedSkinComponentException(lookup); + } } return base.GetDrawableComponent(lookup); diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs index d2ebc68c52..2c2f228fae 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.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.Bindables; using osu.Framework.Graphics; using osu.Game.Rulesets.Osu.Objects; @@ -41,139 +42,178 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy public override Drawable? GetDrawableComponent(ISkinComponentLookup lookup) { - if (lookup is OsuSkinComponentLookup osuComponent) + switch (lookup) { - switch (osuComponent.Component) - { - case OsuSkinComponents.FollowPoint: - return this.GetAnimation("followpoint", true, true, true, startAtCurrentTime: false, maxSize: new Vector2(OsuHitObject.OBJECT_RADIUS * 2, OsuHitObject.OBJECT_RADIUS)); + case SkinComponentsContainerLookup containerLookup: + // Only handle per ruleset defaults here. + if (containerLookup.Ruleset == null) + return base.GetDrawableComponent(lookup); - case OsuSkinComponents.SliderScorePoint: - return this.GetAnimation("sliderscorepoint", false, false, maxSize: OsuHitObject.OBJECT_DIMENSIONS); + // Skin has configuration. + if (base.GetDrawableComponent(lookup) is Drawable d) + return d; - case OsuSkinComponents.SliderFollowCircle: - var followCircleContent = this.GetAnimation("sliderfollowcircle", true, true, true, maxSize: MAX_FOLLOW_CIRCLE_AREA_SIZE); - if (followCircleContent != null) - return new LegacyFollowCircle(followCircleContent); + // Our own ruleset components default. + switch (containerLookup.Target) + { + case SkinComponentsContainerLookup.TargetArea.MainHUDComponents: + return new DefaultSkinComponentsContainer(container => + { + var keyCounter = container.OfType().FirstOrDefault(); - return null; + if (keyCounter != null) + { + // set the anchor to top right so that it won't squash to the return button to the top + keyCounter.Anchor = Anchor.CentreRight; + keyCounter.Origin = Anchor.CentreRight; + keyCounter.X = 0; + // 340px is the default height inherit from stable + keyCounter.Y = container.ToLocalSpace(new Vector2(0, container.ScreenSpaceDrawQuad.Centre.Y - 340f)).Y; + } + }) + { + Children = new Drawable[] + { + new LegacyKeyCounterDisplay(), + } + }; + } - case OsuSkinComponents.SliderBall: - if (GetTexture("sliderb") != null || GetTexture("sliderb0") != null) - return new LegacySliderBall(this); + return null; - return null; + case OsuSkinComponentLookup osuComponent: + switch (osuComponent.Component) + { + case OsuSkinComponents.FollowPoint: + return this.GetAnimation("followpoint", true, true, true, startAtCurrentTime: false, maxSize: new Vector2(OsuHitObject.OBJECT_RADIUS * 2, OsuHitObject.OBJECT_RADIUS)); - case OsuSkinComponents.SliderBody: - if (hasHitCircle.Value) - return new LegacySliderBody(); + case OsuSkinComponents.SliderScorePoint: + return this.GetAnimation("sliderscorepoint", false, false, maxSize: OsuHitObject.OBJECT_DIMENSIONS); - return null; + case OsuSkinComponents.SliderFollowCircle: + var followCircleContent = this.GetAnimation("sliderfollowcircle", true, true, true, maxSize: MAX_FOLLOW_CIRCLE_AREA_SIZE); + if (followCircleContent != null) + return new LegacyFollowCircle(followCircleContent); - case OsuSkinComponents.SliderTailHitCircle: - if (hasHitCircle.Value) - return new LegacyMainCirclePiece("sliderendcircle", false); - - return null; - - case OsuSkinComponents.SliderHeadHitCircle: - if (hasHitCircle.Value) - return new LegacySliderHeadHitCircle(); - - return null; - - case OsuSkinComponents.ReverseArrow: - if (hasHitCircle.Value) - return new LegacyReverseArrow(); - - return null; - - case OsuSkinComponents.HitCircle: - if (hasHitCircle.Value) - return new LegacyMainCirclePiece(); - - return null; - - case OsuSkinComponents.Cursor: - if (GetTexture("cursor") != null) - return new LegacyCursor(this); - - return null; - - case OsuSkinComponents.CursorTrail: - if (GetTexture("cursortrail") != null) - return new LegacyCursorTrail(this); - - return null; - - case OsuSkinComponents.CursorRipple: - if (GetTexture("cursor-ripple") != null) - { - var ripple = this.GetAnimation("cursor-ripple", false, false); - - // In stable this element was scaled down to 50% and opacity 20%, but this makes the elements WAY too big and inflexible. - // If anyone complains about these not being applied, this can be uncommented. - // - // But if no one complains I'd rather fix this in lazer. Wiki documentation doesn't mention size, - // so we might be okay. - // - // if (ripple != null) - // { - // ripple.Scale = new Vector2(0.5f); - // ripple.Alpha = 0.2f; - // } - - return ripple; - } - - return null; - - case OsuSkinComponents.CursorParticles: - if (GetTexture("star2") != null) - return new LegacyCursorParticles(); - - return null; - - case OsuSkinComponents.CursorSmoke: - if (GetTexture("cursor-smoke") != null) - return new LegacySmokeSegment(); - - return null; - - case OsuSkinComponents.HitCircleText: - if (!this.HasFont(LegacyFont.HitCircle)) return null; - const float hitcircle_text_scale = 0.8f; - return new LegacySpriteText(LegacyFont.HitCircle) - { - // stable applies a blanket 0.8x scale to hitcircle fonts - Scale = new Vector2(hitcircle_text_scale), - MaxSizePerGlyph = OsuHitObject.OBJECT_DIMENSIONS * 2 / hitcircle_text_scale, - }; + case OsuSkinComponents.SliderBall: + if (GetTexture("sliderb") != null || GetTexture("sliderb0") != null) + return new LegacySliderBall(this); - case OsuSkinComponents.SpinnerBody: - bool hasBackground = GetTexture("spinner-background") != null; + return null; - if (GetTexture("spinner-top") != null && !hasBackground) - return new LegacyNewStyleSpinner(); - else if (hasBackground) - return new LegacyOldStyleSpinner(); + case OsuSkinComponents.SliderBody: + if (hasHitCircle.Value) + return new LegacySliderBody(); - return null; + return null; - case OsuSkinComponents.ApproachCircle: - if (GetTexture(@"approachcircle") != null) - return new LegacyApproachCircle(); + case OsuSkinComponents.SliderTailHitCircle: + if (hasHitCircle.Value) + return new LegacyMainCirclePiece("sliderendcircle", false); - return null; + return null; - default: - throw new UnsupportedSkinComponentException(lookup); - } + case OsuSkinComponents.SliderHeadHitCircle: + if (hasHitCircle.Value) + return new LegacySliderHeadHitCircle(); + + return null; + + case OsuSkinComponents.ReverseArrow: + if (hasHitCircle.Value) + return new LegacyReverseArrow(); + + return null; + + case OsuSkinComponents.HitCircle: + if (hasHitCircle.Value) + return new LegacyMainCirclePiece(); + + return null; + + case OsuSkinComponents.Cursor: + if (GetTexture("cursor") != null) + return new LegacyCursor(this); + + return null; + + case OsuSkinComponents.CursorTrail: + if (GetTexture("cursortrail") != null) + return new LegacyCursorTrail(this); + + return null; + + case OsuSkinComponents.CursorRipple: + if (GetTexture("cursor-ripple") != null) + { + var ripple = this.GetAnimation("cursor-ripple", false, false); + + // In stable this element was scaled down to 50% and opacity 20%, but this makes the elements WAY too big and inflexible. + // If anyone complains about these not being applied, this can be uncommented. + // + // But if no one complains I'd rather fix this in lazer. Wiki documentation doesn't mention size, + // so we might be okay. + // + // if (ripple != null) + // { + // ripple.Scale = new Vector2(0.5f); + // ripple.Alpha = 0.2f; + // } + + return ripple; + } + + return null; + + case OsuSkinComponents.CursorParticles: + if (GetTexture("star2") != null) + return new LegacyCursorParticles(); + + return null; + + case OsuSkinComponents.CursorSmoke: + if (GetTexture("cursor-smoke") != null) + return new LegacySmokeSegment(); + + return null; + + case OsuSkinComponents.HitCircleText: + if (!this.HasFont(LegacyFont.HitCircle)) + return null; + + const float hitcircle_text_scale = 0.8f; + return new LegacySpriteText(LegacyFont.HitCircle) + { + // stable applies a blanket 0.8x scale to hitcircle fonts + Scale = new Vector2(hitcircle_text_scale), + MaxSizePerGlyph = OsuHitObject.OBJECT_DIMENSIONS * 2 / hitcircle_text_scale, + }; + + case OsuSkinComponents.SpinnerBody: + bool hasBackground = GetTexture("spinner-background") != null; + + if (GetTexture("spinner-top") != null && !hasBackground) + return new LegacyNewStyleSpinner(); + else if (hasBackground) + return new LegacyOldStyleSpinner(); + + return null; + + case OsuSkinComponents.ApproachCircle: + if (GetTexture(@"approachcircle") != null) + return new LegacyApproachCircle(); + + return null; + + default: + throw new UnsupportedSkinComponentException(lookup); + } + + default: + return base.GetDrawableComponent(lookup); } - - return base.GetDrawableComponent(lookup); } public override IBindable? GetConfig(TLookup lookup) diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index 4ca0e3cac0..b1b171eef9 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -23,7 +23,6 @@ using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Scoring; using osu.Game.Screens.Play.HUD; using osu.Game.Screens.Play.HUD.HitErrorMeters; -using osuTK; using osuTK.Graphics; namespace osu.Game.Skinning @@ -356,16 +355,57 @@ namespace osu.Game.Skinning switch (lookup) { case SkinComponentsContainerLookup containerLookup: + // Only handle global level defaults for now. + if (containerLookup.Ruleset != null) + return null; switch (containerLookup.Target) { case SkinComponentsContainerLookup.TargetArea.MainHUDComponents: - return createDefaultHUDComponents(containerLookup); + return new DefaultSkinComponentsContainer(container => + { + var score = container.OfType().FirstOrDefault(); + var accuracy = container.OfType().FirstOrDefault(); - default: - return null; + if (score != null && accuracy != null) + { + accuracy.Y = container.ToLocalSpace(score.ScreenSpaceDrawQuad.BottomRight).Y; + } + + var songProgress = container.OfType().FirstOrDefault(); + + if (songProgress != null && accuracy != null) + { + songProgress.Anchor = Anchor.TopRight; + songProgress.Origin = Anchor.CentreRight; + songProgress.X = -accuracy.ScreenSpaceDeltaToParentSpace(accuracy.ScreenSpaceDrawQuad.Size).X - 18; + songProgress.Y = container.ToLocalSpace(accuracy.ScreenSpaceDrawQuad.TopLeft).Y + (accuracy.ScreenSpaceDeltaToParentSpace(accuracy.ScreenSpaceDrawQuad.Size).Y / 2); + } + + var hitError = container.OfType().FirstOrDefault(); + + if (hitError != null) + { + hitError.Anchor = Anchor.BottomCentre; + hitError.Origin = Anchor.CentreLeft; + hitError.Rotation = -90; + } + }) + { + Children = new Drawable[] + { + new LegacyComboCounter(), + new LegacyScoreCounter(), + new LegacyAccuracyCounter(), + new LegacySongProgress(), + new LegacyHealthDisplay(), + new BarHitErrorMeter(), + } + }; } + return null; + case GameplaySkinComponentLookup resultComponent: // kind of wasteful that we throw this away, but should do for now. @@ -388,84 +428,6 @@ namespace osu.Game.Skinning return null; } - private static DefaultSkinComponentsContainer? createDefaultHUDComponents(SkinComponentsContainerLookup containerLookup) - { - switch (containerLookup.Ruleset?.ShortName) - { - case null: - { - return new DefaultSkinComponentsContainer(container => - { - var score = container.OfType().FirstOrDefault(); - var accuracy = container.OfType().FirstOrDefault(); - - if (score != null && accuracy != null) - { - accuracy.Y = container.ToLocalSpace(score.ScreenSpaceDrawQuad.BottomRight).Y; - } - - var songProgress = container.OfType().FirstOrDefault(); - - if (songProgress != null && accuracy != null) - { - songProgress.Anchor = Anchor.TopRight; - songProgress.Origin = Anchor.CentreRight; - songProgress.X = -accuracy.ScreenSpaceDeltaToParentSpace(accuracy.ScreenSpaceDrawQuad.Size).X - 18; - songProgress.Y = container.ToLocalSpace(accuracy.ScreenSpaceDrawQuad.TopLeft).Y + (accuracy.ScreenSpaceDeltaToParentSpace(accuracy.ScreenSpaceDrawQuad.Size).Y / 2); - } - - var hitError = container.OfType().FirstOrDefault(); - - if (hitError != null) - { - hitError.Anchor = Anchor.BottomCentre; - hitError.Origin = Anchor.CentreLeft; - hitError.Rotation = -90; - } - }) - { - Children = new Drawable[] - { - new LegacyComboCounter(), - new LegacyScoreCounter(), - new LegacyAccuracyCounter(), - new LegacySongProgress(), - new LegacyHealthDisplay(), - new BarHitErrorMeter(), - } - }; - } - - case @"osu": - case @"fruits": - { - return new DefaultSkinComponentsContainer(container => - { - var keyCounter = container.OfType().FirstOrDefault(); - - if (keyCounter != null) - { - // set the anchor to top right so that it won't squash to the return button to the top - keyCounter.Anchor = Anchor.CentreRight; - keyCounter.Origin = Anchor.CentreRight; - keyCounter.X = 0; - // 340px is the default height inherit from stable - keyCounter.Y = container.ToLocalSpace(new Vector2(0, container.ScreenSpaceDrawQuad.Centre.Y - 340f)).Y; - } - }) - { - Children = new Drawable[] - { - new LegacyKeyCounterDisplay(), - } - }; - } - - default: - return null; - } - } - private Texture? getParticleTexture(HitResult result) { switch (result) From f7b45a26defef5a47f3a0ebf6f91acff623661da Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 6 Aug 2024 19:25:23 +0900 Subject: [PATCH 1836/2493] Improve test coverage and segregation --- .../TestSceneModCustomisationPanel.cs | 103 +++++++++++------- 1 file changed, 64 insertions(+), 39 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModCustomisationPanel.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModCustomisationPanel.cs index 16c9c2bc14..1ada5f40ab 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModCustomisationPanel.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModCustomisationPanel.cs @@ -12,6 +12,7 @@ using osu.Game.Overlays.Mods; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.Mods; using osuTK; +using osuTK.Input; namespace osu.Game.Tests.Visual.UserInterface { @@ -27,6 +28,9 @@ namespace osu.Game.Tests.Visual.UserInterface [SetUp] public void SetUp() => Schedule(() => { + SelectedMods.Value = Array.Empty(); + InputManager.MoveMouseTo(Vector2.One); + Child = new Container { RelativeSizeAxes = Axes.Both, @@ -71,66 +75,87 @@ namespace osu.Game.Tests.Visual.UserInterface } [Test] - public void TestHoverExpand() + public void TestHoverDoesNotExpandWhenNoCustomisableMods() { - // Can not expand by hovering when no supported mod - { - AddStep("hover header", () => InputManager.MoveMouseTo(header)); + AddStep("hover header", () => InputManager.MoveMouseTo(header)); - AddAssert("not expanded", () => !panel.Expanded); + checkExpanded(false); - AddStep("hover content", () => InputManager.MoveMouseTo(content)); + AddStep("hover content", () => InputManager.MoveMouseTo(content)); - AddAssert("neither expanded", () => !panel.Expanded); + checkExpanded(false); - AddStep("left from content", () => InputManager.MoveMouseTo(Vector2.One)); - } + AddStep("left from content", () => InputManager.MoveMouseTo(Vector2.One)); + } + [Test] + public void TestHoverExpandsWithCustomisableMods() + { AddStep("add customisable mod", () => { SelectedMods.Value = new[] { new OsuModDoubleTime() }; panel.Enabled.Value = true; }); - // Can expand by hovering when supported mod + AddStep("hover header", () => InputManager.MoveMouseTo(header)); + checkExpanded(true); + + AddStep("move to content", () => InputManager.MoveMouseTo(content)); + checkExpanded(true); + + AddStep("move away", () => InputManager.MoveMouseTo(Vector2.One)); + checkExpanded(false); + + AddStep("hover header", () => InputManager.MoveMouseTo(header)); + checkExpanded(true); + + AddStep("move away", () => InputManager.MoveMouseTo(Vector2.One)); + checkExpanded(false); + } + + [Test] + public void TestExpandedStatePersistsWhenClicked() + { + AddStep("add customisable mod", () => { - AddStep("hover header", () => InputManager.MoveMouseTo(header)); + SelectedMods.Value = new[] { new OsuModDoubleTime() }; + panel.Enabled.Value = true; + }); - AddAssert("expanded", () => panel.Expanded); + AddStep("hover header", () => InputManager.MoveMouseTo(header)); + checkExpanded(true); - AddStep("hover content", () => InputManager.MoveMouseTo(content)); + AddStep("click", () => InputManager.Click(MouseButton.Left)); + checkExpanded(false); + AddStep("click", () => InputManager.Click(MouseButton.Left)); + checkExpanded(true); - AddAssert("still expanded", () => panel.Expanded); - } + AddStep("move away", () => InputManager.MoveMouseTo(Vector2.One)); + checkExpanded(true); - // Will collapse when mouse left from content + AddStep("click", () => InputManager.Click(MouseButton.Left)); + checkExpanded(false); + } + + [Test] + public void TestHoverExpandsAndCollapsesWhenHeaderClicked() + { + AddStep("add customisable mod", () => { - AddStep("left from content", () => InputManager.MoveMouseTo(Vector2.One)); + SelectedMods.Value = new[] { new OsuModDoubleTime() }; + panel.Enabled.Value = true; + }); - AddAssert("not expanded", () => !panel.Expanded); - } + AddStep("hover header", () => InputManager.MoveMouseTo(header)); + checkExpanded(true); - // Will collapse when mouse left from header - { - AddStep("hover header", () => InputManager.MoveMouseTo(header)); + AddStep("click", () => InputManager.Click(MouseButton.Left)); + checkExpanded(false); + } - AddAssert("expanded", () => panel.Expanded); - - AddStep("left from header", () => InputManager.MoveMouseTo(Vector2.One)); - - AddAssert("not expanded", () => !panel.Expanded); - } - - // Not collapse when mouse left if not expanded by hovering - { - AddStep("expand not by hovering", () => panel.Expanded = true); - - AddStep("hover content", () => InputManager.MoveMouseTo(content)); - - AddStep("moust left", () => InputManager.MoveMouseTo(Vector2.One)); - - AddAssert("still expanded", () => panel.Expanded); - } + private void checkExpanded(bool expanded) + { + AddUntilStep(expanded ? "is expanded" : "not expanded", () => panel.Expanded, () => Is.EqualTo(expanded)); } } } From a28913af7a332737a06089fdf99826660f31e702 Mon Sep 17 00:00:00 2001 From: Givikap120 Date: Tue, 6 Aug 2024 14:47:05 +0300 Subject: [PATCH 1837/2493] multiplied numbers in multipliers --- osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs | 2 +- osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs | 2 +- osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs index f0be2440c1..1fbe03395c 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs @@ -23,7 +23,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills private double currentStrain; - private double skillMultiplier => 23.55 * 1.06; + private double skillMultiplier => 24.963; private double strainDecayBase => 0.15; private double strainDecay(double ms) => Math.Pow(strainDecayBase, ms / 1000); diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs index 8caaae665a..9ca6a35d3d 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs @@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills hasHiddenMod = mods.Any(m => m is OsuModHidden); } - private double skillMultiplier => 0.052 * 1.06; + private double skillMultiplier => 0.05512; private double strainDecayBase => 0.15; private double currentStrain; diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs index f54f135f63..93e6e2d1e4 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs @@ -16,7 +16,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills /// public class Speed : OsuStrainSkill { - private double skillMultiplier => 1375 * 1.04; + private double skillMultiplier => 1430; private double strainDecayBase => 0.3; private double currentStrain; From 1aea8e911cad4e86d7108fb8067b3ce30df84975 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 7 Aug 2024 01:33:54 +0900 Subject: [PATCH 1838/2493] Add test coverage of chat mentions --- .../Visual/Online/TestSceneDrawableChannel.cs | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/osu.Game.Tests/Visual/Online/TestSceneDrawableChannel.cs b/osu.Game.Tests/Visual/Online/TestSceneDrawableChannel.cs index dd12ee34ed..6a077708e3 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneDrawableChannel.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneDrawableChannel.cs @@ -33,6 +33,34 @@ namespace osu.Game.Tests.Visual.Online }); } + [Test] + public void TestMention() + { + AddStep("add normal message", () => channel.AddNewMessages( + new Message(1) + { + Sender = new APIUser + { + Id = 2, + Username = "TestUser2" + }, + Content = "Hello how are you today?", + Timestamp = new DateTimeOffset(2021, 12, 11, 13, 33, 24, TimeSpan.Zero) + })); + + AddStep("add mention", () => channel.AddNewMessages( + new Message(2) + { + Sender = new APIUser + { + Id = 2, + Username = "TestUser2" + }, + Content = $"Hello {API.LocalUser.Value.Username} how are you today?", + Timestamp = new DateTimeOffset(2021, 12, 11, 13, 33, 25, TimeSpan.Zero) + })); + } + [Test] public void TestDaySeparators() { From a61bf670d8f2f716f1b8df0b02421f8f67aff886 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 7 Aug 2024 01:18:45 +0900 Subject: [PATCH 1839/2493] Highlight mentions in chat --- osu.Game/Overlays/Chat/ChatLine.cs | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/osu.Game/Overlays/Chat/ChatLine.cs b/osu.Game/Overlays/Chat/ChatLine.cs index 29c6ec2564..3f8862de36 100644 --- a/osu.Game/Overlays/Chat/ChatLine.cs +++ b/osu.Game/Overlays/Chat/ChatLine.cs @@ -18,6 +18,7 @@ using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; +using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Chat; using osuTK; @@ -104,6 +105,8 @@ namespace osu.Game.Overlays.Chat } } + private bool isMention; + /// /// The colour used to paint the author's username. /// @@ -255,12 +258,21 @@ namespace osu.Game.Overlays.Chat private void styleMessageContent(SpriteText text) { text.Shadow = false; - text.Font = text.Font.With(size: FontSize, italics: Message.IsAction); + text.Font = text.Font.With(size: FontSize, italics: Message.IsAction, weight: isMention ? FontWeight.SemiBold : FontWeight.Medium); - bool messageHasColour = Message.IsAction && !string.IsNullOrEmpty(message.Sender.Colour); - text.Colour = messageHasColour ? Color4Extensions.FromHex(message.Sender.Colour) : colourProvider?.Content1 ?? Colour4.White; + Color4 messageColour = colourProvider?.Content1 ?? Colour4.White; + + if (isMention) + messageColour = colourProvider?.Highlight1 ?? Color4.Orange; + else if (Message.IsAction && !string.IsNullOrEmpty(message.Sender.Colour)) + messageColour = Color4Extensions.FromHex(message.Sender.Colour); + + text.Colour = messageColour; } + [Resolved] + private IAPIProvider api { get; set; } = null!; + private void updateMessageContent() { this.FadeTo(message is LocalEchoMessage ? 0.4f : 1.0f, 500, Easing.OutQuint); @@ -280,6 +292,8 @@ namespace osu.Game.Overlays.Chat // remove non-existent channels from the link list message.Links.RemoveAll(link => link.Action == LinkAction.OpenChannel && chatManager?.AvailableChannels.Any(c => c.Name == link.Argument.ToString()) != true); + isMention = MessageNotifier.CheckContainsUsername(message.DisplayContent, api.LocalUser.Value.Username); + drawableContentFlow.Clear(); drawableContentFlow.AddLinks(message.DisplayContent, message.Links); } From 06ff858256f1dac610e1daece956252d4a281d4f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 7 Aug 2024 14:40:52 +0900 Subject: [PATCH 1840/2493] Fix `PresentBeatmap` sometimes favouring an already `DeletePending` beatmap --- osu.Game/OsuGame.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 53b2fd5904..7e4d2ccf39 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -642,10 +642,10 @@ namespace osu.Game Live databasedSet = null; if (beatmap.OnlineID > 0) - databasedSet = BeatmapManager.QueryBeatmapSet(s => s.OnlineID == beatmap.OnlineID); + databasedSet = BeatmapManager.QueryBeatmapSet(s => s.OnlineID == beatmap.OnlineID && !s.DeletePending); if (beatmap is BeatmapSetInfo localBeatmap) - databasedSet ??= BeatmapManager.QueryBeatmapSet(s => s.Hash == localBeatmap.Hash); + databasedSet ??= BeatmapManager.QueryBeatmapSet(s => s.Hash == localBeatmap.Hash && !s.DeletePending); if (databasedSet == null) { From 5a63c25f4956b042259e77a3a42d48103393201a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 7 Aug 2024 14:42:34 +0900 Subject: [PATCH 1841/2493] Fix clicking the beatmap import notification at the daily challenge screen exiting to main menu --- .../DailyChallenge/DailyChallenge.cs | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs index da2d9036c5..c1e1142625 100644 --- a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs @@ -44,7 +44,7 @@ using osuTK; namespace osu.Game.Screens.OnlinePlay.DailyChallenge { [Cached(typeof(IPreviewTrackOwner))] - public partial class DailyChallenge : OsuScreen, IPreviewTrackOwner + public partial class DailyChallenge : OsuScreen, IPreviewTrackOwner, IHandlePresentBeatmap { private readonly Room room; private readonly PlaylistItem playlistItem; @@ -546,5 +546,23 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge if (metadataClient.IsNotNull()) metadataClient.MultiplayerRoomScoreSet -= onRoomScoreSet; } + + [Resolved] + private OsuGame? game { get; set; } + + public void PresentBeatmap(WorkingBeatmap beatmap, RulesetInfo ruleset) + { + if (!this.IsCurrentScreen()) + return; + + // We can only handle the current daily challenge beatmap. + // If the import was for a different beatmap, pass the duty off to global handling. + if (beatmap.BeatmapSetInfo.OnlineID != playlistItem.Beatmap.BeatmapSet!.OnlineID) + { + game?.PresentBeatmap(beatmap.BeatmapSetInfo, b => b.ID == beatmap.BeatmapInfo.ID); + } + + // And if we're handling, we don't really have much to do here. + } } } From fcede9abd786854bc0858fe90db2cbdf320a56ac Mon Sep 17 00:00:00 2001 From: smallketchup82 Date: Wed, 7 Aug 2024 03:34:07 -0400 Subject: [PATCH 1842/2493] Bump velopack version --- 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 7a2bb599fd..d86fcec396 100644 --- a/osu.Desktop/osu.Desktop.csproj +++ b/osu.Desktop/osu.Desktop.csproj @@ -26,7 +26,7 @@ - + From dccf766ff3ba723e737257a0dabdeadc6496423f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 7 Aug 2024 19:01:14 +0900 Subject: [PATCH 1843/2493] Remove obsoleted download setting --- osu.Game/Configuration/OsuConfigManager.cs | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index bef1cf2899..86b8ba98c3 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -67,12 +67,6 @@ namespace osu.Game.Configuration SetDefault(OsuSetting.Username, string.Empty); SetDefault(OsuSetting.Token, string.Empty); -#pragma warning disable CS0618 // Type or member is obsolete - // this default set MUST remain despite the setting being deprecated, because `SetDefault()` calls are implicitly used to declare the type returned for the lookup. - // if this is removed, the setting will be interpreted as a string, and `Migrate()` will fail due to cast failure. - // can be removed 20240618 - SetDefault(OsuSetting.AutomaticallyDownloadWhenSpectating, false); -#pragma warning restore CS0618 // Type or member is obsolete SetDefault(OsuSetting.AutomaticallyDownloadMissingBeatmaps, false); SetDefault(OsuSetting.SavePassword, true).ValueChanged += enabled => @@ -244,12 +238,6 @@ namespace osu.Game.Configuration // migrations can be added here using a condition like: // if (combined < 20220103) { performMigration() } - if (combined < 20230918) - { -#pragma warning disable CS0618 // Type or member is obsolete - SetValue(OsuSetting.AutomaticallyDownloadMissingBeatmaps, Get(OsuSetting.AutomaticallyDownloadWhenSpectating)); // can be removed 20240618 -#pragma warning restore CS0618 // Type or member is obsolete - } } public override TrackedSettings CreateTrackedSettings() @@ -424,9 +412,6 @@ namespace osu.Game.Configuration EditorAutoSeekOnPlacement, DiscordRichPresence, - [Obsolete($"Use {nameof(AutomaticallyDownloadMissingBeatmaps)} instead.")] // can be removed 20240318 - AutomaticallyDownloadWhenSpectating, - ShowOnlineExplicitContent, LastProcessedMetadataId, SafeAreaConsiderations, From 227878b67adf0cdb9789e5f1080ad57e9e9cfad6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 7 Aug 2024 19:01:47 +0900 Subject: [PATCH 1844/2493] Change default for "automatically download beatmaps" to enabled --- osu.Game/Configuration/OsuConfigManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index 86b8ba98c3..d00856dd80 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -67,7 +67,7 @@ namespace osu.Game.Configuration SetDefault(OsuSetting.Username, string.Empty); SetDefault(OsuSetting.Token, string.Empty); - SetDefault(OsuSetting.AutomaticallyDownloadMissingBeatmaps, false); + SetDefault(OsuSetting.AutomaticallyDownloadMissingBeatmaps, true); SetDefault(OsuSetting.SavePassword, true).ValueChanged += enabled => { From 43f1fe350d2874d3e2082db8b11805a21b313fce Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 7 Aug 2024 14:40:52 +0900 Subject: [PATCH 1845/2493] Fix `PresentBeatmap` sometimes favouring an already `DeletePending` beatmap --- osu.Game/OsuGame.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 53b2fd5904..7e4d2ccf39 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -642,10 +642,10 @@ namespace osu.Game Live databasedSet = null; if (beatmap.OnlineID > 0) - databasedSet = BeatmapManager.QueryBeatmapSet(s => s.OnlineID == beatmap.OnlineID); + databasedSet = BeatmapManager.QueryBeatmapSet(s => s.OnlineID == beatmap.OnlineID && !s.DeletePending); if (beatmap is BeatmapSetInfo localBeatmap) - databasedSet ??= BeatmapManager.QueryBeatmapSet(s => s.Hash == localBeatmap.Hash); + databasedSet ??= BeatmapManager.QueryBeatmapSet(s => s.Hash == localBeatmap.Hash && !s.DeletePending); if (databasedSet == null) { From 3c05b975a08dacc644f9877f49f58e40bf92668b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 7 Aug 2024 14:42:34 +0900 Subject: [PATCH 1846/2493] Fix clicking the beatmap import notification at the daily challenge screen exiting to main menu --- .../DailyChallenge/DailyChallenge.cs | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs index da2d9036c5..c1e1142625 100644 --- a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs @@ -44,7 +44,7 @@ using osuTK; namespace osu.Game.Screens.OnlinePlay.DailyChallenge { [Cached(typeof(IPreviewTrackOwner))] - public partial class DailyChallenge : OsuScreen, IPreviewTrackOwner + public partial class DailyChallenge : OsuScreen, IPreviewTrackOwner, IHandlePresentBeatmap { private readonly Room room; private readonly PlaylistItem playlistItem; @@ -546,5 +546,23 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge if (metadataClient.IsNotNull()) metadataClient.MultiplayerRoomScoreSet -= onRoomScoreSet; } + + [Resolved] + private OsuGame? game { get; set; } + + public void PresentBeatmap(WorkingBeatmap beatmap, RulesetInfo ruleset) + { + if (!this.IsCurrentScreen()) + return; + + // We can only handle the current daily challenge beatmap. + // If the import was for a different beatmap, pass the duty off to global handling. + if (beatmap.BeatmapSetInfo.OnlineID != playlistItem.Beatmap.BeatmapSet!.OnlineID) + { + game?.PresentBeatmap(beatmap.BeatmapSetInfo, b => b.ID == beatmap.BeatmapInfo.ID); + } + + // And if we're handling, we don't really have much to do here. + } } } From b081b4771457b901c8cbf31164619c7a211055a4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 5 Aug 2024 17:36:01 +0900 Subject: [PATCH 1847/2493] Add test of daily challenge flow from main menu --- .../Visual/Menus/TestSceneMainMenu.cs | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/osu.Game.Tests/Visual/Menus/TestSceneMainMenu.cs b/osu.Game.Tests/Visual/Menus/TestSceneMainMenu.cs index 57cff38ab0..792b9441fc 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneMainMenu.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneMainMenu.cs @@ -1,11 +1,15 @@ // 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 NUnit.Framework; using osu.Framework.Graphics.Containers; using osu.Framework.Testing; +using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; +using osu.Game.Online.Metadata; +using osu.Game.Online.Rooms; using osu.Game.Overlays; using osu.Game.Screens.Menu; using osuTK.Input; @@ -23,6 +27,47 @@ namespace osu.Game.Tests.Visual.Menus AddStep("disable return to top on idle", () => Game.ChildrenOfType().Single().ReturnToTopOnIdle = false); } + [Test] + public void TestDailyChallenge() + { + AddStep("set up API", () => ((DummyAPIAccess)API).HandleRequest = req => + { + switch (req) + { + case GetRoomRequest getRoomRequest: + if (getRoomRequest.RoomId != 1234) + return false; + + var beatmap = CreateAPIBeatmap(); + beatmap.OnlineID = 1001; + getRoomRequest.TriggerSuccess(new Room + { + RoomID = { Value = 1234 }, + Playlist = + { + new PlaylistItem(beatmap) + }, + EndDate = { Value = DateTimeOffset.Now.AddSeconds(60) } + }); + return true; + + default: + return false; + } + }); + + AddStep("beatmap of the day active", () => Game.ChildrenOfType().Single().DailyChallengeUpdated(new DailyChallengeInfo + { + RoomID = 1234, + })); + + AddStep("enter menu", () => InputManager.Key(Key.P)); + AddStep("enter submenu", () => InputManager.Key(Key.P)); + AddStep("enter daily challenge", () => InputManager.Key(Key.D)); + + AddUntilStep("wait for daily challenge screen", () => Game.ScreenStack.CurrentScreen, Is.TypeOf); + } + [Test] public void TestOnlineMenuBannerTrusted() { From 6870311c1e08f55b5a68210b58f81d8731d6b782 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 5 Aug 2024 18:07:46 +0900 Subject: [PATCH 1848/2493] Remove requirement of specifying `animateOnnter` in `BackgroundScreen` ctor --- .../Background/TestSceneBackgroundScreenDefault.cs | 5 ----- osu.Game/Screens/BackgroundScreen.cs | 12 +++++------- osu.Game/Screens/BackgroundScreenStack.cs | 6 +++++- .../Screens/Backgrounds/BackgroundScreenDefault.cs | 5 ----- osu.Game/Screens/Menu/IntroScreen.cs | 2 +- .../Components/OnlinePlayBackgroundScreen.cs | 1 - .../OnlinePlay/OnlinePlayScreenWaveContainer.cs | 1 - 7 files changed, 11 insertions(+), 21 deletions(-) diff --git a/osu.Game.Tests/Visual/Background/TestSceneBackgroundScreenDefault.cs b/osu.Game.Tests/Visual/Background/TestSceneBackgroundScreenDefault.cs index 37f2ee0b3f..7865d8fef7 100644 --- a/osu.Game.Tests/Visual/Background/TestSceneBackgroundScreenDefault.cs +++ b/osu.Game.Tests/Visual/Background/TestSceneBackgroundScreenDefault.cs @@ -304,11 +304,6 @@ namespace osu.Game.Tests.Visual.Background { private bool? lastLoadTriggerCausedChange; - public TestBackgroundScreenDefault() - : base(false) - { - } - public override bool Next() { bool didChange = base.Next(); diff --git a/osu.Game/Screens/BackgroundScreen.cs b/osu.Game/Screens/BackgroundScreen.cs index 73af9b1bf2..53f0b39ef7 100644 --- a/osu.Game/Screens/BackgroundScreen.cs +++ b/osu.Game/Screens/BackgroundScreen.cs @@ -17,13 +17,12 @@ namespace osu.Game.Screens private const float x_movement_amount = 50; - private readonly bool animateOnEnter; - public override bool IsPresent => base.IsPresent || Scheduler.HasPendingTasks; - protected BackgroundScreen(bool animateOnEnter = true) + public bool AnimateEntry { get; set; } = true; + + protected BackgroundScreen() { - this.animateOnEnter = animateOnEnter; Anchor = Anchor.Centre; Origin = Anchor.Centre; } @@ -53,12 +52,11 @@ namespace osu.Game.Screens public override void OnEntering(ScreenTransitionEvent e) { - if (animateOnEnter) + if (AnimateEntry) { this.FadeOut(); - this.MoveToX(x_movement_amount); - this.FadeIn(TRANSITION_LENGTH, Easing.InOutQuart); + this.MoveToX(x_movement_amount); this.MoveToX(0, TRANSITION_LENGTH, Easing.InOutQuart); } diff --git a/osu.Game/Screens/BackgroundScreenStack.cs b/osu.Game/Screens/BackgroundScreenStack.cs index 99ca383b9f..55cd270581 100644 --- a/osu.Game/Screens/BackgroundScreenStack.cs +++ b/osu.Game/Screens/BackgroundScreenStack.cs @@ -27,10 +27,14 @@ namespace osu.Game.Screens if (screen == null) return false; - if (EqualityComparer.Default.Equals((BackgroundScreen)CurrentScreen, screen)) + bool isFirstScreen = CurrentScreen == null; + screen.AnimateEntry = !isFirstScreen; + + if (EqualityComparer.Default.Equals((BackgroundScreen?)CurrentScreen, screen)) return false; base.Push(screen); + return true; } } diff --git a/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs b/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs index 090e006671..7be96718bd 100644 --- a/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs +++ b/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs @@ -42,11 +42,6 @@ namespace osu.Game.Screens.Backgrounds protected virtual bool AllowStoryboardBackground => true; - public BackgroundScreenDefault(bool animateOnEnter = true) - : base(animateOnEnter) - { - } - [BackgroundDependencyLoader] private void load(IAPIProvider api, SkinManager skinManager, OsuConfigManager config) { diff --git a/osu.Game/Screens/Menu/IntroScreen.cs b/osu.Game/Screens/Menu/IntroScreen.cs index ac7dffc241..0dc54b321f 100644 --- a/osu.Game/Screens/Menu/IntroScreen.cs +++ b/osu.Game/Screens/Menu/IntroScreen.cs @@ -90,7 +90,7 @@ namespace osu.Game.Screens.Menu /// protected bool UsingThemedIntro { get; private set; } - protected override BackgroundScreen CreateBackground() => new BackgroundScreenDefault(false) + protected override BackgroundScreen CreateBackground() => new BackgroundScreenDefault { Colour = Color4.Black }; diff --git a/osu.Game/Screens/OnlinePlay/Components/OnlinePlayBackgroundScreen.cs b/osu.Game/Screens/OnlinePlay/Components/OnlinePlayBackgroundScreen.cs index 014473dfee..ea422f83e3 100644 --- a/osu.Game/Screens/OnlinePlay/Components/OnlinePlayBackgroundScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Components/OnlinePlayBackgroundScreen.cs @@ -21,7 +21,6 @@ namespace osu.Game.Screens.OnlinePlay.Components private PlaylistItemBackground? background; protected OnlinePlayBackgroundScreen() - : base(false) { AddInternal(new Box { diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlayScreenWaveContainer.cs b/osu.Game/Screens/OnlinePlay/OnlinePlayScreenWaveContainer.cs index bfa68d82cd..7898e0845a 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlayScreenWaveContainer.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlayScreenWaveContainer.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. -#nullable disable using osu.Framework.Extensions.Color4Extensions; using osu.Game.Graphics.Containers; From cfccd74441fcf39e4417e6eeb12dd596c6968d2d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 5 Aug 2024 18:07:56 +0900 Subject: [PATCH 1849/2493] Add daily challenge intro sequence --- osu.Game/Screens/Menu/MainMenu.cs | 2 +- .../DailyChallenge/DailyChallengeIntro.cs | 42 +++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) create mode 100644 osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeIntro.cs diff --git a/osu.Game/Screens/Menu/MainMenu.cs b/osu.Game/Screens/Menu/MainMenu.cs index 00b9d909a1..dfe5460aee 100644 --- a/osu.Game/Screens/Menu/MainMenu.cs +++ b/osu.Game/Screens/Menu/MainMenu.cs @@ -150,7 +150,7 @@ namespace osu.Game.Screens.Menu OnPlaylists = () => this.Push(new Playlists()), OnDailyChallenge = room => { - this.Push(new DailyChallenge(room)); + this.Push(new DailyChallengeIntro(room)); }, OnExit = () => { diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeIntro.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeIntro.cs new file mode 100644 index 0000000000..2ca5359f5a --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeIntro.cs @@ -0,0 +1,42 @@ +// 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.Graphics; +using osu.Framework.Screens; +using osu.Game.Graphics.Sprites; +using osu.Game.Online.Rooms; + +namespace osu.Game.Screens.OnlinePlay.DailyChallenge +{ + public partial class DailyChallengeIntro : OsuScreen + { + private readonly Room room; + + public DailyChallengeIntro(Room room) + { + this.room = room; + + ValidForResume = false; + } + + public override void OnEntering(ScreenTransitionEvent e) + { + base.OnEntering(e); + + InternalChildren = new Drawable[] + { + new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Text = "wangs" + } + }; + + Scheduler.AddDelayed(() => + { + this.Push(new DailyChallenge(room)); + }, 2000); + } + } +} From a0615a8f1895cfb7802cd8debe43df858f090a38 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 6 Aug 2024 11:00:04 +0300 Subject: [PATCH 1850/2493] Frenzi's WIP animation --- .../TestSceneDailyChallengeIntro.cs | 89 +++++++ .../DailyChallenge/DailyChallengeIntro.cs | 251 +++++++++++++++++- 2 files changed, 332 insertions(+), 8 deletions(-) create mode 100644 osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeIntro.cs diff --git a/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeIntro.cs b/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeIntro.cs new file mode 100644 index 0000000000..a3541d957e --- /dev/null +++ b/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeIntro.cs @@ -0,0 +1,89 @@ +// 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 NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Screens; +using osu.Framework.Testing; +using osu.Game.Online.API; +using osu.Game.Online.Metadata; +using osu.Game.Online.Rooms; +using osu.Game.Overlays; +using osu.Game.Rulesets.Osu.Mods; +using osu.Game.Tests.Resources; +using osu.Game.Tests.Visual.Metadata; +using osu.Game.Tests.Visual.OnlinePlay; +using CreateRoomRequest = osu.Game.Online.Rooms.CreateRoomRequest; + +namespace osu.Game.Tests.Visual.DailyChallenge +{ + public partial class TestSceneDailyChallengeIntro : OnlinePlayTestScene + { + [Cached(typeof(MetadataClient))] + private TestMetadataClient metadataClient = new TestMetadataClient(); + + [Cached(typeof(INotificationOverlay))] + private NotificationOverlay notificationOverlay = new NotificationOverlay(); + + [BackgroundDependencyLoader] + private void load() + { + base.Content.Add(notificationOverlay); + base.Content.Add(metadataClient); + } + + [Test] + [Solo] + public void TestDailyChallenge() + { + var room = new Room + { + RoomID = { Value = 1234 }, + Name = { Value = "Daily Challenge: June 4, 2024" }, + Playlist = + { + new PlaylistItem(CreateAPIBeatmapSet().Beatmaps.First()) + { + RequiredMods = [new APIMod(new OsuModTraceable())], + AllowedMods = [new APIMod(new OsuModDoubleTime())] + } + }, + EndDate = { Value = DateTimeOffset.Now.AddHours(12) }, + Category = { Value = RoomCategory.DailyChallenge } + }; + + AddStep("add room", () => API.Perform(new CreateRoomRequest(room))); + AddStep("push screen", () => LoadScreen(new Screens.OnlinePlay.DailyChallenge.DailyChallengeIntro(room))); + } + + [Test] + public void TestNotifications() + { + var room = new Room + { + RoomID = { Value = 1234 }, + Name = { Value = "Daily Challenge: June 4, 2024" }, + Playlist = + { + new PlaylistItem(TestResources.CreateTestBeatmapSetInfo().Beatmaps.First()) + { + RequiredMods = [new APIMod(new OsuModTraceable())], + AllowedMods = [new APIMod(new OsuModDoubleTime())] + } + }, + EndDate = { Value = DateTimeOffset.Now.AddHours(12) }, + Category = { Value = RoomCategory.DailyChallenge } + }; + + AddStep("add room", () => API.Perform(new CreateRoomRequest(room))); + AddStep("set daily challenge info", () => metadataClient.DailyChallengeInfo.Value = new DailyChallengeInfo { RoomID = 1234 }); + + Screens.OnlinePlay.DailyChallenge.DailyChallenge screen = null!; + AddStep("push screen", () => LoadScreen(screen = new Screens.OnlinePlay.DailyChallenge.DailyChallenge(room))); + AddUntilStep("wait for screen", () => screen.IsCurrentScreen()); + AddStep("daily challenge ended", () => metadataClient.DailyChallengeInfo.Value = null); + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeIntro.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeIntro.cs index 2ca5359f5a..e10b587270 100644 --- a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeIntro.cs +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeIntro.cs @@ -1,42 +1,277 @@ // 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.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; using osu.Framework.Screens; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.Drawables; +using osu.Game.Extensions; +using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Online.Rooms; +using osu.Game.Overlays; +using osu.Game.Screens.OnlinePlay.Match; +using osuTK; namespace osu.Game.Screens.OnlinePlay.DailyChallenge { public partial class DailyChallengeIntro : OsuScreen { private readonly Room room; + private readonly PlaylistItem item; + + private FillFlowContainer introContent = null!; + private Container topPart = null!; + private Container bottomPart = null!; + private Container beatmapBackground = null!; + private Container beatmapTitle = null!; + + private bool beatmapBackgroundLoaded; + + [Cached] + private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Plum); public DailyChallengeIntro(Room room) { this.room = room; + item = room.Playlist.Single(); ValidForResume = false; } - public override void OnEntering(ScreenTransitionEvent e) - { - base.OnEntering(e); + protected override BackgroundScreen CreateBackground() => new DailyChallengeIntroBackgroundScreen(colourProvider); + [BackgroundDependencyLoader] + private void load() + { InternalChildren = new Drawable[] { - new OsuSpriteText + introContent = new FillFlowContainer { + Alpha = 0f, Anchor = Anchor.Centre, Origin = Anchor.Centre, - Text = "wangs" + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0f, 10f), + Children = new Drawable[] + { + new Container + { + AutoSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Child = topPart = new Container + { + AutoSizeAxes = Axes.Both, + Margin = new MarginPadding { Right = 200f }, + CornerRadius = 10f, + Masking = true, + Shear = new Vector2(OsuGame.SHEAR, 0f), + Children = new Drawable[] + { + new Box + { + Colour = colourProvider.Background3, + RelativeSizeAxes = Axes.Both, + }, + new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Text = "Today's Challenge", + Margin = new MarginPadding { Horizontal = 10f, Vertical = 5f }, + Shear = new Vector2(-OsuGame.SHEAR, 0f), + // Colour = Color4.Black, + Font = OsuFont.GetFont(size: 32, weight: FontWeight.Light, typeface: Typeface.TorusAlternate), + }, + } + }, + }, + beatmapBackground = new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(500f, 150f), + CornerRadius = 20f, + BorderColour = colourProvider.Content2, + BorderThickness = 3f, + Masking = true, + Shear = new Vector2(OsuGame.SHEAR, 0f), + Children = new Drawable[] + { + new Box + { + Colour = colourProvider.Background3, + RelativeSizeAxes = Axes.Both, + }, + } + }, + beatmapTitle = new Container + { + Width = 500f, + Margin = new MarginPadding { Right = 160f * OsuGame.SHEAR }, + AutoSizeAxes = Axes.Y, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + CornerRadius = 10f, + Masking = true, + Shear = new Vector2(OsuGame.SHEAR, 0f), + Children = new Drawable[] + { + new Box + { + Colour = colourProvider.Background3, + RelativeSizeAxes = Axes.Both, + }, + new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Text = item.Beatmap.GetDisplayString(), + Margin = new MarginPadding { Horizontal = 10f, Vertical = 5f }, + Shear = new Vector2(-OsuGame.SHEAR, 0f), + Font = OsuFont.GetFont(size: 24), + }, + } + }, + new Container + { + AutoSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Child = bottomPart = new Container + { + Alpha = 0f, + AlwaysPresent = true, + AutoSizeAxes = Axes.Both, + Margin = new MarginPadding { Left = 210f }, + CornerRadius = 10f, + Masking = true, + Shear = new Vector2(OsuGame.SHEAR, 0f), + Children = new Drawable[] + { + new Box + { + Colour = colourProvider.Background3, + RelativeSizeAxes = Axes.Both, + }, + new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Text = "Sunday, July 28th", + Margin = new MarginPadding { Horizontal = 10f, Vertical = 5f }, + Shear = new Vector2(-OsuGame.SHEAR, 0f), + Font = OsuFont.GetFont(size: 32, weight: FontWeight.Light, typeface: Typeface.TorusAlternate), + }, + } + }, + } + } } }; - Scheduler.AddDelayed(() => + LoadComponentAsync(new OnlineBeatmapSetCover(item.Beatmap.BeatmapSet as IBeatmapSetOnlineInfo) { - this.Push(new DailyChallenge(room)); - }, 2000); + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + FillMode = FillMode.Fit, + Scale = new Vector2(1.2f), + Shear = new Vector2(-OsuGame.SHEAR, 0f), + }, c => + { + beatmapBackground.Add(c); + beatmapBackgroundLoaded = true; + updateAnimationState(); + }); + } + + private bool animationBegan; + + public override void OnEntering(ScreenTransitionEvent e) + { + base.OnEntering(e); + this.FadeInFromZero(400, Easing.OutQuint); + updateAnimationState(); + } + + public override void OnSuspending(ScreenTransitionEvent e) + { + this.FadeOut(200, Easing.OutQuint); + base.OnSuspending(e); + } + + private void updateAnimationState() + { + if (!beatmapBackgroundLoaded || !this.IsCurrentScreen()) + return; + + if (animationBegan) + return; + + beginAnimation(); + animationBegan = true; + } + + private void beginAnimation() + { + introContent.Show(); + + topPart.MoveToX(-500).MoveToX(0, 300, Easing.OutQuint) + .FadeInFromZero(400, Easing.OutQuint); + + bottomPart.MoveToX(500).MoveToX(0, 300, Easing.OutQuint) + .FadeInFromZero(400, Easing.OutQuint); + + this.Delay(400).Schedule(() => + { + introContent.AutoSizeDuration = 200; + introContent.AutoSizeEasing = Easing.OutQuint; + }); + + this.Delay(500).Schedule(() => ApplyToBackground(bs => ((RoomBackgroundScreen)bs).SelectedItem.Value = item)); + + beatmapBackground.FadeOut().Delay(500) + .FadeIn(200, Easing.InQuart); + + beatmapTitle.FadeOut().Delay(500) + .FadeIn(200, Easing.InQuart); + + introContent.Delay(1800).FadeOut(200, Easing.OutQuint) + .OnComplete(_ => + { + if (this.IsCurrentScreen()) + this.Push(new DailyChallenge(room)); + }); + } + + private partial class DailyChallengeIntroBackgroundScreen : RoomBackgroundScreen + { + private readonly OverlayColourProvider colourProvider; + + public DailyChallengeIntroBackgroundScreen(OverlayColourProvider colourProvider) + : base(null) + { + this.colourProvider = colourProvider; + } + + [BackgroundDependencyLoader] + private void load() + { + AddInternal(new Box + { + Depth = float.MinValue, + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background5.Opacity(0.6f), + }); + } } } } From 083fe32d200043a4363be73948753ef3fe470030 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 7 Aug 2024 18:29:15 +0900 Subject: [PATCH 1851/2493] Improve feel of animation --- .../DailyChallenge/DailyChallengeIntro.cs | 279 +++++++++++------- 1 file changed, 172 insertions(+), 107 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeIntro.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeIntro.cs index e10b587270..b85cdbc2d1 100644 --- a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeIntro.cs +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeIntro.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.Linq; using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; @@ -8,6 +9,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Screens; +using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Drawables; using osu.Game.Extensions; @@ -15,8 +17,11 @@ using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Online.Rooms; using osu.Game.Overlays; +using osu.Game.Rulesets; using osu.Game.Screens.OnlinePlay.Match; +using osu.Game.Screens.Play.HUD; using osuTK; +using osuTK.Graphics; namespace osu.Game.Screens.OnlinePlay.DailyChallenge { @@ -25,11 +30,13 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge private readonly Room room; private readonly PlaylistItem item; - private FillFlowContainer introContent = null!; - private Container topPart = null!; - private Container bottomPart = null!; + private Container introContent = null!; + private Container topTitleDisplay = null!; + private Container bottomDateDisplay = null!; private Container beatmapBackground = null!; - private Container beatmapTitle = null!; + private Box flash = null!; + + private FillFlowContainer beatmapContent = null!; private bool beatmapBackgroundLoaded; @@ -49,79 +56,103 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge [BackgroundDependencyLoader] private void load() { + Ruleset ruleset = Ruleset.Value.CreateInstance(); + InternalChildren = new Drawable[] { - introContent = new FillFlowContainer + introContent = new Container { Alpha = 0f, + RelativeSizeAxes = Axes.Both, Anchor = Anchor.Centre, Origin = Anchor.Centre, - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Vertical, - Spacing = new Vector2(0f, 10f), + Shear = new Vector2(OsuGame.SHEAR, 0f), Children = new Drawable[] { - new Container + beatmapContent = new FillFlowContainer { + AlwaysPresent = true, // so we can get the size ahead of time + Direction = FillDirection.Vertical, AutoSizeAxes = Axes.Both, Anchor = Anchor.Centre, Origin = Anchor.Centre, - Child = topPart = new Container - { - AutoSizeAxes = Axes.Both, - Margin = new MarginPadding { Right = 200f }, - CornerRadius = 10f, - Masking = true, - Shear = new Vector2(OsuGame.SHEAR, 0f), - Children = new Drawable[] - { - new Box - { - Colour = colourProvider.Background3, - RelativeSizeAxes = Axes.Both, - }, - new OsuSpriteText - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Text = "Today's Challenge", - Margin = new MarginPadding { Horizontal = 10f, Vertical = 5f }, - Shear = new Vector2(-OsuGame.SHEAR, 0f), - // Colour = Color4.Black, - Font = OsuFont.GetFont(size: 32, weight: FontWeight.Light, typeface: Typeface.TorusAlternate), - }, - } - }, - }, - beatmapBackground = new Container - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Size = new Vector2(500f, 150f), - CornerRadius = 20f, - BorderColour = colourProvider.Content2, - BorderThickness = 3f, - Masking = true, - Shear = new Vector2(OsuGame.SHEAR, 0f), + Alpha = 0, + Scale = new Vector2(0.001f), + Spacing = new Vector2(10), Children = new Drawable[] { - new Box + beatmapBackground = new Container { - Colour = colourProvider.Background3, - RelativeSizeAxes = Axes.Both, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Size = new Vector2(500f, 150f), + CornerRadius = 20f, + BorderColour = colourProvider.Content2, + BorderThickness = 3f, + Masking = true, + Children = new Drawable[] + { + new Box + { + Colour = colourProvider.Background3, + RelativeSizeAxes = Axes.Both, + }, + flash = new Box + { + Colour = Color4.White, + Blending = BlendingParameters.Additive, + RelativeSizeAxes = Axes.Both, + Depth = float.MinValue, + } + } }, + new Container + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Width = 500f, + AutoSizeAxes = Axes.Y, + CornerRadius = 10f, + Masking = true, + Children = new Drawable[] + { + new Box + { + Colour = colourProvider.Background3, + RelativeSizeAxes = Axes.Both, + }, + new TruncatingSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.X, + Shear = new Vector2(-OsuGame.SHEAR, 0f), + Text = item.Beatmap.GetDisplayString(), + Padding = new MarginPadding { Vertical = 5f, Horizontal = 5f }, + Font = OsuFont.GetFont(size: 24), + }, + } + }, + new ModFlowDisplay + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + AutoSizeAxes = Axes.Both, + Shear = new Vector2(-OsuGame.SHEAR, 0f), + Current = + { + Value = item.RequiredMods.Select(m => m.ToMod(ruleset)).ToArray() + }, + } } }, - beatmapTitle = new Container + topTitleDisplay = new Container { - Width = 500f, - Margin = new MarginPadding { Right = 160f * OsuGame.SHEAR }, - AutoSizeAxes = Axes.Y, Anchor = Anchor.Centre, - Origin = Anchor.Centre, + Origin = Anchor.CentreRight, + AutoSizeAxes = Axes.Both, CornerRadius = 10f, Masking = true, - Shear = new Vector2(OsuGame.SHEAR, 0f), Children = new Drawable[] { new Box @@ -133,46 +164,38 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Text = item.Beatmap.GetDisplayString(), + Text = "Today's Challenge", Margin = new MarginPadding { Horizontal = 10f, Vertical = 5f }, Shear = new Vector2(-OsuGame.SHEAR, 0f), - Font = OsuFont.GetFont(size: 24), + Font = OsuFont.GetFont(size: 32, weight: FontWeight.Light, typeface: Typeface.TorusAlternate), }, } }, - new Container + bottomDateDisplay = new Container { - AutoSizeAxes = Axes.Both, Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Child = bottomPart = new Container + Origin = Anchor.CentreLeft, + AutoSizeAxes = Axes.Both, + CornerRadius = 10f, + Masking = true, + Children = new Drawable[] { - Alpha = 0f, - AlwaysPresent = true, - AutoSizeAxes = Axes.Both, - Margin = new MarginPadding { Left = 210f }, - CornerRadius = 10f, - Masking = true, - Shear = new Vector2(OsuGame.SHEAR, 0f), - Children = new Drawable[] + new Box { - new Box - { - Colour = colourProvider.Background3, - RelativeSizeAxes = Axes.Both, - }, - new OsuSpriteText - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Text = "Sunday, July 28th", - Margin = new MarginPadding { Horizontal = 10f, Vertical = 5f }, - Shear = new Vector2(-OsuGame.SHEAR, 0f), - Font = OsuFont.GetFont(size: 32, weight: FontWeight.Light, typeface: Typeface.TorusAlternate), - }, - } - }, - } + Colour = colourProvider.Background3, + RelativeSizeAxes = Axes.Both, + }, + new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Text = room.Name.Value.Split(':', StringSplitOptions.TrimEntries).Last(), + Margin = new MarginPadding { Horizontal = 10f, Vertical = 5f }, + Shear = new Vector2(-OsuGame.SHEAR, 0f), + Font = OsuFont.GetFont(size: 32, weight: FontWeight.Light, typeface: Typeface.TorusAlternate), + }, + } + }, } } }; @@ -188,12 +211,33 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge }, c => { beatmapBackground.Add(c); + beatmapBackgroundLoaded = true; updateAnimationState(); }); } private bool animationBegan; + private bool trackContent; + + private const float initial_v_shift = 32; + private const float final_v_shift = 340; + + protected override void Update() + { + base.Update(); + + if (trackContent) + { + float vShift = initial_v_shift + (beatmapContent.DrawHeight * beatmapContent.Scale.Y) / 2; + + float yPos = (float)Interpolation.DampContinuously(bottomDateDisplay.Y, vShift, 16, Clock.ElapsedFrameTime); + float xPos = (float)Interpolation.DampContinuously(bottomDateDisplay.X, getShearForY(vShift) + final_v_shift, 16, Clock.ElapsedFrameTime); + + topTitleDisplay.Position = new Vector2(-xPos, -yPos); + bottomDateDisplay.Position = new Vector2(xPos, yPos); + } + } public override void OnEntering(ScreenTransitionEvent e) { @@ -222,36 +266,57 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge private void beginAnimation() { - introContent.Show(); + const float v_spacing = 0; - topPart.MoveToX(-500).MoveToX(0, 300, Easing.OutQuint) - .FadeInFromZero(400, Easing.OutQuint); - - bottomPart.MoveToX(500).MoveToX(0, 300, Easing.OutQuint) - .FadeInFromZero(400, Easing.OutQuint); - - this.Delay(400).Schedule(() => + using (BeginDelayedSequence(200)) { - introContent.AutoSizeDuration = 200; - introContent.AutoSizeEasing = Easing.OutQuint; - }); + introContent.Show(); - this.Delay(500).Schedule(() => ApplyToBackground(bs => ((RoomBackgroundScreen)bs).SelectedItem.Value = item)); + topTitleDisplay.MoveToOffset(new Vector2(getShearForY(-initial_v_shift), -initial_v_shift)); + bottomDateDisplay.MoveToOffset(new Vector2(getShearForY(initial_v_shift), initial_v_shift)); - beatmapBackground.FadeOut().Delay(500) - .FadeIn(200, Easing.InQuart); + topTitleDisplay.MoveToX(getShearForY(topTitleDisplay.Y) - 500) + .MoveToX(getShearForY(topTitleDisplay.Y) - v_spacing, 300, Easing.OutQuint) + .FadeInFromZero(400, Easing.OutQuint); - beatmapTitle.FadeOut().Delay(500) - .FadeIn(200, Easing.InQuart); + bottomDateDisplay.MoveToX(getShearForY(bottomDateDisplay.Y) + 500) + .MoveToX(getShearForY(bottomDateDisplay.Y) + v_spacing, 300, Easing.OutQuint) + .FadeInFromZero(400, Easing.OutQuint); - introContent.Delay(1800).FadeOut(200, Easing.OutQuint) - .OnComplete(_ => + using (BeginDelayedSequence(500)) + { + Schedule(() => trackContent = true); + + beatmapContent + .ScaleTo(1f, 500, Easing.InQuint) + .Then() + .ScaleTo(1.1f, 3000); + + using (BeginDelayedSequence(240)) + { + beatmapContent.FadeInFromZero(280, Easing.InQuad); + + flash + .Delay(400) + .FadeOutFromOne(5000, Easing.OutQuint); + + ApplyToBackground(bs => ((RoomBackgroundScreen)bs).SelectedItem.Value = item); + + using (BeginDelayedSequence(2600)) { - if (this.IsCurrentScreen()) - this.Push(new DailyChallenge(room)); - }); + introContent.FadeOut(200, Easing.OutQuint).OnComplete(_ => + { + if (this.IsCurrentScreen()) + this.Push(new DailyChallenge(room)); + }); + } + } + } + } } + private static float getShearForY(float yPos) => yPos * -OsuGame.SHEAR * 2; + private partial class DailyChallengeIntroBackgroundScreen : RoomBackgroundScreen { private readonly OverlayColourProvider colourProvider; From e52d80a41b8883683021090343b4e6e8cab3852d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 7 Aug 2024 18:51:44 +0900 Subject: [PATCH 1852/2493] Add more difficulty information and further tweaks to visuals --- .../DailyChallenge/DailyChallengeIntro.cs | 127 +++++++++++++----- 1 file changed, 97 insertions(+), 30 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeIntro.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeIntro.cs index b85cdbc2d1..073ed1c217 100644 --- a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeIntro.cs +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeIntro.cs @@ -4,6 +4,7 @@ using System; 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; @@ -12,7 +13,6 @@ using osu.Framework.Screens; using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Drawables; -using osu.Game.Extensions; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Online.Rooms; @@ -40,6 +40,14 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge private bool beatmapBackgroundLoaded; + private bool animationBegan; + private bool trackContent; + + private IBindable starDifficulty = null!; + + private const float initial_v_shift = 32; + private const float final_v_shift = 340; + [Cached] private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Plum); @@ -54,10 +62,14 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge protected override BackgroundScreen CreateBackground() => new DailyChallengeIntroBackgroundScreen(colourProvider); [BackgroundDependencyLoader] - private void load() + private void load(BeatmapDifficultyCache difficultyCache) { + const float horizontal_info_size = 500f; + Ruleset ruleset = Ruleset.Value.CreateInstance(); + StarRatingDisplay starRatingDisplay; + InternalChildren = new Drawable[] { introContent = new Container @@ -85,7 +97,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, - Size = new Vector2(500f, 150f), + Size = new Vector2(horizontal_info_size, 150f), CornerRadius = 20f, BorderColour = colourProvider.Content2, BorderThickness = 3f, @@ -110,7 +122,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, - Width = 500f, + Width = horizontal_info_size, AutoSizeAxes = Axes.Y, CornerRadius = 10f, Masking = true, @@ -121,28 +133,82 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge Colour = colourProvider.Background3, RelativeSizeAxes = Axes.Both, }, - new TruncatingSpriteText + new FillFlowContainer { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, RelativeSizeAxes = Axes.X, - Shear = new Vector2(-OsuGame.SHEAR, 0f), - Text = item.Beatmap.GetDisplayString(), - Padding = new MarginPadding { Vertical = 5f, Horizontal = 5f }, - Font = OsuFont.GetFont(size: 24), + AutoSizeAxes = Axes.Y, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Direction = FillDirection.Vertical, + Padding = new MarginPadding(5f), + Children = new Drawable[] + { + new TruncatingSpriteText + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Shear = new Vector2(-OsuGame.SHEAR, 0f), + MaxWidth = horizontal_info_size, + Text = item.Beatmap.BeatmapSet!.Metadata.GetDisplayTitleRomanisable(false), + Padding = new MarginPadding { Horizontal = 5f }, + Font = OsuFont.GetFont(size: 26), + }, + new TruncatingSpriteText + { + Text = $"Difficulty: {item.Beatmap.DifficultyName}", + Font = OsuFont.GetFont(size: 20, italics: true), + MaxWidth = horizontal_info_size, + Shear = new Vector2(-OsuGame.SHEAR, 0f), + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + }, + new TruncatingSpriteText + { + Text = $"by {item.Beatmap.Metadata.Author.Username}", + Font = OsuFont.GetFont(size: 16, italics: true), + MaxWidth = horizontal_info_size, + Shear = new Vector2(-OsuGame.SHEAR, 0f), + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + }, + starRatingDisplay = new StarRatingDisplay(default) + { + Shear = new Vector2(-OsuGame.SHEAR, 0f), + Margin = new MarginPadding(5), + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + } + } }, } }, - new ModFlowDisplay + new Container { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, - AutoSizeAxes = Axes.Both, - Shear = new Vector2(-OsuGame.SHEAR, 0f), - Current = + Width = horizontal_info_size, + AutoSizeAxes = Axes.Y, + CornerRadius = 10f, + Masking = true, + Children = new Drawable[] { - Value = item.RequiredMods.Select(m => m.ToMod(ruleset)).ToArray() - }, + new Box + { + Colour = colourProvider.Background3, + RelativeSizeAxes = Axes.Both, + }, + new ModFlowDisplay + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + AutoSizeAxes = Axes.Both, + Shear = new Vector2(-OsuGame.SHEAR, 0f), + Current = + { + Value = item.RequiredMods.Select(m => m.ToMod(ruleset)).ToArray() + }, + } + } } } }, @@ -200,6 +266,13 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge } }; + starDifficulty = difficultyCache.GetBindableDifficulty(item.Beatmap); + starDifficulty.BindValueChanged(star => + { + if (star.NewValue != null) + starRatingDisplay.Current.Value = star.NewValue.Value; + }, true); + LoadComponentAsync(new OnlineBeatmapSetCover(item.Beatmap.BeatmapSet as IBeatmapSetOnlineInfo) { RelativeSizeAxes = Axes.Both, @@ -217,12 +290,6 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge }); } - private bool animationBegan; - private bool trackContent; - - private const float initial_v_shift = 32; - private const float final_v_shift = 340; - protected override void Update() { base.Update(); @@ -248,7 +315,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge public override void OnSuspending(ScreenTransitionEvent e) { - this.FadeOut(200, Easing.OutQuint); + this.FadeOut(800, Easing.OutQuint); base.OnSuspending(e); } @@ -290,21 +357,21 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge beatmapContent .ScaleTo(1f, 500, Easing.InQuint) .Then() - .ScaleTo(1.1f, 3000); + .ScaleTo(1.02f, 3000); using (BeginDelayedSequence(240)) { beatmapContent.FadeInFromZero(280, Easing.InQuad); - flash - .Delay(400) - .FadeOutFromOne(5000, Easing.OutQuint); + using (BeginDelayedSequence(300)) + Schedule(() => ApplyToBackground(bs => ((RoomBackgroundScreen)bs).SelectedItem.Value = item)); - ApplyToBackground(bs => ((RoomBackgroundScreen)bs).SelectedItem.Value = item); + using (BeginDelayedSequence(400)) + flash.FadeOutFromOne(5000, Easing.OutQuint); using (BeginDelayedSequence(2600)) { - introContent.FadeOut(200, Easing.OutQuint).OnComplete(_ => + Schedule(() => { if (this.IsCurrentScreen()) this.Push(new DailyChallenge(room)); From 775f76f4724f5155efad42860cb1775c8dc279b0 Mon Sep 17 00:00:00 2001 From: kstefanowicz Date: Wed, 7 Aug 2024 07:47:35 -0400 Subject: [PATCH 1853/2493] Have placeholder text change while focused --- osu.Game/Online/Chat/StandAloneChatDisplay.cs | 10 +++++++++- .../OnlinePlay/Multiplayer/GameplayChatDisplay.cs | 7 ++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/osu.Game/Online/Chat/StandAloneChatDisplay.cs b/osu.Game/Online/Chat/StandAloneChatDisplay.cs index 3a094cc074..469ba19fd1 100644 --- a/osu.Game/Online/Chat/StandAloneChatDisplay.cs +++ b/osu.Game/Online/Chat/StandAloneChatDisplay.cs @@ -128,6 +128,9 @@ namespace osu.Game.Online.Chat public partial class ChatTextBox : HistoryTextBox { + public Action Focus; + public Action FocusLost; + protected override bool OnKeyDown(KeyDownEvent e) { // Chat text boxes are generally used in places where they retain focus, but shouldn't block interaction with other @@ -153,13 +156,18 @@ namespace osu.Game.Online.Chat BackgroundFocused = new Color4(10, 10, 10, 255); } + protected override void OnFocus(FocusEvent e) + { + base.OnFocus(e); + Focus?.Invoke(); + } + protected override void OnFocusLost(FocusLostEvent e) { base.OnFocusLost(e); FocusLost?.Invoke(); } - public Action FocusLost; } public partial class StandAloneDrawableChannel : DrawableChannel diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/GameplayChatDisplay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/GameplayChatDisplay.cs index 656071ad43..d1a73457e3 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/GameplayChatDisplay.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/GameplayChatDisplay.cs @@ -42,8 +42,13 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer Background.Alpha = 0.2f; - TextBox.FocusLost = () => expandedFromTextBoxFocus.Value = false; TextBox.PlaceholderText = ChatStrings.InGameInputPlaceholder; + TextBox.Focus = () => TextBox.PlaceholderText = Resources.Localisation.Web.ChatStrings.InputPlaceholder; + TextBox.FocusLost = () => + { + TextBox.PlaceholderText = ChatStrings.InGameInputPlaceholder; + expandedFromTextBoxFocus.Value = false; + }; } protected override bool OnHover(HoverEvent e) => true; // use UI mouse cursor. From 518c1aa5a0a823a88365ca66a4cce8dae9fdbea8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 7 Aug 2024 14:01:30 +0200 Subject: [PATCH 1854/2493] Remove weird `Expanded` / `ExpandedState` duality --- .../TestSceneFreeModSelectOverlay.cs | 4 ++- .../TestSceneModCustomisationPanel.cs | 15 ++++++---- .../TestSceneModSelectOverlay.cs | 8 +++-- .../Overlays/Mods/ModCustomisationHeader.cs | 4 +-- .../Overlays/Mods/ModCustomisationPanel.cs | 30 ++++++++----------- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 12 ++++---- 6 files changed, 39 insertions(+), 34 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneFreeModSelectOverlay.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneFreeModSelectOverlay.cs index 3097d24595..4316653dde 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneFreeModSelectOverlay.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneFreeModSelectOverlay.cs @@ -61,7 +61,9 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("select difficulty adjust", () => freeModSelectOverlay.SelectedMods.Value = new[] { new OsuModDifficultyAdjust() }); AddWaitStep("wait some", 3); - AddAssert("customisation area not expanded", () => !this.ChildrenOfType().Single().Expanded); + AddAssert("customisation area not expanded", + () => this.ChildrenOfType().Single().ExpandedState.Value, + () => Is.EqualTo(ModCustomisationPanel.ModCustomisationPanelState.Collapsed)); } [Test] diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModCustomisationPanel.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModCustomisationPanel.cs index 1ada5f40ab..c2739e1bbd 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModCustomisationPanel.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModCustomisationPanel.cs @@ -55,22 +55,26 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep("set DT", () => { SelectedMods.Value = new[] { new OsuModDoubleTime() }; - panel.Enabled.Value = panel.Expanded = true; + panel.Enabled.Value = true; + panel.ExpandedState.Value = ModCustomisationPanel.ModCustomisationPanelState.Expanded; }); AddStep("set DA", () => { SelectedMods.Value = new Mod[] { new OsuModDifficultyAdjust() }; - panel.Enabled.Value = panel.Expanded = true; + panel.Enabled.Value = true; + panel.ExpandedState.Value = ModCustomisationPanel.ModCustomisationPanelState.Expanded; }); AddStep("set FL+WU+DA+AD", () => { SelectedMods.Value = new Mod[] { new OsuModFlashlight(), new ModWindUp(), new OsuModDifficultyAdjust(), new OsuModApproachDifferent() }; - panel.Enabled.Value = panel.Expanded = true; + panel.Enabled.Value = true; + panel.ExpandedState.Value = ModCustomisationPanel.ModCustomisationPanelState.Expanded; }); AddStep("set empty", () => { SelectedMods.Value = Array.Empty(); - panel.Enabled.Value = panel.Expanded = false; + panel.Enabled.Value = false; + panel.ExpandedState.Value = ModCustomisationPanel.ModCustomisationPanelState.Collapsed; }); } @@ -155,7 +159,8 @@ namespace osu.Game.Tests.Visual.UserInterface private void checkExpanded(bool expanded) { - AddUntilStep(expanded ? "is expanded" : "not expanded", () => panel.Expanded, () => Is.EqualTo(expanded)); + AddUntilStep(expanded ? "is expanded" : "not expanded", () => panel.ExpandedState.Value, + () => expanded ? Is.Not.EqualTo(ModCustomisationPanel.ModCustomisationPanelState.Collapsed) : Is.EqualTo(ModCustomisationPanel.ModCustomisationPanelState.Collapsed)); } } } diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs index 0057582755..f21c64f7fe 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs @@ -999,7 +999,9 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep("press mouse", () => InputManager.PressButton(MouseButton.Left)); AddAssert("search still not focused", () => !this.ChildrenOfType().Single().HasFocus); AddStep("release mouse", () => InputManager.ReleaseButton(MouseButton.Left)); - AddAssert("customisation panel closed by click", () => !this.ChildrenOfType().Single().Expanded); + AddAssert("customisation panel closed by click", + () => this.ChildrenOfType().Single().ExpandedState.Value, + () => Is.EqualTo(ModCustomisationPanel.ModCustomisationPanelState.Collapsed)); if (textSearchStartsActive) AddAssert("search focused", () => this.ChildrenOfType().Single().HasFocus); @@ -1022,7 +1024,9 @@ namespace osu.Game.Tests.Visual.UserInterface private void assertCustomisationToggleState(bool disabled, bool active) { AddUntilStep($"customisation panel is {(disabled ? "" : "not ")}disabled", () => modSelectOverlay.ChildrenOfType().Single().Enabled.Value == !disabled); - AddAssert($"customisation panel is {(active ? "" : "not ")}active", () => modSelectOverlay.ChildrenOfType().Single().Expanded == active); + AddAssert($"customisation panel is {(active ? "" : "not ")}active", + () => modSelectOverlay.ChildrenOfType().Single().ExpandedState.Value, + () => active ? Is.Not.EqualTo(ModCustomisationPanel.ModCustomisationPanelState.Collapsed) : Is.EqualTo(ModCustomisationPanel.ModCustomisationPanelState.Collapsed)); } private T getSelectedMod() where T : Mod => SelectedMods.Value.OfType().Single(); diff --git a/osu.Game/Overlays/Mods/ModCustomisationHeader.cs b/osu.Game/Overlays/Mods/ModCustomisationHeader.cs index 6d0ca7a769..abd48a0dcb 100644 --- a/osu.Game/Overlays/Mods/ModCustomisationHeader.cs +++ b/osu.Game/Overlays/Mods/ModCustomisationHeader.cs @@ -143,8 +143,8 @@ namespace osu.Game.Overlays.Mods { if (Enabled.Value) { - if (!touchedThisFrame) - panel.UpdateHoverExpansion(ModCustomisationPanelState.ExpandedByHover); + if (!touchedThisFrame && panel.ExpandedState.Value == ModCustomisationPanelState.Collapsed) + panel.ExpandedState.Value = ModCustomisationPanelState.ExpandedByHover; } return base.OnHover(e); diff --git a/osu.Game/Overlays/Mods/ModCustomisationPanel.cs b/osu.Game/Overlays/Mods/ModCustomisationPanel.cs index a551081a7b..f13ef2725f 100644 --- a/osu.Game/Overlays/Mods/ModCustomisationPanel.cs +++ b/osu.Game/Overlays/Mods/ModCustomisationPanel.cs @@ -38,13 +38,7 @@ namespace osu.Game.Overlays.Mods public readonly BindableBool Enabled = new BindableBool(); - public readonly Bindable ExpandedState = new Bindable(ModCustomisationPanelState.Collapsed); - - public bool Expanded - { - get => ExpandedState.Value > ModCustomisationPanelState.Collapsed; - set => ExpandedState.Value = value ? ModCustomisationPanelState.Expanded : ModCustomisationPanelState.Collapsed; - } + public readonly Bindable ExpandedState = new Bindable(); public Bindable> SelectedMods { get; } = new Bindable>(Array.Empty()); @@ -52,9 +46,9 @@ namespace osu.Game.Overlays.Mods // Handle{Non}PositionalInput controls whether the panel should act as a blocking layer on the screen. only block when the panel is expanded. // These properties are used because they correctly handle blocking/unblocking hover when mouse is pointing at a drawable outside - // (returning Expanded.Value to OnHover or overriding Block{Non}PositionalInput doesn't work). - public override bool HandlePositionalInput => Expanded; - public override bool HandleNonPositionalInput => Expanded; + // (handling OnHover or overriding Block{Non}PositionalInput doesn't work). + public override bool HandlePositionalInput => ExpandedState.Value != ModCustomisationPanelState.Collapsed; + public override bool HandleNonPositionalInput => ExpandedState.Value != ModCustomisationPanelState.Collapsed; [BackgroundDependencyLoader] private void load() @@ -140,7 +134,7 @@ namespace osu.Game.Overlays.Mods protected override bool OnClick(ClickEvent e) { - Expanded = false; + ExpandedState.Value = ModCustomisationPanelState.Collapsed; return base.OnClick(e); } @@ -153,7 +147,7 @@ namespace osu.Game.Overlays.Mods switch (e.Action) { case GlobalAction.Back: - Expanded = false; + ExpandedState.Value = ModCustomisationPanelState.Collapsed; return true; } @@ -168,7 +162,7 @@ namespace osu.Game.Overlays.Mods { content.ClearTransforms(); - if (Expanded) + if (ExpandedState.Value != ModCustomisationPanelState.Collapsed) { content.AutoSizeDuration = 400; content.AutoSizeEasing = Easing.OutQuint; @@ -193,7 +187,7 @@ namespace osu.Game.Overlays.Mods private void updateMods() { - Expanded = false; + ExpandedState.Value = ModCustomisationPanelState.Collapsed; sectionsFlow.Clear(); // Importantly, the selected mods bindable is already ordered by the mod select overlay (following the order of mod columns and panels). @@ -216,10 +210,10 @@ namespace osu.Game.Overlays.Mods private partial class FocusGrabbingContainer : InputBlockingContainer { - public readonly IBindable ExpandedState = new Bindable(ModCustomisationPanelState.Collapsed); + public readonly Bindable ExpandedState = new Bindable(); - public override bool RequestsFocus => panel.Expanded; - public override bool AcceptsFocus => panel.Expanded; + public override bool RequestsFocus => panel.ExpandedState.Value != ModCustomisationPanelState.Collapsed; + public override bool AcceptsFocus => panel.ExpandedState.Value != ModCustomisationPanelState.Collapsed; private readonly ModCustomisationPanel panel; @@ -233,7 +227,7 @@ namespace osu.Game.Overlays.Mods if (ExpandedState.Value is ModCustomisationPanelState.ExpandedByHover && !ReceivePositionalInputAt(e.ScreenSpaceMousePosition)) { - panel.UpdateHoverExpansion(ModCustomisationPanelState.Collapsed); + ExpandedState.Value = ModCustomisationPanelState.Collapsed; } base.OnHoverLost(e); diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index e4c5269768..74890df5d9 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -368,18 +368,18 @@ namespace osu.Game.Overlays.Mods customisationPanel.Enabled.Value = true; if (anyModPendingConfiguration) - customisationPanel.Expanded = true; + customisationPanel.ExpandedState.Value = ModCustomisationPanel.ModCustomisationPanelState.Expanded; } else { - customisationPanel.Expanded = false; + customisationPanel.ExpandedState.Value = ModCustomisationPanel.ModCustomisationPanelState.Collapsed; customisationPanel.Enabled.Value = false; } } private void updateCustomisationVisualState() { - if (customisationPanel.Expanded) + if (customisationPanel.ExpandedState.Value != ModCustomisationPanel.ModCustomisationPanelState.Collapsed) { columnScroll.FadeColour(OsuColour.Gray(0.5f), 400, Easing.OutQuint); SearchTextBox.FadeColour(OsuColour.Gray(0.5f), 400, Easing.OutQuint); @@ -544,7 +544,7 @@ namespace osu.Game.Overlays.Mods nonFilteredColumnCount += 1; } - customisationPanel.Expanded = false; + customisationPanel.ExpandedState.Value = ModCustomisationPanel.ModCustomisationPanelState.Collapsed; } #endregion @@ -571,7 +571,7 @@ namespace osu.Game.Overlays.Mods // wherein activating the binding will both change the contents of the search text box and deselect all mods. case GlobalAction.DeselectAllMods: { - if (!SearchTextBox.HasFocus && !customisationPanel.Expanded) + if (!SearchTextBox.HasFocus && customisationPanel.ExpandedState.Value == ModCustomisationPanel.ModCustomisationPanelState.Collapsed) { DisplayedFooterContent?.DeselectAllModsButton?.TriggerClick(); return true; @@ -637,7 +637,7 @@ namespace osu.Game.Overlays.Mods if (e.Repeat || e.Key != Key.Tab) return false; - if (customisationPanel.Expanded) + if (customisationPanel.ExpandedState.Value != ModCustomisationPanel.ModCustomisationPanelState.Collapsed) return true; // TODO: should probably eventually support typical platform search shortcuts (`Ctrl-F`, `/`) From f83d43c38b16512f28e479a7a163b2fdc8237427 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 7 Aug 2024 14:07:20 +0200 Subject: [PATCH 1855/2493] Get rid of weird method --- osu.Game/Overlays/Mods/ModCustomisationPanel.cs | 8 -------- 1 file changed, 8 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModCustomisationPanel.cs b/osu.Game/Overlays/Mods/ModCustomisationPanel.cs index f13ef2725f..75cd5d6c91 100644 --- a/osu.Game/Overlays/Mods/ModCustomisationPanel.cs +++ b/osu.Game/Overlays/Mods/ModCustomisationPanel.cs @@ -177,14 +177,6 @@ namespace osu.Game.Overlays.Mods } } - public void UpdateHoverExpansion(ModCustomisationPanelState state) - { - if (state > ModCustomisationPanelState.Collapsed && state <= ExpandedState.Value) - return; - - ExpandedState.Value = state; - } - private void updateMods() { ExpandedState.Value = ModCustomisationPanelState.Collapsed; From cfd7f96e76cbece16f096f0cbe518249b57ce471 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 7 Aug 2024 23:29:24 +0900 Subject: [PATCH 1856/2493] Add missing exit line causing completely incorrect behaviour --- osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs index c1e1142625..e915fdc8ec 100644 --- a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs @@ -559,6 +559,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge // If the import was for a different beatmap, pass the duty off to global handling. if (beatmap.BeatmapSetInfo.OnlineID != playlistItem.Beatmap.BeatmapSet!.OnlineID) { + this.Exit(); game?.PresentBeatmap(beatmap.BeatmapSetInfo, b => b.ID == beatmap.BeatmapInfo.ID); } From 10f704cc416504d24c6a6530c3edffd3534a2082 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 7 Aug 2024 23:50:09 +0900 Subject: [PATCH 1857/2493] Fix xmldoc --- osu.Game/Localisation/ChatStrings.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Localisation/ChatStrings.cs b/osu.Game/Localisation/ChatStrings.cs index 4661f9a53e..6841e7d938 100644 --- a/osu.Game/Localisation/ChatStrings.cs +++ b/osu.Game/Localisation/ChatStrings.cs @@ -25,10 +25,10 @@ namespace osu.Game.Localisation public static LocalisableString MentionUser => new TranslatableString(getKey(@"mention_user"), @"Mention"); /// - /// "press enter to type message..." + /// "press enter to chat..." /// public static LocalisableString InGameInputPlaceholder => new TranslatableString(getKey(@"in_game_input_placeholder"), @"press enter to chat..."); - private static string getKey(string key) => $"{prefix}:{key}"; + private static string getKey(string key) => $@"{prefix}:{key}"; } } From 089ff559d39476a1ef4926af0a470700cce6eeff Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 8 Aug 2024 00:42:31 +0900 Subject: [PATCH 1858/2493] Fix inspection --- osu.Game/Online/Chat/StandAloneChatDisplay.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Online/Chat/StandAloneChatDisplay.cs b/osu.Game/Online/Chat/StandAloneChatDisplay.cs index 469ba19fd1..e100b5fe5b 100644 --- a/osu.Game/Online/Chat/StandAloneChatDisplay.cs +++ b/osu.Game/Online/Chat/StandAloneChatDisplay.cs @@ -167,7 +167,6 @@ namespace osu.Game.Online.Chat base.OnFocusLost(e); FocusLost?.Invoke(); } - } public partial class StandAloneDrawableChannel : DrawableChannel From 85805bffdefc07432624cd17a63ab28d28ee394a Mon Sep 17 00:00:00 2001 From: clayton Date: Wed, 7 Aug 2024 14:36:03 -0700 Subject: [PATCH 1859/2493] Remove `Special#` values from `ManiaAction` and remove enum offset --- osu.Game.Rulesets.Mania/ManiaInputManager.cs | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/osu.Game.Rulesets.Mania/ManiaInputManager.cs b/osu.Game.Rulesets.Mania/ManiaInputManager.cs index a41e72660b..36ccf68d76 100644 --- a/osu.Game.Rulesets.Mania/ManiaInputManager.cs +++ b/osu.Game.Rulesets.Mania/ManiaInputManager.cs @@ -19,16 +19,8 @@ namespace osu.Game.Rulesets.Mania public enum ManiaAction { - [Description("Special 1")] - Special1 = 1, - - [Description("Special 2")] - Special2, - - // This offsets the start value of normal keys in-case we add more special keys - // above at a later time, without breaking replays/configs. [Description("Key 1")] - Key1 = 10, + Key1, [Description("Key 2")] Key2, From 606b0556d58e8c4b66f8469caa65d01497473584 Mon Sep 17 00:00:00 2001 From: clayton Date: Wed, 7 Aug 2024 14:36:03 -0700 Subject: [PATCH 1860/2493] Fix key binding generators --- .../DualStageVariantGenerator.cs | 9 +++----- .../SingleStageVariantGenerator.cs | 4 +--- .../VariantMappingGenerator.cs | 21 +++++++------------ 3 files changed, 11 insertions(+), 23 deletions(-) diff --git a/osu.Game.Rulesets.Mania/DualStageVariantGenerator.cs b/osu.Game.Rulesets.Mania/DualStageVariantGenerator.cs index e9d26b4aa1..6a7634da01 100644 --- a/osu.Game.Rulesets.Mania/DualStageVariantGenerator.cs +++ b/osu.Game.Rulesets.Mania/DualStageVariantGenerator.cs @@ -45,18 +45,15 @@ namespace osu.Game.Rulesets.Mania LeftKeys = stage1LeftKeys, RightKeys = stage1RightKeys, SpecialKey = InputKey.V, - SpecialAction = ManiaAction.Special1, - NormalActionStart = ManiaAction.Key1 - }.GenerateKeyBindingsFor(singleStageVariant, out var nextNormal); + }.GenerateKeyBindingsFor(singleStageVariant); var stage2Bindings = new VariantMappingGenerator { LeftKeys = stage2LeftKeys, RightKeys = stage2RightKeys, SpecialKey = InputKey.B, - SpecialAction = ManiaAction.Special2, - NormalActionStart = nextNormal - }.GenerateKeyBindingsFor(singleStageVariant, out _); + ActionStart = (ManiaAction)singleStageVariant, + }.GenerateKeyBindingsFor(singleStageVariant); return stage1Bindings.Concat(stage2Bindings); } diff --git a/osu.Game.Rulesets.Mania/SingleStageVariantGenerator.cs b/osu.Game.Rulesets.Mania/SingleStageVariantGenerator.cs index 44ffeb5ec2..c642da6dc4 100644 --- a/osu.Game.Rulesets.Mania/SingleStageVariantGenerator.cs +++ b/osu.Game.Rulesets.Mania/SingleStageVariantGenerator.cs @@ -34,8 +34,6 @@ namespace osu.Game.Rulesets.Mania LeftKeys = leftKeys, RightKeys = rightKeys, SpecialKey = InputKey.Space, - SpecialAction = ManiaAction.Special1, - NormalActionStart = ManiaAction.Key1, - }.GenerateKeyBindingsFor(variant, out _); + }.GenerateKeyBindingsFor(variant); } } diff --git a/osu.Game.Rulesets.Mania/VariantMappingGenerator.cs b/osu.Game.Rulesets.Mania/VariantMappingGenerator.cs index 2742ee087b..2195c9e1b9 100644 --- a/osu.Game.Rulesets.Mania/VariantMappingGenerator.cs +++ b/osu.Game.Rulesets.Mania/VariantMappingGenerator.cs @@ -26,37 +26,30 @@ namespace osu.Game.Rulesets.Mania public InputKey SpecialKey; /// - /// The at which the normal columns should begin. + /// The at which the columns should begin. /// - public ManiaAction NormalActionStart; - - /// - /// The for the special column. - /// - public ManiaAction SpecialAction; + public ManiaAction ActionStart; /// /// Generates a list of s for a specific number of columns. /// /// The number of columns that need to be bound. - /// The next to use for normal columns. /// The keybindings. - public IEnumerable GenerateKeyBindingsFor(int columns, out ManiaAction nextNormalAction) + public IEnumerable GenerateKeyBindingsFor(int columns) { - ManiaAction currentNormalAction = NormalActionStart; + ManiaAction currentAction = ActionStart; var bindings = new List(); for (int i = LeftKeys.Length - columns / 2; i < LeftKeys.Length; i++) - bindings.Add(new KeyBinding(LeftKeys[i], currentNormalAction++)); + bindings.Add(new KeyBinding(LeftKeys[i], currentAction++)); if (columns % 2 == 1) - bindings.Add(new KeyBinding(SpecialKey, SpecialAction)); + bindings.Add(new KeyBinding(SpecialKey, currentAction++)); for (int i = 0; i < columns / 2; i++) - bindings.Add(new KeyBinding(RightKeys[i], currentNormalAction++)); + bindings.Add(new KeyBinding(RightKeys[i], currentAction++)); - nextNormalAction = currentNormalAction; return bindings; } } From e7f9bba9b57b0e694abe53331e08fe82f339357e Mon Sep 17 00:00:00 2001 From: clayton Date: Wed, 7 Aug 2024 14:36:04 -0700 Subject: [PATCH 1861/2493] Fix replay frames and auto generator --- .../Replays/ManiaAutoGenerator.cs | 23 +--- .../Replays/ManiaReplayFrame.cs | 101 +----------------- 2 files changed, 6 insertions(+), 118 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Replays/ManiaAutoGenerator.cs b/osu.Game.Rulesets.Mania/Replays/ManiaAutoGenerator.cs index dd3208bd89..a5cc94ea9a 100644 --- a/osu.Game.Rulesets.Mania/Replays/ManiaAutoGenerator.cs +++ b/osu.Game.Rulesets.Mania/Replays/ManiaAutoGenerator.cs @@ -17,28 +17,9 @@ namespace osu.Game.Rulesets.Mania.Replays public new ManiaBeatmap Beatmap => (ManiaBeatmap)base.Beatmap; - private readonly ManiaAction[] columnActions; - public ManiaAutoGenerator(ManiaBeatmap beatmap) : base(beatmap) { - columnActions = new ManiaAction[Beatmap.TotalColumns]; - - var normalAction = ManiaAction.Key1; - var specialAction = ManiaAction.Special1; - int totalCounter = 0; - - foreach (var stage in Beatmap.Stages) - { - for (int i = 0; i < stage.Columns; i++) - { - if (stage.IsSpecialColumn(i)) - columnActions[totalCounter] = specialAction++; - else - columnActions[totalCounter] = normalAction++; - totalCounter++; - } - } } protected override void GenerateFrames() @@ -57,11 +38,11 @@ namespace osu.Game.Rulesets.Mania.Replays switch (point) { case HitPoint: - actions.Add(columnActions[point.Column]); + actions.Add((ManiaAction)point.Column); break; case ReleasePoint: - actions.Remove(columnActions[point.Column]); + actions.Remove((ManiaAction)point.Column); break; } } diff --git a/osu.Game.Rulesets.Mania/Replays/ManiaReplayFrame.cs b/osu.Game.Rulesets.Mania/Replays/ManiaReplayFrame.cs index 29249ba474..f80c442025 100644 --- a/osu.Game.Rulesets.Mania/Replays/ManiaReplayFrame.cs +++ b/osu.Game.Rulesets.Mania/Replays/ManiaReplayFrame.cs @@ -1,11 +1,9 @@ // 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 osu.Game.Beatmaps; using osu.Game.Replays.Legacy; -using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Replays; using osu.Game.Rulesets.Replays.Types; @@ -27,118 +25,27 @@ namespace osu.Game.Rulesets.Mania.Replays public void FromLegacy(LegacyReplayFrame legacyFrame, IBeatmap beatmap, ReplayFrame? lastFrame = null) { - var maniaBeatmap = (ManiaBeatmap)beatmap; - - var normalAction = ManiaAction.Key1; - var specialAction = ManiaAction.Special1; - + var action = ManiaAction.Key1; int activeColumns = (int)(legacyFrame.MouseX ?? 0); - int counter = 0; while (activeColumns > 0) { - bool isSpecial = isColumnAtIndexSpecial(maniaBeatmap, counter); - if ((activeColumns & 1) > 0) - Actions.Add(isSpecial ? specialAction : normalAction); + Actions.Add(action); - if (isSpecial) - specialAction++; - else - normalAction++; - - counter++; + action++; activeColumns >>= 1; } } public LegacyReplayFrame ToLegacy(IBeatmap beatmap) { - var maniaBeatmap = (ManiaBeatmap)beatmap; - int keys = 0; foreach (var action in Actions) - { - switch (action) - { - case ManiaAction.Special1: - keys |= 1 << getSpecialColumnIndex(maniaBeatmap, 0); - break; - - case ManiaAction.Special2: - keys |= 1 << getSpecialColumnIndex(maniaBeatmap, 1); - break; - - default: - // the index in lazer, which doesn't include special keys. - int nonSpecialKeyIndex = action - ManiaAction.Key1; - - // the index inclusive of special keys. - int overallIndex = 0; - - // iterate to find the index including special keys. - for (; overallIndex < maniaBeatmap.TotalColumns; overallIndex++) - { - // skip over special columns. - if (isColumnAtIndexSpecial(maniaBeatmap, overallIndex)) - continue; - // found a non-special column to use. - if (nonSpecialKeyIndex == 0) - break; - // found a non-special column but not ours. - nonSpecialKeyIndex--; - } - - keys |= 1 << overallIndex; - break; - } - } + keys |= 1 << (int)action; return new LegacyReplayFrame(Time, keys, null, ReplayButtonState.None); } - - /// - /// Find the overall index (across all stages) for a specified special key. - /// - /// The beatmap. - /// The special key offset (0 is S1). - /// The overall index for the special column. - private int getSpecialColumnIndex(ManiaBeatmap maniaBeatmap, int specialOffset) - { - for (int i = 0; i < maniaBeatmap.TotalColumns; i++) - { - if (isColumnAtIndexSpecial(maniaBeatmap, i)) - { - if (specialOffset == 0) - return i; - - specialOffset--; - } - } - - throw new ArgumentException("Special key index is too high.", nameof(specialOffset)); - } - - /// - /// Check whether the column at an overall index (across all stages) is a special column. - /// - /// The beatmap. - /// The overall index to check. - private bool isColumnAtIndexSpecial(ManiaBeatmap beatmap, int index) - { - foreach (var stage in beatmap.Stages) - { - if (index >= stage.Columns) - { - index -= stage.Columns; - continue; - } - - return stage.IsSpecialColumn(index); - } - - throw new ArgumentException("Column index is too high.", nameof(index)); - } } } From 5ad255ecbee0d42e6860192fdcc350bf0e449e0b Mon Sep 17 00:00:00 2001 From: clayton Date: Wed, 7 Aug 2024 14:36:04 -0700 Subject: [PATCH 1862/2493] Remove special actions from `Stage` constructor --- osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs | 5 ++--- osu.Game.Rulesets.Mania/UI/Stage.cs | 4 ++-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs b/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs index b3420c49f3..1f388144bd 100644 --- a/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs +++ b/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs @@ -66,13 +66,12 @@ namespace osu.Game.Rulesets.Mania.UI Content = new[] { new Drawable[stageDefinitions.Count] } }); - var normalColumnAction = ManiaAction.Key1; - var specialColumnAction = ManiaAction.Special1; + var columnAction = ManiaAction.Key1; int firstColumnIndex = 0; for (int i = 0; i < stageDefinitions.Count; i++) { - var newStage = new Stage(firstColumnIndex, stageDefinitions[i], ref normalColumnAction, ref specialColumnAction); + var newStage = new Stage(firstColumnIndex, stageDefinitions[i], ref columnAction); playfieldGrid.Content[0][i] = newStage; diff --git a/osu.Game.Rulesets.Mania/UI/Stage.cs b/osu.Game.Rulesets.Mania/UI/Stage.cs index a4a09c9a82..86f2243561 100644 --- a/osu.Game.Rulesets.Mania/UI/Stage.cs +++ b/osu.Game.Rulesets.Mania/UI/Stage.cs @@ -59,7 +59,7 @@ namespace osu.Game.Rulesets.Mania.UI private ISkinSource currentSkin = null!; - public Stage(int firstColumnIndex, StageDefinition definition, ref ManiaAction normalColumnStartAction, ref ManiaAction specialColumnStartAction) + public Stage(int firstColumnIndex, StageDefinition definition, ref ManiaAction columnStartAction) { this.firstColumnIndex = firstColumnIndex; Definition = definition; @@ -138,7 +138,7 @@ namespace osu.Game.Rulesets.Mania.UI { RelativeSizeAxes = Axes.Both, Width = 1, - Action = { Value = isSpecial ? specialColumnStartAction++ : normalColumnStartAction++ } + Action = { Value = columnStartAction++ } }; topLevelContainer.Add(column.TopLevelContainer.CreateProxy()); From 93e193d7190c56e5995d373fe2d6a2bd290131e3 Mon Sep 17 00:00:00 2001 From: clayton Date: Wed, 7 Aug 2024 14:36:04 -0700 Subject: [PATCH 1863/2493] Add realm migration to remap key bindings --- osu.Game/Database/RealmAccess.cs | 48 +++++++++++++++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) diff --git a/osu.Game/Database/RealmAccess.cs b/osu.Game/Database/RealmAccess.cs index ff76142bcc..ec86d18d4e 100644 --- a/osu.Game/Database/RealmAccess.cs +++ b/osu.Game/Database/RealmAccess.cs @@ -92,8 +92,9 @@ namespace osu.Game.Database /// 39 2023-12-19 Migrate any EndTimeObjectCount and TotalObjectCount values of 0 to -1 to better identify non-calculated values. /// 40 2023-12-21 Add ScoreInfo.Version to keep track of which build scores were set on. /// 41 2024-04-17 Add ScoreInfo.TotalScoreWithoutMods for future mod multiplier rebalances. + /// 42 2024-08-07 Update mania key bindings to reflect changes to ManiaAction ///
- private const int schema_version = 41; + private const int schema_version = 42; /// /// Lock object which is held during sections, blocking realm retrieval during blocking periods. @@ -1145,6 +1146,51 @@ namespace osu.Game.Database } } + break; + + case 42: + for (int columns = 1; columns <= 10; columns++) + { + remapKeyBindingsForVariant(columns, false); + remapKeyBindingsForVariant(columns, true); + } + + // Replace existing key bindings with new ones reflecting changes to ManiaAction: + // - "Special#" actions are removed and "Key#" actions are inserted in their place. + // - All actions are renumbered to remove the old offsets. + void remapKeyBindingsForVariant(int columns, bool dual) + { + // https://github.com/ppy/osu/blob/8773c2f7ebc226942d6124eb95c07a83934272ea/osu.Game.Rulesets.Mania/ManiaRuleset.cs#L327-L336 + int variant = dual ? 1000 + columns * 2 : columns; + + var oldKeyBindingsQuery = migration.NewRealm + .All() + .Where(kb => kb.RulesetName == @"mania" && kb.Variant == variant); + var oldKeyBindings = oldKeyBindingsQuery.Detach(); + + migration.NewRealm.RemoveRange(oldKeyBindingsQuery); + + // https://github.com/ppy/osu/blob/8773c2f7ebc226942d6124eb95c07a83934272ea/osu.Game.Rulesets.Mania/ManiaInputManager.cs#L22-L31 + int oldNormalAction = 10; // Old Key1 offset + int oldSpecialAction = 1; // Old Special1 offset + + for (int column = 0; column < columns * (dual ? 2 : 1); column++) + { + if (columns % 2 == 1 && column % columns == columns / 2) + remapKeyBinding(oldSpecialAction++, column); + else + remapKeyBinding(oldNormalAction++, column); + } + + void remapKeyBinding(int oldAction, int newAction) + { + var oldKeyBinding = oldKeyBindings.Find(kb => kb.ActionInt == oldAction); + + if (oldKeyBinding != null) + migration.NewRealm.Add(new RealmKeyBinding(newAction, oldKeyBinding.KeyCombination, @"mania", variant)); + } + } + break; } From 48d9bc982fdf4fde520ea2cca35155023fbad34c Mon Sep 17 00:00:00 2001 From: clayton Date: Wed, 7 Aug 2024 14:36:04 -0700 Subject: [PATCH 1864/2493] Fix tests --- .../ManiaLegacyReplayTest.cs | 12 ++++++------ .../Skinning/TestSceneStage.cs | 5 ++--- .../TestSceneAutoGeneration.cs | 8 ++++---- osu.Game.Rulesets.Mania.Tests/TestSceneStage.cs | 4 +--- .../Visual/Gameplay/TestScenePauseInputHandling.cs | 6 +++--- 5 files changed, 16 insertions(+), 19 deletions(-) diff --git a/osu.Game.Rulesets.Mania.Tests/ManiaLegacyReplayTest.cs b/osu.Game.Rulesets.Mania.Tests/ManiaLegacyReplayTest.cs index 641631d05e..de036c7b74 100644 --- a/osu.Game.Rulesets.Mania.Tests/ManiaLegacyReplayTest.cs +++ b/osu.Game.Rulesets.Mania.Tests/ManiaLegacyReplayTest.cs @@ -12,8 +12,8 @@ namespace osu.Game.Rulesets.Mania.Tests { [TestCase(ManiaAction.Key1)] [TestCase(ManiaAction.Key1, ManiaAction.Key2)] - [TestCase(ManiaAction.Special1)] - [TestCase(ManiaAction.Key8)] + [TestCase(ManiaAction.Key5)] + [TestCase(ManiaAction.Key9)] public void TestEncodeDecodeSingleStage(params ManiaAction[] actions) { var beatmap = new ManiaBeatmap(new StageDefinition(9)); @@ -29,11 +29,11 @@ namespace osu.Game.Rulesets.Mania.Tests [TestCase(ManiaAction.Key1)] [TestCase(ManiaAction.Key1, ManiaAction.Key2)] - [TestCase(ManiaAction.Special1)] - [TestCase(ManiaAction.Special2)] - [TestCase(ManiaAction.Special1, ManiaAction.Special2)] - [TestCase(ManiaAction.Special1, ManiaAction.Key5)] + [TestCase(ManiaAction.Key3)] [TestCase(ManiaAction.Key8)] + [TestCase(ManiaAction.Key3, ManiaAction.Key8)] + [TestCase(ManiaAction.Key3, ManiaAction.Key6)] + [TestCase(ManiaAction.Key10)] public void TestEncodeDecodeDualStage(params ManiaAction[] actions) { var beatmap = new ManiaBeatmap(new StageDefinition(5)); diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStage.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStage.cs index d44a38fdec..091a4cb55b 100644 --- a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStage.cs +++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStage.cs @@ -14,12 +14,11 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning { SetContents(_ => { - ManiaAction normalAction = ManiaAction.Key1; - ManiaAction specialAction = ManiaAction.Special1; + ManiaAction action = ManiaAction.Key1; return new ManiaInputManager(new ManiaRuleset().RulesetInfo, 4) { - Child = new Stage(0, new StageDefinition(4), ref normalAction, ref specialAction) + Child = new Stage(0, new StageDefinition(4), ref action) }; }); } diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneAutoGeneration.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneAutoGeneration.cs index e3846e8213..9a3167b97f 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneAutoGeneration.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneAutoGeneration.cs @@ -36,8 +36,8 @@ namespace osu.Game.Rulesets.Mania.Tests Assert.AreEqual(generated.Frames.Count, frame_offset + 2, "Incorrect number of frames"); Assert.AreEqual(1000, generated.Frames[frame_offset].Time, "Incorrect hit time"); Assert.AreEqual(1000 + ManiaAutoGenerator.RELEASE_DELAY, generated.Frames[frame_offset + 1].Time, "Incorrect release time"); - Assert.IsTrue(checkContains(generated.Frames[frame_offset], ManiaAction.Special1), "Special1 has not been pressed"); - Assert.IsFalse(checkContains(generated.Frames[frame_offset + 1], ManiaAction.Special1), "Special1 has not been released"); + Assert.IsTrue(checkContains(generated.Frames[frame_offset], ManiaAction.Key1), "Key1 has not been pressed"); + Assert.IsFalse(checkContains(generated.Frames[frame_offset + 1], ManiaAction.Key1), "Key1 has not been released"); } [Test] @@ -57,8 +57,8 @@ namespace osu.Game.Rulesets.Mania.Tests Assert.AreEqual(generated.Frames.Count, frame_offset + 2, "Incorrect number of frames"); Assert.AreEqual(1000, generated.Frames[frame_offset].Time, "Incorrect hit time"); Assert.AreEqual(3000, generated.Frames[frame_offset + 1].Time, "Incorrect release time"); - Assert.IsTrue(checkContains(generated.Frames[frame_offset], ManiaAction.Special1), "Special1 has not been pressed"); - Assert.IsFalse(checkContains(generated.Frames[frame_offset + 1], ManiaAction.Special1), "Special1 has not been released"); + Assert.IsTrue(checkContains(generated.Frames[frame_offset], ManiaAction.Key1), "Key1 has not been pressed"); + Assert.IsFalse(checkContains(generated.Frames[frame_offset + 1], ManiaAction.Key1), "Key1 has not been released"); } [Test] diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneStage.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneStage.cs index db04142915..195365fb18 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneStage.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneStage.cs @@ -131,9 +131,7 @@ namespace osu.Game.Rulesets.Mania.Tests private ScrollingTestContainer createStage(ScrollingDirection direction, ManiaAction action) { - var specialAction = ManiaAction.Special1; - - var stage = new Stage(0, new StageDefinition(2), ref action, ref specialAction); + var stage = new Stage(0, new StageDefinition(2), ref action); stages.Add(stage); return new ScrollingTestContainer(direction) diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePauseInputHandling.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePauseInputHandling.cs index 2d03d0cb7c..bc66947ccd 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePauseInputHandling.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePauseInputHandling.cs @@ -107,7 +107,7 @@ namespace osu.Game.Tests.Visual.Gameplay KeyCounter counter = null!; loadPlayer(() => new ManiaRuleset()); - AddStep("get key counter", () => counter = this.ChildrenOfType().Single(k => k.Trigger is KeyCounterActionTrigger actionTrigger && actionTrigger.Action == ManiaAction.Special1)); + AddStep("get key counter", () => counter = this.ChildrenOfType().Single(k => k.Trigger is KeyCounterActionTrigger actionTrigger && actionTrigger.Action == ManiaAction.Key4)); checkKey(() => counter, 0, false); AddStep("press space", () => InputManager.PressKey(Key.Space)); @@ -174,7 +174,7 @@ namespace osu.Game.Tests.Visual.Gameplay KeyCounter counter = null!; loadPlayer(() => new ManiaRuleset()); - AddStep("get key counter", () => counter = this.ChildrenOfType().Single(k => k.Trigger is KeyCounterActionTrigger actionTrigger && actionTrigger.Action == ManiaAction.Special1)); + AddStep("get key counter", () => counter = this.ChildrenOfType().Single(k => k.Trigger is KeyCounterActionTrigger actionTrigger && actionTrigger.Action == ManiaAction.Key4)); AddStep("press space", () => InputManager.PressKey(Key.Space)); AddStep("pause", () => Player.Pause()); @@ -237,7 +237,7 @@ namespace osu.Game.Tests.Visual.Gameplay KeyCounter counter = null!; loadPlayer(() => new ManiaRuleset()); - AddStep("get key counter", () => counter = this.ChildrenOfType().Single(k => k.Trigger is KeyCounterActionTrigger actionTrigger && actionTrigger.Action == ManiaAction.Special1)); + AddStep("get key counter", () => counter = this.ChildrenOfType().Single(k => k.Trigger is KeyCounterActionTrigger actionTrigger && actionTrigger.Action == ManiaAction.Key4)); AddStep("press space", () => InputManager.PressKey(Key.Space)); checkKey(() => counter, 1, true); From 8e63c1753677eaa8a5fa58ca90947dac32bf88ee Mon Sep 17 00:00:00 2001 From: clayton Date: Wed, 7 Aug 2024 15:02:53 -0700 Subject: [PATCH 1865/2493] Apply CodeFactor lint --- 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 ec86d18d4e..cb91d6923b 100644 --- a/osu.Game/Database/RealmAccess.cs +++ b/osu.Game/Database/RealmAccess.cs @@ -1161,7 +1161,7 @@ namespace osu.Game.Database void remapKeyBindingsForVariant(int columns, bool dual) { // https://github.com/ppy/osu/blob/8773c2f7ebc226942d6124eb95c07a83934272ea/osu.Game.Rulesets.Mania/ManiaRuleset.cs#L327-L336 - int variant = dual ? 1000 + columns * 2 : columns; + int variant = dual ? 1000 + (columns * 2) : columns; var oldKeyBindingsQuery = migration.NewRealm .All() From 9bafdeeeff69a2cb122bcdd9bfaa65a71077e978 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 8 Aug 2024 13:17:25 +0900 Subject: [PATCH 1866/2493] Improve animation --- .../DailyChallenge/DailyChallengeIntro.cs | 21 ++++++++----------- 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeIntro.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeIntro.cs index 073ed1c217..aa18779d81 100644 --- a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeIntro.cs +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeIntro.cs @@ -45,7 +45,6 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge private IBindable starDifficulty = null!; - private const float initial_v_shift = 32; private const float final_v_shift = 340; [Cached] @@ -296,10 +295,10 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge if (trackContent) { - float vShift = initial_v_shift + (beatmapContent.DrawHeight * beatmapContent.Scale.Y) / 2; + float vShift = (beatmapContent.DrawHeight * beatmapContent.Scale.Y) / 2; float yPos = (float)Interpolation.DampContinuously(bottomDateDisplay.Y, vShift, 16, Clock.ElapsedFrameTime); - float xPos = (float)Interpolation.DampContinuously(bottomDateDisplay.X, getShearForY(vShift) + final_v_shift, 16, Clock.ElapsedFrameTime); + float xPos = (float)Interpolation.DampContinuously(bottomDateDisplay.X, getShearForY(vShift) + beatmapContent.Scale.Y * final_v_shift, 16, Clock.ElapsedFrameTime); topTitleDisplay.Position = new Vector2(-xPos, -yPos); bottomDateDisplay.Position = new Vector2(xPos, yPos); @@ -333,34 +332,32 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge private void beginAnimation() { - const float v_spacing = 0; + const float v_spacing = 5; + const float initial_h_shift = 300; using (BeginDelayedSequence(200)) { introContent.Show(); - topTitleDisplay.MoveToOffset(new Vector2(getShearForY(-initial_v_shift), -initial_v_shift)); - bottomDateDisplay.MoveToOffset(new Vector2(getShearForY(initial_v_shift), initial_v_shift)); - - topTitleDisplay.MoveToX(getShearForY(topTitleDisplay.Y) - 500) + topTitleDisplay.MoveToX(getShearForY(topTitleDisplay.Y) - initial_h_shift) .MoveToX(getShearForY(topTitleDisplay.Y) - v_spacing, 300, Easing.OutQuint) .FadeInFromZero(400, Easing.OutQuint); - bottomDateDisplay.MoveToX(getShearForY(bottomDateDisplay.Y) + 500) + bottomDateDisplay.MoveToX(getShearForY(bottomDateDisplay.Y) + initial_h_shift) .MoveToX(getShearForY(bottomDateDisplay.Y) + v_spacing, 300, Easing.OutQuint) .FadeInFromZero(400, Easing.OutQuint); using (BeginDelayedSequence(500)) { - Schedule(() => trackContent = true); - beatmapContent .ScaleTo(1f, 500, Easing.InQuint) .Then() - .ScaleTo(1.02f, 3000); + .ScaleTo(1.1f, 3000); using (BeginDelayedSequence(240)) { + Schedule(() => trackContent = true); + beatmapContent.FadeInFromZero(280, Easing.InQuad); using (BeginDelayedSequence(300)) From f72f5ee7e3484b6f4644b784756962cad750f791 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 8 Aug 2024 13:29:10 +0900 Subject: [PATCH 1867/2493] More improvements maybe --- .../DailyChallenge/DailyChallengeIntro.cs | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeIntro.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeIntro.cs index aa18779d81..af0a015efe 100644 --- a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeIntro.cs +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeIntro.cs @@ -45,8 +45,6 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge private IBindable starDifficulty = null!; - private const float final_v_shift = 340; - [Cached] private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Plum); @@ -295,13 +293,11 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge if (trackContent) { - float vShift = (beatmapContent.DrawHeight * beatmapContent.Scale.Y) / 2; + float yPos = (beatmapContent.DrawHeight * beatmapContent.Scale.Y) / 2 - 20 * beatmapContent.Scale.Y; + float xPos = getShearForY(yPos) + beatmapContent.Scale.Y * 320; - float yPos = (float)Interpolation.DampContinuously(bottomDateDisplay.Y, vShift, 16, Clock.ElapsedFrameTime); - float xPos = (float)Interpolation.DampContinuously(bottomDateDisplay.X, getShearForY(vShift) + beatmapContent.Scale.Y * final_v_shift, 16, Clock.ElapsedFrameTime); - - topTitleDisplay.Position = new Vector2(-xPos, -yPos); - bottomDateDisplay.Position = new Vector2(xPos, yPos); + topTitleDisplay.Position = new Vector2(-xPos, yPos); + bottomDateDisplay.Position = new Vector2(xPos, -yPos); } } @@ -335,6 +331,8 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge const float v_spacing = 5; const float initial_h_shift = 300; + introContent.ScaleTo(1.2f, 8000); + using (BeginDelayedSequence(200)) { introContent.Show(); @@ -350,9 +348,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge using (BeginDelayedSequence(500)) { beatmapContent - .ScaleTo(1f, 500, Easing.InQuint) - .Then() - .ScaleTo(1.1f, 3000); + .ScaleTo(1f, 500, Easing.InQuint); using (BeginDelayedSequence(240)) { From 25dddb694a3db52a0bd0ed345c28728786dcc0ff Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 8 Aug 2024 13:35:55 +0900 Subject: [PATCH 1868/2493] And then completely change the animation to a new style --- .../DailyChallenge/DailyChallengeIntro.cs | 170 +++++++++--------- 1 file changed, 88 insertions(+), 82 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeIntro.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeIntro.cs index af0a015efe..550b7aeda8 100644 --- a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeIntro.cs +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeIntro.cs @@ -10,7 +10,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Screens; -using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Drawables; using osu.Game.Graphics; @@ -38,10 +37,11 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge private FillFlowContainer beatmapContent = null!; + private Container titleContainer = null!; + private bool beatmapBackgroundLoaded; private bool animationBegan; - private bool trackContent; private IBindable starDifficulty = null!; @@ -78,6 +78,67 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge Shear = new Vector2(OsuGame.SHEAR, 0f), Children = new Drawable[] { + titleContainer = new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + topTitleDisplay = new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.CentreRight, + AutoSizeAxes = Axes.Both, + CornerRadius = 10f, + Masking = true, + X = -10, + Children = new Drawable[] + { + new Box + { + Colour = colourProvider.Background3, + RelativeSizeAxes = Axes.Both, + }, + new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Text = "Today's Challenge", + Margin = new MarginPadding { Horizontal = 10f, Vertical = 5f }, + Shear = new Vector2(-OsuGame.SHEAR, 0f), + Font = OsuFont.GetFont(size: 32, weight: FontWeight.Light, typeface: Typeface.TorusAlternate), + }, + } + }, + bottomDateDisplay = new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.CentreLeft, + AutoSizeAxes = Axes.Both, + CornerRadius = 10f, + Masking = true, + X = 10, + Children = new Drawable[] + { + new Box + { + Colour = colourProvider.Background3, + RelativeSizeAxes = Axes.Both, + }, + new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Text = room.Name.Value.Split(':', StringSplitOptions.TrimEntries).Last(), + Margin = new MarginPadding { Horizontal = 10f, Vertical = 5f }, + Shear = new Vector2(-OsuGame.SHEAR, 0f), + Font = OsuFont.GetFont(size: 32, weight: FontWeight.Light, typeface: Typeface.TorusAlternate), + }, + } + }, + } + }, beatmapContent = new FillFlowContainer { AlwaysPresent = true, // so we can get the size ahead of time @@ -209,56 +270,6 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge } } }, - topTitleDisplay = new Container - { - Anchor = Anchor.Centre, - Origin = Anchor.CentreRight, - AutoSizeAxes = Axes.Both, - CornerRadius = 10f, - Masking = true, - Children = new Drawable[] - { - new Box - { - Colour = colourProvider.Background3, - RelativeSizeAxes = Axes.Both, - }, - new OsuSpriteText - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Text = "Today's Challenge", - Margin = new MarginPadding { Horizontal = 10f, Vertical = 5f }, - Shear = new Vector2(-OsuGame.SHEAR, 0f), - Font = OsuFont.GetFont(size: 32, weight: FontWeight.Light, typeface: Typeface.TorusAlternate), - }, - } - }, - bottomDateDisplay = new Container - { - Anchor = Anchor.Centre, - Origin = Anchor.CentreLeft, - AutoSizeAxes = Axes.Both, - CornerRadius = 10f, - Masking = true, - Children = new Drawable[] - { - new Box - { - Colour = colourProvider.Background3, - RelativeSizeAxes = Axes.Both, - }, - new OsuSpriteText - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Text = room.Name.Value.Split(':', StringSplitOptions.TrimEntries).Last(), - Margin = new MarginPadding { Horizontal = 10f, Vertical = 5f }, - Shear = new Vector2(-OsuGame.SHEAR, 0f), - Font = OsuFont.GetFont(size: 32, weight: FontWeight.Light, typeface: Typeface.TorusAlternate), - }, - } - }, } } }; @@ -287,20 +298,6 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge }); } - protected override void Update() - { - base.Update(); - - if (trackContent) - { - float yPos = (beatmapContent.DrawHeight * beatmapContent.Scale.Y) / 2 - 20 * beatmapContent.Scale.Y; - float xPos = getShearForY(yPos) + beatmapContent.Scale.Y * 320; - - topTitleDisplay.Position = new Vector2(-xPos, yPos); - bottomDateDisplay.Position = new Vector2(xPos, -yPos); - } - } - public override void OnEntering(ScreenTransitionEvent e) { base.OnEntering(e); @@ -328,32 +325,43 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge private void beginAnimation() { - const float v_spacing = 5; - const float initial_h_shift = 300; - - introContent.ScaleTo(1.2f, 8000); - using (BeginDelayedSequence(200)) { introContent.Show(); - topTitleDisplay.MoveToX(getShearForY(topTitleDisplay.Y) - initial_h_shift) - .MoveToX(getShearForY(topTitleDisplay.Y) - v_spacing, 300, Easing.OutQuint) - .FadeInFromZero(400, Easing.OutQuint); + const float y_offset_start = 260; + const float y_offset_end = 20; - bottomDateDisplay.MoveToX(getShearForY(bottomDateDisplay.Y) + initial_h_shift) - .MoveToX(getShearForY(bottomDateDisplay.Y) + v_spacing, 300, Easing.OutQuint) - .FadeInFromZero(400, Easing.OutQuint); + topTitleDisplay + .FadeInFromZero(400, Easing.OutQuint); + + topTitleDisplay.MoveToY(-y_offset_start) + .MoveToY(-y_offset_end, 300, Easing.OutQuint) + .Then() + .MoveToY(0, 4000); + + bottomDateDisplay.MoveToY(y_offset_start) + .MoveToY(y_offset_end, 300, Easing.OutQuint) + .Then() + .MoveToY(0, 4000); using (BeginDelayedSequence(500)) { beatmapContent - .ScaleTo(1f, 500, Easing.InQuint); + .ScaleTo(3) + .ScaleTo(1f, 500, Easing.In) + .Then() + .ScaleTo(1.1f, 4000); + + using (BeginDelayedSequence(100)) + { + titleContainer + .ScaleTo(0.4f, 400, Easing.In) + .FadeOut(500, Easing.OutQuint); + } using (BeginDelayedSequence(240)) { - Schedule(() => trackContent = true); - beatmapContent.FadeInFromZero(280, Easing.InQuad); using (BeginDelayedSequence(300)) @@ -375,8 +383,6 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge } } - private static float getShearForY(float yPos) => yPos * -OsuGame.SHEAR * 2; - private partial class DailyChallengeIntroBackgroundScreen : RoomBackgroundScreen { private readonly OverlayColourProvider colourProvider; From 5891780427eb46527a56d4aa6acf695f9678c1f5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 8 Aug 2024 14:15:40 +0900 Subject: [PATCH 1869/2493] Show initial text a bit longer --- .../Screens/OnlinePlay/DailyChallenge/DailyChallengeIntro.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeIntro.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeIntro.cs index 550b7aeda8..7570012e43 100644 --- a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeIntro.cs +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeIntro.cs @@ -345,7 +345,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge .Then() .MoveToY(0, 4000); - using (BeginDelayedSequence(500)) + using (BeginDelayedSequence(1000)) { beatmapContent .ScaleTo(3) From 278d887ee5ed0c34a9510afa175115ac98e4fd83 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 8 Aug 2024 15:10:14 +0900 Subject: [PATCH 1870/2493] Fix test failures due to missing room name --- osu.Game.Tests/Visual/Menus/TestSceneMainMenu.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game.Tests/Visual/Menus/TestSceneMainMenu.cs b/osu.Game.Tests/Visual/Menus/TestSceneMainMenu.cs index 792b9441fc..613d8347b7 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneMainMenu.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneMainMenu.cs @@ -43,6 +43,7 @@ namespace osu.Game.Tests.Visual.Menus getRoomRequest.TriggerSuccess(new Room { RoomID = { Value = 1234 }, + Name = { Value = "Aug 8, 2024" }, Playlist = { new PlaylistItem(beatmap) From f91a3e9a350ac3f10f490059ee9ec4bfd4190d55 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 8 Aug 2024 14:41:22 +0900 Subject: [PATCH 1871/2493] Start playing daily challenge track as part of intro sequence --- .../DailyChallenge/DailyChallenge.cs | 37 ++++++++++--------- .../DailyChallenge/DailyChallengeIntro.cs | 21 ++++++++++- 2 files changed, 39 insertions(+), 19 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs index b1ff24aa48..6885bc050d 100644 --- a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs @@ -389,7 +389,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge base.LoadComplete(); beatmapAvailabilityTracker.SelectedItem.Value = playlistItem; - beatmapAvailabilityTracker.Availability.BindValueChanged(_ => trySetDailyChallengeBeatmap(), true); + beatmapAvailabilityTracker.Availability.BindValueChanged(_ => TrySetDailyChallengeBeatmap(this, beatmapManager, rulesets, musicController, playlistItem), true); userModsSelectOverlayRegistration = overlayManager?.RegisterBlockingOverlay(userModsSelectOverlay); userModsSelectOverlay.SelectedItem.Value = playlistItem; @@ -401,15 +401,6 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge dailyChallengeInfo.BindValueChanged(dailyChallengeChanged); } - private void trySetDailyChallengeBeatmap() - { - var beatmap = beatmapManager.QueryBeatmap(b => b.OnlineID == playlistItem.Beatmap.OnlineID); - Beatmap.Value = beatmapManager.GetWorkingBeatmap(beatmap); // this will gracefully fall back to dummy beatmap if missing locally. - Ruleset.Value = rulesets.GetRuleset(playlistItem.RulesetID); - - applyLoopingToTrack(); - } - private void onlineStateChanged(ValueChangedEvent state) => Schedule(() => { if (state.NewValue != APIState.Online) @@ -443,7 +434,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge waves.Show(); roomManager.JoinRoom(room); - applyLoopingToTrack(); + startLoopingTrack(this, musicController); metadataClient.BeginWatchingMultiplayerRoom(room.RoomID.Value!.Value).ContinueWith(t => { @@ -465,14 +456,14 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge }); beatmapAvailabilityTracker.SelectedItem.Value = playlistItem; - beatmapAvailabilityTracker.Availability.BindValueChanged(_ => trySetDailyChallengeBeatmap(), true); + beatmapAvailabilityTracker.Availability.BindValueChanged(_ => TrySetDailyChallengeBeatmap(this, beatmapManager, rulesets, musicController, playlistItem), true); userModsSelectOverlay.SelectedItem.Value = playlistItem; } public override void OnResuming(ScreenTransitionEvent e) { base.OnResuming(e); - applyLoopingToTrack(); + startLoopingTrack(this, musicController); // re-apply mods as they may have been changed by a child screen // (one known instance of this is showing a replay). updateMods(); @@ -501,17 +492,27 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge return base.OnExiting(e); } - private void applyLoopingToTrack() + public static void TrySetDailyChallengeBeatmap(OsuScreen screen, BeatmapManager beatmaps, RulesetStore rulesets, MusicController music, PlaylistItem item) { - if (!this.IsCurrentScreen()) + var beatmap = beatmaps.QueryBeatmap(b => b.OnlineID == item.Beatmap.OnlineID); + + screen.Beatmap.Value = beatmaps.GetWorkingBeatmap(beatmap); // this will gracefully fall back to dummy beatmap if missing locally. + screen.Ruleset.Value = rulesets.GetRuleset(item.RulesetID); + + startLoopingTrack(screen, music); + } + + private static void startLoopingTrack(OsuScreen screen, MusicController music) + { + if (!screen.IsCurrentScreen()) return; - var track = Beatmap.Value?.Track; + var track = screen.Beatmap.Value?.Track; if (track != null) { - Beatmap.Value?.PrepareTrackForPreview(true); - musicController.EnsurePlayingSomething(); + screen.Beatmap.Value?.PrepareTrackForPreview(true); + music.EnsurePlayingSomething(); } } diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeIntro.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeIntro.cs index 7570012e43..83664fdd61 100644 --- a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeIntro.cs +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeIntro.cs @@ -26,6 +26,10 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge { public partial class DailyChallengeIntro : OsuScreen { + public override bool DisallowExternalBeatmapRulesetChanges => true; + + public override bool? ApplyModTrackAdjustments => true; + private readonly Room room; private readonly PlaylistItem item; @@ -48,6 +52,15 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge [Cached] private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Plum); + [Resolved] + private BeatmapManager beatmapManager { get; set; } = null!; + + [Resolved] + private RulesetStore rulesets { get; set; } = null!; + + [Resolved] + private MusicController musicController { get; set; } = null!; + public DailyChallengeIntro(Room room) { this.room = room; @@ -365,7 +378,13 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge beatmapContent.FadeInFromZero(280, Easing.InQuad); using (BeginDelayedSequence(300)) - Schedule(() => ApplyToBackground(bs => ((RoomBackgroundScreen)bs).SelectedItem.Value = item)); + { + Schedule(() => + { + DailyChallenge.TrySetDailyChallengeBeatmap(this, beatmapManager, rulesets, musicController, item); + ApplyToBackground(bs => ((RoomBackgroundScreen)bs).SelectedItem.Value = item); + }); + } using (BeginDelayedSequence(400)) flash.FadeOutFromOne(5000, Easing.OutQuint); From e95d61d4c239cfe55e48611b30cc42c0a0a6a8e5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 8 Aug 2024 15:01:39 +0900 Subject: [PATCH 1872/2493] Remove accidental double handling of beatmap availability in `DailyChallenge` --- osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs index 6885bc050d..9c8d0ff133 100644 --- a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs @@ -455,8 +455,6 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge }); }); - beatmapAvailabilityTracker.SelectedItem.Value = playlistItem; - beatmapAvailabilityTracker.Availability.BindValueChanged(_ => TrySetDailyChallengeBeatmap(this, beatmapManager, rulesets, musicController, playlistItem), true); userModsSelectOverlay.SelectedItem.Value = playlistItem; } From 5d66eda9826f7489416963e3e6fd5123f6bfd4cc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 8 Aug 2024 14:54:00 +0900 Subject: [PATCH 1873/2493] Add support for automatically downloading daily challenge during the intro display --- osu.Game/Beatmaps/BeatmapManager.cs | 2 +- .../OnlinePlay/DailyChallenge/DailyChallenge.cs | 3 +++ .../DailyChallenge/DailyChallengeIntro.cs | 13 ++++++++++++- 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index e90b3c703f..cd818941ff 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -563,7 +563,7 @@ namespace osu.Game.Beatmaps remove => workingBeatmapCache.OnInvalidated -= value; } - public override bool IsAvailableLocally(BeatmapSetInfo model) => Realm.Run(realm => realm.All().Any(s => s.OnlineID == model.OnlineID)); + public override bool IsAvailableLocally(BeatmapSetInfo model) => Realm.Run(realm => realm.All().Any(s => s.OnlineID == model.OnlineID && !s.DeletePending)); #endregion diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs index 9c8d0ff133..7cdf546080 100644 --- a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs @@ -492,6 +492,9 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge public static void TrySetDailyChallengeBeatmap(OsuScreen screen, BeatmapManager beatmaps, RulesetStore rulesets, MusicController music, PlaylistItem item) { + if (!screen.IsCurrentScreen()) + return; + var beatmap = beatmaps.QueryBeatmap(b => b.OnlineID == item.Beatmap.OnlineID); screen.Beatmap.Value = beatmaps.GetWorkingBeatmap(beatmap); // this will gracefully fall back to dummy beatmap if missing locally. diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeIntro.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeIntro.cs index 83664fdd61..7254a1f796 100644 --- a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeIntro.cs +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeIntro.cs @@ -12,6 +12,7 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Screens; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Drawables; +using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Online.Rooms; @@ -72,7 +73,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge protected override BackgroundScreen CreateBackground() => new DailyChallengeIntroBackgroundScreen(colourProvider); [BackgroundDependencyLoader] - private void load(BeatmapDifficultyCache difficultyCache) + private void load(BeatmapDifficultyCache difficultyCache, BeatmapModelDownloader beatmapDownloader, OsuConfigManager config) { const float horizontal_info_size = 500f; @@ -309,11 +310,21 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge beatmapBackgroundLoaded = true; updateAnimationState(); }); + + if (config.Get(OsuSetting.AutomaticallyDownloadMissingBeatmaps)) + { + if (!beatmapManager.IsAvailableLocally(new BeatmapSetInfo { OnlineID = item.Beatmap.BeatmapSet!.OnlineID })) + beatmapDownloader.Download(item.Beatmap.BeatmapSet!, config.Get(OsuSetting.PreferNoVideo)); + } } public override void OnEntering(ScreenTransitionEvent e) { base.OnEntering(e); + + beatmapAvailabilityTracker.SelectedItem.Value = playlistItem; + beatmapAvailabilityTracker.Availability.BindValueChanged(_ => TrySetDailyChallengeBeatmap(this, beatmapManager, rulesets, musicController, playlistItem), true); + this.FadeInFromZero(400, Easing.OutQuint); updateAnimationState(); } From 3f3145e109bb9ccaf4c9c3b9ec6d22952e5398ca Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 8 Aug 2024 15:01:50 +0900 Subject: [PATCH 1874/2493] Start playing music during intro if download finishes early --- .../DailyChallenge/DailyChallengeIntro.cs | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeIntro.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeIntro.cs index 7254a1f796..d00a1ef1e9 100644 --- a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeIntro.cs +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeIntro.cs @@ -15,6 +15,7 @@ using osu.Game.Beatmaps.Drawables; using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; +using osu.Game.Online; using osu.Game.Online.Rooms; using osu.Game.Overlays; using osu.Game.Rulesets; @@ -53,6 +54,11 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge [Cached] private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Plum); + [Cached] + private readonly OnlinePlayBeatmapAvailabilityTracker beatmapAvailabilityTracker = new OnlinePlayBeatmapAvailabilityTracker(); + + private bool shouldBePlayingMusic; + [Resolved] private BeatmapManager beatmapManager { get; set; } = null!; @@ -83,6 +89,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge InternalChildren = new Drawable[] { + beatmapAvailabilityTracker, introContent = new Container { Alpha = 0f, @@ -322,8 +329,12 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge { base.OnEntering(e); - beatmapAvailabilityTracker.SelectedItem.Value = playlistItem; - beatmapAvailabilityTracker.Availability.BindValueChanged(_ => TrySetDailyChallengeBeatmap(this, beatmapManager, rulesets, musicController, playlistItem), true); + beatmapAvailabilityTracker.SelectedItem.Value = item; + beatmapAvailabilityTracker.Availability.BindValueChanged(availability => + { + if (shouldBePlayingMusic && availability.NewValue.State == DownloadState.LocallyAvailable) + DailyChallenge.TrySetDailyChallengeBeatmap(this, beatmapManager, rulesets, musicController, item); + }, true); this.FadeInFromZero(400, Easing.OutQuint); updateAnimationState(); @@ -392,6 +403,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge { Schedule(() => { + shouldBePlayingMusic = true; DailyChallenge.TrySetDailyChallengeBeatmap(this, beatmapManager, rulesets, musicController, item); ApplyToBackground(bs => ((RoomBackgroundScreen)bs).SelectedItem.Value = item); }); From 60d383448f22f36e9cc6c95f568cd81bb0ca5bef Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 8 Aug 2024 16:29:54 +0900 Subject: [PATCH 1875/2493] Avoid making non-ruleset transformers in `Ruleset.CreateSkinTransformer` This didn't make any sense, so let's do it a better way. --- .../Argon/CatchArgonSkinTransformer.cs | 2 +- .../Legacy/CatchLegacySkinTransformer.cs | 2 +- .../Skinning/Argon/OsuArgonSkinTransformer.cs | 2 +- .../Legacy/OsuLegacySkinTransformer.cs | 3 +- .../Argon/TaikoArgonSkinTransformer.cs | 2 +- osu.Game/Rulesets/Ruleset.cs | 14 +------ osu.Game/Skinning/ArgonSkin.cs | 21 ++++++++-- osu.Game/Skinning/ArgonSkinTransformer.cs | 40 ------------------- osu.Game/Skinning/LegacySkin.cs | 16 +++++--- osu.Game/Skinning/LegacySkinTransformer.cs | 22 +--------- osu.Game/Skinning/Skin.cs | 5 +-- osu.Game/Skinning/TrianglesSkin.cs | 2 +- .../Skinning/UserConfiguredLayoutContainer.cs | 15 +++++++ 13 files changed, 55 insertions(+), 91 deletions(-) delete mode 100644 osu.Game/Skinning/ArgonSkinTransformer.cs create mode 100644 osu.Game/Skinning/UserConfiguredLayoutContainer.cs diff --git a/osu.Game.Rulesets.Catch/Skinning/Argon/CatchArgonSkinTransformer.cs b/osu.Game.Rulesets.Catch/Skinning/Argon/CatchArgonSkinTransformer.cs index a67945df98..520c2de248 100644 --- a/osu.Game.Rulesets.Catch/Skinning/Argon/CatchArgonSkinTransformer.cs +++ b/osu.Game.Rulesets.Catch/Skinning/Argon/CatchArgonSkinTransformer.cs @@ -6,7 +6,7 @@ using osu.Game.Skinning; namespace osu.Game.Rulesets.Catch.Skinning.Argon { - public class CatchArgonSkinTransformer : ArgonSkinTransformer + public class CatchArgonSkinTransformer : SkinTransformer { public CatchArgonSkinTransformer(ISkin skin) : base(skin) diff --git a/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs b/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs index abd321ddb1..44fc3ecc07 100644 --- a/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs @@ -40,7 +40,7 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy return base.GetDrawableComponent(lookup) as Container; // Skin has configuration. - if (base.GetDrawableComponent(lookup) is Drawable d) + if (base.GetDrawableComponent(lookup) is UserConfiguredLayoutContainer d) return d; // Our own ruleset components default. diff --git a/osu.Game.Rulesets.Osu/Skinning/Argon/OsuArgonSkinTransformer.cs b/osu.Game.Rulesets.Osu/Skinning/Argon/OsuArgonSkinTransformer.cs index 2cc36331ae..ec63e1194d 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Argon/OsuArgonSkinTransformer.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Argon/OsuArgonSkinTransformer.cs @@ -7,7 +7,7 @@ using osu.Game.Skinning; namespace osu.Game.Rulesets.Osu.Skinning.Argon { - public class OsuArgonSkinTransformer : ArgonSkinTransformer + public class OsuArgonSkinTransformer : SkinTransformer { public OsuArgonSkinTransformer(ISkin skin) : base(skin) diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs index 2c2f228fae..9a8eaa7d7d 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs @@ -50,7 +50,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy return base.GetDrawableComponent(lookup); // Skin has configuration. - if (base.GetDrawableComponent(lookup) is Drawable d) + if (base.GetDrawableComponent(lookup) is UserConfiguredLayoutContainer d) return d; // Our own ruleset components default. @@ -74,6 +74,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy { Children = new Drawable[] { + new LegacyComboCounter(), new LegacyKeyCounterDisplay(), } }; diff --git a/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs b/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs index 7d38d6c9e5..973b4a91ff 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs @@ -7,7 +7,7 @@ using osu.Game.Skinning; namespace osu.Game.Rulesets.Taiko.Skinning.Argon { - public class TaikoArgonSkinTransformer : ArgonSkinTransformer + public class TaikoArgonSkinTransformer : SkinTransformer { public TaikoArgonSkinTransformer(ISkin skin) : base(skin) diff --git a/osu.Game/Rulesets/Ruleset.cs b/osu.Game/Rulesets/Ruleset.cs index ee010e9621..fb0e225c94 100644 --- a/osu.Game/Rulesets/Ruleset.cs +++ b/osu.Game/Rulesets/Ruleset.cs @@ -212,19 +212,7 @@ namespace osu.Game.Rulesets /// The source skin. /// The current beatmap. /// A skin with a transformer applied, or null if no transformation is provided by this ruleset. - public virtual ISkin? CreateSkinTransformer(ISkin skin, IBeatmap beatmap) - { - switch (skin) - { - case LegacySkin: - return new LegacySkinTransformer(skin); - - case ArgonSkin: - return new ArgonSkinTransformer(skin); - } - - return null; - } + public virtual ISkin? CreateSkinTransformer(ISkin skin, IBeatmap beatmap) => null; protected Ruleset() { diff --git a/osu.Game/Skinning/ArgonSkin.cs b/osu.Game/Skinning/ArgonSkin.cs index 707281db31..85abb1edcd 100644 --- a/osu.Game/Skinning/ArgonSkin.cs +++ b/osu.Game/Skinning/ArgonSkin.cs @@ -7,6 +7,7 @@ using JetBrains.Annotations; using osu.Framework.Audio.Sample; using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Textures; using osu.Game.Audio; using osu.Game.Beatmaps.Formats; @@ -93,15 +94,12 @@ namespace osu.Game.Skinning // Temporary until default skin has a valid hit lighting. if ((lookup as SkinnableSprite.SpriteComponentLookup)?.LookupName == @"lighting") return Drawable.Empty(); - if (base.GetDrawableComponent(lookup) is Drawable c) + if (base.GetDrawableComponent(lookup) is UserConfiguredLayoutContainer c) return c; switch (lookup) { case SkinComponentsContainerLookup containerLookup: - // Only handle global level defaults for now. - if (containerLookup.Ruleset != null) - return null; switch (containerLookup.Target) { @@ -114,6 +112,21 @@ namespace osu.Game.Skinning return songSelectComponents; case SkinComponentsContainerLookup.TargetArea.MainHUDComponents: + if (containerLookup.Ruleset != null) + { + return new Container + { + RelativeSizeAxes = Axes.Both, + Child = new ArgonComboCounter + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Position = new Vector2(36, -66), + Scale = new Vector2(1.3f), + }, + }; + } + var mainHUDComponents = new DefaultSkinComponentsContainer(container => { var health = container.OfType().FirstOrDefault(); diff --git a/osu.Game/Skinning/ArgonSkinTransformer.cs b/osu.Game/Skinning/ArgonSkinTransformer.cs deleted file mode 100644 index 8ca8f79b41..0000000000 --- a/osu.Game/Skinning/ArgonSkinTransformer.cs +++ /dev/null @@ -1,40 +0,0 @@ -// 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.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Game.Screens.Play.HUD; -using osuTK; - -namespace osu.Game.Skinning -{ - public class ArgonSkinTransformer : SkinTransformer - { - public ArgonSkinTransformer(ISkin skin) - : base(skin) - { - } - - public override Drawable? GetDrawableComponent(ISkinComponentLookup lookup) - { - if (lookup is SkinComponentsContainerLookup containerLookup - && containerLookup.Target == SkinComponentsContainerLookup.TargetArea.MainHUDComponents - && containerLookup.Ruleset != null) - { - return base.GetDrawableComponent(lookup) ?? new Container - { - RelativeSizeAxes = Axes.Both, - Child = new ArgonComboCounter - { - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - Position = new Vector2(36, -66), - Scale = new Vector2(1.3f), - }, - }; - } - - return base.GetDrawableComponent(lookup); - } - } -} diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index 38bf1631b4..734e80d2ed 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -13,6 +13,7 @@ using osu.Framework.Audio.Sample; using osu.Framework.Bindables; using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Textures; using osu.Framework.IO.Stores; using osu.Game.Audio; @@ -349,19 +350,24 @@ namespace osu.Game.Skinning public override Drawable? GetDrawableComponent(ISkinComponentLookup lookup) { - if (base.GetDrawableComponent(lookup) is Drawable c) + if (base.GetDrawableComponent(lookup) is UserConfiguredLayoutContainer c) return c; switch (lookup) { case SkinComponentsContainerLookup containerLookup: - // Only handle global level defaults for now. - if (containerLookup.Ruleset != null) - return null; - switch (containerLookup.Target) { case SkinComponentsContainerLookup.TargetArea.MainHUDComponents: + if (containerLookup.Ruleset != null) + { + return new Container + { + RelativeSizeAxes = Axes.Both, + Child = new LegacyComboCounter(), + }; + } + return new DefaultSkinComponentsContainer(container => { var score = container.OfType().FirstOrDefault(); diff --git a/osu.Game/Skinning/LegacySkinTransformer.cs b/osu.Game/Skinning/LegacySkinTransformer.cs index dbfa52de84..b54e9a1bdf 100644 --- a/osu.Game/Skinning/LegacySkinTransformer.cs +++ b/osu.Game/Skinning/LegacySkinTransformer.cs @@ -2,42 +2,24 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Audio.Sample; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; using osu.Game.Audio; using osu.Game.Rulesets.Objects.Legacy; using static osu.Game.Skinning.SkinConfiguration; namespace osu.Game.Skinning { - public class LegacySkinTransformer : SkinTransformer + public abstract class LegacySkinTransformer : SkinTransformer { /// /// Whether the skin being transformed is able to provide legacy resources for the ruleset. /// public virtual bool IsProvidingLegacyResources => this.HasFont(LegacyFont.Combo); - public LegacySkinTransformer(ISkin skin) + protected LegacySkinTransformer(ISkin skin) : base(skin) { } - public override Drawable? GetDrawableComponent(ISkinComponentLookup lookup) - { - if (lookup is SkinComponentsContainerLookup containerLookup - && containerLookup.Target == SkinComponentsContainerLookup.TargetArea.MainHUDComponents - && containerLookup.Ruleset != null) - { - return base.GetDrawableComponent(lookup) ?? new Container - { - RelativeSizeAxes = Axes.Both, - Child = new LegacyComboCounter(), - }; - } - - return base.GetDrawableComponent(lookup); - } - public override ISample? GetSample(ISampleInfo sampleInfo) { if (!(sampleInfo is ConvertHitObjectParser.LegacyHitSampleInfo legacySample)) diff --git a/osu.Game/Skinning/Skin.cs b/osu.Game/Skinning/Skin.cs index 5bac5c3d81..226d2fcb89 100644 --- a/osu.Game/Skinning/Skin.cs +++ b/osu.Game/Skinning/Skin.cs @@ -14,7 +14,6 @@ using osu.Framework.Audio.Sample; using osu.Framework.Bindables; using osu.Framework.Extensions.TypeExtensions; using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Textures; using osu.Framework.IO.Stores; using osu.Framework.Logging; @@ -26,7 +25,7 @@ using osu.Game.Screens.Play.HUD; namespace osu.Game.Skinning { - public abstract class Skin : IDisposable, ISkin + public abstract partial class Skin : IDisposable, ISkin { private readonly IStorageResourceProvider? resources; @@ -195,7 +194,7 @@ namespace osu.Game.Skinning if (!LayoutInfos.TryGetValue(containerLookup.Target, out var layoutInfo)) return null; if (!layoutInfo.TryGetDrawableInfo(containerLookup.Ruleset, out var drawableInfos)) return null; - return new Container + return new UserConfiguredLayoutContainer { RelativeSizeAxes = Axes.Both, ChildrenEnumerable = drawableInfos.Select(i => i.CreateInstance()) diff --git a/osu.Game/Skinning/TrianglesSkin.cs b/osu.Game/Skinning/TrianglesSkin.cs index 6158d4c7bf..29abb1949f 100644 --- a/osu.Game/Skinning/TrianglesSkin.cs +++ b/osu.Game/Skinning/TrianglesSkin.cs @@ -64,7 +64,7 @@ namespace osu.Game.Skinning // Temporary until default skin has a valid hit lighting. if ((lookup as SkinnableSprite.SpriteComponentLookup)?.LookupName == @"lighting") return Drawable.Empty(); - if (base.GetDrawableComponent(lookup) is Drawable c) + if (base.GetDrawableComponent(lookup) is UserConfiguredLayoutContainer c) return c; switch (lookup) diff --git a/osu.Game/Skinning/UserConfiguredLayoutContainer.cs b/osu.Game/Skinning/UserConfiguredLayoutContainer.cs new file mode 100644 index 0000000000..1b5a27b53b --- /dev/null +++ b/osu.Game/Skinning/UserConfiguredLayoutContainer.cs @@ -0,0 +1,15 @@ +// 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.Graphics.Containers; + +namespace osu.Game.Skinning +{ + /// + /// This signifies that a call resolved a configuration created + /// by a user in their skin. Generally this should be given priority over any local defaults or overrides. + /// + public partial class UserConfiguredLayoutContainer : Container + { + } +} From 88c5997cb36e9642ca7519e9a5a1e1d21bd6b51e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 8 Aug 2024 16:36:34 +0900 Subject: [PATCH 1876/2493] Add back removed xmldoc --- osu.Game/Skinning/LegacySkinTransformer.cs | 3 +++ osu.Game/Skinning/Skin.cs | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game/Skinning/LegacySkinTransformer.cs b/osu.Game/Skinning/LegacySkinTransformer.cs index b54e9a1bdf..367e5bae01 100644 --- a/osu.Game/Skinning/LegacySkinTransformer.cs +++ b/osu.Game/Skinning/LegacySkinTransformer.cs @@ -8,6 +8,9 @@ using static osu.Game.Skinning.SkinConfiguration; namespace osu.Game.Skinning { + /// + /// Transformer used to handle support of legacy features for individual rulesets. + /// public abstract class LegacySkinTransformer : SkinTransformer { /// diff --git a/osu.Game/Skinning/Skin.cs b/osu.Game/Skinning/Skin.cs index 226d2fcb89..fa09d0c087 100644 --- a/osu.Game/Skinning/Skin.cs +++ b/osu.Game/Skinning/Skin.cs @@ -25,7 +25,7 @@ using osu.Game.Screens.Play.HUD; namespace osu.Game.Skinning { - public abstract partial class Skin : IDisposable, ISkin + public abstract class Skin : IDisposable, ISkin { private readonly IStorageResourceProvider? resources; From 03d543ec99990aa8dbd0d56dbe8c399cda9a6065 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 8 Aug 2024 16:53:22 +0900 Subject: [PATCH 1877/2493] Fix potential test failure in daily challenge tests See https://github.com/ppy/osu/actions/runs/10296877688/job/28500580680. --- osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs index b1ff24aa48..e7dab3c4fb 100644 --- a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; @@ -455,6 +456,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge MultiplayerPlaylistItemStats[] stats = t.GetResultSafely(); var itemStats = stats.SingleOrDefault(item => item.PlaylistItemID == playlistItem.ID); + if (itemStats == null) return; Schedule(() => @@ -462,7 +464,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge breakdown.SetInitialCounts(itemStats.TotalScoreDistribution); totals.SetInitialCounts(itemStats.TotalScoreDistribution.Sum(c => c), itemStats.CumulativeScore); }); - }); + }, TaskContinuationOptions.OnlyOnRanToCompletion); beatmapAvailabilityTracker.SelectedItem.Value = playlistItem; beatmapAvailabilityTracker.Availability.BindValueChanged(_ => trySetDailyChallengeBeatmap(), true); From e12dba24ae2953ab70df50b55cb726eebc59a33a Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 8 Aug 2024 10:49:17 +0300 Subject: [PATCH 1878/2493] Remove macOS/Xcode version pinning in iOS workflow --- .github/workflows/ci.yml | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ba65cfa33a..4abd55e3f4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -121,9 +121,7 @@ jobs: build-only-ios: name: Build only (iOS) - # `macos-13` is required, because the newest Microsoft.iOS.Sdk versions require Xcode 14.3. - # TODO: can be changed to `macos-latest` once `macos-13` becomes latest (currently in beta: https://github.com/actions/runner-images/tree/main#available-images) - runs-on: macos-13 + runs-on: macos-latest timeout-minutes: 60 steps: - name: Checkout @@ -137,8 +135,5 @@ jobs: - name: Install .NET Workloads run: dotnet workload install maui-ios - - name: Select Xcode 15.2 - run: sudo xcode-select -s /Applications/Xcode_15.2.app/Contents/Developer - - name: Build run: dotnet build -c Debug osu.iOS From 64271b7bea32663b72ca5f57585dc5880cf28b03 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 8 Aug 2024 13:03:00 +0300 Subject: [PATCH 1879/2493] Remove `iPhone`/`iPhoneSimulator` configurations --- osu.iOS.props | 6 - osu.sln | 336 ++++---------------------------------------------- 2 files changed, 24 insertions(+), 318 deletions(-) diff --git a/osu.iOS.props b/osu.iOS.props index 196d5594ad..b77aeb6b0f 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -16,12 +16,6 @@ false -all - - ios-arm64 - - - iossimulator-x64 - diff --git a/osu.sln b/osu.sln index aeec0843be..829e43fc65 100644 --- a/osu.sln +++ b/osu.sln @@ -98,445 +98,157 @@ EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU - Debug|iPhone = Debug|iPhone - Debug|iPhoneSimulator = Debug|iPhoneSimulator Release|Any CPU = Release|Any CPU - Release|iPhone = Release|iPhone - Release|iPhoneSimulator = Release|iPhoneSimulator EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {2A66DD92-ADB1-4994-89E2-C94E04ACDA0D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {2A66DD92-ADB1-4994-89E2-C94E04ACDA0D}.Debug|Any CPU.Build.0 = Debug|Any CPU - {2A66DD92-ADB1-4994-89E2-C94E04ACDA0D}.Debug|iPhone.ActiveCfg = Debug|Any CPU - {2A66DD92-ADB1-4994-89E2-C94E04ACDA0D}.Debug|iPhone.Build.0 = Debug|Any CPU - {2A66DD92-ADB1-4994-89E2-C94E04ACDA0D}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU - {2A66DD92-ADB1-4994-89E2-C94E04ACDA0D}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU {2A66DD92-ADB1-4994-89E2-C94E04ACDA0D}.Release|Any CPU.ActiveCfg = Release|Any CPU {2A66DD92-ADB1-4994-89E2-C94E04ACDA0D}.Release|Any CPU.Build.0 = Release|Any CPU - {2A66DD92-ADB1-4994-89E2-C94E04ACDA0D}.Release|iPhone.ActiveCfg = Release|Any CPU - {2A66DD92-ADB1-4994-89E2-C94E04ACDA0D}.Release|iPhone.Build.0 = Release|Any CPU - {2A66DD92-ADB1-4994-89E2-C94E04ACDA0D}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU - {2A66DD92-ADB1-4994-89E2-C94E04ACDA0D}.Release|iPhoneSimulator.Build.0 = Release|Any CPU {C92A607B-1FDD-4954-9F92-03FF547D9080}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {C92A607B-1FDD-4954-9F92-03FF547D9080}.Debug|Any CPU.Build.0 = Debug|Any CPU - {C92A607B-1FDD-4954-9F92-03FF547D9080}.Debug|iPhone.ActiveCfg = Debug|Any CPU - {C92A607B-1FDD-4954-9F92-03FF547D9080}.Debug|iPhone.Build.0 = Debug|Any CPU - {C92A607B-1FDD-4954-9F92-03FF547D9080}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU - {C92A607B-1FDD-4954-9F92-03FF547D9080}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU {C92A607B-1FDD-4954-9F92-03FF547D9080}.Release|Any CPU.ActiveCfg = Release|Any CPU {C92A607B-1FDD-4954-9F92-03FF547D9080}.Release|Any CPU.Build.0 = Release|Any CPU - {C92A607B-1FDD-4954-9F92-03FF547D9080}.Release|iPhone.ActiveCfg = Release|Any CPU - {C92A607B-1FDD-4954-9F92-03FF547D9080}.Release|iPhone.Build.0 = Release|Any CPU - {C92A607B-1FDD-4954-9F92-03FF547D9080}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU - {C92A607B-1FDD-4954-9F92-03FF547D9080}.Release|iPhoneSimulator.Build.0 = Release|Any CPU {58F6C80C-1253-4A0E-A465-B8C85EBEADF3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {58F6C80C-1253-4A0E-A465-B8C85EBEADF3}.Debug|Any CPU.Build.0 = Debug|Any CPU - {58F6C80C-1253-4A0E-A465-B8C85EBEADF3}.Debug|iPhone.ActiveCfg = Debug|Any CPU - {58F6C80C-1253-4A0E-A465-B8C85EBEADF3}.Debug|iPhone.Build.0 = Debug|Any CPU - {58F6C80C-1253-4A0E-A465-B8C85EBEADF3}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU - {58F6C80C-1253-4A0E-A465-B8C85EBEADF3}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU {58F6C80C-1253-4A0E-A465-B8C85EBEADF3}.Release|Any CPU.ActiveCfg = Release|Any CPU {58F6C80C-1253-4A0E-A465-B8C85EBEADF3}.Release|Any CPU.Build.0 = Release|Any CPU - {58F6C80C-1253-4A0E-A465-B8C85EBEADF3}.Release|iPhone.ActiveCfg = Release|Any CPU - {58F6C80C-1253-4A0E-A465-B8C85EBEADF3}.Release|iPhone.Build.0 = Release|Any CPU - {58F6C80C-1253-4A0E-A465-B8C85EBEADF3}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU - {58F6C80C-1253-4A0E-A465-B8C85EBEADF3}.Release|iPhoneSimulator.Build.0 = Release|Any CPU {F167E17A-7DE6-4AF5-B920-A5112296C695}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {F167E17A-7DE6-4AF5-B920-A5112296C695}.Debug|Any CPU.Build.0 = Debug|Any CPU - {F167E17A-7DE6-4AF5-B920-A5112296C695}.Debug|iPhone.ActiveCfg = Debug|Any CPU - {F167E17A-7DE6-4AF5-B920-A5112296C695}.Debug|iPhone.Build.0 = Debug|Any CPU - {F167E17A-7DE6-4AF5-B920-A5112296C695}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU - {F167E17A-7DE6-4AF5-B920-A5112296C695}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU {F167E17A-7DE6-4AF5-B920-A5112296C695}.Release|Any CPU.ActiveCfg = Release|Any CPU {F167E17A-7DE6-4AF5-B920-A5112296C695}.Release|Any CPU.Build.0 = Release|Any CPU - {F167E17A-7DE6-4AF5-B920-A5112296C695}.Release|iPhone.ActiveCfg = Release|Any CPU - {F167E17A-7DE6-4AF5-B920-A5112296C695}.Release|iPhone.Build.0 = Release|Any CPU - {F167E17A-7DE6-4AF5-B920-A5112296C695}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU - {F167E17A-7DE6-4AF5-B920-A5112296C695}.Release|iPhoneSimulator.Build.0 = Release|Any CPU {48F4582B-7687-4621-9CBE-5C24197CB536}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {48F4582B-7687-4621-9CBE-5C24197CB536}.Debug|Any CPU.Build.0 = Debug|Any CPU - {48F4582B-7687-4621-9CBE-5C24197CB536}.Debug|iPhone.ActiveCfg = Debug|Any CPU - {48F4582B-7687-4621-9CBE-5C24197CB536}.Debug|iPhone.Build.0 = Debug|Any CPU - {48F4582B-7687-4621-9CBE-5C24197CB536}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU - {48F4582B-7687-4621-9CBE-5C24197CB536}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU {48F4582B-7687-4621-9CBE-5C24197CB536}.Release|Any CPU.ActiveCfg = Release|Any CPU {48F4582B-7687-4621-9CBE-5C24197CB536}.Release|Any CPU.Build.0 = Release|Any CPU - {48F4582B-7687-4621-9CBE-5C24197CB536}.Release|iPhone.ActiveCfg = Release|Any CPU - {48F4582B-7687-4621-9CBE-5C24197CB536}.Release|iPhone.Build.0 = Release|Any CPU - {48F4582B-7687-4621-9CBE-5C24197CB536}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU - {48F4582B-7687-4621-9CBE-5C24197CB536}.Release|iPhoneSimulator.Build.0 = Release|Any CPU {54377672-20B1-40AF-8087-5CF73BF3953A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {54377672-20B1-40AF-8087-5CF73BF3953A}.Debug|Any CPU.Build.0 = Debug|Any CPU - {54377672-20B1-40AF-8087-5CF73BF3953A}.Debug|iPhone.ActiveCfg = Debug|Any CPU - {54377672-20B1-40AF-8087-5CF73BF3953A}.Debug|iPhone.Build.0 = Debug|Any CPU - {54377672-20B1-40AF-8087-5CF73BF3953A}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU - {54377672-20B1-40AF-8087-5CF73BF3953A}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU {54377672-20B1-40AF-8087-5CF73BF3953A}.Release|Any CPU.ActiveCfg = Release|Any CPU {54377672-20B1-40AF-8087-5CF73BF3953A}.Release|Any CPU.Build.0 = Release|Any CPU - {54377672-20B1-40AF-8087-5CF73BF3953A}.Release|iPhone.ActiveCfg = Release|Any CPU - {54377672-20B1-40AF-8087-5CF73BF3953A}.Release|iPhone.Build.0 = Release|Any CPU - {54377672-20B1-40AF-8087-5CF73BF3953A}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU - {54377672-20B1-40AF-8087-5CF73BF3953A}.Release|iPhoneSimulator.Build.0 = Release|Any CPU {419659FD-72EA-4678-9EB8-B22A746CED70}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {419659FD-72EA-4678-9EB8-B22A746CED70}.Debug|Any CPU.Build.0 = Debug|Any CPU - {419659FD-72EA-4678-9EB8-B22A746CED70}.Debug|iPhone.ActiveCfg = Debug|Any CPU - {419659FD-72EA-4678-9EB8-B22A746CED70}.Debug|iPhone.Build.0 = Debug|Any CPU - {419659FD-72EA-4678-9EB8-B22A746CED70}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU - {419659FD-72EA-4678-9EB8-B22A746CED70}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU {419659FD-72EA-4678-9EB8-B22A746CED70}.Release|Any CPU.ActiveCfg = Release|Any CPU {419659FD-72EA-4678-9EB8-B22A746CED70}.Release|Any CPU.Build.0 = Release|Any CPU - {419659FD-72EA-4678-9EB8-B22A746CED70}.Release|iPhone.ActiveCfg = Release|Any CPU - {419659FD-72EA-4678-9EB8-B22A746CED70}.Release|iPhone.Build.0 = Release|Any CPU - {419659FD-72EA-4678-9EB8-B22A746CED70}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU - {419659FD-72EA-4678-9EB8-B22A746CED70}.Release|iPhoneSimulator.Build.0 = Release|Any CPU {3AD63355-D6B1-4365-8D31-5652C989BEF1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {3AD63355-D6B1-4365-8D31-5652C989BEF1}.Debug|Any CPU.Build.0 = Debug|Any CPU - {3AD63355-D6B1-4365-8D31-5652C989BEF1}.Debug|iPhone.ActiveCfg = Debug|Any CPU - {3AD63355-D6B1-4365-8D31-5652C989BEF1}.Debug|iPhone.Build.0 = Debug|Any CPU - {3AD63355-D6B1-4365-8D31-5652C989BEF1}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU - {3AD63355-D6B1-4365-8D31-5652C989BEF1}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU {3AD63355-D6B1-4365-8D31-5652C989BEF1}.Release|Any CPU.ActiveCfg = Release|Any CPU {3AD63355-D6B1-4365-8D31-5652C989BEF1}.Release|Any CPU.Build.0 = Release|Any CPU - {3AD63355-D6B1-4365-8D31-5652C989BEF1}.Release|iPhone.ActiveCfg = Release|Any CPU - {3AD63355-D6B1-4365-8D31-5652C989BEF1}.Release|iPhone.Build.0 = Release|Any CPU - {3AD63355-D6B1-4365-8D31-5652C989BEF1}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU - {3AD63355-D6B1-4365-8D31-5652C989BEF1}.Release|iPhoneSimulator.Build.0 = Release|Any CPU {7E9E9C34-B204-406B-82E2-E01E900699CD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {7E9E9C34-B204-406B-82E2-E01E900699CD}.Debug|Any CPU.Build.0 = Debug|Any CPU - {7E9E9C34-B204-406B-82E2-E01E900699CD}.Debug|iPhone.ActiveCfg = Debug|Any CPU - {7E9E9C34-B204-406B-82E2-E01E900699CD}.Debug|iPhone.Build.0 = Debug|Any CPU - {7E9E9C34-B204-406B-82E2-E01E900699CD}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU - {7E9E9C34-B204-406B-82E2-E01E900699CD}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU {7E9E9C34-B204-406B-82E2-E01E900699CD}.Release|Any CPU.ActiveCfg = Release|Any CPU {7E9E9C34-B204-406B-82E2-E01E900699CD}.Release|Any CPU.Build.0 = Release|Any CPU - {7E9E9C34-B204-406B-82E2-E01E900699CD}.Release|iPhone.ActiveCfg = Release|Any CPU - {7E9E9C34-B204-406B-82E2-E01E900699CD}.Release|iPhone.Build.0 = Release|Any CPU - {7E9E9C34-B204-406B-82E2-E01E900699CD}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU - {7E9E9C34-B204-406B-82E2-E01E900699CD}.Release|iPhoneSimulator.Build.0 = Release|Any CPU {B698561F-FB28-46B1-857E-3CA7B92F9D70}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {B698561F-FB28-46B1-857E-3CA7B92F9D70}.Debug|Any CPU.Build.0 = Debug|Any CPU - {B698561F-FB28-46B1-857E-3CA7B92F9D70}.Debug|iPhone.ActiveCfg = Debug|Any CPU - {B698561F-FB28-46B1-857E-3CA7B92F9D70}.Debug|iPhone.Build.0 = Debug|Any CPU - {B698561F-FB28-46B1-857E-3CA7B92F9D70}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU - {B698561F-FB28-46B1-857E-3CA7B92F9D70}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU {B698561F-FB28-46B1-857E-3CA7B92F9D70}.Release|Any CPU.ActiveCfg = Release|Any CPU {B698561F-FB28-46B1-857E-3CA7B92F9D70}.Release|Any CPU.Build.0 = Release|Any CPU - {B698561F-FB28-46B1-857E-3CA7B92F9D70}.Release|iPhone.ActiveCfg = Release|Any CPU - {B698561F-FB28-46B1-857E-3CA7B92F9D70}.Release|iPhone.Build.0 = Release|Any CPU - {B698561F-FB28-46B1-857E-3CA7B92F9D70}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU - {B698561F-FB28-46B1-857E-3CA7B92F9D70}.Release|iPhoneSimulator.Build.0 = Release|Any CPU {6A2D5D58-0261-4A75-BE84-2BE8B076B7C2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {6A2D5D58-0261-4A75-BE84-2BE8B076B7C2}.Debug|Any CPU.Build.0 = Debug|Any CPU - {6A2D5D58-0261-4A75-BE84-2BE8B076B7C2}.Debug|iPhone.ActiveCfg = Debug|Any CPU - {6A2D5D58-0261-4A75-BE84-2BE8B076B7C2}.Debug|iPhone.Build.0 = Debug|Any CPU - {6A2D5D58-0261-4A75-BE84-2BE8B076B7C2}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU - {6A2D5D58-0261-4A75-BE84-2BE8B076B7C2}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU {6A2D5D58-0261-4A75-BE84-2BE8B076B7C2}.Release|Any CPU.ActiveCfg = Release|Any CPU {6A2D5D58-0261-4A75-BE84-2BE8B076B7C2}.Release|Any CPU.Build.0 = Release|Any CPU - {6A2D5D58-0261-4A75-BE84-2BE8B076B7C2}.Release|iPhone.ActiveCfg = Release|Any CPU - {6A2D5D58-0261-4A75-BE84-2BE8B076B7C2}.Release|iPhone.Build.0 = Release|Any CPU - {6A2D5D58-0261-4A75-BE84-2BE8B076B7C2}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU - {6A2D5D58-0261-4A75-BE84-2BE8B076B7C2}.Release|iPhoneSimulator.Build.0 = Release|Any CPU {5672CA4D-1B37-425B-A118-A8DA26E78938}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {5672CA4D-1B37-425B-A118-A8DA26E78938}.Debug|Any CPU.Build.0 = Debug|Any CPU - {5672CA4D-1B37-425B-A118-A8DA26E78938}.Debug|iPhone.ActiveCfg = Debug|Any CPU - {5672CA4D-1B37-425B-A118-A8DA26E78938}.Debug|iPhone.Build.0 = Debug|Any CPU - {5672CA4D-1B37-425B-A118-A8DA26E78938}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU - {5672CA4D-1B37-425B-A118-A8DA26E78938}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU {5672CA4D-1B37-425B-A118-A8DA26E78938}.Release|Any CPU.ActiveCfg = Release|Any CPU {5672CA4D-1B37-425B-A118-A8DA26E78938}.Release|Any CPU.Build.0 = Release|Any CPU - {5672CA4D-1B37-425B-A118-A8DA26E78938}.Release|iPhone.ActiveCfg = Release|Any CPU - {5672CA4D-1B37-425B-A118-A8DA26E78938}.Release|iPhone.Build.0 = Release|Any CPU - {5672CA4D-1B37-425B-A118-A8DA26E78938}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU - {5672CA4D-1B37-425B-A118-A8DA26E78938}.Release|iPhoneSimulator.Build.0 = Release|Any CPU {5789E78D-38F9-4072-AB7B-978F34B2C17F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {5789E78D-38F9-4072-AB7B-978F34B2C17F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {5789E78D-38F9-4072-AB7B-978F34B2C17F}.Debug|iPhone.ActiveCfg = Debug|Any CPU - {5789E78D-38F9-4072-AB7B-978F34B2C17F}.Debug|iPhone.Build.0 = Debug|Any CPU - {5789E78D-38F9-4072-AB7B-978F34B2C17F}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU - {5789E78D-38F9-4072-AB7B-978F34B2C17F}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU {5789E78D-38F9-4072-AB7B-978F34B2C17F}.Release|Any CPU.ActiveCfg = Release|Any CPU {5789E78D-38F9-4072-AB7B-978F34B2C17F}.Release|Any CPU.Build.0 = Release|Any CPU - {5789E78D-38F9-4072-AB7B-978F34B2C17F}.Release|iPhone.ActiveCfg = Release|Any CPU - {5789E78D-38F9-4072-AB7B-978F34B2C17F}.Release|iPhone.Build.0 = Release|Any CPU - {5789E78D-38F9-4072-AB7B-978F34B2C17F}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU - {5789E78D-38F9-4072-AB7B-978F34B2C17F}.Release|iPhoneSimulator.Build.0 = Release|Any CPU - {3F082D0B-A964-43D7-BDF7-C256D76A50D0}.Debug|Any CPU.ActiveCfg = Debug|iPhoneSimulator - {3F082D0B-A964-43D7-BDF7-C256D76A50D0}.Debug|iPhone.ActiveCfg = Debug|iPhone - {3F082D0B-A964-43D7-BDF7-C256D76A50D0}.Debug|iPhone.Build.0 = Debug|iPhone - {3F082D0B-A964-43D7-BDF7-C256D76A50D0}.Debug|iPhoneSimulator.ActiveCfg = Debug|iPhoneSimulator - {3F082D0B-A964-43D7-BDF7-C256D76A50D0}.Debug|iPhoneSimulator.Build.0 = Debug|iPhoneSimulator - {3F082D0B-A964-43D7-BDF7-C256D76A50D0}.Release|Any CPU.ActiveCfg = Release|iPhone - {3F082D0B-A964-43D7-BDF7-C256D76A50D0}.Release|iPhone.ActiveCfg = Release|iPhone - {3F082D0B-A964-43D7-BDF7-C256D76A50D0}.Release|iPhone.Build.0 = Release|iPhone - {3F082D0B-A964-43D7-BDF7-C256D76A50D0}.Release|iPhoneSimulator.ActiveCfg = Release|iPhoneSimulator - {3F082D0B-A964-43D7-BDF7-C256D76A50D0}.Release|iPhoneSimulator.Build.0 = Release|iPhoneSimulator - {65FF8E19-6934-469B-B690-23C6D6E56A17}.Debug|Any CPU.ActiveCfg = Debug|iPhoneSimulator - {65FF8E19-6934-469B-B690-23C6D6E56A17}.Debug|iPhone.ActiveCfg = Debug|iPhone - {65FF8E19-6934-469B-B690-23C6D6E56A17}.Debug|iPhone.Build.0 = Debug|iPhone - {65FF8E19-6934-469B-B690-23C6D6E56A17}.Debug|iPhoneSimulator.ActiveCfg = Debug|iPhoneSimulator - {65FF8E19-6934-469B-B690-23C6D6E56A17}.Debug|iPhoneSimulator.Build.0 = Debug|iPhoneSimulator - {65FF8E19-6934-469B-B690-23C6D6E56A17}.Release|Any CPU.ActiveCfg = Release|iPhone - {65FF8E19-6934-469B-B690-23C6D6E56A17}.Release|iPhone.ActiveCfg = Release|iPhone - {65FF8E19-6934-469B-B690-23C6D6E56A17}.Release|iPhone.Build.0 = Release|iPhone - {65FF8E19-6934-469B-B690-23C6D6E56A17}.Release|iPhoneSimulator.ActiveCfg = Release|iPhoneSimulator - {65FF8E19-6934-469B-B690-23C6D6E56A17}.Release|iPhoneSimulator.Build.0 = Release|iPhoneSimulator - {7E408809-66AC-49D1-AF4D-98834F9B979A}.Debug|Any CPU.ActiveCfg = Debug|iPhoneSimulator - {7E408809-66AC-49D1-AF4D-98834F9B979A}.Debug|iPhone.ActiveCfg = Debug|iPhone - {7E408809-66AC-49D1-AF4D-98834F9B979A}.Debug|iPhone.Build.0 = Debug|iPhone - {7E408809-66AC-49D1-AF4D-98834F9B979A}.Debug|iPhoneSimulator.ActiveCfg = Debug|iPhoneSimulator - {7E408809-66AC-49D1-AF4D-98834F9B979A}.Debug|iPhoneSimulator.Build.0 = Debug|iPhoneSimulator - {7E408809-66AC-49D1-AF4D-98834F9B979A}.Release|Any CPU.ActiveCfg = Release|iPhone - {7E408809-66AC-49D1-AF4D-98834F9B979A}.Release|iPhone.ActiveCfg = Release|iPhone - {7E408809-66AC-49D1-AF4D-98834F9B979A}.Release|iPhone.Build.0 = Release|iPhone - {7E408809-66AC-49D1-AF4D-98834F9B979A}.Release|iPhoneSimulator.ActiveCfg = Release|iPhoneSimulator - {7E408809-66AC-49D1-AF4D-98834F9B979A}.Release|iPhoneSimulator.Build.0 = Release|iPhoneSimulator - {6653CA6F-DB06-4604-A3FD-762E25C2AF96}.Debug|Any CPU.ActiveCfg = Debug|iPhoneSimulator - {6653CA6F-DB06-4604-A3FD-762E25C2AF96}.Debug|iPhone.ActiveCfg = Debug|iPhone - {6653CA6F-DB06-4604-A3FD-762E25C2AF96}.Debug|iPhone.Build.0 = Debug|iPhone - {6653CA6F-DB06-4604-A3FD-762E25C2AF96}.Debug|iPhoneSimulator.ActiveCfg = Debug|iPhoneSimulator - {6653CA6F-DB06-4604-A3FD-762E25C2AF96}.Debug|iPhoneSimulator.Build.0 = Debug|iPhoneSimulator - {6653CA6F-DB06-4604-A3FD-762E25C2AF96}.Release|Any CPU.ActiveCfg = Release|iPhone - {6653CA6F-DB06-4604-A3FD-762E25C2AF96}.Release|iPhone.ActiveCfg = Release|iPhone - {6653CA6F-DB06-4604-A3FD-762E25C2AF96}.Release|iPhone.Build.0 = Release|iPhone - {6653CA6F-DB06-4604-A3FD-762E25C2AF96}.Release|iPhoneSimulator.ActiveCfg = Release|iPhoneSimulator - {6653CA6F-DB06-4604-A3FD-762E25C2AF96}.Release|iPhoneSimulator.Build.0 = Release|iPhoneSimulator - {39FD990E-B6CE-4B2A-999F-BC008CF2C64C}.Debug|Any CPU.ActiveCfg = Debug|iPhoneSimulator - {39FD990E-B6CE-4B2A-999F-BC008CF2C64C}.Debug|iPhone.ActiveCfg = Debug|iPhone - {39FD990E-B6CE-4B2A-999F-BC008CF2C64C}.Debug|iPhone.Build.0 = Debug|iPhone - {39FD990E-B6CE-4B2A-999F-BC008CF2C64C}.Debug|iPhoneSimulator.ActiveCfg = Debug|iPhoneSimulator - {39FD990E-B6CE-4B2A-999F-BC008CF2C64C}.Debug|iPhoneSimulator.Build.0 = Debug|iPhoneSimulator - {39FD990E-B6CE-4B2A-999F-BC008CF2C64C}.Release|Any CPU.ActiveCfg = Release|iPhone - {39FD990E-B6CE-4B2A-999F-BC008CF2C64C}.Release|iPhone.ActiveCfg = Release|iPhone - {39FD990E-B6CE-4B2A-999F-BC008CF2C64C}.Release|iPhone.Build.0 = Release|iPhone - {39FD990E-B6CE-4B2A-999F-BC008CF2C64C}.Release|iPhoneSimulator.ActiveCfg = Release|iPhoneSimulator - {39FD990E-B6CE-4B2A-999F-BC008CF2C64C}.Release|iPhoneSimulator.Build.0 = Release|iPhoneSimulator - {4004C7B7-1A62-43F1-9DF2-52450FA67E70}.Debug|Any CPU.ActiveCfg = Debug|iPhoneSimulator - {4004C7B7-1A62-43F1-9DF2-52450FA67E70}.Debug|iPhone.ActiveCfg = Debug|iPhone - {4004C7B7-1A62-43F1-9DF2-52450FA67E70}.Debug|iPhone.Build.0 = Debug|iPhone - {4004C7B7-1A62-43F1-9DF2-52450FA67E70}.Debug|iPhoneSimulator.ActiveCfg = Debug|iPhoneSimulator - {4004C7B7-1A62-43F1-9DF2-52450FA67E70}.Debug|iPhoneSimulator.Build.0 = Debug|iPhoneSimulator - {4004C7B7-1A62-43F1-9DF2-52450FA67E70}.Release|Any CPU.ActiveCfg = Release|iPhone - {4004C7B7-1A62-43F1-9DF2-52450FA67E70}.Release|iPhone.ActiveCfg = Release|iPhone - {4004C7B7-1A62-43F1-9DF2-52450FA67E70}.Release|iPhone.Build.0 = Release|iPhone - {4004C7B7-1A62-43F1-9DF2-52450FA67E70}.Release|iPhoneSimulator.ActiveCfg = Release|iPhoneSimulator - {4004C7B7-1A62-43F1-9DF2-52450FA67E70}.Release|iPhoneSimulator.Build.0 = Release|iPhoneSimulator + {3F082D0B-A964-43D7-BDF7-C256D76A50D0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3F082D0B-A964-43D7-BDF7-C256D76A50D0}.Release|Any CPU.Build.0 = Release|Any CPU + {3F082D0B-A964-43D7-BDF7-C256D76A50D0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3F082D0B-A964-43D7-BDF7-C256D76A50D0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {65FF8E19-6934-469B-B690-23C6D6E56A17}.Release|Any CPU.ActiveCfg = Release|Any CPU + {65FF8E19-6934-469B-B690-23C6D6E56A17}.Release|Any CPU.Build.0 = Release|Any CPU + {65FF8E19-6934-469B-B690-23C6D6E56A17}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {65FF8E19-6934-469B-B690-23C6D6E56A17}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7E408809-66AC-49D1-AF4D-98834F9B979A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7E408809-66AC-49D1-AF4D-98834F9B979A}.Release|Any CPU.Build.0 = Release|Any CPU + {7E408809-66AC-49D1-AF4D-98834F9B979A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7E408809-66AC-49D1-AF4D-98834F9B979A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6653CA6F-DB06-4604-A3FD-762E25C2AF96}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6653CA6F-DB06-4604-A3FD-762E25C2AF96}.Release|Any CPU.Build.0 = Release|Any CPU + {6653CA6F-DB06-4604-A3FD-762E25C2AF96}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6653CA6F-DB06-4604-A3FD-762E25C2AF96}.Debug|Any CPU.Build.0 = Debug|Any CPU + {39FD990E-B6CE-4B2A-999F-BC008CF2C64C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {39FD990E-B6CE-4B2A-999F-BC008CF2C64C}.Release|Any CPU.Build.0 = Release|Any CPU + {39FD990E-B6CE-4B2A-999F-BC008CF2C64C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {39FD990E-B6CE-4B2A-999F-BC008CF2C64C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4004C7B7-1A62-43F1-9DF2-52450FA67E70}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4004C7B7-1A62-43F1-9DF2-52450FA67E70}.Release|Any CPU.Build.0 = Release|Any CPU + {4004C7B7-1A62-43F1-9DF2-52450FA67E70}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4004C7B7-1A62-43F1-9DF2-52450FA67E70}.Debug|Any CPU.Build.0 = Debug|Any CPU {D1D5F9A8-B40B-40E6-B02F-482D03346D3D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {D1D5F9A8-B40B-40E6-B02F-482D03346D3D}.Debug|Any CPU.Build.0 = Debug|Any CPU {D1D5F9A8-B40B-40E6-B02F-482D03346D3D}.Debug|Any CPU.Deploy.0 = Debug|Any CPU - {D1D5F9A8-B40B-40E6-B02F-482D03346D3D}.Debug|iPhone.ActiveCfg = Debug|Any CPU - {D1D5F9A8-B40B-40E6-B02F-482D03346D3D}.Debug|iPhone.Build.0 = Debug|Any CPU - {D1D5F9A8-B40B-40E6-B02F-482D03346D3D}.Debug|iPhone.Deploy.0 = Debug|Any CPU - {D1D5F9A8-B40B-40E6-B02F-482D03346D3D}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU - {D1D5F9A8-B40B-40E6-B02F-482D03346D3D}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU - {D1D5F9A8-B40B-40E6-B02F-482D03346D3D}.Debug|iPhoneSimulator.Deploy.0 = Debug|Any CPU {D1D5F9A8-B40B-40E6-B02F-482D03346D3D}.Release|Any CPU.ActiveCfg = Release|Any CPU {D1D5F9A8-B40B-40E6-B02F-482D03346D3D}.Release|Any CPU.Build.0 = Release|Any CPU {D1D5F9A8-B40B-40E6-B02F-482D03346D3D}.Release|Any CPU.Deploy.0 = Release|Any CPU - {D1D5F9A8-B40B-40E6-B02F-482D03346D3D}.Release|iPhone.ActiveCfg = Release|Any CPU - {D1D5F9A8-B40B-40E6-B02F-482D03346D3D}.Release|iPhone.Build.0 = Release|Any CPU - {D1D5F9A8-B40B-40E6-B02F-482D03346D3D}.Release|iPhone.Deploy.0 = Release|Any CPU - {D1D5F9A8-B40B-40E6-B02F-482D03346D3D}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU - {D1D5F9A8-B40B-40E6-B02F-482D03346D3D}.Release|iPhoneSimulator.Build.0 = Release|Any CPU - {D1D5F9A8-B40B-40E6-B02F-482D03346D3D}.Release|iPhoneSimulator.Deploy.0 = Release|Any CPU {C5379ECB-3A94-4D2F-AC3B-2615AC23EB0D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {C5379ECB-3A94-4D2F-AC3B-2615AC23EB0D}.Debug|Any CPU.Build.0 = Debug|Any CPU {C5379ECB-3A94-4D2F-AC3B-2615AC23EB0D}.Debug|Any CPU.Deploy.0 = Debug|Any CPU - {C5379ECB-3A94-4D2F-AC3B-2615AC23EB0D}.Debug|iPhone.ActiveCfg = Debug|Any CPU - {C5379ECB-3A94-4D2F-AC3B-2615AC23EB0D}.Debug|iPhone.Build.0 = Debug|Any CPU - {C5379ECB-3A94-4D2F-AC3B-2615AC23EB0D}.Debug|iPhone.Deploy.0 = Debug|Any CPU - {C5379ECB-3A94-4D2F-AC3B-2615AC23EB0D}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU - {C5379ECB-3A94-4D2F-AC3B-2615AC23EB0D}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU - {C5379ECB-3A94-4D2F-AC3B-2615AC23EB0D}.Debug|iPhoneSimulator.Deploy.0 = Debug|Any CPU {C5379ECB-3A94-4D2F-AC3B-2615AC23EB0D}.Release|Any CPU.ActiveCfg = Release|Any CPU {C5379ECB-3A94-4D2F-AC3B-2615AC23EB0D}.Release|Any CPU.Build.0 = Release|Any CPU {C5379ECB-3A94-4D2F-AC3B-2615AC23EB0D}.Release|Any CPU.Deploy.0 = Release|Any CPU - {C5379ECB-3A94-4D2F-AC3B-2615AC23EB0D}.Release|iPhone.ActiveCfg = Release|Any CPU - {C5379ECB-3A94-4D2F-AC3B-2615AC23EB0D}.Release|iPhone.Build.0 = Release|Any CPU - {C5379ECB-3A94-4D2F-AC3B-2615AC23EB0D}.Release|iPhone.Deploy.0 = Release|Any CPU - {C5379ECB-3A94-4D2F-AC3B-2615AC23EB0D}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU - {C5379ECB-3A94-4D2F-AC3B-2615AC23EB0D}.Release|iPhoneSimulator.Build.0 = Release|Any CPU - {C5379ECB-3A94-4D2F-AC3B-2615AC23EB0D}.Release|iPhoneSimulator.Deploy.0 = Release|Any CPU {531F1092-DB27-445D-AA33-2A77C7187C99}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {531F1092-DB27-445D-AA33-2A77C7187C99}.Debug|Any CPU.Build.0 = Debug|Any CPU {531F1092-DB27-445D-AA33-2A77C7187C99}.Debug|Any CPU.Deploy.0 = Debug|Any CPU - {531F1092-DB27-445D-AA33-2A77C7187C99}.Debug|iPhone.ActiveCfg = Debug|Any CPU - {531F1092-DB27-445D-AA33-2A77C7187C99}.Debug|iPhone.Build.0 = Debug|Any CPU - {531F1092-DB27-445D-AA33-2A77C7187C99}.Debug|iPhone.Deploy.0 = Debug|Any CPU - {531F1092-DB27-445D-AA33-2A77C7187C99}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU - {531F1092-DB27-445D-AA33-2A77C7187C99}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU - {531F1092-DB27-445D-AA33-2A77C7187C99}.Debug|iPhoneSimulator.Deploy.0 = Debug|Any CPU {531F1092-DB27-445D-AA33-2A77C7187C99}.Release|Any CPU.ActiveCfg = Release|Any CPU {531F1092-DB27-445D-AA33-2A77C7187C99}.Release|Any CPU.Build.0 = Release|Any CPU {531F1092-DB27-445D-AA33-2A77C7187C99}.Release|Any CPU.Deploy.0 = Release|Any CPU - {531F1092-DB27-445D-AA33-2A77C7187C99}.Release|iPhone.ActiveCfg = Release|Any CPU - {531F1092-DB27-445D-AA33-2A77C7187C99}.Release|iPhone.Build.0 = Release|Any CPU - {531F1092-DB27-445D-AA33-2A77C7187C99}.Release|iPhone.Deploy.0 = Release|Any CPU - {531F1092-DB27-445D-AA33-2A77C7187C99}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU - {531F1092-DB27-445D-AA33-2A77C7187C99}.Release|iPhoneSimulator.Build.0 = Release|Any CPU - {531F1092-DB27-445D-AA33-2A77C7187C99}.Release|iPhoneSimulator.Deploy.0 = Release|Any CPU {90CAB706-39CB-4B93-9629-3218A6FF8E9B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {90CAB706-39CB-4B93-9629-3218A6FF8E9B}.Debug|Any CPU.Build.0 = Debug|Any CPU {90CAB706-39CB-4B93-9629-3218A6FF8E9B}.Debug|Any CPU.Deploy.0 = Debug|Any CPU - {90CAB706-39CB-4B93-9629-3218A6FF8E9B}.Debug|iPhone.ActiveCfg = Debug|Any CPU - {90CAB706-39CB-4B93-9629-3218A6FF8E9B}.Debug|iPhone.Build.0 = Debug|Any CPU - {90CAB706-39CB-4B93-9629-3218A6FF8E9B}.Debug|iPhone.Deploy.0 = Debug|Any CPU - {90CAB706-39CB-4B93-9629-3218A6FF8E9B}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU - {90CAB706-39CB-4B93-9629-3218A6FF8E9B}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU - {90CAB706-39CB-4B93-9629-3218A6FF8E9B}.Debug|iPhoneSimulator.Deploy.0 = Debug|Any CPU {90CAB706-39CB-4B93-9629-3218A6FF8E9B}.Release|Any CPU.ActiveCfg = Release|Any CPU {90CAB706-39CB-4B93-9629-3218A6FF8E9B}.Release|Any CPU.Build.0 = Release|Any CPU {90CAB706-39CB-4B93-9629-3218A6FF8E9B}.Release|Any CPU.Deploy.0 = Release|Any CPU - {90CAB706-39CB-4B93-9629-3218A6FF8E9B}.Release|iPhone.ActiveCfg = Release|Any CPU - {90CAB706-39CB-4B93-9629-3218A6FF8E9B}.Release|iPhone.Build.0 = Release|Any CPU - {90CAB706-39CB-4B93-9629-3218A6FF8E9B}.Release|iPhone.Deploy.0 = Release|Any CPU - {90CAB706-39CB-4B93-9629-3218A6FF8E9B}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU - {90CAB706-39CB-4B93-9629-3218A6FF8E9B}.Release|iPhoneSimulator.Build.0 = Release|Any CPU - {90CAB706-39CB-4B93-9629-3218A6FF8E9B}.Release|iPhoneSimulator.Deploy.0 = Release|Any CPU {3701A0A1-8476-42C6-B5C4-D24129B4A484}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {3701A0A1-8476-42C6-B5C4-D24129B4A484}.Debug|Any CPU.Build.0 = Debug|Any CPU {3701A0A1-8476-42C6-B5C4-D24129B4A484}.Debug|Any CPU.Deploy.0 = Debug|Any CPU - {3701A0A1-8476-42C6-B5C4-D24129B4A484}.Debug|iPhone.ActiveCfg = Debug|Any CPU - {3701A0A1-8476-42C6-B5C4-D24129B4A484}.Debug|iPhone.Build.0 = Debug|Any CPU - {3701A0A1-8476-42C6-B5C4-D24129B4A484}.Debug|iPhone.Deploy.0 = Debug|Any CPU - {3701A0A1-8476-42C6-B5C4-D24129B4A484}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU - {3701A0A1-8476-42C6-B5C4-D24129B4A484}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU - {3701A0A1-8476-42C6-B5C4-D24129B4A484}.Debug|iPhoneSimulator.Deploy.0 = Debug|Any CPU {3701A0A1-8476-42C6-B5C4-D24129B4A484}.Release|Any CPU.ActiveCfg = Release|Any CPU {3701A0A1-8476-42C6-B5C4-D24129B4A484}.Release|Any CPU.Build.0 = Release|Any CPU {3701A0A1-8476-42C6-B5C4-D24129B4A484}.Release|Any CPU.Deploy.0 = Release|Any CPU - {3701A0A1-8476-42C6-B5C4-D24129B4A484}.Release|iPhone.ActiveCfg = Release|Any CPU - {3701A0A1-8476-42C6-B5C4-D24129B4A484}.Release|iPhone.Build.0 = Release|Any CPU - {3701A0A1-8476-42C6-B5C4-D24129B4A484}.Release|iPhone.Deploy.0 = Release|Any CPU - {3701A0A1-8476-42C6-B5C4-D24129B4A484}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU - {3701A0A1-8476-42C6-B5C4-D24129B4A484}.Release|iPhoneSimulator.Build.0 = Release|Any CPU - {3701A0A1-8476-42C6-B5C4-D24129B4A484}.Release|iPhoneSimulator.Deploy.0 = Release|Any CPU {5CC222DC-5716-4499-B897-DCBDDA4A5CF9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {5CC222DC-5716-4499-B897-DCBDDA4A5CF9}.Debug|Any CPU.Build.0 = Debug|Any CPU {5CC222DC-5716-4499-B897-DCBDDA4A5CF9}.Debug|Any CPU.Deploy.0 = Debug|Any CPU - {5CC222DC-5716-4499-B897-DCBDDA4A5CF9}.Debug|iPhone.ActiveCfg = Debug|Any CPU - {5CC222DC-5716-4499-B897-DCBDDA4A5CF9}.Debug|iPhone.Build.0 = Debug|Any CPU - {5CC222DC-5716-4499-B897-DCBDDA4A5CF9}.Debug|iPhone.Deploy.0 = Debug|Any CPU - {5CC222DC-5716-4499-B897-DCBDDA4A5CF9}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU - {5CC222DC-5716-4499-B897-DCBDDA4A5CF9}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU - {5CC222DC-5716-4499-B897-DCBDDA4A5CF9}.Debug|iPhoneSimulator.Deploy.0 = Debug|Any CPU {5CC222DC-5716-4499-B897-DCBDDA4A5CF9}.Release|Any CPU.ActiveCfg = Release|Any CPU {5CC222DC-5716-4499-B897-DCBDDA4A5CF9}.Release|Any CPU.Build.0 = Release|Any CPU {5CC222DC-5716-4499-B897-DCBDDA4A5CF9}.Release|Any CPU.Deploy.0 = Release|Any CPU - {5CC222DC-5716-4499-B897-DCBDDA4A5CF9}.Release|iPhone.ActiveCfg = Release|Any CPU - {5CC222DC-5716-4499-B897-DCBDDA4A5CF9}.Release|iPhone.Build.0 = Release|Any CPU - {5CC222DC-5716-4499-B897-DCBDDA4A5CF9}.Release|iPhone.Deploy.0 = Release|Any CPU - {5CC222DC-5716-4499-B897-DCBDDA4A5CF9}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU - {5CC222DC-5716-4499-B897-DCBDDA4A5CF9}.Release|iPhoneSimulator.Build.0 = Release|Any CPU - {5CC222DC-5716-4499-B897-DCBDDA4A5CF9}.Release|iPhoneSimulator.Deploy.0 = Release|Any CPU {93632F2D-2BB4-46C1-A7B8-F8CF2FB27118}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {93632F2D-2BB4-46C1-A7B8-F8CF2FB27118}.Debug|Any CPU.Build.0 = Debug|Any CPU - {93632F2D-2BB4-46C1-A7B8-F8CF2FB27118}.Debug|iPhone.ActiveCfg = Debug|Any CPU - {93632F2D-2BB4-46C1-A7B8-F8CF2FB27118}.Debug|iPhone.Build.0 = Debug|Any CPU - {93632F2D-2BB4-46C1-A7B8-F8CF2FB27118}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU - {93632F2D-2BB4-46C1-A7B8-F8CF2FB27118}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU {93632F2D-2BB4-46C1-A7B8-F8CF2FB27118}.Release|Any CPU.ActiveCfg = Release|Any CPU {93632F2D-2BB4-46C1-A7B8-F8CF2FB27118}.Release|Any CPU.Build.0 = Release|Any CPU - {93632F2D-2BB4-46C1-A7B8-F8CF2FB27118}.Release|iPhone.ActiveCfg = Release|Any CPU - {93632F2D-2BB4-46C1-A7B8-F8CF2FB27118}.Release|iPhone.Build.0 = Release|Any CPU - {93632F2D-2BB4-46C1-A7B8-F8CF2FB27118}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU - {93632F2D-2BB4-46C1-A7B8-F8CF2FB27118}.Release|iPhoneSimulator.Build.0 = Release|Any CPU {9014CA66-5217-49F6-8C1E-3430FD08EF61}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {9014CA66-5217-49F6-8C1E-3430FD08EF61}.Debug|Any CPU.Build.0 = Debug|Any CPU - {9014CA66-5217-49F6-8C1E-3430FD08EF61}.Debug|iPhone.ActiveCfg = Debug|Any CPU - {9014CA66-5217-49F6-8C1E-3430FD08EF61}.Debug|iPhone.Build.0 = Debug|Any CPU - {9014CA66-5217-49F6-8C1E-3430FD08EF61}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU - {9014CA66-5217-49F6-8C1E-3430FD08EF61}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU {9014CA66-5217-49F6-8C1E-3430FD08EF61}.Release|Any CPU.ActiveCfg = Release|Any CPU {9014CA66-5217-49F6-8C1E-3430FD08EF61}.Release|Any CPU.Build.0 = Release|Any CPU - {9014CA66-5217-49F6-8C1E-3430FD08EF61}.Release|iPhone.ActiveCfg = Release|Any CPU - {9014CA66-5217-49F6-8C1E-3430FD08EF61}.Release|iPhone.Build.0 = Release|Any CPU - {9014CA66-5217-49F6-8C1E-3430FD08EF61}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU - {9014CA66-5217-49F6-8C1E-3430FD08EF61}.Release|iPhoneSimulator.Build.0 = Release|Any CPU {561DFD5E-5896-40D1-9708-4D692F5BAE66}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {561DFD5E-5896-40D1-9708-4D692F5BAE66}.Debug|Any CPU.Build.0 = Debug|Any CPU - {561DFD5E-5896-40D1-9708-4D692F5BAE66}.Debug|iPhone.ActiveCfg = Debug|Any CPU - {561DFD5E-5896-40D1-9708-4D692F5BAE66}.Debug|iPhone.Build.0 = Debug|Any CPU - {561DFD5E-5896-40D1-9708-4D692F5BAE66}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU - {561DFD5E-5896-40D1-9708-4D692F5BAE66}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU {561DFD5E-5896-40D1-9708-4D692F5BAE66}.Release|Any CPU.ActiveCfg = Release|Any CPU {561DFD5E-5896-40D1-9708-4D692F5BAE66}.Release|Any CPU.Build.0 = Release|Any CPU - {561DFD5E-5896-40D1-9708-4D692F5BAE66}.Release|iPhone.ActiveCfg = Release|Any CPU - {561DFD5E-5896-40D1-9708-4D692F5BAE66}.Release|iPhone.Build.0 = Release|Any CPU - {561DFD5E-5896-40D1-9708-4D692F5BAE66}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU - {561DFD5E-5896-40D1-9708-4D692F5BAE66}.Release|iPhoneSimulator.Build.0 = Release|Any CPU {B325271C-85E7-4DB3-8BBB-B70F242954F8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {B325271C-85E7-4DB3-8BBB-B70F242954F8}.Debug|Any CPU.Build.0 = Debug|Any CPU - {B325271C-85E7-4DB3-8BBB-B70F242954F8}.Debug|iPhone.ActiveCfg = Debug|Any CPU - {B325271C-85E7-4DB3-8BBB-B70F242954F8}.Debug|iPhone.Build.0 = Debug|Any CPU - {B325271C-85E7-4DB3-8BBB-B70F242954F8}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU - {B325271C-85E7-4DB3-8BBB-B70F242954F8}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU {B325271C-85E7-4DB3-8BBB-B70F242954F8}.Release|Any CPU.ActiveCfg = Release|Any CPU {B325271C-85E7-4DB3-8BBB-B70F242954F8}.Release|Any CPU.Build.0 = Release|Any CPU - {B325271C-85E7-4DB3-8BBB-B70F242954F8}.Release|iPhone.ActiveCfg = Release|Any CPU - {B325271C-85E7-4DB3-8BBB-B70F242954F8}.Release|iPhone.Build.0 = Release|Any CPU - {B325271C-85E7-4DB3-8BBB-B70F242954F8}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU - {B325271C-85E7-4DB3-8BBB-B70F242954F8}.Release|iPhoneSimulator.Build.0 = Release|Any CPU {4C834F7F-07CA-46C7-8C7B-F10A1B3BC738}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {4C834F7F-07CA-46C7-8C7B-F10A1B3BC738}.Debug|Any CPU.Build.0 = Debug|Any CPU - {4C834F7F-07CA-46C7-8C7B-F10A1B3BC738}.Debug|iPhone.ActiveCfg = Debug|Any CPU - {4C834F7F-07CA-46C7-8C7B-F10A1B3BC738}.Debug|iPhone.Build.0 = Debug|Any CPU - {4C834F7F-07CA-46C7-8C7B-F10A1B3BC738}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU - {4C834F7F-07CA-46C7-8C7B-F10A1B3BC738}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU {4C834F7F-07CA-46C7-8C7B-F10A1B3BC738}.Release|Any CPU.ActiveCfg = Release|Any CPU {4C834F7F-07CA-46C7-8C7B-F10A1B3BC738}.Release|Any CPU.Build.0 = Release|Any CPU - {4C834F7F-07CA-46C7-8C7B-F10A1B3BC738}.Release|iPhone.ActiveCfg = Release|Any CPU - {4C834F7F-07CA-46C7-8C7B-F10A1B3BC738}.Release|iPhone.Build.0 = Release|Any CPU - {4C834F7F-07CA-46C7-8C7B-F10A1B3BC738}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU - {4C834F7F-07CA-46C7-8C7B-F10A1B3BC738}.Release|iPhoneSimulator.Build.0 = Release|Any CPU {AD923016-F318-49B7-B08B-89DED6DC2422}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {AD923016-F318-49B7-B08B-89DED6DC2422}.Debug|Any CPU.Build.0 = Debug|Any CPU - {AD923016-F318-49B7-B08B-89DED6DC2422}.Debug|iPhone.ActiveCfg = Debug|Any CPU - {AD923016-F318-49B7-B08B-89DED6DC2422}.Debug|iPhone.Build.0 = Debug|Any CPU - {AD923016-F318-49B7-B08B-89DED6DC2422}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU - {AD923016-F318-49B7-B08B-89DED6DC2422}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU {AD923016-F318-49B7-B08B-89DED6DC2422}.Release|Any CPU.ActiveCfg = Release|Any CPU {AD923016-F318-49B7-B08B-89DED6DC2422}.Release|Any CPU.Build.0 = Release|Any CPU - {AD923016-F318-49B7-B08B-89DED6DC2422}.Release|iPhone.ActiveCfg = Release|Any CPU - {AD923016-F318-49B7-B08B-89DED6DC2422}.Release|iPhone.Build.0 = Release|Any CPU - {AD923016-F318-49B7-B08B-89DED6DC2422}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU - {AD923016-F318-49B7-B08B-89DED6DC2422}.Release|iPhoneSimulator.Build.0 = Release|Any CPU {B9B92246-02EB-4118-9C6F-85A0D726AA70}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {B9B92246-02EB-4118-9C6F-85A0D726AA70}.Debug|Any CPU.Build.0 = Debug|Any CPU - {B9B92246-02EB-4118-9C6F-85A0D726AA70}.Debug|iPhone.ActiveCfg = Debug|Any CPU - {B9B92246-02EB-4118-9C6F-85A0D726AA70}.Debug|iPhone.Build.0 = Debug|Any CPU - {B9B92246-02EB-4118-9C6F-85A0D726AA70}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU - {B9B92246-02EB-4118-9C6F-85A0D726AA70}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU {B9B92246-02EB-4118-9C6F-85A0D726AA70}.Release|Any CPU.ActiveCfg = Release|Any CPU {B9B92246-02EB-4118-9C6F-85A0D726AA70}.Release|Any CPU.Build.0 = Release|Any CPU - {B9B92246-02EB-4118-9C6F-85A0D726AA70}.Release|iPhone.ActiveCfg = Release|Any CPU - {B9B92246-02EB-4118-9C6F-85A0D726AA70}.Release|iPhone.Build.0 = Release|Any CPU - {B9B92246-02EB-4118-9C6F-85A0D726AA70}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU - {B9B92246-02EB-4118-9C6F-85A0D726AA70}.Release|iPhoneSimulator.Build.0 = Release|Any CPU {B9022390-8184-4548-9DB1-50EB8878D20A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {B9022390-8184-4548-9DB1-50EB8878D20A}.Debug|Any CPU.Build.0 = Debug|Any CPU - {B9022390-8184-4548-9DB1-50EB8878D20A}.Debug|iPhone.ActiveCfg = Debug|Any CPU - {B9022390-8184-4548-9DB1-50EB8878D20A}.Debug|iPhone.Build.0 = Debug|Any CPU - {B9022390-8184-4548-9DB1-50EB8878D20A}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU - {B9022390-8184-4548-9DB1-50EB8878D20A}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU {B9022390-8184-4548-9DB1-50EB8878D20A}.Release|Any CPU.ActiveCfg = Release|Any CPU {B9022390-8184-4548-9DB1-50EB8878D20A}.Release|Any CPU.Build.0 = Release|Any CPU - {B9022390-8184-4548-9DB1-50EB8878D20A}.Release|iPhone.ActiveCfg = Release|Any CPU - {B9022390-8184-4548-9DB1-50EB8878D20A}.Release|iPhone.Build.0 = Release|Any CPU - {B9022390-8184-4548-9DB1-50EB8878D20A}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU - {B9022390-8184-4548-9DB1-50EB8878D20A}.Release|iPhoneSimulator.Build.0 = Release|Any CPU {1743BF7C-E6AE-4A06-BAD9-166D62894303}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {1743BF7C-E6AE-4A06-BAD9-166D62894303}.Debug|Any CPU.Build.0 = Debug|Any CPU - {1743BF7C-E6AE-4A06-BAD9-166D62894303}.Debug|iPhone.ActiveCfg = Debug|Any CPU - {1743BF7C-E6AE-4A06-BAD9-166D62894303}.Debug|iPhone.Build.0 = Debug|Any CPU - {1743BF7C-E6AE-4A06-BAD9-166D62894303}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU - {1743BF7C-E6AE-4A06-BAD9-166D62894303}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU {1743BF7C-E6AE-4A06-BAD9-166D62894303}.Release|Any CPU.ActiveCfg = Release|Any CPU {1743BF7C-E6AE-4A06-BAD9-166D62894303}.Release|Any CPU.Build.0 = Release|Any CPU - {1743BF7C-E6AE-4A06-BAD9-166D62894303}.Release|iPhone.ActiveCfg = Release|Any CPU - {1743BF7C-E6AE-4A06-BAD9-166D62894303}.Release|iPhone.Build.0 = Release|Any CPU - {1743BF7C-E6AE-4A06-BAD9-166D62894303}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU - {1743BF7C-E6AE-4A06-BAD9-166D62894303}.Release|iPhoneSimulator.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From d84d0310e09b00d120b263238c8c00349dc56d21 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 8 Aug 2024 12:56:03 +0900 Subject: [PATCH 1880/2493] Move mute button to master volume circle --- osu.Game/Overlays/Volume/MasterVolumeMeter.cs | 54 +++++++++++++++++++ osu.Game/Overlays/Volume/MuteButton.cs | 12 ++--- osu.Game/Overlays/Volume/VolumeMeter.cs | 16 +++--- osu.Game/Overlays/VolumeOverlay.cs | 53 ++++++++---------- 4 files changed, 93 insertions(+), 42 deletions(-) create mode 100644 osu.Game/Overlays/Volume/MasterVolumeMeter.cs diff --git a/osu.Game/Overlays/Volume/MasterVolumeMeter.cs b/osu.Game/Overlays/Volume/MasterVolumeMeter.cs new file mode 100644 index 0000000000..951a6d53b1 --- /dev/null +++ b/osu.Game/Overlays/Volume/MasterVolumeMeter.cs @@ -0,0 +1,54 @@ +// 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.Audio; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osuTK.Graphics; + +namespace osu.Game.Overlays.Volume +{ + public partial class MasterVolumeMeter : VolumeMeter + { + private MuteButton muteButton = null!; + + public Bindable IsMuted { get; } = new Bindable(); + + private readonly BindableDouble muteAdjustment = new BindableDouble(); + + [Resolved] + private VolumeOverlay volumeOverlay { get; set; } = null!; + + public MasterVolumeMeter(string name, float circleSize, Color4 meterColour) + : base(name, circleSize, meterColour) + { + } + + [BackgroundDependencyLoader] + private void load(AudioManager audio) + { + IsMuted.BindValueChanged(muted => + { + if (muted.NewValue) + audio.AddAdjustment(AdjustableProperty.Volume, muteAdjustment); + else + audio.RemoveAdjustment(AdjustableProperty.Volume, muteAdjustment); + }); + + Add(muteButton = new MuteButton + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.Centre, + Blending = BlendingParameters.Additive, + X = CircleSize / 2, + Y = CircleSize * 0.23f, + Current = { BindTarget = IsMuted } + }); + + muteButton.Current.ValueChanged += _ => volumeOverlay.Show(); + } + + public void ToggleMute() => muteButton.Current.Value = !muteButton.Current.Value; + } +} diff --git a/osu.Game/Overlays/Volume/MuteButton.cs b/osu.Game/Overlays/Volume/MuteButton.cs index 1dc8d754b7..a04d79bd20 100644 --- a/osu.Game/Overlays/Volume/MuteButton.cs +++ b/osu.Game/Overlays/Volume/MuteButton.cs @@ -35,16 +35,16 @@ namespace osu.Game.Overlays.Volume private Color4 hoveredColour, unhoveredColour; - private const float width = 100; - public const float HEIGHT = 35; - public MuteButton() { + const float width = 30; + const float height = 30; + Content.BorderThickness = 3; - Content.CornerRadius = HEIGHT / 2; + Content.CornerRadius = height / 2; Content.CornerExponent = 2; - Size = new Vector2(width, HEIGHT); + Size = new Vector2(width, height); Action = () => Current.Value = !Current.Value; } @@ -52,7 +52,7 @@ namespace osu.Game.Overlays.Volume [BackgroundDependencyLoader] private void load(OsuColour colours) { - hoveredColour = colours.YellowDark; + hoveredColour = colours.PinkLight; Content.BorderColour = unhoveredColour = colours.Gray1; BackgroundColour = colours.Gray1; diff --git a/osu.Game/Overlays/Volume/VolumeMeter.cs b/osu.Game/Overlays/Volume/VolumeMeter.cs index e96cd0fa46..9e0c599386 100644 --- a/osu.Game/Overlays/Volume/VolumeMeter.cs +++ b/osu.Game/Overlays/Volume/VolumeMeter.cs @@ -35,8 +35,12 @@ namespace osu.Game.Overlays.Volume private CircularProgress volumeCircle; private CircularProgress volumeCircleGlow; + protected static readonly Vector2 LABEL_SIZE = new Vector2(120, 20); + public BindableDouble Bindable { get; } = new BindableDouble { MinValue = 0, MaxValue = 1, Precision = 0.01 }; - private readonly float circleSize; + + protected readonly float CircleSize; + private readonly Color4 meterColour; private readonly string name; @@ -73,7 +77,7 @@ namespace osu.Game.Overlays.Volume public VolumeMeter(string name, float circleSize, Color4 meterColour) { - this.circleSize = circleSize; + CircleSize = circleSize; this.meterColour = meterColour; this.name = name; @@ -101,7 +105,7 @@ namespace osu.Game.Overlays.Volume { new Container { - Size = new Vector2(circleSize), + Size = new Vector2(CircleSize), Children = new Drawable[] { new BufferedContainer @@ -199,7 +203,7 @@ namespace osu.Game.Overlays.Volume { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Font = OsuFont.Numeric.With(size: 0.16f * circleSize) + Font = OsuFont.Numeric.With(size: 0.16f * CircleSize) }).WithEffect(new GlowEffect { Colour = Color4.Transparent, @@ -209,10 +213,10 @@ namespace osu.Game.Overlays.Volume }, new Container { - Size = new Vector2(120, 20), + Size = LABEL_SIZE, CornerRadius = 10, Masking = true, - Margin = new MarginPadding { Left = circleSize + 10 }, + Margin = new MarginPadding { Left = CircleSize + 10 }, Origin = Anchor.CentreLeft, Anchor = Anchor.CentreLeft, Children = new Drawable[] diff --git a/osu.Game/Overlays/VolumeOverlay.cs b/osu.Game/Overlays/VolumeOverlay.cs index 6f9861c703..0d801ff118 100644 --- a/osu.Game/Overlays/VolumeOverlay.cs +++ b/osu.Game/Overlays/VolumeOverlay.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.Linq; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Bindables; @@ -20,21 +21,19 @@ using osuTK.Graphics; namespace osu.Game.Overlays { + [Cached] public partial class VolumeOverlay : VisibilityContainer { + public Bindable IsMuted { get; } = new Bindable(); + private const float offset = 10; private VolumeMeter volumeMeterMaster = null!; private VolumeMeter volumeMeterEffect = null!; private VolumeMeter volumeMeterMusic = null!; - private MuteButton muteButton = null!; private SelectionCycleFillFlowContainer volumeMeters = null!; - private readonly BindableDouble muteAdjustment = new BindableDouble(); - - public Bindable IsMuted { get; } = new Bindable(); - [BackgroundDependencyLoader] private void load(AudioManager audio, OsuColour colours) { @@ -49,14 +48,7 @@ namespace osu.Game.Overlays Width = 300, Colour = ColourInfo.GradientHorizontal(Color4.Black.Opacity(0.75f), Color4.Black.Opacity(0)) }, - muteButton = new MuteButton - { - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - Margin = new MarginPadding(10), - Current = { BindTarget = IsMuted } - }, - volumeMeters = new SelectionCycleFillFlowContainer + new FillFlowContainer { Direction = FillDirection.Vertical, AutoSizeAxes = Axes.Both, @@ -64,26 +56,29 @@ namespace osu.Game.Overlays Origin = Anchor.CentreLeft, Spacing = new Vector2(0, offset), Margin = new MarginPadding { Left = offset }, - Children = new[] + Children = new Drawable[] { - volumeMeterEffect = new VolumeMeter("EFFECTS", 125, colours.BlueDarker), - volumeMeterMaster = new VolumeMeter("MASTER", 150, colours.PinkDarker), - volumeMeterMusic = new VolumeMeter("MUSIC", 125, colours.BlueDarker), - } - } + volumeMeters = new SelectionCycleFillFlowContainer + { + Direction = FillDirection.Vertical, + AutoSizeAxes = Axes.Both, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Spacing = new Vector2(0, offset), + Children = new[] + { + volumeMeterEffect = new VolumeMeter("EFFECTS", 125, colours.BlueDarker), + volumeMeterMaster = new MasterVolumeMeter("MASTER", 150, colours.PinkDarker) { IsMuted = { BindTarget = IsMuted }, }, + volumeMeterMusic = new VolumeMeter("MUSIC", 125, colours.BlueDarker), + } + }, + }, + }, }); volumeMeterMaster.Bindable.BindTo(audio.Volume); volumeMeterEffect.Bindable.BindTo(audio.VolumeSample); volumeMeterMusic.Bindable.BindTo(audio.VolumeTrack); - - IsMuted.BindValueChanged(muted => - { - if (muted.NewValue) - audio.AddAdjustment(AdjustableProperty.Volume, muteAdjustment); - else - audio.RemoveAdjustment(AdjustableProperty.Volume, muteAdjustment); - }); } protected override void LoadComplete() @@ -92,8 +87,6 @@ namespace osu.Game.Overlays foreach (var volumeMeter in volumeMeters) volumeMeter.Bindable.ValueChanged += _ => Show(); - - muteButton.Current.ValueChanged += _ => Show(); } public bool Adjust(GlobalAction action, float amount = 1, bool isPrecise = false) @@ -130,7 +123,7 @@ namespace osu.Game.Overlays case GlobalAction.ToggleMute: Show(); - muteButton.Current.Value = !muteButton.Current.Value; + volumeMeters.OfType().First().ToggleMute(); return true; } From 0cb3b6a1f8e2668d9593eddaf9d0abe0af6936fd Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 8 Aug 2024 22:10:26 +0900 Subject: [PATCH 1881/2493] Add back `TrySetDailyChallengeBeatmap` call for safety --- osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs index 9b22d368a7..5b341956bb 100644 --- a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs @@ -458,6 +458,8 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge }, TaskContinuationOptions.OnlyOnRanToCompletion); userModsSelectOverlay.SelectedItem.Value = playlistItem; + + TrySetDailyChallengeBeatmap(this, beatmapManager, rulesets, musicController, playlistItem); } public override void OnResuming(ScreenTransitionEvent e) From 80c814008f32b348d0cb6322ca4bb2f89607c440 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 9 Aug 2024 00:19:36 +0900 Subject: [PATCH 1882/2493] Update in line with new changes --- .../Legacy/CatchLegacySkinTransformer.cs | 3 +- .../Argon/ManiaArgonSkinTransformer.cs | 50 ++++++++--------- .../Legacy/LegacyManiaComboCounter.cs | 3 +- .../Legacy/ManiaLegacySkinTransformer.cs | 48 ++++++++-------- .../Legacy/OsuLegacySkinTransformer.cs | 55 ++++++++++--------- .../Visual/Gameplay/TestSceneSkinEditor.cs | 2 +- osu.Game/Skinning/LegacySkin.cs | 17 ++++-- 7 files changed, 93 insertions(+), 85 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs b/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs index 44fc3ecc07..df8c04638d 100644 --- a/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs @@ -4,7 +4,6 @@ using System.Linq; using osu.Framework.Bindables; using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; using osu.Game.Skinning; using osuTK; using osuTK.Graphics; @@ -37,7 +36,7 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy // Modifications for global components. if (containerLookup.Ruleset == null) - return base.GetDrawableComponent(lookup) as Container; + return base.GetDrawableComponent(lookup); // Skin has configuration. if (base.GetDrawableComponent(lookup) is UserConfiguredLayoutContainer d) diff --git a/osu.Game.Rulesets.Mania/Skinning/Argon/ManiaArgonSkinTransformer.cs b/osu.Game.Rulesets.Mania/Skinning/Argon/ManiaArgonSkinTransformer.cs index b0a6086f2a..f541cea0f5 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Argon/ManiaArgonSkinTransformer.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Argon/ManiaArgonSkinTransformer.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Diagnostics; using System.Linq; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -15,7 +14,7 @@ using osuTK.Graphics; namespace osu.Game.Rulesets.Mania.Skinning.Argon { - public class ManiaArgonSkinTransformer : ArgonSkinTransformer + public class ManiaArgonSkinTransformer : SkinTransformer { private readonly ManiaBeatmap beatmap; @@ -30,32 +29,31 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon switch (lookup) { case SkinComponentsContainerLookup containerLookup: - switch (containerLookup.Target) + if (containerLookup.Target != SkinComponentsContainerLookup.TargetArea.MainHUDComponents) + return base.GetDrawableComponent(lookup); + + // Only handle per ruleset defaults here. + if (containerLookup.Ruleset == null) + return base.GetDrawableComponent(lookup); + + // Skin has configuration. + if (base.GetDrawableComponent(lookup) is UserConfiguredLayoutContainer d) + return d; + + return new DefaultSkinComponentsContainer(container => { - case SkinComponentsContainerLookup.TargetArea.MainHUDComponents when containerLookup.Ruleset != null: - Debug.Assert(containerLookup.Ruleset.ShortName == ManiaRuleset.SHORT_NAME); + var combo = container.ChildrenOfType().FirstOrDefault(); - var rulesetHUDComponents = Skin.GetDrawableComponent(lookup); - - rulesetHUDComponents ??= new DefaultSkinComponentsContainer(container => - { - var combo = container.ChildrenOfType().FirstOrDefault(); - - if (combo != null) - { - combo.Anchor = Anchor.TopCentre; - combo.Origin = Anchor.Centre; - combo.Y = 200; - } - }) - { - new ArgonManiaComboCounter(), - }; - - return rulesetHUDComponents; - } - - break; + if (combo != null) + { + combo.Anchor = Anchor.TopCentre; + combo.Origin = Anchor.Centre; + combo.Y = 200; + } + }) + { + new ArgonManiaComboCounter(), + }; case GameplaySkinComponentLookup resultComponent: // This should eventually be moved to a skin setting, when supported. diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyManiaComboCounter.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyManiaComboCounter.cs index 00619834c8..a51a50c604 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyManiaComboCounter.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyManiaComboCounter.cs @@ -3,7 +3,6 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Graphics; using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Skinning; @@ -49,7 +48,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy private void updateAnchor() { // if the anchor isn't a vertical center, set top or bottom anchor based on scroll direction - if (!Anchor.HasFlagFast(Anchor.y1)) + if (!Anchor.HasFlag(Anchor.y1)) { Anchor &= ~(Anchor.y0 | Anchor.y2); Anchor |= direction.Value == ScrollingDirection.Up ? Anchor.y2 : Anchor.y0; diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs index c539c239bd..6ac6f6ed18 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs @@ -5,7 +5,6 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.Linq; using osu.Framework.Audio.Sample; using osu.Framework.Bindables; @@ -82,32 +81,31 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy switch (lookup) { case SkinComponentsContainerLookup containerLookup: - switch (containerLookup.Target) + if (containerLookup.Target != SkinComponentsContainerLookup.TargetArea.MainHUDComponents) + return base.GetDrawableComponent(lookup); + + // Modifications for global components. + if (containerLookup.Ruleset == null) + return base.GetDrawableComponent(lookup); + + // Skin has configuration. + if (base.GetDrawableComponent(lookup) is UserConfiguredLayoutContainer d) + return d; + + return new DefaultSkinComponentsContainer(container => { - case SkinComponentsContainerLookup.TargetArea.MainHUDComponents when containerLookup.Ruleset != null: - Debug.Assert(containerLookup.Ruleset.ShortName == ManiaRuleset.SHORT_NAME); + var combo = container.ChildrenOfType().FirstOrDefault(); - var rulesetHUDComponents = Skin.GetDrawableComponent(lookup); - - rulesetHUDComponents ??= new DefaultSkinComponentsContainer(container => - { - var combo = container.ChildrenOfType().FirstOrDefault(); - - if (combo != null) - { - combo.Anchor = Anchor.TopCentre; - combo.Origin = Anchor.Centre; - combo.Y = this.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.ComboPosition)?.Value ?? 0; - } - }) - { - new LegacyManiaComboCounter(), - }; - - return rulesetHUDComponents; - } - - break; + if (combo != null) + { + combo.Anchor = Anchor.TopCentre; + combo.Origin = Anchor.Centre; + combo.Y = this.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.ComboPosition)?.Value ?? 0; + } + }) + { + new LegacyManiaComboCounter(), + }; case GameplaySkinComponentLookup resultComponent: return getResult(resultComponent.Component); diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs index 9a8eaa7d7d..c2381fff88 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs @@ -45,6 +45,9 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy switch (lookup) { case SkinComponentsContainerLookup containerLookup: + if (containerLookup.Target != SkinComponentsContainerLookup.TargetArea.MainHUDComponents) + return base.GetDrawableComponent(lookup); + // Only handle per ruleset defaults here. if (containerLookup.Ruleset == null) return base.GetDrawableComponent(lookup); @@ -53,34 +56,36 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy if (base.GetDrawableComponent(lookup) is UserConfiguredLayoutContainer d) return d; - // Our own ruleset components default. - switch (containerLookup.Target) + return new DefaultSkinComponentsContainer(container => { - case SkinComponentsContainerLookup.TargetArea.MainHUDComponents: - return new DefaultSkinComponentsContainer(container => - { - var keyCounter = container.OfType().FirstOrDefault(); + var keyCounter = container.OfType().FirstOrDefault(); - if (keyCounter != null) - { - // set the anchor to top right so that it won't squash to the return button to the top - keyCounter.Anchor = Anchor.CentreRight; - keyCounter.Origin = Anchor.CentreRight; - keyCounter.X = 0; - // 340px is the default height inherit from stable - keyCounter.Y = container.ToLocalSpace(new Vector2(0, container.ScreenSpaceDrawQuad.Centre.Y - 340f)).Y; - } - }) - { - Children = new Drawable[] - { - new LegacyComboCounter(), - new LegacyKeyCounterDisplay(), - } - }; - } + if (keyCounter != null) + { + // set the anchor to top right so that it won't squash to the return button to the top + keyCounter.Anchor = Anchor.CentreRight; + keyCounter.Origin = Anchor.CentreRight; + keyCounter.X = 0; + // 340px is the default height inherit from stable + keyCounter.Y = container.ToLocalSpace(new Vector2(0, container.ScreenSpaceDrawQuad.Centre.Y - 340f)).Y; + } - return null; + var combo = container.OfType().FirstOrDefault(); + + if (combo != null) + { + combo.Anchor = Anchor.BottomLeft; + combo.Origin = Anchor.BottomLeft; + combo.Scale = new Vector2(1.28f); + } + }) + { + Children = new Drawable[] + { + new LegacyDefaultComboCounter(), + new LegacyKeyCounterDisplay(), + } + }; case OsuSkinComponentLookup osuComponent: switch (osuComponent.Component) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs index f44daa1ecb..7466442674 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs @@ -443,7 +443,7 @@ namespace osu.Game.Tests.Visual.Gameplay AddAssert("no combo in global target", () => !globalHUDTarget.Components.OfType().Any()); AddAssert("combo placed in ruleset target", () => rulesetHUDTarget.Components.OfType().Count() == 1); - AddStep("add combo to global target", () => globalHUDTarget.Add(new LegacyComboCounter + AddStep("add combo to global target", () => globalHUDTarget.Add(new LegacyDefaultComboCounter { Anchor = Anchor.Centre, Origin = Anchor.Centre, diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index 2da09839e9..8f6e634dd6 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -13,7 +13,6 @@ using osu.Framework.Audio.Sample; using osu.Framework.Bindables; using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Textures; using osu.Framework.IO.Stores; using osu.Game.Audio; @@ -24,6 +23,7 @@ using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Scoring; using osu.Game.Screens.Play.HUD; using osu.Game.Screens.Play.HUD.HitErrorMeters; +using osuTK; using osuTK.Graphics; namespace osu.Game.Skinning @@ -367,10 +367,19 @@ namespace osu.Game.Skinning case SkinComponentsContainerLookup.TargetArea.MainHUDComponents: if (containerLookup.Ruleset != null) { - return new Container + return new DefaultSkinComponentsContainer(container => { - RelativeSizeAxes = Axes.Both, - Child = new LegacyComboCounter(), + var combo = container.OfType().FirstOrDefault(); + + if (combo != null) + { + combo.Anchor = Anchor.BottomLeft; + combo.Origin = Anchor.BottomLeft; + combo.Scale = new Vector2(1.28f); + } + }) + { + new LegacyDefaultComboCounter() }; } From 7666e8b9320b8e84a4ff621cc53bf477518b7b1a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 9 Aug 2024 15:11:57 +0900 Subject: [PATCH 1883/2493] Remove `SupportsClosestAnchor` for the time being This may have had a good reason to be added, but I can't find that reason, so let's keep things simple for the time being. --- .../Skinning/Argon/ArgonManiaComboCounter.cs | 5 +---- .../Skinning/Legacy/LegacyManiaComboCounter.cs | 4 +--- osu.Game/Overlays/SkinEditor/SkinEditor.cs | 3 --- osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs | 3 +-- osu.Game/Skinning/ISerialisableDrawable.cs | 8 -------- 5 files changed, 3 insertions(+), 20 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonManiaComboCounter.cs b/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonManiaComboCounter.cs index ad515528fb..9a4eea993d 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonManiaComboCounter.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonManiaComboCounter.cs @@ -10,20 +10,17 @@ using osu.Game.Graphics.Sprites; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Screens.Play.HUD; -using osu.Game.Skinning; using osuTK.Graphics; namespace osu.Game.Rulesets.Mania.Skinning.Argon { - public partial class ArgonManiaComboCounter : ComboCounter, ISerialisableDrawable + public partial class ArgonManiaComboCounter : ComboCounter { private OsuSpriteText text = null!; protected override double RollingDuration => 500; protected override Easing RollingEasing => Easing.OutQuint; - bool ISerialisableDrawable.SupportsClosestAnchor => false; - [BackgroundDependencyLoader] private void load(ScoreProcessor scoreProcessor) { diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyManiaComboCounter.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyManiaComboCounter.cs index a51a50c604..c1fe4a1028 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyManiaComboCounter.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyManiaComboCounter.cs @@ -11,10 +11,8 @@ using osuTK.Graphics; namespace osu.Game.Rulesets.Mania.Skinning.Legacy { - public partial class LegacyManiaComboCounter : LegacyComboCounter, ISerialisableDrawable + public partial class LegacyManiaComboCounter : LegacyComboCounter { - bool ISerialisableDrawable.SupportsClosestAnchor => false; - [BackgroundDependencyLoader] private void load(ISkinSource skin) { diff --git a/osu.Game/Overlays/SkinEditor/SkinEditor.cs b/osu.Game/Overlays/SkinEditor/SkinEditor.cs index 7bbd875542..484af34603 100644 --- a/osu.Game/Overlays/SkinEditor/SkinEditor.cs +++ b/osu.Game/Overlays/SkinEditor/SkinEditor.cs @@ -444,9 +444,6 @@ namespace osu.Game.Overlays.SkinEditor drawableComponent.Origin = Anchor.TopCentre; drawableComponent.Anchor = Anchor.TopCentre; drawableComponent.Y = targetContainer.DrawSize.Y / 2; - - if (!component.SupportsClosestAnchor) - component.UsesFixedAnchor = true; } try diff --git a/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs b/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs index c86221c7fb..722ffd6d07 100644 --- a/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs +++ b/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs @@ -103,8 +103,7 @@ namespace osu.Game.Overlays.SkinEditor { var closestItem = new TernaryStateRadioMenuItem("Closest", MenuItemType.Standard, _ => applyClosestAnchors()) { - State = { Value = GetStateFromSelection(selection, c => !c.Item.UsesFixedAnchor), }, - Action = { Disabled = selection.Any(c => !c.Item.SupportsClosestAnchor) }, + State = { Value = GetStateFromSelection(selection, c => !c.Item.UsesFixedAnchor) } }; yield return new OsuMenuItem("Anchor") diff --git a/osu.Game/Skinning/ISerialisableDrawable.cs b/osu.Game/Skinning/ISerialisableDrawable.cs index 898186bcc1..c9dcaca6d1 100644 --- a/osu.Game/Skinning/ISerialisableDrawable.cs +++ b/osu.Game/Skinning/ISerialisableDrawable.cs @@ -27,14 +27,6 @@ namespace osu.Game.Skinning /// bool IsEditable => true; - /// - /// Whether this component supports the "closest" anchor. - /// - /// - /// This is disabled by some components that shift position automatically. - /// - bool SupportsClosestAnchor => true; - /// /// In the context of the skin layout editor, whether this has a permanent anchor defined. /// If , this 's is automatically determined by proximity, From 3f20f05801d8a23d970b2954cd5b956e929284c9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 9 Aug 2024 15:39:01 +0900 Subject: [PATCH 1884/2493] Remove unnecessary `UsesFixedAnchor` specifications --- .../Skinning/Argon/ArgonManiaComboCounter.cs | 2 -- .../Skinning/Legacy/LegacyManiaComboCounter.cs | 2 -- 2 files changed, 4 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonManiaComboCounter.cs b/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonManiaComboCounter.cs index 9a4eea993d..16f2109896 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonManiaComboCounter.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonManiaComboCounter.cs @@ -37,8 +37,6 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon text.FadeOut(200, Easing.InQuint); } }); - - UsesFixedAnchor = true; } [Resolved] diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyManiaComboCounter.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyManiaComboCounter.cs index c1fe4a1028..5832210836 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyManiaComboCounter.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyManiaComboCounter.cs @@ -22,8 +22,6 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy PopOutCountText.Anchor = Anchor.Centre; PopOutCountText.Origin = Anchor.Centre; PopOutCountText.Colour = skin.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.ComboBreakColour)?.Value ?? Color4.Red; - - UsesFixedAnchor = true; } [Resolved] From 161734af954994ba2095cb77ca19498ba9fdf08f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 9 Aug 2024 15:46:57 +0900 Subject: [PATCH 1885/2493] Simplify argon mania combo counter implementation by sharing with base counter --- .../Skinning/Argon/ArgonManiaComboCounter.cs | 36 +------------------ .../Argon/ManiaArgonSkinTransformer.cs | 1 + .../Screens/Play/HUD/ArgonComboCounter.cs | 14 ++++---- 3 files changed, 9 insertions(+), 42 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonManiaComboCounter.cs b/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonManiaComboCounter.cs index 16f2109896..e77650bed1 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonManiaComboCounter.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonManiaComboCounter.cs @@ -4,41 +4,13 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; -using osu.Framework.Graphics.Sprites; -using osu.Game.Graphics; -using osu.Game.Graphics.Sprites; -using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Screens.Play.HUD; -using osuTK.Graphics; namespace osu.Game.Rulesets.Mania.Skinning.Argon { - public partial class ArgonManiaComboCounter : ComboCounter + public partial class ArgonManiaComboCounter : ArgonComboCounter { - private OsuSpriteText text = null!; - - protected override double RollingDuration => 500; - protected override Easing RollingEasing => Easing.OutQuint; - - [BackgroundDependencyLoader] - private void load(ScoreProcessor scoreProcessor) - { - Current.BindTo(scoreProcessor.Combo); - Current.BindValueChanged(combo => - { - if (combo.OldValue == 0 && combo.NewValue > 0) - text.FadeIn(200, Easing.OutQuint); - else if (combo.OldValue > 0 && combo.NewValue == 0) - { - if (combo.OldValue > 1) - text.FlashColour(Color4.Red, 2000, Easing.OutQuint); - - text.FadeOut(200, Easing.InQuint); - } - }); - } - [Resolved] private IScrollingInfo scrollingInfo { get; set; } = null!; @@ -47,7 +19,6 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon protected override void LoadComplete() { base.LoadComplete(); - text.Alpha = Current.Value > 0 ? 1 : 0; direction = scrollingInfo.Direction.GetBoundCopy(); direction.BindValueChanged(_ => updateAnchor()); @@ -67,10 +38,5 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon if ((Y < 0 && direction.Value == ScrollingDirection.Down) || (Y > 0 && direction.Value == ScrollingDirection.Up)) Y = -Y; } - - protected override IHasText CreateText() => text = new OsuSpriteText - { - Font = OsuFont.Torus.With(size: 32, fixedWidth: true), - }; } } diff --git a/osu.Game.Rulesets.Mania/Skinning/Argon/ManiaArgonSkinTransformer.cs b/osu.Game.Rulesets.Mania/Skinning/Argon/ManiaArgonSkinTransformer.cs index f541cea0f5..224db77f59 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Argon/ManiaArgonSkinTransformer.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Argon/ManiaArgonSkinTransformer.cs @@ -46,6 +46,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon if (combo != null) { + combo.ShowLabel.Value = false; combo.Anchor = Anchor.TopCentre; combo.Origin = Anchor.Centre; combo.Y = 200; diff --git a/osu.Game/Screens/Play/HUD/ArgonComboCounter.cs b/osu.Game/Screens/Play/HUD/ArgonComboCounter.cs index db0480c566..3f74a8d4e8 100644 --- a/osu.Game/Screens/Play/HUD/ArgonComboCounter.cs +++ b/osu.Game/Screens/Play/HUD/ArgonComboCounter.cs @@ -19,7 +19,7 @@ namespace osu.Game.Screens.Play.HUD { public partial class ArgonComboCounter : ComboCounter { - private ArgonCounterTextComponent text = null!; + protected ArgonCounterTextComponent Text = null!; protected override double RollingDuration => 250; @@ -43,16 +43,16 @@ namespace osu.Game.Screens.Play.HUD bool wasIncrease = combo.NewValue > combo.OldValue; bool wasMiss = combo.OldValue > 1 && combo.NewValue == 0; - float newScale = Math.Clamp(text.NumberContainer.Scale.X * (wasIncrease ? 1.1f : 0.8f), 0.6f, 1.4f); + float newScale = Math.Clamp(Text.NumberContainer.Scale.X * (wasIncrease ? 1.1f : 0.8f), 0.6f, 1.4f); float duration = wasMiss ? 2000 : 500; - text.NumberContainer + Text.NumberContainer .ScaleTo(new Vector2(newScale)) .ScaleTo(Vector2.One, duration, Easing.OutQuint); if (wasMiss) - text.FlashColour(Color4.Red, duration, Easing.OutQuint); + Text.FlashColour(Color4.Red, duration, Easing.OutQuint); }); } @@ -70,8 +70,8 @@ namespace osu.Game.Screens.Play.HUD { int digitsRequiredForDisplayCount = getDigitsRequiredForDisplayCount(); - if (digitsRequiredForDisplayCount != text.WireframeTemplate.Length) - text.WireframeTemplate = new string('#', digitsRequiredForDisplayCount); + if (digitsRequiredForDisplayCount != Text.WireframeTemplate.Length) + Text.WireframeTemplate = new string('#', digitsRequiredForDisplayCount); } private int getDigitsRequiredForDisplayCount() @@ -86,7 +86,7 @@ namespace osu.Game.Screens.Play.HUD protected override LocalisableString FormatCount(int count) => $@"{count}x"; - protected override IHasText CreateText() => text = new ArgonCounterTextComponent(Anchor.TopLeft, MatchesStrings.MatchScoreStatsCombo.ToUpper()) + protected override IHasText CreateText() => Text = new ArgonCounterTextComponent(Anchor.TopLeft, MatchesStrings.MatchScoreStatsCombo.ToUpper()) { WireframeOpacity = { BindTarget = WireframeOpacity }, ShowLabel = { BindTarget = ShowLabel }, From 2114f092c7422d11d16020c26865f29fd3f2d4c4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 9 Aug 2024 16:31:47 +0900 Subject: [PATCH 1886/2493] Add failing test coverage showing coordinate truncation --- .../Formats/LegacyBeatmapDecoderTest.cs | 34 +++++++++++++++++++ .../Resources/hitobject-coordinates-lazer.osu | 6 ++++ .../hitobject-coordinates-legacy.osu | 5 +++ 3 files changed, 45 insertions(+) create mode 100644 osu.Game.Tests/Resources/hitobject-coordinates-lazer.osu create mode 100644 osu.Game.Tests/Resources/hitobject-coordinates-legacy.osu diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs index 19378821b3..54ebebeb7b 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs @@ -468,6 +468,40 @@ namespace osu.Game.Tests.Beatmaps.Formats } } + [Test] + public void TestDecodeBeatmapHitObjectCoordinatesLegacy() + { + var decoder = new LegacyBeatmapDecoder(); + + using (var resStream = TestResources.OpenResource("hitobject-coordinates-legacy.osu")) + using (var stream = new LineBufferedReader(resStream)) + { + var hitObjects = decoder.Decode(stream).HitObjects; + + var positionData = hitObjects[0] as IHasPosition; + + Assert.IsNotNull(positionData); + Assert.AreEqual(new Vector2(256, 256), positionData!.Position); + } + } + + [Test] + public void TestDecodeBeatmapHitObjectCoordinatesLazer() + { + var decoder = new LegacyBeatmapDecoder(LegacyBeatmapEncoder.FIRST_LAZER_VERSION); + + using (var resStream = TestResources.OpenResource("hitobject-coordinates-lazer.osu")) + using (var stream = new LineBufferedReader(resStream)) + { + var hitObjects = decoder.Decode(stream).HitObjects; + + var positionData = hitObjects[0] as IHasPosition; + + Assert.IsNotNull(positionData); + Assert.AreEqual(new Vector2(256.99853f, 256.001f), positionData!.Position); + } + } + [Test] public void TestDecodeBeatmapHitObjects() { diff --git a/osu.Game.Tests/Resources/hitobject-coordinates-lazer.osu b/osu.Game.Tests/Resources/hitobject-coordinates-lazer.osu new file mode 100644 index 0000000000..bb898a1521 --- /dev/null +++ b/osu.Game.Tests/Resources/hitobject-coordinates-lazer.osu @@ -0,0 +1,6 @@ +osu file format v128 + +[HitObjects] +// Coordinates should be preserves in lazer beatmaps. + +256.99853,256.001,1000,49,0,0:0:0:0: diff --git a/osu.Game.Tests/Resources/hitobject-coordinates-legacy.osu b/osu.Game.Tests/Resources/hitobject-coordinates-legacy.osu new file mode 100644 index 0000000000..e914c2fb36 --- /dev/null +++ b/osu.Game.Tests/Resources/hitobject-coordinates-legacy.osu @@ -0,0 +1,5 @@ +osu file format v14 + +[HitObjects] +// Coordinates should be truncated to int values in legacy beatmaps. +256.99853,256.001,1000,49,0,0:0:0:0: From d072c6a743ffb6a2a3614587703de90588dbe169 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 9 Aug 2024 16:19:25 +0900 Subject: [PATCH 1887/2493] Fix hit object coordinates being truncated to `int` values Closes https://github.com/ppy/osu/issues/29340. --- osu.Game/Beatmaps/Formats/LegacyDecoder.cs | 6 ++++++ osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs | 7 +++++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs index 30a78a16ed..ca4fadf458 100644 --- a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs @@ -18,6 +18,12 @@ namespace osu.Game.Beatmaps.Formats { public const int LATEST_VERSION = 14; + /// + /// The .osu format (beatmap) version. + /// + /// osu!stable's versions end at . + /// osu!lazer's versions starts at . + /// protected readonly int FormatVersion; protected LegacyDecoder(int version) diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs index 37a87462ca..8e6ffa20cc 100644 --- a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs @@ -32,7 +32,7 @@ namespace osu.Game.Rulesets.Objects.Legacy protected readonly double Offset; /// - /// The beatmap version. + /// The .osu format (beatmap) version. /// protected readonly int FormatVersion; @@ -48,7 +48,10 @@ namespace osu.Game.Rulesets.Objects.Legacy { string[] split = text.Split(','); - Vector2 pos = new Vector2((int)Parsing.ParseFloat(split[0], Parsing.MAX_COORDINATE_VALUE), (int)Parsing.ParseFloat(split[1], Parsing.MAX_COORDINATE_VALUE)); + Vector2 pos = + FormatVersion >= LegacyBeatmapEncoder.FIRST_LAZER_VERSION + ? new Vector2(Parsing.ParseFloat(split[0], Parsing.MAX_COORDINATE_VALUE), Parsing.ParseFloat(split[1], Parsing.MAX_COORDINATE_VALUE)) + : new Vector2((int)Parsing.ParseFloat(split[0], Parsing.MAX_COORDINATE_VALUE), (int)Parsing.ParseFloat(split[1], Parsing.MAX_COORDINATE_VALUE)); double startTime = Parsing.ParseDouble(split[2]) + Offset; From 52b2d73e046dd4958658b75212d161dbbe176077 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 9 Aug 2024 17:10:18 +0900 Subject: [PATCH 1888/2493] Only show daily challenge notification if it started within the last 30 minutes --- osu.Game/Screens/Menu/DailyChallengeButton.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/Menu/DailyChallengeButton.cs b/osu.Game/Screens/Menu/DailyChallengeButton.cs index a5616b95a0..b1f276b2ec 100644 --- a/osu.Game/Screens/Menu/DailyChallengeButton.cs +++ b/osu.Game/Screens/Menu/DailyChallengeButton.cs @@ -104,8 +104,7 @@ namespace osu.Game.Screens.Menu { base.LoadComplete(); - info.BindValueChanged(_ => dailyChallengeChanged(postNotification: true)); - dailyChallengeChanged(postNotification: false); + info.BindValueChanged(dailyChallengeChanged, true); } protected override void Update() @@ -131,28 +130,29 @@ namespace osu.Game.Screens.Menu } } - private void dailyChallengeChanged(bool postNotification) + private void dailyChallengeChanged(ValueChangedEvent info) { UpdateState(); scheduledCountdownUpdate?.Cancel(); scheduledCountdownUpdate = null; - if (info.Value == null) + if (this.info.Value == null) { Room = null; cover.OnlineInfo = TooltipContent = null; } else { - var roomRequest = new GetRoomRequest(info.Value.Value.RoomID); + var roomRequest = new GetRoomRequest(this.info.Value.Value.RoomID); roomRequest.Success += room => { Room = room; cover.OnlineInfo = TooltipContent = room.Playlist.FirstOrDefault()?.Beatmap.BeatmapSet as APIBeatmapSet; - if (postNotification) + // We only want to notify the user if a new challenge recently went live. + if (Math.Abs((DateTimeOffset.Now - room.StartDate.Value!.Value).TotalSeconds) < 600) notificationOverlay?.Post(new NewDailyChallengeNotification(room)); updateCountdown(); From e146c8e23083307c5ea2f222243b16bf612bbcc4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 9 Aug 2024 17:11:11 +0900 Subject: [PATCH 1889/2493] Ensure only one daily challenge notification is fired per room --- osu.Game/Screens/Menu/DailyChallengeButton.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Menu/DailyChallengeButton.cs b/osu.Game/Screens/Menu/DailyChallengeButton.cs index b1f276b2ec..ac04afdc4d 100644 --- a/osu.Game/Screens/Menu/DailyChallengeButton.cs +++ b/osu.Game/Screens/Menu/DailyChallengeButton.cs @@ -130,6 +130,8 @@ namespace osu.Game.Screens.Menu } } + private long? lastNotifiedDailyChallengeRoomId; + private void dailyChallengeChanged(ValueChangedEvent info) { UpdateState(); @@ -152,8 +154,11 @@ namespace osu.Game.Screens.Menu cover.OnlineInfo = TooltipContent = room.Playlist.FirstOrDefault()?.Beatmap.BeatmapSet as APIBeatmapSet; // We only want to notify the user if a new challenge recently went live. - if (Math.Abs((DateTimeOffset.Now - room.StartDate.Value!.Value).TotalSeconds) < 600) + if (Math.Abs((DateTimeOffset.Now - room.StartDate.Value!.Value).TotalSeconds) < 600 && room.RoomID.Value != lastNotifiedDailyChallengeRoomId) + { + lastNotifiedDailyChallengeRoomId = room.RoomID.Value; notificationOverlay?.Post(new NewDailyChallengeNotification(room)); + } updateCountdown(); Scheduler.AddDelayed(updateCountdown, 1000, true); From f6ada68e47b8f686b8d7a78a43652c67cda48ca0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 9 Aug 2024 17:26:54 +0900 Subject: [PATCH 1890/2493] Fix migration failure due to change in class name --- osu.Game/Skinning/Skin.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Skinning/Skin.cs b/osu.Game/Skinning/Skin.cs index a885a0083d..3a83815f0e 100644 --- a/osu.Game/Skinning/Skin.cs +++ b/osu.Game/Skinning/Skin.cs @@ -251,13 +251,15 @@ namespace osu.Game.Skinning { case 1: { + // Combo counters were moved out of the global HUD components into per-ruleset. + // This is to allow some rulesets to customise further (ie. mania and catch moving the combo to within their play area). if (target != SkinComponentsContainerLookup.TargetArea.MainHUDComponents || !layout.TryGetDrawableInfo(null, out var globalHUDComponents) || resources == null) break; var comboCounters = globalHUDComponents.Where(c => - c.Type.Name == nameof(LegacyComboCounter) || + c.Type.Name == nameof(LegacyDefaultComboCounter) || c.Type.Name == nameof(DefaultComboCounter) || c.Type.Name == nameof(ArgonComboCounter)).ToArray(); From 0a8e342830059a0c71a5b0515038d70146319e28 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 9 Aug 2024 17:37:39 +0900 Subject: [PATCH 1891/2493] Fix occasionally `ChatOverlay` test failures due to RNG usage See https://github.com/ppy/osu/actions/runs/10302758137/job/28517150950. Same ID gets chosen twice for PM channel. --- osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs index 58feab4ebb..7f9b9acf1c 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs @@ -717,9 +717,12 @@ namespace osu.Game.Tests.Visual.Online Type = ChannelType.Public, }; + private static int privateChannelUser = DummyAPIAccess.DUMMY_USER_ID + 1; + private Channel createPrivateChannel() { - int id = RNG.Next(0, DummyAPIAccess.DUMMY_USER_ID - 1); + int id = Interlocked.Increment(ref privateChannelUser); + return new Channel(new APIUser { Id = id, From 18c80870d81e004a0b8979015bcb15a135db00a8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 9 Aug 2024 17:40:48 +0900 Subject: [PATCH 1892/2493] Update one more RNG usage in same tests --- osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs index 7f9b9acf1c..372cf60853 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs @@ -19,7 +19,6 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Input; using osu.Framework.Logging; using osu.Framework.Testing; -using osu.Framework.Utils; using osu.Game.Configuration; using osu.Game.Graphics.UserInterface; using osu.Game.Online.API; @@ -122,7 +121,7 @@ namespace osu.Game.Tests.Visual.Online return true; case PostMessageRequest postMessage: - postMessage.TriggerSuccess(new Message(RNG.Next(0, 10000000)) + postMessage.TriggerSuccess(new Message(getNextTestID()) { Content = postMessage.Message.Content, ChannelId = postMessage.Message.ChannelId, @@ -717,11 +716,9 @@ namespace osu.Game.Tests.Visual.Online Type = ChannelType.Public, }; - private static int privateChannelUser = DummyAPIAccess.DUMMY_USER_ID + 1; - private Channel createPrivateChannel() { - int id = Interlocked.Increment(ref privateChannelUser); + int id = getNextTestID(); return new Channel(new APIUser { @@ -742,6 +739,10 @@ namespace osu.Game.Tests.Visual.Online }; } + private static int testId = DummyAPIAccess.DUMMY_USER_ID + 1; + + private static int getNextTestID() => Interlocked.Increment(ref testId); + private partial class TestChatOverlay : ChatOverlay { public bool SlowLoading { get; set; } From c8a77271994778a53eed43a37cbe07a324dc07f4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 9 Aug 2024 17:46:10 +0900 Subject: [PATCH 1893/2493] Make ID retrieval global to all tests and fix multiple other usages --- osu.Game.Tests/Resources/TestResources.cs | 9 +++++++-- .../Visual/Multiplayer/TestSceneMultiplayer.cs | 4 ++-- osu.Game.Tests/Visual/Online/TestSceneChannelList.cs | 8 ++++---- osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs | 9 +++------ 4 files changed, 16 insertions(+), 14 deletions(-) diff --git a/osu.Game.Tests/Resources/TestResources.cs b/osu.Game.Tests/Resources/TestResources.cs index a77dc8d49b..e0572e604c 100644 --- a/osu.Game.Tests/Resources/TestResources.cs +++ b/osu.Game.Tests/Resources/TestResources.cs @@ -73,7 +73,12 @@ namespace osu.Game.Tests.Resources private static string getTempFilename() => temp_storage.GetFullPath(Guid.NewGuid() + ".osz"); - private static int importId; + private static int testId = 1; + + /// + /// Get a unique int value which is incremented each call. + /// + public static int GetNextTestID() => Interlocked.Increment(ref testId); /// /// Create a test beatmap set model. @@ -88,7 +93,7 @@ namespace osu.Game.Tests.Resources RulesetInfo getRuleset() => rulesets?[j++ % rulesets.Length]; - int setId = Interlocked.Increment(ref importId); + int setId = GetNextTestID(); var metadata = new BeatmapMetadata { diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs index 3306b6624e..ad7e211354 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs @@ -139,8 +139,8 @@ namespace osu.Game.Tests.Visual.Multiplayer private void addRandomPlayer() { - int randomUser = RNG.Next(200000, 500000); - multiplayerClient.AddUser(new APIUser { Id = randomUser, Username = $"user {randomUser}" }); + int id = TestResources.GetNextTestID(); + multiplayerClient.AddUser(new APIUser { Id = id, Username = $"user {id}" }); } private void removeLastUser() diff --git a/osu.Game.Tests/Visual/Online/TestSceneChannelList.cs b/osu.Game.Tests/Visual/Online/TestSceneChannelList.cs index a0cca5f53d..5f77e084da 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChannelList.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChannelList.cs @@ -9,13 +9,13 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Testing; -using osu.Framework.Utils; using osu.Game.Graphics.Sprites; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Chat; using osu.Game.Overlays; using osu.Game.Overlays.Chat.ChannelList; using osu.Game.Overlays.Chat.Listing; +using osu.Game.Tests.Resources; namespace osu.Game.Tests.Visual.Online { @@ -160,7 +160,7 @@ namespace osu.Game.Tests.Visual.Online private Channel createRandomPublicChannel() { - int id = RNG.Next(0, 10000); + int id = TestResources.GetNextTestID(); return new Channel { Name = $"#channel-{id}", @@ -171,7 +171,7 @@ namespace osu.Game.Tests.Visual.Online private Channel createRandomPrivateChannel() { - int id = RNG.Next(0, 10000); + int id = TestResources.GetNextTestID(); return new Channel(new APIUser { Id = id, @@ -181,7 +181,7 @@ namespace osu.Game.Tests.Visual.Online private Channel createRandomAnnounceChannel() { - int id = RNG.Next(0, 10000); + int id = TestResources.GetNextTestID(); return new Channel { Name = $"Announce {id}", diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs index 372cf60853..a47205094e 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs @@ -32,6 +32,7 @@ using osu.Game.Overlays.Chat.ChannelList; using osuTK; using osuTK.Input; using osu.Game.Graphics.UserInterfaceV2; +using osu.Game.Tests.Resources; namespace osu.Game.Tests.Visual.Online { @@ -121,7 +122,7 @@ namespace osu.Game.Tests.Visual.Online return true; case PostMessageRequest postMessage: - postMessage.TriggerSuccess(new Message(getNextTestID()) + postMessage.TriggerSuccess(new Message(TestResources.GetNextTestID()) { Content = postMessage.Message.Content, ChannelId = postMessage.Message.ChannelId, @@ -718,7 +719,7 @@ namespace osu.Game.Tests.Visual.Online private Channel createPrivateChannel() { - int id = getNextTestID(); + int id = TestResources.GetNextTestID(); return new Channel(new APIUser { @@ -739,10 +740,6 @@ namespace osu.Game.Tests.Visual.Online }; } - private static int testId = DummyAPIAccess.DUMMY_USER_ID + 1; - - private static int getNextTestID() => Interlocked.Increment(ref testId); - private partial class TestChatOverlay : ChatOverlay { public bool SlowLoading { get; set; } From 8fdd94090b6fde9550ae8c1bac4e51044dc971da Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 9 Aug 2024 18:02:37 +0900 Subject: [PATCH 1894/2493] Show object inspector values during placement --- .../Edit/OsuHitObjectInspector.cs | 10 +++---- .../Compose/Components/HitObjectInspector.cs | 27 +++++++++++++------ 2 files changed, 24 insertions(+), 13 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectInspector.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectInspector.cs index 27e7d5497c..b31fe05995 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectInspector.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectInspector.cs @@ -11,14 +11,14 @@ namespace osu.Game.Rulesets.Osu.Edit { public partial class OsuHitObjectInspector : HitObjectInspector { - protected override void AddInspectorValues() + protected override void AddInspectorValues(HitObject[] objects) { - base.AddInspectorValues(); + base.AddInspectorValues(objects); - if (EditorBeatmap.SelectedHitObjects.Count > 0) + if (objects.Length > 0) { - var firstInSelection = (OsuHitObject)EditorBeatmap.SelectedHitObjects.MinBy(ho => ho.StartTime)!; - var lastInSelection = (OsuHitObject)EditorBeatmap.SelectedHitObjects.MaxBy(ho => ho.GetEndTime())!; + var firstInSelection = (OsuHitObject)objects.MinBy(ho => ho.StartTime)!; + var lastInSelection = (OsuHitObject)objects.MaxBy(ho => ho.GetEndTime())!; Debug.Assert(firstInSelection != null && lastInSelection != null); diff --git a/osu.Game/Screens/Edit/Compose/Components/HitObjectInspector.cs b/osu.Game/Screens/Edit/Compose/Components/HitObjectInspector.cs index de23147e7b..b74a89e3fe 100644 --- a/osu.Game/Screens/Edit/Compose/Components/HitObjectInspector.cs +++ b/osu.Game/Screens/Edit/Compose/Components/HitObjectInspector.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.Linq; using osu.Framework.Extensions.TypeExtensions; using osu.Framework.Threading; @@ -16,6 +17,7 @@ namespace osu.Game.Screens.Edit.Compose.Components base.LoadComplete(); EditorBeatmap.SelectedHitObjects.CollectionChanged += (_, _) => updateInspectorText(); + EditorBeatmap.PlacementObject.BindValueChanged(_ => updateInspectorText()); EditorBeatmap.TransactionBegan += updateInspectorText; EditorBeatmap.TransactionEnded += updateInspectorText; updateInspectorText(); @@ -29,24 +31,33 @@ namespace osu.Game.Screens.Edit.Compose.Components rollingTextUpdate?.Cancel(); rollingTextUpdate = null; - AddInspectorValues(); + HitObject[] objects; + + if (EditorBeatmap.SelectedHitObjects.Count > 0) + objects = EditorBeatmap.SelectedHitObjects.ToArray(); + else if (EditorBeatmap.PlacementObject.Value != null) + objects = new[] { EditorBeatmap.PlacementObject.Value }; + else + objects = Array.Empty(); + + AddInspectorValues(objects); // I'd hope there's a better way to do this, but I don't want to bind to each and every property above to watch for changes. // This is a good middle-ground for the time being. - if (EditorBeatmap.SelectedHitObjects.Count > 0) + if (objects.Length > 0) rollingTextUpdate ??= Scheduler.AddDelayed(updateInspectorText, 250); } - protected virtual void AddInspectorValues() + protected virtual void AddInspectorValues(HitObject[] objects) { - switch (EditorBeatmap.SelectedHitObjects.Count) + switch (objects.Length) { case 0: AddValue("No selection"); break; case 1: - var selected = EditorBeatmap.SelectedHitObjects.Single(); + var selected = objects.Single(); AddHeader("Type"); AddValue($"{selected.GetType().ReadableName()}"); @@ -105,13 +116,13 @@ namespace osu.Game.Screens.Edit.Compose.Components default: AddHeader("Selected Objects"); - AddValue($"{EditorBeatmap.SelectedHitObjects.Count:#,0.##}"); + AddValue($"{objects.Length:#,0.##}"); AddHeader("Start Time"); - AddValue($"{EditorBeatmap.SelectedHitObjects.Min(o => o.StartTime):#,0.##}ms"); + AddValue($"{objects.Min(o => o.StartTime):#,0.##}ms"); AddHeader("End Time"); - AddValue($"{EditorBeatmap.SelectedHitObjects.Max(o => o.GetEndTime()):#,0.##}ms"); + AddValue($"{objects.Max(o => o.GetEndTime()):#,0.##}ms"); break; } } From 3e634a14a4794e8b18f5920c807180896e31603c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 9 Aug 2024 18:43:37 +0900 Subject: [PATCH 1895/2493] Add temporary debug code for multiplayer test failures --- osu.Game/Online/Multiplayer/MultiplayerClient.cs | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index 77ede1fd35..ee2f1d64dc 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -782,7 +782,21 @@ namespace osu.Game.Online.Multiplayer } catch (Exception ex) { - throw new AggregateException($"Item: {JsonConvert.SerializeObject(createPlaylistItem(item))}\n\nRoom:{JsonConvert.SerializeObject(APIRoom)}", ex); + // Temporary code to attempt to figure out long-term failing tests. + bool success = true; + int indexOf = -1234; + + try + { + indexOf = Room.Playlist.IndexOf(Room.Playlist.Single(existing => existing.ID == item.ID)); + Room.Playlist[indexOf] = item; + } + catch + { + success = false; + } + + throw new AggregateException($"Index: {indexOf} Length: {Room.Playlist.Count} Retry success: {success} Item: {JsonConvert.SerializeObject(createPlaylistItem(item))}\n\nRoom:{JsonConvert.SerializeObject(APIRoom)}", ex); } ItemChanged?.Invoke(item); From fa9a835eb5366ea55def9aa280700c55b51ef28f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 9 Aug 2024 19:29:23 +0900 Subject: [PATCH 1896/2493] Make icon smaller --- osu.Game/Overlays/Volume/MuteButton.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Volume/MuteButton.cs b/osu.Game/Overlays/Volume/MuteButton.cs index a04d79bd20..bee5cce199 100644 --- a/osu.Game/Overlays/Volume/MuteButton.cs +++ b/osu.Game/Overlays/Volume/MuteButton.cs @@ -71,7 +71,7 @@ namespace osu.Game.Overlays.Volume Current.BindValueChanged(muted => { icon.Icon = muted.NewValue ? FontAwesome.Solid.VolumeMute : FontAwesome.Solid.VolumeUp; - icon.Size = new Vector2(muted.NewValue ? 18 : 20); + icon.Size = new Vector2(muted.NewValue ? 12 : 16); icon.Margin = new MarginPadding { Right = muted.NewValue ? 2 : 0 }; }, true); } From 179a3ad8dd31c8ecfb4684a2ad5529b91a87d1ae Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 9 Aug 2024 19:55:56 +0900 Subject: [PATCH 1897/2493] Hack around the border looking ugly This is an o!f issue because borders are applied into the individual sprites of the container via masking, rather than being isolated to the container itself. In this case, it'll be applied to the "flash" sprite, which is using additive blending, causing further issues. --- osu.Game/Overlays/Volume/MuteButton.cs | 32 ++++++++++++++++++-------- 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/osu.Game/Overlays/Volume/MuteButton.cs b/osu.Game/Overlays/Volume/MuteButton.cs index bee5cce199..878842867d 100644 --- a/osu.Game/Overlays/Volume/MuteButton.cs +++ b/osu.Game/Overlays/Volume/MuteButton.cs @@ -7,13 +7,13 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; using osu.Game.Graphics; using osu.Game.Graphics.UserInterface; using osuTK; -using osuTK.Graphics; namespace osu.Game.Overlays.Volume { @@ -33,29 +33,28 @@ namespace osu.Game.Overlays.Volume } } - private Color4 hoveredColour, unhoveredColour; + private ColourInfo hoveredBorderColour; + private ColourInfo unhoveredBorderColour; + private CompositeDrawable border = null!; public MuteButton() { const float width = 30; const float height = 30; - Content.BorderThickness = 3; + Size = new Vector2(width, height); Content.CornerRadius = height / 2; Content.CornerExponent = 2; - Size = new Vector2(width, height); - Action = () => Current.Value = !Current.Value; } [BackgroundDependencyLoader] private void load(OsuColour colours) { - hoveredColour = colours.PinkLight; - - Content.BorderColour = unhoveredColour = colours.Gray1; BackgroundColour = colours.Gray1; + hoveredBorderColour = colours.PinkLight; + unhoveredBorderColour = colours.Gray1; SpriteIcon icon; @@ -65,6 +64,19 @@ namespace osu.Game.Overlays.Volume { Anchor = Anchor.Centre, Origin = Anchor.Centre, + }, + border = new CircularContainer + { + RelativeSizeAxes = Axes.Both, + Masking = true, + BorderThickness = 3, + BorderColour = unhoveredBorderColour, + Child = new Box + { + RelativeSizeAxes = Axes.Both, + Alpha = 0, + AlwaysPresent = true + } } }); @@ -78,13 +90,13 @@ namespace osu.Game.Overlays.Volume protected override bool OnHover(HoverEvent e) { - Content.TransformTo, ColourInfo>("BorderColour", hoveredColour, 500, Easing.OutQuint); + border.TransformTo(nameof(BorderColour), hoveredBorderColour, 500, Easing.OutQuint); return false; } protected override void OnHoverLost(HoverLostEvent e) { - Content.TransformTo, ColourInfo>("BorderColour", unhoveredColour, 500, Easing.OutQuint); + border.TransformTo(nameof(BorderColour), unhoveredBorderColour, 500, Easing.OutQuint); } protected override bool OnMouseDown(MouseDownEvent e) From 3896a081a56529044848830bd913bdc3313d16df Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 9 Aug 2024 22:50:56 +0900 Subject: [PATCH 1898/2493] 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 3b3385ecfe..ae30563a19 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -10,7 +10,7 @@ true - + diff --git a/osu.iOS.props b/osu.iOS.props index 196d5594ad..9fa1b691e9 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -23,6 +23,6 @@ iossimulator-x64 - + From c6fa348d82f48f07d516885976288c4aa437f3bc Mon Sep 17 00:00:00 2001 From: Jamie Taylor Date: Fri, 9 Aug 2024 23:03:03 +0900 Subject: [PATCH 1899/2493] Add sound design for daily challenge intro animation --- osu.Game/Screens/Menu/ButtonSystem.cs | 2 +- .../DailyChallenge/DailyChallengeIntro.cs | 86 ++++++++++++++++++- 2 files changed, 86 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Menu/ButtonSystem.cs b/osu.Game/Screens/Menu/ButtonSystem.cs index e9fff9bb07..0997ab8003 100644 --- a/osu.Game/Screens/Menu/ButtonSystem.cs +++ b/osu.Game/Screens/Menu/ButtonSystem.cs @@ -138,7 +138,7 @@ namespace osu.Game.Screens.Menu }); buttonsPlay.Add(new MainMenuButton(ButtonSystemStrings.Multi, @"button-default-select", OsuIcon.Online, new Color4(94, 63, 186, 255), onMultiplayer, Key.M)); buttonsPlay.Add(new MainMenuButton(ButtonSystemStrings.Playlists, @"button-default-select", OsuIcon.Tournament, new Color4(94, 63, 186, 255), onPlaylists, Key.L)); - buttonsPlay.Add(new DailyChallengeButton(@"button-default-select", new Color4(94, 63, 186, 255), onDailyChallenge, Key.D)); + buttonsPlay.Add(new DailyChallengeButton(@"button-daily-select", new Color4(94, 63, 186, 255), onDailyChallenge, Key.D)); buttonsPlay.ForEach(b => b.VisibleState = ButtonSystemState.Play); buttonsEdit.Add(new MainMenuButton(EditorStrings.BeatmapEditor.ToLower(), @"button-default-select", OsuIcon.Beatmap, new Color4(238, 170, 0, 255), _ => OnEditBeatmap?.Invoke(), Key.B, Key.E) diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeIntro.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeIntro.cs index d00a1ef1e9..83ea0f8088 100644 --- a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeIntro.cs +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeIntro.cs @@ -4,6 +4,8 @@ using System; using System.Linq; using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Audio.Sample; using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; @@ -68,6 +70,18 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge [Resolved] private MusicController musicController { get; set; } = null!; + private Sample? dateWindupSample; + private Sample? dateImpactSample; + private Sample? beatmapWindupSample; + private Sample? beatmapImpactSample; + + private SampleChannel? dateWindupChannel; + private SampleChannel? dateImpactChannel; + private SampleChannel? beatmapWindupChannel; + private SampleChannel? beatmapImpactChannel; + + private IDisposable? duckOperation; + public DailyChallengeIntro(Room room) { this.room = room; @@ -79,7 +93,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge protected override BackgroundScreen CreateBackground() => new DailyChallengeIntroBackgroundScreen(colourProvider); [BackgroundDependencyLoader] - private void load(BeatmapDifficultyCache difficultyCache, BeatmapModelDownloader beatmapDownloader, OsuConfigManager config) + private void load(BeatmapDifficultyCache difficultyCache, BeatmapModelDownloader beatmapDownloader, OsuConfigManager config, AudioManager audio) { const float horizontal_info_size = 500f; @@ -323,6 +337,11 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge if (!beatmapManager.IsAvailableLocally(new BeatmapSetInfo { OnlineID = item.Beatmap.BeatmapSet!.OnlineID })) beatmapDownloader.Download(item.Beatmap.BeatmapSet!, config.Get(OsuSetting.PreferNoVideo)); } + + dateWindupSample = audio.Samples.Get(@"DailyChallenge/date-windup"); + dateImpactSample = audio.Samples.Get(@"DailyChallenge/date-impact"); + beatmapWindupSample = audio.Samples.Get(@"DailyChallenge/beatmap-windup"); + beatmapImpactSample = audio.Samples.Get(@"DailyChallenge/beatmap-impact"); } public override void OnEntering(ScreenTransitionEvent e) @@ -338,6 +357,8 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge this.FadeInFromZero(400, Easing.OutQuint); updateAnimationState(); + + playDateWindupSample(); } public override void OnSuspending(ScreenTransitionEvent e) @@ -346,6 +367,12 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge base.OnSuspending(e); } + protected override void Dispose(bool isDisposing) + { + resetAudio(); + base.Dispose(isDisposing); + } + private void updateAnimationState() { if (!beatmapBackgroundLoaded || !this.IsCurrentScreen()) @@ -380,6 +407,29 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge .Then() .MoveToY(0, 4000); + using (BeginDelayedSequence(150)) + { + Schedule(() => + { + playDateImpactSample(); + playBeatmapWindupSample(); + + duckOperation?.Dispose(); + duckOperation = musicController.Duck(new DuckParameters + { + RestoreDuration = 1500f, + }); + }); + + using (BeginDelayedSequence(2750)) + { + Schedule(() => + { + duckOperation?.Dispose(); + }); + } + } + using (BeginDelayedSequence(1000)) { beatmapContent @@ -406,6 +456,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge shouldBePlayingMusic = true; DailyChallenge.TrySetDailyChallengeBeatmap(this, beatmapManager, rulesets, musicController, item); ApplyToBackground(bs => ((RoomBackgroundScreen)bs).SelectedItem.Value = item); + playBeatmapImpactSample(); }); } @@ -425,6 +476,39 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge } } + private void playDateWindupSample() + { + dateWindupChannel = dateWindupSample?.GetChannel(); + dateWindupChannel?.Play(); + } + + private void playDateImpactSample() + { + dateImpactChannel = dateImpactSample?.GetChannel(); + dateImpactChannel?.Play(); + } + + private void playBeatmapWindupSample() + { + beatmapWindupChannel = beatmapWindupSample?.GetChannel(); + beatmapWindupChannel?.Play(); + } + + private void playBeatmapImpactSample() + { + beatmapImpactChannel = beatmapImpactSample?.GetChannel(); + beatmapImpactChannel?.Play(); + } + + private void resetAudio() + { + dateWindupChannel?.Stop(); + dateImpactChannel?.Stop(); + beatmapWindupChannel?.Stop(); + beatmapImpactChannel?.Stop(); + duckOperation?.Dispose(); + } + private partial class DailyChallengeIntroBackgroundScreen : RoomBackgroundScreen { private readonly OverlayColourProvider colourProvider; From 81777f22b4463bafa8de3da717db42193b7f904a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 10 Aug 2024 00:11:39 +0900 Subject: [PATCH 1900/2493] 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 ae30563a19..b5a355a77f 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -10,7 +10,7 @@ true - + diff --git a/osu.iOS.props b/osu.iOS.props index 9fa1b691e9..7b3903c352 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -23,6 +23,6 @@ iossimulator-x64 - + From c29b40ae6593e21d3fdebcb4a8dbff37f1edbbca Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 10 Aug 2024 02:08:02 +0900 Subject: [PATCH 1901/2493] 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 c4d76a2441..6b07a33af2 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -36,7 +36,7 @@ - + From 2dee8bef7e861b435bcaf8c737c8255b5aafe0a4 Mon Sep 17 00:00:00 2001 From: ArijanJ Date: Fri, 9 Aug 2024 22:50:37 +0200 Subject: [PATCH 1902/2493] Add option to hide song progress time/text --- osu.Game/Localisation/HUD/SongProgressStrings.cs | 10 ++++++++++ osu.Game/Screens/Play/HUD/ArgonSongProgress.cs | 4 ++++ osu.Game/Screens/Play/HUD/DefaultSongProgress.cs | 13 +++++++++++++ 3 files changed, 27 insertions(+) diff --git a/osu.Game/Localisation/HUD/SongProgressStrings.cs b/osu.Game/Localisation/HUD/SongProgressStrings.cs index 4c621e8e8c..332f15cb17 100644 --- a/osu.Game/Localisation/HUD/SongProgressStrings.cs +++ b/osu.Game/Localisation/HUD/SongProgressStrings.cs @@ -19,6 +19,16 @@ namespace osu.Game.Localisation.HUD /// public static LocalisableString ShowGraphDescription => new TranslatableString(getKey(@"show_graph_description"), "Whether a graph displaying difficulty throughout the beatmap should be shown"); + /// + /// "Show time" + /// + public static LocalisableString ShowTime => new TranslatableString(getKey(@"show_time"), "Show time"); + + /// + /// "Whether the passed and remaining time should be shown" + /// + public static LocalisableString ShowTimeDescription => new TranslatableString(getKey(@"show_time_description"), "Whether the passed and remaining time should be shown"); + private static string getKey(string key) => $"{prefix}:{key}"; } } diff --git a/osu.Game/Screens/Play/HUD/ArgonSongProgress.cs b/osu.Game/Screens/Play/HUD/ArgonSongProgress.cs index 7db3f9fd3c..ebebfebfb3 100644 --- a/osu.Game/Screens/Play/HUD/ArgonSongProgress.cs +++ b/osu.Game/Screens/Play/HUD/ArgonSongProgress.cs @@ -26,6 +26,9 @@ namespace osu.Game.Screens.Play.HUD [SettingSource(typeof(SongProgressStrings), nameof(SongProgressStrings.ShowGraph), nameof(SongProgressStrings.ShowGraphDescription))] public Bindable ShowGraph { get; } = new BindableBool(true); + [SettingSource(typeof(SongProgressStrings), nameof(SongProgressStrings.ShowTime), nameof(SongProgressStrings.ShowTimeDescription))] + public Bindable ShowTime { get; } = new BindableBool(true); + [Resolved] private Player? player { get; set; } @@ -90,6 +93,7 @@ namespace osu.Game.Screens.Play.HUD Interactive.BindValueChanged(_ => bar.Interactive = Interactive.Value, true); ShowGraph.BindValueChanged(_ => updateGraphVisibility(), true); + ShowTime.BindValueChanged(_ => info.FadeTo(ShowTime.Value ? 1 : 0, 200, Easing.In), true); } protected override void UpdateObjects(IEnumerable objects) diff --git a/osu.Game/Screens/Play/HUD/DefaultSongProgress.cs b/osu.Game/Screens/Play/HUD/DefaultSongProgress.cs index f01c11855c..1586ac72a1 100644 --- a/osu.Game/Screens/Play/HUD/DefaultSongProgress.cs +++ b/osu.Game/Screens/Play/HUD/DefaultSongProgress.cs @@ -33,6 +33,9 @@ namespace osu.Game.Screens.Play.HUD [SettingSource(typeof(SongProgressStrings), nameof(SongProgressStrings.ShowGraph), nameof(SongProgressStrings.ShowGraphDescription))] public Bindable ShowGraph { get; } = new BindableBool(true); + [SettingSource(typeof(SongProgressStrings), nameof(SongProgressStrings.ShowTime), nameof(SongProgressStrings.ShowTimeDescription))] + public Bindable ShowTime { get; } = new BindableBool(true); + [Resolved] private Player? player { get; set; } @@ -82,9 +85,11 @@ namespace osu.Game.Screens.Play.HUD { Interactive.BindValueChanged(_ => updateBarVisibility(), true); ShowGraph.BindValueChanged(_ => updateGraphVisibility(), true); + ShowTime.BindValueChanged(_ => updateTimeVisibility(), true); base.LoadComplete(); } + protected override void UpdateObjects(IEnumerable objects) { @@ -129,6 +134,14 @@ namespace osu.Game.Screens.Play.HUD updateInfoMargin(); } + private void updateTimeVisibility() + { + info.FadeTo(ShowTime.Value ? 1 : 0, transition_duration, Easing.In); + + updateInfoMargin(); + } + + private void updateInfoMargin() { float finalMargin = bottom_bar_height + (Interactive.Value ? handle_size.Y : 0) + (ShowGraph.Value ? graph_height : 0); From d01e76d9db3c4cffff803114edcbdf4c32dd6b5a Mon Sep 17 00:00:00 2001 From: ArijanJ Date: Fri, 9 Aug 2024 23:08:22 +0200 Subject: [PATCH 1903/2493] Fix double blank line --- osu.Game/Screens/Play/HUD/DefaultSongProgress.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Screens/Play/HUD/DefaultSongProgress.cs b/osu.Game/Screens/Play/HUD/DefaultSongProgress.cs index 1586ac72a1..adb93c3ba3 100644 --- a/osu.Game/Screens/Play/HUD/DefaultSongProgress.cs +++ b/osu.Game/Screens/Play/HUD/DefaultSongProgress.cs @@ -89,7 +89,6 @@ namespace osu.Game.Screens.Play.HUD base.LoadComplete(); } - protected override void UpdateObjects(IEnumerable objects) { From fed5b9d7477f2118609b742c139c72aac800fd11 Mon Sep 17 00:00:00 2001 From: ArijanJ Date: Sat, 10 Aug 2024 09:45:30 +0200 Subject: [PATCH 1904/2493] Fix one more inspectcode warning --- osu.Game/Screens/Play/HUD/DefaultSongProgress.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Screens/Play/HUD/DefaultSongProgress.cs b/osu.Game/Screens/Play/HUD/DefaultSongProgress.cs index adb93c3ba3..6b2bb2b718 100644 --- a/osu.Game/Screens/Play/HUD/DefaultSongProgress.cs +++ b/osu.Game/Screens/Play/HUD/DefaultSongProgress.cs @@ -140,7 +140,6 @@ namespace osu.Game.Screens.Play.HUD updateInfoMargin(); } - private void updateInfoMargin() { float finalMargin = bottom_bar_height + (Interactive.Value ? handle_size.Y : 0) + (ShowGraph.Value ? graph_height : 0); From 3acd00b9b703a0a5a7445192eabac94bfe385594 Mon Sep 17 00:00:00 2001 From: StanR Date: Sat, 10 Aug 2024 22:30:24 +0500 Subject: [PATCH 1905/2493] Tweak some values --- .../Difficulty/Evaluators/RhythmEvaluator.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs index ba77bc6a61..d2f58925cd 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs @@ -60,7 +60,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators } private const int history_time_max = 5 * 1000; // 5 seconds of calculatingRhythmBonus max. - private const double rhythm_multiplier = 1.05; + private const double rhythm_multiplier = 1.1; private const int max_island_size = 7; /// @@ -103,7 +103,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators double prevDelta = prevObj.StrainTime; double lastDelta = lastObj.StrainTime; - double currRatio = 1.0 + 8.0 * Math.Min(0.5, Math.Pow(Math.Sin(Math.PI / (Math.Min(prevDelta, currDelta) / Math.Max(prevDelta, currDelta))), 2)); // fancy function to calculate rhythmbonuses. + double currRatio = 1.0 + 8.4 * Math.Min(0.5, Math.Pow(Math.Sin(Math.PI / (Math.Min(prevDelta, currDelta) / Math.Max(prevDelta, currDelta))), 2)); // fancy function to calculate rhythmbonuses. double windowPenalty = Math.Min(1, Math.Max(0, Math.Abs(prevDelta - currDelta) - currObj.HitWindowGreat * 0.3) / (currObj.HitWindowGreat * 0.3)); @@ -132,7 +132,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators effectiveRatio *= 0.25; if (island.IsSimilarPolarity(previousIsland, deltaDifferenceEpsilon)) // repeated island polartiy (2 -> 4, 3 -> 5) - effectiveRatio *= 0.50; + effectiveRatio *= 0.35; if (lastDelta > prevDelta + deltaDifferenceEpsilon && prevDelta > currDelta + deltaDifferenceEpsilon) // previous increase happened a note ago, 1/1->1/2-1/4, dont want to buff this. effectiveRatio *= 0.125; From 2233602184b725d4a468b53737f02d7788639c55 Mon Sep 17 00:00:00 2001 From: clayton Date: Sun, 11 Aug 2024 09:45:42 -0700 Subject: [PATCH 1906/2493] Update mania replay decode test to include 18K keypress --- .../Formats/LegacyScoreDecoderTest.cs | 9 ++++----- .../Resources/Replays/mania-replay.osr | Bin 1012 -> 1042 bytes 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs index 3759bfd034..070ade4ad9 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs @@ -19,7 +19,7 @@ using osu.Game.Replays; using osu.Game.Rulesets; using osu.Game.Rulesets.Catch; using osu.Game.Rulesets.Mania; -using osu.Game.Rulesets.Mania.Mods; +using osu.Game.Rulesets.Mania.Replays; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Mods; @@ -65,14 +65,13 @@ namespace osu.Game.Tests.Beatmaps.Formats Assert.AreEqual(829_931, score.ScoreInfo.LegacyTotalScore); Assert.AreEqual(3, score.ScoreInfo.MaxCombo); - Assert.IsTrue(score.ScoreInfo.Mods.Any(m => m is ManiaModClassic)); - Assert.IsTrue(score.ScoreInfo.APIMods.Any(m => m.Acronym == "CL")); - Assert.IsTrue(score.ScoreInfo.ModsJson.Contains("CL")); + Assert.That(score.ScoreInfo.APIMods.Select(m => m.Acronym), Is.EquivalentTo(new[] { "CL", "9K", "DS" })); Assert.That((2 * 300d + 1 * 200) / (3 * 305d), Is.EqualTo(score.ScoreInfo.Accuracy).Within(0.0001)); Assert.AreEqual(ScoreRank.B, score.ScoreInfo.Rank); - Assert.That(score.Replay.Frames, Is.Not.Empty); + Assert.That(score.Replay.Frames, Has.One.Matches(frame => + frame.Time == 414 && frame.Actions.SequenceEqual(new[] { ManiaAction.Key1, ManiaAction.Key16 }))); } } diff --git a/osu.Game.Tests/Resources/Replays/mania-replay.osr b/osu.Game.Tests/Resources/Replays/mania-replay.osr index da1a7bdd28e5b89eddd9742bce2b27bc8e75e6f5..ad55a5a31873acc943379b6391bb068ef92397b7 100644 GIT binary patch delta 941 zcmV;e15*6-2a*VoEL&k=VPRomVPRomVPRomVPRomVPRomVPRomVPRomVFCaE00000 z009610PCp?00RI3000033lK3dIX5zVF)T4SFgZAUF)U$Fy<3(_*a(UP003P80Du4` zM3IL|f2kp_Cwm-NDGQfLnv}R{A+5uM=V$!>%NF%V;(li`g9UYd{}pt#j-NjTL^Rx$ z)8vox9t#(tVgh24S9qwBpR*G=!suY`TLgA|2YbKlAt?C3gt0(^Ne~qSalq8ciz#p@2y$@@79OY<$k=i&@ z$%){vIws|+*G$%o*Abhy7QAvG3_AklP}%Uj6WtwNj^#D~Ap6s`y6)+@Vs|9!>rhF$ z(Th@AK+#MQ+@eBuu>#W^VSow$rY2vZvtyLf$Fe-%Y}I8;wFHWVyQB;i47XyCq#ga- zf6<@ARV=7N+nveRe(H&`|49MZB_STJbhkQ&Kg%tRZ5su!GX3k;q-nc^O3L)?dAyz6 zkq*PQof<5?`eqvXkU4Gq?CG7&!{XQ>l-81EDy%S!hcCnQx-Eg^znRa2m3+kl(&{_4 z|E#XY&k`FWw(AwgOBN}Z{pzUR1N`=ce>&ZHKn37E`?&xm+$MMK8G%yB-KT2|g&Q|v z!KGRky;bp!dh$;i3BgA{n8d#`oL{ZD{RXnE>twCR4liAX%w50Txmgdjn0*~7b8UJS z9`hlcqw752ctoxbZSXM@6|MMd(ofzlz~{ZqD%2&ADT$6q)$)FS?(9xRT2XjCf1zRl z=;!6R(l-}a+w+m{IJ5YVzyr}Z1|w6+wA>Y>tB-E9T=9J7eOSVn*3{NocS0mZaN9Iz z*8||a6~)(jJ?Q4h=*1y|Up7!MiC7a#ZYI3i;~I4y+@wb|cGLJW*H)1H2@k~Rna5gx zC}}6l4~U_i7R;**PnLpaABm`ie?{Ulv?yRhX^Ccj3l6au<|2+bfq5wf z{;YNi$U9q;a9n;o1AC&o44(&hq*5gqE1yu^<%DjP#j?h@8p1e)RU%gINB4K-`759> z6$9)02}Os4pSJ8KLxV_3sf!8h1%oh54Pr5{yWq7hhBpIMpL1g4;G#Hr7_gz8oO)mX P-u?|V0000000000tK-15 delta 911 zcmV;A191G32=oV#EL$>UWo0=sIWaXiGGjF`V>B^gIWRagV=^){GcsglGy(ts00000 z009610PCp?00RI3000003lK3dIX5zVF)T4SFgZAUF)U$Fy<3(_*a&O`003P803ZP5 zLy?C{f3PYVk96LhNin}dI^y)M#Q-6F;~hK6;6v0f4($qcC=D#w1v}gT>+Q44apIe> z^u{Hu`T^&O5+{?7)C=5^o4~{|a(=klR#xS2K1jz$5#VC2cu?g{{PKLP6wJ^#SieUk zU{__vBIA6}WQCuDj>EFa^^_#$Q4P>ae;2H4=ZGmx7w}kC2MuRN0?rGS(8}Y5 zhz;zQ^pgdCFAA95G5naWn3}GRN*enPdqN;!X`#^cNS3(>dQ8HnIdQ)YOF?B;rDi!z~! ze>k`!wUtQ?QkF@%v-^b`=F}eWcIcyuEA?+EC5nJ6&VHDlo)zLnjA}iYry^Wc282uw zb8MhH5)cj#aw!_hD@mb6mKMjw{o@f8;PD*nHwXocs~eYj)Vr=h%$l3Zu|vmp^tLml zymcsXvkSx^NtzJrIgdT3GIkW!%>zeQe|zJn8w6pYSsUr1=*o_Q#S)6A-dHA@d9~(^0+ax|D@!;n;Z|| zFvVlqvtKZJf2x8EVoS#`3YPzS-*vaun`i1N*BG`MvK_+XgTPh^GgHpdy!e==e?Np^ zP79t-TF}QHA=p$CMZ8h|*&agcSs?d99UKOtBUW5%$#uK?C(WYoiHQt9+}D}2*H`GI z+MHqHE17uF?Dqy%$~lzbk4H;u{VzwEFmdj}XdWCiMA_1-@_?MecxP^r?UDsYE^qoo z$|(tSLa2UCM@zI7zrxT> Date: Sun, 11 Aug 2024 09:45:43 -0700 Subject: [PATCH 1907/2493] Fix mouseX legacy replay parsing for high key counts in mania --- osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs index ba1fdd6adf..6ad118547b 100644 --- a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs +++ b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs @@ -280,8 +280,11 @@ namespace osu.Game.Scoring.Legacy continue; } + // In mania, mouseX encodes the pressed keys in the lower 20 bits + int mouseXParseLimit = currentRuleset.RulesetInfo.OnlineID == 3 ? (1 << 20) - 1 : Parsing.MAX_COORDINATE_VALUE; + float diff = Parsing.ParseFloat(split[0]); - float mouseX = Parsing.ParseFloat(split[1], Parsing.MAX_COORDINATE_VALUE); + float mouseX = Parsing.ParseFloat(split[1], mouseXParseLimit); float mouseY = Parsing.ParseFloat(split[2], Parsing.MAX_COORDINATE_VALUE); lastTime += diff; From d2eb6ccb8caa7455d1daba4ca9d2518ecbec4ed5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 12 Aug 2024 14:27:21 +0900 Subject: [PATCH 1908/2493] Standardise skin transformer code structure --- .../Legacy/CatchLegacySkinTransformer.cs | 49 +++++++------- .../Legacy/OsuLegacySkinTransformer.cs | 65 ++++++++++--------- 2 files changed, 61 insertions(+), 53 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs b/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs index df8c04638d..4efdafc034 100644 --- a/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs @@ -31,10 +31,7 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy switch (lookup) { case SkinComponentsContainerLookup containerLookup: - if (containerLookup.Target != SkinComponentsContainerLookup.TargetArea.MainHUDComponents) - return base.GetDrawableComponent(lookup); - - // Modifications for global components. + // Only handle per ruleset defaults here. if (containerLookup.Ruleset == null) return base.GetDrawableComponent(lookup); @@ -43,27 +40,33 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy return d; // Our own ruleset components default. - // todo: remove CatchSkinComponents.CatchComboCounter and refactor LegacyCatchComboCounter to be added here instead. - return new DefaultSkinComponentsContainer(container => + switch (containerLookup.Target) { - var keyCounter = container.OfType().FirstOrDefault(); + case SkinComponentsContainerLookup.TargetArea.MainHUDComponents: + // todo: remove CatchSkinComponents.CatchComboCounter and refactor LegacyCatchComboCounter to be added here instead. + return new DefaultSkinComponentsContainer(container => + { + var keyCounter = container.OfType().FirstOrDefault(); - if (keyCounter != null) - { - // set the anchor to top right so that it won't squash to the return button to the top - keyCounter.Anchor = Anchor.CentreRight; - keyCounter.Origin = Anchor.CentreRight; - keyCounter.X = 0; - // 340px is the default height inherit from stable - keyCounter.Y = container.ToLocalSpace(new Vector2(0, container.ScreenSpaceDrawQuad.Centre.Y - 340f)).Y; - } - }) - { - Children = new Drawable[] - { - new LegacyKeyCounterDisplay(), - } - }; + if (keyCounter != null) + { + // set the anchor to top right so that it won't squash to the return button to the top + keyCounter.Anchor = Anchor.CentreRight; + keyCounter.Origin = Anchor.CentreRight; + keyCounter.X = 0; + // 340px is the default height inherit from stable + keyCounter.Y = container.ToLocalSpace(new Vector2(0, container.ScreenSpaceDrawQuad.Centre.Y - 340f)).Y; + } + }) + { + Children = new Drawable[] + { + new LegacyKeyCounterDisplay(), + } + }; + } + + return null; case CatchSkinComponentLookup catchSkinComponent: switch (catchSkinComponent.Component) diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs index c2381fff88..aad8ffb1cf 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs @@ -45,9 +45,6 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy switch (lookup) { case SkinComponentsContainerLookup containerLookup: - if (containerLookup.Target != SkinComponentsContainerLookup.TargetArea.MainHUDComponents) - return base.GetDrawableComponent(lookup); - // Only handle per ruleset defaults here. if (containerLookup.Ruleset == null) return base.GetDrawableComponent(lookup); @@ -56,42 +53,50 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy if (base.GetDrawableComponent(lookup) is UserConfiguredLayoutContainer d) return d; - return new DefaultSkinComponentsContainer(container => + // Our own ruleset components default. + switch (containerLookup.Target) { - var keyCounter = container.OfType().FirstOrDefault(); + case SkinComponentsContainerLookup.TargetArea.MainHUDComponents: + return new DefaultSkinComponentsContainer(container => + { + var keyCounter = container.OfType().FirstOrDefault(); - if (keyCounter != null) - { - // set the anchor to top right so that it won't squash to the return button to the top - keyCounter.Anchor = Anchor.CentreRight; - keyCounter.Origin = Anchor.CentreRight; - keyCounter.X = 0; - // 340px is the default height inherit from stable - keyCounter.Y = container.ToLocalSpace(new Vector2(0, container.ScreenSpaceDrawQuad.Centre.Y - 340f)).Y; - } + if (keyCounter != null) + { + // set the anchor to top right so that it won't squash to the return button to the top + keyCounter.Anchor = Anchor.CentreRight; + keyCounter.Origin = Anchor.CentreRight; + keyCounter.X = 0; + // 340px is the default height inherit from stable + keyCounter.Y = container.ToLocalSpace(new Vector2(0, container.ScreenSpaceDrawQuad.Centre.Y - 340f)).Y; + } - var combo = container.OfType().FirstOrDefault(); + var combo = container.OfType().FirstOrDefault(); - if (combo != null) - { - combo.Anchor = Anchor.BottomLeft; - combo.Origin = Anchor.BottomLeft; - combo.Scale = new Vector2(1.28f); - } - }) - { - Children = new Drawable[] - { - new LegacyDefaultComboCounter(), - new LegacyKeyCounterDisplay(), - } - }; + if (combo != null) + { + combo.Anchor = Anchor.BottomLeft; + combo.Origin = Anchor.BottomLeft; + combo.Scale = new Vector2(1.28f); + } + }) + { + Children = new Drawable[] + { + new LegacyDefaultComboCounter(), + new LegacyKeyCounterDisplay(), + } + }; + } + + return null; case OsuSkinComponentLookup osuComponent: switch (osuComponent.Component) { case OsuSkinComponents.FollowPoint: - return this.GetAnimation("followpoint", true, true, true, startAtCurrentTime: false, maxSize: new Vector2(OsuHitObject.OBJECT_RADIUS * 2, OsuHitObject.OBJECT_RADIUS)); + return this.GetAnimation("followpoint", true, true, true, startAtCurrentTime: false, + maxSize: new Vector2(OsuHitObject.OBJECT_RADIUS * 2, OsuHitObject.OBJECT_RADIUS)); case OsuSkinComponents.SliderScorePoint: return this.GetAnimation("sliderscorepoint", false, false, maxSize: OsuHitObject.OBJECT_DIMENSIONS); From 306e84c7aca8ac5dc89be8fb0f15941902ac05f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 12 Aug 2024 10:46:58 +0200 Subject: [PATCH 1909/2493] Move disposal method to more expected location --- .../OnlinePlay/DailyChallenge/DailyChallengeIntro.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeIntro.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeIntro.cs index 83ea0f8088..e59031f663 100644 --- a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeIntro.cs +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeIntro.cs @@ -367,12 +367,6 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge base.OnSuspending(e); } - protected override void Dispose(bool isDisposing) - { - resetAudio(); - base.Dispose(isDisposing); - } - private void updateAnimationState() { if (!beatmapBackgroundLoaded || !this.IsCurrentScreen()) @@ -500,6 +494,12 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge beatmapImpactChannel?.Play(); } + protected override void Dispose(bool isDisposing) + { + resetAudio(); + base.Dispose(isDisposing); + } + private void resetAudio() { dateWindupChannel?.Stop(); From 041c70e4ebb8065ef5ab0b866ad17991c345319a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 12 Aug 2024 11:19:02 +0200 Subject: [PATCH 1910/2493] Fix tests --- osu.Game.Tests/Visual/Menus/TestSceneMainMenu.cs | 1 + .../Visual/UserInterface/TestSceneMainMenuButton.cs | 10 ++++++++-- osu.Game/Screens/Menu/DailyChallengeButton.cs | 4 +++- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Menus/TestSceneMainMenu.cs b/osu.Game.Tests/Visual/Menus/TestSceneMainMenu.cs index 613d8347b7..aab3716463 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneMainMenu.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneMainMenu.cs @@ -48,6 +48,7 @@ namespace osu.Game.Tests.Visual.Menus { new PlaylistItem(beatmap) }, + StartDate = { Value = DateTimeOffset.Now.AddMinutes(-30) }, EndDate = { Value = DateTimeOffset.Now.AddSeconds(60) } }); return true; diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneMainMenuButton.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneMainMenuButton.cs index af98aa21db..98f2b129ff 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneMainMenuButton.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneMainMenuButton.cs @@ -58,6 +58,7 @@ namespace osu.Game.Tests.Visual.UserInterface { new PlaylistItem(beatmap) }, + StartDate = { Value = DateTimeOffset.Now.AddMinutes(-5) }, EndDate = { Value = DateTimeOffset.Now.AddSeconds(30) } }); return true; @@ -95,8 +96,13 @@ namespace osu.Game.Tests.Visual.UserInterface }, }; }); - AddAssert("no notification posted", () => notificationOverlay.AllNotifications.Count(), () => Is.Zero); + AddAssert("notification posted", () => notificationOverlay.AllNotifications.Count(), () => Is.EqualTo(1)); + AddStep("clear notifications", () => + { + foreach (var notification in notificationOverlay.AllNotifications) + notification.Close(runFlingAnimation: false); + }); AddStep("beatmap of the day not active", () => metadataClient.DailyChallengeUpdated(null)); AddAssert("no notification posted", () => notificationOverlay.AllNotifications.Count(), () => Is.Zero); @@ -105,7 +111,7 @@ namespace osu.Game.Tests.Visual.UserInterface { RoomID = 1234, })); - AddAssert("notification posted", () => notificationOverlay.AllNotifications.Count(), () => Is.EqualTo(1)); + AddAssert("no notification posted", () => notificationOverlay.AllNotifications.Count(), () => Is.Zero); } } } diff --git a/osu.Game/Screens/Menu/DailyChallengeButton.cs b/osu.Game/Screens/Menu/DailyChallengeButton.cs index ac04afdc4d..2357c60f3d 100644 --- a/osu.Game/Screens/Menu/DailyChallengeButton.cs +++ b/osu.Game/Screens/Menu/DailyChallengeButton.cs @@ -154,7 +154,9 @@ namespace osu.Game.Screens.Menu cover.OnlineInfo = TooltipContent = room.Playlist.FirstOrDefault()?.Beatmap.BeatmapSet as APIBeatmapSet; // We only want to notify the user if a new challenge recently went live. - if (Math.Abs((DateTimeOffset.Now - room.StartDate.Value!.Value).TotalSeconds) < 600 && room.RoomID.Value != lastNotifiedDailyChallengeRoomId) + if (room.StartDate.Value != null + && Math.Abs((DateTimeOffset.Now - room.StartDate.Value!.Value).TotalSeconds) < 600 + && room.RoomID.Value != lastNotifiedDailyChallengeRoomId) { lastNotifiedDailyChallengeRoomId = room.RoomID.Value; notificationOverlay?.Post(new NewDailyChallengeNotification(room)); From 54a1d791362c31da283ca3afd33a3841e440e92e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 12 Aug 2024 11:23:36 +0200 Subject: [PATCH 1911/2493] Clean up some naming weirdness --- osu.Game/Screens/Menu/DailyChallengeButton.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Menu/DailyChallengeButton.cs b/osu.Game/Screens/Menu/DailyChallengeButton.cs index 2357c60f3d..234c4c27d5 100644 --- a/osu.Game/Screens/Menu/DailyChallengeButton.cs +++ b/osu.Game/Screens/Menu/DailyChallengeButton.cs @@ -132,21 +132,21 @@ namespace osu.Game.Screens.Menu private long? lastNotifiedDailyChallengeRoomId; - private void dailyChallengeChanged(ValueChangedEvent info) + private void dailyChallengeChanged(ValueChangedEvent _) { UpdateState(); scheduledCountdownUpdate?.Cancel(); scheduledCountdownUpdate = null; - if (this.info.Value == null) + if (info.Value == null) { Room = null; cover.OnlineInfo = TooltipContent = null; } else { - var roomRequest = new GetRoomRequest(this.info.Value.Value.RoomID); + var roomRequest = new GetRoomRequest(info.Value.Value.RoomID); roomRequest.Success += room => { From 96bd374b18fa12df7545f0aeca4660147e0feec7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 12 Aug 2024 11:24:59 +0200 Subject: [PATCH 1912/2493] Change notification interval to 30 minutes --- osu.Game/Screens/Menu/DailyChallengeButton.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Menu/DailyChallengeButton.cs b/osu.Game/Screens/Menu/DailyChallengeButton.cs index 234c4c27d5..e19ba6612c 100644 --- a/osu.Game/Screens/Menu/DailyChallengeButton.cs +++ b/osu.Game/Screens/Menu/DailyChallengeButton.cs @@ -155,7 +155,7 @@ namespace osu.Game.Screens.Menu // We only want to notify the user if a new challenge recently went live. if (room.StartDate.Value != null - && Math.Abs((DateTimeOffset.Now - room.StartDate.Value!.Value).TotalSeconds) < 600 + && Math.Abs((DateTimeOffset.Now - room.StartDate.Value!.Value).TotalSeconds) < 1800 && room.RoomID.Value != lastNotifiedDailyChallengeRoomId) { lastNotifiedDailyChallengeRoomId = room.RoomID.Value; From b567ab2a3923d6579cf23849447546e704a621fa Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 12 Aug 2024 20:28:21 +0900 Subject: [PATCH 1913/2493] Fix context menus sometimes not being clickable at song select Closes https://github.com/ppy/osu/issues/21602. --- osu.Game/Screens/Select/BeatmapCarousel.cs | 3 +-- osu.Game/Screens/Select/SongSelect.cs | 3 ++- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index cd0d2eea2c..b0f198d486 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -22,7 +22,6 @@ using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.Database; using osu.Game.Graphics.Containers; -using osu.Game.Graphics.Cursor; using osu.Game.Input.Bindings; using osu.Game.Screens.Select.Carousel; using osuTK; @@ -209,7 +208,7 @@ namespace osu.Game.Screens.Select public BeatmapCarousel() { root = new CarouselRoot(this); - InternalChild = new OsuContextMenuContainer + InternalChild = new Container { RelativeSizeAxes = Axes.Both, Padding = new MarginPadding diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index 307043a312..2ee5a6f3cb 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -26,6 +26,7 @@ using osu.Game.Collections; using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Cursor; using osu.Game.Graphics.UserInterface; using osu.Game.Input.Bindings; using osu.Game.Overlays; @@ -1081,7 +1082,7 @@ namespace osu.Game.Screens.Select Anchor = Anchor.Centre; Origin = Anchor.Centre; Width = panel_overflow; // avoid horizontal masking so the panels don't clip when screen stack is pushed. - InternalChild = Content = new Container + InternalChild = Content = new OsuContextMenuContainer { RelativeSizeAxes = Axes.Both, Anchor = Anchor.Centre, From 7cbb4ab6f1c689f7f57d1ca6f046d4b49cdd1d65 Mon Sep 17 00:00:00 2001 From: CloneWith Date: Tue, 13 Aug 2024 11:50:33 +0800 Subject: [PATCH 1914/2493] Get in-game locale for wiki pages --- osu.Game/Overlays/WikiOverlay.cs | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/osu.Game/Overlays/WikiOverlay.cs b/osu.Game/Overlays/WikiOverlay.cs index ffbc168fb7..88450ea6db 100644 --- a/osu.Game/Overlays/WikiOverlay.cs +++ b/osu.Game/Overlays/WikiOverlay.cs @@ -10,6 +10,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Extensions; +using osu.Game.Localisation; using osu.Game.Online.API; using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests.Responses; @@ -38,11 +39,20 @@ namespace osu.Game.Overlays private WikiArticlePage articlePage; + private Bindable language; + public WikiOverlay() : base(OverlayColourScheme.Orange, false) { } + [BackgroundDependencyLoader] + private void load(OsuGameBase game) + { + // Fetch current language on load for translated pages (if possible) + language = game.CurrentLanguage.GetBoundCopy(); + } + public void ShowPage(string pagePath = INDEX_PATH) { path.Value = pagePath.Trim('/'); @@ -113,12 +123,13 @@ namespace osu.Game.Overlays cancellationToken?.Cancel(); request?.Cancel(); + // Language code + path, or just path1 + path2 in case string[] values = e.NewValue.Split('/', 2); - if (values.Length > 1 && LanguageExtensions.TryParseCultureCode(values[0], out var language)) - request = new GetWikiRequest(values[1], language); + if (values.Length > 1 && LanguageExtensions.TryParseCultureCode(values[0], out var lang)) + request = new GetWikiRequest(values[1], lang); else - request = new GetWikiRequest(e.NewValue); + request = new GetWikiRequest(e.NewValue, language.Value); Loading.Show(); From 12a1889fac182cf521dc87693fb9a6c0e7ecb636 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 13 Aug 2024 14:03:26 +0900 Subject: [PATCH 1915/2493] Use key offsets instead of cast --- osu.Game.Rulesets.Mania/Replays/ManiaAutoGenerator.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Replays/ManiaAutoGenerator.cs b/osu.Game.Rulesets.Mania/Replays/ManiaAutoGenerator.cs index a5cc94ea9a..5d4cebca30 100644 --- a/osu.Game.Rulesets.Mania/Replays/ManiaAutoGenerator.cs +++ b/osu.Game.Rulesets.Mania/Replays/ManiaAutoGenerator.cs @@ -38,11 +38,11 @@ namespace osu.Game.Rulesets.Mania.Replays switch (point) { case HitPoint: - actions.Add((ManiaAction)point.Column); + actions.Add(ManiaAction.Key1 + point.Column); break; case ReleasePoint: - actions.Remove((ManiaAction)point.Column); + actions.Remove(ManiaAction.Key1 + point.Column); break; } } From ca91726190636314d46b9bddd6f865dd870781a2 Mon Sep 17 00:00:00 2001 From: CloneWith Date: Tue, 13 Aug 2024 13:34:11 +0800 Subject: [PATCH 1916/2493] Reload wiki page on language change --- osu.Game/Overlays/WikiOverlay.cs | 39 +++++++++++++++++++++----------- 1 file changed, 26 insertions(+), 13 deletions(-) diff --git a/osu.Game/Overlays/WikiOverlay.cs b/osu.Game/Overlays/WikiOverlay.cs index 88450ea6db..0587832533 100644 --- a/osu.Game/Overlays/WikiOverlay.cs +++ b/osu.Game/Overlays/WikiOverlay.cs @@ -68,7 +68,9 @@ namespace osu.Game.Overlays protected override void LoadComplete() { base.LoadComplete(); + path.BindValueChanged(onPathChanged); + language.BindValueChanged(onLangChanged); wikiData.BindTo(Header.WikiPageData); } @@ -110,26 +112,18 @@ namespace osu.Game.Overlays } } - private void onPathChanged(ValueChangedEvent e) + private void loadPage(string path, Language lang) { - // the path could change as a result of redirecting to a newer location of the same page. - // we already have the correct wiki data, so we can safely return here. - if (e.NewValue == wikiData.Value?.Path) - return; - - if (e.NewValue == "error") - return; - cancellationToken?.Cancel(); request?.Cancel(); // Language code + path, or just path1 + path2 in case - string[] values = e.NewValue.Split('/', 2); + string[] values = path.Split('/', 2); - if (values.Length > 1 && LanguageExtensions.TryParseCultureCode(values[0], out var lang)) - request = new GetWikiRequest(values[1], lang); + if (values.Length > 1 && LanguageExtensions.TryParseCultureCode(values[0], out var parsedLang)) + request = new GetWikiRequest(values[1], parsedLang); else - request = new GetWikiRequest(e.NewValue, language.Value); + request = new GetWikiRequest(path, lang); Loading.Show(); @@ -143,6 +137,25 @@ namespace osu.Game.Overlays api.PerformAsync(request); } + private void onPathChanged(ValueChangedEvent e) + { + // the path could change as a result of redirecting to a newer location of the same page. + // we already have the correct wiki data, so we can safely return here. + if (e.NewValue == wikiData.Value?.Path) + return; + + if (e.NewValue == "error") + return; + + loadPage(e.NewValue, language.Value); + } + + private void onLangChanged(ValueChangedEvent e) + { + // Path unmodified, just reload the page with new language value. + loadPage(path.Value, e.NewValue); + } + private void onSuccess(APIWikiPage response) { wikiData.Value = response; From 952a73b005e83eaea0167790462d78447eeba195 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 13 Aug 2024 15:51:33 +0900 Subject: [PATCH 1917/2493] Make `ManiaAction` start at `1` --- osu.Game.Rulesets.Mania/ManiaInputManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Mania/ManiaInputManager.cs b/osu.Game.Rulesets.Mania/ManiaInputManager.cs index 36ccf68d76..9c58139c37 100644 --- a/osu.Game.Rulesets.Mania/ManiaInputManager.cs +++ b/osu.Game.Rulesets.Mania/ManiaInputManager.cs @@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Mania public enum ManiaAction { [Description("Key 1")] - Key1, + Key1 = 1, [Description("Key 2")] Key2, From 3f02869bcc7455357910f716aa1f5841fda7c2ac Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 13 Aug 2024 16:08:06 +0900 Subject: [PATCH 1918/2493] Enable NRT while we're here --- osu.Game/Overlays/WikiOverlay.cs | 40 +++++++++++++------------------- 1 file changed, 16 insertions(+), 24 deletions(-) diff --git a/osu.Game/Overlays/WikiOverlay.cs b/osu.Game/Overlays/WikiOverlay.cs index 0587832533..14a25a909d 100644 --- a/osu.Game/Overlays/WikiOverlay.cs +++ b/osu.Game/Overlays/WikiOverlay.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 System.Threading; @@ -25,32 +23,35 @@ namespace osu.Game.Overlays public string CurrentPath => path.Value; private readonly Bindable path = new Bindable(INDEX_PATH); - - private readonly Bindable wikiData = new Bindable(); + private readonly Bindable wikiData = new Bindable(); + private readonly IBindable language = new Bindable(); [Resolved] - private IAPIProvider api { get; set; } + private IAPIProvider api { get; set; } = null!; - private GetWikiRequest request; + [Resolved] + private OsuGameBase game { get; set; } = null!; - private CancellationTokenSource cancellationToken; + private GetWikiRequest? request; + private CancellationTokenSource? cancellationToken; + private WikiArticlePage? articlePage; private bool displayUpdateRequired = true; - private WikiArticlePage articlePage; - - private Bindable language; - public WikiOverlay() : base(OverlayColourScheme.Orange, false) { } - [BackgroundDependencyLoader] - private void load(OsuGameBase game) + protected override void LoadComplete() { - // Fetch current language on load for translated pages (if possible) - language = game.CurrentLanguage.GetBoundCopy(); + base.LoadComplete(); + + path.BindValueChanged(onPathChanged); + wikiData.BindTo(Header.WikiPageData); + + language.BindTo(game.CurrentLanguage); + language.BindValueChanged(onLangChanged); } public void ShowPage(string pagePath = INDEX_PATH) @@ -65,15 +66,6 @@ namespace osu.Game.Overlays ShowParentPage = showParentPage, }; - protected override void LoadComplete() - { - base.LoadComplete(); - - path.BindValueChanged(onPathChanged); - language.BindValueChanged(onLangChanged); - wikiData.BindTo(Header.WikiPageData); - } - protected override void PopIn() { base.PopIn(); From 4f37643780d18049a384917debbe3ceed2c8d926 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 13 Aug 2024 16:38:56 +0900 Subject: [PATCH 1919/2493] Revert "Make `ManiaAction` start at `1`" This reverts commit 952a73b005e83eaea0167790462d78447eeba195. --- osu.Game.Rulesets.Mania/ManiaInputManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Mania/ManiaInputManager.cs b/osu.Game.Rulesets.Mania/ManiaInputManager.cs index 9c58139c37..36ccf68d76 100644 --- a/osu.Game.Rulesets.Mania/ManiaInputManager.cs +++ b/osu.Game.Rulesets.Mania/ManiaInputManager.cs @@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Mania public enum ManiaAction { [Description("Key 1")] - Key1 = 1, + Key1, [Description("Key 2")] Key2, From 58354e3e6841b2a2c44be1cc34a0dfdb40455151 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 13 Aug 2024 17:18:11 +0900 Subject: [PATCH 1920/2493] Fix another test The last two PRs didn't interact well together. --- osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs index 070ade4ad9..713f2f3fb1 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs @@ -71,7 +71,7 @@ namespace osu.Game.Tests.Beatmaps.Formats Assert.AreEqual(ScoreRank.B, score.ScoreInfo.Rank); Assert.That(score.Replay.Frames, Has.One.Matches(frame => - frame.Time == 414 && frame.Actions.SequenceEqual(new[] { ManiaAction.Key1, ManiaAction.Key16 }))); + frame.Time == 414 && frame.Actions.SequenceEqual(new[] { ManiaAction.Key1, ManiaAction.Key18 }))); } } From 14a00621f8280a82449e843dd1817f06889e391f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 13 Aug 2024 17:28:16 +0900 Subject: [PATCH 1921/2493] Fix occasional test failures in `TestSceneBetmapRecommendations` The game was being constructed befor the API was setup, which could mean depending on test execution ordering and speed, the recommendations array would not be filled. Easy to reproduce by `[Solo]`ing `TestCorrectStarRatingIsUsed`. See https://github.com/ppy/osu/runs/28689915929#r0s0. --- .../Visual/SongSelect/TestSceneBeatmapRecommendations.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs index a368e901f5..16c8bc1a6b 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs @@ -31,8 +31,6 @@ namespace osu.Game.Tests.Visual.SongSelect [SetUpSteps] public override void SetUpSteps() { - base.SetUpSteps(); - AddStep("populate ruleset statistics", () => { Dictionary rulesetStatistics = new Dictionary(); @@ -68,6 +66,8 @@ namespace osu.Game.Tests.Visual.SongSelect return 0; } } + + base.SetUpSteps(); } [Test] From 93c1a27f226101b37685ff67166d01d71982f43e Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 13 Aug 2024 18:34:15 +0900 Subject: [PATCH 1922/2493] Add failing test --- osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs index b03fa00f76..3eff7ca017 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs @@ -248,7 +248,8 @@ namespace osu.Game.Rulesets.Catch.Tests { AddStep("enable hit lighting", () => config.SetValue(OsuSetting.HitLighting, true)); AddStep("catch fruit", () => attemptCatch(new Fruit())); - AddAssert("correct hit lighting colour", () => catcher.ChildrenOfType().First()?.Entry?.ObjectColour == this.ChildrenOfType().First().AccentColour.Value); + AddAssert("correct hit lighting colour", + () => catcher.ChildrenOfType().First()?.Entry?.ObjectColour == this.ChildrenOfType().First().AccentColour.Value); } [Test] @@ -259,6 +260,16 @@ namespace osu.Game.Rulesets.Catch.Tests AddAssert("no hit lighting", () => !catcher.ChildrenOfType().Any()); } + [Test] + public void TestAllExplodedObjectsAtUniquePositions() + { + AddStep("catch normal fruit", () => attemptCatch(new Fruit())); + AddStep("catch normal fruit", () => attemptCatch(new Fruit { IndexInBeatmap = 2, LastInCombo = true })); + AddAssert("two fruit at distinct x coordinates", + () => this.ChildrenOfType().Select(f => f.DrawPosition.X).Distinct(), + () => Has.Exactly(2).Items); + } + private void checkPlate(int count) => AddAssert($"{count} objects on the plate", () => catcher.CaughtObjects.Count() == count); private void checkState(CatcherAnimationState state) => AddAssert($"catcher state is {state}", () => catcher.CurrentState == state); From fbfe3a488744fea1137f315fd518c17e5a6d5c8a Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 13 Aug 2024 18:07:52 +0900 Subject: [PATCH 1923/2493] Fix fruit positions getting mangled when exploded --- .../Objects/Drawables/CaughtObject.cs | 28 ++++++------- .../DrawablePalpableCatchHitObject.cs | 4 ++ .../Objects/Drawables/IHasCatchObjectState.cs | 34 +++++++++++---- osu.Game.Rulesets.Catch/UI/Catcher.cs | 42 ++++++++++++------- 4 files changed, 69 insertions(+), 39 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/CaughtObject.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/CaughtObject.cs index 0c26c52171..b76e0e9bae 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/CaughtObject.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/CaughtObject.cs @@ -21,11 +21,9 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables public Bindable AccentColour { get; } = new Bindable(); public Bindable HyperDash { get; } = new Bindable(); public Bindable IndexInBeatmap { get; } = new Bindable(); - + public Vector2 DisplayPosition => DrawPosition; public Vector2 DisplaySize => Size * Scale; - public float DisplayRotation => Rotation; - public double DisplayStartTime => HitObject.StartTime; /// @@ -44,19 +42,6 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables Size = new Vector2(CatchHitObject.OBJECT_RADIUS * 2); } - /// - /// Copies the hit object visual state from another object. - /// - public virtual void CopyStateFrom(IHasCatchObjectState objectState) - { - HitObject = objectState.HitObject; - Scale = Vector2.Divide(objectState.DisplaySize, Size); - Rotation = objectState.DisplayRotation; - AccentColour.Value = objectState.AccentColour.Value; - HyperDash.Value = objectState.HyperDash.Value; - IndexInBeatmap.Value = objectState.IndexInBeatmap.Value; - } - protected override void FreeAfterUse() { ClearTransforms(); @@ -64,5 +49,16 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables base.FreeAfterUse(); } + + public void RestoreState(CatchObjectState state) + { + HitObject = state.HitObject; + AccentColour.Value = state.AccentColour; + HyperDash.Value = state.HyperDash; + IndexInBeatmap.Value = state.IndexInBeatmap; + Position = state.DisplayPosition; + Scale = Vector2.Divide(state.DisplaySize, Size); + Rotation = state.DisplayRotation; + } } } diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawablePalpableCatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawablePalpableCatchHitObject.cs index ade00918ab..2919f69966 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawablePalpableCatchHitObject.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawablePalpableCatchHitObject.cs @@ -37,6 +37,8 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables /// protected readonly Container ScalingContainer; + public Vector2 DisplayPosition => DrawPosition; + public Vector2 DisplaySize => ScalingContainer.Size * ScalingContainer.Scale; public float DisplayRotation => ScalingContainer.Rotation; @@ -95,5 +97,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables base.OnFree(); } + + public void RestoreState(CatchObjectState state) => throw new NotSupportedException("Cannot restore state into a drawable catch hitobject."); } } diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/IHasCatchObjectState.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/IHasCatchObjectState.cs index 18fc0db6e3..e4a67d8fbf 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/IHasCatchObjectState.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/IHasCatchObjectState.cs @@ -13,17 +13,37 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables public interface IHasCatchObjectState { PalpableCatchHitObject HitObject { get; } - - double DisplayStartTime { get; } - Bindable AccentColour { get; } - Bindable HyperDash { get; } - Bindable IndexInBeatmap { get; } - + double DisplayStartTime { get; } + Vector2 DisplayPosition { get; } Vector2 DisplaySize { get; } - float DisplayRotation { get; } + + void RestoreState(CatchObjectState state); } + + public static class HasCatchObjectStateExtensions + { + public static CatchObjectState SaveState(this IHasCatchObjectState target) => new CatchObjectState( + target.HitObject, + target.AccentColour.Value, + target.HyperDash.Value, + target.IndexInBeatmap.Value, + target.DisplayStartTime, + target.DisplayPosition, + target.DisplaySize, + target.DisplayRotation); + } + + public readonly record struct CatchObjectState( + PalpableCatchHitObject HitObject, + Color4 AccentColour, + bool HyperDash, + int IndexInBeatmap, + double DisplayStartTime, + Vector2 DisplayPosition, + Vector2 DisplaySize, + float DisplayRotation); } diff --git a/osu.Game.Rulesets.Catch/UI/Catcher.cs b/osu.Game.Rulesets.Catch/UI/Catcher.cs index dca01fc61a..6a1b251d60 100644 --- a/osu.Game.Rulesets.Catch/UI/Catcher.cs +++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Buffers; using System.Diagnostics; using System.Linq; using osu.Framework.Allocation; @@ -362,7 +363,7 @@ namespace osu.Game.Rulesets.Catch.UI if (caughtObject == null) return; - caughtObject.CopyStateFrom(drawableObject); + caughtObject.RestoreState(drawableObject.SaveState()); caughtObject.Anchor = Anchor.TopCentre; caughtObject.Position = position; caughtObject.Scale *= caught_fruit_scale_adjust; @@ -411,41 +412,50 @@ namespace osu.Game.Rulesets.Catch.UI } } - private CaughtObject getDroppedObject(CaughtObject caughtObject) + private CaughtObject getDroppedObject(CatchObjectState state) { - var droppedObject = getCaughtObject(caughtObject.HitObject); + var droppedObject = getCaughtObject(state.HitObject); Debug.Assert(droppedObject != null); - droppedObject.CopyStateFrom(caughtObject); + droppedObject.RestoreState(state); droppedObject.Anchor = Anchor.TopLeft; - droppedObject.Position = caughtObjectContainer.ToSpaceOfOtherDrawable(caughtObject.DrawPosition, droppedObjectTarget); + droppedObject.Position = caughtObjectContainer.ToSpaceOfOtherDrawable(state.DisplayPosition, droppedObjectTarget); return droppedObject; } private void clearPlate(DroppedObjectAnimation animation) { - var caughtObjects = caughtObjectContainer.Children.ToArray(); + int caughtCount = caughtObjectContainer.Children.Count; + CatchObjectState[] states = ArrayPool.Shared.Rent(caughtCount); - caughtObjectContainer.Clear(false); + try + { + for (int i = 0; i < caughtCount; i++) + states[i] = caughtObjectContainer.Children[i].SaveState(); - // Use the already returned PoolableDrawables for new objects - var droppedObjects = caughtObjects.Select(getDroppedObject).ToArray(); + caughtObjectContainer.Clear(false); - droppedObjectTarget.AddRange(droppedObjects); - - foreach (var droppedObject in droppedObjects) - applyDropAnimation(droppedObject, animation); + for (int i = 0; i < caughtCount; i++) + { + CaughtObject obj = getDroppedObject(states[i]); + droppedObjectTarget.Add(obj); + applyDropAnimation(obj, animation); + } + } + finally + { + ArrayPool.Shared.Return(states); + } } private void removeFromPlate(CaughtObject caughtObject, DroppedObjectAnimation animation) { + CatchObjectState state = caughtObject.SaveState(); caughtObjectContainer.Remove(caughtObject, false); - var droppedObject = getDroppedObject(caughtObject); - + var droppedObject = getDroppedObject(state); droppedObjectTarget.Add(droppedObject); - applyDropAnimation(droppedObject, animation); } From c9b2a5bb9cc86d22d3731ad3375a734e36dcf219 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 13 Aug 2024 13:01:50 +0300 Subject: [PATCH 1924/2493] Fix user profile overlay colour resetting when changing rulesets --- osu.Game/Overlays/UserProfileOverlay.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/UserProfileOverlay.cs b/osu.Game/Overlays/UserProfileOverlay.cs index ac1fc44cd6..076905819e 100644 --- a/osu.Game/Overlays/UserProfileOverlay.cs +++ b/osu.Game/Overlays/UserProfileOverlay.cs @@ -96,7 +96,8 @@ namespace osu.Game.Overlays { Debug.Assert(user != null); - if (user.OnlineID == Header.User.Value?.User.Id && ruleset?.MatchesOnlineID(Header.User.Value?.Ruleset) == true) + bool sameUser = user.OnlineID == Header.User.Value?.User.Id; + if (sameUser && ruleset?.MatchesOnlineID(Header.User.Value?.Ruleset) == true) return; if (sectionsContainer != null) @@ -118,7 +119,9 @@ namespace osu.Game.Overlays } : Array.Empty(); - changeOverlayColours(OverlayColourScheme.Pink.GetHue()); + if (!sameUser) + changeOverlayColours(OverlayColourScheme.Pink.GetHue()); + recreateBaseContent(); if (API.State.Value != APIState.Offline) From 7acc1772cbef1116160caabdba787015d9ac6bff Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 13 Aug 2024 13:07:21 +0300 Subject: [PATCH 1925/2493] Add test coverage --- .../Online/TestSceneUserProfileOverlay.cs | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs index 3bb38f167f..006610dccd 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs @@ -11,6 +11,7 @@ using osu.Game.Online.API; using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays; +using osu.Game.Rulesets.Taiko; using osu.Game.Users; namespace osu.Game.Tests.Visual.Online @@ -192,13 +193,26 @@ namespace osu.Game.Tests.Visual.Online int hue2 = 0; AddSliderStep("hue 2", 0, 360, 50, h => hue2 = h); - AddStep("show user", () => profile.ShowUser(new APIUser { Id = 1 })); + AddStep("show user", () => profile.ShowUser(new APIUser { Id = 2 })); AddWaitStep("wait some", 3); AddStep("complete request", () => pendingRequest.TriggerSuccess(new APIUser { Username = $"Colorful #{hue2}", - Id = 1, + Id = 2, + CountryCode = CountryCode.JP, + CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c2.jpg", + ProfileHue = hue2, + PlayMode = "osu", + })); + + AddStep("show user different ruleset", () => profile.ShowUser(new APIUser { Id = 2 }, new TaikoRuleset().RulesetInfo)); + AddWaitStep("wait some", 3); + + AddStep("complete request", () => pendingRequest.TriggerSuccess(new APIUser + { + Username = $"Colorful #{hue2}", + Id = 2, CountryCode = CountryCode.JP, CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c2.jpg", ProfileHue = hue2, From 69c5e6a799175cdc781c30724368186f7e788580 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 13 Aug 2024 19:52:14 +0900 Subject: [PATCH 1926/2493] Remove unused property causing CI inspection I don't see this in my Rider locally. I suppose we can remove it, though it was intentionally added so that the struct mirrors the interface. --- .../Objects/Drawables/IHasCatchObjectState.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/IHasCatchObjectState.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/IHasCatchObjectState.cs index e4a67d8fbf..19e66bf995 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/IHasCatchObjectState.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/IHasCatchObjectState.cs @@ -31,7 +31,6 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables target.AccentColour.Value, target.HyperDash.Value, target.IndexInBeatmap.Value, - target.DisplayStartTime, target.DisplayPosition, target.DisplaySize, target.DisplayRotation); @@ -42,7 +41,6 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables Color4 AccentColour, bool HyperDash, int IndexInBeatmap, - double DisplayStartTime, Vector2 DisplayPosition, Vector2 DisplaySize, float DisplayRotation); From b18706274784430b26526fc4b3eecb75c8ef058e Mon Sep 17 00:00:00 2001 From: OliBomby Date: Tue, 13 Aug 2024 12:58:52 +0200 Subject: [PATCH 1927/2493] clarify meaning of flip axis vector --- osu.Game/Utils/GeometryUtils.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/osu.Game/Utils/GeometryUtils.cs b/osu.Game/Utils/GeometryUtils.cs index 5a8ca9722e..810eda9950 100644 --- a/osu.Game/Utils/GeometryUtils.cs +++ b/osu.Game/Utils/GeometryUtils.cs @@ -51,6 +51,9 @@ namespace osu.Game.Utils /// Given a flip direction, a surrounding quad for all selected objects, and a position, /// will return the flipped position in screen space coordinates. /// + /// The direction to flip towards. + /// The quad surrounding all selected objects. The center of this determines the position of the axis. + /// The position to flip. public static Vector2 GetFlippedPosition(Direction direction, Quad quad, Vector2 position) { var centre = quad.Centre; @@ -73,6 +76,9 @@ namespace osu.Game.Utils /// Given a flip axis vector, a surrounding quad for all selected objects, and a position, /// will return the flipped position in screen space coordinates. /// + /// The vector indicating the direction to flip towards. This is perpendicular to the mirroring axis. + /// The quad surrounding all selected objects. The center of this determines the position of the axis. + /// The position to flip. public static Vector2 GetFlippedPosition(Vector2 axis, Quad quad, Vector2 position) { var centre = quad.Centre; From d74ac57092f3d1953345532010936d93f3beb052 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 13 Aug 2024 19:26:24 +0900 Subject: [PATCH 1928/2493] Never call `prepareDrawables` from unsafe context I can't mentally figure out *what* is causing the issue here, but in the case where `prepareDrawables` is called from `JudgementBody.OnSkinChanged` (only happens in a non-pooled scenario), things go very wrong. I think a smell test is enough for anyone to agree that the flow was very bad. Removing this call doesn't seem to cause any issues. `runAnimation` should always be called in `PrepareForUse` (both pooled and non-pooled scenarios) so things should still always be in a correct state. Closes #29398. --- osu.Game/Rulesets/Judgements/DrawableJudgement.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/osu.Game/Rulesets/Judgements/DrawableJudgement.cs b/osu.Game/Rulesets/Judgements/DrawableJudgement.cs index bdeadfd201..1b12bfc945 100644 --- a/osu.Game/Rulesets/Judgements/DrawableJudgement.cs +++ b/osu.Game/Rulesets/Judgements/DrawableJudgement.cs @@ -119,9 +119,6 @@ namespace osu.Game.Rulesets.Judgements private void runAnimation() { - // is a no-op if the drawables are already in a correct state. - prepareDrawables(); - // undo any transforms applies in ApplyMissAnimations/ApplyHitAnimations to get a sane initial state. ApplyTransformsAt(double.MinValue, true); ClearTransforms(true); From bb0c9e24974f2a8ea2b3c2954d2342f053a7da7f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 13 Aug 2024 19:30:29 +0900 Subject: [PATCH 1929/2493] Add log output when judgements aren't being pooled --- osu.Game/Rulesets/Judgements/DrawableJudgement.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/Rulesets/Judgements/DrawableJudgement.cs b/osu.Game/Rulesets/Judgements/DrawableJudgement.cs index 1b12bfc945..0e04cd3eec 100644 --- a/osu.Game/Rulesets/Judgements/DrawableJudgement.cs +++ b/osu.Game/Rulesets/Judgements/DrawableJudgement.cs @@ -7,6 +7,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Pooling; +using osu.Framework.Logging; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Scoring; @@ -112,6 +113,9 @@ namespace osu.Game.Rulesets.Judgements { base.PrepareForUse(); + if (!IsInPool) + Logger.Log($"{nameof(DrawableJudgement)} for judgement type {Result} was not retrieved from a pool. Consider adding to a JudgementPooler."); + Debug.Assert(Result != null); runAnimation(); From 2221c4891f3fa7e9d0b005adefee982ab6091f2d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 13 Aug 2024 21:03:00 +0900 Subject: [PATCH 1930/2493] Remove legacy non-pooled pathway to `DrawableJudgement` --- .../Skinning/TestSceneDrawableJudgement.cs | 16 +++++++++++----- .../UI/DrawableManiaJudgement.cs | 10 ---------- .../Rulesets/Judgements/DrawableJudgement.cs | 11 ----------- 3 files changed, 11 insertions(+), 26 deletions(-) diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneDrawableJudgement.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneDrawableJudgement.cs index c993ba0e0a..b52919987f 100644 --- a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneDrawableJudgement.cs +++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneDrawableJudgement.cs @@ -28,14 +28,20 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning AddStep("Show " + result.GetDescription(), () => { SetContents(_ => - new DrawableManiaJudgement(new JudgementResult(new HitObject { StartTime = Time.Current }, new Judgement()) - { - Type = result - }, null) + { + var drawableManiaJudgement = new DrawableManiaJudgement { Anchor = Anchor.Centre, Origin = Anchor.Centre, - }); + }; + + drawableManiaJudgement.Apply(new JudgementResult(new HitObject { StartTime = Time.Current }, new Judgement()) + { + Type = result + }, null); + + return drawableManiaJudgement; + }); // for test purposes, undo the Y adjustment related to the `ScorePosition` legacy positioning config value // (see `LegacyManiaJudgementPiece.load()`). diff --git a/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs b/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs index 896dfb2b23..9f25a44e21 100644 --- a/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs +++ b/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs @@ -5,22 +5,12 @@ using osu.Framework.Graphics; using osu.Game.Rulesets.Judgements; -using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Mania.UI { public partial class DrawableManiaJudgement : DrawableJudgement { - public DrawableManiaJudgement(JudgementResult result, DrawableHitObject judgedObject) - : base(result, judgedObject) - { - } - - public DrawableManiaJudgement() - { - } - protected override Drawable CreateDefaultJudgement(HitResult result) => new DefaultManiaJudgementPiece(result); private partial class DefaultManiaJudgementPiece : DefaultJudgementPiece diff --git a/osu.Game/Rulesets/Judgements/DrawableJudgement.cs b/osu.Game/Rulesets/Judgements/DrawableJudgement.cs index 0e04cd3eec..8c326ecf49 100644 --- a/osu.Game/Rulesets/Judgements/DrawableJudgement.cs +++ b/osu.Game/Rulesets/Judgements/DrawableJudgement.cs @@ -36,17 +36,6 @@ namespace osu.Game.Rulesets.Judgements private readonly Lazy proxiedAboveHitObjectsContent; public Drawable ProxiedAboveHitObjectsContent => proxiedAboveHitObjectsContent.Value; - /// - /// Creates a drawable which visualises a . - /// - /// The judgement to visualise. - /// The object which was judged. - public DrawableJudgement(JudgementResult result, DrawableHitObject judgedObject) - : this() - { - Apply(result, judgedObject); - } - public DrawableJudgement() { Size = new Vector2(judgement_size); From ae47671d17e6322a688f24a090020f6188539c1e Mon Sep 17 00:00:00 2001 From: OliBomby Date: Tue, 13 Aug 2024 14:21:42 +0200 Subject: [PATCH 1931/2493] clarify angle ranges in HandleFlip --- osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs index da98da5238..bac0a5e273 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs @@ -138,15 +138,17 @@ namespace osu.Game.Rulesets.Osu.Edit switch (gridToolbox.GridType.Value) { case PositionSnapGridType.Square: - flipAxis = GeometryUtils.RotateVector(Vector2.UnitX, -((gridToolbox.GridLinesRotation.Value + 405) % 90 - 45)); + flipAxis = GeometryUtils.RotateVector(Vector2.UnitX, -((gridToolbox.GridLinesRotation.Value + 360 + 45) % 90 - 45)); flipAxis = direction == Direction.Vertical ? flipAxis.PerpendicularLeft : flipAxis; break; case PositionSnapGridType.Triangle: // Hex grid has 3 axes, so you can not directly flip over one of the axes, // however it's still possible to achieve that flip by combining multiple flips over the other axes. + // Angle degree range for vertical = (-120, -60] + // Angle degree range for horizontal = [-30, 30) flipAxis = direction == Direction.Vertical - ? GeometryUtils.RotateVector(Vector2.UnitX, -((gridToolbox.GridLinesRotation.Value + 390) % 60 + 60)) + ? GeometryUtils.RotateVector(Vector2.UnitX, -((gridToolbox.GridLinesRotation.Value + 360 + 30) % 60 + 60)) : GeometryUtils.RotateVector(Vector2.UnitX, -((gridToolbox.GridLinesRotation.Value + 360) % 60 - 30)); break; } From 78ef436ea085c1e08258cf46a0610677af729af4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 14 Aug 2024 12:23:47 +0900 Subject: [PATCH 1932/2493] Update test debug output to test second scenario --- osu.Game/Online/Multiplayer/MultiplayerClient.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index ee2f1d64dc..d2c69c2ceb 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -788,7 +788,7 @@ namespace osu.Game.Online.Multiplayer try { - indexOf = Room.Playlist.IndexOf(Room.Playlist.Single(existing => existing.ID == item.ID)); + indexOf = APIRoom!.Playlist.IndexOf(APIRoom.Playlist.Single(existing => existing.ID == item.ID)); Room.Playlist[indexOf] = item; } catch From dd9705b660d54d75ee8db01269aed840d6c93bb7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 14 Aug 2024 12:26:21 +0900 Subject: [PATCH 1933/2493] Fix file access test failure by forcing retries See https://github.com/ppy/osu/actions/runs/10369630825/job/28708248682. --- .../Visual/Navigation/TestSceneScreenNavigation.cs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs index 3ae1d9786d..2b23581984 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs @@ -47,6 +47,7 @@ using osu.Game.Screens.Select.Carousel; using osu.Game.Screens.Select.Leaderboards; using osu.Game.Screens.Select.Options; using osu.Game.Tests.Beatmaps.IO; +using osu.Game.Utils; using osuTK; using osuTK.Input; using SharpCompress; @@ -240,11 +241,14 @@ namespace osu.Game.Tests.Visual.Navigation AddStep("change beatmap files", () => { - foreach (var file in Game.Beatmap.Value.BeatmapSetInfo.Files.Where(f => Path.GetExtension(f.Filename) == ".osu")) + FileUtils.AttemptOperation(() => { - using (var stream = Game.Storage.GetStream(Path.Combine("files", file.File.GetStoragePath()), FileAccess.ReadWrite)) - stream.WriteByte(0); - } + foreach (var file in Game.Beatmap.Value.BeatmapSetInfo.Files.Where(f => Path.GetExtension(f.Filename) == ".osu")) + { + using (var stream = Game.Storage.GetStream(Path.Combine("files", file.File.GetStoragePath()), FileAccess.ReadWrite)) + stream.WriteByte(0); + } + }); }); AddStep("invalidate cache", () => From f882ad4a53c4beb0521f57cadd7e969e753b69b6 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 14 Aug 2024 15:10:55 +0900 Subject: [PATCH 1934/2493] Add localisation for daily challenge day/week units --- .../DailyChallengeStatsDisplayStrings.cs | 24 +++++++++++++++++++ .../Components/DailyChallengeStatsDisplay.cs | 4 ++-- .../Components/DailyChallengeStatsTooltip.cs | 13 +++++----- 3 files changed, 33 insertions(+), 8 deletions(-) create mode 100644 osu.Game/Localisation/DailyChallengeStatsDisplayStrings.cs diff --git a/osu.Game/Localisation/DailyChallengeStatsDisplayStrings.cs b/osu.Game/Localisation/DailyChallengeStatsDisplayStrings.cs new file mode 100644 index 0000000000..2ef5e45c92 --- /dev/null +++ b/osu.Game/Localisation/DailyChallengeStatsDisplayStrings.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 osu.Framework.Localisation; + +namespace osu.Game.Localisation +{ + public static class DailyChallengeStatsDisplayStrings + { + private const string prefix = @"osu.Game.Resources.Localisation.DailyChallengeStatsDisplay"; + + /// + /// "{0}d" + /// + public static LocalisableString UnitDay(LocalisableString count) => new TranslatableString(getKey(@"unit_day"), @"{0}d", count); + + /// + /// "{0}w" + /// + public static LocalisableString UnitWeek(LocalisableString count) => new TranslatableString(getKey(@"unit_week"), @"{0}w", count); + + private static string getKey(string key) => $@"{prefix}:{key}"; + } +} diff --git a/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsDisplay.cs b/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsDisplay.cs index f55eb595d7..82d3cfafd7 100644 --- a/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsDisplay.cs +++ b/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsDisplay.cs @@ -12,8 +12,8 @@ using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Online.API.Requests.Responses; -using osu.Game.Resources.Localisation.Web; using osu.Game.Scoring; +using osu.Game.Localisation; namespace osu.Game.Overlays.Profile.Header.Components { @@ -106,7 +106,7 @@ namespace osu.Game.Overlays.Profile.Header.Components APIUserDailyChallengeStatistics stats = User.Value.User.DailyChallengeStatistics; - dailyPlayCount.Text = UsersStrings.ShowDailyChallengeUnitDay(stats.PlayCount.ToLocalisableString("N0")); + dailyPlayCount.Text = DailyChallengeStatsDisplayStrings.UnitDay(stats.PlayCount.ToLocalisableString("N0")); dailyPlayCount.Colour = colours.ForRankingTier(tierForPlayCount(stats.PlayCount)); TooltipContent = new DailyChallengeTooltipData(colourProvider, stats); diff --git a/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsTooltip.cs b/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsTooltip.cs index 1b54633b8a..64a8d67c5b 100644 --- a/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsTooltip.cs +++ b/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsTooltip.cs @@ -9,15 +9,16 @@ using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Effects; +using osu.Framework.Graphics.Shapes; using osu.Framework.Localisation; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; +using osu.Game.Localisation; using osu.Game.Online.API.Requests.Responses; using osu.Game.Resources.Localisation.Web; using osu.Game.Scoring; using osuTK; -using Box = osu.Framework.Graphics.Shapes.Box; -using Color4 = osuTK.Graphics.Color4; +using osuTK.Graphics; namespace osu.Game.Overlays.Profile.Header.Components { @@ -112,16 +113,16 @@ namespace osu.Game.Overlays.Profile.Header.Components background.Colour = colourProvider.Background4; topBackground.Colour = colourProvider.Background5; - currentDaily.Value = UsersStrings.ShowDailyChallengeUnitDay(content.Statistics.DailyStreakCurrent.ToLocalisableString(@"N0")); + currentDaily.Value = DailyChallengeStatsDisplayStrings.UnitDay(content.Statistics.DailyStreakCurrent.ToLocalisableString(@"N0")); currentDaily.ValueColour = colours.ForRankingTier(TierForDaily(statistics.DailyStreakCurrent)); - currentWeekly.Value = UsersStrings.ShowDailyChallengeUnitWeek(statistics.WeeklyStreakCurrent.ToLocalisableString(@"N0")); + currentWeekly.Value = DailyChallengeStatsDisplayStrings.UnitWeek(statistics.WeeklyStreakCurrent.ToLocalisableString(@"N0")); currentWeekly.ValueColour = colours.ForRankingTier(TierForWeekly(statistics.WeeklyStreakCurrent)); - bestDaily.Value = UsersStrings.ShowDailyChallengeUnitDay(statistics.DailyStreakBest.ToLocalisableString(@"N0")); + bestDaily.Value = DailyChallengeStatsDisplayStrings.UnitDay(statistics.DailyStreakBest.ToLocalisableString(@"N0")); bestDaily.ValueColour = colours.ForRankingTier(TierForDaily(statistics.DailyStreakBest)); - bestWeekly.Value = UsersStrings.ShowDailyChallengeUnitWeek(statistics.WeeklyStreakBest.ToLocalisableString(@"N0")); + bestWeekly.Value = DailyChallengeStatsDisplayStrings.UnitWeek(statistics.WeeklyStreakBest.ToLocalisableString(@"N0")); bestWeekly.ValueColour = colours.ForRankingTier(TierForWeekly(statistics.WeeklyStreakBest)); topTen.Value = statistics.Top10PercentPlacements.ToLocalisableString(@"N0"); From 7c142bcedf0877a05b9f899b0648cac6b925d354 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 14 Aug 2024 15:12:23 +0900 Subject: [PATCH 1935/2493] Fix incorrect anchor handling in `ArgonManiaComboCounter` --- .../Skinning/Argon/ArgonManiaComboCounter.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonManiaComboCounter.cs b/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonManiaComboCounter.cs index e77650bed1..43d4e89cdb 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonManiaComboCounter.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonManiaComboCounter.cs @@ -30,8 +30,12 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon private void updateAnchor() { - Anchor &= ~(Anchor.y0 | Anchor.y2); - Anchor |= direction.Value == ScrollingDirection.Up ? Anchor.y2 : Anchor.y0; + // if the anchor isn't a vertical center, set top or bottom anchor based on scroll direction + if (!Anchor.HasFlag(Anchor.y1)) + { + Anchor &= ~(Anchor.y0 | Anchor.y2); + Anchor |= direction.Value == ScrollingDirection.Up ? Anchor.y2 : Anchor.y0; + } // since we flip the vertical anchor when changing scroll direction, // we can use the sign of the Y value as an indicator to make the combo counter displayed correctly. From 46d41cb59083eaf60c3c5c2b35a05eea7aa9d55b Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Mon, 12 Aug 2024 20:01:42 -0700 Subject: [PATCH 1936/2493] Add base song select components test scene --- .../SongSelect/TestSceneSongSelectV2.cs | 5 +-- .../TestSceneSongSelectV2Navigation.cs | 3 +- .../SongSelectComponentsTestScene.cs | 45 +++++++++++++++++++ 3 files changed, 48 insertions(+), 5 deletions(-) create mode 100644 osu.Game.Tests/Visual/SongSelectV2/SongSelectComponentsTestScene.cs diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneSongSelectV2.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneSongSelectV2.cs index 0a632793cc..02f503d433 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneSongSelectV2.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneSongSelectV2.cs @@ -17,7 +17,6 @@ using osu.Game.Rulesets.Osu.Mods; using osu.Game.Screens; using osu.Game.Screens.Footer; using osu.Game.Screens.Menu; -using osu.Game.Screens.SelectV2; using osu.Game.Screens.SelectV2.Footer; using osuTK.Input; @@ -63,8 +62,8 @@ namespace osu.Game.Tests.Visual.SongSelect { base.SetUpSteps(); - AddStep("load screen", () => Stack.Push(new SongSelectV2())); - AddUntilStep("wait for load", () => Stack.CurrentScreen is SongSelectV2 songSelect && songSelect.IsLoaded); + AddStep("load screen", () => Stack.Push(new Screens.SelectV2.SongSelectV2())); + AddUntilStep("wait for load", () => Stack.CurrentScreen is Screens.SelectV2.SongSelectV2 songSelect && songSelect.IsLoaded); } #region Footer diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneSongSelectV2Navigation.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneSongSelectV2Navigation.cs index ededb80228..0ca27c539a 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneSongSelectV2Navigation.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneSongSelectV2Navigation.cs @@ -5,7 +5,6 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Testing; using osu.Game.Screens.Menu; -using osu.Game.Screens.SelectV2; using osuTK.Input; namespace osu.Game.Tests.Visual.SongSelect @@ -17,7 +16,7 @@ namespace osu.Game.Tests.Visual.SongSelect base.SetUpSteps(); AddStep("press enter", () => InputManager.Key(Key.Enter)); AddWaitStep("wait", 5); - PushAndConfirm(() => new SongSelectV2()); + PushAndConfirm(() => new Screens.SelectV2.SongSelectV2()); } [Test] diff --git a/osu.Game.Tests/Visual/SongSelectV2/SongSelectComponentsTestScene.cs b/osu.Game.Tests/Visual/SongSelectV2/SongSelectComponentsTestScene.cs new file mode 100644 index 0000000000..ff81d72d12 --- /dev/null +++ b/osu.Game.Tests/Visual/SongSelectV2/SongSelectComponentsTestScene.cs @@ -0,0 +1,45 @@ +// 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.Bindables; +using osu.Framework.Testing; +using osu.Game.Beatmaps; +using osu.Game.Overlays; + +namespace osu.Game.Tests.Visual.SongSelectV2 +{ + public abstract partial class SongSelectComponentsTestScene : OsuTestScene + { + [Cached] + protected readonly OverlayColourProvider ColourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine); + + /// + /// The beatmap. Can be local/online depending on the context. + /// + [Cached(typeof(IBindable))] + protected readonly Bindable BeatmapInfo = new Bindable(); + + protected override void LoadComplete() + { + base.LoadComplete(); + + // mimics song select's `WorkingBeatmap` binding + Beatmap.BindValueChanged(b => + { + BeatmapInfo.Value = b.NewValue.BeatmapInfo; + }); + } + + [SetUpSteps] + public virtual void SetUpSteps() + { + AddStep("reset dependencies", () => + { + Beatmap.SetDefault(); + SelectedMods.SetDefault(); + BeatmapInfo.Value = null; + }); + } + } +} From 625c6fc7eb9c932d167ceb4b2679a1cb2e7758d4 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Mon, 12 Aug 2024 19:52:42 -0700 Subject: [PATCH 1937/2493] Implement song select v2 difficulty name content component --- .../TestSceneDifficultyNameContent.cs | 97 ++++++++++++++++++ .../SelectV2/Wedge/DifficultyNameContent.cs | 98 +++++++++++++++++++ 2 files changed, 195 insertions(+) create mode 100644 osu.Game.Tests/Visual/SongSelectV2/TestSceneDifficultyNameContent.cs create mode 100644 osu.Game/Screens/SelectV2/Wedge/DifficultyNameContent.cs diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneDifficultyNameContent.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneDifficultyNameContent.cs new file mode 100644 index 0000000000..884dae1617 --- /dev/null +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneDifficultyNameContent.cs @@ -0,0 +1,97 @@ +// 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.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Localisation; +using osu.Framework.Testing; +using osu.Game.Beatmaps; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Sprites; +using osu.Game.Screens.SelectV2.Wedge; + +namespace osu.Game.Tests.Visual.SongSelectV2 +{ + public partial class TestSceneDifficultyNameContent : SongSelectComponentsTestScene + { + private Container? content; + private DifficultyNameContent? difficultyNameContent; + private float relativeWidth; + + [BackgroundDependencyLoader] + private void load() + { + AddSliderStep("change relative width", 0, 1f, 0.5f, v => + { + if (content != null) + content.Width = v; + + relativeWidth = v; + }); + } + + public override void SetUpSteps() + { + base.SetUpSteps(); + + AddStep("set content", () => + { + Child = content = new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding(10), + Width = relativeWidth, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = ColourProvider.Background5, + }, + new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding(10), + Child = difficultyNameContent = new DifficultyNameContent(), + } + } + }; + }); + } + + [Test] + public void TestLocalBeatmap() + { + AddAssert("difficulty name is not set", () => LocalisableString.IsNullOrEmpty(difficultyNameContent.ChildrenOfType().Single().Text)); + AddAssert("author is not set", () => !difficultyNameContent.ChildrenOfType().Single().ChildrenOfType().Any()); + + AddStep("set beatmap", () => Beatmap.Value = CreateWorkingBeatmap(new Beatmap + { + BeatmapInfo = new BeatmapInfo + { + DifficultyName = "really long difficulty name that gets truncated", + Metadata = new BeatmapMetadata + { + Author = { Username = "really long username that is autosized" }, + }, + OnlineID = 1, + } + })); + + AddAssert("difficulty name is set", () => !LocalisableString.IsNullOrEmpty(difficultyNameContent.ChildrenOfType().Single().Text)); + AddAssert("author is set", () => difficultyNameContent.ChildrenOfType().Single().ChildrenOfType().Any()); + } + + [Test] + public void TestNullBeatmap() + { + AddStep("set beatmap", () => BeatmapInfo.Value = null); + } + } +} diff --git a/osu.Game/Screens/SelectV2/Wedge/DifficultyNameContent.cs b/osu.Game/Screens/SelectV2/Wedge/DifficultyNameContent.cs new file mode 100644 index 0000000000..1778246841 --- /dev/null +++ b/osu.Game/Screens/SelectV2/Wedge/DifficultyNameContent.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 osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Beatmaps; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Sprites; + +namespace osu.Game.Screens.SelectV2.Wedge +{ + public partial class DifficultyNameContent : CompositeDrawable + { + private OsuSpriteText difficultyName = null!; + private OsuSpriteText mappedByLabel = null!; + private LinkFlowContainer mapperName = null!; + + [Resolved] + private IBindable beatmapInfo { get; set; } = null!; + + public DifficultyNameContent() + { + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + } + + [BackgroundDependencyLoader] + private void load() + { + InternalChild = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Horizontal, + Children = new Drawable[] + { + difficultyName = new TruncatingSpriteText + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Font = OsuFont.GetFont(weight: FontWeight.SemiBold), + }, + mappedByLabel = new OsuSpriteText + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + // TODO: better null display? beatmap carousel panels also just show this text currently. + Text = " mapped by ", + Font = OsuFont.GetFont(size: 14), + }, + mapperName = new LinkFlowContainer(t => t.Font = OsuFont.GetFont(weight: FontWeight.SemiBold, size: 14)) + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + AutoSizeAxes = Axes.Both, + }, + } + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + beatmapInfo.BindValueChanged(b => + { + difficultyName.Text = b.NewValue?.DifficultyName ?? string.Empty; + updateMapper(); + }, true); + } + + private void updateMapper() + { + mapperName.Clear(); + + switch (beatmapInfo.Value) + { + case BeatmapInfo localBeatmap: + // TODO: should be the mapper of the guest difficulty, but that isn't stored correctly yet (see https://github.com/ppy/osu/issues/12965) + mapperName.AddUserLink(localBeatmap.Metadata.Author); + break; + } + } + + protected override void Update() + { + base.Update(); + + // truncate difficulty name when width exceeds bounds, prioritizing mapper name display + difficultyName.MaxWidth = Math.Max(DrawWidth - mappedByLabel.DrawWidth + - mapperName.DrawWidth, 0); + } + } +} From 2b41f71fd0d688627d8375e3f2ad28bf552a5ba2 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Mon, 12 Aug 2024 23:31:00 -0700 Subject: [PATCH 1938/2493] Workaround single-frame layout issues with `{Link|Text|Fill}FlowContainer`s --- .../TestSceneDifficultyNameContent.cs | 4 +-- .../SelectV2/Wedge/DifficultyNameContent.cs | 35 ++++++++++++++++--- 2 files changed, 33 insertions(+), 6 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneDifficultyNameContent.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneDifficultyNameContent.cs index 884dae1617..75bbf8f32a 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneDifficultyNameContent.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneDifficultyNameContent.cs @@ -69,7 +69,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 public void TestLocalBeatmap() { AddAssert("difficulty name is not set", () => LocalisableString.IsNullOrEmpty(difficultyNameContent.ChildrenOfType().Single().Text)); - AddAssert("author is not set", () => !difficultyNameContent.ChildrenOfType().Single().ChildrenOfType().Any()); + AddAssert("author is not set", () => LocalisableString.IsNullOrEmpty(difficultyNameContent.ChildrenOfType().Single().ChildrenOfType().Single().Text)); AddStep("set beatmap", () => Beatmap.Value = CreateWorkingBeatmap(new Beatmap { @@ -85,7 +85,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 })); AddAssert("difficulty name is set", () => !LocalisableString.IsNullOrEmpty(difficultyNameContent.ChildrenOfType().Single().Text)); - AddAssert("author is set", () => difficultyNameContent.ChildrenOfType().Single().ChildrenOfType().Any()); + AddAssert("author is set", () => !LocalisableString.IsNullOrEmpty(difficultyNameContent.ChildrenOfType().Single().ChildrenOfType().Single().Text)); } [Test] diff --git a/osu.Game/Screens/SelectV2/Wedge/DifficultyNameContent.cs b/osu.Game/Screens/SelectV2/Wedge/DifficultyNameContent.cs index 1778246841..7df8b4a3cc 100644 --- a/osu.Game/Screens/SelectV2/Wedge/DifficultyNameContent.cs +++ b/osu.Game/Screens/SelectV2/Wedge/DifficultyNameContent.cs @@ -10,6 +10,10 @@ using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; +using osu.Game.Localisation; +using osu.Game.Online; +using osu.Game.Online.Chat; +using osu.Game.Overlays; namespace osu.Game.Screens.SelectV2.Wedge { @@ -17,11 +21,15 @@ namespace osu.Game.Screens.SelectV2.Wedge { private OsuSpriteText difficultyName = null!; private OsuSpriteText mappedByLabel = null!; - private LinkFlowContainer mapperName = null!; + private OsuHoverContainer mapperLink = null!; + private OsuSpriteText mapperName = null!; [Resolved] private IBindable beatmapInfo { get; set; } = null!; + [Resolved] + private ILinkHandler? linkHandler { get; set; } + public DifficultyNameContent() { RelativeSizeAxes = Axes.X; @@ -52,11 +60,15 @@ namespace osu.Game.Screens.SelectV2.Wedge Text = " mapped by ", Font = OsuFont.GetFont(size: 14), }, - mapperName = new LinkFlowContainer(t => t.Font = OsuFont.GetFont(weight: FontWeight.SemiBold, size: 14)) + mapperLink = new MapperLink { Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, AutoSizeAxes = Axes.Both, + Child = mapperName = new OsuSpriteText + { + Font = OsuFont.GetFont(weight: FontWeight.SemiBold, size: 14), + } }, } }; @@ -75,13 +87,14 @@ namespace osu.Game.Screens.SelectV2.Wedge private void updateMapper() { - mapperName.Clear(); + mapperName.Text = string.Empty; switch (beatmapInfo.Value) { case BeatmapInfo localBeatmap: // TODO: should be the mapper of the guest difficulty, but that isn't stored correctly yet (see https://github.com/ppy/osu/issues/12965) - mapperName.AddUserLink(localBeatmap.Metadata.Author); + mapperName.Text = localBeatmap.Metadata.Author.Username; + mapperLink.Action = () => linkHandler?.HandleLink(new LinkDetails(LinkAction.OpenUserProfile, localBeatmap.Metadata.Author)); break; } } @@ -94,5 +107,19 @@ namespace osu.Game.Screens.SelectV2.Wedge difficultyName.MaxWidth = Math.Max(DrawWidth - mappedByLabel.DrawWidth - mapperName.DrawWidth, 0); } + + /// + /// This class is a workaround for the single-frame layout issues with `{Link|Text|Fill}FlowContainer`s. + /// See https://github.com/ppy/osu-framework/issues/3369. + /// + private partial class MapperLink : OsuHoverContainer + { + [BackgroundDependencyLoader] + private void load(OverlayColourProvider? overlayColourProvider, OsuColour colours) + { + TooltipText = ContextMenuStrings.ViewProfile; + IdleColour = overlayColourProvider?.Light2 ?? colours.Blue; + } + } } } From f8796e3192ebdc792a3a825399e913dbb6298ef2 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Tue, 13 Aug 2024 22:15:10 -0700 Subject: [PATCH 1939/2493] Move resizing width and background logic to `SongSelectComponentsTestScene` --- .../SongSelectComponentsTestScene.cs | 45 ++++++++++++++++ .../TestSceneDifficultyNameContent.cs | 51 +------------------ 2 files changed, 47 insertions(+), 49 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelectV2/SongSelectComponentsTestScene.cs b/osu.Game.Tests/Visual/SongSelectV2/SongSelectComponentsTestScene.cs index ff81d72d12..4548355992 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/SongSelectComponentsTestScene.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/SongSelectComponentsTestScene.cs @@ -3,6 +3,9 @@ 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.Beatmaps; using osu.Game.Overlays; @@ -11,6 +14,11 @@ namespace osu.Game.Tests.Visual.SongSelectV2 { public abstract partial class SongSelectComponentsTestScene : OsuTestScene { + protected Container ComponentContainer = null!; + + private Container? resizeContainer; + private float relativeWidth; + [Cached] protected readonly OverlayColourProvider ColourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine); @@ -20,6 +28,18 @@ namespace osu.Game.Tests.Visual.SongSelectV2 [Cached(typeof(IBindable))] protected readonly Bindable BeatmapInfo = new Bindable(); + [BackgroundDependencyLoader] + private void load() + { + AddSliderStep("change relative width", 0, 1f, 0.5f, v => + { + if (resizeContainer != null) + resizeContainer.Width = v; + + relativeWidth = v; + }); + } + protected override void LoadComplete() { base.LoadComplete(); @@ -40,6 +60,31 @@ namespace osu.Game.Tests.Visual.SongSelectV2 SelectedMods.SetDefault(); BeatmapInfo.Value = null; }); + + AddStep("set content", () => + { + Child = resizeContainer = new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding(10), + Width = relativeWidth, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = ColourProvider.Background5, + }, + ComponentContainer = new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding(10), + } + } + }; + }); } } } diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneDifficultyNameContent.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneDifficultyNameContent.cs index 75bbf8f32a..b556268be0 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneDifficultyNameContent.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneDifficultyNameContent.cs @@ -3,10 +3,6 @@ using System.Linq; using NUnit.Framework; -using osu.Framework.Allocation; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; using osu.Framework.Localisation; using osu.Framework.Testing; using osu.Game.Beatmaps; @@ -18,56 +14,13 @@ namespace osu.Game.Tests.Visual.SongSelectV2 { public partial class TestSceneDifficultyNameContent : SongSelectComponentsTestScene { - private Container? content; private DifficultyNameContent? difficultyNameContent; - private float relativeWidth; - - [BackgroundDependencyLoader] - private void load() - { - AddSliderStep("change relative width", 0, 1f, 0.5f, v => - { - if (content != null) - content.Width = v; - - relativeWidth = v; - }); - } - - public override void SetUpSteps() - { - base.SetUpSteps(); - - AddStep("set content", () => - { - Child = content = new Container - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Padding = new MarginPadding(10), - Width = relativeWidth, - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = ColourProvider.Background5, - }, - new Container - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Padding = new MarginPadding(10), - Child = difficultyNameContent = new DifficultyNameContent(), - } - } - }; - }); - } [Test] public void TestLocalBeatmap() { + AddStep("set component", () => ComponentContainer.Child = difficultyNameContent = new DifficultyNameContent()); + AddAssert("difficulty name is not set", () => LocalisableString.IsNullOrEmpty(difficultyNameContent.ChildrenOfType().Single().Text)); AddAssert("author is not set", () => LocalisableString.IsNullOrEmpty(difficultyNameContent.ChildrenOfType().Single().ChildrenOfType().Single().Text)); From c24f1444f9ee854a4c0d2cc5eb5ada9ab18fd743 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Tue, 13 Aug 2024 22:21:26 -0700 Subject: [PATCH 1940/2493] Directly resolve `IBindable` by making a local variant of `DifficultyNameContent` --- .../SongSelectComponentsTestScene.cs | 20 ------- .../TestSceneDifficultyNameContent.cs | 8 +-- .../SelectV2/Wedge/DifficultyNameContent.cs | 57 ++++--------------- .../Wedge/LocalDifficultyNameContent.cs | 34 +++++++++++ 4 files changed, 46 insertions(+), 73 deletions(-) create mode 100644 osu.Game/Screens/SelectV2/Wedge/LocalDifficultyNameContent.cs diff --git a/osu.Game.Tests/Visual/SongSelectV2/SongSelectComponentsTestScene.cs b/osu.Game.Tests/Visual/SongSelectV2/SongSelectComponentsTestScene.cs index 4548355992..d984a3a11a 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/SongSelectComponentsTestScene.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/SongSelectComponentsTestScene.cs @@ -2,12 +2,10 @@ // See the LICENCE file in the repository root for full licence text. 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.Beatmaps; using osu.Game.Overlays; namespace osu.Game.Tests.Visual.SongSelectV2 @@ -22,12 +20,6 @@ namespace osu.Game.Tests.Visual.SongSelectV2 [Cached] protected readonly OverlayColourProvider ColourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine); - /// - /// The beatmap. Can be local/online depending on the context. - /// - [Cached(typeof(IBindable))] - protected readonly Bindable BeatmapInfo = new Bindable(); - [BackgroundDependencyLoader] private void load() { @@ -40,17 +32,6 @@ namespace osu.Game.Tests.Visual.SongSelectV2 }); } - protected override void LoadComplete() - { - base.LoadComplete(); - - // mimics song select's `WorkingBeatmap` binding - Beatmap.BindValueChanged(b => - { - BeatmapInfo.Value = b.NewValue.BeatmapInfo; - }); - } - [SetUpSteps] public virtual void SetUpSteps() { @@ -58,7 +39,6 @@ namespace osu.Game.Tests.Visual.SongSelectV2 { Beatmap.SetDefault(); SelectedMods.SetDefault(); - BeatmapInfo.Value = null; }); AddStep("set content", () => diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneDifficultyNameContent.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneDifficultyNameContent.cs index b556268be0..e32d6ddb80 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneDifficultyNameContent.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneDifficultyNameContent.cs @@ -19,7 +19,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 [Test] public void TestLocalBeatmap() { - AddStep("set component", () => ComponentContainer.Child = difficultyNameContent = new DifficultyNameContent()); + AddStep("set component", () => ComponentContainer.Child = difficultyNameContent = new LocalDifficultyNameContent()); AddAssert("difficulty name is not set", () => LocalisableString.IsNullOrEmpty(difficultyNameContent.ChildrenOfType().Single().Text)); AddAssert("author is not set", () => LocalisableString.IsNullOrEmpty(difficultyNameContent.ChildrenOfType().Single().ChildrenOfType().Single().Text)); @@ -40,11 +40,5 @@ namespace osu.Game.Tests.Visual.SongSelectV2 AddAssert("difficulty name is set", () => !LocalisableString.IsNullOrEmpty(difficultyNameContent.ChildrenOfType().Single().Text)); AddAssert("author is set", () => !LocalisableString.IsNullOrEmpty(difficultyNameContent.ChildrenOfType().Single().ChildrenOfType().Single().Text)); } - - [Test] - public void TestNullBeatmap() - { - AddStep("set beatmap", () => BeatmapInfo.Value = null); - } } } diff --git a/osu.Game/Screens/SelectV2/Wedge/DifficultyNameContent.cs b/osu.Game/Screens/SelectV2/Wedge/DifficultyNameContent.cs index 7df8b4a3cc..f49714bee8 100644 --- a/osu.Game/Screens/SelectV2/Wedge/DifficultyNameContent.cs +++ b/osu.Game/Screens/SelectV2/Wedge/DifficultyNameContent.cs @@ -3,34 +3,24 @@ using System; using osu.Framework.Allocation; -using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Localisation; -using osu.Game.Online; -using osu.Game.Online.Chat; using osu.Game.Overlays; namespace osu.Game.Screens.SelectV2.Wedge { - public partial class DifficultyNameContent : CompositeDrawable + public abstract partial class DifficultyNameContent : CompositeDrawable { - private OsuSpriteText difficultyName = null!; + protected OsuSpriteText DifficultyName = null!; private OsuSpriteText mappedByLabel = null!; - private OsuHoverContainer mapperLink = null!; - private OsuSpriteText mapperName = null!; + protected OsuHoverContainer MapperLink = null!; + protected OsuSpriteText MapperName = null!; - [Resolved] - private IBindable beatmapInfo { get; set; } = null!; - - [Resolved] - private ILinkHandler? linkHandler { get; set; } - - public DifficultyNameContent() + protected DifficultyNameContent() { RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; @@ -46,7 +36,7 @@ namespace osu.Game.Screens.SelectV2.Wedge Direction = FillDirection.Horizontal, Children = new Drawable[] { - difficultyName = new TruncatingSpriteText + DifficultyName = new TruncatingSpriteText { Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, @@ -60,12 +50,12 @@ namespace osu.Game.Screens.SelectV2.Wedge Text = " mapped by ", Font = OsuFont.GetFont(size: 14), }, - mapperLink = new MapperLink + MapperLink = new MapperLinkContainer { Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, AutoSizeAxes = Axes.Both, - Child = mapperName = new OsuSpriteText + Child = MapperName = new OsuSpriteText { Font = OsuFont.GetFont(weight: FontWeight.SemiBold, size: 14), } @@ -74,45 +64,20 @@ namespace osu.Game.Screens.SelectV2.Wedge }; } - protected override void LoadComplete() - { - base.LoadComplete(); - - beatmapInfo.BindValueChanged(b => - { - difficultyName.Text = b.NewValue?.DifficultyName ?? string.Empty; - updateMapper(); - }, true); - } - - private void updateMapper() - { - mapperName.Text = string.Empty; - - switch (beatmapInfo.Value) - { - case BeatmapInfo localBeatmap: - // TODO: should be the mapper of the guest difficulty, but that isn't stored correctly yet (see https://github.com/ppy/osu/issues/12965) - mapperName.Text = localBeatmap.Metadata.Author.Username; - mapperLink.Action = () => linkHandler?.HandleLink(new LinkDetails(LinkAction.OpenUserProfile, localBeatmap.Metadata.Author)); - break; - } - } - protected override void Update() { base.Update(); // truncate difficulty name when width exceeds bounds, prioritizing mapper name display - difficultyName.MaxWidth = Math.Max(DrawWidth - mappedByLabel.DrawWidth - - mapperName.DrawWidth, 0); + DifficultyName.MaxWidth = Math.Max(DrawWidth - mappedByLabel.DrawWidth + - MapperName.DrawWidth, 0); } /// /// This class is a workaround for the single-frame layout issues with `{Link|Text|Fill}FlowContainer`s. /// See https://github.com/ppy/osu-framework/issues/3369. /// - private partial class MapperLink : OsuHoverContainer + private partial class MapperLinkContainer : OsuHoverContainer { [BackgroundDependencyLoader] private void load(OverlayColourProvider? overlayColourProvider, OsuColour colours) diff --git a/osu.Game/Screens/SelectV2/Wedge/LocalDifficultyNameContent.cs b/osu.Game/Screens/SelectV2/Wedge/LocalDifficultyNameContent.cs new file mode 100644 index 0000000000..66f8cb02b2 --- /dev/null +++ b/osu.Game/Screens/SelectV2/Wedge/LocalDifficultyNameContent.cs @@ -0,0 +1,34 @@ +// 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.Bindables; +using osu.Game.Beatmaps; +using osu.Game.Online; +using osu.Game.Online.Chat; + +namespace osu.Game.Screens.SelectV2.Wedge +{ + public partial class LocalDifficultyNameContent : DifficultyNameContent + { + [Resolved] + private IBindable beatmap { get; set; } = null!; + + [Resolved] + private ILinkHandler? linkHandler { get; set; } + + protected override void LoadComplete() + { + base.LoadComplete(); + + beatmap.BindValueChanged(b => + { + DifficultyName.Text = b.NewValue.BeatmapInfo.DifficultyName; + + // TODO: should be the mapper of the guest difficulty, but that isn't stored correctly yet (see https://github.com/ppy/osu/issues/12965) + MapperName.Text = b.NewValue.Metadata.Author.Username; + MapperLink.Action = () => linkHandler?.HandleLink(new LinkDetails(LinkAction.OpenUserProfile, b.NewValue.Metadata.Author)); + }, true); + } + } +} From 21745105445fc4d540b2c93f1fd70f8c223337ae Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 14 Aug 2024 15:51:07 +0900 Subject: [PATCH 1941/2493] Move other V2 tests to new test namespace --- .../{SongSelect => SongSelectV2}/TestSceneBeatmapInfoWedgeV2.cs | 2 +- .../{SongSelect => SongSelectV2}/TestSceneLeaderboardScoreV2.cs | 2 +- .../{SongSelect => SongSelectV2}/TestSceneSongSelectV2.cs | 2 +- .../TestSceneSongSelectV2Navigation.cs | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) rename osu.Game.Tests/Visual/{SongSelect => SongSelectV2}/TestSceneBeatmapInfoWedgeV2.cs (99%) rename osu.Game.Tests/Visual/{SongSelect => SongSelectV2}/TestSceneLeaderboardScoreV2.cs (99%) rename osu.Game.Tests/Visual/{SongSelect => SongSelectV2}/TestSceneSongSelectV2.cs (99%) rename osu.Game.Tests/Visual/{SongSelect => SongSelectV2}/TestSceneSongSelectV2Navigation.cs (95%) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedgeV2.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapInfoWedgeV2.cs similarity index 99% rename from osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedgeV2.cs rename to osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapInfoWedgeV2.cs index 2a3269ea0a..aad2bd6334 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedgeV2.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapInfoWedgeV2.cs @@ -18,7 +18,7 @@ using osu.Game.Rulesets.Objects.Types; using osu.Game.Screens.Select; using osuTK; -namespace osu.Game.Tests.Visual.SongSelect +namespace osu.Game.Tests.Visual.SongSelectV2 { [TestFixture] public partial class TestSceneBeatmapInfoWedgeV2 : OsuTestScene diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneLeaderboardScoreV2.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneLeaderboardScoreV2.cs similarity index 99% rename from osu.Game.Tests/Visual/SongSelect/TestSceneLeaderboardScoreV2.cs rename to osu.Game.Tests/Visual/SongSelectV2/TestSceneLeaderboardScoreV2.cs index 33af4907a1..4a733b2cbe 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneLeaderboardScoreV2.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneLeaderboardScoreV2.cs @@ -24,7 +24,7 @@ using osu.Game.Tests.Resources; using osu.Game.Users; using osuTK; -namespace osu.Game.Tests.Visual.SongSelect +namespace osu.Game.Tests.Visual.SongSelectV2 { public partial class TestSceneLeaderboardScoreV2 : OsuTestScene { diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneSongSelectV2.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelectV2.cs similarity index 99% rename from osu.Game.Tests/Visual/SongSelect/TestSceneSongSelectV2.cs rename to osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelectV2.cs index 02f503d433..c93c41d558 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneSongSelectV2.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelectV2.cs @@ -20,7 +20,7 @@ using osu.Game.Screens.Menu; using osu.Game.Screens.SelectV2.Footer; using osuTK.Input; -namespace osu.Game.Tests.Visual.SongSelect +namespace osu.Game.Tests.Visual.SongSelectV2 { public partial class TestSceneSongSelectV2 : ScreenTestScene { diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneSongSelectV2Navigation.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelectV2Navigation.cs similarity index 95% rename from osu.Game.Tests/Visual/SongSelect/TestSceneSongSelectV2Navigation.cs rename to osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelectV2Navigation.cs index 0ca27c539a..a72146352e 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneSongSelectV2Navigation.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelectV2Navigation.cs @@ -7,7 +7,7 @@ using osu.Framework.Testing; using osu.Game.Screens.Menu; using osuTK.Input; -namespace osu.Game.Tests.Visual.SongSelect +namespace osu.Game.Tests.Visual.SongSelectV2 { public partial class TestSceneSongSelectV2Navigation : OsuGameTestScene { From 11bd0c9a6141a61023cdb43e1f4d3755d55c5c21 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Wed, 14 Aug 2024 00:41:43 -0700 Subject: [PATCH 1942/2493] Inline single-frame layout issue comment instead --- osu.Game/Screens/SelectV2/Wedge/DifficultyNameContent.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/SelectV2/Wedge/DifficultyNameContent.cs b/osu.Game/Screens/SelectV2/Wedge/DifficultyNameContent.cs index f49714bee8..4a3dc34cf9 100644 --- a/osu.Game/Screens/SelectV2/Wedge/DifficultyNameContent.cs +++ b/osu.Game/Screens/SelectV2/Wedge/DifficultyNameContent.cs @@ -50,6 +50,8 @@ namespace osu.Game.Screens.SelectV2.Wedge Text = " mapped by ", Font = OsuFont.GetFont(size: 14), }, + // This is not a `LinkFlowContainer` as there are single-frame layout issues when Update() + // is being used for layout, see https://github.com/ppy/osu-framework/issues/3369. MapperLink = new MapperLinkContainer { Anchor = Anchor.BottomLeft, @@ -73,10 +75,6 @@ namespace osu.Game.Screens.SelectV2.Wedge - MapperName.DrawWidth, 0); } - /// - /// This class is a workaround for the single-frame layout issues with `{Link|Text|Fill}FlowContainer`s. - /// See https://github.com/ppy/osu-framework/issues/3369. - /// private partial class MapperLinkContainer : OsuHoverContainer { [BackgroundDependencyLoader] From 6f2bc7e6f12350e66c4c5679d33d0e7db1490484 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Wed, 14 Aug 2024 00:44:03 -0700 Subject: [PATCH 1943/2493] Use `Content` override instead --- .../SongSelectComponentsTestScene.cs | 26 +++++++++---------- .../TestSceneDifficultyNameContent.cs | 2 +- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelectV2/SongSelectComponentsTestScene.cs b/osu.Game.Tests/Visual/SongSelectV2/SongSelectComponentsTestScene.cs index d984a3a11a..1583d229c5 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/SongSelectComponentsTestScene.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/SongSelectComponentsTestScene.cs @@ -12,14 +12,19 @@ namespace osu.Game.Tests.Visual.SongSelectV2 { public abstract partial class SongSelectComponentsTestScene : OsuTestScene { - protected Container ComponentContainer = null!; + [Cached] + protected readonly OverlayColourProvider ColourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine); + + protected override Container Content { get; } = new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding(10), + }; private Container? resizeContainer; private float relativeWidth; - [Cached] - protected readonly OverlayColourProvider ColourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine); - [BackgroundDependencyLoader] private void load() { @@ -41,9 +46,9 @@ namespace osu.Game.Tests.Visual.SongSelectV2 SelectedMods.SetDefault(); }); - AddStep("set content", () => + AddStep("setup content", () => { - Child = resizeContainer = new Container + base.Content.Add(resizeContainer = new Container { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, @@ -56,14 +61,9 @@ namespace osu.Game.Tests.Visual.SongSelectV2 RelativeSizeAxes = Axes.Both, Colour = ColourProvider.Background5, }, - ComponentContainer = new Container - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Padding = new MarginPadding(10), - } + Content } - }; + }); }); } } diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneDifficultyNameContent.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneDifficultyNameContent.cs index e32d6ddb80..49e7e2bc1a 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneDifficultyNameContent.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneDifficultyNameContent.cs @@ -19,7 +19,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 [Test] public void TestLocalBeatmap() { - AddStep("set component", () => ComponentContainer.Child = difficultyNameContent = new LocalDifficultyNameContent()); + AddStep("set component", () => Child = difficultyNameContent = new LocalDifficultyNameContent()); AddAssert("difficulty name is not set", () => LocalisableString.IsNullOrEmpty(difficultyNameContent.ChildrenOfType().Single().Text)); AddAssert("author is not set", () => LocalisableString.IsNullOrEmpty(difficultyNameContent.ChildrenOfType().Single().ChildrenOfType().Single().Text)); From 28ab65243d0159ba9ea7015ef58d7916c53c61e6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 14 Aug 2024 20:45:27 +0900 Subject: [PATCH 1944/2493] Remove daily challenge tooltip from main menu Now that we have a nice intro screen for the daily challenge, it's generally thought that we want to "spoil" the beatmap until the intro is shown. Also I was never a huge fan of having a tooltip on a main menu button.. just feels a bit odd. --- osu.Game/Screens/Menu/DailyChallengeButton.cs | 34 +------------------ 1 file changed, 1 insertion(+), 33 deletions(-) diff --git a/osu.Game/Screens/Menu/DailyChallengeButton.cs b/osu.Game/Screens/Menu/DailyChallengeButton.cs index e19ba6612c..e6593c9b0d 100644 --- a/osu.Game/Screens/Menu/DailyChallengeButton.cs +++ b/osu.Game/Screens/Menu/DailyChallengeButton.cs @@ -9,12 +9,10 @@ using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Shapes; using osu.Framework.Threading; using osu.Framework.Utils; using osu.Game.Beatmaps.Drawables; -using osu.Game.Beatmaps.Drawables.Cards; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Localisation; @@ -30,7 +28,7 @@ using osuTK.Input; namespace osu.Game.Screens.Menu { - public partial class DailyChallengeButton : MainMenuButton, IHasCustomTooltip + public partial class DailyChallengeButton : MainMenuButton { public Room? Room { get; private set; } @@ -201,36 +199,6 @@ namespace osu.Game.Screens.Menu base.UpdateState(); } - public ITooltip GetCustomTooltip() => new DailyChallengeTooltip(); - public APIBeatmapSet? TooltipContent { get; private set; } - - internal partial class DailyChallengeTooltip : CompositeDrawable, ITooltip - { - [Cached] - private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple); - - private APIBeatmapSet? lastContent; - - [BackgroundDependencyLoader] - private void load() - { - AutoSizeAxes = Axes.Both; - } - - public void Move(Vector2 pos) => Position = pos; - - public void SetContent(APIBeatmapSet? content) - { - if (content == lastContent) - return; - - lastContent = content; - - ClearInternal(); - if (content != null) - AddInternal(new BeatmapCardNano(content)); - } - } } } From 1665d9a93e03177295036ac68c1ca551ad95df89 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 14 Aug 2024 21:01:35 +0900 Subject: [PATCH 1945/2493] Fix failing test setup --- .../SongSelectComponentsTestScene.cs | 37 +++++++++---------- 1 file changed, 17 insertions(+), 20 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelectV2/SongSelectComponentsTestScene.cs b/osu.Game.Tests/Visual/SongSelectV2/SongSelectComponentsTestScene.cs index 1583d229c5..c7f1597051 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/SongSelectComponentsTestScene.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/SongSelectComponentsTestScene.cs @@ -28,6 +28,23 @@ namespace osu.Game.Tests.Visual.SongSelectV2 [BackgroundDependencyLoader] private void load() { + base.Content.Child = resizeContainer = new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding(10), + Width = relativeWidth, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = ColourProvider.Background5, + }, + Content + } + }; + AddSliderStep("change relative width", 0, 1f, 0.5f, v => { if (resizeContainer != null) @@ -45,26 +62,6 @@ namespace osu.Game.Tests.Visual.SongSelectV2 Beatmap.SetDefault(); SelectedMods.SetDefault(); }); - - AddStep("setup content", () => - { - base.Content.Add(resizeContainer = new Container - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Padding = new MarginPadding(10), - Width = relativeWidth, - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = ColourProvider.Background5, - }, - Content - } - }); - }); } } } From e603888130dce54965411cda873d05c2a4b63de3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 14 Aug 2024 21:09:28 +0900 Subject: [PATCH 1946/2493] Update remaining tests to use new base class (and tidy up `V2` suffixes) --- .../SongSelectComponentsTestScene.cs | 2 +- ...edgeV2.cs => TestSceneBeatmapInfoWedge.cs} | 16 +++++++------- ...coreV2.cs => TestSceneLeaderboardScore.cs} | 21 ++----------------- ...SongSelectV2.cs => TestSceneSongSelect.cs} | 4 ++-- ...on.cs => TestSceneSongSelectNavigation.cs} | 2 +- 5 files changed, 14 insertions(+), 31 deletions(-) rename osu.Game.Tests/Visual/SongSelectV2/{TestSceneBeatmapInfoWedgeV2.cs => TestSceneBeatmapInfoWedge.cs} (97%) rename osu.Game.Tests/Visual/SongSelectV2/{TestSceneLeaderboardScoreV2.cs => TestSceneLeaderboardScore.cs} (91%) rename osu.Game.Tests/Visual/SongSelectV2/{TestSceneSongSelectV2.cs => TestSceneSongSelect.cs} (98%) rename osu.Game.Tests/Visual/SongSelectV2/{TestSceneSongSelectV2Navigation.cs => TestSceneSongSelectNavigation.cs} (92%) diff --git a/osu.Game.Tests/Visual/SongSelectV2/SongSelectComponentsTestScene.cs b/osu.Game.Tests/Visual/SongSelectV2/SongSelectComponentsTestScene.cs index c7f1597051..b7b0101a7c 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/SongSelectComponentsTestScene.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/SongSelectComponentsTestScene.cs @@ -45,7 +45,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 } }; - AddSliderStep("change relative width", 0, 1f, 0.5f, v => + AddSliderStep("change relative width", 0, 1f, 1f, v => { if (resizeContainer != null) resizeContainer.Width = v; diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapInfoWedgeV2.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapInfoWedge.cs similarity index 97% rename from osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapInfoWedgeV2.cs rename to osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapInfoWedge.cs index aad2bd6334..35bd4ee958 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapInfoWedgeV2.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapInfoWedge.cs @@ -20,8 +20,7 @@ using osuTK; namespace osu.Game.Tests.Visual.SongSelectV2 { - [TestFixture] - public partial class TestSceneBeatmapInfoWedgeV2 : OsuTestScene + public partial class TestSceneBeatmapInfoWedge : SongSelectComponentsTestScene { private RulesetStore rulesets = null!; private TestBeatmapInfoWedgeV2 infoWedge = null!; @@ -33,6 +32,13 @@ namespace osu.Game.Tests.Visual.SongSelectV2 this.rulesets = rulesets; } + public override void SetUpSteps() + { + base.SetUpSteps(); + + AddStep("reset mods", () => SelectedMods.SetDefault()); + } + protected override void LoadComplete() { base.LoadComplete(); @@ -107,12 +113,6 @@ namespace osu.Game.Tests.Visual.SongSelectV2 AddAssert("check artist", () => infoWedge.Info!.ArtistLabel.Current.Value == $"{ruleset.ShortName}Artist"); } - [SetUpSteps] - public void SetUpSteps() - { - AddStep("reset mods", () => SelectedMods.SetDefault()); - } - [Test] public void TestTruncation() { diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneLeaderboardScoreV2.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneLeaderboardScore.cs similarity index 91% rename from osu.Game.Tests/Visual/SongSelectV2/TestSceneLeaderboardScoreV2.cs rename to osu.Game.Tests/Visual/SongSelectV2/TestSceneLeaderboardScore.cs index 4a733b2cbe..a7d0d70c03 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneLeaderboardScoreV2.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneLeaderboardScore.cs @@ -7,7 +7,6 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Testing; using osu.Framework.Utils; using osu.Game.Configuration; using osu.Game.Graphics.Sprites; @@ -26,7 +25,7 @@ using osuTK; namespace osu.Game.Tests.Visual.SongSelectV2 { - public partial class TestSceneLeaderboardScoreV2 : OsuTestScene + public partial class TestSceneLeaderboardScore : SongSelectComponentsTestScene { [Cached] private OverlayColourProvider colourProvider { get; set; } = new OverlayColourProvider(OverlayColourScheme.Aquamarine); @@ -36,19 +35,6 @@ namespace osu.Game.Tests.Visual.SongSelectV2 private FillFlowContainer? fillFlow; private OsuSpriteText? drawWidthText; - private float relativeWidth; - - [BackgroundDependencyLoader] - private void load() - { - // TODO: invalidation seems to be one-off when clicking slider to a certain value, so drag for now - // doesn't seem to happen in-game (when toggling window mode) - AddSliderStep("change relative width", 0, 1f, 0.6f, v => - { - relativeWidth = v; - if (fillFlow != null) fillFlow.Width = v; - }); - } [Test] public void TestSheared() @@ -59,7 +45,6 @@ namespace osu.Game.Tests.Visual.SongSelectV2 { fillFlow = new FillFlowContainer { - Width = relativeWidth, Anchor = Anchor.Centre, Origin = Anchor.Centre, RelativeSizeAxes = Axes.X, @@ -94,7 +79,6 @@ namespace osu.Game.Tests.Visual.SongSelectV2 { fillFlow = new FillFlowContainer { - Width = relativeWidth, Anchor = Anchor.Centre, Origin = Anchor.Centre, RelativeSizeAxes = Axes.X, @@ -118,8 +102,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 }); } - [SetUpSteps] - public void SetUpSteps() + public override void SetUpSteps() { AddToggleStep("toggle scoring mode", v => config.SetValue(OsuSetting.ScoreDisplayMode, v ? ScoringMode.Classic : ScoringMode.Standardised)); } diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelectV2.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelect.cs similarity index 98% rename from osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelectV2.cs rename to osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelect.cs index c93c41d558..d43026c960 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelectV2.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelect.cs @@ -22,7 +22,7 @@ using osuTK.Input; namespace osu.Game.Tests.Visual.SongSelectV2 { - public partial class TestSceneSongSelectV2 : ScreenTestScene + public partial class TestSceneSongSelect : ScreenTestScene { [Cached] private readonly ScreenFooter screenScreenFooter; @@ -30,7 +30,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 [Cached] private readonly OsuLogo logo; - public TestSceneSongSelectV2() + public TestSceneSongSelect() { Children = new Drawable[] { diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelectV2Navigation.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelectNavigation.cs similarity index 92% rename from osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelectV2Navigation.cs rename to osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelectNavigation.cs index a72146352e..5173cb5673 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelectV2Navigation.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelectNavigation.cs @@ -9,7 +9,7 @@ using osuTK.Input; namespace osu.Game.Tests.Visual.SongSelectV2 { - public partial class TestSceneSongSelectV2Navigation : OsuGameTestScene + public partial class TestSceneSongSelectNavigation : OsuGameTestScene { public override void SetUpSteps() { From 054366b25dc83a3c7ed4598bb7b341b3ec7c568b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 15 Aug 2024 13:07:42 +0900 Subject: [PATCH 1947/2493] Use zero baseline for legacy sprite text display --- osu.Game/Skinning/LegacySpriteText.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Skinning/LegacySpriteText.cs b/osu.Game/Skinning/LegacySpriteText.cs index fdd8716d5a..1028b5bb9d 100644 --- a/osu.Game/Skinning/LegacySpriteText.cs +++ b/osu.Game/Skinning/LegacySpriteText.cs @@ -96,7 +96,7 @@ namespace osu.Game.Skinning if (maxSize != null) texture = texture.WithMaximumSize(maxSize.Value); - glyph = new TexturedCharacterGlyph(new CharacterGlyph(character, 0, 0, texture.Width, texture.Height, null), texture, 1f / texture.ScaleAdjust); + glyph = new TexturedCharacterGlyph(new CharacterGlyph(character, 0, 0, texture.Width, 0, null), texture, 1f / texture.ScaleAdjust); } cache[character] = glyph; From ff1ab2bb0ef28335a834a1175a6dbc55b9e3f8b6 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 15 Aug 2024 14:59:40 +0900 Subject: [PATCH 1948/2493] Remove position-flipping logic from mania combo counters for now We need a general method to do this amicably, such as an HUD target that flips the position of its children when the direction is flipped. Something to consider later. --- .../Skinning/Argon/ArgonManiaComboCounter.cs | 34 ------------------- .../Legacy/LegacyManiaComboCounter.cs | 34 ------------------- 2 files changed, 68 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonManiaComboCounter.cs b/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonManiaComboCounter.cs index 43d4e89cdb..04c08cc509 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonManiaComboCounter.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonManiaComboCounter.cs @@ -1,46 +1,12 @@ // 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.Bindables; -using osu.Framework.Graphics; -using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Screens.Play.HUD; namespace osu.Game.Rulesets.Mania.Skinning.Argon { public partial class ArgonManiaComboCounter : ArgonComboCounter { - [Resolved] - private IScrollingInfo scrollingInfo { get; set; } = null!; - private IBindable direction = null!; - - protected override void LoadComplete() - { - base.LoadComplete(); - - direction = scrollingInfo.Direction.GetBoundCopy(); - direction.BindValueChanged(_ => updateAnchor()); - - // two schedules are required so that updateAnchor is executed in the next frame, - // which is when the combo counter receives its Y position by the default layout in ArgonManiaSkinTransformer. - Schedule(() => Schedule(updateAnchor)); - } - - private void updateAnchor() - { - // if the anchor isn't a vertical center, set top or bottom anchor based on scroll direction - if (!Anchor.HasFlag(Anchor.y1)) - { - Anchor &= ~(Anchor.y0 | Anchor.y2); - Anchor |= direction.Value == ScrollingDirection.Up ? Anchor.y2 : Anchor.y0; - } - - // since we flip the vertical anchor when changing scroll direction, - // we can use the sign of the Y value as an indicator to make the combo counter displayed correctly. - if ((Y < 0 && direction.Value == ScrollingDirection.Down) || (Y > 0 && direction.Value == ScrollingDirection.Up)) - Y = -Y; - } } } diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyManiaComboCounter.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyManiaComboCounter.cs index 5832210836..000e96540a 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyManiaComboCounter.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyManiaComboCounter.cs @@ -2,9 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; -using osu.Framework.Bindables; using osu.Framework.Graphics; -using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Skinning; using osuTK; using osuTK.Graphics; @@ -24,38 +22,6 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy PopOutCountText.Colour = skin.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.ComboBreakColour)?.Value ?? Color4.Red; } - [Resolved] - private IScrollingInfo scrollingInfo { get; set; } = null!; - - private IBindable direction = null!; - - protected override void LoadComplete() - { - base.LoadComplete(); - - direction = scrollingInfo.Direction.GetBoundCopy(); - direction.BindValueChanged(_ => updateAnchor()); - - // two schedules are required so that updateAnchor is executed in the next frame, - // which is when the combo counter receives its Y position by the default layout in LegacyManiaSkinTransformer. - Schedule(() => Schedule(updateAnchor)); - } - - private void updateAnchor() - { - // if the anchor isn't a vertical center, set top or bottom anchor based on scroll direction - if (!Anchor.HasFlag(Anchor.y1)) - { - Anchor &= ~(Anchor.y0 | Anchor.y2); - Anchor |= direction.Value == ScrollingDirection.Up ? Anchor.y2 : Anchor.y0; - } - - // since we flip the vertical anchor when changing scroll direction, - // we can use the sign of the Y value as an indicator to make the combo counter displayed correctly. - if ((Y < 0 && direction.Value == ScrollingDirection.Down) || (Y > 0 && direction.Value == ScrollingDirection.Up)) - Y = -Y; - } - protected override void OnCountIncrement() { base.OnCountIncrement(); From 3a4546d62df3ceb50ddc7dac61e3989488b1a239 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 15 Aug 2024 15:02:28 +0900 Subject: [PATCH 1949/2493] Remove `x` symbol from argon mania combo counter --- .../Skinning/Argon/ArgonManiaComboCounter.cs | 2 +- osu.Game/Screens/Play/HUD/ArgonComboCounter.cs | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonManiaComboCounter.cs b/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonManiaComboCounter.cs index 04c08cc509..2f93a1fb90 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonManiaComboCounter.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonManiaComboCounter.cs @@ -7,6 +7,6 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon { public partial class ArgonManiaComboCounter : ArgonComboCounter { - + protected override bool DisplayXSymbol => false; } } diff --git a/osu.Game/Screens/Play/HUD/ArgonComboCounter.cs b/osu.Game/Screens/Play/HUD/ArgonComboCounter.cs index 3f74a8d4e8..e82e8f4b6f 100644 --- a/osu.Game/Screens/Play/HUD/ArgonComboCounter.cs +++ b/osu.Game/Screens/Play/HUD/ArgonComboCounter.cs @@ -23,6 +23,8 @@ namespace osu.Game.Screens.Play.HUD protected override double RollingDuration => 250; + protected virtual bool DisplayXSymbol => true; + [SettingSource("Wireframe opacity", "Controls the opacity of the wireframes behind the digits.")] public BindableFloat WireframeOpacity { get; } = new BindableFloat(0.25f) { @@ -76,15 +78,15 @@ namespace osu.Game.Screens.Play.HUD private int getDigitsRequiredForDisplayCount() { - // one for the single presumed starting digit, one for the "x" at the end. - int digitsRequired = 2; + // one for the single presumed starting digit, one for the "x" at the end (unless disabled). + int digitsRequired = DisplayXSymbol ? 2 : 1; long c = DisplayedCount; while ((c /= 10) > 0) digitsRequired++; return digitsRequired; } - protected override LocalisableString FormatCount(int count) => $@"{count}x"; + protected override LocalisableString FormatCount(int count) => DisplayXSymbol ? $@"{count}x" : count.ToString(); protected override IHasText CreateText() => Text = new ArgonCounterTextComponent(Anchor.TopLeft, MatchesStrings.MatchScoreStatsCombo.ToUpper()) { From 66adddbfb8020c019bbdf672ade3f00b4aead7ef Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 15 Aug 2024 15:48:17 +0900 Subject: [PATCH 1950/2493] Actually bring back position-flipping logic and disable "closest" anchor --- .../Skinning/Argon/ArgonManiaComboCounter.cs | 40 +++++++++++++++++++ .../Legacy/LegacyManiaComboCounter.cs | 34 ++++++++++++++++ 2 files changed, 74 insertions(+) diff --git a/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonManiaComboCounter.cs b/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonManiaComboCounter.cs index 2f93a1fb90..8d51b59324 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonManiaComboCounter.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonManiaComboCounter.cs @@ -1,6 +1,10 @@ // 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.Bindables; +using osu.Framework.Graphics; +using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Screens.Play.HUD; namespace osu.Game.Rulesets.Mania.Skinning.Argon @@ -8,5 +12,41 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon public partial class ArgonManiaComboCounter : ArgonComboCounter { protected override bool DisplayXSymbol => false; + + [Resolved] + private IScrollingInfo scrollingInfo { get; set; } = null!; + + private IBindable direction = null!; + + protected override void LoadComplete() + { + base.LoadComplete(); + + // the logic of flipping the position of the combo counter w.r.t. the direction does not work with "Closest" anchor, + // because it always forces the anchor to be top or bottom based on scrolling direction. + UsesFixedAnchor = true; + + direction = scrollingInfo.Direction.GetBoundCopy(); + direction.BindValueChanged(_ => updateAnchor()); + + // two schedules are required so that updateAnchor is executed in the next frame, + // which is when the combo counter receives its Y position by the default layout in ArgonManiaSkinTransformer. + Schedule(() => Schedule(updateAnchor)); + } + + private void updateAnchor() + { + // if the anchor isn't a vertical center, set top or bottom anchor based on scroll direction + if (!Anchor.HasFlag(Anchor.y1)) + { + Anchor &= ~(Anchor.y0 | Anchor.y2); + Anchor |= direction.Value == ScrollingDirection.Up ? Anchor.y2 : Anchor.y0; + } + + // since we flip the vertical anchor when changing scroll direction, + // we can use the sign of the Y value as an indicator to make the combo counter displayed correctly. + if ((Y < 0 && direction.Value == ScrollingDirection.Down) || (Y > 0 && direction.Value == ScrollingDirection.Up)) + Y = -Y; + } } } diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyManiaComboCounter.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyManiaComboCounter.cs index 000e96540a..5832210836 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyManiaComboCounter.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyManiaComboCounter.cs @@ -2,7 +2,9 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Skinning; using osuTK; using osuTK.Graphics; @@ -22,6 +24,38 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy PopOutCountText.Colour = skin.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.ComboBreakColour)?.Value ?? Color4.Red; } + [Resolved] + private IScrollingInfo scrollingInfo { get; set; } = null!; + + private IBindable direction = null!; + + protected override void LoadComplete() + { + base.LoadComplete(); + + direction = scrollingInfo.Direction.GetBoundCopy(); + direction.BindValueChanged(_ => updateAnchor()); + + // two schedules are required so that updateAnchor is executed in the next frame, + // which is when the combo counter receives its Y position by the default layout in LegacyManiaSkinTransformer. + Schedule(() => Schedule(updateAnchor)); + } + + private void updateAnchor() + { + // if the anchor isn't a vertical center, set top or bottom anchor based on scroll direction + if (!Anchor.HasFlag(Anchor.y1)) + { + Anchor &= ~(Anchor.y0 | Anchor.y2); + Anchor |= direction.Value == ScrollingDirection.Up ? Anchor.y2 : Anchor.y0; + } + + // since we flip the vertical anchor when changing scroll direction, + // we can use the sign of the Y value as an indicator to make the combo counter displayed correctly. + if ((Y < 0 && direction.Value == ScrollingDirection.Down) || (Y > 0 && direction.Value == ScrollingDirection.Up)) + Y = -Y; + } + protected override void OnCountIncrement() { base.OnCountIncrement(); From 26da2c06372ca53e1772a59db6d7422946a4ee39 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 15 Aug 2024 16:16:48 +0900 Subject: [PATCH 1951/2493] Update `MultiplayerClient` test output with new knowledge --- .../Online/Multiplayer/MultiplayerClient.cs | 24 ++++++++----------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index d2c69c2ceb..07e779c2ba 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -5,14 +5,15 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using System.Text; using System.Threading; using System.Threading.Tasks; -using Newtonsoft.Json; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Development; using osu.Framework.Graphics; using osu.Game.Database; +using osu.Game.Localisation; using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Multiplayer.Countdown; @@ -22,7 +23,6 @@ using osu.Game.Overlays.Notifications; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Utils; -using osu.Game.Localisation; namespace osu.Game.Online.Multiplayer { @@ -777,26 +777,22 @@ namespace osu.Game.Online.Multiplayer Room.Playlist[Room.Playlist.IndexOf(Room.Playlist.Single(existing => existing.ID == item.ID))] = item; int existingIndex = APIRoom.Playlist.IndexOf(APIRoom.Playlist.Single(existing => existing.ID == item.ID)); + APIRoom.Playlist.RemoveAt(existingIndex); APIRoom.Playlist.Insert(existingIndex, createPlaylistItem(item)); } catch (Exception ex) { // Temporary code to attempt to figure out long-term failing tests. - bool success = true; - int indexOf = -1234; + StringBuilder exceptionText = new StringBuilder(); - try - { - indexOf = APIRoom!.Playlist.IndexOf(APIRoom.Playlist.Single(existing => existing.ID == item.ID)); - Room.Playlist[indexOf] = item; - } - catch - { - success = false; - } + exceptionText.AppendLine("MultiplayerClient test failure investigation"); + exceptionText.AppendLine($"Exception : {exceptionText}"); + exceptionText.AppendLine($"Lookup : {item.ID}"); + exceptionText.AppendLine($"Items in Room.Playlist : {string.Join(',', Room.Playlist.Select(i => i.ID))}"); + exceptionText.AppendLine($"Items in APIRoom.Playlist: {string.Join(',', APIRoom!.Playlist.Select(i => i.ID))}"); - throw new AggregateException($"Index: {indexOf} Length: {Room.Playlist.Count} Retry success: {success} Item: {JsonConvert.SerializeObject(createPlaylistItem(item))}\n\nRoom:{JsonConvert.SerializeObject(APIRoom)}", ex); + throw new AggregateException(exceptionText.ToString()); } ItemChanged?.Invoke(item); From 4b279ecaa8b1209a65f3a44667aac9cbb9dfdb93 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 15 Aug 2024 16:44:15 +0900 Subject: [PATCH 1952/2493] Fix mistake --- osu.Game/Online/Multiplayer/MultiplayerClient.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index 07e779c2ba..4aa0d92098 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -787,7 +787,7 @@ namespace osu.Game.Online.Multiplayer StringBuilder exceptionText = new StringBuilder(); exceptionText.AppendLine("MultiplayerClient test failure investigation"); - exceptionText.AppendLine($"Exception : {exceptionText}"); + exceptionText.AppendLine($"Exception : {ex.ToString()}"); exceptionText.AppendLine($"Lookup : {item.ID}"); exceptionText.AppendLine($"Items in Room.Playlist : {string.Join(',', Room.Playlist.Select(i => i.ID))}"); exceptionText.AppendLine($"Items in APIRoom.Playlist: {string.Join(',', APIRoom!.Playlist.Select(i => i.ID))}"); From 49c71f78631879a30177ef59797e379d2d9ad251 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 15 Aug 2024 16:16:52 +0900 Subject: [PATCH 1953/2493] Fix beatmap skin always overriding ruleset HUD components --- .../Skinning/Legacy/CatchLegacySkinTransformer.cs | 4 ++++ .../Skinning/Legacy/OsuLegacySkinTransformer.cs | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs b/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs index 44fc3ecc07..394fc5080d 100644 --- a/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs @@ -43,6 +43,10 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy if (base.GetDrawableComponent(lookup) is UserConfiguredLayoutContainer d) return d; + // we don't have enough assets to display these components (this is especially the case on a "beatmap" skin). + if (!IsProvidingLegacyResources) + return null; + // Our own ruleset components default. // todo: remove CatchSkinComponents.CatchComboCounter and refactor LegacyCatchComboCounter to be added here instead. return new DefaultSkinComponentsContainer(container => diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs index 9a8eaa7d7d..a0265dd6ee 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs @@ -53,6 +53,10 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy if (base.GetDrawableComponent(lookup) is UserConfiguredLayoutContainer d) return d; + // we don't have enough assets to display these components (this is especially the case on a "beatmap" skin). + if (!IsProvidingLegacyResources) + return null; + // Our own ruleset components default. switch (containerLookup.Target) { From a421231aad9894cb29d07646f465443ac607496c Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 15 Aug 2024 16:55:03 +0900 Subject: [PATCH 1954/2493] Fix beatmap skin on mania breaking HUD apart --- .../Skinning/Legacy/ManiaLegacySkinTransformer.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs index 6ac6f6ed18..1e06eb4817 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs @@ -92,6 +92,10 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy if (base.GetDrawableComponent(lookup) is UserConfiguredLayoutContainer d) return d; + // we don't have enough assets to display these components (this is especially the case on a "beatmap" skin). + if (!IsProvidingLegacyResources) + return null; + return new DefaultSkinComponentsContainer(container => { var combo = container.ChildrenOfType().FirstOrDefault(); From 358572ebb32b83c72129d88aa00046b4f82c60bc Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 15 Aug 2024 16:57:29 +0900 Subject: [PATCH 1955/2493] Update code order to match everything else --- .../Argon/ManiaArgonSkinTransformer.cs | 35 ++++++++++--------- .../Legacy/ManiaLegacySkinTransformer.cs | 33 +++++++++-------- 2 files changed, 37 insertions(+), 31 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Skinning/Argon/ManiaArgonSkinTransformer.cs b/osu.Game.Rulesets.Mania/Skinning/Argon/ManiaArgonSkinTransformer.cs index 224db77f59..dbd690f890 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Argon/ManiaArgonSkinTransformer.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Argon/ManiaArgonSkinTransformer.cs @@ -29,9 +29,6 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon switch (lookup) { case SkinComponentsContainerLookup containerLookup: - if (containerLookup.Target != SkinComponentsContainerLookup.TargetArea.MainHUDComponents) - return base.GetDrawableComponent(lookup); - // Only handle per ruleset defaults here. if (containerLookup.Ruleset == null) return base.GetDrawableComponent(lookup); @@ -40,21 +37,27 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon if (base.GetDrawableComponent(lookup) is UserConfiguredLayoutContainer d) return d; - return new DefaultSkinComponentsContainer(container => + switch (containerLookup.Target) { - var combo = container.ChildrenOfType().FirstOrDefault(); + case SkinComponentsContainerLookup.TargetArea.MainHUDComponents: + return new DefaultSkinComponentsContainer(container => + { + var combo = container.ChildrenOfType().FirstOrDefault(); - if (combo != null) - { - combo.ShowLabel.Value = false; - combo.Anchor = Anchor.TopCentre; - combo.Origin = Anchor.Centre; - combo.Y = 200; - } - }) - { - new ArgonManiaComboCounter(), - }; + if (combo != null) + { + combo.ShowLabel.Value = false; + combo.Anchor = Anchor.TopCentre; + combo.Origin = Anchor.Centre; + combo.Y = 200; + } + }) + { + new ArgonManiaComboCounter(), + }; + } + + return null; case GameplaySkinComponentLookup resultComponent: // This should eventually be moved to a skin setting, when supported. diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs index 1e06eb4817..c25b77610a 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs @@ -81,9 +81,6 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy switch (lookup) { case SkinComponentsContainerLookup containerLookup: - if (containerLookup.Target != SkinComponentsContainerLookup.TargetArea.MainHUDComponents) - return base.GetDrawableComponent(lookup); - // Modifications for global components. if (containerLookup.Ruleset == null) return base.GetDrawableComponent(lookup); @@ -96,20 +93,26 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy if (!IsProvidingLegacyResources) return null; - return new DefaultSkinComponentsContainer(container => + switch (containerLookup.Target) { - var combo = container.ChildrenOfType().FirstOrDefault(); + case SkinComponentsContainerLookup.TargetArea.MainHUDComponents: + return new DefaultSkinComponentsContainer(container => + { + var combo = container.ChildrenOfType().FirstOrDefault(); - if (combo != null) - { - combo.Anchor = Anchor.TopCentre; - combo.Origin = Anchor.Centre; - combo.Y = this.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.ComboPosition)?.Value ?? 0; - } - }) - { - new LegacyManiaComboCounter(), - }; + if (combo != null) + { + combo.Anchor = Anchor.TopCentre; + combo.Origin = Anchor.Centre; + combo.Y = this.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.ComboPosition)?.Value ?? 0; + } + }) + { + new LegacyManiaComboCounter(), + }; + } + + return null; case GameplaySkinComponentLookup resultComponent: return getResult(resultComponent.Component); From 74272378735ddd9f88b0df0f6c53ed7edeb38170 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 15 Aug 2024 17:03:08 +0900 Subject: [PATCH 1956/2493] Try make code look better --- .../Skinning/Argon/ArgonManiaComboCounter.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonManiaComboCounter.cs b/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonManiaComboCounter.cs index 8d51b59324..5b23cea496 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonManiaComboCounter.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonManiaComboCounter.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; @@ -43,10 +44,9 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon Anchor |= direction.Value == ScrollingDirection.Up ? Anchor.y2 : Anchor.y0; } - // since we flip the vertical anchor when changing scroll direction, - // we can use the sign of the Y value as an indicator to make the combo counter displayed correctly. - if ((Y < 0 && direction.Value == ScrollingDirection.Down) || (Y > 0 && direction.Value == ScrollingDirection.Up)) - Y = -Y; + // change the sign of the Y coordinate in line with the scrolling direction. + // i.e. if the user changes direction from down to up, the anchor is changed from top to bottom, and the Y is flipped from positive to negative here. + Y = Math.Abs(Y) * (direction.Value == ScrollingDirection.Up ? -1 : 1); } } } From b5f615882f1645d8a2cdd43cd2c8eb606068fde5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 15 Aug 2024 17:25:30 +0900 Subject: [PATCH 1957/2493] Ensure the "Change Difficulty" menu uses up-to-date difficulty names Closes https://github.com/ppy/osu/issues/29391. --- osu.Game/Screens/Edit/Editor.cs | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 71d4693ac6..167ac92874 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -1286,10 +1286,23 @@ namespace osu.Game.Screens.Edit foreach (var beatmap in rulesetBeatmaps) { bool isCurrentDifficulty = playableBeatmap.BeatmapInfo.Equals(beatmap); - difficultyItems.Add(new DifficultyMenuItem(beatmap, isCurrentDifficulty, SwitchToDifficulty)); + var difficultyMenuItem = new DifficultyMenuItem(beatmap, isCurrentDifficulty, SwitchToDifficulty); + difficultyItems.Add(difficultyMenuItem); } } + // Ensure difficulty names are updated when modified in the editor. + // Maybe we could trigger less often but this seems to work well enough. + editorBeatmap.SaveStateTriggered += () => + { + foreach (var beatmapInfo in Beatmap.Value.BeatmapSetInfo.Beatmaps) + { + var menuItem = difficultyItems.OfType().FirstOrDefault(i => i.BeatmapInfo.Equals(beatmapInfo)); + if (menuItem != null) + menuItem.Text.Value = string.IsNullOrEmpty(beatmapInfo.DifficultyName) ? "(unnamed)" : beatmapInfo.DifficultyName; + } + }; + return new EditorMenuItem(EditorStrings.ChangeDifficulty) { Items = difficultyItems }; } From c3600467bf38eb281d41cc35f3ba469c9251a38f Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Thu, 15 Aug 2024 11:49:15 -0700 Subject: [PATCH 1958/2493] Make collection button test less broken --- osu.Game/Screens/Ranking/CollectionPopover.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game/Screens/Ranking/CollectionPopover.cs b/osu.Game/Screens/Ranking/CollectionPopover.cs index e285c80056..6617ac334f 100644 --- a/osu.Game/Screens/Ranking/CollectionPopover.cs +++ b/osu.Game/Screens/Ranking/CollectionPopover.cs @@ -58,8 +58,7 @@ namespace osu.Game.Screens.Ranking .AsEnumerable() .Select(c => new CollectionToggleMenuItem(c.ToLive(realm), beatmapInfo)).Cast().ToList(); - if (manageCollectionsDialog != null) - collectionItems.Add(new OsuMenuItem("Manage...", MenuItemType.Standard, manageCollectionsDialog.Show)); + collectionItems.Add(new OsuMenuItem("Manage...", MenuItemType.Standard, () => manageCollectionsDialog?.Show())); return collectionItems.ToArray(); } From f717938a288974cef9f4fb4c94c18024158c7549 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Thu, 15 Aug 2024 22:49:05 +0200 Subject: [PATCH 1959/2493] Fix grid snap slider placement double-click does not make new segment if anchor not hovered --- .../Blueprints/Sliders/SliderPlacementBlueprint.cs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs index 91cd270af6..013f790f65 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs @@ -359,8 +359,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders } // Update the cursor position. - var result = positionSnapProvider?.FindSnappedPositionAndTime(inputManager.CurrentState.Mouse.Position, state == SliderPlacementState.ControlPoints ? SnapType.GlobalGrids : SnapType.All); - cursor.Position = ToLocalSpace(result?.ScreenSpacePosition ?? inputManager.CurrentState.Mouse.Position) - HitObject.Position; + cursor.Position = getCursorPosition(); } else if (cursor != null) { @@ -374,6 +373,12 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders } } + private Vector2 getCursorPosition() + { + var result = positionSnapProvider?.FindSnappedPositionAndTime(inputManager.CurrentState.Mouse.Position, state == SliderPlacementState.ControlPoints ? SnapType.GlobalGrids : SnapType.All); + return ToLocalSpace(result?.ScreenSpacePosition ?? inputManager.CurrentState.Mouse.Position) - HitObject.Position; + } + /// /// Whether a new control point can be placed at the current mouse position. /// @@ -386,7 +391,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders var lastPiece = controlPointVisualiser.Pieces.Single(p => p.ControlPoint == last); lastPoint = last; - return lastPiece.IsHovered != true; + // We may only place a new control point if the cursor is not overlapping with the last control point. + // If snapping is enabled, the cursor may not hover the last piece while still placing the control point at the same position. + return !lastPiece.IsHovered && (last is null || Vector2.DistanceSquared(last.Position, getCursorPosition()) > 1f); } private void placeNewControlPoint() From 29fda745a42a12b9e53f7e6b417c9219e0701d58 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Thu, 15 Aug 2024 22:59:26 +0200 Subject: [PATCH 1960/2493] add failure test --- .../Editor/TestSceneSliderPlacementBlueprint.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs index bc1e4f9864..aa6a6f08d8 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs @@ -299,6 +299,14 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor }); assertControlPointTypeDuringPlacement(0, PathType.BSpline(4)); + AddStep("press alt-2", () => + { + InputManager.PressKey(Key.AltLeft); + InputManager.Key(Key.Number2); + InputManager.ReleaseKey(Key.AltLeft); + }); + assertControlPointTypeDuringPlacement(0, PathType.BEZIER); + AddStep("start new segment via S", () => InputManager.Key(Key.S)); assertControlPointTypeDuringPlacement(2, PathType.LINEAR); @@ -309,7 +317,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor addClickStep(MouseButton.Right); assertPlaced(true); - assertFinalControlPointType(0, PathType.BSpline(4)); + assertFinalControlPointType(0, PathType.BEZIER); assertFinalControlPointType(2, PathType.PERFECT_CURVE); } From 00e210147a457849a799296493e965efb029ad38 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Thu, 15 Aug 2024 23:11:07 +0200 Subject: [PATCH 1961/2493] Fix inputs being eaten by PathControlPointVisualizer when no control points are selected --- .../Sliders/Components/PathControlPointVisualiser.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs index 6251d17d85..df369dcef5 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs @@ -309,8 +309,13 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components if (!e.AltPressed) return false; + // If no pieces are selected, we can't change the path type. + if (Pieces.All(p => !p.IsSelected.Value)) + return false; + var type = path_types[e.Key - Key.Number1]; + // The first control point can never be inherit type if (Pieces[0].IsSelected.Value && type == null) return false; From ac064e814f4c6f8d465afcc41237211141b1193f Mon Sep 17 00:00:00 2001 From: OliBomby Date: Fri, 16 Aug 2024 00:15:40 +0200 Subject: [PATCH 1962/2493] Add BinarySearchUtils --- .../ControlPoints/ControlPointInfo.cs | 28 +----- osu.Game/Utils/BinarySearchUtils.cs | 99 +++++++++++++++++++ 2 files changed, 103 insertions(+), 24 deletions(-) create mode 100644 osu.Game/Utils/BinarySearchUtils.cs diff --git a/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs b/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs index f8e72a1e34..4fc77084d6 100644 --- a/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs +++ b/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs @@ -230,32 +230,12 @@ namespace osu.Game.Beatmaps.ControlPoints { ArgumentNullException.ThrowIfNull(list); - if (list.Count == 0) - return null; + int index = BinarySearchUtils.BinarySearch(list, time, c => c.Time, EqualitySelection.Rightmost); - if (time < list[0].Time) - return null; + if (index < 0) + index = ~index - 1; - if (time >= list[^1].Time) - return list[^1]; - - int l = 0; - int r = list.Count - 2; - - while (l <= r) - { - int pivot = l + ((r - l) >> 1); - - if (list[pivot].Time < time) - l = pivot + 1; - else if (list[pivot].Time > time) - r = pivot - 1; - else - return list[pivot]; - } - - // l will be the first control point with Time > time, but we want the one before it - return list[l - 1]; + return index >= 0 ? list[index] : null; } /// diff --git a/osu.Game/Utils/BinarySearchUtils.cs b/osu.Game/Utils/BinarySearchUtils.cs new file mode 100644 index 0000000000..de5fc101d5 --- /dev/null +++ b/osu.Game/Utils/BinarySearchUtils.cs @@ -0,0 +1,99 @@ +// 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; + +namespace osu.Game.Utils +{ + public class BinarySearchUtils + { + /// + /// Finds the index of the item in the sorted list which has its property equal to the search term. + /// If no exact match is found, the complement of the index of the first item greater than the search term will be returned. + /// + /// The type of the items in the list to search. + /// The type of the property to perform the search on. + /// The list of items to search. + /// The query to find. + /// Function that maps an item in the list to its index property. + /// Determines which index to return if there are multiple exact matches. + /// The index of the found item. Will return the complement of the index of the first item greater than the search query if no exact match is found. + public static int BinarySearch(IReadOnlyList list, T2 searchTerm, Func termFunc, EqualitySelection equalitySelection = EqualitySelection.FirstFound) + { + int n = list.Count; + + if (n == 0) + return -1; + + var comparer = Comparer.Default; + + if (comparer.Compare(searchTerm, termFunc(list[0])) == -1) + return -1; + + if (comparer.Compare(searchTerm, termFunc(list[^1])) == 1) + return ~n; + + int min = 0; + int max = n - 1; + bool equalityFound = false; + + while (min <= max) + { + int mid = min + (max - min) / 2; + T2 midTerm = termFunc(list[mid]); + + switch (comparer.Compare(midTerm, searchTerm)) + { + case 0: + equalityFound = true; + + switch (equalitySelection) + { + case EqualitySelection.Leftmost: + max = mid - 1; + break; + + case EqualitySelection.Rightmost: + min = mid + 1; + break; + + default: + case EqualitySelection.FirstFound: + return mid; + } + + break; + + case 1: + max = mid - 1; + break; + + case -1: + min = mid + 1; + break; + } + } + + if (!equalityFound) return ~min; + + switch (equalitySelection) + { + case EqualitySelection.Leftmost: + return min; + + case EqualitySelection.Rightmost: + return min - 1; + } + + return ~min; + } + } + + public enum EqualitySelection + { + FirstFound, + Leftmost, + Rightmost + } +} From 2e11172e8e7f2cd0b2d2686920259c89edcb78b6 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Fri, 16 Aug 2024 01:01:24 +0200 Subject: [PATCH 1963/2493] Take into account next timing point when snapping time --- .../ControlPoints/ControlPointInfo.cs | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs b/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs index 4fc77084d6..026d44faa1 100644 --- a/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs +++ b/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs @@ -74,6 +74,19 @@ namespace osu.Game.Beatmaps.ControlPoints [NotNull] public TimingControlPoint TimingPointAt(double time) => BinarySearchWithFallback(TimingPoints, time, TimingPoints.Count > 0 ? TimingPoints[0] : TimingControlPoint.DEFAULT); + /// + /// Finds the first timing point that is active strictly after , or null if no such point exists. + /// + /// The time after which to find the timing control point. + /// The timing control point. + [CanBeNull] + public TimingControlPoint TimingPointAfter(double time) + { + int index = BinarySearchUtils.BinarySearch(TimingPoints, time, c => c.Time, EqualitySelection.Rightmost); + index = index < 0 ? ~index : index + 1; + return index < TimingPoints.Count ? TimingPoints[index] : null; + } + /// /// Finds the maximum BPM represented by any timing control point. /// @@ -156,7 +169,14 @@ namespace osu.Game.Beatmaps.ControlPoints public double GetClosestSnappedTime(double time, int beatDivisor, double? referenceTime = null) { var timingPoint = TimingPointAt(referenceTime ?? time); - return getClosestSnappedTime(timingPoint, time, beatDivisor); + double snappedTime = getClosestSnappedTime(timingPoint, time, beatDivisor); + + if (referenceTime.HasValue) + return snappedTime; + + // If there is a timing point right after the given time, we should check if it is closer than the snapped time and snap to it. + var timingPointAfter = TimingPointAfter(time); + return timingPointAfter is null || Math.Abs(time - snappedTime) < Math.Abs(time - timingPointAfter.Time) ? snappedTime : timingPointAfter.Time; } /// From 3a84409546386f552c2f4d31773dfef0eb400b51 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Fri, 16 Aug 2024 01:36:51 +0200 Subject: [PATCH 1964/2493] Use TimingPointAfter for seeking check --- osu.Game/Screens/Edit/EditorClock.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/EditorClock.cs b/osu.Game/Screens/Edit/EditorClock.cs index 773abaa737..5b9c662c95 100644 --- a/osu.Game/Screens/Edit/EditorClock.cs +++ b/osu.Game/Screens/Edit/EditorClock.cs @@ -132,7 +132,7 @@ namespace osu.Game.Screens.Edit seekTime = timingPoint.Time + closestBeat * seekAmount; // limit forward seeking to only up to the next timing point's start time. - var nextTimingPoint = ControlPointInfo.TimingPoints.FirstOrDefault(t => t.Time > timingPoint.Time); + var nextTimingPoint = ControlPointInfo.TimingPointAfter(timingPoint.Time); if (seekTime > nextTimingPoint?.Time) seekTime = nextTimingPoint.Time; From 3565a10ea2c00d7a617be229faf723156a715f1c Mon Sep 17 00:00:00 2001 From: OliBomby Date: Fri, 16 Aug 2024 01:45:28 +0200 Subject: [PATCH 1965/2493] fix confusing return statement at the end --- osu.Game/Utils/BinarySearchUtils.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game/Utils/BinarySearchUtils.cs b/osu.Game/Utils/BinarySearchUtils.cs index de5fc101d5..08ce4e363d 100644 --- a/osu.Game/Utils/BinarySearchUtils.cs +++ b/osu.Game/Utils/BinarySearchUtils.cs @@ -82,11 +82,10 @@ namespace osu.Game.Utils case EqualitySelection.Leftmost: return min; + default: case EqualitySelection.Rightmost: return min - 1; } - - return ~min; } } From fda17a5a72f327139cf982ebb68fbb25add6b5b4 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Tue, 13 Aug 2024 23:52:52 -0700 Subject: [PATCH 1966/2493] Expose `BeatmapCardNormal` height const --- .../Beatmaps/Drawables/Cards/BeatmapCardNormal.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardNormal.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardNormal.cs index c6ba4f234a..46ab7ec5f6 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardNormal.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardNormal.cs @@ -23,7 +23,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards protected override Drawable IdleContent => idleBottomContent; protected override Drawable DownloadInProgressContent => downloadProgressBar; - private const float height = 100; + public const float HEIGHT = 100; [Cached] private readonly BeatmapCardContent content; @@ -42,14 +42,14 @@ namespace osu.Game.Beatmaps.Drawables.Cards public BeatmapCardNormal(APIBeatmapSet beatmapSet, bool allowExpansion = true) : base(beatmapSet, allowExpansion) { - content = new BeatmapCardContent(height); + content = new BeatmapCardContent(HEIGHT); } [BackgroundDependencyLoader] private void load() { Width = WIDTH; - Height = height; + Height = HEIGHT; FillFlowContainer leftIconArea = null!; FillFlowContainer titleBadgeArea = null!; @@ -65,7 +65,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards thumbnail = new BeatmapCardThumbnail(BeatmapSet, BeatmapSet) { Name = @"Left (icon) area", - Size = new Vector2(height), + Size = new Vector2(HEIGHT), Padding = new MarginPadding { Right = CORNER_RADIUS }, Child = leftIconArea = new FillFlowContainer { @@ -77,8 +77,8 @@ namespace osu.Game.Beatmaps.Drawables.Cards }, buttonContainer = new CollapsibleButtonContainer(BeatmapSet) { - X = height - CORNER_RADIUS, - Width = WIDTH - height + CORNER_RADIUS, + X = HEIGHT - CORNER_RADIUS, + Width = WIDTH - HEIGHT + CORNER_RADIUS, FavouriteState = { BindTarget = FavouriteState }, ButtonsCollapsedWidth = CORNER_RADIUS, ButtonsExpandedWidth = 30, From e2bf02cf948edc82d7cbe1049b0fe9638fc656bc Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Sat, 10 Aug 2024 00:49:51 -0700 Subject: [PATCH 1967/2493] Fix preview play button having incorrect click area --- osu.Game/Beatmaps/Drawables/Cards/BeatmapCardThumbnail.cs | 5 ++--- osu.Game/Beatmaps/Drawables/Cards/Buttons/PlayButton.cs | 2 ++ 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardThumbnail.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardThumbnail.cs index 976f797760..1f6f638618 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardThumbnail.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardThumbnail.cs @@ -90,10 +90,9 @@ namespace osu.Game.Beatmaps.Drawables.Cards protected override void Update() { base.Update(); - progress.Progress = playButton.Progress.Value; - playButton.Scale = new Vector2(DrawWidth / 100); - progress.Size = new Vector2(50 * DrawWidth / 100); + progress.Progress = playButton.Progress.Value; + progress.Size = new Vector2(50 * playButton.DrawWidth / (BeatmapCardNormal.HEIGHT - BeatmapCard.CORNER_RADIUS)); } private void updateState() diff --git a/osu.Game/Beatmaps/Drawables/Cards/Buttons/PlayButton.cs b/osu.Game/Beatmaps/Drawables/Cards/Buttons/PlayButton.cs index f808fd21b7..f6caf4815d 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/Buttons/PlayButton.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/Buttons/PlayButton.cs @@ -79,6 +79,8 @@ namespace osu.Game.Beatmaps.Drawables.Cards.Buttons { base.Update(); + icon.Scale = new Vector2(DrawWidth / (BeatmapCardNormal.HEIGHT - BeatmapCard.CORNER_RADIUS)); + if (Playing.Value && previewTrack != null && previewTrack.TrackLoaded) progress.Value = previewTrack.CurrentTime / previewTrack.Length; else From 68bad9a277b33b9ae35c2f4ea1fd7d3a128832b5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 16 Aug 2024 17:39:45 +0900 Subject: [PATCH 1968/2493] Attempt file operations more than once in another test instance See https://github.com/ppy/osu/pull/29433/checks?check_run_id=28833985792. --- .../Visual/Navigation/TestSceneScreenNavigation.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs index 2b23581984..db9ecd90b9 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs @@ -276,8 +276,11 @@ namespace osu.Game.Tests.Visual.Navigation AddStep("delete beatmap files", () => { - foreach (var file in Game.Beatmap.Value.BeatmapSetInfo.Files.Where(f => Path.GetExtension(f.Filename) == ".osu")) - Game.Storage.Delete(Path.Combine("files", file.File.GetStoragePath())); + FileUtils.AttemptOperation(() => + { + foreach (var file in Game.Beatmap.Value.BeatmapSetInfo.Files.Where(f => Path.GetExtension(f.Filename) == ".osu")) + Game.Storage.Delete(Path.Combine("files", file.File.GetStoragePath())); + }); }); AddStep("invalidate cache", () => From e0da4763462a5743ca0ba77f57e0c4b79b81aa47 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 16 Aug 2024 18:12:46 +0900 Subject: [PATCH 1969/2493] Add tests for util function --- osu.Game.Tests/Utils/BinarySearchUtilsTest.cs | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 osu.Game.Tests/Utils/BinarySearchUtilsTest.cs diff --git a/osu.Game.Tests/Utils/BinarySearchUtilsTest.cs b/osu.Game.Tests/Utils/BinarySearchUtilsTest.cs new file mode 100644 index 0000000000..bc125ec76c --- /dev/null +++ b/osu.Game.Tests/Utils/BinarySearchUtilsTest.cs @@ -0,0 +1,63 @@ +// 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 NUnit.Framework; +using osu.Game.Utils; + +namespace osu.Game.Tests.Utils +{ + [TestFixture] + public class BinarySearchUtilsTest + { + [Test] + public void TestEmptyList() + { + Assert.That(BinarySearchUtils.BinarySearch(Array.Empty(), 0, x => x), Is.EqualTo(-1)); + Assert.That(BinarySearchUtils.BinarySearch(Array.Empty(), 0, x => x, EqualitySelection.Leftmost), Is.EqualTo(-1)); + Assert.That(BinarySearchUtils.BinarySearch(Array.Empty(), 0, x => x, EqualitySelection.Rightmost), Is.EqualTo(-1)); + } + + [TestCase(new[] { 1 }, 0, -1)] + [TestCase(new[] { 1 }, 1, 0)] + [TestCase(new[] { 1 }, 2, -2)] + [TestCase(new[] { 1, 3 }, 0, -1)] + [TestCase(new[] { 1, 3 }, 1, 0)] + [TestCase(new[] { 1, 3 }, 2, -2)] + [TestCase(new[] { 1, 3 }, 3, 1)] + [TestCase(new[] { 1, 3 }, 4, -3)] + public void TestUniqueScenarios(int[] values, int search, int expectedIndex) + { + Assert.That(BinarySearchUtils.BinarySearch(values, search, x => x, EqualitySelection.FirstFound), Is.EqualTo(expectedIndex)); + Assert.That(BinarySearchUtils.BinarySearch(values, search, x => x, EqualitySelection.Leftmost), Is.EqualTo(expectedIndex)); + Assert.That(BinarySearchUtils.BinarySearch(values, search, x => x, EqualitySelection.Rightmost), Is.EqualTo(expectedIndex)); + } + + [TestCase(new[] { 1, 2, 2 }, 2, 1)] + [TestCase(new[] { 1, 2, 2, 2 }, 2, 1)] + [TestCase(new[] { 1, 2, 2, 2, 3 }, 2, 2)] + [TestCase(new[] { 1, 2, 2, 3 }, 2, 1)] + public void TestFirstFoundDuplicateScenarios(int[] values, int search, int expectedIndex) + { + Assert.That(BinarySearchUtils.BinarySearch(values, search, x => x), Is.EqualTo(expectedIndex)); + } + + [TestCase(new[] { 1, 2, 2 }, 2, 1)] + [TestCase(new[] { 1, 2, 2, 2 }, 2, 1)] + [TestCase(new[] { 1, 2, 2, 2, 3 }, 2, 1)] + [TestCase(new[] { 1, 2, 2, 3 }, 2, 1)] + public void TestLeftMostDuplicateScenarios(int[] values, int search, int expectedIndex) + { + Assert.That(BinarySearchUtils.BinarySearch(values, search, x => x, EqualitySelection.Leftmost), Is.EqualTo(expectedIndex)); + } + + [TestCase(new[] { 1, 2, 2 }, 2, 2)] + [TestCase(new[] { 1, 2, 2, 2 }, 2, 3)] + [TestCase(new[] { 1, 2, 2, 2, 3 }, 2, 3)] + [TestCase(new[] { 1, 2, 2, 3 }, 2, 2)] + public void TestRightMostDuplicateScenarios(int[] values, int search, int expectedIndex) + { + Assert.That(BinarySearchUtils.BinarySearch(values, search, x => x, EqualitySelection.Rightmost), Is.EqualTo(expectedIndex)); + } + } +} From 7a47597234a0d45f80af6e80b0a2fd23afb8f00c Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 16 Aug 2024 18:21:06 +0900 Subject: [PATCH 1970/2493] Add one more case --- osu.Game.Tests/Utils/BinarySearchUtilsTest.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game.Tests/Utils/BinarySearchUtilsTest.cs b/osu.Game.Tests/Utils/BinarySearchUtilsTest.cs index bc125ec76c..cbf6cdf32a 100644 --- a/osu.Game.Tests/Utils/BinarySearchUtilsTest.cs +++ b/osu.Game.Tests/Utils/BinarySearchUtilsTest.cs @@ -33,6 +33,7 @@ namespace osu.Game.Tests.Utils Assert.That(BinarySearchUtils.BinarySearch(values, search, x => x, EqualitySelection.Rightmost), Is.EqualTo(expectedIndex)); } + [TestCase(new[] { 1, 1 }, 1, 0)] [TestCase(new[] { 1, 2, 2 }, 2, 1)] [TestCase(new[] { 1, 2, 2, 2 }, 2, 1)] [TestCase(new[] { 1, 2, 2, 2, 3 }, 2, 2)] @@ -42,6 +43,7 @@ namespace osu.Game.Tests.Utils Assert.That(BinarySearchUtils.BinarySearch(values, search, x => x), Is.EqualTo(expectedIndex)); } + [TestCase(new[] { 1, 1 }, 1, 0)] [TestCase(new[] { 1, 2, 2 }, 2, 1)] [TestCase(new[] { 1, 2, 2, 2 }, 2, 1)] [TestCase(new[] { 1, 2, 2, 2, 3 }, 2, 1)] @@ -51,6 +53,7 @@ namespace osu.Game.Tests.Utils Assert.That(BinarySearchUtils.BinarySearch(values, search, x => x, EqualitySelection.Leftmost), Is.EqualTo(expectedIndex)); } + [TestCase(new[] { 1, 1 }, 1, 1)] [TestCase(new[] { 1, 2, 2 }, 2, 2)] [TestCase(new[] { 1, 2, 2, 2 }, 2, 3)] [TestCase(new[] { 1, 2, 2, 2, 3 }, 2, 3)] From e5fab9cfbe2cf20225dbf9cfe94f5a23d8cff711 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Fri, 16 Aug 2024 11:55:07 +0200 Subject: [PATCH 1971/2493] Remove select action to end placement --- osu.Game/Rulesets/Edit/PlacementBlueprint.cs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/osu.Game/Rulesets/Edit/PlacementBlueprint.cs b/osu.Game/Rulesets/Edit/PlacementBlueprint.cs index 60b979da59..a50a7f4169 100644 --- a/osu.Game/Rulesets/Edit/PlacementBlueprint.cs +++ b/osu.Game/Rulesets/Edit/PlacementBlueprint.cs @@ -125,10 +125,6 @@ namespace osu.Game.Rulesets.Edit switch (e.Action) { - case GlobalAction.Select: - EndPlacement(true); - return true; - case GlobalAction.Back: EndPlacement(false); return true; From 5624c1d304a8cf40428d88e4e36b5262a1274604 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Fri, 16 Aug 2024 13:22:09 +0200 Subject: [PATCH 1972/2493] Make break periods in bottom timeline transparent --- .../Edit/Components/Timelines/Summary/Parts/BreakPart.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/BreakPart.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/BreakPart.cs index 17e0d47676..3cff976f72 100644 --- a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/BreakPart.cs +++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/BreakPart.cs @@ -70,7 +70,8 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts RelativeSizeAxes = Axes.Both; InternalChild = new Circle { RelativeSizeAxes = Axes.Both }; - Colour = colours.Gray6; + Colour = colours.Gray7; + Alpha = 0.8f; } public LocalisableString TooltipText => $"{breakPeriod.StartTime.ToEditorFormattedString()} - {breakPeriod.EndTime.ToEditorFormattedString()} break time"; From 4cc38cea63a97611d6f5fbc902700c2e374d4dae Mon Sep 17 00:00:00 2001 From: OliBomby Date: Fri, 16 Aug 2024 14:24:03 +0200 Subject: [PATCH 1973/2493] fix last anchor converting to implicit segment --- osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs index 8a8964ccd4..b0173b3ae3 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs @@ -450,7 +450,7 @@ namespace osu.Game.Beatmaps.Formats // Explicit segments have a new format in which the type is injected into the middle of the control point string. // To preserve compatibility with osu-stable as much as possible, explicit segments with the same type are converted to use implicit segments by duplicating the control point. // One exception are consecutive perfect curves, which aren't supported in osu!stable and can lead to decoding issues if encoded as implicit segments - bool needsExplicitSegment = point.Type != lastType || point.Type == PathType.PERFECT_CURVE; + bool needsExplicitSegment = point.Type != lastType || point.Type == PathType.PERFECT_CURVE || i == pathData.Path.ControlPoints.Count - 1; // Another exception to this is when the last two control points of the last segment were duplicated. This is not a scenario supported by osu!stable. // Lazer does not add implicit segments for the last two control points of _any_ explicit segment, so an explicit segment is forced in order to maintain consistency with the decoder. From a2e26ba9ffb93308e73263a7243833d429b873cb Mon Sep 17 00:00:00 2001 From: OliBomby Date: Fri, 16 Aug 2024 14:24:55 +0200 Subject: [PATCH 1974/2493] Fix perfect curve anchors losing type between reloads --- osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs index 8e6ffa20cc..d4e3053856 100644 --- a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs @@ -347,7 +347,7 @@ namespace osu.Game.Rulesets.Objects.Legacy vertices[i] = new PathControlPoint { Position = points[i] }; // Edge-case rules (to match stable). - if (type == PathType.PERFECT_CURVE) + if (type == PathType.PERFECT_CURVE && FormatVersion < LegacyBeatmapEncoder.FIRST_LAZER_VERSION) { int endPointLength = endPoint == null ? 0 : 1; From b253d8ecbf3c6399e1fd84eb8738d03608db6ba2 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Fri, 16 Aug 2024 14:43:09 +0200 Subject: [PATCH 1975/2493] Hide scroll speed in bottom timeline --- .../Timelines/Summary/Parts/EffectPointVisualisation.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/EffectPointVisualisation.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/EffectPointVisualisation.cs index 17fedb933a..e3f90558c5 100644 --- a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/EffectPointVisualisation.cs +++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/EffectPointVisualisation.cs @@ -9,8 +9,10 @@ using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Shapes; using osu.Framework.Localisation; using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Configuration; using osu.Game.Extensions; using osu.Game.Graphics; +using osu.Game.Rulesets.UI.Scrolling; namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts { @@ -79,7 +81,10 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts { ClearInternal(); - AddInternal(new ControlPointVisualisation(effect)); + var drawableRuleset = beatmap.BeatmapInfo.Ruleset.CreateInstance().CreateDrawableRulesetWith(beatmap.PlayableBeatmap); + + if (drawableRuleset is IDrawableScrollingRuleset scrollingRuleset && scrollingRuleset.VisualisationMethod != ScrollVisualisationMethod.Constant) + AddInternal(new ControlPointVisualisation(effect)); if (!kiai.Value) return; From 621c4d65a3d721ebb0547de891c2048d92f32c27 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Fri, 16 Aug 2024 14:43:33 +0200 Subject: [PATCH 1976/2493] Hide scroll speed in effect row attribute --- .../Timing/RowAttributes/EffectRowAttribute.cs | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Timing/RowAttributes/EffectRowAttribute.cs b/osu.Game/Screens/Edit/Timing/RowAttributes/EffectRowAttribute.cs index ad22aa81fc..253bfdd73a 100644 --- a/osu.Game/Screens/Edit/Timing/RowAttributes/EffectRowAttribute.cs +++ b/osu.Game/Screens/Edit/Timing/RowAttributes/EffectRowAttribute.cs @@ -5,6 +5,8 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Configuration; +using osu.Game.Rulesets.UI.Scrolling; namespace osu.Game.Screens.Edit.Timing.RowAttributes { @@ -15,6 +17,10 @@ namespace osu.Game.Screens.Edit.Timing.RowAttributes private AttributeText kiaiModeBubble = null!; private AttributeText text = null!; + private AttributeProgressBar progressBar = null!; + + [Resolved] + protected EditorBeatmap Beatmap { get; private set; } = null!; public EffectRowAttribute(EffectControlPoint effect) : base(effect, "effect") @@ -28,7 +34,7 @@ namespace osu.Game.Screens.Edit.Timing.RowAttributes { Content.AddRange(new Drawable[] { - new AttributeProgressBar(Point) + progressBar = new AttributeProgressBar(Point) { Current = scrollSpeed, }, @@ -36,6 +42,14 @@ namespace osu.Game.Screens.Edit.Timing.RowAttributes kiaiModeBubble = new AttributeText(Point) { Text = "kiai" }, }); + var drawableRuleset = Beatmap.BeatmapInfo.Ruleset.CreateInstance().CreateDrawableRulesetWith(Beatmap.PlayableBeatmap); + + if (drawableRuleset is not IDrawableScrollingRuleset scrollingRuleset || scrollingRuleset.VisualisationMethod == ScrollVisualisationMethod.Constant) + { + text.Hide(); + progressBar.Hide(); + } + kiaiMode.BindValueChanged(enabled => kiaiModeBubble.FadeTo(enabled.NewValue ? 1 : 0), true); scrollSpeed.BindValueChanged(_ => updateText(), true); } From 3d4bc8a2cc5da3c8985e9d0ef7330ee21e49f311 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Fri, 16 Aug 2024 15:04:38 +0200 Subject: [PATCH 1977/2493] fix tests --- .../Editing/TestScenePlacementBlueprint.cs | 32 +------------------ 1 file changed, 1 insertion(+), 31 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestScenePlacementBlueprint.cs b/osu.Game.Tests/Visual/Editing/TestScenePlacementBlueprint.cs index 8173536ba4..fe74e1b346 100644 --- a/osu.Game.Tests/Visual/Editing/TestScenePlacementBlueprint.cs +++ b/osu.Game.Tests/Visual/Editing/TestScenePlacementBlueprint.cs @@ -96,32 +96,6 @@ namespace osu.Game.Tests.Visual.Editing AddAssert("slider placed", () => EditorBeatmap.HitObjects.Count, () => Is.EqualTo(1)); } - [Test] - public void TestCommitPlacementViaGlobalAction() - { - Playfield playfield = null!; - - AddStep("select slider placement tool", () => InputManager.Key(Key.Number3)); - AddStep("move mouse to top left of playfield", () => - { - playfield = this.ChildrenOfType().Single(); - var location = (3 * playfield.ScreenSpaceDrawQuad.TopLeft + playfield.ScreenSpaceDrawQuad.BottomRight) / 4; - InputManager.MoveMouseTo(location); - }); - AddStep("begin placement", () => InputManager.Click(MouseButton.Left)); - AddStep("move mouse to bottom right of playfield", () => - { - var location = (playfield.ScreenSpaceDrawQuad.TopLeft + 3 * playfield.ScreenSpaceDrawQuad.BottomRight) / 4; - InputManager.MoveMouseTo(location); - }); - AddStep("confirm via global action", () => - { - globalActionContainer.TriggerPressed(GlobalAction.Select); - globalActionContainer.TriggerReleased(GlobalAction.Select); - }); - AddAssert("slider placed", () => EditorBeatmap.HitObjects.Count, () => Is.EqualTo(1)); - } - [Test] public void TestAbortPlacementViaGlobalAction() { @@ -272,11 +246,7 @@ namespace osu.Game.Tests.Visual.Editing var location = (playfield.ScreenSpaceDrawQuad.TopLeft + 3 * playfield.ScreenSpaceDrawQuad.BottomRight) / 4; InputManager.MoveMouseTo(location); }); - AddStep("confirm via global action", () => - { - globalActionContainer.TriggerPressed(GlobalAction.Select); - globalActionContainer.TriggerReleased(GlobalAction.Select); - }); + AddStep("confirm via right click", () => InputManager.Click(MouseButton.Right)); AddAssert("slider placed", () => EditorBeatmap.HitObjects.Count, () => Is.EqualTo(1)); AddAssert("slider samples have drum bank", () => EditorBeatmap.HitObjects[0].Samples.All(s => s.Bank == HitSampleInfo.BANK_DRUM)); From d1d195cf18f872b8a57f696d58c12aae1ed31fcd Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 17 Aug 2024 02:30:59 +0900 Subject: [PATCH 1978/2493] Fix incorrect skin lookup shortcutting causing sprites to no longer work --- osu.Game/Skinning/ArgonSkin.cs | 8 ++++---- osu.Game/Skinning/LegacySkin.cs | 8 ++++---- osu.Game/Skinning/TrianglesSkin.cs | 8 ++++---- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/osu.Game/Skinning/ArgonSkin.cs b/osu.Game/Skinning/ArgonSkin.cs index 85abb1edcd..c66df82e0d 100644 --- a/osu.Game/Skinning/ArgonSkin.cs +++ b/osu.Game/Skinning/ArgonSkin.cs @@ -94,13 +94,13 @@ namespace osu.Game.Skinning // Temporary until default skin has a valid hit lighting. if ((lookup as SkinnableSprite.SpriteComponentLookup)?.LookupName == @"lighting") return Drawable.Empty(); - if (base.GetDrawableComponent(lookup) is UserConfiguredLayoutContainer c) - return c; - switch (lookup) { case SkinComponentsContainerLookup containerLookup: + if (base.GetDrawableComponent(lookup) is UserConfiguredLayoutContainer c) + return c; + switch (containerLookup.Target) { case SkinComponentsContainerLookup.TargetArea.SongSelect: @@ -257,7 +257,7 @@ namespace osu.Game.Skinning return null; } - return null; + return base.GetDrawableComponent(lookup); } public override IBindable? GetConfig(TLookup lookup) diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index 8f6e634dd6..bbca0178d5 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -356,12 +356,12 @@ namespace osu.Game.Skinning public override Drawable? GetDrawableComponent(ISkinComponentLookup lookup) { - if (base.GetDrawableComponent(lookup) is UserConfiguredLayoutContainer c) - return c; - switch (lookup) { case SkinComponentsContainerLookup containerLookup: + if (base.GetDrawableComponent(lookup) is UserConfiguredLayoutContainer c) + return c; + switch (containerLookup.Target) { case SkinComponentsContainerLookup.TargetArea.MainHUDComponents: @@ -445,7 +445,7 @@ namespace osu.Game.Skinning return null; } - return null; + return base.GetDrawableComponent(lookup); } private Texture? getParticleTexture(HitResult result) diff --git a/osu.Game/Skinning/TrianglesSkin.cs b/osu.Game/Skinning/TrianglesSkin.cs index 29abb1949f..7971aee794 100644 --- a/osu.Game/Skinning/TrianglesSkin.cs +++ b/osu.Game/Skinning/TrianglesSkin.cs @@ -64,12 +64,12 @@ namespace osu.Game.Skinning // Temporary until default skin has a valid hit lighting. if ((lookup as SkinnableSprite.SpriteComponentLookup)?.LookupName == @"lighting") return Drawable.Empty(); - if (base.GetDrawableComponent(lookup) is UserConfiguredLayoutContainer c) - return c; - switch (lookup) { case SkinComponentsContainerLookup containerLookup: + if (base.GetDrawableComponent(lookup) is UserConfiguredLayoutContainer c) + return c; + // Only handle global level defaults for now. if (containerLookup.Ruleset != null) return null; @@ -178,7 +178,7 @@ namespace osu.Game.Skinning return null; } - return null; + return base.GetDrawableComponent(lookup); } public override IBindable? GetConfig(TLookup lookup) From f74263db8111d5abe4825816f938697b00bab562 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Sat, 17 Aug 2024 00:59:44 +0300 Subject: [PATCH 1979/2493] Remove extra box in OnlinePlayBackgroundScreen --- .../Components/OnlinePlayBackgroundScreen.cs | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Components/OnlinePlayBackgroundScreen.cs b/osu.Game/Screens/OnlinePlay/Components/OnlinePlayBackgroundScreen.cs index ea422f83e3..ef7c1747e9 100644 --- a/osu.Game/Screens/OnlinePlay/Components/OnlinePlayBackgroundScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Components/OnlinePlayBackgroundScreen.cs @@ -3,10 +3,8 @@ using System.Threading; using osu.Framework.Allocation; -using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; -using osu.Framework.Graphics.Shapes; using osu.Framework.Screens; using osu.Game.Beatmaps; using osu.Game.Online.Rooms; @@ -20,16 +18,6 @@ namespace osu.Game.Screens.OnlinePlay.Components private CancellationTokenSource? cancellationSource; private PlaylistItemBackground? background; - protected OnlinePlayBackgroundScreen() - { - AddInternal(new Box - { - RelativeSizeAxes = Axes.Both, - Depth = float.MinValue, - Colour = ColourInfo.GradientVertical(Color4.Black.Opacity(0.9f), Color4.Black.Opacity(0.6f)) - }); - } - [BackgroundDependencyLoader] private void load() { @@ -83,6 +71,7 @@ namespace osu.Game.Screens.OnlinePlay.Components } newBackground.Depth = newDepth; + newBackground.Colour = ColourInfo.GradientVertical(new Color4(0.1f, 0.1f, 0.1f, 1f), new Color4(0.4f, 0.4f, 0.4f, 1f)); newBackground.BlurTo(new Vector2(10)); AddInternal(background = newBackground); From 04a2d67ca4131d56d22a6cf3d6ca2c432726f01b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 17 Aug 2024 15:13:44 +0900 Subject: [PATCH 1980/2493] Fix legacy combo counter bounce animation not always playing As mentioned [in discord](https://discord.com/channels/188630481301012481/1097318920991559880/1274231995261649006). --- osu.Game/Skinning/LegacyDefaultComboCounter.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/osu.Game/Skinning/LegacyDefaultComboCounter.cs b/osu.Game/Skinning/LegacyDefaultComboCounter.cs index f633358993..6c81b1f959 100644 --- a/osu.Game/Skinning/LegacyDefaultComboCounter.cs +++ b/osu.Game/Skinning/LegacyDefaultComboCounter.cs @@ -41,9 +41,6 @@ namespace osu.Game.Skinning protected override void OnCountIncrement() { - scheduledPopOut?.Cancel(); - scheduledPopOut = null; - DisplayedCountText.Show(); PopOutCountText.Text = FormatCount(Current.Value); From 3cd5820b5b903227d80b24ae57faa8996467ceed Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Sat, 17 Aug 2024 10:34:39 +0300 Subject: [PATCH 1981/2493] Make PositionSnapGrid a BufferedContainer --- .../Compose/Components/PositionSnapGrid.cs | 28 +++++++++++-------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/PositionSnapGrid.cs b/osu.Game/Screens/Edit/Compose/Components/PositionSnapGrid.cs index e576ac1e49..cbdf02488a 100644 --- a/osu.Game/Screens/Edit/Compose/Components/PositionSnapGrid.cs +++ b/osu.Game/Screens/Edit/Compose/Components/PositionSnapGrid.cs @@ -2,15 +2,17 @@ // See the LICENCE file in the repository root for full licence text. 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.Layout; using osuTK; +using osuTK.Graphics; namespace osu.Game.Screens.Edit.Compose.Components { - public abstract partial class PositionSnapGrid : CompositeDrawable + public abstract partial class PositionSnapGrid : BufferedContainer { /// /// The position of the origin of this in local coordinates. @@ -20,7 +22,10 @@ namespace osu.Game.Screens.Edit.Compose.Components protected readonly LayoutValue GridCache = new LayoutValue(Invalidation.RequiredParentSizeToFit); protected PositionSnapGrid() + : base(cachedFrameBuffer: true) { + BackgroundColour = Color4.White.Opacity(0); + StartPosition.BindValueChanged(_ => GridCache.Invalidate()); AddLayout(GridCache); @@ -30,7 +35,8 @@ namespace osu.Game.Screens.Edit.Compose.Components { base.Update(); - if (GridCache.IsValid) return; + if (GridCache.IsValid) + return; ClearInternal(); @@ -38,6 +44,7 @@ namespace osu.Game.Screens.Edit.Compose.Components CreateContent(); GridCache.Validate(); + ForceRedraw(); } protected abstract void CreateContent(); @@ -53,7 +60,6 @@ namespace osu.Game.Screens.Edit.Compose.Components { Colour = Colour4.White, Alpha = 0.3f, - Origin = Anchor.CentreLeft, RelativeSizeAxes = Axes.X, Height = lineWidth, Y = 0, @@ -62,28 +68,26 @@ namespace osu.Game.Screens.Edit.Compose.Components { Colour = Colour4.White, Alpha = 0.3f, - Origin = Anchor.CentreLeft, + Origin = Anchor.BottomLeft, + Anchor = Anchor.BottomLeft, RelativeSizeAxes = Axes.X, - Height = lineWidth, - Y = drawSize.Y, + Height = lineWidth }, new Box { Colour = Colour4.White, Alpha = 0.3f, - Origin = Anchor.TopCentre, RelativeSizeAxes = Axes.Y, - Width = lineWidth, - X = 0, + Width = lineWidth }, new Box { Colour = Colour4.White, Alpha = 0.3f, - Origin = Anchor.TopCentre, + Origin = Anchor.TopRight, + Anchor = Anchor.TopRight, RelativeSizeAxes = Axes.Y, - Width = lineWidth, - X = drawSize.X, + Width = lineWidth }, }); } From 6dd08e9a964a978d715c6dd7fe148e7392f8aa73 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Sat, 17 Aug 2024 11:26:46 -0700 Subject: [PATCH 1982/2493] Fix beatmap carousel panels not blocking hover of other panels in song select --- osu.Game/Screens/Select/Carousel/DrawableCarouselItem.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselItem.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselItem.cs index 4c9ac57d9d..755008d370 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselItem.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselItem.cs @@ -166,6 +166,8 @@ namespace osu.Game.Screens.Select.Carousel return true; } + protected override bool OnHover(HoverEvent e) => true; + protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); From e75ae4a37bc0c427e73e975a48a7cb92b067db0c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 19 Aug 2024 04:04:24 +0900 Subject: [PATCH 1983/2493] More hardening of `TestMultiplayerClient` to attempt to fix test failures --- osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs index 4c3deac1d7..efa9dc4990 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs @@ -208,6 +208,9 @@ namespace osu.Game.Tests.Visual.Multiplayer protected override async Task JoinRoom(long roomId, string? password = null) { + if (RoomJoined || ServerAPIRoom != null) + throw new InvalidOperationException("Already joined a room"); + roomId = clone(roomId); password = clone(password); @@ -260,6 +263,8 @@ namespace osu.Game.Tests.Visual.Multiplayer protected override Task LeaveRoomInternal() { RoomJoined = false; + ServerAPIRoom = null; + ServerRoom = null; return Task.CompletedTask; } From 95d06333c1d948d6d8372bd8b708fc2b38a6817c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 19 Aug 2024 13:49:59 +0900 Subject: [PATCH 1984/2493] Fix typo in editor field --- 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 3c1d0fbb1c..484fbd5084 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs @@ -71,7 +71,7 @@ namespace osu.Game.Rulesets.Osu.Edit protected readonly OsuGridToolboxGroup OsuGridToolboxGroup = new OsuGridToolboxGroup(); [Cached] - protected readonly FreehandSliderToolboxGroup FreehandlSliderToolboxGroup = new FreehandSliderToolboxGroup(); + protected readonly FreehandSliderToolboxGroup FreehandSliderToolboxGroup = new FreehandSliderToolboxGroup(); [BackgroundDependencyLoader] private void load() @@ -110,7 +110,7 @@ namespace osu.Game.Rulesets.Osu.Edit RotationHandler = BlueprintContainer.SelectionHandler.RotationHandler, ScaleHandler = (OsuSelectionScaleHandler)BlueprintContainer.SelectionHandler.ScaleHandler, }, - FreehandlSliderToolboxGroup + FreehandSliderToolboxGroup } ); } From 9e962ce314f188c4bf1f17db7510cb4bf62236ee Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 19 Aug 2024 14:14:12 +0900 Subject: [PATCH 1985/2493] Add failing test case --- .../Gameplay/TestScenePauseInputHandling.cs | 46 ++++++++++++++++++- 1 file changed, 44 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePauseInputHandling.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePauseInputHandling.cs index bc66947ccd..843e924660 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePauseInputHandling.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePauseInputHandling.cs @@ -47,6 +47,11 @@ namespace osu.Game.Tests.Visual.Gameplay { Position = OsuPlayfield.BASE_SIZE / 2, StartTime = 5000, + }, + new HitCircle + { + Position = OsuPlayfield.BASE_SIZE / 2, + StartTime = 10000, } } }; @@ -281,6 +286,38 @@ namespace osu.Game.Tests.Visual.Gameplay AddAssert("button is released in kbc", () => !Player.DrawableRuleset.Playfield.FindClosestParent()!.PressedActions.Any()); } + [Test] + public void TestOsuRegisterInputFromPressingOrangeCursorButPressIsBlocked_PauseWhileHolding() + { + KeyCounter counter = null!; + + loadPlayer(() => new OsuRuleset()); + AddStep("get key counter", () => counter = this.ChildrenOfType().Single(k => k.Trigger is KeyCounterActionTrigger actionTrigger && actionTrigger.Action == OsuAction.LeftButton)); + + AddStep("press Z", () => InputManager.PressKey(Key.Z)); + AddAssert("circle hit", () => Player.ScoreProcessor.HighestCombo.Value, () => Is.EqualTo(1)); + + AddStep("pause", () => Player.Pause()); + AddStep("release Z", () => InputManager.ReleaseKey(Key.Z)); + + AddStep("resume", () => Player.Resume()); + AddStep("go to resume cursor", () => InputManager.MoveMouseTo(this.ChildrenOfType().Single())); + AddStep("press Z to resume", () => InputManager.PressKey(Key.Z)); + AddStep("release Z", () => InputManager.ReleaseKey(Key.Z)); + + checkKey(() => counter, 1, false); + + seekTo(5000); + + AddStep("press Z", () => InputManager.PressKey(Key.Z)); + + checkKey(() => counter, 2, true); + AddAssert("circle hit", () => Player.ScoreProcessor.HighestCombo.Value, () => Is.EqualTo(2)); + + AddStep("release Z", () => InputManager.ReleaseKey(Key.Z)); + checkKey(() => counter, 2, false); + } + private void loadPlayer(Func createRuleset) { AddStep("set ruleset", () => currentRuleset = createRuleset()); @@ -288,12 +325,17 @@ namespace osu.Game.Tests.Visual.Gameplay AddUntilStep("player loaded", () => Player.IsLoaded && Player.Alpha == 1); AddUntilStep("wait for hud", () => Player.HUDOverlay.ChildrenOfType().All(s => s.ComponentsLoaded)); - AddStep("seek to gameplay", () => Player.GameplayClockContainer.Seek(0)); - AddUntilStep("wait for seek to finish", () => Player.DrawableRuleset.FrameStableClock.CurrentTime, () => Is.EqualTo(0).Within(500)); + seekTo(0); AddAssert("not in break", () => !Player.IsBreakTime.Value); AddStep("move cursor to center", () => InputManager.MoveMouseTo(Player.DrawableRuleset.Playfield)); } + private void seekTo(double time) + { + AddStep($"seek to {time}ms", () => Player.GameplayClockContainer.Seek(time)); + AddUntilStep("wait for seek to finish", () => Player.DrawableRuleset.FrameStableClock.CurrentTime, () => Is.EqualTo(time).Within(500)); + } + private void checkKey(Func counter, int count, bool active) { AddAssert($"key count = {count}", () => counter().CountPresses.Value, () => Is.EqualTo(count)); From 62dec1cd786717eb6c2f575dd80aca9c85be8967 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 19 Aug 2024 14:14:39 +0900 Subject: [PATCH 1986/2493] Fix oversight in input blocking from osu! gameplay resume --- osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs b/osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs index d90d3d26eb..b12895ae52 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs @@ -13,6 +13,7 @@ using osu.Game.Rulesets.Osu.UI.Cursor; using osu.Game.Rulesets.UI; using osu.Game.Screens.Play; using osuTK.Graphics; +using TagLib.Flac; namespace osu.Game.Rulesets.Osu.UI { @@ -172,13 +173,14 @@ namespace osu.Game.Rulesets.Osu.UI Depth = float.MinValue; } - public bool OnPressed(KeyBindingPressEvent e) + protected override void Update() { - bool block = BlockNextPress; + base.Update(); BlockNextPress = false; - return block; } + public bool OnPressed(KeyBindingPressEvent e) => BlockNextPress; + public void OnReleased(KeyBindingReleaseEvent e) { } From 4a3f4c3a55ac513b85d1d1434b4ac145e3e53e78 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 19 Aug 2024 14:46:36 +0900 Subject: [PATCH 1987/2493] Don't duck music when effect volume is set to zero Addresses https://github.com/ppy/osu/discussions/28984. --- osu.Game/Overlays/MusicController.cs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/osu.Game/Overlays/MusicController.cs b/osu.Game/Overlays/MusicController.cs index d9bb92b4b7..27c7cd0f49 100644 --- a/osu.Game/Overlays/MusicController.cs +++ b/osu.Game/Overlays/MusicController.cs @@ -60,6 +60,8 @@ namespace osu.Game.Overlays [Resolved] private RealmAccess realm { get; set; } = null!; + private BindableNumber sampleVolume = null!; + private readonly BindableDouble audioDuckVolume = new BindableDouble(1); private AudioFilter audioDuckFilter = null!; @@ -69,6 +71,7 @@ namespace osu.Game.Overlays { AddInternal(audioDuckFilter = new AudioFilter(audio.TrackMixer)); audio.Tracks.AddAdjustment(AdjustableProperty.Volume, audioDuckVolume); + sampleVolume = audio.VolumeSample.GetBoundCopy(); } protected override void LoadComplete() @@ -269,6 +272,10 @@ namespace osu.Game.Overlays /// A which will restore the duck operation when disposed. public IDisposable Duck(DuckParameters? parameters = null) { + // Don't duck if samples have no volume, it sounds weird. + if (sampleVolume.Value == 0) + return new InvokeOnDisposal(() => { }); + parameters ??= new DuckParameters(); duckOperations.Add(parameters); @@ -302,6 +309,10 @@ namespace osu.Game.Overlays /// Parameters defining the ducking operation. public void DuckMomentarily(double delayUntilRestore, DuckParameters? parameters = null) { + // Don't duck if samples have no volume, it sounds weird. + if (sampleVolume.Value == 0) + return; + parameters ??= new DuckParameters(); IDisposable duckOperation = Duck(parameters); From ca92c116b5acc75945278fffc46d0d49d651ca9c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 19 Aug 2024 15:01:11 +0900 Subject: [PATCH 1988/2493] Fix osu!catch trail spacing not matching osu!stable expectations Closes https://github.com/ppy/osu/issues/28997. --- osu.Game.Rulesets.Catch/UI/CatcherArea.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs index 21faec56de..338e1364a9 100644 --- a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs +++ b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs @@ -110,9 +110,9 @@ namespace osu.Game.Rulesets.Catch.UI if (Catcher.Dashing || Catcher.HyperDashing) { - double generationInterval = Catcher.HyperDashing ? 25 : 50; + const double trail_generation_interval = 16; - if (Time.Current - catcherTrails.LastDashTrailTime >= generationInterval) + if (Time.Current - catcherTrails.LastDashTrailTime >= trail_generation_interval) displayCatcherTrail(Catcher.HyperDashing ? CatcherTrailAnimation.HyperDashing : CatcherTrailAnimation.Dashing); } From 86d0079dcdadbbf1521dc3d8520616b8bb27a529 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 19 Aug 2024 15:43:57 +0900 Subject: [PATCH 1989/2493] Rewrite the fix to look less hacky and direct to the point --- osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs | 23 +++++++++++--------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs b/osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs index b12895ae52..8ae08ed021 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs @@ -3,6 +3,7 @@ using System; using osu.Framework.Allocation; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; @@ -36,9 +37,11 @@ namespace osu.Game.Rulesets.Osu.UI { OsuResumeOverlayInputBlocker? inputBlocker = null; - if (drawableRuleset != null) + var drawableOsuRuleset = (DrawableOsuRuleset?)drawableRuleset; + + if (drawableOsuRuleset != null) { - var osuPlayfield = (OsuPlayfield)drawableRuleset.Playfield; + var osuPlayfield = drawableOsuRuleset.Playfield; osuPlayfield.AttachResumeOverlayInputBlocker(inputBlocker = new OsuResumeOverlayInputBlocker()); } @@ -46,13 +49,14 @@ namespace osu.Game.Rulesets.Osu.UI { Child = clickToResumeCursor = new OsuClickToResumeCursor { - ResumeRequested = () => + ResumeRequested = action => { // since the user had to press a button to tap the resume cursor, // block that press event from potentially reaching a hit circle that's behind the cursor. // we cannot do this from OsuClickToResumeCursor directly since we're in a different input manager tree than the gameplay one, // so we rely on a dedicated input blocking component that's implanted in there to do that for us. - if (inputBlocker != null) + // note this only matters when the user didn't pause while they were holding the same key that they are resuming with. + if (inputBlocker != null && !drawableOsuRuleset.AsNonNull().KeyBindingInputManager.PressedActions.Contains(action)) inputBlocker.BlockNextPress = true; Resume(); @@ -95,7 +99,7 @@ namespace osu.Game.Rulesets.Osu.UI { public override bool HandlePositionalInput => true; - public Action? ResumeRequested; + public Action? ResumeRequested; private Container scaleTransitionContainer = null!; public OsuClickToResumeCursor() @@ -137,7 +141,7 @@ namespace osu.Game.Rulesets.Osu.UI return false; scaleTransitionContainer.ScaleTo(2, TRANSITION_TIME, Easing.OutQuint); - ResumeRequested?.Invoke(); + ResumeRequested?.Invoke(e.Action); return true; } @@ -173,14 +177,13 @@ namespace osu.Game.Rulesets.Osu.UI Depth = float.MinValue; } - protected override void Update() + public bool OnPressed(KeyBindingPressEvent e) { - base.Update(); + bool block = BlockNextPress; BlockNextPress = false; + return block; } - public bool OnPressed(KeyBindingPressEvent e) => BlockNextPress; - public void OnReleased(KeyBindingReleaseEvent e) { } From 2a49167aa0051bc491d374de9a0b05c61daf12e5 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 19 Aug 2024 15:44:17 +0900 Subject: [PATCH 1990/2493] Remove flac whatever --- osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs b/osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs index 8ae08ed021..b045b82960 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs @@ -14,7 +14,6 @@ using osu.Game.Rulesets.Osu.UI.Cursor; using osu.Game.Rulesets.UI; using osu.Game.Screens.Play; using osuTK.Graphics; -using TagLib.Flac; namespace osu.Game.Rulesets.Osu.UI { From 1bd2f4c6a2a2c77411d2bf74ec3e4a408f82ed59 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 19 Aug 2024 15:45:18 +0900 Subject: [PATCH 1991/2493] Fix skin editor components sidebar not reloading when changing skins Closes https://github.com/ppy/osu/issues/29098. --- osu.Game/Overlays/SkinEditor/SkinEditor.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Overlays/SkinEditor/SkinEditor.cs b/osu.Game/Overlays/SkinEditor/SkinEditor.cs index 484af34603..03acf1e68c 100644 --- a/osu.Game/Overlays/SkinEditor/SkinEditor.cs +++ b/osu.Game/Overlays/SkinEditor/SkinEditor.cs @@ -421,6 +421,9 @@ namespace osu.Game.Overlays.SkinEditor if (targetContainer != null) changeHandler = new SkinEditorChangeHandler(targetContainer); hasBegunMutating = true; + + // Reload sidebar components. + selectedTarget.TriggerChange(); } /// From 005b1038a3e31092cdf8174bc42ddfe6f497ef25 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 19 Aug 2024 20:23:25 +0900 Subject: [PATCH 1992/2493] Change "hold for menu" button to only show for touch by default --- osu.Game/Configuration/OsuConfigManager.cs | 3 +++ osu.Game/Localisation/GameplaySettingsStrings.cs | 5 +++++ .../Settings/Sections/Gameplay/HUDSettings.cs | 5 +++++ osu.Game/Screens/Play/HUD/HoldForMenuButton.cs | 16 ++++++++++++++-- 4 files changed, 27 insertions(+), 2 deletions(-) diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index d00856dd80..8d6c244b35 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -205,6 +205,8 @@ namespace osu.Game.Configuration SetDefault(OsuSetting.EditorTimelineShowTimingChanges, true); SetDefault(OsuSetting.EditorTimelineShowTicks, true); + + SetDefault(OsuSetting.AlwaysShowHoldForMenuButton, false); } protected override bool CheckLookupContainsPrivateInformation(OsuSetting lookup) @@ -429,5 +431,6 @@ namespace osu.Game.Configuration HideCountryFlags, EditorTimelineShowTimingChanges, EditorTimelineShowTicks, + AlwaysShowHoldForMenuButton } } diff --git a/osu.Game/Localisation/GameplaySettingsStrings.cs b/osu.Game/Localisation/GameplaySettingsStrings.cs index 8ee76fdd55..6de61f7ebe 100644 --- a/osu.Game/Localisation/GameplaySettingsStrings.cs +++ b/osu.Game/Localisation/GameplaySettingsStrings.cs @@ -84,6 +84,11 @@ namespace osu.Game.Localisation /// public static LocalisableString AlwaysShowGameplayLeaderboard => new TranslatableString(getKey(@"gameplay_leaderboard"), @"Always show gameplay leaderboard"); + /// + /// "Always show hold for menu button" + /// + public static LocalisableString AlwaysShowHoldForMenuButton => new TranslatableString(getKey(@"always_show_hold_for_menu_button"), @"Always show hold for menu button"); + /// /// "Always play first combo break sound" /// diff --git a/osu.Game/Overlays/Settings/Sections/Gameplay/HUDSettings.cs b/osu.Game/Overlays/Settings/Sections/Gameplay/HUDSettings.cs index 3e67b2f103..f4dd319152 100644 --- a/osu.Game/Overlays/Settings/Sections/Gameplay/HUDSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Gameplay/HUDSettings.cs @@ -41,6 +41,11 @@ namespace osu.Game.Overlays.Settings.Sections.Gameplay Current = config.GetBindable(OsuSetting.GameplayLeaderboard), }, new SettingsCheckbox + { + LabelText = GameplaySettingsStrings.AlwaysShowHoldForMenuButton, + Current = config.GetBindable(OsuSetting.AlwaysShowHoldForMenuButton), + }, + new SettingsCheckbox { ClassicDefault = false, LabelText = GameplaySettingsStrings.ShowHealthDisplayWhenCantFail, diff --git a/osu.Game/Screens/Play/HUD/HoldForMenuButton.cs b/osu.Game/Screens/Play/HUD/HoldForMenuButton.cs index 6d045e5f01..41600c2bb8 100644 --- a/osu.Game/Screens/Play/HUD/HoldForMenuButton.cs +++ b/osu.Game/Screens/Play/HUD/HoldForMenuButton.cs @@ -40,6 +40,10 @@ namespace osu.Game.Screens.Play.HUD private OsuSpriteText text; + private Bindable alwaysShow; + + public override bool PropagatePositionalInputSubTree => alwaysShow.Value || touchActive.Value; + public HoldForMenuButton() { Direction = FillDirection.Horizontal; @@ -50,7 +54,7 @@ namespace osu.Game.Screens.Play.HUD } [BackgroundDependencyLoader(true)] - private void load(Player player) + private void load(Player player, OsuConfigManager config) { Children = new Drawable[] { @@ -71,6 +75,8 @@ namespace osu.Game.Screens.Play.HUD }; AutoSizeAxes = Axes.Both; + + alwaysShow = config.GetBindable(OsuSetting.AlwaysShowHoldForMenuButton); } [Resolved] @@ -119,7 +125,9 @@ namespace osu.Game.Screens.Play.HUD if (text.Alpha > 0 || button.Progress.Value > 0 || button.IsHovered) Alpha = 1; - else + else if (touchActive.Value) + Alpha = 0.08f; + else if (alwaysShow.Value) { float minAlpha = touchActive.Value ? .08f : 0; @@ -127,6 +135,10 @@ namespace osu.Game.Screens.Play.HUD Math.Clamp(Clock.ElapsedFrameTime, 0, 200), Alpha, Math.Clamp(1 - positionalAdjust, minAlpha, 1), 0, 200, Easing.OutQuint); } + else + { + Alpha = 0; + } } private partial class HoldButton : HoldToConfirmContainer, IKeyBindingHandler From 6985e2e657c4ed875aa8305f4a5d8f7fab651d1e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 19 Aug 2024 20:28:02 +0900 Subject: [PATCH 1993/2493] Increase default visibility on touch platforms --- osu.Game/Screens/Play/HUD/HoldForMenuButton.cs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/HoldForMenuButton.cs b/osu.Game/Screens/Play/HUD/HoldForMenuButton.cs index 41600c2bb8..89d083eca9 100644 --- a/osu.Game/Screens/Play/HUD/HoldForMenuButton.cs +++ b/osu.Game/Screens/Play/HUD/HoldForMenuButton.cs @@ -30,6 +30,8 @@ namespace osu.Game.Screens.Play.HUD { public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; + public override bool PropagatePositionalInputSubTree => alwaysShow.Value || touchActive.Value; + public readonly Bindable IsPaused = new Bindable(); public readonly Bindable ReplayLoaded = new Bindable(); @@ -42,8 +44,6 @@ namespace osu.Game.Screens.Play.HUD private Bindable alwaysShow; - public override bool PropagatePositionalInputSubTree => alwaysShow.Value || touchActive.Value; - public HoldForMenuButton() { Direction = FillDirection.Horizontal; @@ -123,10 +123,13 @@ namespace osu.Game.Screens.Play.HUD { base.Update(); + // While the button is hovered or still animating, keep fully visible. if (text.Alpha > 0 || button.Progress.Value > 0 || button.IsHovered) Alpha = 1; + // When touch input is detected, keep visible at a constant opacity. else if (touchActive.Value) - Alpha = 0.08f; + Alpha = 0.5f; + // Otherwise, if the user chooses, show it when the mouse is nearby. else if (alwaysShow.Value) { float minAlpha = touchActive.Value ? .08f : 0; @@ -136,9 +139,7 @@ namespace osu.Game.Screens.Play.HUD Alpha, Math.Clamp(1 - positionalAdjust, minAlpha, 1), 0, 200, Easing.OutQuint); } else - { Alpha = 0; - } } private partial class HoldButton : HoldToConfirmContainer, IKeyBindingHandler From 610ebc5481ebc605ce06d5537e8ad4355c517cd6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 19 Aug 2024 20:50:11 +0900 Subject: [PATCH 1994/2493] Fix toolbar PP change showing `+0` instead of `0` --- osu.Game.Tests/Visual/Menus/TestSceneToolbarUserButton.cs | 2 +- .../Toolbar/TransientUserStatisticsUpdateDisplay.cs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Visual/Menus/TestSceneToolbarUserButton.cs b/osu.Game.Tests/Visual/Menus/TestSceneToolbarUserButton.cs index 1a4ca65975..a81c940d82 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneToolbarUserButton.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneToolbarUserButton.cs @@ -142,7 +142,7 @@ namespace osu.Game.Tests.Visual.Menus new UserStatistics { GlobalRank = 111_111, - PP = 1357 + PP = 1357.1m }); }); AddStep("Was null", () => diff --git a/osu.Game/Overlays/Toolbar/TransientUserStatisticsUpdateDisplay.cs b/osu.Game/Overlays/Toolbar/TransientUserStatisticsUpdateDisplay.cs index c6f373d55f..a25df08309 100644 --- a/osu.Game/Overlays/Toolbar/TransientUserStatisticsUpdateDisplay.cs +++ b/osu.Game/Overlays/Toolbar/TransientUserStatisticsUpdateDisplay.cs @@ -24,7 +24,7 @@ namespace osu.Game.Overlays.Toolbar public Bindable LatestUpdate { get; } = new Bindable(); private Statistic globalRank = null!; - private Statistic pp = null!; + private Statistic pp = null!; [BackgroundDependencyLoader] private void load(UserStatisticsWatcher? userStatisticsWatcher) @@ -43,7 +43,7 @@ namespace osu.Game.Overlays.Toolbar Children = new Drawable[] { globalRank = new Statistic(UsersStrings.ShowRankGlobalSimple, @"#", Comparer.Create((before, after) => before - after)), - pp = new Statistic(RankingsStrings.StatPerformance, string.Empty, Comparer.Create((before, after) => Math.Sign(after - before))), + pp = new Statistic(RankingsStrings.StatPerformance, string.Empty, Comparer.Create((before, after) => Math.Sign(after - before))), } }; @@ -83,7 +83,7 @@ namespace osu.Game.Overlays.Toolbar } if (update.After.PP != null) - pp.Display(update.Before.PP ?? update.After.PP.Value, Math.Abs((update.After.PP - update.Before.PP) ?? 0M), update.After.PP.Value); + pp.Display((int)(update.Before.PP ?? update.After.PP.Value), (int)Math.Abs((update.After.PP - update.Before.PP) ?? 0M), (int)update.After.PP.Value); this.Delay(5000).FadeOut(500, Easing.OutQuint); }); From 67de43213c4a097dcf211d42549fd86b4f89133f Mon Sep 17 00:00:00 2001 From: TheOmyNomy Date: Mon, 19 Aug 2024 23:21:06 +1000 Subject: [PATCH 1995/2493] Apply current cursor expansion scale to trail parts --- osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs | 15 +++++++++++---- osu.Game.Rulesets.Osu/UI/Cursor/OsuCursor.cs | 5 +++++ .../UI/Cursor/OsuCursorContainer.cs | 13 ++++++++++--- 3 files changed, 26 insertions(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs b/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs index 6452444fed..a4bccb0aff 100644 --- a/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs +++ b/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs @@ -38,6 +38,11 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor private double timeOffset; private float time; + /// + /// The scale used on creation of a new trail part. + /// + public Vector2 NewPartScale = Vector2.One; + private Anchor trailOrigin = Anchor.Centre; protected Anchor TrailOrigin @@ -188,6 +193,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor { parts[currentIndex].Position = localSpacePosition; parts[currentIndex].Time = time + 1; + parts[currentIndex].Scale = NewPartScale; ++parts[currentIndex].InvalidationID; currentIndex = (currentIndex + 1) % max_sprites; @@ -199,6 +205,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor { public Vector2 Position; public float Time; + public Vector2 Scale; public long InvalidationID; } @@ -280,7 +287,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor vertexBatch.Add(new TexturedTrailVertex { - Position = new Vector2(part.Position.X - texture.DisplayWidth * originPosition.X, part.Position.Y + texture.DisplayHeight * (1 - originPosition.Y)), + Position = new Vector2(part.Position.X - texture.DisplayWidth * originPosition.X * part.Scale.X, part.Position.Y + texture.DisplayHeight * (1 - originPosition.Y) * part.Scale.Y), TexturePosition = textureRect.BottomLeft, TextureRect = new Vector4(0, 0, 1, 1), Colour = DrawColourInfo.Colour.BottomLeft.Linear, @@ -289,7 +296,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor vertexBatch.Add(new TexturedTrailVertex { - Position = new Vector2(part.Position.X + texture.DisplayWidth * (1 - originPosition.X), part.Position.Y + texture.DisplayHeight * (1 - originPosition.Y)), + Position = new Vector2(part.Position.X + texture.DisplayWidth * (1 - originPosition.X) * part.Scale.X, part.Position.Y + texture.DisplayHeight * (1 - originPosition.Y) * part.Scale.Y), TexturePosition = textureRect.BottomRight, TextureRect = new Vector4(0, 0, 1, 1), Colour = DrawColourInfo.Colour.BottomRight.Linear, @@ -298,7 +305,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor vertexBatch.Add(new TexturedTrailVertex { - Position = new Vector2(part.Position.X + texture.DisplayWidth * (1 - originPosition.X), part.Position.Y - texture.DisplayHeight * originPosition.Y), + Position = new Vector2(part.Position.X + texture.DisplayWidth * (1 - originPosition.X) * part.Scale.X, part.Position.Y - texture.DisplayHeight * originPosition.Y * part.Scale.Y), TexturePosition = textureRect.TopRight, TextureRect = new Vector4(0, 0, 1, 1), Colour = DrawColourInfo.Colour.TopRight.Linear, @@ -307,7 +314,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor vertexBatch.Add(new TexturedTrailVertex { - Position = new Vector2(part.Position.X - texture.DisplayWidth * originPosition.X, part.Position.Y - texture.DisplayHeight * originPosition.Y), + Position = new Vector2(part.Position.X - texture.DisplayWidth * originPosition.X * part.Scale.X, part.Position.Y - texture.DisplayHeight * originPosition.Y * part.Scale.Y), TexturePosition = textureRect.TopLeft, TextureRect = new Vector4(0, 0, 1, 1), Colour = DrawColourInfo.Colour.TopLeft.Linear, diff --git a/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursor.cs b/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursor.cs index d8f50c1f5d..0bb316e0aa 100644 --- a/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursor.cs +++ b/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursor.cs @@ -31,6 +31,11 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor private SkinnableCursor skinnableCursor => (SkinnableCursor)cursorSprite.Drawable; + /// + /// The current expanded scale of the cursor. + /// + public Vector2 CurrentExpandedScale => skinnableCursor.ExpandTarget?.Scale ?? Vector2.One; + public IBindable CursorScale => cursorScale; private readonly Bindable cursorScale = new BindableFloat(1); diff --git a/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursorContainer.cs b/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursorContainer.cs index ba8a634ff7..9ac81d13a7 100644 --- a/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursorContainer.cs +++ b/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursorContainer.cs @@ -23,14 +23,13 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor public new OsuCursor ActiveCursor => (OsuCursor)base.ActiveCursor; protected override Drawable CreateCursor() => new OsuCursor(); - protected override Container Content => fadeContainer; private readonly Container fadeContainer; private readonly Bindable showTrail = new Bindable(true); - private readonly Drawable cursorTrail; + private readonly SkinnableDrawable cursorTrail; private readonly CursorRippleVisualiser rippleVisualiser; @@ -39,7 +38,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor InternalChild = fadeContainer = new Container { RelativeSizeAxes = Axes.Both, - Children = new[] + Children = new CompositeDrawable[] { cursorTrail = new SkinnableDrawable(new OsuSkinComponentLookup(OsuSkinComponents.CursorTrail), _ => new DefaultCursorTrail(), confineMode: ConfineMode.NoScaling), rippleVisualiser = new CursorRippleVisualiser(), @@ -79,6 +78,14 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor ActiveCursor.Contract(); } + protected override void Update() + { + base.Update(); + + // We can direct cast here because the cursor trail is always a derived class of CursorTrail. + ((CursorTrail)cursorTrail.Drawable).NewPartScale = ActiveCursor.CurrentExpandedScale; + } + public bool OnPressed(KeyBindingPressEvent e) { switch (e.Action) From 59ba48bc8130cd6b96128df531d685698010e3f6 Mon Sep 17 00:00:00 2001 From: Layendan Date: Mon, 19 Aug 2024 07:58:20 -0700 Subject: [PATCH 1996/2493] Fix crash if favourite button api request fails --- osu.Game/Screens/Ranking/FavouriteButton.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Ranking/FavouriteButton.cs b/osu.Game/Screens/Ranking/FavouriteButton.cs index daa6312020..bb4f25080c 100644 --- a/osu.Game/Screens/Ranking/FavouriteButton.cs +++ b/osu.Game/Screens/Ranking/FavouriteButton.cs @@ -78,7 +78,7 @@ namespace osu.Game.Screens.Ranking { Logger.Error(e, $"Failed to fetch beatmap info: {e.Message}"); - loading.Hide(); + Schedule(() => loading.Hide()); Enabled.Value = false; }; api.Queue(beatmapSetRequest); From 5ba1b4fe3d16bd95204137857e20cf343f5e701a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 20 Aug 2024 01:12:57 +0900 Subject: [PATCH 1997/2493] Update test coverage --- .../Visual/Gameplay/TestSceneHoldForMenuButton.cs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneHoldForMenuButton.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneHoldForMenuButton.cs index 3c225d60e0..cd1334165b 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneHoldForMenuButton.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneHoldForMenuButton.cs @@ -1,13 +1,13 @@ // 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.Linq; using NUnit.Framework; +using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Framework.Testing; +using osu.Game.Configuration; using osu.Game.Screens.Play.HUD; using osuTK; using osuTK.Input; @@ -21,11 +21,19 @@ namespace osu.Game.Tests.Visual.Gameplay protected override double TimePerAction => 100; // required for the early exit test, since hold-to-confirm delay is 200ms - private HoldForMenuButton holdForMenuButton; + private HoldForMenuButton holdForMenuButton = null!; + + [Resolved] + private OsuConfigManager config { get; set; } = null!; [SetUpSteps] public void SetUpSteps() { + AddStep("set button always on", () => + { + config.SetValue(OsuSetting.AlwaysShowHoldForMenuButton, true); + }); + AddStep("create button", () => { exitAction = false; From 86c3c115f6fbe315a4ef99c9218b73239e703573 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 20 Aug 2024 12:15:33 +0900 Subject: [PATCH 1998/2493] Make grid/distance snap binds T/Y respectively --- osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs index bbcf4fa2d4..4476160f81 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs @@ -54,11 +54,8 @@ namespace osu.Game.Rulesets.Osu.Edit protected override IEnumerable CreateTernaryButtons() => base.CreateTernaryButtons() - .Concat(DistanceSnapProvider.CreateTernaryButtons()) - .Concat(new[] - { - new TernaryButton(rectangularGridSnapToggle, "Grid Snap", () => new SpriteIcon { Icon = OsuIcon.EditorGridSnap }) - }); + .Append(new TernaryButton(rectangularGridSnapToggle, "Grid Snap", () => new SpriteIcon { Icon = OsuIcon.EditorGridSnap })) + .Concat(DistanceSnapProvider.CreateTernaryButtons()); private BindableList selectedHitObjects; From a3234e2cdefaca43ca0aa76a092bc1bda00a156f Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 20 Aug 2024 12:28:36 +0900 Subject: [PATCH 1999/2493] Add failing test case --- .../Skinning/ManiaSkinnableTestScene.cs | 10 +-- .../Skinning/TestSceneComboCounter.cs | 83 ++++++++++++++++--- 2 files changed, 75 insertions(+), 18 deletions(-) diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaSkinnableTestScene.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaSkinnableTestScene.cs index abf01aa4a4..b2e8ebd581 100644 --- a/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaSkinnableTestScene.cs +++ b/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaSkinnableTestScene.cs @@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning public abstract partial class ManiaSkinnableTestScene : SkinnableTestScene { [Cached(Type = typeof(IScrollingInfo))] - private readonly TestScrollingInfo scrollingInfo = new TestScrollingInfo(); + protected readonly TestScrollingInfo ScrollingInfo = new TestScrollingInfo(); [Cached] private readonly StageDefinition stage = new StageDefinition(4); @@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning protected ManiaSkinnableTestScene() { - scrollingInfo.Direction.Value = ScrollingDirection.Down; + ScrollingInfo.Direction.Value = ScrollingDirection.Down; Add(new Box { @@ -43,16 +43,16 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning [Test] public void TestScrollingDown() { - AddStep("change direction to down", () => scrollingInfo.Direction.Value = ScrollingDirection.Down); + AddStep("change direction to down", () => ScrollingInfo.Direction.Value = ScrollingDirection.Down); } [Test] public void TestScrollingUp() { - AddStep("change direction to up", () => scrollingInfo.Direction.Value = ScrollingDirection.Up); + AddStep("change direction to up", () => ScrollingInfo.Direction.Value = ScrollingDirection.Up); } - private class TestScrollingInfo : IScrollingInfo + protected class TestScrollingInfo : IScrollingInfo { public readonly Bindable Direction = new Bindable(); diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneComboCounter.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneComboCounter.cs index c1e1cfd7af..ccdebb502c 100644 --- a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneComboCounter.cs +++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneComboCounter.cs @@ -1,13 +1,17 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using NUnit.Framework; using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Framework.Testing; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Mania.Skinning.Argon; using osu.Game.Rulesets.Mania.Skinning.Legacy; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Scoring; +using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Skinning; namespace osu.Game.Rulesets.Mania.Tests.Skinning @@ -17,22 +21,75 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning [Cached] private ScoreProcessor scoreProcessor = new ScoreProcessor(new ManiaRuleset()); - [SetUpSteps] - public void SetUpSteps() + [Test] + public void TestDisplay() { - AddStep("setup", () => SetContents(s => - { - if (s is ArgonSkin) - return new ArgonManiaComboCounter(); - - if (s is LegacySkin) - return new LegacyManiaComboCounter(); - - return new LegacyManiaComboCounter(); - })); - + setup(Anchor.Centre); AddRepeatStep("perform hit", () => scoreProcessor.ApplyResult(new JudgementResult(new HitObject(), new Judgement()) { Type = HitResult.Great }), 20); AddStep("perform miss", () => scoreProcessor.ApplyResult(new JudgementResult(new HitObject(), new Judgement()) { Type = HitResult.Miss })); } + + [Test] + public void TestAnchorOrigin() + { + AddStep("set direction down", () => ScrollingInfo.Direction.Value = ScrollingDirection.Down); + setup(Anchor.TopCentre, 20); + AddStep("set direction up", () => ScrollingInfo.Direction.Value = ScrollingDirection.Up); + check(Anchor.BottomCentre, -20); + + AddStep("set direction up", () => ScrollingInfo.Direction.Value = ScrollingDirection.Up); + setup(Anchor.BottomCentre, -20); + AddStep("set direction down", () => ScrollingInfo.Direction.Value = ScrollingDirection.Down); + check(Anchor.TopCentre, 20); + + AddStep("set direction down", () => ScrollingInfo.Direction.Value = ScrollingDirection.Down); + setup(Anchor.Centre, 20); + AddStep("set direction up", () => ScrollingInfo.Direction.Value = ScrollingDirection.Up); + check(Anchor.Centre, 20); + + AddStep("set direction up", () => ScrollingInfo.Direction.Value = ScrollingDirection.Up); + setup(Anchor.Centre, -20); + AddStep("set direction down", () => ScrollingInfo.Direction.Value = ScrollingDirection.Down); + check(Anchor.Centre, -20); + } + + private void setup(Anchor anchor, float y = 0) + { + AddStep($"setup {anchor} {y}", () => SetContents(s => + { + var container = new Container + { + RelativeSizeAxes = Axes.Both, + }; + + if (s is ArgonSkin) + container.Add(new ArgonManiaComboCounter()); + else if (s is LegacySkin) + container.Add(new LegacyManiaComboCounter()); + else + container.Add(new LegacyManiaComboCounter()); + + container.Child.Anchor = anchor; + container.Child.Origin = Anchor.Centre; + container.Child.Y = y; + + return container; + })); + } + + private void check(Anchor anchor, float y) + { + AddAssert($"check {anchor} {y}", () => + { + foreach (var combo in this.ChildrenOfType()) + { + var drawableCombo = (Drawable)combo; + if (drawableCombo.Anchor != anchor || drawableCombo.Y != y) + return false; + } + + return true; + }); + } } } From 4d74625bc7cf278bf273b7c5e51f5df4e8fdb759 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 20 Aug 2024 12:39:51 +0900 Subject: [PATCH 2000/2493] Fix mania combo counter positioning break on centre anchor --- .../Skinning/Argon/ArgonManiaComboCounter.cs | 10 +++++----- .../Skinning/Legacy/LegacyManiaComboCounter.cs | 18 +++++++++--------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonManiaComboCounter.cs b/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonManiaComboCounter.cs index 5b23cea496..6626e5f1c7 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonManiaComboCounter.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonManiaComboCounter.cs @@ -38,11 +38,11 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon private void updateAnchor() { // if the anchor isn't a vertical center, set top or bottom anchor based on scroll direction - if (!Anchor.HasFlag(Anchor.y1)) - { - Anchor &= ~(Anchor.y0 | Anchor.y2); - Anchor |= direction.Value == ScrollingDirection.Up ? Anchor.y2 : Anchor.y0; - } + if (Anchor.HasFlag(Anchor.y1)) + return; + + Anchor &= ~(Anchor.y0 | Anchor.y2); + Anchor |= direction.Value == ScrollingDirection.Up ? Anchor.y2 : Anchor.y0; // change the sign of the Y coordinate in line with the scrolling direction. // i.e. if the user changes direction from down to up, the anchor is changed from top to bottom, and the Y is flipped from positive to negative here. diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyManiaComboCounter.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyManiaComboCounter.cs index 5832210836..07d014b416 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyManiaComboCounter.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyManiaComboCounter.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; @@ -44,16 +45,15 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy private void updateAnchor() { // if the anchor isn't a vertical center, set top or bottom anchor based on scroll direction - if (!Anchor.HasFlag(Anchor.y1)) - { - Anchor &= ~(Anchor.y0 | Anchor.y2); - Anchor |= direction.Value == ScrollingDirection.Up ? Anchor.y2 : Anchor.y0; - } + if (Anchor.HasFlag(Anchor.y1)) + return; - // since we flip the vertical anchor when changing scroll direction, - // we can use the sign of the Y value as an indicator to make the combo counter displayed correctly. - if ((Y < 0 && direction.Value == ScrollingDirection.Down) || (Y > 0 && direction.Value == ScrollingDirection.Up)) - Y = -Y; + Anchor &= ~(Anchor.y0 | Anchor.y2); + Anchor |= direction.Value == ScrollingDirection.Up ? Anchor.y2 : Anchor.y0; + + // change the sign of the Y coordinate in line with the scrolling direction. + // i.e. if the user changes direction from down to up, the anchor is changed from top to bottom, and the Y is flipped from positive to negative here. + Y = Math.Abs(Y) * (direction.Value == ScrollingDirection.Up ? -1 : 1); } protected override void OnCountIncrement() From 180c4a02485398dd6af523c4665476aa51a1665e Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 20 Aug 2024 14:20:52 +0900 Subject: [PATCH 2001/2493] Fix tests by removing assumption --- osu.Game.Rulesets.Osu/UI/Cursor/OsuCursorContainer.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursorContainer.cs b/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursorContainer.cs index 9ac81d13a7..8c0871d54f 100644 --- a/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursorContainer.cs +++ b/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursorContainer.cs @@ -82,8 +82,8 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor { base.Update(); - // We can direct cast here because the cursor trail is always a derived class of CursorTrail. - ((CursorTrail)cursorTrail.Drawable).NewPartScale = ActiveCursor.CurrentExpandedScale; + if (cursorTrail.Drawable is CursorTrail trail) + trail.NewPartScale = ActiveCursor.CurrentExpandedScale; } public bool OnPressed(KeyBindingPressEvent e) From 4a19ed7472f27859ef47dc2907c617c33b786365 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 20 Aug 2024 15:20:48 +0900 Subject: [PATCH 2002/2493] Add test --- .../TestSceneCursorTrail.cs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneCursorTrail.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneCursorTrail.cs index 4db66fde4b..17f365f820 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneCursorTrail.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneCursorTrail.cs @@ -88,6 +88,21 @@ namespace osu.Game.Rulesets.Osu.Tests AddAssert("trail is disjoint", () => this.ChildrenOfType().Single().DisjointTrail, () => Is.True); } + [Test] + public void TestClickExpand() + { + createTest(() => new Container + { + RelativeSizeAxes = Axes.Both, + Scale = new Vector2(10), + Child = new CursorTrail(), + }); + + AddStep("expand", () => this.ChildrenOfType().Single().NewPartScale = new Vector2(3)); + AddWaitStep("let the cursor trail draw a bit", 5); + AddStep("contract", () => this.ChildrenOfType().Single().NewPartScale = Vector2.One); + } + private void createTest(Func createContent) => AddStep("create trail", () => { Clear(); From 2e67ff1d92fa25d4faf231b1d26403926ae92773 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 20 Aug 2024 16:14:05 +0900 Subject: [PATCH 2003/2493] Fix tests --- .../Editor/TestSceneOsuEditorGrids.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorGrids.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorGrids.cs index b17f4e7487..b70ecfbba8 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorGrids.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorGrids.cs @@ -24,24 +24,24 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor [Test] public void TestGridToggles() { - AddStep("enable distance snap grid", () => InputManager.Key(Key.T)); + AddStep("enable distance snap grid", () => InputManager.Key(Key.Y)); AddStep("select second object", () => EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects.ElementAt(1))); AddUntilStep("distance snap grid visible", () => this.ChildrenOfType().Any()); gridActive(false); - AddStep("enable rectangular grid", () => InputManager.Key(Key.Y)); + AddStep("enable rectangular grid", () => InputManager.Key(Key.T)); AddStep("select second object", () => EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects.ElementAt(1))); AddUntilStep("distance snap grid still visible", () => this.ChildrenOfType().Any()); gridActive(true); - AddStep("disable distance snap grid", () => InputManager.Key(Key.T)); + AddStep("disable distance snap grid", () => InputManager.Key(Key.Y)); AddUntilStep("distance snap grid hidden", () => !this.ChildrenOfType().Any()); AddStep("select second object", () => EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects.ElementAt(1))); gridActive(true); - AddStep("disable rectangular grid", () => InputManager.Key(Key.Y)); + AddStep("disable rectangular grid", () => InputManager.Key(Key.T)); AddUntilStep("distance snap grid still hidden", () => !this.ChildrenOfType().Any()); gridActive(false); } @@ -57,7 +57,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor AddStep("release alt", () => InputManager.ReleaseKey(Key.AltLeft)); AddUntilStep("distance snap grid hidden", () => !this.ChildrenOfType().Any()); - AddStep("enable distance snap grid", () => InputManager.Key(Key.T)); + AddStep("enable distance snap grid", () => InputManager.Key(Key.Y)); AddUntilStep("distance snap grid visible", () => this.ChildrenOfType().Any()); AddStep("hold alt", () => InputManager.PressKey(Key.AltLeft)); AddUntilStep("distance snap grid hidden", () => !this.ChildrenOfType().Any()); @@ -70,7 +70,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor { double distanceSnap = double.PositiveInfinity; - AddStep("enable distance snap grid", () => InputManager.Key(Key.T)); + AddStep("enable distance snap grid", () => InputManager.Key(Key.Y)); AddStep("select second object", () => EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects.ElementAt(1))); AddUntilStep("distance snap grid visible", () => this.ChildrenOfType().Any()); @@ -170,7 +170,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor [Test] public void TestGridSizeToggling() { - AddStep("enable rectangular grid", () => InputManager.Key(Key.Y)); + AddStep("enable rectangular grid", () => InputManager.Key(Key.T)); AddUntilStep("rectangular grid visible", () => this.ChildrenOfType().Any()); gridSizeIs(4); From 2ecf5ec939d2eb5eb12a91fe846365392a8af6a7 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 20 Aug 2024 16:22:25 +0900 Subject: [PATCH 2004/2493] Add further test coverage --- .../Gameplay/TestScenePauseInputHandling.cs | 35 +++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePauseInputHandling.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePauseInputHandling.cs index 843e924660..8a41d8b573 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePauseInputHandling.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePauseInputHandling.cs @@ -52,6 +52,11 @@ namespace osu.Game.Tests.Visual.Gameplay { Position = OsuPlayfield.BASE_SIZE / 2, StartTime = 10000, + }, + new HitCircle + { + Position = OsuPlayfield.BASE_SIZE / 2, + StartTime = 15000, } } }; @@ -261,7 +266,7 @@ namespace osu.Game.Tests.Visual.Gameplay } [Test] - public void TestOsuRegisterInputFromPressingOrangeCursorButPressIsBlocked() + public void TestOsuHitCircleNotReceivingInputOnResume() { KeyCounter counter = null!; @@ -287,7 +292,7 @@ namespace osu.Game.Tests.Visual.Gameplay } [Test] - public void TestOsuRegisterInputFromPressingOrangeCursorButPressIsBlocked_PauseWhileHolding() + public void TestOsuHitCircleNotReceivingInputOnResume_PauseWhileHoldingSameKey() { KeyCounter counter = null!; @@ -318,6 +323,32 @@ namespace osu.Game.Tests.Visual.Gameplay checkKey(() => counter, 2, false); } + [Test] + public void TestOsuHitCircleNotReceivingInputOnResume_PauseWhileHoldingOtherKey() + { + loadPlayer(() => new OsuRuleset()); + + AddStep("press X", () => InputManager.PressKey(Key.X)); + AddAssert("circle hit", () => Player.ScoreProcessor.HighestCombo.Value, () => Is.EqualTo(1)); + + seekTo(5000); + + AddStep("pause", () => Player.Pause()); + AddStep("release X", () => InputManager.ReleaseKey(Key.X)); + + AddStep("resume", () => Player.Resume()); + AddStep("go to resume cursor", () => InputManager.MoveMouseTo(this.ChildrenOfType().Single())); + AddStep("press Z to resume", () => InputManager.PressKey(Key.Z)); + AddStep("release Z", () => InputManager.ReleaseKey(Key.Z)); + + AddAssert("circle not hit", () => Player.ScoreProcessor.HighestCombo.Value, () => Is.EqualTo(1)); + + AddStep("press X", () => InputManager.PressKey(Key.X)); + AddStep("release X", () => InputManager.ReleaseKey(Key.X)); + + AddAssert("circle hit", () => Player.ScoreProcessor.HighestCombo.Value, () => Is.EqualTo(2)); + } + private void loadPlayer(Func createRuleset) { AddStep("set ruleset", () => currentRuleset = createRuleset()); From 373ff47a94ac29fed06f5c49dd6d5ff438e8fe74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 20 Aug 2024 09:53:40 +0200 Subject: [PATCH 2005/2493] Remove dead row attribute classes These aren't shown on the control point table since difficulty and sample control points were moved into objects. --- .../Screens/Edit/Timing/ControlPointTable.cs | 6 -- .../RowAttributes/DifficultyRowAttribute.cs | 44 -------------- .../RowAttributes/SampleRowAttribute.cs | 57 ------------------- 3 files changed, 107 deletions(-) delete mode 100644 osu.Game/Screens/Edit/Timing/RowAttributes/DifficultyRowAttribute.cs delete mode 100644 osu.Game/Screens/Edit/Timing/RowAttributes/SampleRowAttribute.cs diff --git a/osu.Game/Screens/Edit/Timing/ControlPointTable.cs b/osu.Game/Screens/Edit/Timing/ControlPointTable.cs index 2204fabf57..8dc0ced30e 100644 --- a/osu.Game/Screens/Edit/Timing/ControlPointTable.cs +++ b/osu.Game/Screens/Edit/Timing/ControlPointTable.cs @@ -323,14 +323,8 @@ namespace osu.Game.Screens.Edit.Timing case TimingControlPoint timing: return new TimingRowAttribute(timing); - case DifficultyControlPoint difficulty: - return new DifficultyRowAttribute(difficulty); - case EffectControlPoint effect: return new EffectRowAttribute(effect); - - case SampleControlPoint sample: - return new SampleRowAttribute(sample); } throw new ArgumentOutOfRangeException(nameof(controlPoint), $"Control point type {controlPoint.GetType()} is not supported"); diff --git a/osu.Game/Screens/Edit/Timing/RowAttributes/DifficultyRowAttribute.cs b/osu.Game/Screens/Edit/Timing/RowAttributes/DifficultyRowAttribute.cs deleted file mode 100644 index 43f3739503..0000000000 --- a/osu.Game/Screens/Edit/Timing/RowAttributes/DifficultyRowAttribute.cs +++ /dev/null @@ -1,44 +0,0 @@ -// 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.Bindables; -using osu.Framework.Graphics; -using osu.Game.Beatmaps.ControlPoints; -using osu.Game.Graphics.Sprites; - -namespace osu.Game.Screens.Edit.Timing.RowAttributes -{ - public partial class DifficultyRowAttribute : RowAttribute - { - private readonly BindableNumber speedMultiplier; - - private OsuSpriteText text = null!; - - public DifficultyRowAttribute(DifficultyControlPoint difficulty) - : base(difficulty, "difficulty") - { - speedMultiplier = difficulty.SliderVelocityBindable.GetBoundCopy(); - } - - [BackgroundDependencyLoader] - private void load() - { - Content.AddRange(new Drawable[] - { - new AttributeProgressBar(Point) - { - Current = speedMultiplier, - }, - text = new AttributeText(Point) - { - Width = 45, - }, - }); - - speedMultiplier.BindValueChanged(_ => updateText(), true); - } - - private void updateText() => text.Text = $"{speedMultiplier.Value:n2}x"; - } -} diff --git a/osu.Game/Screens/Edit/Timing/RowAttributes/SampleRowAttribute.cs b/osu.Game/Screens/Edit/Timing/RowAttributes/SampleRowAttribute.cs deleted file mode 100644 index e86a991521..0000000000 --- a/osu.Game/Screens/Edit/Timing/RowAttributes/SampleRowAttribute.cs +++ /dev/null @@ -1,57 +0,0 @@ -// 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.Bindables; -using osu.Framework.Graphics; -using osu.Game.Beatmaps.ControlPoints; -using osu.Game.Graphics.Sprites; - -namespace osu.Game.Screens.Edit.Timing.RowAttributes -{ - public partial class SampleRowAttribute : RowAttribute - { - private AttributeText sampleText = null!; - private OsuSpriteText volumeText = null!; - - private readonly Bindable sampleBank; - private readonly BindableNumber volume; - - public SampleRowAttribute(SampleControlPoint sample) - : base(sample, "sample") - { - sampleBank = sample.SampleBankBindable.GetBoundCopy(); - volume = sample.SampleVolumeBindable.GetBoundCopy(); - } - - [BackgroundDependencyLoader] - private void load() - { - AttributeProgressBar progress; - - Content.AddRange(new Drawable[] - { - sampleText = new AttributeText(Point), - progress = new AttributeProgressBar(Point), - volumeText = new AttributeText(Point) - { - Width = 40, - }, - }); - - volume.BindValueChanged(vol => - { - progress.Current.Value = vol.NewValue / 100f; - updateText(); - }, true); - - sampleBank.BindValueChanged(_ => updateText(), true); - } - - private void updateText() - { - volumeText.Text = $"{volume.Value}%"; - sampleText.Text = $"{sampleBank.Value}"; - } - } -} From c85b04bca5854e3f6cab6bf79aca17de1a2d1d77 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 20 Aug 2024 17:11:22 +0900 Subject: [PATCH 2006/2493] Add more test coverage to better show overlapping break / kiai sections --- .../Visual/Editing/TestSceneEditorSummaryTimeline.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorSummaryTimeline.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorSummaryTimeline.cs index ddca2f8553..677d3135ba 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorSummaryTimeline.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorSummaryTimeline.cs @@ -24,7 +24,10 @@ namespace osu.Game.Tests.Visual.Editing beatmap.ControlPointInfo.Add(100000, new TimingControlPoint { BeatLength = 100 }); beatmap.ControlPointInfo.Add(50000, new DifficultyControlPoint { SliderVelocity = 2 }); + beatmap.ControlPointInfo.Add(80000, new EffectControlPoint { KiaiMode = true }); + beatmap.ControlPointInfo.Add(110000, new EffectControlPoint { KiaiMode = false }); beatmap.BeatmapInfo.Bookmarks = new[] { 75000, 125000 }; + beatmap.Breaks.Add(new ManualBreakPeriod(90000, 120000)); editorBeatmap = new EditorBeatmap(beatmap); } From bccc797bcb0ac6598af5ac4145d71cb9b84664cd Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 20 Aug 2024 17:45:37 +0900 Subject: [PATCH 2007/2493] Move break display to background of summary timeline --- .../Components/Timelines/Summary/Parts/BreakPart.cs | 6 +++--- .../Components/Timelines/Summary/SummaryTimeline.cs | 13 ++++++------- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/BreakPart.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/BreakPart.cs index 3cff976f72..be3a7b7268 100644 --- a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/BreakPart.cs +++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/BreakPart.cs @@ -69,9 +69,9 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts RelativePositionAxes = Axes.X; RelativeSizeAxes = Axes.Both; - InternalChild = new Circle { RelativeSizeAxes = Axes.Both }; - Colour = colours.Gray7; - Alpha = 0.8f; + InternalChild = new Box { RelativeSizeAxes = Axes.Both }; + Colour = colours.Gray5; + Alpha = 0.4f; } public LocalisableString TooltipText => $"{breakPeriod.StartTime.ToEditorFormattedString()} - {breakPeriod.EndTime.ToEditorFormattedString()} break time"; diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/SummaryTimeline.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/SummaryTimeline.cs index a495442c1d..4ab7c88178 100644 --- a/osu.Game/Screens/Edit/Components/Timelines/Summary/SummaryTimeline.cs +++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/SummaryTimeline.cs @@ -59,6 +59,12 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary RelativeSizeAxes = Axes.Both, Height = 0.4f, }, + new BreakPart + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + }, new ControlPointPart { Anchor = Anchor.Centre, @@ -73,13 +79,6 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary RelativeSizeAxes = Axes.Both, Height = 0.4f }, - new BreakPart - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Both, - Height = 0.15f - }, new MarkerPart { RelativeSizeAxes = Axes.Both }, }; } From 73f2f5cb1268f39ca91a729050ba248c8c62689e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 20 Aug 2024 17:59:55 +0900 Subject: [PATCH 2008/2493] Fix more tests --- osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs | 2 ++ osu.Game.Tests/Visual/Gameplay/TestScenePause.cs | 2 ++ 2 files changed, 4 insertions(+) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs index 16b2a54a45..91f22a291c 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs @@ -174,6 +174,7 @@ namespace osu.Game.Tests.Visual.Gameplay holdForMenu.Action += () => activated = true; }); + AddStep("set hold button always visible", () => localConfig.SetValue(OsuSetting.AlwaysShowHoldForMenuButton, true)); AddStep("set showhud false", () => hudOverlay.ShowHud.Value = false); AddUntilStep("hidetarget is hidden", () => hideTarget.Alpha, () => Is.LessThanOrEqualTo(0)); @@ -214,6 +215,7 @@ namespace osu.Game.Tests.Visual.Gameplay progress.ChildrenOfType().Single().OnSeek += _ => seeked = true; }); + AddStep("set hold button always visible", () => localConfig.SetValue(OsuSetting.AlwaysShowHoldForMenuButton, true)); AddStep("set showhud false", () => hudOverlay.ShowHud.Value = false); AddUntilStep("hidetarget is hidden", () => hideTarget.Alpha, () => Is.LessThanOrEqualTo(0)); diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs index 030f2592ed..6aa2c4e40d 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs @@ -320,6 +320,8 @@ namespace osu.Game.Tests.Visual.Gameplay [Test] public void TestExitViaHoldToExit() { + AddStep("set hold button always visible", () => LocalConfig.SetValue(OsuSetting.AlwaysShowHoldForMenuButton, true)); + AddStep("exit", () => { InputManager.MoveMouseTo(Player.HUDOverlay.HoldToQuit.First(c => c is HoldToConfirmContainer)); From a33294ac42717717c5fd603bea2d92fdca18ed50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 20 Aug 2024 11:14:42 +0200 Subject: [PATCH 2009/2493] Redesign timing table tracking - On entering the screen, the timing point active at the current instant of the map is selected. This is the *only* time where the selected point is changed automatically for the user. - The ongoing automatic tracking of the relevant point after the initial selection is *gone*. Even knowing the fact that it was supposed to track the supposedly relevant "last selected type" of control point, I always found the tracking to be fairly arbitrary in how it works. Removing this behaviour also incidentally fixes https://github.com/ppy/osu/issues/23147. In its stead, to indicate which timing groups are having an effect, they receive an indicator line on the left (coloured using the relevant control points' representing colours), as well as a slight highlight effect. - If there is no control point selected, the table will autoscroll to the latest timing group, unless the user manually scrolled the table before. - If the selected control point changes, the table will autoscroll to the newly selected point, *regardless* of whether the user manually scrolled the table before. - A new button is added which permits the user to select the latest timing group. As per the point above, this will autoscroll the user to that group at the same time. --- .../Screens/Edit/Timing/ControlPointList.cs | 83 +++--------- .../Screens/Edit/Timing/ControlPointTable.cs | 126 ++++++++++++++---- 2 files changed, 117 insertions(+), 92 deletions(-) diff --git a/osu.Game/Screens/Edit/Timing/ControlPointList.cs b/osu.Game/Screens/Edit/Timing/ControlPointList.cs index b7367dddda..4df52a0a3a 100644 --- a/osu.Game/Screens/Edit/Timing/ControlPointList.cs +++ b/osu.Game/Screens/Edit/Timing/ControlPointList.cs @@ -11,7 +11,6 @@ using osu.Framework.Input.Events; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterfaceV2; -using osu.Game.Overlays; using osuTK; namespace osu.Game.Screens.Edit.Timing @@ -31,7 +30,7 @@ namespace osu.Game.Screens.Edit.Timing private Bindable selectedGroup { get; set; } = null!; [BackgroundDependencyLoader] - private void load(OverlayColourProvider colours) + private void load() { RelativeSizeAxes = Axes.Both; @@ -68,6 +67,14 @@ namespace osu.Game.Screens.Edit.Timing Anchor = Anchor.BottomRight, Origin = Anchor.BottomRight, }, + new RoundedButton + { + Text = "Go to current time", + Action = goToCurrentGroup, + Size = new Vector2(140, 30), + Anchor = Anchor.BottomRight, + Origin = Anchor.BottomRight, + }, } }, }; @@ -97,78 +104,18 @@ namespace osu.Game.Screens.Edit.Timing { base.Update(); - trackActivePoint(); - addButton.Enabled.Value = clock.CurrentTimeAccurate != selectedGroup.Value?.Time; } - private Type? trackedType; - - /// - /// Given the user has selected a control point group, we want to track any group which is - /// active at the current point in time which matches the type the user has selected. - /// - /// So if the user is currently looking at a timing point and seeks into the future, a - /// future timing point would be automatically selected if it is now the new "current" point. - /// - private void trackActivePoint() + private void goToCurrentGroup() { - // For simplicity only match on the first type of the active control point. - if (selectedGroup.Value == null) - trackedType = null; - else - { - switch (selectedGroup.Value.ControlPoints.Count) - { - // If the selected group has no control points, clear the tracked type. - // Otherwise the user will be unable to select a group with no control points. - case 0: - trackedType = null; - break; + double accurateTime = clock.CurrentTimeAccurate; - // If the selected group only has one control point, update the tracking type. - case 1: - trackedType = selectedGroup.Value?.ControlPoints[0].GetType(); - break; + var activeTimingPoint = Beatmap.ControlPointInfo.TimingPointAt(accurateTime); + var activeEffectPoint = Beatmap.ControlPointInfo.EffectPointAt(accurateTime); - // If the selected group has more than one control point, choose the first as the tracking type - // if we don't already have a singular tracked type. - default: - trackedType ??= selectedGroup.Value?.ControlPoints[0].GetType(); - break; - } - } - - if (trackedType != null) - { - double accurateTime = clock.CurrentTimeAccurate; - - // We don't have an efficient way of looking up groups currently, only individual point types. - // To improve the efficiency of this in the future, we should reconsider the overall structure of ControlPointInfo. - - // Find the next group which has the same type as the selected one. - ControlPointGroup? found = null; - - for (int i = 0; i < Beatmap.ControlPointInfo.Groups.Count; i++) - { - var g = Beatmap.ControlPointInfo.Groups[i]; - - if (g.Time > accurateTime) - continue; - - for (int j = 0; j < g.ControlPoints.Count; j++) - { - if (g.ControlPoints[j].GetType() == trackedType) - { - found = g; - break; - } - } - } - - if (found != null) - selectedGroup.Value = found; - } + double latestActiveTime = Math.Max(activeTimingPoint.Time, activeEffectPoint.Time); + selectedGroup.Value = Beatmap.ControlPointInfo.GroupAt(latestActiveTime); } private void delete() diff --git a/osu.Game/Screens/Edit/Timing/ControlPointTable.cs b/osu.Game/Screens/Edit/Timing/ControlPointTable.cs index 8dc0ced30e..501d8c0e41 100644 --- a/osu.Game/Screens/Edit/Timing/ControlPointTable.cs +++ b/osu.Game/Screens/Edit/Timing/ControlPointTable.cs @@ -6,6 +6,7 @@ using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Pooling; using osu.Framework.Graphics.Shapes; @@ -27,10 +28,27 @@ namespace osu.Game.Screens.Edit.Timing { public BindableList Groups { get; } = new BindableList(); + [Cached] + private Bindable activeTimingPoint { get; } = new Bindable(); + + [Cached] + private Bindable activeEffectPoint { get; } = new Bindable(); + + [Resolved] + private EditorBeatmap beatmap { get; set; } = null!; + + [Resolved] + private Bindable selectedGroup { get; set; } = null!; + + [Resolved] + private EditorClock editorClock { get; set; } = null!; + private const float timing_column_width = 300; private const float row_height = 25; private const float row_horizontal_padding = 20; + private ControlPointRowList list = null!; + [BackgroundDependencyLoader] private void load(OverlayColourProvider colours) { @@ -65,7 +83,7 @@ namespace osu.Game.Screens.Edit.Timing { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, - Margin = new MarginPadding { Left = ControlPointTable.timing_column_width } + Margin = new MarginPadding { Left = timing_column_width } }, } }, @@ -73,7 +91,7 @@ namespace osu.Game.Screens.Edit.Timing { RelativeSizeAxes = Axes.Both, Padding = new MarginPadding { Top = row_height }, - Child = new ControlPointRowList + Child = list = new ControlPointRowList { RelativeSizeAxes = Axes.Both, RowData = { BindTarget = Groups, }, @@ -82,40 +100,63 @@ namespace osu.Game.Screens.Edit.Timing }; } + protected override void LoadComplete() + { + base.LoadComplete(); + + selectedGroup.BindValueChanged(_ => scrollToMostRelevantRow(force: true), true); + } + + protected override void Update() + { + base.Update(); + + scrollToMostRelevantRow(force: false); + } + + private void scrollToMostRelevantRow(bool force) + { + double accurateTime = editorClock.CurrentTimeAccurate; + + activeTimingPoint.Value = beatmap.ControlPointInfo.TimingPointAt(accurateTime); + activeEffectPoint.Value = beatmap.ControlPointInfo.EffectPointAt(accurateTime); + + double latestActiveTime = Math.Max(activeTimingPoint.Value?.Time ?? double.NegativeInfinity, activeEffectPoint.Value?.Time ?? double.NegativeInfinity); + var groupToShow = selectedGroup.Value ?? beatmap.ControlPointInfo.GroupAt(latestActiveTime); + list.ScrollTo(groupToShow, force); + } + private partial class ControlPointRowList : VirtualisedListContainer { - [Resolved] - private Bindable selectedGroup { get; set; } = null!; - public ControlPointRowList() : base(row_height, 50) { } - protected override ScrollContainer CreateScrollContainer() => new OsuScrollContainer(); + protected override ScrollContainer CreateScrollContainer() => new UserTrackingScrollContainer(); - protected override void LoadComplete() + protected new UserTrackingScrollContainer Scroll => (UserTrackingScrollContainer)base.Scroll; + + public void ScrollTo(ControlPointGroup group, bool force) { - base.LoadComplete(); + if (Scroll.UserScrolling && !force) + return; - selectedGroup.BindValueChanged(val => - { - // can't use `.ScrollIntoView()` here because of the list virtualisation not giving - // child items valid coordinates from the start, so ballpark something similar - // using estimated row height. - var row = Items.FlowingChildren.SingleOrDefault(item => item.Row.Equals(val.NewValue)); + // can't use `.ScrollIntoView()` here because of the list virtualisation not giving + // child items valid coordinates from the start, so ballpark something similar + // using estimated row height. + var row = Items.FlowingChildren.SingleOrDefault(item => item.Row.Equals(group)); - if (row == null) - return; + if (row == null) + return; - float minPos = row.Y; - float maxPos = minPos + row_height; + float minPos = row.Y; + float maxPos = minPos + row_height; - if (minPos < Scroll.Current) - Scroll.ScrollTo(minPos); - else if (maxPos > Scroll.Current + Scroll.DisplayableContent) - Scroll.ScrollTo(maxPos - Scroll.DisplayableContent); - }); + if (minPos < Scroll.Current) + Scroll.ScrollTo(minPos); + else if (maxPos > Scroll.Current + Scroll.DisplayableContent) + Scroll.ScrollTo(maxPos - Scroll.DisplayableContent); } } @@ -130,13 +171,23 @@ namespace osu.Game.Screens.Edit.Timing private readonly BindableWithCurrent current = new BindableWithCurrent(); private Box background = null!; + private Box currentIndicator = null!; [Resolved] private OverlayColourProvider colourProvider { get; set; } = null!; + [Resolved] + private OsuColour colours { get; set; } = null!; + [Resolved] private Bindable selectedGroup { get; set; } = null!; + [Resolved] + private Bindable activeTimingPoint { get; set; } = null!; + + [Resolved] + private Bindable activeEffectPoint { get; set; } = null!; + [Resolved] private EditorClock editorClock { get; set; } = null!; @@ -153,6 +204,12 @@ namespace osu.Game.Screens.Edit.Timing Colour = colourProvider.Background1, Alpha = 0, }, + currentIndicator = new Box + { + RelativeSizeAxes = Axes.Y, + Width = 5, + Alpha = 0, + }, new Container { RelativeSizeAxes = Axes.Both, @@ -174,7 +231,9 @@ namespace osu.Game.Screens.Edit.Timing { base.LoadComplete(); - selectedGroup.BindValueChanged(_ => updateState(), true); + selectedGroup.BindValueChanged(_ => updateState()); + activeEffectPoint.BindValueChanged(_ => updateState()); + activeTimingPoint.BindValueChanged(_ => updateState(), true); FinishTransforms(true); } @@ -213,12 +272,31 @@ namespace osu.Game.Screens.Edit.Timing { bool isSelected = selectedGroup.Value?.Equals(current.Value) == true; + bool hasCurrentTimingPoint = activeTimingPoint.Value != null && current.Value.ControlPoints.Contains(activeTimingPoint.Value); + bool hasCurrentEffectPoint = activeEffectPoint.Value != null && current.Value.ControlPoints.Contains(activeEffectPoint.Value); + if (IsHovered || isSelected) background.FadeIn(100, Easing.OutQuint); + else if (hasCurrentTimingPoint || hasCurrentEffectPoint) + background.FadeTo(0.2f, 100, Easing.OutQuint); else background.FadeOut(100, Easing.OutQuint); background.Colour = isSelected ? colourProvider.Colour3 : colourProvider.Background1; + + if (hasCurrentTimingPoint || hasCurrentEffectPoint) + { + currentIndicator.FadeIn(100, Easing.OutQuint); + + if (hasCurrentTimingPoint && hasCurrentEffectPoint) + currentIndicator.Colour = ColourInfo.GradientVertical(activeTimingPoint.Value!.GetRepresentingColour(colours), activeEffectPoint.Value!.GetRepresentingColour(colours)); + else if (hasCurrentTimingPoint) + currentIndicator.Colour = activeTimingPoint.Value!.GetRepresentingColour(colours); + else + currentIndicator.Colour = activeEffectPoint.Value!.GetRepresentingColour(colours); + } + else + currentIndicator.FadeOut(100, Easing.OutQuint); } } From 333e5b8cac7aa19afac0014732325390dbcdb323 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 20 Aug 2024 11:23:39 +0200 Subject: [PATCH 2010/2493] Remove outdated tests --- .../Visual/Editing/TestSceneTimingScreen.cs | 34 ------------------- 1 file changed, 34 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneTimingScreen.cs b/osu.Game.Tests/Visual/Editing/TestSceneTimingScreen.cs index 6181024230..cf07ce2431 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneTimingScreen.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneTimingScreen.cs @@ -114,40 +114,6 @@ namespace osu.Game.Tests.Visual.Editing }); } - [Test] - public void TestTrackingCurrentTimeWhileRunning() - { - AddStep("Select first effect point", () => - { - InputManager.MoveMouseTo(Child.ChildrenOfType().First()); - InputManager.Click(MouseButton.Left); - }); - - AddUntilStep("Selection changed", () => timingScreen.SelectedGroup.Value.Time == 54670); - AddUntilStep("Ensure seeked to correct time", () => EditorClock.CurrentTimeAccurate == 54670); - - AddStep("Seek to just before next point", () => EditorClock.Seek(69000)); - AddStep("Start clock", () => EditorClock.Start()); - - AddUntilStep("Selection changed", () => timingScreen.SelectedGroup.Value.Time == 69670); - } - - [Test] - public void TestTrackingCurrentTimeWhilePaused() - { - AddStep("Select first effect point", () => - { - InputManager.MoveMouseTo(Child.ChildrenOfType().First()); - InputManager.Click(MouseButton.Left); - }); - - AddUntilStep("Selection changed", () => timingScreen.SelectedGroup.Value.Time == 54670); - AddUntilStep("Ensure seeked to correct time", () => EditorClock.CurrentTimeAccurate == 54670); - - AddStep("Seek to later", () => EditorClock.Seek(80000)); - AddUntilStep("Selection changed", () => timingScreen.SelectedGroup.Value.Time == 69670); - } - [Test] public void TestScrollControlGroupIntoView() { From 3202c77279b305c268eaed0d857fac252bae1ba7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 20 Aug 2024 12:36:05 +0200 Subject: [PATCH 2011/2493] Add failing test --- .../TestSceneHitObjectSampleAdjustments.cs | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneHitObjectSampleAdjustments.cs b/osu.Game.Tests/Visual/Editing/TestSceneHitObjectSampleAdjustments.cs index 75a68237c8..65eec740f0 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneHitObjectSampleAdjustments.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneHitObjectSampleAdjustments.cs @@ -548,6 +548,63 @@ namespace osu.Game.Tests.Visual.Editing hitObjectNodeHasSamples(2, 1, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_WHISTLE); } + [Test] + public void TestHotkeysUnifySliderSamplesAndNodeSamples() + { + AddStep("add slider", () => + { + EditorBeatmap.Clear(); + EditorBeatmap.Add(new Slider + { + Position = new Vector2(256, 256), + StartTime = 1000, + Path = new SliderPath(new[] { new PathControlPoint(Vector2.Zero), new PathControlPoint(new Vector2(250, 0)) }), + Samples = + { + new HitSampleInfo(HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT), + new HitSampleInfo(HitSampleInfo.HIT_WHISTLE, bank: HitSampleInfo.BANK_DRUM), + }, + NodeSamples = new List> + { + new List + { + new HitSampleInfo(HitSampleInfo.HIT_NORMAL, bank: HitSampleInfo.BANK_DRUM), + new HitSampleInfo(HitSampleInfo.HIT_CLAP, bank: HitSampleInfo.BANK_DRUM), + }, + new List + { + new HitSampleInfo(HitSampleInfo.HIT_NORMAL, bank: HitSampleInfo.BANK_SOFT), + new HitSampleInfo(HitSampleInfo.HIT_WHISTLE, bank: HitSampleInfo.BANK_SOFT), + }, + } + }); + }); + AddStep("select everything", () => EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects)); + + AddStep("set soft bank", () => + { + InputManager.PressKey(Key.LShift); + InputManager.Key(Key.E); + InputManager.ReleaseKey(Key.LShift); + }); + + hitObjectHasSampleBank(0, HitSampleInfo.BANK_SOFT); + hitObjectHasSamples(0, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_WHISTLE); + hitObjectNodeHasSampleBank(0, 0, HitSampleInfo.BANK_SOFT); + hitObjectNodeHasSamples(0, 0, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_CLAP); + hitObjectNodeHasSampleBank(0, 1, HitSampleInfo.BANK_SOFT); + hitObjectNodeHasSamples(0, 1, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_WHISTLE); + + AddStep("unify whistle addition", () => InputManager.Key(Key.W)); + + hitObjectHasSampleBank(0, HitSampleInfo.BANK_SOFT); + hitObjectHasSamples(0, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_WHISTLE); + hitObjectNodeHasSampleBank(0, 0, HitSampleInfo.BANK_SOFT); + hitObjectNodeHasSamples(0, 0, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_CLAP, HitSampleInfo.HIT_WHISTLE); + hitObjectNodeHasSampleBank(0, 1, HitSampleInfo.BANK_SOFT); + hitObjectNodeHasSamples(0, 1, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_WHISTLE); + } + [Test] public void TestSelectingObjectDoesNotMutateSamples() { From c9f1ef536136c6a639c38538d6f29b5414bf95d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 20 Aug 2024 12:36:13 +0200 Subject: [PATCH 2012/2493] Fix incorrect bank set / sample addition logic Closes https://github.com/ppy/osu/issues/29361. Typical case of a few early-returns gone wrong leading to `NodeSamples` not being checked correctly. --- .../Edit/Compose/Components/EditorSelectionHandler.cs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/EditorSelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/EditorSelectionHandler.cs index a4efe66bf8..472b48425f 100644 --- a/osu.Game/Screens/Edit/Compose/Components/EditorSelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/EditorSelectionHandler.cs @@ -229,7 +229,7 @@ namespace osu.Game.Screens.Edit.Compose.Components EditorBeatmap.PerformOnSelection(h => { - if (h.Samples.All(s => s.Bank == bankName)) + if (hasRelevantBank(h)) return; h.Samples = h.Samples.Select(s => s.With(newBank: bankName)).ToList(); @@ -269,10 +269,8 @@ namespace osu.Game.Screens.Edit.Compose.Components EditorBeatmap.PerformOnSelection(h => { // Make sure there isn't already an existing sample - if (h.Samples.Any(s => s.Name == sampleName)) - return; - - h.Samples.Add(h.CreateHitSampleInfo(sampleName)); + if (h.Samples.All(s => s.Name != sampleName)) + h.Samples.Add(h.CreateHitSampleInfo(sampleName)); if (h is IHasRepeats hasRepeats) { From bb964e32fa5a1e5f2aeb1b3f14308f9c85be02ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 20 Aug 2024 13:36:52 +0200 Subject: [PATCH 2013/2493] Fix crash on attempting to edit particular beatmaps Closes https://github.com/ppy/osu/issues/29492. I'm not immediately sure why this happened, but some old locally modified beatmaps in my local realm database have a `BeatDivisor` of 0 stored, which is then passed to `BindableBeatDivisor.SetArbitraryDivisor()`, which then blows up. To stop this from happening, just refuse to use values outside of a sane range. --- osu.Game/Screens/Edit/BindableBeatDivisor.cs | 10 +++++++++- .../Edit/Compose/Components/BeatDivisorControl.cs | 6 +++--- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Edit/BindableBeatDivisor.cs b/osu.Game/Screens/Edit/BindableBeatDivisor.cs index 4b0726658f..3bb1b4e079 100644 --- a/osu.Game/Screens/Edit/BindableBeatDivisor.cs +++ b/osu.Game/Screens/Edit/BindableBeatDivisor.cs @@ -16,6 +16,9 @@ namespace osu.Game.Screens.Edit { public static readonly int[] PREDEFINED_DIVISORS = { 1, 2, 3, 4, 6, 8, 12, 16 }; + public const int MINIMUM_DIVISOR = 1; + public const int MAXIMUM_DIVISOR = 64; + public Bindable ValidDivisors { get; } = new Bindable(BeatDivisorPresetCollection.COMMON); public BindableBeatDivisor(int value = 1) @@ -30,8 +33,12 @@ namespace osu.Game.Screens.Edit /// /// The intended divisor. /// Forces changing the valid divisors to a known preset. - public void SetArbitraryDivisor(int divisor, bool preferKnownPresets = false) + /// Whether the divisor was successfully set. + public bool SetArbitraryDivisor(int divisor, bool preferKnownPresets = false) { + if (divisor < MINIMUM_DIVISOR || divisor > MAXIMUM_DIVISOR) + return false; + // If the current valid divisor range doesn't contain the proposed value, attempt to find one which does. if (preferKnownPresets || !ValidDivisors.Value.Presets.Contains(divisor)) { @@ -44,6 +51,7 @@ namespace osu.Game.Screens.Edit } Value = divisor; + return true; } private void updateBindableProperties() diff --git a/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs b/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs index 1d8266d610..3c2a66b8bb 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs @@ -330,14 +330,14 @@ namespace osu.Game.Screens.Edit.Compose.Components private void setPresetsFromTextBoxEntry() { - if (!int.TryParse(divisorTextBox.Text, out int divisor) || divisor < 1 || divisor > 64) + if (!int.TryParse(divisorTextBox.Text, out int divisor) || !BeatDivisor.SetArbitraryDivisor(divisor)) { + // the text either didn't parse as a divisor, or the divisor was not set due to being out of range. + // force a state update to reset the text box's value to the last sane value. updateState(); return; } - BeatDivisor.SetArbitraryDivisor(divisor); - this.HidePopover(); } From c2dd2ad9783412d61a819805a42f1fa4a9dfd12a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 20 Aug 2024 13:40:57 +0200 Subject: [PATCH 2014/2493] Clamp beat divisor to sane range when decoding In my view this is a nice change, but do note that on its own it does nothing to fix https://github.com/ppy/osu/issues/29492, because of `BeatmapInfo` reference management foibles when opening the editor. See also: https://github.com/ppy/osu/issues/20883#issuecomment-1288149271, https://github.com/ppy/osu/pull/28473. --- osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs index 9418a389aa..b068c87fbb 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs @@ -16,6 +16,7 @@ using osu.Game.Rulesets; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Legacy; using osu.Game.Rulesets.Objects.Types; +using osu.Game.Screens.Edit; namespace osu.Game.Beatmaps.Formats { @@ -336,7 +337,7 @@ namespace osu.Game.Beatmaps.Formats break; case @"BeatDivisor": - beatmap.BeatmapInfo.BeatDivisor = Parsing.ParseInt(pair.Value); + beatmap.BeatmapInfo.BeatDivisor = Math.Clamp(Parsing.ParseInt(pair.Value), BindableBeatDivisor.MINIMUM_DIVISOR, BindableBeatDivisor.MAXIMUM_DIVISOR); break; case @"GridSize": From 2011d5525f7aab8fa1809d16f8801dffaa507f51 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 20 Aug 2024 22:21:10 +0900 Subject: [PATCH 2015/2493] Add flaky test attribute to some tests See occurences like https://github.com/ppy/osu/actions/runs/10471058714. --- osu.Game.Tests/Visual/Gameplay/TestSceneLeadIn.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneLeadIn.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneLeadIn.cs index 5a71369976..5af7540f6f 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneLeadIn.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneLeadIn.cs @@ -27,6 +27,7 @@ namespace osu.Game.Tests.Visual.Gameplay [TestCase(2000, 0)] [TestCase(3000, first_hit_object - 3000)] [TestCase(10000, first_hit_object - 10000)] + [FlakyTest] public void TestLeadInProducesCorrectStartTime(double leadIn, double expectedStartTime) { loadPlayerWithBeatmap(new TestBeatmap(new OsuRuleset().RulesetInfo) @@ -41,6 +42,7 @@ namespace osu.Game.Tests.Visual.Gameplay [TestCase(0, 0)] [TestCase(-1000, -1000)] [TestCase(-10000, -10000)] + [FlakyTest] public void TestStoryboardProducesCorrectStartTimeSimpleAlpha(double firstStoryboardEvent, double expectedStartTime) { var storyboard = new Storyboard(); @@ -64,6 +66,7 @@ namespace osu.Game.Tests.Visual.Gameplay [TestCase(0, 0, true)] [TestCase(-1000, -1000, true)] [TestCase(-10000, -10000, true)] + [FlakyTest] public void TestStoryboardProducesCorrectStartTimeFadeInAfterOtherEvents(double firstStoryboardEvent, double expectedStartTime, bool addEventToLoop) { const double loop_start_time = -20000; From 8e273709f12b58af01d7b6711ba7be11f17010c9 Mon Sep 17 00:00:00 2001 From: jkh675 Date: Tue, 20 Aug 2024 22:48:11 +0800 Subject: [PATCH 2016/2493] Implement copy url in beatmap and beatmap set carousel --- .../Select/Carousel/DrawableCarouselBeatmap.cs | 9 ++++++++- .../Select/Carousel/DrawableCarouselBeatmapSet.cs | 11 ++++++++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs index f725d98342..70c82576cc 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs @@ -17,6 +17,7 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; +using osu.Framework.Platform; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Drawables; using osu.Game.Collections; @@ -25,6 +26,7 @@ using osu.Game.Graphics; using osu.Game.Graphics.Backgrounds; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; +using osu.Game.Online.API; using osu.Game.Overlays; using osu.Game.Resources.Localisation.Web; using osu.Game.Rulesets; @@ -53,6 +55,7 @@ namespace osu.Game.Screens.Select.Carousel private Action? selectRequested; private Action? hideRequested; + private Action? copyBeatmapSetUrl; private Triangles triangles = null!; @@ -89,7 +92,7 @@ namespace osu.Game.Screens.Select.Carousel } [BackgroundDependencyLoader] - private void load(BeatmapManager? manager, SongSelect? songSelect) + private void load(BeatmapManager? manager, SongSelect? songSelect, Clipboard clipboard, IAPIProvider api) { Header.Height = height; @@ -102,6 +105,8 @@ namespace osu.Game.Screens.Select.Carousel if (manager != null) hideRequested = manager.Hide; + copyBeatmapSetUrl += () => clipboard.SetText($@"{api.WebsiteRootUrl}/beatmapsets/{beatmapInfo.BeatmapSet.OnlineID}#{beatmapInfo.Ruleset.ShortName}/{beatmapInfo.OnlineID}"); + Header.Children = new Drawable[] { background = new Box @@ -288,6 +293,8 @@ namespace osu.Game.Screens.Select.Carousel items.Add(new OsuMenuItem("Collections") { Items = collectionItems }); + items.Add(new OsuMenuItem("Copy URL", MenuItemType.Standard, () => copyBeatmapSetUrl?.Invoke())); + if (hideRequested != null) items.Add(new OsuMenuItem(CommonStrings.ButtonsHide.ToSentence(), MenuItemType.Destructive, () => hideRequested(beatmapInfo))); diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs index bd659d7423..12db8f663a 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs @@ -8,18 +8,22 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.UserInterface; +using osu.Framework.Platform; using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Collections; using osu.Game.Database; using osu.Game.Graphics.UserInterface; +using osu.Game.Online.API; using osu.Game.Overlays; +using osu.Game.Rulesets; namespace osu.Game.Screens.Select.Carousel { @@ -29,6 +33,7 @@ namespace osu.Game.Screens.Select.Carousel private Action restoreHiddenRequested = null!; private Action? viewDetails; + private Action? copyBeatmapSetUrl; [Resolved] private IDialogOverlay? dialogOverlay { get; set; } @@ -65,7 +70,7 @@ namespace osu.Game.Screens.Select.Carousel } [BackgroundDependencyLoader] - private void load(BeatmapSetOverlay? beatmapOverlay, SongSelect? songSelect) + private void load(BeatmapSetOverlay? beatmapOverlay, SongSelect? songSelect, Clipboard clipboard, IBindable ruleset, IAPIProvider api) { if (songSelect != null) mainMenuItems = songSelect.CreateForwardNavigationMenuItemsForBeatmap(() => (((CarouselBeatmapSet)Item!).GetNextToSelect() as CarouselBeatmap)!.BeatmapInfo); @@ -78,6 +83,8 @@ namespace osu.Game.Screens.Select.Carousel if (beatmapOverlay != null) viewDetails = beatmapOverlay.FetchAndShowBeatmapSet; + + copyBeatmapSetUrl += () => clipboard.SetText($@"{api.WebsiteRootUrl}/beatmapsets/{beatmapSet.OnlineID}#{ruleset.Value.ShortName}"); } protected override void Update() @@ -287,6 +294,8 @@ namespace osu.Game.Screens.Select.Carousel if (beatmapSet.Beatmaps.Any(b => b.Hidden)) items.Add(new OsuMenuItem("Restore all hidden", MenuItemType.Standard, () => restoreHiddenRequested(beatmapSet))); + items.Add(new OsuMenuItem("Copy URL", MenuItemType.Standard, () => copyBeatmapSetUrl?.Invoke())); + if (dialogOverlay != null) items.Add(new OsuMenuItem("Delete...", MenuItemType.Destructive, () => dialogOverlay.Push(new BeatmapDeleteDialog(beatmapSet)))); return items.ToArray(); From 20658ef4eeebbf3d09515c777305ed145a9646b3 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 21 Aug 2024 00:02:05 +0900 Subject: [PATCH 2017/2493] Fix legacy key counter position not matching stable --- .../Skinning/Legacy/CatchLegacySkinTransformer.cs | 6 ++---- .../Skinning/Legacy/OsuLegacySkinTransformer.cs | 6 ++---- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs b/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs index 81279456d5..f3626eb55d 100644 --- a/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs @@ -56,10 +56,8 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy { // set the anchor to top right so that it won't squash to the return button to the top keyCounter.Anchor = Anchor.CentreRight; - keyCounter.Origin = Anchor.CentreRight; - keyCounter.X = 0; - // 340px is the default height inherit from stable - keyCounter.Y = container.ToLocalSpace(new Vector2(0, container.ScreenSpaceDrawQuad.Centre.Y - 340f)).Y; + keyCounter.Origin = Anchor.TopRight; + keyCounter.Position = new Vector2(0, -40) * 1.6f; } }) { diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs index 491eb02e26..457c191583 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs @@ -69,10 +69,8 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy { // set the anchor to top right so that it won't squash to the return button to the top keyCounter.Anchor = Anchor.CentreRight; - keyCounter.Origin = Anchor.CentreRight; - keyCounter.X = 0; - // 340px is the default height inherit from stable - keyCounter.Y = container.ToLocalSpace(new Vector2(0, container.ScreenSpaceDrawQuad.Centre.Y - 340f)).Y; + keyCounter.Origin = Anchor.TopRight; + keyCounter.Position = new Vector2(0, -40) * 1.6f; } var combo = container.OfType().FirstOrDefault(); From 0d358a1dae593b83cf8e871b838de09880f848e8 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 21 Aug 2024 02:53:11 +0900 Subject: [PATCH 2018/2493] Fix resume overlay appearing behind HUD/skip overlays --- osu.Game/Screens/Play/Player.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 9a3d83782f..f362373b24 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -442,7 +442,6 @@ namespace osu.Game.Screens.Play }, // display the cursor above some HUD elements. DrawableRuleset.Cursor?.CreateProxy() ?? new Container(), - DrawableRuleset.ResumeOverlay?.CreateProxy() ?? new Container(), HUDOverlay = new HUDOverlay(DrawableRuleset, GameplayState.Mods, Configuration.AlwaysShowLeaderboard) { HoldToQuit = @@ -470,6 +469,7 @@ namespace osu.Game.Screens.Play RequestSkip = () => progressToResults(false), Alpha = 0 }, + DrawableRuleset.ResumeOverlay?.CreateProxy() ?? new Container(), PauseOverlay = new PauseOverlay { OnResume = Resume, From ae4fefeba15d0a64371d6def3da9ced22c65d607 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 21 Aug 2024 03:22:03 +0900 Subject: [PATCH 2019/2493] Add failing test case --- .../TestSceneModCustomisationPanel.cs | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModCustomisationPanel.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModCustomisationPanel.cs index c2739e1bbd..0d8ea05612 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModCustomisationPanel.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModCustomisationPanel.cs @@ -7,6 +7,8 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Testing; +using osu.Game.Graphics.UserInterface; using osu.Game.Overlays; using osu.Game.Overlays.Mods; using osu.Game.Rulesets.Mods; @@ -157,6 +159,27 @@ namespace osu.Game.Tests.Visual.UserInterface checkExpanded(false); } + [Test] + public void TestDraggingKeepsPanelExpanded() + { + AddStep("add customisable mod", () => + { + SelectedMods.Value = new[] { new OsuModDoubleTime() }; + panel.Enabled.Value = true; + }); + + AddStep("hover header", () => InputManager.MoveMouseTo(header)); + checkExpanded(true); + + AddStep("hover slider bar nub", () => InputManager.MoveMouseTo(panel.ChildrenOfType>().First().ChildrenOfType().Single())); + AddStep("hold", () => InputManager.PressButton(MouseButton.Left)); + AddStep("drag outside", () => InputManager.MoveMouseTo(Vector2.Zero)); + checkExpanded(true); + + AddStep("release", () => InputManager.ReleaseButton(MouseButton.Left)); + checkExpanded(false); + } + private void checkExpanded(bool expanded) { AddUntilStep(expanded ? "is expanded" : "not expanded", () => panel.ExpandedState.Value, From b7599dd1f830d5b5c617c025ba8b86893e368da5 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 21 Aug 2024 03:23:23 +0900 Subject: [PATCH 2020/2493] Keep mod customisation panel open when dragging a drawable --- .../Overlays/Mods/ModCustomisationPanel.cs | 25 +++++++++++++------ 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModCustomisationPanel.cs b/osu.Game/Overlays/Mods/ModCustomisationPanel.cs index 75cd5d6c91..91d7fdda73 100644 --- a/osu.Game/Overlays/Mods/ModCustomisationPanel.cs +++ b/osu.Game/Overlays/Mods/ModCustomisationPanel.cs @@ -11,6 +11,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Shapes; +using osu.Framework.Input; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Game.Configuration; @@ -214,15 +215,23 @@ namespace osu.Game.Overlays.Mods this.panel = panel; } - protected override void OnHoverLost(HoverLostEvent e) - { - if (ExpandedState.Value is ModCustomisationPanelState.ExpandedByHover - && !ReceivePositionalInputAt(e.ScreenSpaceMousePosition)) - { - ExpandedState.Value = ModCustomisationPanelState.Collapsed; - } + private InputManager? inputManager; - base.OnHoverLost(e); + protected override void LoadComplete() + { + base.LoadComplete(); + inputManager = GetContainingInputManager(); + } + + protected override void Update() + { + base.Update(); + + if (ExpandedState.Value == ModCustomisationPanelState.ExpandedByHover) + { + if (!ReceivePositionalInputAt(inputManager!.CurrentState.Mouse.Position) && inputManager.DraggedDrawable == null) + ExpandedState.Value = ModCustomisationPanelState.Collapsed; + } } } From 637c9aeef0c4879c3ad8e5adbf8b7fcfe9c21472 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 21 Aug 2024 03:35:26 +0900 Subject: [PATCH 2021/2493] Add `DailyChallengeIntroPlayed` session static --- osu.Game/Configuration/SessionStatics.cs | 6 ++++++ osu.Game/Screens/Menu/DailyChallengeButton.cs | 7 +++++++ osu.Game/Screens/Menu/MainMenu.cs | 5 ++++- .../OnlinePlay/DailyChallenge/DailyChallengeIntro.cs | 5 +++++ 4 files changed, 22 insertions(+), 1 deletion(-) diff --git a/osu.Game/Configuration/SessionStatics.cs b/osu.Game/Configuration/SessionStatics.cs index 1548b781a7..225f209380 100644 --- a/osu.Game/Configuration/SessionStatics.cs +++ b/osu.Game/Configuration/SessionStatics.cs @@ -80,5 +80,11 @@ namespace osu.Game.Configuration /// Stores the local user's last score (can be completed or aborted). /// LastLocalUserScore, + + /// + /// Whether the intro animation for the daily challenge screen has been played once. + /// This is reset when a new challenge is up. + /// + DailyChallengeIntroPlayed, } } diff --git a/osu.Game/Screens/Menu/DailyChallengeButton.cs b/osu.Game/Screens/Menu/DailyChallengeButton.cs index e6593c9b0d..d47866ef73 100644 --- a/osu.Game/Screens/Menu/DailyChallengeButton.cs +++ b/osu.Game/Screens/Menu/DailyChallengeButton.cs @@ -13,6 +13,7 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Threading; using osu.Framework.Utils; using osu.Game.Beatmaps.Drawables; +using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Localisation; @@ -46,6 +47,9 @@ namespace osu.Game.Screens.Menu [Resolved] private INotificationOverlay? notificationOverlay { get; set; } + [Resolved] + private SessionStatics statics { get; set; } = null!; + public DailyChallengeButton(string sampleName, Color4 colour, Action? clickAction = null, params Key[] triggerKeys) : base(ButtonSystemStrings.DailyChallenge, sampleName, OsuIcon.DailyChallenge, colour, clickAction, triggerKeys) { @@ -148,6 +152,9 @@ namespace osu.Game.Screens.Menu roomRequest.Success += room => { + // force showing intro on the first time when a new daily challenge is up. + statics.SetValue(Static.DailyChallengeIntroPlayed, false); + Room = room; cover.OnlineInfo = TooltipContent = room.Playlist.FirstOrDefault()?.Beatmap.BeatmapSet as APIBeatmapSet; diff --git a/osu.Game/Screens/Menu/MainMenu.cs b/osu.Game/Screens/Menu/MainMenu.cs index dfe5460aee..64a173e088 100644 --- a/osu.Game/Screens/Menu/MainMenu.cs +++ b/osu.Game/Screens/Menu/MainMenu.cs @@ -150,7 +150,10 @@ namespace osu.Game.Screens.Menu OnPlaylists = () => this.Push(new Playlists()), OnDailyChallenge = room => { - this.Push(new DailyChallengeIntro(room)); + if (statics.Get(Static.DailyChallengeIntroPlayed)) + this.Push(new DailyChallenge(room)); + else + this.Push(new DailyChallengeIntro(room)); }, OnExit = () => { diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeIntro.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeIntro.cs index e59031f663..619e7c1e42 100644 --- a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeIntro.cs +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeIntro.cs @@ -70,6 +70,9 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge [Resolved] private MusicController musicController { get; set; } = null!; + [Resolved] + private SessionStatics statics { get; set; } = null!; + private Sample? dateWindupSample; private Sample? dateImpactSample; private Sample? beatmapWindupSample; @@ -461,6 +464,8 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge { Schedule(() => { + statics.SetValue(Static.DailyChallengeIntroPlayed, true); + if (this.IsCurrentScreen()) this.Push(new DailyChallenge(room)); }); From 1ce9e97fd45bb81f13a8e6a799af43d6342922af Mon Sep 17 00:00:00 2001 From: OliBomby Date: Tue, 20 Aug 2024 23:38:38 +0200 Subject: [PATCH 2022/2493] add arrow indicator --- .../Edit/Compose/Components/Timeline/SamplePointPiece.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs index 6cd7044943..9c42d072d1 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs @@ -12,6 +12,7 @@ using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; +using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; using osu.Game.Audio; @@ -165,6 +166,13 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline [Resolved(canBeNull: true)] private EditorBeatmap beatmap { get; set; } = null!; + protected override Drawable CreateArrow() => new Triangle + { + Size = new Vector2(20), + Anchor = Anchor.Centre, + Origin = Anchor.TopCentre, + }; + public SampleEditPopover(HitObject hitObject) { this.hitObject = hitObject; From 09ca190b8ddb44d27b16763f6a6c19d71332e001 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Wed, 21 Aug 2024 00:10:15 +0200 Subject: [PATCH 2023/2493] re-implement ConvexHull 100% original --- osu.Game/Utils/GeometryUtils.cs | 39 +++++++++++++++++++-------------- 1 file changed, 22 insertions(+), 17 deletions(-) diff --git a/osu.Game/Utils/GeometryUtils.cs b/osu.Game/Utils/GeometryUtils.cs index 810eda9950..d4c1dc2db7 100644 --- a/osu.Game/Utils/GeometryUtils.cs +++ b/osu.Game/Utils/GeometryUtils.cs @@ -154,33 +154,38 @@ namespace osu.Game.Utils { List p = points.ToList(); - if (p.Count <= 1) + if (p.Count < 3) return p; - int n = p.Count, k = 0; - List hull = new List(new Vector2[2 * n]); - p.Sort((a, b) => a.X == b.X ? a.Y.CompareTo(b.Y) : a.X.CompareTo(b.X)); - // Build lower hull - for (int i = 0; i < n; ++i) + List upper = new List(); + List lower = new List(); + + // Build the lower hull + for (int i = 0; i < p.Count; i++) { - while (k >= 2 && cross(hull[k - 2], hull[k - 1], p[i]) <= 0) - k--; - hull[k] = p[i]; - k++; + while (lower.Count >= 2 && cross(lower[^2], lower[^1], p[i]) <= 0) + lower.RemoveAt(lower.Count - 1); + + lower.Add(p[i]); } - // Build upper hull - for (int i = n - 2, t = k + 1; i >= 0; i--) + // Build the upper hull + for (int i = p.Count - 1; i >= 0; i--) { - while (k >= t && cross(hull[k - 2], hull[k - 1], p[i]) <= 0) - k--; - hull[k] = p[i]; - k++; + while (upper.Count >= 2 && cross(upper[^2], upper[^1], p[i]) <= 0) + upper.RemoveAt(upper.Count - 1); + + upper.Add(p[i]); } - return hull.Take(k - 1).ToList(); + // Remove the last point of each half because it's a duplicate of the first point of the other half + lower.RemoveAt(lower.Count - 1); + upper.RemoveAt(upper.Count - 1); + + lower.AddRange(upper); + return lower; float cross(Vector2 o, Vector2 a, Vector2 b) => (a.X - o.X) * (b.Y - o.Y) - (a.Y - o.Y) * (b.X - o.X); } From ff3bffc7d9e0e955afe3ad3ad498c2d3259e6877 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Wed, 21 Aug 2024 00:30:57 +0200 Subject: [PATCH 2024/2493] add test --- osu.Game.Tests/Utils/GeometryUtilsTest.cs | 33 +++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 osu.Game.Tests/Utils/GeometryUtilsTest.cs diff --git a/osu.Game.Tests/Utils/GeometryUtilsTest.cs b/osu.Game.Tests/Utils/GeometryUtilsTest.cs new file mode 100644 index 0000000000..ded4656ac1 --- /dev/null +++ b/osu.Game.Tests/Utils/GeometryUtilsTest.cs @@ -0,0 +1,33 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Game.Utils; +using osuTK; + +namespace osu.Game.Tests.Utils +{ + [TestFixture] + public class GeometryUtilsTest + { + [TestCase(new int[] { }, new int[] { })] + [TestCase(new[] { 0, 0 }, new[] { 0, 0 })] + [TestCase(new[] { 0, 0, 1, 1, 1, -1, 2, 0 }, new[] { 0, 0, 1, 1, 2, 0, 1, -1 })] + [TestCase(new[] { 0, 0, 1, 1, 1, -1, 2, 0, 1, 0 }, new[] { 0, 0, 1, 1, 2, 0, 1, -1 })] + [TestCase(new[] { 0, 0, 1, 1, 2, -1, 2, 0, 1, 0, 4, 10 }, new[] { 0, 0, 4, 10, 2, -1 })] + public void TestConvexHull(int[] values, int[] expected) + { + var points = new Vector2[values.Length / 2]; + for (int i = 0; i < values.Length; i += 2) + points[i / 2] = new Vector2(values[i], values[i + 1]); + + var expectedPoints = new Vector2[expected.Length / 2]; + for (int i = 0; i < expected.Length; i += 2) + expectedPoints[i / 2] = new Vector2(expected[i], expected[i + 1]); + + var hull = GeometryUtils.GetConvexHull(points); + + Assert.That(hull, Is.EquivalentTo(expectedPoints)); + } + } +} From db568bfb79d5d037e8a0e9d1485917421483da9a Mon Sep 17 00:00:00 2001 From: OliBomby Date: Wed, 21 Aug 2024 01:01:17 +0200 Subject: [PATCH 2025/2493] add tests --- .../Editor/TestSceneSliderChangeStates.cs | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderChangeStates.cs diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderChangeStates.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderChangeStates.cs new file mode 100644 index 0000000000..0b8f2f7417 --- /dev/null +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderChangeStates.cs @@ -0,0 +1,47 @@ +// 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.Objects; +using osu.Game.Rulesets.Objects.Types; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Tests.Beatmaps; +using osuTK; + +namespace osu.Game.Rulesets.Osu.Tests.Editor +{ + public partial class TestSceneSliderChangeStates : TestSceneOsuEditor + { + protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(ruleset, false); + + [TestCase(SplineType.Catmull)] + [TestCase(SplineType.BSpline)] + [TestCase(SplineType.Linear)] + [TestCase(SplineType.PerfectCurve)] + public void TestSliderRetainsCurveTypes(SplineType splineType) + { + Slider? slider = null; + PathType pathType = new PathType(splineType); + + AddStep("add slider", () => EditorBeatmap.Add(slider = new Slider + { + StartTime = 500, + Path = new SliderPath(new[] + { + new PathControlPoint(Vector2.Zero, pathType), + new PathControlPoint(new Vector2(200, 0), pathType), + }) + })); + AddAssert("slider has correct spline type", () => ((Slider)EditorBeatmap.HitObjects[0]).Path.ControlPoints.All(p => p.Type == pathType)); + AddStep("remove object", () => EditorBeatmap.Remove(slider)); + AddAssert("slider removed", () => EditorBeatmap.HitObjects.Count == 0); + addUndoSteps(); + AddAssert("slider not removed", () => EditorBeatmap.HitObjects.Count == 1); + AddAssert("slider has correct spline type", () => ((Slider)EditorBeatmap.HitObjects[0]).Path.ControlPoints.All(p => p.Type == pathType)); + } + + private void addUndoSteps() => AddStep("undo", () => Editor.Undo()); + } +} From 8d72ec8bd6977676a56dd4bacb7e53a2190f0469 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Wed, 21 Aug 2024 01:50:52 +0200 Subject: [PATCH 2026/2493] move timing point binary search back inline --- .../NonVisual/ControlPointInfoTest.cs | 58 +++++++++++ osu.Game.Tests/Utils/BinarySearchUtilsTest.cs | 66 ------------- .../ControlPoints/ControlPointInfo.cs | 80 ++++++++++++++- osu.Game/Utils/BinarySearchUtils.cs | 98 ------------------- 4 files changed, 136 insertions(+), 166 deletions(-) delete mode 100644 osu.Game.Tests/Utils/BinarySearchUtilsTest.cs delete mode 100644 osu.Game/Utils/BinarySearchUtils.cs diff --git a/osu.Game.Tests/NonVisual/ControlPointInfoTest.cs b/osu.Game.Tests/NonVisual/ControlPointInfoTest.cs index 2d5d425ee8..d7df3d318d 100644 --- a/osu.Game.Tests/NonVisual/ControlPointInfoTest.cs +++ b/osu.Game.Tests/NonVisual/ControlPointInfoTest.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.Linq; using NUnit.Framework; using osu.Game.Beatmaps.ControlPoints; @@ -286,5 +287,62 @@ namespace osu.Game.Tests.NonVisual Assert.That(cpi.TimingPoints[0].BeatLength, Is.Not.EqualTo(cpiCopy.TimingPoints[0].BeatLength)); } + + [Test] + public void TestBinarySearchEmptyList() + { + Assert.That(ControlPointInfo.BinarySearch(Array.Empty(), 0, EqualitySelection.FirstFound), Is.EqualTo(-1)); + Assert.That(ControlPointInfo.BinarySearch(Array.Empty(), 0, EqualitySelection.Leftmost), Is.EqualTo(-1)); + Assert.That(ControlPointInfo.BinarySearch(Array.Empty(), 0, EqualitySelection.Rightmost), Is.EqualTo(-1)); + } + + [TestCase(new[] { 1 }, 0, -1)] + [TestCase(new[] { 1 }, 1, 0)] + [TestCase(new[] { 1 }, 2, -2)] + [TestCase(new[] { 1, 3 }, 0, -1)] + [TestCase(new[] { 1, 3 }, 1, 0)] + [TestCase(new[] { 1, 3 }, 2, -2)] + [TestCase(new[] { 1, 3 }, 3, 1)] + [TestCase(new[] { 1, 3 }, 4, -3)] + public void TestBinarySearchUniqueScenarios(int[] values, int search, int expectedIndex) + { + var items = values.Select(t => new TimingControlPoint { Time = t }).ToArray(); + Assert.That(ControlPointInfo.BinarySearch(items, search, EqualitySelection.FirstFound), Is.EqualTo(expectedIndex)); + Assert.That(ControlPointInfo.BinarySearch(items, search, EqualitySelection.Leftmost), Is.EqualTo(expectedIndex)); + Assert.That(ControlPointInfo.BinarySearch(items, search, EqualitySelection.Rightmost), Is.EqualTo(expectedIndex)); + } + + [TestCase(new[] { 1, 1 }, 1, 0)] + [TestCase(new[] { 1, 2, 2 }, 2, 1)] + [TestCase(new[] { 1, 2, 2, 2 }, 2, 1)] + [TestCase(new[] { 1, 2, 2, 2, 3 }, 2, 2)] + [TestCase(new[] { 1, 2, 2, 3 }, 2, 1)] + public void TestBinarySearchFirstFoundDuplicateScenarios(int[] values, int search, int expectedIndex) + { + var items = values.Select(t => new TimingControlPoint { Time = t }).ToArray(); + Assert.That(ControlPointInfo.BinarySearch(items, search, EqualitySelection.FirstFound), Is.EqualTo(expectedIndex)); + } + + [TestCase(new[] { 1, 1 }, 1, 0)] + [TestCase(new[] { 1, 2, 2 }, 2, 1)] + [TestCase(new[] { 1, 2, 2, 2 }, 2, 1)] + [TestCase(new[] { 1, 2, 2, 2, 3 }, 2, 1)] + [TestCase(new[] { 1, 2, 2, 3 }, 2, 1)] + public void TestBinarySearchLeftMostDuplicateScenarios(int[] values, int search, int expectedIndex) + { + var items = values.Select(t => new TimingControlPoint { Time = t }).ToArray(); + Assert.That(ControlPointInfo.BinarySearch(items, search, EqualitySelection.Leftmost), Is.EqualTo(expectedIndex)); + } + + [TestCase(new[] { 1, 1 }, 1, 1)] + [TestCase(new[] { 1, 2, 2 }, 2, 2)] + [TestCase(new[] { 1, 2, 2, 2 }, 2, 3)] + [TestCase(new[] { 1, 2, 2, 2, 3 }, 2, 3)] + [TestCase(new[] { 1, 2, 2, 3 }, 2, 2)] + public void TestBinarySearchRightMostDuplicateScenarios(int[] values, int search, int expectedIndex) + { + var items = values.Select(t => new TimingControlPoint { Time = t }).ToArray(); + Assert.That(ControlPointInfo.BinarySearch(items, search, EqualitySelection.Rightmost), Is.EqualTo(expectedIndex)); + } } } diff --git a/osu.Game.Tests/Utils/BinarySearchUtilsTest.cs b/osu.Game.Tests/Utils/BinarySearchUtilsTest.cs deleted file mode 100644 index cbf6cdf32a..0000000000 --- a/osu.Game.Tests/Utils/BinarySearchUtilsTest.cs +++ /dev/null @@ -1,66 +0,0 @@ -// 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 NUnit.Framework; -using osu.Game.Utils; - -namespace osu.Game.Tests.Utils -{ - [TestFixture] - public class BinarySearchUtilsTest - { - [Test] - public void TestEmptyList() - { - Assert.That(BinarySearchUtils.BinarySearch(Array.Empty(), 0, x => x), Is.EqualTo(-1)); - Assert.That(BinarySearchUtils.BinarySearch(Array.Empty(), 0, x => x, EqualitySelection.Leftmost), Is.EqualTo(-1)); - Assert.That(BinarySearchUtils.BinarySearch(Array.Empty(), 0, x => x, EqualitySelection.Rightmost), Is.EqualTo(-1)); - } - - [TestCase(new[] { 1 }, 0, -1)] - [TestCase(new[] { 1 }, 1, 0)] - [TestCase(new[] { 1 }, 2, -2)] - [TestCase(new[] { 1, 3 }, 0, -1)] - [TestCase(new[] { 1, 3 }, 1, 0)] - [TestCase(new[] { 1, 3 }, 2, -2)] - [TestCase(new[] { 1, 3 }, 3, 1)] - [TestCase(new[] { 1, 3 }, 4, -3)] - public void TestUniqueScenarios(int[] values, int search, int expectedIndex) - { - Assert.That(BinarySearchUtils.BinarySearch(values, search, x => x, EqualitySelection.FirstFound), Is.EqualTo(expectedIndex)); - Assert.That(BinarySearchUtils.BinarySearch(values, search, x => x, EqualitySelection.Leftmost), Is.EqualTo(expectedIndex)); - Assert.That(BinarySearchUtils.BinarySearch(values, search, x => x, EqualitySelection.Rightmost), Is.EqualTo(expectedIndex)); - } - - [TestCase(new[] { 1, 1 }, 1, 0)] - [TestCase(new[] { 1, 2, 2 }, 2, 1)] - [TestCase(new[] { 1, 2, 2, 2 }, 2, 1)] - [TestCase(new[] { 1, 2, 2, 2, 3 }, 2, 2)] - [TestCase(new[] { 1, 2, 2, 3 }, 2, 1)] - public void TestFirstFoundDuplicateScenarios(int[] values, int search, int expectedIndex) - { - Assert.That(BinarySearchUtils.BinarySearch(values, search, x => x), Is.EqualTo(expectedIndex)); - } - - [TestCase(new[] { 1, 1 }, 1, 0)] - [TestCase(new[] { 1, 2, 2 }, 2, 1)] - [TestCase(new[] { 1, 2, 2, 2 }, 2, 1)] - [TestCase(new[] { 1, 2, 2, 2, 3 }, 2, 1)] - [TestCase(new[] { 1, 2, 2, 3 }, 2, 1)] - public void TestLeftMostDuplicateScenarios(int[] values, int search, int expectedIndex) - { - Assert.That(BinarySearchUtils.BinarySearch(values, search, x => x, EqualitySelection.Leftmost), Is.EqualTo(expectedIndex)); - } - - [TestCase(new[] { 1, 1 }, 1, 1)] - [TestCase(new[] { 1, 2, 2 }, 2, 2)] - [TestCase(new[] { 1, 2, 2, 2 }, 2, 3)] - [TestCase(new[] { 1, 2, 2, 2, 3 }, 2, 3)] - [TestCase(new[] { 1, 2, 2, 3 }, 2, 2)] - public void TestRightMostDuplicateScenarios(int[] values, int search, int expectedIndex) - { - Assert.That(BinarySearchUtils.BinarySearch(values, search, x => x, EqualitySelection.Rightmost), Is.EqualTo(expectedIndex)); - } - } -} diff --git a/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs b/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs index 026d44faa1..8666f01129 100644 --- a/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs +++ b/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs @@ -82,7 +82,7 @@ namespace osu.Game.Beatmaps.ControlPoints [CanBeNull] public TimingControlPoint TimingPointAfter(double time) { - int index = BinarySearchUtils.BinarySearch(TimingPoints, time, c => c.Time, EqualitySelection.Rightmost); + int index = BinarySearch(TimingPoints, time, EqualitySelection.Rightmost); index = index < 0 ? ~index : index + 1; return index < TimingPoints.Count ? TimingPoints[index] : null; } @@ -250,7 +250,7 @@ namespace osu.Game.Beatmaps.ControlPoints { ArgumentNullException.ThrowIfNull(list); - int index = BinarySearchUtils.BinarySearch(list, time, c => c.Time, EqualitySelection.Rightmost); + int index = BinarySearch(list, time, EqualitySelection.Rightmost); if (index < 0) index = ~index - 1; @@ -258,6 +258,75 @@ namespace osu.Game.Beatmaps.ControlPoints return index >= 0 ? list[index] : null; } + /// + /// Binary searches one of the control point lists to find the active control point at . + /// + /// The list to search. + /// The time to find the control point at. + /// Determines which index to return if there are multiple exact matches. + /// The index of the control point at . Will return the complement of the index of the control point after if no exact match is found. + public static int BinarySearch(IReadOnlyList list, double time, EqualitySelection equalitySelection) + where T : class, IControlPoint + { + ArgumentNullException.ThrowIfNull(list); + + int n = list.Count; + + if (n == 0) + return -1; + + if (time < list[0].Time) + return -1; + + if (time > list[^1].Time) + return ~n; + + int l = 0; + int r = n - 1; + bool equalityFound = false; + + while (l <= r) + { + int pivot = l + ((r - l) >> 1); + + if (list[pivot].Time < time) + l = pivot + 1; + else if (list[pivot].Time > time) + r = pivot - 1; + else + { + equalityFound = true; + + switch (equalitySelection) + { + case EqualitySelection.Leftmost: + r = pivot - 1; + break; + + case EqualitySelection.Rightmost: + l = pivot + 1; + break; + + default: + case EqualitySelection.FirstFound: + return pivot; + } + } + } + + if (!equalityFound) return ~l; + + switch (equalitySelection) + { + case EqualitySelection.Leftmost: + return l; + + default: + case EqualitySelection.Rightmost: + return l - 1; + } + } + /// /// Check whether should be added. /// @@ -328,4 +397,11 @@ namespace osu.Game.Beatmaps.ControlPoints return controlPointInfo; } } + + public enum EqualitySelection + { + FirstFound, + Leftmost, + Rightmost + } } diff --git a/osu.Game/Utils/BinarySearchUtils.cs b/osu.Game/Utils/BinarySearchUtils.cs deleted file mode 100644 index 08ce4e363d..0000000000 --- a/osu.Game/Utils/BinarySearchUtils.cs +++ /dev/null @@ -1,98 +0,0 @@ -// 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; - -namespace osu.Game.Utils -{ - public class BinarySearchUtils - { - /// - /// Finds the index of the item in the sorted list which has its property equal to the search term. - /// If no exact match is found, the complement of the index of the first item greater than the search term will be returned. - /// - /// The type of the items in the list to search. - /// The type of the property to perform the search on. - /// The list of items to search. - /// The query to find. - /// Function that maps an item in the list to its index property. - /// Determines which index to return if there are multiple exact matches. - /// The index of the found item. Will return the complement of the index of the first item greater than the search query if no exact match is found. - public static int BinarySearch(IReadOnlyList list, T2 searchTerm, Func termFunc, EqualitySelection equalitySelection = EqualitySelection.FirstFound) - { - int n = list.Count; - - if (n == 0) - return -1; - - var comparer = Comparer.Default; - - if (comparer.Compare(searchTerm, termFunc(list[0])) == -1) - return -1; - - if (comparer.Compare(searchTerm, termFunc(list[^1])) == 1) - return ~n; - - int min = 0; - int max = n - 1; - bool equalityFound = false; - - while (min <= max) - { - int mid = min + (max - min) / 2; - T2 midTerm = termFunc(list[mid]); - - switch (comparer.Compare(midTerm, searchTerm)) - { - case 0: - equalityFound = true; - - switch (equalitySelection) - { - case EqualitySelection.Leftmost: - max = mid - 1; - break; - - case EqualitySelection.Rightmost: - min = mid + 1; - break; - - default: - case EqualitySelection.FirstFound: - return mid; - } - - break; - - case 1: - max = mid - 1; - break; - - case -1: - min = mid + 1; - break; - } - } - - if (!equalityFound) return ~min; - - switch (equalitySelection) - { - case EqualitySelection.Leftmost: - return min; - - default: - case EqualitySelection.Rightmost: - return min - 1; - } - } - } - - public enum EqualitySelection - { - FirstFound, - Leftmost, - Rightmost - } -} From c4f08b42abacb959008d35535246fae3a0cb801f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 21 Aug 2024 09:05:10 +0200 Subject: [PATCH 2027/2493] Use colours to distinguish buttons better --- osu.Game/Screens/Edit/Timing/ControlPointList.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Timing/ControlPointList.cs b/osu.Game/Screens/Edit/Timing/ControlPointList.cs index 4df52a0a3a..cbef0b9064 100644 --- a/osu.Game/Screens/Edit/Timing/ControlPointList.cs +++ b/osu.Game/Screens/Edit/Timing/ControlPointList.cs @@ -9,6 +9,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Input.Events; using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Graphics; using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterfaceV2; using osuTK; @@ -30,7 +31,7 @@ namespace osu.Game.Screens.Edit.Timing private Bindable selectedGroup { get; set; } = null!; [BackgroundDependencyLoader] - private void load() + private void load(OsuColour colours) { RelativeSizeAxes = Axes.Both; @@ -59,6 +60,7 @@ namespace osu.Game.Screens.Edit.Timing Action = delete, Anchor = Anchor.BottomRight, Origin = Anchor.BottomRight, + BackgroundColour = colours.Red3, }, addButton = new RoundedButton { @@ -66,6 +68,7 @@ namespace osu.Game.Screens.Edit.Timing Size = new Vector2(160, 30), Anchor = Anchor.BottomRight, Origin = Anchor.BottomRight, + BackgroundColour = colours.Green3, }, new RoundedButton { From a0002943a1ac11f653570366710f61ef45cd289c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 21 Aug 2024 15:51:02 +0900 Subject: [PATCH 2028/2493] Adjust centre marker visuals a bit --- .../Components/Timeline/CentreMarker.cs | 32 +++++++++++-------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/CentreMarker.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/CentreMarker.cs index 7d8622905c..5282fbf1fc 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/CentreMarker.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/CentreMarker.cs @@ -14,22 +14,19 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline { public partial class CentreMarker : CompositeDrawable { - 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.TopCentre; - Origin = Anchor.TopCentre; - } - [BackgroundDependencyLoader] private void load(OverlayColourProvider colours) { + const float triangle_width = 8; + const float bar_width = 2f; + + RelativeSizeAxes = Axes.Y; + + Anchor = Anchor.TopCentre; + Origin = Anchor.TopCentre; + + Size = new Vector2(triangle_width, 1); + InternalChildren = new Drawable[] { new Box @@ -47,6 +44,15 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline Origin = Anchor.BottomCentre, Size = new Vector2(triangle_width, triangle_width * 0.8f), Scale = new Vector2(1, -1), + EdgeSmoothness = new Vector2(1, 0), + Colour = colours.Colour2, + }, + new Triangle + { + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + Size = new Vector2(triangle_width, triangle_width * 0.8f), + Scale = new Vector2(1, 1), Colour = colours.Colour2, }, }; From 3065f808a78761935ca84cc4f4c03882eeed4806 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 21 Aug 2024 00:50:41 +0900 Subject: [PATCH 2029/2493] Simplify timing point display on timeline --- .../Compose/Components/Timeline/Timeline.cs | 7 +- .../Timeline/TimelineControlPointDisplay.cs | 98 ----------- .../Timeline/TimelineControlPointGroup.cs | 52 ------ .../Timeline/TimelineTimingChangeDisplay.cs | 164 ++++++++++++++++++ .../Components/Timeline/TimingPointPiece.cs | 29 ---- .../Components/Timeline/TopPointPiece.cs | 91 ---------- 6 files changed, 168 insertions(+), 273 deletions(-) delete mode 100644 osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointDisplay.cs delete mode 100644 osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointGroup.cs create mode 100644 osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTimingChangeDisplay.cs delete mode 100644 osu.Game/Screens/Edit/Compose/Components/Timeline/TimingPointPiece.cs delete mode 100644 osu.Game/Screens/Edit/Compose/Components/Timeline/TopPointPiece.cs diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs index 7a28f7bbaa..af53697b05 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs @@ -78,7 +78,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline private TimelineTickDisplay ticks = null!; - private TimelineControlPointDisplay controlPoints = null!; + private TimelineTimingChangeDisplay controlPoints = null!; private Container mainContent = null!; @@ -117,10 +117,11 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline AddRange(new Drawable[] { - controlPoints = new TimelineControlPointDisplay + ticks = new TimelineTickDisplay(), + controlPoints = new TimelineTimingChangeDisplay { RelativeSizeAxes = Axes.X, - Height = timeline_expanded_height, + Height = timeline_expanded_height - timeline_height, }, ticks, mainContent = new Container diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointDisplay.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointDisplay.cs deleted file mode 100644 index 116a3ee105..0000000000 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointDisplay.cs +++ /dev/null @@ -1,98 +0,0 @@ -// 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.Bindables; -using osu.Framework.Caching; -using osu.Game.Beatmaps.ControlPoints; -using osu.Game.Screens.Edit.Components.Timelines.Summary.Parts; - -namespace osu.Game.Screens.Edit.Compose.Components.Timeline -{ - /// - /// The part of the timeline that displays the control points. - /// - public partial class TimelineControlPointDisplay : TimelinePart - { - [Resolved] - private Timeline timeline { get; set; } = null!; - - /// - /// The visible time/position range of the timeline. - /// - private (float min, float max) visibleRange = (float.MinValue, float.MaxValue); - - private readonly Cached groupCache = new Cached(); - - private readonly IBindableList controlPointGroups = new BindableList(); - - protected override void LoadBeatmap(EditorBeatmap beatmap) - { - base.LoadBeatmap(beatmap); - - controlPointGroups.UnbindAll(); - controlPointGroups.BindTo(beatmap.ControlPointInfo.Groups); - controlPointGroups.BindCollectionChanged((_, _) => groupCache.Invalidate(), true); - } - - protected override void Update() - { - base.Update(); - - if (DrawWidth <= 0) return; - - (float, float) newRange = ( - (ToLocalSpace(timeline.ScreenSpaceDrawQuad.TopLeft).X - TopPointPiece.WIDTH) / DrawWidth * Content.RelativeChildSize.X, - (ToLocalSpace(timeline.ScreenSpaceDrawQuad.TopRight).X) / DrawWidth * Content.RelativeChildSize.X); - - if (visibleRange != newRange) - { - visibleRange = newRange; - groupCache.Invalidate(); - } - - if (!groupCache.IsValid) - { - recreateDrawableGroups(); - groupCache.Validate(); - } - } - - private void recreateDrawableGroups() - { - // Remove groups outside the visible range - foreach (TimelineControlPointGroup drawableGroup in this) - { - if (!shouldBeVisible(drawableGroup.Group)) - drawableGroup.Expire(); - } - - // Add remaining ones - for (int i = 0; i < controlPointGroups.Count; i++) - { - var group = controlPointGroups[i]; - - if (!shouldBeVisible(group)) - continue; - - bool alreadyVisible = false; - - foreach (var g in this) - { - if (ReferenceEquals(g.Group, group)) - { - alreadyVisible = true; - break; - } - } - - if (alreadyVisible) - continue; - - Add(new TimelineControlPointGroup(group)); - } - } - - private bool shouldBeVisible(ControlPointGroup group) => group.Time >= visibleRange.min && group.Time <= visibleRange.max; - } -} diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointGroup.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointGroup.cs deleted file mode 100644 index 98556fda45..0000000000 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointGroup.cs +++ /dev/null @@ -1,52 +0,0 @@ -// 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 osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Game.Beatmaps.ControlPoints; - -namespace osu.Game.Screens.Edit.Compose.Components.Timeline -{ - public partial class TimelineControlPointGroup : CompositeDrawable - { - public readonly ControlPointGroup Group; - - private readonly IBindableList controlPoints = new BindableList(); - - public TimelineControlPointGroup(ControlPointGroup group) - { - Group = group; - - RelativePositionAxes = Axes.X; - RelativeSizeAxes = Axes.Y; - AutoSizeAxes = Axes.X; - - Origin = Anchor.TopLeft; - - // offset visually to avoid overlapping timeline tick display. - X = (float)group.Time + 6; - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - controlPoints.BindTo(Group.ControlPoints); - controlPoints.BindCollectionChanged((_, _) => - { - ClearInternal(); - - foreach (var point in controlPoints) - { - switch (point) - { - case TimingControlPoint timingPoint: - AddInternal(new TimingPointPiece(timingPoint)); - break; - } - } - }, true); - } - } -} diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTimingChangeDisplay.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTimingChangeDisplay.cs new file mode 100644 index 0000000000..908aa6bc76 --- /dev/null +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTimingChangeDisplay.cs @@ -0,0 +1,164 @@ +// 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.Framework.Bindables; +using osu.Framework.Caching; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Screens.Edit.Components.Timelines.Summary.Parts; +using osu.Game.Screens.Edit.Components.Timelines.Summary.Visualisations; +using osuTK.Graphics; + +namespace osu.Game.Screens.Edit.Compose.Components.Timeline +{ + /// + /// The part of the timeline that displays the control points. + /// + public partial class TimelineTimingChangeDisplay : TimelinePart + { + [Resolved] + private Timeline timeline { get; set; } = null!; + + /// + /// The visible time/position range of the timeline. + /// + private (float min, float max) visibleRange = (float.MinValue, float.MaxValue); + + private readonly Cached groupCache = new Cached(); + + private ControlPointInfo controlPointInfo = null!; + + protected override void LoadBeatmap(EditorBeatmap beatmap) + { + base.LoadBeatmap(beatmap); + + beatmap.ControlPointInfo.ControlPointsChanged += () => groupCache.Invalidate(); + controlPointInfo = beatmap.ControlPointInfo; + } + + protected override void Update() + { + base.Update(); + + if (DrawWidth <= 0) return; + + (float, float) newRange = ( + (ToLocalSpace(timeline.ScreenSpaceDrawQuad.TopLeft).X - TimingPointPiece.WIDTH) / DrawWidth * Content.RelativeChildSize.X, + (ToLocalSpace(timeline.ScreenSpaceDrawQuad.TopRight).X + TimingPointPiece.WIDTH) / DrawWidth * Content.RelativeChildSize.X); + + if (visibleRange != newRange) + { + visibleRange = newRange; + groupCache.Invalidate(); + } + + if (!groupCache.IsValid) + { + recreateDrawableGroups(); + groupCache.Validate(); + } + } + + private void recreateDrawableGroups() + { + // Remove groups outside the visible range (or timing points which have since been removed from the beatmap). + foreach (TimingPointPiece drawableGroup in this) + { + if (!controlPointInfo.TimingPoints.Contains(drawableGroup.Point) || !shouldBeVisible(drawableGroup.Point)) + drawableGroup.Expire(); + } + + // Add remaining / new ones. + foreach (TimingControlPoint t in controlPointInfo.TimingPoints) + attemptAddTimingPoint(t); + } + + private void attemptAddTimingPoint(TimingControlPoint point) + { + if (!shouldBeVisible(point)) + return; + + foreach (var child in this) + { + if (ReferenceEquals(child.Point, point)) + return; + } + + Add(new TimingPointPiece(point)); + } + + private bool shouldBeVisible(TimingControlPoint point) => point.Time >= visibleRange.min && point.Time <= visibleRange.max; + + public partial class TimingPointPiece : CompositeDrawable + { + public const float WIDTH = 16; + + public readonly TimingControlPoint Point; + + private readonly BindableNumber beatLength; + + protected OsuSpriteText Label { get; private set; } = null!; + + public TimingPointPiece(TimingControlPoint timingPoint) + { + RelativePositionAxes = Axes.X; + + RelativeSizeAxes = Axes.Y; + Width = WIDTH; + + Origin = Anchor.TopRight; + + Point = timingPoint; + + beatLength = timingPoint.BeatLengthBindable.GetBoundCopy(); + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + const float corner_radius = PointVisualisation.MAX_WIDTH / 2; + + InternalChildren = new Drawable[] + { + new Container + { + RelativeSizeAxes = Axes.Both, + 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, + Rotation = 90, + Padding = new MarginPadding { Horizontal = 2 }, + Font = OsuFont.Default.With(size: 12, weight: FontWeight.SemiBold), + } + }; + + beatLength.BindValueChanged(beatLength => + { + Label.Text = $"{60000 / beatLength.NewValue:n1} BPM"; + }, true); + } + + protected override void Update() + { + base.Update(); + X = (float)Point.Time; + } + } + } +} diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimingPointPiece.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimingPointPiece.cs deleted file mode 100644 index 2a4ad66918..0000000000 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimingPointPiece.cs +++ /dev/null @@ -1,29 +0,0 @@ -// 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.Bindables; -using osu.Game.Beatmaps.ControlPoints; - -namespace osu.Game.Screens.Edit.Compose.Components.Timeline -{ - public partial class TimingPointPiece : TopPointPiece - { - private readonly BindableNumber beatLength; - - public TimingPointPiece(TimingControlPoint point) - : base(point) - { - beatLength = point.BeatLengthBindable.GetBoundCopy(); - } - - [BackgroundDependencyLoader] - private void load() - { - beatLength.BindValueChanged(beatLength => - { - Label.Text = $"{60000 / beatLength.NewValue:n1} BPM"; - }, true); - } - } -} diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TopPointPiece.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TopPointPiece.cs deleted file mode 100644 index a40a805361..0000000000 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TopPointPiece.cs +++ /dev/null @@ -1,91 +0,0 @@ -// 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.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 - { - protected readonly ControlPoint Point; - - protected OsuSpriteText Label { get; private set; } = null!; - - public const float WIDTH = 80; - - public TopPointPiece(ControlPoint point) - { - Point = point; - Width = WIDTH; - Height = 16; - Margin = new MarginPadding { Vertical = 4 }; - - Origin = Anchor.TopCentre; - Anchor = Anchor.TopCentre; - } - - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { - const float corner_radius = 4; - const float arrow_extension = 3; - const float triangle_portion = 15; - - InternalChildren = new Drawable[] - { - // 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), - 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.CentreLeft, - Origin = Anchor.CentreLeft, - Padding = new MarginPadding(3), - Font = OsuFont.Default.With(size: 14, weight: FontWeight.SemiBold), - Colour = colours.B5, - } - }; - } - } -} From 1a48a6f6542404e79cc8787d895e75ab90742ac5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 21 Aug 2024 00:44:29 +0900 Subject: [PATCH 2030/2493] Reduce size of hit objects on timeline --- .../Compose/Components/Timeline/TimelineHitObjectBlueprint.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs index a168dcbd3e..6c0d5af247 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs @@ -30,7 +30,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline { public partial class TimelineHitObjectBlueprint : SelectionBlueprint { - private const float circle_size = 38; + private const float circle_size = 32; private Container? repeatsContainer; From 7e6490133d6588582171c1121083021f4ae88075 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 20 Aug 2024 22:24:48 +0900 Subject: [PATCH 2031/2493] Adjust visuals of tick display (and fine tune some other timeline elements) --- osu.Game/Screens/Edit/BindableBeatDivisor.cs | 8 ++-- .../Compose/Components/BeatDivisorControl.cs | 2 +- .../Components/Timeline/CentreMarker.cs | 7 +--- .../Compose/Components/Timeline/Timeline.cs | 40 +++++++++---------- .../Timeline/TimelineTickDisplay.cs | 26 +++++++----- .../Timeline/TimelineTimingChangeDisplay.cs | 5 +-- 6 files changed, 42 insertions(+), 46 deletions(-) diff --git a/osu.Game/Screens/Edit/BindableBeatDivisor.cs b/osu.Game/Screens/Edit/BindableBeatDivisor.cs index 3bb1b4e079..bd9c9bab9a 100644 --- a/osu.Game/Screens/Edit/BindableBeatDivisor.cs +++ b/osu.Game/Screens/Edit/BindableBeatDivisor.cs @@ -145,18 +145,18 @@ namespace osu.Game.Screens.Edit { case 1: case 2: - return new Vector2(0.6f, 0.9f); + return new Vector2(1, 0.9f); case 3: case 4: - return new Vector2(0.5f, 0.8f); + return new Vector2(0.8f, 0.8f); case 6: case 8: - return new Vector2(0.4f, 0.7f); + return new Vector2(0.8f, 0.7f); default: - return new Vector2(0.3f, 0.6f); + return new Vector2(0.8f, 0.6f); } } diff --git a/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs b/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs index 3c2a66b8bb..43a2abe4c4 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs @@ -526,7 +526,7 @@ namespace osu.Game.Screens.Edit.Compose.Components AlwaysDisplayed = alwaysDisplayed; Divisor = divisor; - Size = new Vector2(6f, 18) * BindableBeatDivisor.GetSize(divisor); + Size = new Vector2(4, 18) * BindableBeatDivisor.GetSize(divisor); Alpha = alwaysDisplayed ? 1 : 0; InternalChild = new Box { RelativeSizeAxes = Axes.Both }; diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/CentreMarker.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/CentreMarker.cs index 5282fbf1fc..c63dfdfb55 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/CentreMarker.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/CentreMarker.cs @@ -2,9 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; -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.Game.Overlays; @@ -29,14 +27,13 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline InternalChildren = new Drawable[] { - new Box + new Circle { Anchor = Anchor.Centre, Origin = Anchor.Centre, RelativeSizeAxes = Axes.Y, Width = bar_width, - Blending = BlendingParameters.Additive, - Colour = ColourInfo.GradientVertical(colours.Colour2.Opacity(0.6f), colours.Colour2.Opacity(0)), + Colour = colours.Colour2, }, new Triangle { diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs index af53697b05..3fa9fc8e3d 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs @@ -14,7 +14,9 @@ using osu.Framework.Input.Events; using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.Graphics; +using osu.Game.Overlays; using osu.Game.Rulesets.Edit; +using osu.Game.Screens.Edit.Components.Timelines.Summary.Visualisations; using osuTK; using osuTK.Input; @@ -24,7 +26,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline public partial class Timeline : ZoomableScrollContainer, IPositionSnapProvider { private const float timeline_height = 80; - private const float timeline_expanded_height = 94; + private const float timeline_expanded_height = 80; private readonly Drawable userContent; @@ -103,32 +105,28 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline } [BackgroundDependencyLoader] - private void load(IBindable beatmap, OsuColour colours, OsuConfigManager config) + private void load(IBindable beatmap, OsuColour colours, OverlayColourProvider colourProvider, OsuConfigManager config) { CentreMarker centreMarker; // We don't want the centre marker to scroll AddInternal(centreMarker = new CentreMarker()); - ticks = new TimelineTickDisplay - { - Padding = new MarginPadding { Vertical = 2, }, - }; + ticks = new TimelineTickDisplay(); AddRange(new Drawable[] { - ticks = new TimelineTickDisplay(), + ticks, controlPoints = new TimelineTimingChangeDisplay { - RelativeSizeAxes = Axes.X, - Height = timeline_expanded_height - timeline_height, + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, }, - ticks, mainContent = new Container { RelativeSizeAxes = Axes.X, Height = timeline_height, - Depth = float.MaxValue, Children = new[] { waveform = new WaveformGraph @@ -139,19 +137,19 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline MidColour = colours.BlueDark, HighColour = colours.BlueDarker, }, - ticks.CreateProxy(), centreMarker.CreateProxy(), - new Box - { - Name = "zero marker", - RelativeSizeAxes = Axes.Y, - Width = 2, - Origin = Anchor.TopCentre, - Colour = colours.YellowDarker, - }, + ticks.CreateProxy(), userContent, } }, + new Box + { + Name = "zero marker", + RelativeSizeAxes = Axes.Y, + Width = TimelineTickDisplay.TICK_WIDTH / 2, + Origin = Anchor.TopCentre, + Colour = colourProvider.Background1, + }, }); waveformOpacity = config.GetBindable(OsuSetting.EditorWaveformOpacity); @@ -195,7 +193,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline if (visible.NewValue || alwaysShowControlPoints) { this.ResizeHeightTo(timeline_expanded_height, 200, Easing.OutQuint); - mainContent.MoveToY(15, 200, Easing.OutQuint); + mainContent.MoveToY(0, 200, Easing.OutQuint); // delay the fade in else masking looks weird. controlPoints.Delay(180).FadeIn(400, Easing.OutQuint); diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs index 4796c08809..66d0df9e18 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs @@ -17,6 +17,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline { public partial class TimelineTickDisplay : TimelinePart { + public const float TICK_WIDTH = 3; + // With current implementation every tick in the sub-tree should be visible, no need to check whether they are masked away. public override bool UpdateSubTreeMasking() => false; @@ -138,20 +140,15 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline // even though "bar lines" take up the full vertical space, we render them in two pieces because it allows for less anchor/origin churn. - Vector2 size = Vector2.One; - - if (indexInBar != 0) - size = BindableBeatDivisor.GetSize(divisor); + var size = indexInBar == 0 + ? new Vector2(1.3f, 1) + : BindableBeatDivisor.GetSize(divisor); var line = getNextUsableLine(); line.X = xPos; - line.Anchor = Anchor.CentreLeft; - line.Origin = Anchor.Centre; - - line.Height = 0.6f + size.Y * 0.4f; - line.Width = PointVisualisation.MAX_WIDTH * (0.6f + 0.4f * size.X); - + line.Width = TICK_WIDTH * size.X; + line.Height = size.Y; line.Colour = colour; } @@ -174,8 +171,15 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline Drawable getNextUsableLine() { PointVisualisation point; + if (drawableIndex >= Count) - Add(point = new PointVisualisation(0)); + { + Add(point = new PointVisualisation(0) + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.Centre, + }); + } else point = Children[drawableIndex]; diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTimingChangeDisplay.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTimingChangeDisplay.cs index 908aa6bc76..419f7e111f 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTimingChangeDisplay.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTimingChangeDisplay.cs @@ -12,7 +12,6 @@ using osu.Game.Beatmaps.ControlPoints; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Screens.Edit.Components.Timelines.Summary.Parts; -using osu.Game.Screens.Edit.Components.Timelines.Summary.Visualisations; using osuTK.Graphics; namespace osu.Game.Screens.Edit.Compose.Components.Timeline @@ -122,8 +121,6 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline [BackgroundDependencyLoader] private void load(OsuColour colours) { - const float corner_radius = PointVisualisation.MAX_WIDTH / 2; - InternalChildren = new Drawable[] { new Container @@ -131,7 +128,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline RelativeSizeAxes = Axes.Both, Colour = Point.GetRepresentingColour(colours), Masking = true, - CornerRadius = corner_radius, + CornerRadius = TimelineTickDisplay.TICK_WIDTH / 2, Child = new Box { Colour = Color4.White, From fef56cc29eeca9d0af0a6ae5daadd8a5505cd324 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 21 Aug 2024 15:57:52 +0900 Subject: [PATCH 2032/2493] Remove expanding behaviour of timeline completely --- .../Compose/Components/Timeline/Timeline.cs | 51 ++----------------- .../Screens/Edit/EditorScreenWithTimeline.cs | 10 +--- osu.Game/Screens/Edit/Timing/TimingScreen.cs | 8 --- 3 files changed, 4 insertions(+), 65 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs index 3fa9fc8e3d..840f1311db 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs @@ -16,7 +16,6 @@ using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Overlays; using osu.Game.Rulesets.Edit; -using osu.Game.Screens.Edit.Components.Timelines.Summary.Visualisations; using osuTK; using osuTK.Input; @@ -26,25 +25,9 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline public partial class Timeline : ZoomableScrollContainer, IPositionSnapProvider { private const float timeline_height = 80; - private const float timeline_expanded_height = 80; private readonly Drawable userContent; - private bool alwaysShowControlPoints; - - public bool AlwaysShowControlPoints - { - get => alwaysShowControlPoints; - set - { - if (value == alwaysShowControlPoints) - return; - - alwaysShowControlPoints = value; - controlPointsVisible.TriggerChange(); - } - } - [Resolved] private EditorClock editorClock { get; set; } = null!; @@ -80,12 +63,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline private TimelineTickDisplay ticks = null!; - private TimelineTimingChangeDisplay controlPoints = null!; - - private Container mainContent = null!; - private Bindable waveformOpacity = null!; - private Bindable controlPointsVisible = null!; private Bindable ticksVisible = null!; private double trackLengthForZoom; @@ -112,18 +90,16 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline // We don't want the centre marker to scroll AddInternal(centreMarker = new CentreMarker()); - ticks = new TimelineTickDisplay(); - AddRange(new Drawable[] { - ticks, - controlPoints = new TimelineTimingChangeDisplay + ticks = new TimelineTickDisplay(), + new TimelineTimingChangeDisplay { RelativeSizeAxes = Axes.Both, Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, }, - mainContent = new Container + new Container { RelativeSizeAxes = Axes.X, Height = timeline_height, @@ -153,7 +129,6 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline }); waveformOpacity = config.GetBindable(OsuSetting.EditorWaveformOpacity); - controlPointsVisible = config.GetBindable(OsuSetting.EditorTimelineShowTimingChanges); ticksVisible = config.GetBindable(OsuSetting.EditorTimelineShowTicks); track.BindTo(editorClock.Track); @@ -187,26 +162,6 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline waveformOpacity.BindValueChanged(_ => updateWaveformOpacity(), true); ticksVisible.BindValueChanged(visible => ticks.FadeTo(visible.NewValue ? 1 : 0, 200, Easing.OutQuint), true); - - controlPointsVisible.BindValueChanged(visible => - { - if (visible.NewValue || alwaysShowControlPoints) - { - this.ResizeHeightTo(timeline_expanded_height, 200, Easing.OutQuint); - mainContent.MoveToY(0, 200, Easing.OutQuint); - - // delay the fade in else masking looks weird. - controlPoints.Delay(180).FadeIn(400, Easing.OutQuint); - } - else - { - controlPoints.FadeOut(200, Easing.OutQuint); - - // likewise, delay the resize until the fade is complete. - this.Delay(180).ResizeHeightTo(timeline_height, 200, Easing.OutQuint); - mainContent.Delay(180).MoveToY(0, 200, Easing.OutQuint); - } - }, true); } private void updateWaveformOpacity() => diff --git a/osu.Game/Screens/Edit/EditorScreenWithTimeline.cs b/osu.Game/Screens/Edit/EditorScreenWithTimeline.cs index 01908e45c7..5bbf293e0a 100644 --- a/osu.Game/Screens/Edit/EditorScreenWithTimeline.cs +++ b/osu.Game/Screens/Edit/EditorScreenWithTimeline.cs @@ -106,18 +106,10 @@ namespace osu.Game.Screens.Edit MainContent.Add(content); content.FadeInFromZero(300, Easing.OutQuint); - LoadComponentAsync(TimelineArea = new TimelineArea(CreateTimelineContent()), timeline => - { - ConfigureTimeline(timeline); - timelineContent.Add(timeline); - }); + LoadComponentAsync(TimelineArea = new TimelineArea(CreateTimelineContent()), timelineContent.Add); }); } - protected virtual void ConfigureTimeline(TimelineArea timelineArea) - { - } - protected abstract Drawable CreateMainContent(); protected virtual Drawable CreateTimelineContent() => new Container(); diff --git a/osu.Game/Screens/Edit/Timing/TimingScreen.cs b/osu.Game/Screens/Edit/Timing/TimingScreen.cs index 67d4429be8..3f911f5067 100644 --- a/osu.Game/Screens/Edit/Timing/TimingScreen.cs +++ b/osu.Game/Screens/Edit/Timing/TimingScreen.cs @@ -6,7 +6,6 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Beatmaps.ControlPoints; -using osu.Game.Screens.Edit.Compose.Components.Timeline; namespace osu.Game.Screens.Edit.Timing { @@ -54,12 +53,5 @@ namespace osu.Game.Screens.Edit.Timing SelectedGroup.Value = EditorBeatmap.ControlPointInfo.GroupAt(nearestTimingPoint.Time); } } - - protected override void ConfigureTimeline(TimelineArea timelineArea) - { - base.ConfigureTimeline(timelineArea); - - timelineArea.Timeline.AlwaysShowControlPoints = true; - } } } From 3d5b57454efbad0da9bf19a099f6bf7b311a7965 Mon Sep 17 00:00:00 2001 From: jkh675 Date: Wed, 21 Aug 2024 16:21:49 +0800 Subject: [PATCH 2033/2493] Fix null reference --- osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs index 70c82576cc..dbdeaf442a 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs @@ -105,7 +105,8 @@ namespace osu.Game.Screens.Select.Carousel if (manager != null) hideRequested = manager.Hide; - copyBeatmapSetUrl += () => clipboard.SetText($@"{api.WebsiteRootUrl}/beatmapsets/{beatmapInfo.BeatmapSet.OnlineID}#{beatmapInfo.Ruleset.ShortName}/{beatmapInfo.OnlineID}"); + if (beatmapInfo.BeatmapSet != null) + copyBeatmapSetUrl += () => clipboard.SetText($@"{api.WebsiteRootUrl}/beatmapsets/{beatmapInfo.BeatmapSet.OnlineID}#{beatmapInfo.Ruleset.ShortName}/{beatmapInfo.OnlineID}"); Header.Children = new Drawable[] { From c92af710297fb7596ef34812e31b0aa442929234 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 21 Aug 2024 17:30:26 +0900 Subject: [PATCH 2034/2493] Add in-gameplay version of kiai star fountains/burst --- .../Visual/Menus/TestSceneStarFountain.cs | 39 ++++++-- osu.Game/Screens/Menu/StarFountain.cs | 28 ++++-- .../Screens/Play/KiaiGameplayFountains.cs | 94 +++++++++++++++++++ osu.Game/Screens/Play/Player.cs | 16 +++- 4 files changed, 158 insertions(+), 19 deletions(-) create mode 100644 osu.Game/Screens/Play/KiaiGameplayFountains.cs diff --git a/osu.Game.Tests/Visual/Menus/TestSceneStarFountain.cs b/osu.Game.Tests/Visual/Menus/TestSceneStarFountain.cs index bb327e5962..36e9375697 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneStarFountain.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneStarFountain.cs @@ -4,17 +4,17 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Graphics; -using osu.Framework.Testing; using osu.Framework.Utils; using osu.Game.Screens.Menu; +using osu.Game.Screens.Play; namespace osu.Game.Tests.Visual.Menus { [TestFixture] public partial class TestSceneStarFountain : OsuTestScene { - [SetUpSteps] - public void SetUpSteps() + [Test] + public void TestMenu() { AddStep("make fountains", () => { @@ -34,11 +34,7 @@ namespace osu.Game.Tests.Visual.Menus }, }; }); - } - [Test] - public void TestPew() - { AddRepeatStep("activate fountains sometimes", () => { foreach (var fountain in Children.OfType()) @@ -48,5 +44,34 @@ namespace osu.Game.Tests.Visual.Menus } }, 150); } + + [Test] + public void TestGameplay() + { + AddStep("make fountains", () => + { + Children = new[] + { + new KiaiGameplayFountains.GameplayStarFountain + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + X = 75, + }, + new KiaiGameplayFountains.GameplayStarFountain + { + Anchor = Anchor.BottomRight, + Origin = Anchor.BottomRight, + X = -75, + }, + }; + }); + + AddRepeatStep("activate fountains", () => + { + ((StarFountain)Children[0]).Shoot(1); + ((StarFountain)Children[1]).Shoot(-1); + }, 150); + } } } diff --git a/osu.Game/Screens/Menu/StarFountain.cs b/osu.Game/Screens/Menu/StarFountain.cs index dd5171c6be..92e9dd6df9 100644 --- a/osu.Game/Screens/Menu/StarFountain.cs +++ b/osu.Game/Screens/Menu/StarFountain.cs @@ -21,9 +21,11 @@ namespace osu.Game.Screens.Menu [BackgroundDependencyLoader] private void load() { - InternalChild = spewer = new StarFountainSpewer(); + InternalChild = spewer = CreateSpewer(); } + protected virtual StarFountainSpewer CreateSpewer() => new StarFountainSpewer(); + public void Shoot(int direction) => spewer.Shoot(direction); protected override void SkinChanged(ISkinSource skin) @@ -38,17 +40,23 @@ namespace osu.Game.Screens.Menu private const int particle_duration_max = 1000; private double? lastShootTime; - private int lastShootDirection; + + protected int LastShootDirection { get; private set; } protected override float ParticleGravity => 800; - private const double shoot_duration = 800; + protected virtual double ShootDuration => 800; [Resolved] private ISkinSource skin { get; set; } = null!; public StarFountainSpewer() - : base(null, 240, particle_duration_max) + : this(240) + { + } + + protected StarFountainSpewer(int perSecond) + : base(null, perSecond, particle_duration_max) { } @@ -67,16 +75,16 @@ namespace osu.Game.Screens.Menu StartAngle = getRandomVariance(4), EndAngle = getRandomVariance(2), EndScale = 2.2f + getRandomVariance(0.4f), - Velocity = new Vector2(getCurrentAngle(), -1400 + getRandomVariance(100)), + Velocity = new Vector2(GetCurrentAngle(), -1400 + getRandomVariance(100)), }; } - private float getCurrentAngle() + protected virtual float GetCurrentAngle() { - const float x_velocity_from_direction = 500; const float x_velocity_random_variance = 60; + const float x_velocity_from_direction = 500; - return lastShootDirection * x_velocity_from_direction * (float)(1 - 2 * (Clock.CurrentTime - lastShootTime!.Value) / shoot_duration) + getRandomVariance(x_velocity_random_variance); + return LastShootDirection * x_velocity_from_direction * (float)(1 - 2 * (Clock.CurrentTime - lastShootTime!.Value) / ShootDuration) + getRandomVariance(x_velocity_random_variance); } private ScheduledDelegate? deactivateDelegate; @@ -86,10 +94,10 @@ namespace osu.Game.Screens.Menu Active.Value = true; deactivateDelegate?.Cancel(); - deactivateDelegate = Scheduler.AddDelayed(() => Active.Value = false, shoot_duration); + deactivateDelegate = Scheduler.AddDelayed(() => Active.Value = false, ShootDuration); lastShootTime = Clock.CurrentTime; - lastShootDirection = direction; + LastShootDirection = direction; } private static float getRandomVariance(float variance) => RNG.NextSingle(-variance, variance); diff --git a/osu.Game/Screens/Play/KiaiGameplayFountains.cs b/osu.Game/Screens/Play/KiaiGameplayFountains.cs new file mode 100644 index 0000000000..7659c61123 --- /dev/null +++ b/osu.Game/Screens/Play/KiaiGameplayFountains.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. + +#nullable disable +using System; +using osu.Framework.Allocation; +using osu.Framework.Audio.Track; +using osu.Framework.Graphics; +using osu.Framework.Utils; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Graphics.Containers; +using osu.Game.Screens.Menu; + +namespace osu.Game.Screens.Play +{ + public partial class KiaiGameplayFountains : BeatSyncedContainer + { + private StarFountain leftFountain = null!; + private StarFountain rightFountain = null!; + + [BackgroundDependencyLoader] + private void load() + { + RelativeSizeAxes = Axes.Both; + + Children = new[] + { + leftFountain = new GameplayStarFountain + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + X = 75, + }, + rightFountain = new GameplayStarFountain + { + Anchor = Anchor.BottomRight, + Origin = Anchor.BottomRight, + X = -75, + }, + }; + } + + 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; + + leftFountain.Shoot(1); + rightFountain.Shoot(-1); + lastTrigger = Clock.CurrentTime; + } + + public partial class GameplayStarFountain : StarFountain + { + protected override StarFountainSpewer CreateSpewer() => new GameplayStarFountainSpewer(); + + private partial class GameplayStarFountainSpewer : StarFountainSpewer + { + protected override double ShootDuration => 400; + + public GameplayStarFountainSpewer() + : base(perSecond: 180) + { + } + + protected override float GetCurrentAngle() + { + const float x_velocity_from_direction = 450; + const float x_velocity_to_direction = 600; + + return LastShootDirection * RNG.NextSingle(x_velocity_from_direction, x_velocity_to_direction); + } + } + } + } +} diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 9a3d83782f..05f101f20c 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -405,8 +405,20 @@ namespace osu.Game.Screens.Play protected virtual GameplayClockContainer CreateGameplayClockContainer(WorkingBeatmap beatmap, double gameplayStart) => new MasterGameplayClockContainer(beatmap, gameplayStart); - private Drawable createUnderlayComponents() => - DimmableStoryboard = new DimmableStoryboard(GameplayState.Storyboard, GameplayState.Mods) { RelativeSizeAxes = Axes.Both }; + private Drawable createUnderlayComponents() + { + var container = new Container + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + DimmableStoryboard = new DimmableStoryboard(GameplayState.Storyboard, GameplayState.Mods) { RelativeSizeAxes = Axes.Both }, + new KiaiGameplayFountains(), + }, + }; + + return container; + } private Drawable createGameplayComponents(IWorkingBeatmap working) => new ScalingContainer(ScalingMode.Gameplay) { From eefd7cf0833e3e18a40b698c5a090b7934ec1205 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Wed, 21 Aug 2024 12:03:15 +0200 Subject: [PATCH 2035/2493] add back protection against perfect curve segments with > 3 points --- .../Objects/Legacy/ConvertHitObjectParser.cs | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs index d4e3053856..c518a3e8b2 100644 --- a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs @@ -347,17 +347,23 @@ namespace osu.Game.Rulesets.Objects.Legacy vertices[i] = new PathControlPoint { Position = points[i] }; // Edge-case rules (to match stable). - if (type == PathType.PERFECT_CURVE && FormatVersion < LegacyBeatmapEncoder.FIRST_LAZER_VERSION) + if (type == PathType.PERFECT_CURVE) { int endPointLength = endPoint == null ? 0 : 1; - if (vertices.Length + endPointLength != 3) - type = PathType.BEZIER; - else if (isLinear(points[0], points[1], endPoint ?? points[2])) + if (FormatVersion < LegacyBeatmapEncoder.FIRST_LAZER_VERSION) { - // osu-stable special-cased colinear perfect curves to a linear path - type = PathType.LINEAR; + if (vertices.Length + endPointLength != 3) + type = PathType.BEZIER; + else if (isLinear(points[0], points[1], endPoint ?? points[2])) + { + // osu-stable special-cased colinear perfect curves to a linear path + type = PathType.LINEAR; + } } + else if (vertices.Length + endPointLength > 3) + // Lazer supports perfect curves with less than 3 points and colinear points + type = PathType.BEZIER; } // The first control point must have a definite type. From 28d0a245556e3be98ad2d3612358d86ead9e0e27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 21 Aug 2024 12:27:56 +0200 Subject: [PATCH 2036/2493] Fix the fix The more proper way to do this would be to address the underlying issue, which is https://github.com/ppy/osu/issues/29546, but let's do this locally for now. --- osu.Game/Screens/Ranking/FavouriteButton.cs | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Ranking/FavouriteButton.cs b/osu.Game/Screens/Ranking/FavouriteButton.cs index bb4f25080c..aecaf7c5b9 100644 --- a/osu.Game/Screens/Ranking/FavouriteButton.cs +++ b/osu.Game/Screens/Ranking/FavouriteButton.cs @@ -78,8 +78,11 @@ namespace osu.Game.Screens.Ranking { Logger.Error(e, $"Failed to fetch beatmap info: {e.Message}"); - Schedule(() => loading.Hide()); - Enabled.Value = false; + Schedule(() => + { + loading.Hide(); + Enabled.Value = false; + }); }; api.Queue(beatmapSetRequest); } @@ -109,8 +112,12 @@ namespace osu.Game.Screens.Ranking favouriteRequest.Failure += e => { Logger.Error(e, $"Failed to {actionType.ToString().ToLowerInvariant()} beatmap: {e.Message}"); - Enabled.Value = true; - loading.Hide(); + + Schedule(() => + { + Enabled.Value = true; + loading.Hide(); + }); }; api.Queue(favouriteRequest); From 094b184191d241212a673c7074cdc8d85d2ee863 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Wed, 21 Aug 2024 12:28:56 +0200 Subject: [PATCH 2037/2493] snap the slider duration in normal drag --- .../Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs index 785febab4b..691c053e4d 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs @@ -265,6 +265,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders else { double minDistance = distanceSnapProvider?.GetBeatSnapDistanceAt(HitObject, false) * oldVelocityMultiplier ?? 1; + // Add a small amount to the proposed distance to make it easier to snap to the full length of the slider. + proposedDistance = distanceSnapProvider?.FindSnappedDistance(HitObject, (float)proposedDistance + 1) ?? proposedDistance; proposedDistance = MathHelper.Clamp(proposedDistance, minDistance, HitObject.Path.CalculatedDistance); } From 423feadd64032a0dd6bfb08302c3aba58d7e2798 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Wed, 21 Aug 2024 14:12:58 +0200 Subject: [PATCH 2038/2493] Revert "add arrow indicator" This reverts commit 1ce9e97fd45bb81f13a8e6a799af43d6342922af. --- .../Edit/Compose/Components/Timeline/SamplePointPiece.cs | 8 -------- 1 file changed, 8 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs index 9c42d072d1..6cd7044943 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs @@ -12,7 +12,6 @@ using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; -using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; using osu.Game.Audio; @@ -166,13 +165,6 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline [Resolved(canBeNull: true)] private EditorBeatmap beatmap { get; set; } = null!; - protected override Drawable CreateArrow() => new Triangle - { - Size = new Vector2(20), - Anchor = Anchor.Centre, - Origin = Anchor.TopCentre, - }; - public SampleEditPopover(HitObject hitObject) { this.hitObject = hitObject; From 5f88435d960e18aa0f7121801ac144940cee3efc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 21 Aug 2024 15:28:51 +0200 Subject: [PATCH 2039/2493] Add support for retrieving submit/rank date from local metadata cache in version 2 Closes https://github.com/ppy/osu/issues/22416. --- .../LocalCachedBeatmapMetadataSource.cs | 147 ++++++++++++++---- 1 file changed, 118 insertions(+), 29 deletions(-) diff --git a/osu.Game/Beatmaps/LocalCachedBeatmapMetadataSource.cs b/osu.Game/Beatmaps/LocalCachedBeatmapMetadataSource.cs index 27bc803449..96817571f6 100644 --- a/osu.Game/Beatmaps/LocalCachedBeatmapMetadataSource.cs +++ b/osu.Game/Beatmaps/LocalCachedBeatmapMetadataSource.cs @@ -80,6 +80,8 @@ namespace osu.Game.Beatmaps public bool TryLookup(BeatmapInfo beatmapInfo, out OnlineBeatmapMetadata? onlineMetadata) { + Debug.Assert(beatmapInfo.BeatmapSet != null); + if (!Available) { onlineMetadata = null; @@ -94,43 +96,21 @@ namespace osu.Game.Beatmaps return false; } - Debug.Assert(beatmapInfo.BeatmapSet != null); - try { using (var db = new SqliteConnection(string.Concat(@"Data Source=", storage.GetFullPath(@"online.db", true)))) { db.Open(); - using (var cmd = db.CreateCommand()) + switch (getCacheVersion(db)) { - cmd.CommandText = - @"SELECT beatmapset_id, beatmap_id, approved, user_id, checksum, last_update FROM osu_beatmaps WHERE checksum = @MD5Hash OR beatmap_id = @OnlineID OR filename = @Path"; + case 1: + // will eventually become irrelevant due to the monthly recycling of local caches + // can be removed 20250221 + return queryCacheVersion1(db, beatmapInfo, out onlineMetadata); - cmd.Parameters.Add(new SqliteParameter(@"@MD5Hash", beatmapInfo.MD5Hash)); - cmd.Parameters.Add(new SqliteParameter(@"@OnlineID", beatmapInfo.OnlineID)); - cmd.Parameters.Add(new SqliteParameter(@"@Path", beatmapInfo.Path)); - - using (var reader = cmd.ExecuteReader()) - { - if (reader.Read()) - { - logForModel(beatmapInfo.BeatmapSet, $@"Cached local retrieval for {beatmapInfo}."); - - onlineMetadata = new OnlineBeatmapMetadata - { - BeatmapSetID = reader.GetInt32(0), - BeatmapID = reader.GetInt32(1), - BeatmapStatus = (BeatmapOnlineStatus)reader.GetByte(2), - BeatmapSetStatus = (BeatmapOnlineStatus)reader.GetByte(2), - AuthorID = reader.GetInt32(3), - MD5Hash = reader.GetString(4), - LastUpdated = reader.GetDateTimeOffset(5), - // TODO: DateSubmitted and DateRanked are not provided by local cache. - }; - return true; - } - } + case 2: + return queryCacheVersion2(db, beatmapInfo, out onlineMetadata); } } } @@ -211,6 +191,115 @@ namespace osu.Game.Beatmaps }); } + private int getCacheVersion(SqliteConnection connection) + { + using (var cmd = connection.CreateCommand()) + { + cmd.CommandText = @"SELECT COUNT(1) FROM `sqlite_master` WHERE `type` = 'table' AND `name` = 'schema_version'"; + + using var reader = cmd.ExecuteReader(); + + if (!reader.Read()) + throw new InvalidOperationException("Error when attempting to check for existence of `schema_version` table."); + + // No versioning table means that this is the very first version of the schema. + if (reader.GetInt32(0) == 0) + return 1; + } + + using (var cmd = connection.CreateCommand()) + { + cmd.CommandText = @"SELECT `number` FROM `schema_version`"; + + using var reader = cmd.ExecuteReader(); + + if (!reader.Read()) + throw new InvalidOperationException("Error when attempting to query schema version."); + + return reader.GetInt32(0); + } + } + + private bool queryCacheVersion1(SqliteConnection db, BeatmapInfo beatmapInfo, out OnlineBeatmapMetadata? onlineMetadata) + { + Debug.Assert(beatmapInfo.BeatmapSet != null); + + using var cmd = db.CreateCommand(); + + cmd.CommandText = + @"SELECT beatmapset_id, beatmap_id, approved, user_id, checksum, last_update FROM osu_beatmaps WHERE checksum = @MD5Hash OR beatmap_id = @OnlineID OR filename = @Path"; + + cmd.Parameters.Add(new SqliteParameter(@"@MD5Hash", beatmapInfo.MD5Hash)); + cmd.Parameters.Add(new SqliteParameter(@"@OnlineID", beatmapInfo.OnlineID)); + cmd.Parameters.Add(new SqliteParameter(@"@Path", beatmapInfo.Path)); + + using var reader = cmd.ExecuteReader(); + + if (reader.Read()) + { + logForModel(beatmapInfo.BeatmapSet, $@"Cached local retrieval for {beatmapInfo} (cache version 1)."); + + onlineMetadata = new OnlineBeatmapMetadata + { + BeatmapSetID = reader.GetInt32(0), + BeatmapID = reader.GetInt32(1), + BeatmapStatus = (BeatmapOnlineStatus)reader.GetByte(2), + BeatmapSetStatus = (BeatmapOnlineStatus)reader.GetByte(2), + AuthorID = reader.GetInt32(3), + MD5Hash = reader.GetString(4), + LastUpdated = reader.GetDateTimeOffset(5), + // TODO: DateSubmitted and DateRanked are not provided by local cache in this version. + }; + return true; + } + + onlineMetadata = null; + return false; + } + + private bool queryCacheVersion2(SqliteConnection db, BeatmapInfo beatmapInfo, out OnlineBeatmapMetadata? onlineMetadata) + { + Debug.Assert(beatmapInfo.BeatmapSet != null); + + using var cmd = db.CreateCommand(); + + cmd.CommandText = + """ + SELECT `b`.`beatmapset_id`, `b`.`beatmap_id`, `b`.`approved`, `b`.`user_id`, `b`.`checksum`, `b`.`last_update`, `s`.`submit_date`, `s`.`approved_date` + FROM `osu_beatmaps` AS `b` + JOIN `osu_beatmapsets` AS `s` ON `s`.`beatmapset_id` = `b`.`beatmapset_id` + WHERE `b`.`checksum` = @MD5Hash OR `b`.`beatmap_id` = @OnlineID OR `b`.`filename` = @Path + """; + + cmd.Parameters.Add(new SqliteParameter(@"@MD5Hash", beatmapInfo.MD5Hash)); + cmd.Parameters.Add(new SqliteParameter(@"@OnlineID", beatmapInfo.OnlineID)); + cmd.Parameters.Add(new SqliteParameter(@"@Path", beatmapInfo.Path)); + + using var reader = cmd.ExecuteReader(); + + if (reader.Read()) + { + logForModel(beatmapInfo.BeatmapSet, $@"Cached local retrieval for {beatmapInfo} (cache version 2)."); + + onlineMetadata = new OnlineBeatmapMetadata + { + BeatmapSetID = reader.GetInt32(0), + BeatmapID = reader.GetInt32(1), + BeatmapStatus = (BeatmapOnlineStatus)reader.GetByte(2), + BeatmapSetStatus = (BeatmapOnlineStatus)reader.GetByte(2), + AuthorID = reader.GetInt32(3), + MD5Hash = reader.GetString(4), + LastUpdated = reader.GetDateTimeOffset(5), + DateSubmitted = reader.GetDateTimeOffset(6), + DateRanked = reader.GetDateTimeOffset(7), + }; + return true; + } + + onlineMetadata = null; + return false; + } + private static void log(string message) => Logger.Log($@"[{nameof(LocalCachedBeatmapMetadataSource)}] {message}", LoggingTarget.Database); From 843b10ef34a222ff938bc904597569f1862b8e5b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 22 Aug 2024 01:05:47 +0900 Subject: [PATCH 2040/2493] Add back incorrectly removed control point display toggle --- .../Compose/Components/Timeline/Timeline.cs | 29 ++++++++++++++++++- .../Screens/Edit/EditorScreenWithTimeline.cs | 10 ++++++- osu.Game/Screens/Edit/Timing/TimingScreen.cs | 8 +++++ 3 files changed, 45 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs index 840f1311db..a9b0b5c286 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs @@ -28,6 +28,21 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline private readonly Drawable userContent; + private bool alwaysShowControlPoints; + + public bool AlwaysShowControlPoints + { + get => alwaysShowControlPoints; + set + { + if (value == alwaysShowControlPoints) + return; + + alwaysShowControlPoints = value; + controlPointsVisible.TriggerChange(); + } + } + [Resolved] private EditorClock editorClock { get; set; } = null!; @@ -63,7 +78,10 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline private TimelineTickDisplay ticks = null!; + private TimelineTimingChangeDisplay controlPoints = null!; + private Bindable waveformOpacity = null!; + private Bindable controlPointsVisible = null!; private Bindable ticksVisible = null!; private double trackLengthForZoom; @@ -93,7 +111,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline AddRange(new Drawable[] { ticks = new TimelineTickDisplay(), - new TimelineTimingChangeDisplay + controlPoints = new TimelineTimingChangeDisplay { RelativeSizeAxes = Axes.Both, Anchor = Anchor.CentreLeft, @@ -129,6 +147,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline }); waveformOpacity = config.GetBindable(OsuSetting.EditorWaveformOpacity); + controlPointsVisible = config.GetBindable(OsuSetting.EditorTimelineShowTimingChanges); ticksVisible = config.GetBindable(OsuSetting.EditorTimelineShowTicks); track.BindTo(editorClock.Track); @@ -162,6 +181,14 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline waveformOpacity.BindValueChanged(_ => updateWaveformOpacity(), true); ticksVisible.BindValueChanged(visible => ticks.FadeTo(visible.NewValue ? 1 : 0, 200, Easing.OutQuint), true); + + controlPointsVisible.BindValueChanged(visible => + { + if (visible.NewValue || alwaysShowControlPoints) + controlPoints.FadeIn(400, Easing.OutQuint); + else + controlPoints.FadeOut(200, Easing.OutQuint); + }, true); } private void updateWaveformOpacity() => diff --git a/osu.Game/Screens/Edit/EditorScreenWithTimeline.cs b/osu.Game/Screens/Edit/EditorScreenWithTimeline.cs index 5bbf293e0a..01908e45c7 100644 --- a/osu.Game/Screens/Edit/EditorScreenWithTimeline.cs +++ b/osu.Game/Screens/Edit/EditorScreenWithTimeline.cs @@ -106,10 +106,18 @@ namespace osu.Game.Screens.Edit MainContent.Add(content); content.FadeInFromZero(300, Easing.OutQuint); - LoadComponentAsync(TimelineArea = new TimelineArea(CreateTimelineContent()), timelineContent.Add); + LoadComponentAsync(TimelineArea = new TimelineArea(CreateTimelineContent()), timeline => + { + ConfigureTimeline(timeline); + timelineContent.Add(timeline); + }); }); } + protected virtual void ConfigureTimeline(TimelineArea timelineArea) + { + } + protected abstract Drawable CreateMainContent(); protected virtual Drawable CreateTimelineContent() => new Container(); diff --git a/osu.Game/Screens/Edit/Timing/TimingScreen.cs b/osu.Game/Screens/Edit/Timing/TimingScreen.cs index 3f911f5067..67d4429be8 100644 --- a/osu.Game/Screens/Edit/Timing/TimingScreen.cs +++ b/osu.Game/Screens/Edit/Timing/TimingScreen.cs @@ -6,6 +6,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Screens.Edit.Compose.Components.Timeline; namespace osu.Game.Screens.Edit.Timing { @@ -53,5 +54,12 @@ namespace osu.Game.Screens.Edit.Timing SelectedGroup.Value = EditorBeatmap.ControlPointInfo.GroupAt(nearestTimingPoint.Time); } } + + protected override void ConfigureTimeline(TimelineArea timelineArea) + { + base.ConfigureTimeline(timelineArea); + + timelineArea.Timeline.AlwaysShowControlPoints = true; + } } } From fb5fb78fd31fc5ead2106a641d14595c4a299203 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 22 Aug 2024 01:09:22 +0900 Subject: [PATCH 2041/2493] Move zero marker below control points to avoid common overlap scenario --- .../Edit/Compose/Components/Timeline/Timeline.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs index a9b0b5c286..aea8d02838 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs @@ -111,6 +111,14 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline AddRange(new Drawable[] { ticks = new TimelineTickDisplay(), + new Box + { + Name = "zero marker", + RelativeSizeAxes = Axes.Y, + Width = TimelineTickDisplay.TICK_WIDTH / 2, + Origin = Anchor.TopCentre, + Colour = colourProvider.Background1, + }, controlPoints = new TimelineTimingChangeDisplay { RelativeSizeAxes = Axes.Both, @@ -136,14 +144,6 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline userContent, } }, - new Box - { - Name = "zero marker", - RelativeSizeAxes = Axes.Y, - Width = TimelineTickDisplay.TICK_WIDTH / 2, - Origin = Anchor.TopCentre, - Colour = colourProvider.Background1, - }, }); waveformOpacity = config.GetBindable(OsuSetting.EditorWaveformOpacity); From 18a3ab2ffd4364813f094f42161070b757275f50 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 22 Aug 2024 01:45:43 +0900 Subject: [PATCH 2042/2493] Use "link" instead of "URL" --- osu.Game/Graphics/UserInterface/ExternalLinkButton.cs | 2 +- osu.Game/Online/Chat/ExternalLinkOpener.cs | 2 +- osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs | 2 +- osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Graphics/UserInterface/ExternalLinkButton.cs b/osu.Game/Graphics/UserInterface/ExternalLinkButton.cs index 7ba3d55162..dd0b906a17 100644 --- a/osu.Game/Graphics/UserInterface/ExternalLinkButton.cs +++ b/osu.Game/Graphics/UserInterface/ExternalLinkButton.cs @@ -86,7 +86,7 @@ namespace osu.Game.Graphics.UserInterface if (Link != null) { items.Add(new OsuMenuItem("Open", MenuItemType.Highlighted, () => host.OpenUrlExternally(Link))); - items.Add(new OsuMenuItem("Copy URL", MenuItemType.Standard, copyUrl)); + items.Add(new OsuMenuItem("Copy link", MenuItemType.Standard, copyUrl)); } return items.ToArray(); diff --git a/osu.Game/Online/Chat/ExternalLinkOpener.cs b/osu.Game/Online/Chat/ExternalLinkOpener.cs index 82ad4215c2..90fec5fafd 100644 --- a/osu.Game/Online/Chat/ExternalLinkOpener.cs +++ b/osu.Game/Online/Chat/ExternalLinkOpener.cs @@ -60,7 +60,7 @@ namespace osu.Game.Online.Chat }, new PopupDialogCancelButton { - Text = @"Copy URL to the clipboard", + Text = @"Copy link", Action = copyExternalLinkAction }, new PopupDialogCancelButton diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs index dbdeaf442a..851446c3e0 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs @@ -294,7 +294,7 @@ namespace osu.Game.Screens.Select.Carousel items.Add(new OsuMenuItem("Collections") { Items = collectionItems }); - items.Add(new OsuMenuItem("Copy URL", MenuItemType.Standard, () => copyBeatmapSetUrl?.Invoke())); + items.Add(new OsuMenuItem("Copy link", MenuItemType.Standard, () => copyBeatmapSetUrl?.Invoke())); if (hideRequested != null) items.Add(new OsuMenuItem(CommonStrings.ButtonsHide.ToSentence(), MenuItemType.Destructive, () => hideRequested(beatmapInfo))); diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs index 12db8f663a..5f4edaf070 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs @@ -294,7 +294,7 @@ namespace osu.Game.Screens.Select.Carousel if (beatmapSet.Beatmaps.Any(b => b.Hidden)) items.Add(new OsuMenuItem("Restore all hidden", MenuItemType.Standard, () => restoreHiddenRequested(beatmapSet))); - items.Add(new OsuMenuItem("Copy URL", MenuItemType.Standard, () => copyBeatmapSetUrl?.Invoke())); + items.Add(new OsuMenuItem("Copy link", MenuItemType.Standard, () => copyBeatmapSetUrl?.Invoke())); if (dialogOverlay != null) items.Add(new OsuMenuItem("Delete...", MenuItemType.Destructive, () => dialogOverlay.Push(new BeatmapDeleteDialog(beatmapSet)))); From 87123d99bf43803f466b3eee5949cf8c8e5406a6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 22 Aug 2024 02:08:01 +0900 Subject: [PATCH 2043/2493] Move URL implementation to extension methods and share with other usages --- osu.Game/Beatmaps/BeatmapInfoExtensions.cs | 13 +++++++++++++ osu.Game/Beatmaps/BeatmapSetInfoExtensions.cs | 16 ++++++++++++++++ .../BeatmapSet/BeatmapSetHeaderContent.cs | 3 ++- .../Select/Carousel/DrawableCarouselBeatmap.cs | 15 +++++++++------ .../Carousel/DrawableCarouselBeatmapSet.cs | 17 ++++++++++++----- 5 files changed, 52 insertions(+), 12 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapInfoExtensions.cs b/osu.Game/Beatmaps/BeatmapInfoExtensions.cs index b00d0ba316..a82a288239 100644 --- a/osu.Game/Beatmaps/BeatmapInfoExtensions.cs +++ b/osu.Game/Beatmaps/BeatmapInfoExtensions.cs @@ -2,6 +2,8 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Localisation; +using osu.Game.Online.API; +using osu.Game.Rulesets; using osu.Game.Screens.Select; namespace osu.Game.Beatmaps @@ -48,5 +50,16 @@ namespace osu.Game.Beatmaps } private static string getVersionString(IBeatmapInfo beatmapInfo) => string.IsNullOrEmpty(beatmapInfo.DifficultyName) ? string.Empty : $"[{beatmapInfo.DifficultyName}]"; + + /// + /// Get the beatmap info page URL, or null if unavailable. + /// + public static string? GetOnlineURL(this IBeatmapInfo beatmapInfo, IAPIProvider api, IRulesetInfo? ruleset = null) + { + if (beatmapInfo.OnlineID <= 0 || beatmapInfo.BeatmapSet == null) + return null; + + return $@"{api.WebsiteRootUrl}/beatmapsets/{beatmapInfo.BeatmapSet.OnlineID}#{ruleset?.ShortName ?? beatmapInfo.Ruleset.ShortName}/{beatmapInfo.OnlineID}"; + } } } diff --git a/osu.Game/Beatmaps/BeatmapSetInfoExtensions.cs b/osu.Game/Beatmaps/BeatmapSetInfoExtensions.cs index 965544da40..8a107ed486 100644 --- a/osu.Game/Beatmaps/BeatmapSetInfoExtensions.cs +++ b/osu.Game/Beatmaps/BeatmapSetInfoExtensions.cs @@ -6,6 +6,8 @@ using System.Linq; using osu.Game.Database; using osu.Game.Extensions; using osu.Game.Models; +using osu.Game.Online.API; +using osu.Game.Rulesets; namespace osu.Game.Beatmaps { @@ -29,5 +31,19 @@ namespace osu.Game.Beatmaps /// The name of the file to get the storage path of. public static RealmNamedFileUsage? GetFile(this IHasRealmFiles model, string filename) => model.Files.SingleOrDefault(f => string.Equals(f.Filename, filename, StringComparison.OrdinalIgnoreCase)); + + /// + /// Get the beatmapset info page URL, or null if unavailable. + /// + public static string? GetOnlineURL(this IBeatmapSetInfo beatmapSetInfo, IAPIProvider api, IRulesetInfo? ruleset = null) + { + if (beatmapSetInfo.OnlineID <= 0) + return null; + + if (ruleset != null) + return $@"{api.WebsiteRootUrl}/beatmapsets/{beatmapSetInfo.OnlineID}#{ruleset.ShortName}"; + + return $@"{api.WebsiteRootUrl}/beatmapsets/{beatmapSetInfo.OnlineID}"; + } } } diff --git a/osu.Game/Overlays/BeatmapSet/BeatmapSetHeaderContent.cs b/osu.Game/Overlays/BeatmapSet/BeatmapSetHeaderContent.cs index 7ff8352054..168056ea58 100644 --- a/osu.Game/Overlays/BeatmapSet/BeatmapSetHeaderContent.cs +++ b/osu.Game/Overlays/BeatmapSet/BeatmapSetHeaderContent.cs @@ -200,7 +200,8 @@ namespace osu.Game.Overlays.BeatmapSet private void updateExternalLink() { - if (externalLink != null) externalLink.Link = $@"{api.WebsiteRootUrl}/beatmapsets/{BeatmapSet.Value?.OnlineID}#{Picker.Beatmap.Value?.Ruleset.ShortName}/{Picker.Beatmap.Value?.OnlineID}"; + if (externalLink != null) + externalLink.Link = Picker.Beatmap.Value?.GetOnlineURL(api) ?? BeatmapSet.Value?.GetOnlineURL(api); } [BackgroundDependencyLoader] diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs index 851446c3e0..dd9f2226e9 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs @@ -55,7 +55,6 @@ namespace osu.Game.Screens.Select.Carousel private Action? selectRequested; private Action? hideRequested; - private Action? copyBeatmapSetUrl; private Triangles triangles = null!; @@ -82,6 +81,12 @@ namespace osu.Game.Screens.Select.Carousel [Resolved] private IBindable> mods { get; set; } = null!; + [Resolved] + private Clipboard clipboard { get; set; } = null!; + + [Resolved] + private IAPIProvider api { get; set; } = null!; + private IBindable starDifficultyBindable = null!; private CancellationTokenSource? starDifficultyCancellationSource; @@ -92,7 +97,7 @@ namespace osu.Game.Screens.Select.Carousel } [BackgroundDependencyLoader] - private void load(BeatmapManager? manager, SongSelect? songSelect, Clipboard clipboard, IAPIProvider api) + private void load(BeatmapManager? manager, SongSelect? songSelect, IAPIProvider api) { Header.Height = height; @@ -105,9 +110,6 @@ namespace osu.Game.Screens.Select.Carousel if (manager != null) hideRequested = manager.Hide; - if (beatmapInfo.BeatmapSet != null) - copyBeatmapSetUrl += () => clipboard.SetText($@"{api.WebsiteRootUrl}/beatmapsets/{beatmapInfo.BeatmapSet.OnlineID}#{beatmapInfo.Ruleset.ShortName}/{beatmapInfo.OnlineID}"); - Header.Children = new Drawable[] { background = new Box @@ -294,7 +296,8 @@ namespace osu.Game.Screens.Select.Carousel items.Add(new OsuMenuItem("Collections") { Items = collectionItems }); - items.Add(new OsuMenuItem("Copy link", MenuItemType.Standard, () => copyBeatmapSetUrl?.Invoke())); + if (beatmapInfo.GetOnlineURL(api) is string url) + items.Add(new OsuMenuItem("Copy link", MenuItemType.Standard, () => clipboard.SetText(url))); if (hideRequested != null) items.Add(new OsuMenuItem(CommonStrings.ButtonsHide.ToSentence(), MenuItemType.Destructive, () => hideRequested(beatmapInfo))); diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs index 5f4edaf070..3233347991 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs @@ -33,7 +33,6 @@ namespace osu.Game.Screens.Select.Carousel private Action restoreHiddenRequested = null!; private Action? viewDetails; - private Action? copyBeatmapSetUrl; [Resolved] private IDialogOverlay? dialogOverlay { get; set; } @@ -44,6 +43,15 @@ namespace osu.Game.Screens.Select.Carousel [Resolved] private RealmAccess realm { get; set; } = null!; + [Resolved] + private Clipboard clipboard { get; set; } = null!; + + [Resolved] + private IAPIProvider api { get; set; } = null!; + + [Resolved] + private IBindable ruleset { get; set; } = null!; + public IEnumerable DrawableBeatmaps => beatmapContainer?.IsLoaded != true ? Enumerable.Empty() : beatmapContainer.AliveChildren; private Container? beatmapContainer; @@ -70,7 +78,7 @@ namespace osu.Game.Screens.Select.Carousel } [BackgroundDependencyLoader] - private void load(BeatmapSetOverlay? beatmapOverlay, SongSelect? songSelect, Clipboard clipboard, IBindable ruleset, IAPIProvider api) + private void load(BeatmapSetOverlay? beatmapOverlay, SongSelect? songSelect) { if (songSelect != null) mainMenuItems = songSelect.CreateForwardNavigationMenuItemsForBeatmap(() => (((CarouselBeatmapSet)Item!).GetNextToSelect() as CarouselBeatmap)!.BeatmapInfo); @@ -83,8 +91,6 @@ namespace osu.Game.Screens.Select.Carousel if (beatmapOverlay != null) viewDetails = beatmapOverlay.FetchAndShowBeatmapSet; - - copyBeatmapSetUrl += () => clipboard.SetText($@"{api.WebsiteRootUrl}/beatmapsets/{beatmapSet.OnlineID}#{ruleset.Value.ShortName}"); } protected override void Update() @@ -294,7 +300,8 @@ namespace osu.Game.Screens.Select.Carousel if (beatmapSet.Beatmaps.Any(b => b.Hidden)) items.Add(new OsuMenuItem("Restore all hidden", MenuItemType.Standard, () => restoreHiddenRequested(beatmapSet))); - items.Add(new OsuMenuItem("Copy link", MenuItemType.Standard, () => copyBeatmapSetUrl?.Invoke())); + if (beatmapSet.GetOnlineURL(api, ruleset.Value) is string url) + items.Add(new OsuMenuItem("Copy link", MenuItemType.Standard, () => clipboard.SetText(url))); if (dialogOverlay != null) items.Add(new OsuMenuItem("Delete...", MenuItemType.Destructive, () => dialogOverlay.Push(new BeatmapDeleteDialog(beatmapSet)))); From ac5a3a095919b48d9777a2828f8fb989251fed0c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 22 Aug 2024 02:17:11 +0900 Subject: [PATCH 2044/2493] Remove one unused parameter --- osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs index dd9f2226e9..89ace49ccd 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs @@ -97,7 +97,7 @@ namespace osu.Game.Screens.Select.Carousel } [BackgroundDependencyLoader] - private void load(BeatmapManager? manager, SongSelect? songSelect, IAPIProvider api) + private void load(BeatmapManager? manager, SongSelect? songSelect) { Header.Height = height; From fc02b4b942ef23a783a619f2493e1ff92221e3b5 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 22 Aug 2024 05:39:57 +0900 Subject: [PATCH 2045/2493] Alter NRT usage --- osu.Game/Overlays/Mods/ModCustomisationPanel.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModCustomisationPanel.cs b/osu.Game/Overlays/Mods/ModCustomisationPanel.cs index 91d7fdda73..6cec5a35a8 100644 --- a/osu.Game/Overlays/Mods/ModCustomisationPanel.cs +++ b/osu.Game/Overlays/Mods/ModCustomisationPanel.cs @@ -215,12 +215,12 @@ namespace osu.Game.Overlays.Mods this.panel = panel; } - private InputManager? inputManager; + private InputManager inputManager = null!; protected override void LoadComplete() { base.LoadComplete(); - inputManager = GetContainingInputManager(); + inputManager = GetContainingInputManager()!; } protected override void Update() @@ -229,7 +229,7 @@ namespace osu.Game.Overlays.Mods if (ExpandedState.Value == ModCustomisationPanelState.ExpandedByHover) { - if (!ReceivePositionalInputAt(inputManager!.CurrentState.Mouse.Position) && inputManager.DraggedDrawable == null) + if (!ReceivePositionalInputAt(inputManager.CurrentState.Mouse.Position) && inputManager.DraggedDrawable == null) ExpandedState.Value = ModCustomisationPanelState.Collapsed; } } From 1efa6b7221b32130803bfa0e02e781b99a33fb4a Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 22 Aug 2024 05:40:43 +0900 Subject: [PATCH 2046/2493] Merge if branches --- osu.Game/Overlays/Mods/ModCustomisationPanel.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModCustomisationPanel.cs b/osu.Game/Overlays/Mods/ModCustomisationPanel.cs index 6cec5a35a8..522481bc6b 100644 --- a/osu.Game/Overlays/Mods/ModCustomisationPanel.cs +++ b/osu.Game/Overlays/Mods/ModCustomisationPanel.cs @@ -227,10 +227,11 @@ namespace osu.Game.Overlays.Mods { base.Update(); - if (ExpandedState.Value == ModCustomisationPanelState.ExpandedByHover) + if (ExpandedState.Value == ModCustomisationPanelState.ExpandedByHover + && !ReceivePositionalInputAt(inputManager.CurrentState.Mouse.Position) + && inputManager.DraggedDrawable == null) { - if (!ReceivePositionalInputAt(inputManager.CurrentState.Mouse.Position) && inputManager.DraggedDrawable == null) - ExpandedState.Value = ModCustomisationPanelState.Collapsed; + ExpandedState.Value = ModCustomisationPanelState.Collapsed; } } } From a669c53df7ea119792c3c42007791ba5941c6635 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 22 Aug 2024 06:00:07 +0900 Subject: [PATCH 2047/2493] Add failing test cases --- .../UserInterface/TestSceneMainMenuButton.cs | 73 +++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneMainMenuButton.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneMainMenuButton.cs index 98f2b129ff..e534547c27 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneMainMenuButton.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneMainMenuButton.cs @@ -6,6 +6,7 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; +using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Localisation; using osu.Game.Online.API; @@ -71,6 +72,7 @@ namespace osu.Game.Tests.Visual.UserInterface NotificationOverlay notificationOverlay = null!; DependencyProvidingContainer buttonContainer = null!; + AddStep("set intro played flag", () => Dependencies.Get().SetValue(Static.DailyChallengeIntroPlayed, true)); AddStep("beatmap of the day active", () => metadataClient.DailyChallengeUpdated(new DailyChallengeInfo { RoomID = 1234, @@ -96,6 +98,7 @@ namespace osu.Game.Tests.Visual.UserInterface }, }; }); + AddAssert("intro played flag reset", () => !Dependencies.Get().Get(Static.DailyChallengeIntroPlayed)); AddAssert("notification posted", () => notificationOverlay.AllNotifications.Count(), () => Is.EqualTo(1)); AddStep("clear notifications", () => @@ -103,15 +106,85 @@ namespace osu.Game.Tests.Visual.UserInterface foreach (var notification in notificationOverlay.AllNotifications) notification.Close(runFlingAnimation: false); }); + + AddStep("set intro played flag", () => Dependencies.Get().SetValue(Static.DailyChallengeIntroPlayed, true)); + AddStep("beatmap of the day not active", () => metadataClient.DailyChallengeUpdated(null)); AddAssert("no notification posted", () => notificationOverlay.AllNotifications.Count(), () => Is.Zero); + AddAssert("intro played flag still set", () => Dependencies.Get().Get(Static.DailyChallengeIntroPlayed)); AddStep("hide button's parent", () => buttonContainer.Hide()); + AddStep("beatmap of the day active", () => metadataClient.DailyChallengeUpdated(new DailyChallengeInfo { RoomID = 1234, })); AddAssert("no notification posted", () => notificationOverlay.AllNotifications.Count(), () => Is.Zero); + AddAssert("intro played flag still set", () => Dependencies.Get().Get(Static.DailyChallengeIntroPlayed)); + } + + [Test] + public void TestDailyChallengeButtonOldChallenge() + { + AddStep("set up API", () => dummyAPI.HandleRequest = req => + { + switch (req) + { + case GetRoomRequest getRoomRequest: + if (getRoomRequest.RoomId != 1234) + return false; + + var beatmap = CreateAPIBeatmap(); + beatmap.OnlineID = 1001; + getRoomRequest.TriggerSuccess(new Room + { + RoomID = { Value = 1234 }, + Playlist = + { + new PlaylistItem(beatmap) + }, + StartDate = { Value = DateTimeOffset.Now.AddMinutes(-50) }, + EndDate = { Value = DateTimeOffset.Now.AddSeconds(30) } + }); + return true; + + default: + return false; + } + }); + + NotificationOverlay notificationOverlay = null!; + + AddStep("beatmap of the day not active", () => metadataClient.DailyChallengeUpdated(null)); + AddStep("add content", () => + { + notificationOverlay = new NotificationOverlay(); + Children = new Drawable[] + { + notificationOverlay, + new DependencyProvidingContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + AutoSizeAxes = Axes.Both, + CachedDependencies = [(typeof(INotificationOverlay), notificationOverlay)], + Child = new DailyChallengeButton(@"button-default-select", new Color4(102, 68, 204, 255), _ => { }, 0, Key.D) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + ButtonSystemState = ButtonSystemState.TopLevel, + }, + }, + }; + }); + + AddStep("set intro played flag", () => Dependencies.Get().SetValue(Static.DailyChallengeIntroPlayed, true)); + AddStep("beatmap of the day active", () => metadataClient.DailyChallengeUpdated(new DailyChallengeInfo + { + RoomID = 1234 + })); + AddAssert("no notification posted", () => notificationOverlay.AllNotifications.Count(), () => Is.Zero); + AddAssert("intro played flag reset", () => !Dependencies.Get().Get(Static.DailyChallengeIntroPlayed)); } } } From 922814fab37f7f915f80265740640f049acd2056 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 22 Aug 2024 06:00:37 +0900 Subject: [PATCH 2048/2493] Fix flag reset on connection dropouts --- osu.Game/Screens/Menu/DailyChallengeButton.cs | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/osu.Game/Screens/Menu/DailyChallengeButton.cs b/osu.Game/Screens/Menu/DailyChallengeButton.cs index d47866ef73..4dbebf0ae9 100644 --- a/osu.Game/Screens/Menu/DailyChallengeButton.cs +++ b/osu.Game/Screens/Menu/DailyChallengeButton.cs @@ -132,7 +132,7 @@ namespace osu.Game.Screens.Menu } } - private long? lastNotifiedDailyChallengeRoomId; + private long? lastDailyChallengeRoomID; private void dailyChallengeChanged(ValueChangedEvent _) { @@ -152,19 +152,19 @@ namespace osu.Game.Screens.Menu roomRequest.Success += room => { - // force showing intro on the first time when a new daily challenge is up. - statics.SetValue(Static.DailyChallengeIntroPlayed, false); - Room = room; cover.OnlineInfo = TooltipContent = room.Playlist.FirstOrDefault()?.Beatmap.BeatmapSet as APIBeatmapSet; - // We only want to notify the user if a new challenge recently went live. - if (room.StartDate.Value != null - && Math.Abs((DateTimeOffset.Now - room.StartDate.Value!.Value).TotalSeconds) < 1800 - && room.RoomID.Value != lastNotifiedDailyChallengeRoomId) + if (room.StartDate.Value != null && room.RoomID.Value != lastDailyChallengeRoomID) { - lastNotifiedDailyChallengeRoomId = room.RoomID.Value; - notificationOverlay?.Post(new NewDailyChallengeNotification(room)); + lastDailyChallengeRoomID = room.RoomID.Value; + + // new challenge is live, reset intro played static. + statics.SetValue(Static.DailyChallengeIntroPlayed, false); + + // we only want to notify the user if the new challenge just went live. + if (Math.Abs((DateTimeOffset.Now - room.StartDate.Value!.Value).TotalSeconds) < 1800) + notificationOverlay?.Post(new NewDailyChallengeNotification(room)); } updateCountdown(); From 7f5f3a4589acddc13e37fd901bfb36ac915592ff Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 22 Aug 2024 13:57:19 +0900 Subject: [PATCH 2049/2493] Fix mod icons potentially showing incorrectly at daily challenge intro Prefer using the beatmap's rulesets over the current user selection. Closes https://github.com/ppy/osu/issues/29559. --- .../DailyChallenge/DailyChallengeIntro.cs | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeIntro.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeIntro.cs index e59031f663..47785c8868 100644 --- a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeIntro.cs +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeIntro.cs @@ -93,14 +93,15 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge protected override BackgroundScreen CreateBackground() => new DailyChallengeIntroBackgroundScreen(colourProvider); [BackgroundDependencyLoader] - private void load(BeatmapDifficultyCache difficultyCache, BeatmapModelDownloader beatmapDownloader, OsuConfigManager config, AudioManager audio) + private void load(RulesetStore rulesets, BeatmapDifficultyCache difficultyCache, BeatmapModelDownloader beatmapDownloader, OsuConfigManager config, AudioManager audio) { const float horizontal_info_size = 500f; - Ruleset ruleset = Ruleset.Value.CreateInstance(); - StarRatingDisplay starRatingDisplay; + IBeatmapInfo beatmap = item.Beatmap; + Ruleset ruleset = rulesets.GetRuleset(item.Beatmap.Ruleset.ShortName)?.CreateInstance() ?? Ruleset.Value.CreateInstance(); + InternalChildren = new Drawable[] { beatmapAvailabilityTracker, @@ -242,13 +243,13 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge Origin = Anchor.TopCentre, Shear = new Vector2(-OsuGame.SHEAR, 0f), MaxWidth = horizontal_info_size, - Text = item.Beatmap.BeatmapSet!.Metadata.GetDisplayTitleRomanisable(false), + Text = beatmap.BeatmapSet!.Metadata.GetDisplayTitleRomanisable(false), Padding = new MarginPadding { Horizontal = 5f }, Font = OsuFont.GetFont(size: 26), }, new TruncatingSpriteText { - Text = $"Difficulty: {item.Beatmap.DifficultyName}", + Text = $"Difficulty: {beatmap.DifficultyName}", Font = OsuFont.GetFont(size: 20, italics: true), MaxWidth = horizontal_info_size, Shear = new Vector2(-OsuGame.SHEAR, 0f), @@ -257,7 +258,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge }, new TruncatingSpriteText { - Text = $"by {item.Beatmap.Metadata.Author.Username}", + Text = $"by {beatmap.Metadata.Author.Username}", Font = OsuFont.GetFont(size: 16, italics: true), MaxWidth = horizontal_info_size, Shear = new Vector2(-OsuGame.SHEAR, 0f), @@ -309,14 +310,14 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge } }; - starDifficulty = difficultyCache.GetBindableDifficulty(item.Beatmap); + starDifficulty = difficultyCache.GetBindableDifficulty(beatmap); starDifficulty.BindValueChanged(star => { if (star.NewValue != null) starRatingDisplay.Current.Value = star.NewValue.Value; }, true); - LoadComponentAsync(new OnlineBeatmapSetCover(item.Beatmap.BeatmapSet as IBeatmapSetOnlineInfo) + LoadComponentAsync(new OnlineBeatmapSetCover(beatmap.BeatmapSet as IBeatmapSetOnlineInfo) { RelativeSizeAxes = Axes.Both, Anchor = Anchor.Centre, @@ -334,8 +335,8 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge if (config.Get(OsuSetting.AutomaticallyDownloadMissingBeatmaps)) { - if (!beatmapManager.IsAvailableLocally(new BeatmapSetInfo { OnlineID = item.Beatmap.BeatmapSet!.OnlineID })) - beatmapDownloader.Download(item.Beatmap.BeatmapSet!, config.Get(OsuSetting.PreferNoVideo)); + if (!beatmapManager.IsAvailableLocally(new BeatmapSetInfo { OnlineID = beatmap.BeatmapSet!.OnlineID })) + beatmapDownloader.Download(beatmap.BeatmapSet!, config.Get(OsuSetting.PreferNoVideo)); } dateWindupSample = audio.Samples.Get(@"DailyChallenge/date-windup"); From f068b7a521c8ce8b29da9161f7ef4c7ab92429ec Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 22 Aug 2024 13:43:10 +0900 Subject: [PATCH 2050/2493] Move copy-to-url method to `OsuGame` to centralise toast popup support --- .../UserInterface/ExternalLinkButton.cs | 23 +++++-------------- osu.Game/Localisation/ToastStrings.cs | 4 ++-- osu.Game/OsuGame.cs | 11 ++++++++- .../Carousel/DrawableCarouselBeatmap.cs | 7 +++--- .../Carousel/DrawableCarouselBeatmapSet.cs | 7 +++--- 5 files changed, 24 insertions(+), 28 deletions(-) diff --git a/osu.Game/Graphics/UserInterface/ExternalLinkButton.cs b/osu.Game/Graphics/UserInterface/ExternalLinkButton.cs index dd0b906a17..806b7a10b8 100644 --- a/osu.Game/Graphics/UserInterface/ExternalLinkButton.cs +++ b/osu.Game/Graphics/UserInterface/ExternalLinkButton.cs @@ -10,9 +10,6 @@ using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; using osu.Framework.Localisation; -using osu.Framework.Platform; -using osu.Game.Overlays; -using osu.Game.Overlays.OSD; using osuTK; using osuTK.Graphics; @@ -25,13 +22,7 @@ namespace osu.Game.Graphics.UserInterface private Color4 hoverColour; [Resolved] - private GameHost host { get; set; } = null!; - - [Resolved] - private Clipboard clipboard { get; set; } = null!; - - [Resolved] - private OnScreenDisplay? onScreenDisplay { get; set; } + private OsuGame? game { get; set; } private readonly SpriteIcon linkIcon; @@ -71,7 +62,7 @@ namespace osu.Game.Graphics.UserInterface protected override bool OnClick(ClickEvent e) { if (Link != null) - host.OpenUrlExternally(Link); + game?.OpenUrlExternally(Link); return true; } @@ -85,7 +76,7 @@ namespace osu.Game.Graphics.UserInterface if (Link != null) { - items.Add(new OsuMenuItem("Open", MenuItemType.Highlighted, () => host.OpenUrlExternally(Link))); + items.Add(new OsuMenuItem("Open", MenuItemType.Highlighted, () => game?.OpenUrlExternally(Link))); items.Add(new OsuMenuItem("Copy link", MenuItemType.Standard, copyUrl)); } @@ -95,11 +86,9 @@ namespace osu.Game.Graphics.UserInterface private void copyUrl() { - if (Link != null) - { - clipboard.SetText(Link); - onScreenDisplay?.Display(new CopyUrlToast()); - } + if (Link == null) return; + + game?.CopyUrlToClipboard(Link); } } } diff --git a/osu.Game/Localisation/ToastStrings.cs b/osu.Game/Localisation/ToastStrings.cs index 942540cfc5..49e8d00371 100644 --- a/osu.Game/Localisation/ToastStrings.cs +++ b/osu.Game/Localisation/ToastStrings.cs @@ -45,9 +45,9 @@ namespace osu.Game.Localisation public static LocalisableString SkinSaved => new TranslatableString(getKey(@"skin_saved"), @"Skin saved"); /// - /// "URL copied" + /// "Link copied to clipboard" /// - public static LocalisableString UrlCopied => new TranslatableString(getKey(@"url_copied"), @"URL copied"); + public static LocalisableString UrlCopied => new TranslatableString(getKey(@"url_copied"), @"Link copied to clipboard"); /// /// "Speed changed to {0:N2}x" diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 7e4d2ccf39..089db3b698 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -54,6 +54,7 @@ using osu.Game.Overlays.BeatmapListing; using osu.Game.Overlays.Mods; using osu.Game.Overlays.Music; using osu.Game.Overlays.Notifications; +using osu.Game.Overlays.OSD; using osu.Game.Overlays.SkinEditor; using osu.Game.Overlays.Toolbar; using osu.Game.Overlays.Volume; @@ -142,6 +143,8 @@ namespace osu.Game private Container overlayOffsetContainer; + private OnScreenDisplay onScreenDisplay; + [Resolved] private FrameworkConfigManager frameworkConfig { get; set; } @@ -497,6 +500,12 @@ namespace osu.Game } }); + public void CopyUrlToClipboard(string url) => waitForReady(() => onScreenDisplay, _ => + { + dependencies.Get().SetText(url); + onScreenDisplay.Display(new CopyUrlToast()); + }); + public void OpenUrlExternally(string url, bool forceBypassExternalUrlWarning = false) => waitForReady(() => externalLinkOpener, _ => { bool isTrustedDomain; @@ -1078,7 +1087,7 @@ namespace osu.Game loadComponentSingleFile(volume = new VolumeOverlay(), leftFloatingOverlayContent.Add, true); - var onScreenDisplay = new OnScreenDisplay(); + onScreenDisplay = new OnScreenDisplay(); onScreenDisplay.BeginTracking(this, frameworkConfig); onScreenDisplay.BeginTracking(this, LocalConfig); diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs index 89ace49ccd..66d1480fdc 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs @@ -17,7 +17,6 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; -using osu.Framework.Platform; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Drawables; using osu.Game.Collections; @@ -82,10 +81,10 @@ namespace osu.Game.Screens.Select.Carousel private IBindable> mods { get; set; } = null!; [Resolved] - private Clipboard clipboard { get; set; } = null!; + private IAPIProvider api { get; set; } = null!; [Resolved] - private IAPIProvider api { get; set; } = null!; + private OsuGame? game { get; set; } private IBindable starDifficultyBindable = null!; private CancellationTokenSource? starDifficultyCancellationSource; @@ -297,7 +296,7 @@ namespace osu.Game.Screens.Select.Carousel items.Add(new OsuMenuItem("Collections") { Items = collectionItems }); if (beatmapInfo.GetOnlineURL(api) is string url) - items.Add(new OsuMenuItem("Copy link", MenuItemType.Standard, () => clipboard.SetText(url))); + items.Add(new OsuMenuItem("Copy link", MenuItemType.Standard, () => game?.CopyUrlToClipboard(url))); if (hideRequested != null) items.Add(new OsuMenuItem(CommonStrings.ButtonsHide.ToSentence(), MenuItemType.Destructive, () => hideRequested(beatmapInfo))); diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs index 3233347991..1cd8b065fc 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs @@ -15,7 +15,6 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.UserInterface; -using osu.Framework.Platform; using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Collections; @@ -44,10 +43,10 @@ namespace osu.Game.Screens.Select.Carousel private RealmAccess realm { get; set; } = null!; [Resolved] - private Clipboard clipboard { get; set; } = null!; + private IAPIProvider api { get; set; } = null!; [Resolved] - private IAPIProvider api { get; set; } = null!; + private OsuGame? game { get; set; } [Resolved] private IBindable ruleset { get; set; } = null!; @@ -301,7 +300,7 @@ namespace osu.Game.Screens.Select.Carousel items.Add(new OsuMenuItem("Restore all hidden", MenuItemType.Standard, () => restoreHiddenRequested(beatmapSet))); if (beatmapSet.GetOnlineURL(api, ruleset.Value) is string url) - items.Add(new OsuMenuItem("Copy link", MenuItemType.Standard, () => clipboard.SetText(url))); + items.Add(new OsuMenuItem("Copy link", MenuItemType.Standard, () => game?.CopyUrlToClipboard(url))); if (dialogOverlay != null) items.Add(new OsuMenuItem("Delete...", MenuItemType.Destructive, () => dialogOverlay.Push(new BeatmapDeleteDialog(beatmapSet)))); From 9df12e3d8750c56b0190e407c25f44db7cb6f340 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 22 Aug 2024 14:15:36 +0900 Subject: [PATCH 2051/2493] Move seek button to left to differentiate mutating operations --- .../Screens/Edit/Timing/ControlPointList.cs | 29 +++++++++++++------ 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/osu.Game/Screens/Edit/Timing/ControlPointList.cs b/osu.Game/Screens/Edit/Timing/ControlPointList.cs index cbef0b9064..8699c388b3 100644 --- a/osu.Game/Screens/Edit/Timing/ControlPointList.cs +++ b/osu.Game/Screens/Edit/Timing/ControlPointList.cs @@ -44,6 +44,26 @@ namespace osu.Game.Screens.Edit.Timing Groups = { BindTarget = Beatmap.ControlPointInfo.Groups, }, }, new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Direction = FillDirection.Horizontal, + Margin = new MarginPadding(margins), + Spacing = new Vector2(5), + Children = new Drawable[] + { + new RoundedButton + { + Text = "Select closest to current time", + Action = goToCurrentGroup, + Size = new Vector2(220, 30), + Anchor = Anchor.BottomRight, + Origin = Anchor.BottomRight, + }, + } + }, + new FillFlowContainer { AutoSizeAxes = Axes.Both, Anchor = Anchor.BottomRight, @@ -68,15 +88,6 @@ namespace osu.Game.Screens.Edit.Timing Size = new Vector2(160, 30), Anchor = Anchor.BottomRight, Origin = Anchor.BottomRight, - BackgroundColour = colours.Green3, - }, - new RoundedButton - { - Text = "Go to current time", - Action = goToCurrentGroup, - Size = new Vector2(140, 30), - Anchor = Anchor.BottomRight, - Origin = Anchor.BottomRight, }, } }, From dfb4a76e29758853c9c0cd136107b7693bbaed12 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 22 Aug 2024 14:05:59 +0900 Subject: [PATCH 2052/2493] Fix test being repeat step --- osu.Game.Tests/Visual/Menus/TestSceneStarFountain.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Menus/TestSceneStarFountain.cs b/osu.Game.Tests/Visual/Menus/TestSceneStarFountain.cs index 36e9375697..29fa7287d2 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneStarFountain.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneStarFountain.cs @@ -67,11 +67,11 @@ namespace osu.Game.Tests.Visual.Menus }; }); - AddRepeatStep("activate fountains", () => + AddStep("activate fountains", () => { ((StarFountain)Children[0]).Shoot(1); ((StarFountain)Children[1]).Shoot(-1); - }, 150); + }); } } } From 236a273e09d0e05fc17561a449cd9dc2d95b2c82 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 22 Aug 2024 15:11:23 +0900 Subject: [PATCH 2053/2493] Simplify `DailyChallengeIntro` test scene Seems like some bad copy-paste in the past. Most of this is already being done in `TestSceneDailyChallenge`. --- .../TestSceneDailyChallengeIntro.cs | 37 ++++--------------- 1 file changed, 7 insertions(+), 30 deletions(-) diff --git a/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeIntro.cs b/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeIntro.cs index a3541d957e..cff2387aed 100644 --- a/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeIntro.cs +++ b/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeIntro.cs @@ -5,13 +5,12 @@ using System; using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; -using osu.Framework.Screens; -using osu.Framework.Testing; using osu.Game.Online.API; using osu.Game.Online.Metadata; using osu.Game.Online.Rooms; using osu.Game.Overlays; using osu.Game.Rulesets.Osu.Mods; +using osu.Game.Screens.OnlinePlay.DailyChallenge; using osu.Game.Tests.Resources; using osu.Game.Tests.Visual.Metadata; using osu.Game.Tests.Visual.OnlinePlay; @@ -27,6 +26,8 @@ namespace osu.Game.Tests.Visual.DailyChallenge [Cached(typeof(INotificationOverlay))] private NotificationOverlay notificationOverlay = new NotificationOverlay(); + private Room room = null!; + [BackgroundDependencyLoader] private void load() { @@ -35,33 +36,15 @@ namespace osu.Game.Tests.Visual.DailyChallenge } [Test] - [Solo] public void TestDailyChallenge() { - var room = new Room - { - RoomID = { Value = 1234 }, - Name = { Value = "Daily Challenge: June 4, 2024" }, - Playlist = - { - new PlaylistItem(CreateAPIBeatmapSet().Beatmaps.First()) - { - RequiredMods = [new APIMod(new OsuModTraceable())], - AllowedMods = [new APIMod(new OsuModDoubleTime())] - } - }, - EndDate = { Value = DateTimeOffset.Now.AddHours(12) }, - Category = { Value = RoomCategory.DailyChallenge } - }; - - AddStep("add room", () => API.Perform(new CreateRoomRequest(room))); - AddStep("push screen", () => LoadScreen(new Screens.OnlinePlay.DailyChallenge.DailyChallengeIntro(room))); + startChallenge(); + AddStep("push screen", () => LoadScreen(new DailyChallengeIntro(room))); } - [Test] - public void TestNotifications() + private void startChallenge() { - var room = new Room + room = new Room { RoomID = { Value = 1234 }, Name = { Value = "Daily Challenge: June 4, 2024" }, @@ -78,12 +61,6 @@ namespace osu.Game.Tests.Visual.DailyChallenge }; AddStep("add room", () => API.Perform(new CreateRoomRequest(room))); - AddStep("set daily challenge info", () => metadataClient.DailyChallengeInfo.Value = new DailyChallengeInfo { RoomID = 1234 }); - - Screens.OnlinePlay.DailyChallenge.DailyChallenge screen = null!; - AddStep("push screen", () => LoadScreen(screen = new Screens.OnlinePlay.DailyChallenge.DailyChallenge(room))); - AddUntilStep("wait for screen", () => screen.IsCurrentScreen()); - AddStep("daily challenge ended", () => metadataClient.DailyChallengeInfo.Value = null); } } } From 9b9986b6f2c508c37fb6674c7b47381fa6681148 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 22 Aug 2024 15:14:52 +0900 Subject: [PATCH 2054/2493] Add isolated test for daily challenge intro flag --- .../DailyChallenge/TestSceneDailyChallengeIntro.cs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeIntro.cs b/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeIntro.cs index cff2387aed..08d44d7405 100644 --- a/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeIntro.cs +++ b/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeIntro.cs @@ -5,6 +5,7 @@ using System; using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; +using osu.Game.Configuration; using osu.Game.Online.API; using osu.Game.Online.Metadata; using osu.Game.Online.Rooms; @@ -42,6 +43,19 @@ namespace osu.Game.Tests.Visual.DailyChallenge AddStep("push screen", () => LoadScreen(new DailyChallengeIntro(room))); } + [Test] + public void TestPlayIntroOnceFlag() + { + AddStep("set intro played flag", () => Dependencies.Get().SetValue(Static.DailyChallengeIntroPlayed, true)); + + startChallenge(); + + AddAssert("intro played flag reset", () => Dependencies.Get().Get(Static.DailyChallengeIntroPlayed), () => Is.False); + + AddStep("push screen", () => LoadScreen(new DailyChallengeIntro(room))); + AddUntilStep("intro played flag set", () => Dependencies.Get().Get(Static.DailyChallengeIntroPlayed), () => Is.True); + } + private void startChallenge() { room = new Room From b3be04aff1111dbc81885da82985e017a7a73647 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 22 Aug 2024 16:09:11 +0900 Subject: [PATCH 2055/2493] Remove "leftover files" notification when migration partly fails People were deleting files they shouldn't, causing osu! to lose track of where the real user files are. For now let's just keep things simple and not let the users know that some files got left behind. Usually the files which are left behind are minimal and it should be fine to leave this up to the user. Closes https://github.com/ppy/osu/issues/29505. --- osu.Game/Localisation/MaintenanceSettingsStrings.cs | 5 ----- osu.Game/OsuGameBase.cs | 10 ++++++++-- .../Sections/Maintenance/MigrationRunScreen.cs | 12 ------------ 3 files changed, 8 insertions(+), 19 deletions(-) diff --git a/osu.Game/Localisation/MaintenanceSettingsStrings.cs b/osu.Game/Localisation/MaintenanceSettingsStrings.cs index 2e5f1d29df..03e15e8393 100644 --- a/osu.Game/Localisation/MaintenanceSettingsStrings.cs +++ b/osu.Game/Localisation/MaintenanceSettingsStrings.cs @@ -34,11 +34,6 @@ namespace osu.Game.Localisation /// public static LocalisableString ProhibitedInteractDuringMigration => new TranslatableString(getKey(@"prohibited_interact_during_migration"), @"Please avoid interacting with the game!"); - /// - /// "Some files couldn't be cleaned up during migration. Clicking this notification will open the folder so you can manually clean things up." - /// - public static LocalisableString FailedCleanupNotification => new TranslatableString(getKey(@"failed_cleanup_notification"), @"Some files couldn't be cleaned up during migration. Clicking this notification will open the folder so you can manually clean things up."); - /// /// "Please select a new location" /// diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 5e4ec5a61d..1988a06503 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -515,6 +515,12 @@ namespace osu.Game /// Whether a restart operation was queued. public virtual bool RestartAppWhenExited() => false; + /// + /// Perform migration of user data to a specified path. + /// + /// The path to migrate to. + /// Whether migration succeeded to completion. If false, some files were left behind. + /// public bool Migrate(string path) { Logger.Log($@"Migrating osu! data from ""{Storage.GetFullPath(string.Empty)}"" to ""{path}""..."); @@ -542,10 +548,10 @@ namespace osu.Game if (!readyToRun.Wait(30000) || !success) throw new TimeoutException("Attempting to block for migration took too long."); - bool? cleanupSucceded = (Storage as OsuStorage)?.Migrate(Host.GetStorage(path)); + bool? cleanupSucceeded = (Storage as OsuStorage)?.Migrate(Host.GetStorage(path)); Logger.Log(@"Migration complete!"); - return cleanupSucceded != false; + return cleanupSucceeded != false; } finally { diff --git a/osu.Game/Overlays/Settings/Sections/Maintenance/MigrationRunScreen.cs b/osu.Game/Overlays/Settings/Sections/Maintenance/MigrationRunScreen.cs index 5b24460ac2..bfc9e820c6 100644 --- a/osu.Game/Overlays/Settings/Sections/Maintenance/MigrationRunScreen.cs +++ b/osu.Game/Overlays/Settings/Sections/Maintenance/MigrationRunScreen.cs @@ -108,18 +108,6 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance { Logger.Error(task.Exception, $"Error during migration: {task.Exception?.Message}"); } - else if (!task.GetResultSafely()) - { - notifications.Post(new SimpleNotification - { - Text = MaintenanceSettingsStrings.FailedCleanupNotification, - Activated = () => - { - originalStorage.PresentExternally(); - return true; - } - }); - } Schedule(this.Exit); }); From 67f0ea5d7dd9b41632d53ab847c351669e86ca51 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 22 Aug 2024 16:22:00 +0900 Subject: [PATCH 2056/2493] Fix flooring causing delta to not work as expected --- .../Menus/TestSceneToolbarUserButton.cs | 27 +++++++++++++++++-- .../TransientUserStatisticsUpdateDisplay.cs | 2 +- 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Menus/TestSceneToolbarUserButton.cs b/osu.Game.Tests/Visual/Menus/TestSceneToolbarUserButton.cs index a81c940d82..71a45e2398 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneToolbarUserButton.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneToolbarUserButton.cs @@ -97,6 +97,7 @@ namespace osu.Game.Tests.Visual.Menus public void TestTransientUserStatisticsDisplay() { AddStep("Log in", () => dummyAPI.Login("wang", "jang")); + AddStep("Gain", () => { var transientUpdateDisplay = this.ChildrenOfType().Single(); @@ -113,6 +114,7 @@ namespace osu.Game.Tests.Visual.Menus PP = 1357 }); }); + AddStep("Loss", () => { var transientUpdateDisplay = this.ChildrenOfType().Single(); @@ -129,7 +131,9 @@ namespace osu.Game.Tests.Visual.Menus PP = 1234 }); }); - AddStep("No change", () => + + // Tests flooring logic works as expected. + AddStep("Tiny increase in PP", () => { var transientUpdateDisplay = this.ChildrenOfType().Single(); transientUpdateDisplay.LatestUpdate.Value = new UserStatisticsUpdate( @@ -137,7 +141,24 @@ namespace osu.Game.Tests.Visual.Menus new UserStatistics { GlobalRank = 111_111, - PP = 1357 + PP = 1357.6m + }, + new UserStatistics + { + GlobalRank = 111_111, + PP = 1358.1m + }); + }); + + AddStep("No change 1", () => + { + var transientUpdateDisplay = this.ChildrenOfType().Single(); + transientUpdateDisplay.LatestUpdate.Value = new UserStatisticsUpdate( + new ScoreInfo(), + new UserStatistics + { + GlobalRank = 111_111, + PP = 1357m }, new UserStatistics { @@ -145,6 +166,7 @@ namespace osu.Game.Tests.Visual.Menus PP = 1357.1m }); }); + AddStep("Was null", () => { var transientUpdateDisplay = this.ChildrenOfType().Single(); @@ -161,6 +183,7 @@ namespace osu.Game.Tests.Visual.Menus PP = 1357 }); }); + AddStep("Became null", () => { var transientUpdateDisplay = this.ChildrenOfType().Single(); diff --git a/osu.Game/Overlays/Toolbar/TransientUserStatisticsUpdateDisplay.cs b/osu.Game/Overlays/Toolbar/TransientUserStatisticsUpdateDisplay.cs index a25df08309..07c2e72774 100644 --- a/osu.Game/Overlays/Toolbar/TransientUserStatisticsUpdateDisplay.cs +++ b/osu.Game/Overlays/Toolbar/TransientUserStatisticsUpdateDisplay.cs @@ -83,7 +83,7 @@ namespace osu.Game.Overlays.Toolbar } if (update.After.PP != null) - pp.Display((int)(update.Before.PP ?? update.After.PP.Value), (int)Math.Abs((update.After.PP - update.Before.PP) ?? 0M), (int)update.After.PP.Value); + pp.Display((int)(update.Before.PP ?? update.After.PP.Value), (int)Math.Abs(((int?)update.After.PP - (int?)update.Before.PP) ?? 0M), (int)update.After.PP.Value); this.Delay(5000).FadeOut(500, Easing.OutQuint); }); From 9020739f3620b30f3c41875dde554fbfc7db3372 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 22 Aug 2024 10:05:45 +0200 Subject: [PATCH 2057/2493] Remove unused using directives --- .../Settings/Sections/Maintenance/MigrationRunScreen.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game/Overlays/Settings/Sections/Maintenance/MigrationRunScreen.cs b/osu.Game/Overlays/Settings/Sections/Maintenance/MigrationRunScreen.cs index bfc9e820c6..dbfca81624 100644 --- a/osu.Game/Overlays/Settings/Sections/Maintenance/MigrationRunScreen.cs +++ b/osu.Game/Overlays/Settings/Sections/Maintenance/MigrationRunScreen.cs @@ -6,7 +6,6 @@ using System.IO; using System.Threading.Tasks; using osu.Framework.Allocation; -using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Logging; @@ -16,7 +15,6 @@ using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Localisation; -using osu.Game.Overlays.Notifications; using osu.Game.Screens; using osuTK; From 41756520b1ff322ae8a28858becdee362279309e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 22 Aug 2024 17:14:35 +0900 Subject: [PATCH 2058/2493] Rename `SkinComponentsContainer` to `SkinnableContainer` --- .../TestSceneCatchPlayerLegacySkin.cs | 2 +- .../Gameplay/TestSceneBeatmapSkinFallbacks.cs | 4 ++-- .../Visual/Gameplay/TestSceneHUDOverlay.cs | 12 ++++++------ .../Gameplay/TestScenePauseInputHandling.cs | 2 +- .../Visual/Gameplay/TestSceneSkinEditor.cs | 16 ++++++++-------- .../TestSceneSkinEditorComponentsList.cs | 2 +- .../Gameplay/TestSceneSkinnableHUDOverlay.cs | 4 ++-- .../Navigation/TestSceneSkinEditorNavigation.cs | 4 ++-- .../Overlays/SkinEditor/SkinComponentToolbox.cs | 4 ++-- osu.Game/Overlays/SkinEditor/SkinEditor.cs | 14 +++++++------- osu.Game/Screens/Play/HUDOverlay.cs | 10 +++++----- osu.Game/Screens/Select/SongSelect.cs | 2 +- osu.Game/Skinning/Skin.cs | 4 ++-- osu.Game/Skinning/SkinLayoutInfo.cs | 4 ++-- ...ponentsContainer.cs => SkinnableContainer.cs} | 6 +++--- .../Tests/Visual/LegacySkinPlayerTestScene.cs | 4 ++-- 16 files changed, 47 insertions(+), 47 deletions(-) rename osu.Game/Skinning/{SkinComponentsContainer.cs => SkinnableContainer.cs} (94%) diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatchPlayerLegacySkin.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchPlayerLegacySkin.cs index 7812e02a63..792caf6de6 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatchPlayerLegacySkin.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchPlayerLegacySkin.cs @@ -23,7 +23,7 @@ namespace osu.Game.Rulesets.Catch.Tests if (withModifiedSkin) { AddStep("change component scale", () => Player.ChildrenOfType().First().Scale = new Vector2(2f)); - AddStep("update target", () => Player.ChildrenOfType().ForEach(LegacySkin.UpdateDrawableTarget)); + AddStep("update target", () => Player.ChildrenOfType().ForEach(LegacySkin.UpdateDrawableTarget)); AddStep("exit player", () => Player.Exit()); CreateTest(); } diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs index a2ce62105e..c9b9b97580 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs @@ -37,7 +37,7 @@ namespace osu.Game.Tests.Visual.Gameplay public void TestEmptyLegacyBeatmapSkinFallsBack() { CreateSkinTest(TrianglesSkin.CreateInfo(), () => new LegacyBeatmapSkin(new BeatmapInfo(), null)); - AddUntilStep("wait for hud load", () => Player.ChildrenOfType().All(c => c.ComponentsLoaded)); + AddUntilStep("wait for hud load", () => Player.ChildrenOfType().All(c => c.ComponentsLoaded)); AddAssert("hud from default skin", () => AssertComponentsFromExpectedSource(SkinComponentsContainerLookup.TargetArea.MainHUDComponents, skinManager.CurrentSkin.Value)); } @@ -55,7 +55,7 @@ namespace osu.Game.Tests.Visual.Gameplay protected bool AssertComponentsFromExpectedSource(SkinComponentsContainerLookup.TargetArea target, ISkin expectedSource) { - var targetContainer = Player.ChildrenOfType().First(s => s.Lookup.Target == target); + var targetContainer = Player.ChildrenOfType().First(s => s.Lookup.Target == target); var actualComponentsContainer = targetContainer.ChildrenOfType().SingleOrDefault(c => c.Parent == targetContainer); if (actualComponentsContainer == null) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs index 91f22a291c..d51c9b3f88 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs @@ -43,7 +43,7 @@ namespace osu.Game.Tests.Visual.Gameplay private readonly IGameplayClock gameplayClock = new GameplayClockContainer(new TrackVirtual(60000), false, false); // best way to check without exposing. - private Drawable hideTarget => hudOverlay.ChildrenOfType().First(); + private Drawable hideTarget => hudOverlay.ChildrenOfType().First(); private Drawable keyCounterContent => hudOverlay.ChildrenOfType().First().ChildrenOfType().Skip(1).First(); public TestSceneHUDOverlay() @@ -242,8 +242,8 @@ namespace osu.Game.Tests.Visual.Gameplay createNew(); - AddUntilStep("wait for components to be hidden", () => hudOverlay.ChildrenOfType().Single().Alpha == 0); - AddUntilStep("wait for hud load", () => hudOverlay.ChildrenOfType().All(c => c.ComponentsLoaded)); + AddUntilStep("wait for components to be hidden", () => hudOverlay.ChildrenOfType().Single().Alpha == 0); + AddUntilStep("wait for hud load", () => hudOverlay.ChildrenOfType().All(c => c.ComponentsLoaded)); AddStep("bind on update", () => { @@ -260,10 +260,10 @@ namespace osu.Game.Tests.Visual.Gameplay createNew(); - AddUntilStep("wait for components to be hidden", () => hudOverlay.ChildrenOfType().Single().Alpha == 0); + AddUntilStep("wait for components to be hidden", () => hudOverlay.ChildrenOfType().Single().Alpha == 0); - AddStep("reload components", () => hudOverlay.ChildrenOfType().Single().Reload()); - AddUntilStep("skinnable components loaded", () => hudOverlay.ChildrenOfType().Single().ComponentsLoaded); + AddStep("reload components", () => hudOverlay.ChildrenOfType().Single().Reload()); + AddUntilStep("skinnable components loaded", () => hudOverlay.ChildrenOfType().Single().ComponentsLoaded); } private void createNew(Action? action = null) diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePauseInputHandling.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePauseInputHandling.cs index bc66947ccd..2c58e64831 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePauseInputHandling.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePauseInputHandling.cs @@ -286,7 +286,7 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("set ruleset", () => currentRuleset = createRuleset()); AddStep("load player", LoadPlayer); AddUntilStep("player loaded", () => Player.IsLoaded && Player.Alpha == 1); - AddUntilStep("wait for hud", () => Player.HUDOverlay.ChildrenOfType().All(s => s.ComponentsLoaded)); + AddUntilStep("wait for hud", () => Player.HUDOverlay.ChildrenOfType().All(s => s.ComponentsLoaded)); AddStep("seek to gameplay", () => Player.GameplayClockContainer.Seek(0)); AddUntilStep("wait for seek to finish", () => Player.DrawableRuleset.FrameStableClock.CurrentTime, () => Is.EqualTo(0).Within(500)); diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs index 7466442674..cc514cc2fa 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs @@ -46,7 +46,7 @@ namespace osu.Game.Tests.Visual.Gameplay [Resolved] private SkinManager skins { get; set; } = null!; - private SkinComponentsContainer targetContainer => Player.ChildrenOfType().First(); + private SkinnableContainer targetContainer => Player.ChildrenOfType().First(); [SetUpSteps] public override void SetUpSteps() @@ -75,7 +75,7 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("Add big black boxes", () => { - var target = Player.ChildrenOfType().First(); + var target = Player.ChildrenOfType().First(); target.Add(box1 = new BigBlackBox { Position = new Vector2(-90), @@ -200,14 +200,14 @@ namespace osu.Game.Tests.Visual.Gameplay [Test] public void TestUndoEditHistory() { - SkinComponentsContainer firstTarget = null!; + SkinnableContainer firstTarget = null!; TestSkinEditorChangeHandler changeHandler = null!; byte[] defaultState = null!; IEnumerable testComponents = null!; AddStep("Load necessary things", () => { - firstTarget = Player.ChildrenOfType().First(); + firstTarget = Player.ChildrenOfType().First(); changeHandler = new TestSkinEditorChangeHandler(firstTarget); changeHandler.SaveState(); @@ -377,11 +377,11 @@ namespace osu.Game.Tests.Visual.Gameplay () => Is.EqualTo(3)); } - private SkinComponentsContainer globalHUDTarget => Player.ChildrenOfType() - .Single(c => c.Lookup.Target == SkinComponentsContainerLookup.TargetArea.MainHUDComponents && c.Lookup.Ruleset == null); + private SkinnableContainer globalHUDTarget => Player.ChildrenOfType() + .Single(c => c.Lookup.Target == SkinComponentsContainerLookup.TargetArea.MainHUDComponents && c.Lookup.Ruleset == null); - private SkinComponentsContainer rulesetHUDTarget => Player.ChildrenOfType() - .Single(c => c.Lookup.Target == SkinComponentsContainerLookup.TargetArea.MainHUDComponents && c.Lookup.Ruleset != null); + private SkinnableContainer rulesetHUDTarget => Player.ChildrenOfType() + .Single(c => c.Lookup.Target == SkinComponentsContainerLookup.TargetArea.MainHUDComponents && c.Lookup.Ruleset != null); [Test] public void TestMigrationArgon() diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditorComponentsList.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditorComponentsList.cs index b7b2a6c175..42dcfe12e9 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditorComponentsList.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditorComponentsList.cs @@ -20,7 +20,7 @@ namespace osu.Game.Tests.Visual.Gameplay [Test] public void TestToggleEditor() { - var skinComponentsContainer = new SkinComponentsContainer(new SkinComponentsContainerLookup(SkinComponentsContainerLookup.TargetArea.SongSelect)); + var skinComponentsContainer = new SkinnableContainer(new SkinComponentsContainerLookup(SkinComponentsContainerLookup.TargetArea.SongSelect)); AddStep("show available components", () => SetContents(_ => new SkinComponentToolbox(skinComponentsContainer, null) { diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs index d1e224a910..fcaa2996e1 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs @@ -45,7 +45,7 @@ namespace osu.Game.Tests.Visual.Gameplay private IEnumerable hudOverlays => CreatedDrawables.OfType(); // best way to check without exposing. - private Drawable hideTarget => hudOverlay.ChildrenOfType().First(); + private Drawable hideTarget => hudOverlay.ChildrenOfType().First(); private Drawable keyCounterFlow => hudOverlay.ChildrenOfType().First().ChildrenOfType>().Single(); public TestSceneSkinnableHUDOverlay() @@ -101,7 +101,7 @@ namespace osu.Game.Tests.Visual.Gameplay }); AddUntilStep("HUD overlay loaded", () => hudOverlay.IsAlive); AddUntilStep("components container loaded", - () => hudOverlay.ChildrenOfType().Any(scc => scc.ComponentsLoaded)); + () => hudOverlay.ChildrenOfType().Any(scc => scc.ComponentsLoaded)); } protected override Ruleset CreateRulesetForSkinProvider() => new OsuRuleset(); diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneSkinEditorNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneSkinEditorNavigation.cs index 38fb2846aa..5267a57a05 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneSkinEditorNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneSkinEditorNavigation.cs @@ -336,13 +336,13 @@ namespace osu.Game.Tests.Visual.Navigation }); AddStep("change to triangles skin", () => Game.Dependencies.Get().SetSkinFromConfiguration(SkinInfo.TRIANGLES_SKIN.ToString())); - AddUntilStep("components loaded", () => Game.ChildrenOfType().All(c => c.ComponentsLoaded)); + AddUntilStep("components loaded", () => Game.ChildrenOfType().All(c => c.ComponentsLoaded)); // sort of implicitly relies on song select not being skinnable. // TODO: revisit if the above ever changes AddUntilStep("skin changed", () => !skinEditor.ChildrenOfType().Any()); AddStep("change back to modified skin", () => Game.Dependencies.Get().SetSkinFromConfiguration(editedSkinId.ToString())); - AddUntilStep("components loaded", () => Game.ChildrenOfType().All(c => c.ComponentsLoaded)); + AddUntilStep("components loaded", () => Game.ChildrenOfType().All(c => c.ComponentsLoaded)); AddUntilStep("changes saved", () => skinEditor.ChildrenOfType().Any()); } diff --git a/osu.Game/Overlays/SkinEditor/SkinComponentToolbox.cs b/osu.Game/Overlays/SkinEditor/SkinComponentToolbox.cs index a476fc1a6d..85becc1a23 100644 --- a/osu.Game/Overlays/SkinEditor/SkinComponentToolbox.cs +++ b/osu.Game/Overlays/SkinEditor/SkinComponentToolbox.cs @@ -24,7 +24,7 @@ namespace osu.Game.Overlays.SkinEditor { public Action? RequestPlacement; - private readonly SkinComponentsContainer target; + private readonly SkinnableContainer target; private readonly RulesetInfo? ruleset; @@ -35,7 +35,7 @@ namespace osu.Game.Overlays.SkinEditor /// /// The target. This is mainly used as a dependency source to find candidate components. /// A ruleset to filter components by. If null, only components which are not ruleset-specific will be included. - public SkinComponentToolbox(SkinComponentsContainer target, RulesetInfo? ruleset) + public SkinComponentToolbox(SkinnableContainer target, RulesetInfo? ruleset) : base(ruleset == null ? SkinEditorStrings.Components : LocalisableString.Interpolate($"{SkinEditorStrings.Components} ({ruleset.Name})")) { this.target = target; diff --git a/osu.Game/Overlays/SkinEditor/SkinEditor.cs b/osu.Game/Overlays/SkinEditor/SkinEditor.cs index 03acf1e68c..78ddce03c7 100644 --- a/osu.Game/Overlays/SkinEditor/SkinEditor.cs +++ b/osu.Game/Overlays/SkinEditor/SkinEditor.cs @@ -472,18 +472,18 @@ namespace osu.Game.Overlays.SkinEditor settingsSidebar.Add(new SkinSettingsToolbox(component)); } - private IEnumerable availableTargets => targetScreen.ChildrenOfType(); + private IEnumerable availableTargets => targetScreen.ChildrenOfType(); - private SkinComponentsContainer? getFirstTarget() => availableTargets.FirstOrDefault(); + private SkinnableContainer? getFirstTarget() => availableTargets.FirstOrDefault(); - private SkinComponentsContainer? getTarget(SkinComponentsContainerLookup? target) + private SkinnableContainer? getTarget(SkinComponentsContainerLookup? target) { return availableTargets.FirstOrDefault(c => c.Lookup.Equals(target)); } private void revert() { - SkinComponentsContainer[] targetContainers = availableTargets.ToArray(); + SkinnableContainer[] targetContainers = availableTargets.ToArray(); foreach (var t in targetContainers) { @@ -555,7 +555,7 @@ namespace osu.Game.Overlays.SkinEditor if (targetScreen?.IsLoaded != true) return; - SkinComponentsContainer[] targetContainers = availableTargets.ToArray(); + SkinnableContainer[] targetContainers = availableTargets.ToArray(); if (!targetContainers.All(c => c.ComponentsLoaded)) return; @@ -600,7 +600,7 @@ namespace osu.Game.Overlays.SkinEditor public void BringSelectionToFront() { - if (getTarget(selectedTarget.Value) is not SkinComponentsContainer target) + if (getTarget(selectedTarget.Value) is not SkinnableContainer target) return; changeHandler?.BeginChange(); @@ -624,7 +624,7 @@ namespace osu.Game.Overlays.SkinEditor public void SendSelectionToBack() { - if (getTarget(selectedTarget.Value) is not SkinComponentsContainer target) + if (getTarget(selectedTarget.Value) is not SkinnableContainer target) return; changeHandler?.BeginChange(); diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index ef3bb7c04a..73fda62616 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -95,10 +95,10 @@ namespace osu.Game.Screens.Play private readonly BindableBool holdingForHUD = new BindableBool(); - private readonly SkinComponentsContainer mainComponents; + private readonly SkinnableContainer mainComponents; [CanBeNull] - private readonly SkinComponentsContainer rulesetComponents; + private readonly SkinnableContainer rulesetComponents; /// /// A flow which sits at the left side of the screen to house leaderboard (and related) components. @@ -132,7 +132,7 @@ namespace osu.Game.Screens.Play ? (rulesetComponents = new HUDComponentsContainer(drawableRuleset.Ruleset.RulesetInfo) { AlwaysPresent = true, }) : Empty(), PlayfieldSkinLayer = drawableRuleset != null - ? new SkinComponentsContainer(new SkinComponentsContainerLookup(SkinComponentsContainerLookup.TargetArea.Playfield, drawableRuleset.Ruleset.RulesetInfo)) { AlwaysPresent = true, } + ? new SkinnableContainer(new SkinComponentsContainerLookup(SkinComponentsContainerLookup.TargetArea.Playfield, drawableRuleset.Ruleset.RulesetInfo)) { AlwaysPresent = true, } : Empty(), topRightElements = new FillFlowContainer { @@ -280,7 +280,7 @@ namespace osu.Game.Screens.Play else bottomRightElements.Y = 0; - void processDrawables(SkinComponentsContainer components) + void processDrawables(SkinnableContainer components) { // Avoid using foreach due to missing GetEnumerator implementation. // See https://github.com/ppy/osu-framework/blob/e10051e6643731e393b09de40a3a3d209a545031/osu.Framework/Bindables/IBindableList.cs#L41-L44. @@ -440,7 +440,7 @@ namespace osu.Game.Screens.Play } } - private partial class HUDComponentsContainer : SkinComponentsContainer + private partial class HUDComponentsContainer : SkinnableContainer { private Bindable scoringMode; diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index 2ee5a6f3cb..a4a7351338 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -321,7 +321,7 @@ namespace osu.Game.Screens.Select } } }, - new SkinComponentsContainer(new SkinComponentsContainerLookup(SkinComponentsContainerLookup.TargetArea.SongSelect)) + new SkinnableContainer(new SkinComponentsContainerLookup(SkinComponentsContainerLookup.TargetArea.SongSelect)) { RelativeSizeAxes = Axes.Both, }, diff --git a/osu.Game/Skinning/Skin.cs b/osu.Game/Skinning/Skin.cs index 3a83815f0e..7c205b5289 100644 --- a/osu.Game/Skinning/Skin.cs +++ b/osu.Game/Skinning/Skin.cs @@ -162,7 +162,7 @@ namespace osu.Game.Skinning /// Remove all stored customisations for the provided target. /// /// The target container to reset. - public void ResetDrawableTarget(SkinComponentsContainer targetContainer) + public void ResetDrawableTarget(SkinnableContainer targetContainer) { LayoutInfos.Remove(targetContainer.Lookup.Target); } @@ -171,7 +171,7 @@ namespace osu.Game.Skinning /// Update serialised information for the provided target. /// /// The target container to serialise to this skin. - public void UpdateDrawableTarget(SkinComponentsContainer targetContainer) + public void UpdateDrawableTarget(SkinnableContainer targetContainer) { if (!LayoutInfos.TryGetValue(targetContainer.Lookup.Target, out var layoutInfo)) layoutInfos[targetContainer.Lookup.Target] = layoutInfo = new SkinLayoutInfo(); diff --git a/osu.Game/Skinning/SkinLayoutInfo.cs b/osu.Game/Skinning/SkinLayoutInfo.cs index 22c876e5ad..bf6c693621 100644 --- a/osu.Game/Skinning/SkinLayoutInfo.cs +++ b/osu.Game/Skinning/SkinLayoutInfo.cs @@ -11,8 +11,8 @@ using osu.Game.Rulesets; namespace osu.Game.Skinning { /// - /// A serialisable model describing layout of a . - /// May contain multiple configurations for different rulesets, each of which should manifest their own as required. + /// A serialisable model describing layout of a . + /// May contain multiple configurations for different rulesets, each of which should manifest their own as required. /// [Serializable] public class SkinLayoutInfo diff --git a/osu.Game/Skinning/SkinComponentsContainer.cs b/osu.Game/Skinning/SkinnableContainer.cs similarity index 94% rename from osu.Game/Skinning/SkinComponentsContainer.cs rename to osu.Game/Skinning/SkinnableContainer.cs index 02ba43fd39..d2d4fac766 100644 --- a/osu.Game/Skinning/SkinComponentsContainer.cs +++ b/osu.Game/Skinning/SkinnableContainer.cs @@ -16,10 +16,10 @@ namespace osu.Game.Skinning ///
/// /// This is currently used as a means of serialising skin layouts to files. - /// Currently, one json file in a skin will represent one , containing + /// Currently, one json file in a skin will represent one , containing /// the output of . /// - public partial class SkinComponentsContainer : SkinReloadableDrawable, ISerialisableDrawableContainer + public partial class SkinnableContainer : SkinReloadableDrawable, ISerialisableDrawableContainer { private Container? content; @@ -38,7 +38,7 @@ namespace osu.Game.Skinning private CancellationTokenSource? cancellationSource; - public SkinComponentsContainer(SkinComponentsContainerLookup lookup) + public SkinnableContainer(SkinComponentsContainerLookup lookup) { Lookup = lookup; } diff --git a/osu.Game/Tests/Visual/LegacySkinPlayerTestScene.cs b/osu.Game/Tests/Visual/LegacySkinPlayerTestScene.cs index 2e254f5b95..0e1776be8e 100644 --- a/osu.Game/Tests/Visual/LegacySkinPlayerTestScene.cs +++ b/osu.Game/Tests/Visual/LegacySkinPlayerTestScene.cs @@ -45,13 +45,13 @@ namespace osu.Game.Tests.Visual private void addResetTargetsStep() { - AddStep("reset targets", () => this.ChildrenOfType().ForEach(t => + AddStep("reset targets", () => this.ChildrenOfType().ForEach(t => { LegacySkin.ResetDrawableTarget(t); t.Reload(); })); - AddUntilStep("wait for components to load", () => this.ChildrenOfType().All(t => t.ComponentsLoaded)); + AddUntilStep("wait for components to load", () => this.ChildrenOfType().All(t => t.ComponentsLoaded)); } public partial class SkinProvidingPlayer : TestPlayer From 9997271a6a9d9e33c99bc58a4df4566d90b6dca8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 22 Aug 2024 10:49:24 +0200 Subject: [PATCH 2059/2493] Fix more code quality inspections --- .../Settings/Sections/Maintenance/MigrationRunScreen.cs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/osu.Game/Overlays/Settings/Sections/Maintenance/MigrationRunScreen.cs b/osu.Game/Overlays/Settings/Sections/Maintenance/MigrationRunScreen.cs index dbfca81624..e7c87a617f 100644 --- a/osu.Game/Overlays/Settings/Sections/Maintenance/MigrationRunScreen.cs +++ b/osu.Game/Overlays/Settings/Sections/Maintenance/MigrationRunScreen.cs @@ -27,9 +27,6 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance [Resolved(canBeNull: true)] private OsuGame game { get; set; } - [Resolved] - private INotificationOverlay notifications { get; set; } - [Resolved] private Storage storage { get; set; } @@ -97,8 +94,6 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance Beatmap.Value = Beatmap.Default; - var originalStorage = new NativeStorage(storage.GetFullPath(string.Empty), host); - migrationTask = Task.Run(PerformMigration) .ContinueWith(task => { From 1859e173f26def407cf02c610e112d49012c7004 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 22 Aug 2024 11:16:24 +0200 Subject: [PATCH 2060/2493] Fix EVEN MORE code quality inspections! --- .../Settings/Sections/Maintenance/MigrationRunScreen.cs | 7 ------- 1 file changed, 7 deletions(-) diff --git a/osu.Game/Overlays/Settings/Sections/Maintenance/MigrationRunScreen.cs b/osu.Game/Overlays/Settings/Sections/Maintenance/MigrationRunScreen.cs index e7c87a617f..3bba480aaa 100644 --- a/osu.Game/Overlays/Settings/Sections/Maintenance/MigrationRunScreen.cs +++ b/osu.Game/Overlays/Settings/Sections/Maintenance/MigrationRunScreen.cs @@ -9,7 +9,6 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Logging; -using osu.Framework.Platform; using osu.Framework.Screens; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; @@ -27,12 +26,6 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance [Resolved(canBeNull: true)] private OsuGame game { get; set; } - [Resolved] - private Storage storage { get; set; } - - [Resolved] - private GameHost host { get; set; } - public override bool AllowBackButton => false; public override bool AllowExternalScreenChange => false; From f37cab0c6ec1c50fdbd5f5c7312ca0679b662388 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 22 Aug 2024 18:39:36 +0900 Subject: [PATCH 2061/2493] Rename `SkinComponentsContainerLookup` to `GlobalSkinnableContainerLookup` --- .../Legacy/CatchLegacySkinTransformer.cs | 4 ++-- .../Argon/ManiaArgonSkinTransformer.cs | 4 ++-- .../Legacy/ManiaLegacySkinTransformer.cs | 4 ++-- .../Legacy/OsuLegacySkinTransformer.cs | 4 ++-- .../Skins/SkinDeserialisationTest.cs | 20 +++++++++---------- .../Gameplay/TestSceneBeatmapSkinFallbacks.cs | 6 +++--- .../Visual/Gameplay/TestSceneSkinEditor.cs | 4 ++-- .../TestSceneSkinEditorComponentsList.cs | 2 +- osu.Game/Overlays/SkinEditor/SkinEditor.cs | 8 ++++---- osu.Game/Screens/Play/HUDOverlay.cs | 6 +++--- osu.Game/Screens/Select/SongSelect.cs | 2 +- osu.Game/Skinning/ArgonSkin.cs | 6 +++--- ...p.cs => GlobalSkinnableContainerLookup.cs} | 14 ++++++------- osu.Game/Skinning/LegacyBeatmapSkin.cs | 4 ++-- osu.Game/Skinning/LegacySkin.cs | 4 ++-- osu.Game/Skinning/Skin.cs | 16 +++++++-------- osu.Game/Skinning/SkinnableContainer.cs | 4 ++-- osu.Game/Skinning/TrianglesSkin.cs | 6 +++--- 18 files changed, 59 insertions(+), 59 deletions(-) rename osu.Game/Skinning/{SkinComponentsContainerLookup.cs => GlobalSkinnableContainerLookup.cs} (79%) diff --git a/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs b/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs index f3626eb55d..ab0420554e 100644 --- a/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs @@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy { switch (lookup) { - case SkinComponentsContainerLookup containerLookup: + case GlobalSkinnableContainerLookup containerLookup: // Only handle per ruleset defaults here. if (containerLookup.Ruleset == null) return base.GetDrawableComponent(lookup); @@ -46,7 +46,7 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy // Our own ruleset components default. switch (containerLookup.Target) { - case SkinComponentsContainerLookup.TargetArea.MainHUDComponents: + case GlobalSkinnableContainerLookup.GlobalSkinnableContainers.MainHUDComponents: // todo: remove CatchSkinComponents.CatchComboCounter and refactor LegacyCatchComboCounter to be added here instead. return new DefaultSkinComponentsContainer(container => { diff --git a/osu.Game.Rulesets.Mania/Skinning/Argon/ManiaArgonSkinTransformer.cs b/osu.Game.Rulesets.Mania/Skinning/Argon/ManiaArgonSkinTransformer.cs index dbd690f890..f80cb3a88a 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Argon/ManiaArgonSkinTransformer.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Argon/ManiaArgonSkinTransformer.cs @@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon { switch (lookup) { - case SkinComponentsContainerLookup containerLookup: + case GlobalSkinnableContainerLookup containerLookup: // Only handle per ruleset defaults here. if (containerLookup.Ruleset == null) return base.GetDrawableComponent(lookup); @@ -39,7 +39,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon switch (containerLookup.Target) { - case SkinComponentsContainerLookup.TargetArea.MainHUDComponents: + case GlobalSkinnableContainerLookup.GlobalSkinnableContainers.MainHUDComponents: return new DefaultSkinComponentsContainer(container => { var combo = container.ChildrenOfType().FirstOrDefault(); diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs index c25b77610a..20017a78a2 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs @@ -80,7 +80,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy { switch (lookup) { - case SkinComponentsContainerLookup containerLookup: + case GlobalSkinnableContainerLookup containerLookup: // Modifications for global components. if (containerLookup.Ruleset == null) return base.GetDrawableComponent(lookup); @@ -95,7 +95,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy switch (containerLookup.Target) { - case SkinComponentsContainerLookup.TargetArea.MainHUDComponents: + case GlobalSkinnableContainerLookup.GlobalSkinnableContainers.MainHUDComponents: return new DefaultSkinComponentsContainer(container => { var combo = container.ChildrenOfType().FirstOrDefault(); diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs index 457c191583..6609a84be4 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs @@ -44,7 +44,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy { switch (lookup) { - case SkinComponentsContainerLookup containerLookup: + case GlobalSkinnableContainerLookup containerLookup: // Only handle per ruleset defaults here. if (containerLookup.Ruleset == null) return base.GetDrawableComponent(lookup); @@ -60,7 +60,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy // Our own ruleset components default. switch (containerLookup.Target) { - case SkinComponentsContainerLookup.TargetArea.MainHUDComponents: + case GlobalSkinnableContainerLookup.GlobalSkinnableContainers.MainHUDComponents: return new DefaultSkinComponentsContainer(container => { var keyCounter = container.OfType().FirstOrDefault(); diff --git a/osu.Game.Tests/Skins/SkinDeserialisationTest.cs b/osu.Game.Tests/Skins/SkinDeserialisationTest.cs index 534d47d617..ad01a057ad 100644 --- a/osu.Game.Tests/Skins/SkinDeserialisationTest.cs +++ b/osu.Game.Tests/Skins/SkinDeserialisationTest.cs @@ -107,7 +107,7 @@ namespace osu.Game.Tests.Skins var skin = new TestSkin(new SkinInfo(), null, storage); Assert.That(skin.LayoutInfos, Has.Count.EqualTo(2)); - Assert.That(skin.LayoutInfos[SkinComponentsContainerLookup.TargetArea.MainHUDComponents].AllDrawables.ToArray(), Has.Length.EqualTo(9)); + Assert.That(skin.LayoutInfos[GlobalSkinnableContainerLookup.GlobalSkinnableContainers.MainHUDComponents].AllDrawables.ToArray(), Has.Length.EqualTo(9)); } } @@ -120,8 +120,8 @@ namespace osu.Game.Tests.Skins var skin = new TestSkin(new SkinInfo(), null, storage); Assert.That(skin.LayoutInfos, Has.Count.EqualTo(2)); - Assert.That(skin.LayoutInfos[SkinComponentsContainerLookup.TargetArea.MainHUDComponents].AllDrawables.ToArray(), Has.Length.EqualTo(10)); - Assert.That(skin.LayoutInfos[SkinComponentsContainerLookup.TargetArea.MainHUDComponents].AllDrawables.Select(i => i.Type), Contains.Item(typeof(PlayerName))); + Assert.That(skin.LayoutInfos[GlobalSkinnableContainerLookup.GlobalSkinnableContainers.MainHUDComponents].AllDrawables.ToArray(), Has.Length.EqualTo(10)); + Assert.That(skin.LayoutInfos[GlobalSkinnableContainerLookup.GlobalSkinnableContainers.MainHUDComponents].AllDrawables.Select(i => i.Type), Contains.Item(typeof(PlayerName))); } } @@ -134,10 +134,10 @@ namespace osu.Game.Tests.Skins var skin = new TestSkin(new SkinInfo(), null, storage); Assert.That(skin.LayoutInfos, Has.Count.EqualTo(2)); - Assert.That(skin.LayoutInfos[SkinComponentsContainerLookup.TargetArea.MainHUDComponents].AllDrawables.ToArray(), Has.Length.EqualTo(6)); - Assert.That(skin.LayoutInfos[SkinComponentsContainerLookup.TargetArea.SongSelect].AllDrawables.ToArray(), Has.Length.EqualTo(1)); + Assert.That(skin.LayoutInfos[GlobalSkinnableContainerLookup.GlobalSkinnableContainers.MainHUDComponents].AllDrawables.ToArray(), Has.Length.EqualTo(6)); + Assert.That(skin.LayoutInfos[GlobalSkinnableContainerLookup.GlobalSkinnableContainers.SongSelect].AllDrawables.ToArray(), Has.Length.EqualTo(1)); - var skinnableInfo = skin.LayoutInfos[SkinComponentsContainerLookup.TargetArea.SongSelect].AllDrawables.First(); + var skinnableInfo = skin.LayoutInfos[GlobalSkinnableContainerLookup.GlobalSkinnableContainers.SongSelect].AllDrawables.First(); Assert.That(skinnableInfo.Type, Is.EqualTo(typeof(SkinnableSprite))); Assert.That(skinnableInfo.Settings.First().Key, Is.EqualTo("sprite_name")); @@ -148,10 +148,10 @@ namespace osu.Game.Tests.Skins using (var storage = new ZipArchiveReader(stream)) { var skin = new TestSkin(new SkinInfo(), null, storage); - Assert.That(skin.LayoutInfos[SkinComponentsContainerLookup.TargetArea.MainHUDComponents].AllDrawables.ToArray(), Has.Length.EqualTo(8)); - Assert.That(skin.LayoutInfos[SkinComponentsContainerLookup.TargetArea.MainHUDComponents].AllDrawables.Select(i => i.Type), Contains.Item(typeof(UnstableRateCounter))); - Assert.That(skin.LayoutInfos[SkinComponentsContainerLookup.TargetArea.MainHUDComponents].AllDrawables.Select(i => i.Type), Contains.Item(typeof(ColourHitErrorMeter))); - Assert.That(skin.LayoutInfos[SkinComponentsContainerLookup.TargetArea.MainHUDComponents].AllDrawables.Select(i => i.Type), Contains.Item(typeof(LegacySongProgress))); + Assert.That(skin.LayoutInfos[GlobalSkinnableContainerLookup.GlobalSkinnableContainers.MainHUDComponents].AllDrawables.ToArray(), Has.Length.EqualTo(8)); + Assert.That(skin.LayoutInfos[GlobalSkinnableContainerLookup.GlobalSkinnableContainers.MainHUDComponents].AllDrawables.Select(i => i.Type), Contains.Item(typeof(UnstableRateCounter))); + Assert.That(skin.LayoutInfos[GlobalSkinnableContainerLookup.GlobalSkinnableContainers.MainHUDComponents].AllDrawables.Select(i => i.Type), Contains.Item(typeof(ColourHitErrorMeter))); + Assert.That(skin.LayoutInfos[GlobalSkinnableContainerLookup.GlobalSkinnableContainers.MainHUDComponents].AllDrawables.Select(i => i.Type), Contains.Item(typeof(LegacySongProgress))); } } diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs index c9b9b97580..1061f493d4 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs @@ -38,7 +38,7 @@ namespace osu.Game.Tests.Visual.Gameplay { CreateSkinTest(TrianglesSkin.CreateInfo(), () => new LegacyBeatmapSkin(new BeatmapInfo(), null)); AddUntilStep("wait for hud load", () => Player.ChildrenOfType().All(c => c.ComponentsLoaded)); - AddAssert("hud from default skin", () => AssertComponentsFromExpectedSource(SkinComponentsContainerLookup.TargetArea.MainHUDComponents, skinManager.CurrentSkin.Value)); + AddAssert("hud from default skin", () => AssertComponentsFromExpectedSource(GlobalSkinnableContainerLookup.GlobalSkinnableContainers.MainHUDComponents, skinManager.CurrentSkin.Value)); } protected void CreateSkinTest(SkinInfo gameCurrentSkin, Func getBeatmapSkin) @@ -53,7 +53,7 @@ namespace osu.Game.Tests.Visual.Gameplay }); } - protected bool AssertComponentsFromExpectedSource(SkinComponentsContainerLookup.TargetArea target, ISkin expectedSource) + protected bool AssertComponentsFromExpectedSource(GlobalSkinnableContainerLookup.GlobalSkinnableContainers target, ISkin expectedSource) { var targetContainer = Player.ChildrenOfType().First(s => s.Lookup.Target == target); var actualComponentsContainer = targetContainer.ChildrenOfType().SingleOrDefault(c => c.Parent == targetContainer); @@ -63,7 +63,7 @@ namespace osu.Game.Tests.Visual.Gameplay var actualInfo = actualComponentsContainer.CreateSerialisedInfo(); - var expectedComponentsContainer = expectedSource.GetDrawableComponent(new SkinComponentsContainerLookup(target)) as Container; + var expectedComponentsContainer = expectedSource.GetDrawableComponent(new GlobalSkinnableContainerLookup(target)) as Container; if (expectedComponentsContainer == null) return false; diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs index cc514cc2fa..9e53f86e33 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs @@ -378,10 +378,10 @@ namespace osu.Game.Tests.Visual.Gameplay } private SkinnableContainer globalHUDTarget => Player.ChildrenOfType() - .Single(c => c.Lookup.Target == SkinComponentsContainerLookup.TargetArea.MainHUDComponents && c.Lookup.Ruleset == null); + .Single(c => c.Lookup.Target == GlobalSkinnableContainerLookup.GlobalSkinnableContainers.MainHUDComponents && c.Lookup.Ruleset == null); private SkinnableContainer rulesetHUDTarget => Player.ChildrenOfType() - .Single(c => c.Lookup.Target == SkinComponentsContainerLookup.TargetArea.MainHUDComponents && c.Lookup.Ruleset != null); + .Single(c => c.Lookup.Target == GlobalSkinnableContainerLookup.GlobalSkinnableContainers.MainHUDComponents && c.Lookup.Ruleset != null); [Test] public void TestMigrationArgon() diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditorComponentsList.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditorComponentsList.cs index 42dcfe12e9..e4b6358600 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditorComponentsList.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditorComponentsList.cs @@ -20,7 +20,7 @@ namespace osu.Game.Tests.Visual.Gameplay [Test] public void TestToggleEditor() { - var skinComponentsContainer = new SkinnableContainer(new SkinComponentsContainerLookup(SkinComponentsContainerLookup.TargetArea.SongSelect)); + var skinComponentsContainer = new SkinnableContainer(new GlobalSkinnableContainerLookup(GlobalSkinnableContainerLookup.GlobalSkinnableContainers.SongSelect)); AddStep("show available components", () => SetContents(_ => new SkinComponentToolbox(skinComponentsContainer, null) { diff --git a/osu.Game/Overlays/SkinEditor/SkinEditor.cs b/osu.Game/Overlays/SkinEditor/SkinEditor.cs index 78ddce03c7..d1e9676de7 100644 --- a/osu.Game/Overlays/SkinEditor/SkinEditor.cs +++ b/osu.Game/Overlays/SkinEditor/SkinEditor.cs @@ -72,7 +72,7 @@ namespace osu.Game.Overlays.SkinEditor [Cached] private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Blue); - private readonly Bindable selectedTarget = new Bindable(); + private readonly Bindable selectedTarget = new Bindable(); private bool hasBegunMutating; @@ -330,7 +330,7 @@ namespace osu.Game.Overlays.SkinEditor } } - private void targetChanged(ValueChangedEvent target) + private void targetChanged(ValueChangedEvent target) { foreach (var toolbox in componentsSidebar.OfType()) toolbox.Expire(); @@ -360,7 +360,7 @@ namespace osu.Game.Overlays.SkinEditor { Children = new Drawable[] { - new SettingsDropdown + new SettingsDropdown { Items = availableTargets.Select(t => t.Lookup).Distinct(), Current = selectedTarget, @@ -476,7 +476,7 @@ namespace osu.Game.Overlays.SkinEditor private SkinnableContainer? getFirstTarget() => availableTargets.FirstOrDefault(); - private SkinnableContainer? getTarget(SkinComponentsContainerLookup? target) + private SkinnableContainer? getTarget(GlobalSkinnableContainerLookup? target) { return availableTargets.FirstOrDefault(c => c.Lookup.Equals(target)); } diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index 73fda62616..7bddef534c 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -109,7 +109,7 @@ namespace osu.Game.Screens.Play private readonly List hideTargets; /// - /// The container for skin components attached to + /// The container for skin components attached to /// internal readonly Drawable PlayfieldSkinLayer; @@ -132,7 +132,7 @@ namespace osu.Game.Screens.Play ? (rulesetComponents = new HUDComponentsContainer(drawableRuleset.Ruleset.RulesetInfo) { AlwaysPresent = true, }) : Empty(), PlayfieldSkinLayer = drawableRuleset != null - ? new SkinnableContainer(new SkinComponentsContainerLookup(SkinComponentsContainerLookup.TargetArea.Playfield, drawableRuleset.Ruleset.RulesetInfo)) { AlwaysPresent = true, } + ? new SkinnableContainer(new GlobalSkinnableContainerLookup(GlobalSkinnableContainerLookup.GlobalSkinnableContainers.Playfield, drawableRuleset.Ruleset.RulesetInfo)) { AlwaysPresent = true, } : Empty(), topRightElements = new FillFlowContainer { @@ -448,7 +448,7 @@ namespace osu.Game.Screens.Play private OsuConfigManager config { get; set; } public HUDComponentsContainer([CanBeNull] RulesetInfo ruleset = null) - : base(new SkinComponentsContainerLookup(SkinComponentsContainerLookup.TargetArea.MainHUDComponents, ruleset)) + : base(new GlobalSkinnableContainerLookup(GlobalSkinnableContainerLookup.GlobalSkinnableContainers.MainHUDComponents, ruleset)) { RelativeSizeAxes = Axes.Both; } diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index a4a7351338..162ab0aa42 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -321,7 +321,7 @@ namespace osu.Game.Screens.Select } } }, - new SkinnableContainer(new SkinComponentsContainerLookup(SkinComponentsContainerLookup.TargetArea.SongSelect)) + new SkinnableContainer(new GlobalSkinnableContainerLookup(GlobalSkinnableContainerLookup.GlobalSkinnableContainers.SongSelect)) { RelativeSizeAxes = Axes.Both, }, diff --git a/osu.Game/Skinning/ArgonSkin.cs b/osu.Game/Skinning/ArgonSkin.cs index c66df82e0d..0155de588f 100644 --- a/osu.Game/Skinning/ArgonSkin.cs +++ b/osu.Game/Skinning/ArgonSkin.cs @@ -96,14 +96,14 @@ namespace osu.Game.Skinning switch (lookup) { - case SkinComponentsContainerLookup containerLookup: + case GlobalSkinnableContainerLookup containerLookup: if (base.GetDrawableComponent(lookup) is UserConfiguredLayoutContainer c) return c; switch (containerLookup.Target) { - case SkinComponentsContainerLookup.TargetArea.SongSelect: + case GlobalSkinnableContainerLookup.GlobalSkinnableContainers.SongSelect: var songSelectComponents = new DefaultSkinComponentsContainer(_ => { // do stuff when we need to. @@ -111,7 +111,7 @@ namespace osu.Game.Skinning return songSelectComponents; - case SkinComponentsContainerLookup.TargetArea.MainHUDComponents: + case GlobalSkinnableContainerLookup.GlobalSkinnableContainers.MainHUDComponents: if (containerLookup.Ruleset != null) { return new Container diff --git a/osu.Game/Skinning/SkinComponentsContainerLookup.cs b/osu.Game/Skinning/GlobalSkinnableContainerLookup.cs similarity index 79% rename from osu.Game/Skinning/SkinComponentsContainerLookup.cs rename to osu.Game/Skinning/GlobalSkinnableContainerLookup.cs index 34358c3f06..384b4aa23c 100644 --- a/osu.Game/Skinning/SkinComponentsContainerLookup.cs +++ b/osu.Game/Skinning/GlobalSkinnableContainerLookup.cs @@ -9,14 +9,14 @@ using osu.Game.Rulesets; namespace osu.Game.Skinning { /// - /// Represents a lookup of a collection of elements that make up a particular skinnable of the game. + /// Represents a lookup of a collection of elements that make up a particular skinnable of the game. /// - public class SkinComponentsContainerLookup : ISkinComponentLookup, IEquatable + public class GlobalSkinnableContainerLookup : ISkinComponentLookup, IEquatable { /// /// The target area / layer of the game for which skin components will be returned. /// - public readonly TargetArea Target; + public readonly GlobalSkinnableContainers Target; /// /// The ruleset for which skin components should be returned. @@ -24,7 +24,7 @@ namespace osu.Game.Skinning /// public readonly RulesetInfo? Ruleset; - public SkinComponentsContainerLookup(TargetArea target, RulesetInfo? ruleset = null) + public GlobalSkinnableContainerLookup(GlobalSkinnableContainers target, RulesetInfo? ruleset = null) { Target = target; Ruleset = ruleset; @@ -37,7 +37,7 @@ namespace osu.Game.Skinning return $"{Target.GetDescription()} (\"{Ruleset.Name}\" only)"; } - public bool Equals(SkinComponentsContainerLookup? other) + public bool Equals(GlobalSkinnableContainerLookup? other) { if (ReferenceEquals(null, other)) return false; if (ReferenceEquals(this, other)) return true; @@ -51,7 +51,7 @@ namespace osu.Game.Skinning if (ReferenceEquals(this, obj)) return true; if (obj.GetType() != GetType()) return false; - return Equals((SkinComponentsContainerLookup)obj); + return Equals((GlobalSkinnableContainerLookup)obj); } public override int GetHashCode() @@ -62,7 +62,7 @@ namespace osu.Game.Skinning /// /// Represents a particular area or part of a game screen whose layout can be customised using the skin editor. /// - public enum TargetArea + public enum GlobalSkinnableContainers { [Description("HUD")] MainHUDComponents, diff --git a/osu.Game/Skinning/LegacyBeatmapSkin.cs b/osu.Game/Skinning/LegacyBeatmapSkin.cs index 9cd072b607..54e259a807 100644 --- a/osu.Game/Skinning/LegacyBeatmapSkin.cs +++ b/osu.Game/Skinning/LegacyBeatmapSkin.cs @@ -50,11 +50,11 @@ namespace osu.Game.Skinning public override Drawable? GetDrawableComponent(ISkinComponentLookup lookup) { - if (lookup is SkinComponentsContainerLookup containerLookup) + if (lookup is GlobalSkinnableContainerLookup containerLookup) { switch (containerLookup.Target) { - case SkinComponentsContainerLookup.TargetArea.MainHUDComponents: + case GlobalSkinnableContainerLookup.GlobalSkinnableContainers.MainHUDComponents: // this should exist in LegacySkin instead, but there isn't a fallback skin for LegacySkins yet. // therefore keep the check here until fallback default legacy skin is supported. if (!this.HasFont(LegacyFont.Score)) diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index bbca0178d5..d9da208a7b 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -358,13 +358,13 @@ namespace osu.Game.Skinning { switch (lookup) { - case SkinComponentsContainerLookup containerLookup: + case GlobalSkinnableContainerLookup containerLookup: if (base.GetDrawableComponent(lookup) is UserConfiguredLayoutContainer c) return c; switch (containerLookup.Target) { - case SkinComponentsContainerLookup.TargetArea.MainHUDComponents: + case GlobalSkinnableContainerLookup.GlobalSkinnableContainers.MainHUDComponents: if (containerLookup.Ruleset != null) { return new DefaultSkinComponentsContainer(container => diff --git a/osu.Game/Skinning/Skin.cs b/osu.Game/Skinning/Skin.cs index 7c205b5289..581c47402f 100644 --- a/osu.Game/Skinning/Skin.cs +++ b/osu.Game/Skinning/Skin.cs @@ -43,10 +43,10 @@ namespace osu.Game.Skinning public SkinConfiguration Configuration { get; set; } - public IDictionary LayoutInfos => layoutInfos; + public IDictionary LayoutInfos => layoutInfos; - private readonly Dictionary layoutInfos = - new Dictionary(); + private readonly Dictionary layoutInfos = + new Dictionary(); public abstract ISample? GetSample(ISampleInfo sampleInfo); @@ -123,7 +123,7 @@ namespace osu.Game.Skinning } // skininfo files may be null for default skin. - foreach (SkinComponentsContainerLookup.TargetArea skinnableTarget in Enum.GetValues()) + foreach (GlobalSkinnableContainerLookup.GlobalSkinnableContainers skinnableTarget in Enum.GetValues()) { string filename = $"{skinnableTarget}.json"; @@ -187,7 +187,7 @@ namespace osu.Game.Skinning case SkinnableSprite.SpriteComponentLookup sprite: return this.GetAnimation(sprite.LookupName, false, false, maxSize: sprite.MaxSize); - case SkinComponentsContainerLookup containerLookup: + case GlobalSkinnableContainerLookup containerLookup: // It is important to return null if the user has not configured this yet. // This allows skin transformers the opportunity to provide default components. @@ -206,7 +206,7 @@ namespace osu.Game.Skinning #region Deserialisation & Migration - private SkinLayoutInfo? parseLayoutInfo(string jsonContent, SkinComponentsContainerLookup.TargetArea target) + private SkinLayoutInfo? parseLayoutInfo(string jsonContent, GlobalSkinnableContainerLookup.GlobalSkinnableContainers target) { SkinLayoutInfo? layout = null; @@ -245,7 +245,7 @@ namespace osu.Game.Skinning return layout; } - private void applyMigration(SkinLayoutInfo layout, SkinComponentsContainerLookup.TargetArea target, int version) + private void applyMigration(SkinLayoutInfo layout, GlobalSkinnableContainerLookup.GlobalSkinnableContainers target, int version) { switch (version) { @@ -253,7 +253,7 @@ namespace osu.Game.Skinning { // Combo counters were moved out of the global HUD components into per-ruleset. // This is to allow some rulesets to customise further (ie. mania and catch moving the combo to within their play area). - if (target != SkinComponentsContainerLookup.TargetArea.MainHUDComponents || + if (target != GlobalSkinnableContainerLookup.GlobalSkinnableContainers.MainHUDComponents || !layout.TryGetDrawableInfo(null, out var globalHUDComponents) || resources == null) break; diff --git a/osu.Game/Skinning/SkinnableContainer.cs b/osu.Game/Skinning/SkinnableContainer.cs index d2d4fac766..c58992c541 100644 --- a/osu.Game/Skinning/SkinnableContainer.cs +++ b/osu.Game/Skinning/SkinnableContainer.cs @@ -26,7 +26,7 @@ namespace osu.Game.Skinning /// /// The lookup criteria which will be used to retrieve components from the active skin. /// - public SkinComponentsContainerLookup Lookup { get; } + public GlobalSkinnableContainerLookup Lookup { get; } public IBindableList Components => components; @@ -38,7 +38,7 @@ namespace osu.Game.Skinning private CancellationTokenSource? cancellationSource; - public SkinnableContainer(SkinComponentsContainerLookup lookup) + public SkinnableContainer(GlobalSkinnableContainerLookup lookup) { Lookup = lookup; } diff --git a/osu.Game/Skinning/TrianglesSkin.cs b/osu.Game/Skinning/TrianglesSkin.cs index 7971aee794..8e694b4c3f 100644 --- a/osu.Game/Skinning/TrianglesSkin.cs +++ b/osu.Game/Skinning/TrianglesSkin.cs @@ -66,7 +66,7 @@ namespace osu.Game.Skinning switch (lookup) { - case SkinComponentsContainerLookup containerLookup: + case GlobalSkinnableContainerLookup containerLookup: if (base.GetDrawableComponent(lookup) is UserConfiguredLayoutContainer c) return c; @@ -76,7 +76,7 @@ namespace osu.Game.Skinning switch (containerLookup.Target) { - case SkinComponentsContainerLookup.TargetArea.SongSelect: + case GlobalSkinnableContainerLookup.GlobalSkinnableContainers.SongSelect: var songSelectComponents = new DefaultSkinComponentsContainer(_ => { // do stuff when we need to. @@ -84,7 +84,7 @@ namespace osu.Game.Skinning return songSelectComponents; - case SkinComponentsContainerLookup.TargetArea.MainHUDComponents: + case GlobalSkinnableContainerLookup.GlobalSkinnableContainers.MainHUDComponents: var skinnableTargetWrapper = new DefaultSkinComponentsContainer(container => { var score = container.OfType().FirstOrDefault(); From 36b4013fa64857c23c70ab8f591bc2cc6b18c44f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 22 Aug 2024 18:42:38 +0900 Subject: [PATCH 2062/2493] Rename `GameplaySkinComponentLookup` -> `SkinComponentLookup` --- .../CatchSkinComponentLookup.cs | 2 +- .../ManiaSkinComponentLookup.cs | 2 +- .../Argon/ManiaArgonSkinTransformer.cs | 2 +- .../Legacy/ManiaLegacySkinTransformer.cs | 2 +- .../OsuSkinComponentLookup.cs | 2 +- .../Skinning/Argon/OsuArgonSkinTransformer.cs | 2 +- .../Default/OsuTrianglesSkinTransformer.cs | 2 +- .../Argon/TaikoArgonSkinTransformer.cs | 2 +- .../Legacy/TaikoLegacySkinTransformer.cs | 2 +- .../TaikoSkinComponentLookup.cs | 2 +- .../Rulesets/Judgements/DrawableJudgement.cs | 2 +- .../Skinning/GameplaySkinComponentLookup.cs | 28 ------------------- osu.Game/Skinning/ISkinComponentLookup.cs | 2 +- osu.Game/Skinning/LegacySkin.cs | 2 +- osu.Game/Skinning/SkinComponentLookup.cs | 22 +++++++++++++++ 15 files changed, 35 insertions(+), 41 deletions(-) delete mode 100644 osu.Game/Skinning/GameplaySkinComponentLookup.cs create mode 100644 osu.Game/Skinning/SkinComponentLookup.cs diff --git a/osu.Game.Rulesets.Catch/CatchSkinComponentLookup.cs b/osu.Game.Rulesets.Catch/CatchSkinComponentLookup.cs index 596b102ac5..7f91d2990b 100644 --- a/osu.Game.Rulesets.Catch/CatchSkinComponentLookup.cs +++ b/osu.Game.Rulesets.Catch/CatchSkinComponentLookup.cs @@ -5,7 +5,7 @@ using osu.Game.Skinning; namespace osu.Game.Rulesets.Catch { - public class CatchSkinComponentLookup : GameplaySkinComponentLookup + public class CatchSkinComponentLookup : SkinComponentLookup { public CatchSkinComponentLookup(CatchSkinComponents component) : base(component) diff --git a/osu.Game.Rulesets.Mania/ManiaSkinComponentLookup.cs b/osu.Game.Rulesets.Mania/ManiaSkinComponentLookup.cs index 046d1c5b34..f3613eff99 100644 --- a/osu.Game.Rulesets.Mania/ManiaSkinComponentLookup.cs +++ b/osu.Game.Rulesets.Mania/ManiaSkinComponentLookup.cs @@ -5,7 +5,7 @@ using osu.Game.Skinning; namespace osu.Game.Rulesets.Mania { - public class ManiaSkinComponentLookup : GameplaySkinComponentLookup + public class ManiaSkinComponentLookup : SkinComponentLookup { /// /// Creates a new . diff --git a/osu.Game.Rulesets.Mania/Skinning/Argon/ManiaArgonSkinTransformer.cs b/osu.Game.Rulesets.Mania/Skinning/Argon/ManiaArgonSkinTransformer.cs index f80cb3a88a..d13f0ca21b 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Argon/ManiaArgonSkinTransformer.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Argon/ManiaArgonSkinTransformer.cs @@ -59,7 +59,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon return null; - case GameplaySkinComponentLookup resultComponent: + case SkinComponentLookup resultComponent: // This should eventually be moved to a skin setting, when supported. if (Skin is ArgonProSkin && resultComponent.Component >= HitResult.Great) return Drawable.Empty(); diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs index 20017a78a2..c9fb55e9ce 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs @@ -114,7 +114,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy return null; - case GameplaySkinComponentLookup resultComponent: + case SkinComponentLookup resultComponent: return getResult(resultComponent.Component); case ManiaSkinComponentLookup maniaComponent: diff --git a/osu.Game.Rulesets.Osu/OsuSkinComponentLookup.cs b/osu.Game.Rulesets.Osu/OsuSkinComponentLookup.cs index 3b3653e1ba..86a68c799f 100644 --- a/osu.Game.Rulesets.Osu/OsuSkinComponentLookup.cs +++ b/osu.Game.Rulesets.Osu/OsuSkinComponentLookup.cs @@ -5,7 +5,7 @@ using osu.Game.Skinning; namespace osu.Game.Rulesets.Osu { - public class OsuSkinComponentLookup : GameplaySkinComponentLookup + public class OsuSkinComponentLookup : SkinComponentLookup { public OsuSkinComponentLookup(OsuSkinComponents component) : base(component) diff --git a/osu.Game.Rulesets.Osu/Skinning/Argon/OsuArgonSkinTransformer.cs b/osu.Game.Rulesets.Osu/Skinning/Argon/OsuArgonSkinTransformer.cs index ec63e1194d..9f6f65c206 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Argon/OsuArgonSkinTransformer.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Argon/OsuArgonSkinTransformer.cs @@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon { switch (lookup) { - case GameplaySkinComponentLookup resultComponent: + case SkinComponentLookup resultComponent: HitResult result = resultComponent.Component; // This should eventually be moved to a skin setting, when supported. diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/OsuTrianglesSkinTransformer.cs b/osu.Game.Rulesets.Osu/Skinning/Default/OsuTrianglesSkinTransformer.cs index 7a4c768aa2..ef8cb12286 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Default/OsuTrianglesSkinTransformer.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Default/OsuTrianglesSkinTransformer.cs @@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default { switch (lookup) { - case GameplaySkinComponentLookup resultComponent: + case SkinComponentLookup resultComponent: HitResult result = resultComponent.Component; switch (result) diff --git a/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs b/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs index 973b4a91ff..bfc9e8648d 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs @@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon { switch (lookup) { - case GameplaySkinComponentLookup resultComponent: + case SkinComponentLookup resultComponent: // This should eventually be moved to a skin setting, when supported. if (Skin is ArgonProSkin && resultComponent.Component >= HitResult.Great) return Drawable.Empty(); diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs index 894b91e9ce..5bdb824f1c 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs @@ -29,7 +29,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy public override Drawable? GetDrawableComponent(ISkinComponentLookup lookup) { - if (lookup is GameplaySkinComponentLookup) + if (lookup is SkinComponentLookup) { // if a taiko skin is providing explosion sprites, hide the judgements completely if (hasExplosion.Value) diff --git a/osu.Game.Rulesets.Taiko/TaikoSkinComponentLookup.cs b/osu.Game.Rulesets.Taiko/TaikoSkinComponentLookup.cs index 8841c3d3ca..2fa4d3c9cb 100644 --- a/osu.Game.Rulesets.Taiko/TaikoSkinComponentLookup.cs +++ b/osu.Game.Rulesets.Taiko/TaikoSkinComponentLookup.cs @@ -5,7 +5,7 @@ using osu.Game.Skinning; namespace osu.Game.Rulesets.Taiko { - public class TaikoSkinComponentLookup : GameplaySkinComponentLookup + public class TaikoSkinComponentLookup : SkinComponentLookup { public TaikoSkinComponentLookup(TaikoSkinComponents component) : base(component) diff --git a/osu.Game/Rulesets/Judgements/DrawableJudgement.cs b/osu.Game/Rulesets/Judgements/DrawableJudgement.cs index 8c326ecf49..3e70f52ee7 100644 --- a/osu.Game/Rulesets/Judgements/DrawableJudgement.cs +++ b/osu.Game/Rulesets/Judgements/DrawableJudgement.cs @@ -163,7 +163,7 @@ namespace osu.Game.Rulesets.Judgements if (JudgementBody != null) RemoveInternal(JudgementBody, true); - AddInternal(JudgementBody = new SkinnableDrawable(new GameplaySkinComponentLookup(type), _ => + AddInternal(JudgementBody = new SkinnableDrawable(new SkinComponentLookup(type), _ => CreateDefaultJudgement(type), confineMode: ConfineMode.NoScaling)); JudgementBody.OnSkinChanged += () => diff --git a/osu.Game/Skinning/GameplaySkinComponentLookup.cs b/osu.Game/Skinning/GameplaySkinComponentLookup.cs deleted file mode 100644 index c317a17e21..0000000000 --- a/osu.Game/Skinning/GameplaySkinComponentLookup.cs +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System; -using osu.Game.Rulesets.Judgements; -using osu.Game.Rulesets.Scoring; - -namespace osu.Game.Skinning -{ - /// - /// A lookup type intended for use for skinnable gameplay components (not HUD level components). - /// - /// - /// The most common usage of this class is for ruleset-specific skinning implementations, but it can also be used directly - /// (see 's usage for ) where ruleset-agnostic elements are required. - /// - /// An enum lookup type. - public class GameplaySkinComponentLookup : ISkinComponentLookup - where T : Enum - { - public readonly T Component; - - public GameplaySkinComponentLookup(T component) - { - Component = component; - } - } -} diff --git a/osu.Game/Skinning/ISkinComponentLookup.cs b/osu.Game/Skinning/ISkinComponentLookup.cs index 25ee086707..af2b512331 100644 --- a/osu.Game/Skinning/ISkinComponentLookup.cs +++ b/osu.Game/Skinning/ISkinComponentLookup.cs @@ -12,7 +12,7 @@ namespace osu.Game.Skinning /// to scope particular lookup variations. Using this, a ruleset or skin implementation could make its own lookup /// type to scope away from more global contexts. /// - /// More commonly, a ruleset could make use of to do a simple lookup based on + /// More commonly, a ruleset could make use of to do a simple lookup based on /// a provided enum. /// public interface ISkinComponentLookup diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index d9da208a7b..078bef9d0d 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -426,7 +426,7 @@ namespace osu.Game.Skinning return null; - case GameplaySkinComponentLookup resultComponent: + case SkinComponentLookup resultComponent: // kind of wasteful that we throw this away, but should do for now. if (getJudgementAnimation(resultComponent.Component) != null) diff --git a/osu.Game/Skinning/SkinComponentLookup.cs b/osu.Game/Skinning/SkinComponentLookup.cs new file mode 100644 index 0000000000..4da6bb0c08 --- /dev/null +++ b/osu.Game/Skinning/SkinComponentLookup.cs @@ -0,0 +1,22 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; + +namespace osu.Game.Skinning +{ + /// + /// A lookup type intended for use for skinnable components. + /// + /// An enum lookup type. + public class SkinComponentLookup : ISkinComponentLookup + where T : Enum + { + public readonly T Component; + + public SkinComponentLookup(T component) + { + Component = component; + } + } +} From 9a21174582a53c5e77fd423c678002b2b0b0e9a9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 22 Aug 2024 18:45:44 +0900 Subject: [PATCH 2063/2493] Move `GlobalSkinnableContainers` to global scope --- .../Legacy/CatchLegacySkinTransformer.cs | 2 +- .../Argon/ManiaArgonSkinTransformer.cs | 2 +- .../Legacy/ManiaLegacySkinTransformer.cs | 2 +- .../Legacy/OsuLegacySkinTransformer.cs | 2 +- .../Skins/SkinDeserialisationTest.cs | 20 ++++++++--------- .../Gameplay/TestSceneBeatmapSkinFallbacks.cs | 4 ++-- .../Visual/Gameplay/TestSceneSkinEditor.cs | 4 ++-- .../TestSceneSkinEditorComponentsList.cs | 2 +- osu.Game/Screens/Play/HUDOverlay.cs | 6 ++--- osu.Game/Screens/Select/SongSelect.cs | 2 +- osu.Game/Skinning/ArgonSkin.cs | 4 ++-- .../GlobalSkinnableContainerLookup.cs | 16 -------------- .../Skinning/GlobalSkinnableContainers.cs | 22 +++++++++++++++++++ osu.Game/Skinning/LegacyBeatmapSkin.cs | 2 +- osu.Game/Skinning/LegacySkin.cs | 2 +- osu.Game/Skinning/Skin.cs | 14 ++++++------ osu.Game/Skinning/TrianglesSkin.cs | 4 ++-- 17 files changed, 58 insertions(+), 52 deletions(-) create mode 100644 osu.Game/Skinning/GlobalSkinnableContainers.cs diff --git a/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs b/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs index ab0420554e..61ef1de2b9 100644 --- a/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs @@ -46,7 +46,7 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy // Our own ruleset components default. switch (containerLookup.Target) { - case GlobalSkinnableContainerLookup.GlobalSkinnableContainers.MainHUDComponents: + case GlobalSkinnableContainers.MainHUDComponents: // todo: remove CatchSkinComponents.CatchComboCounter and refactor LegacyCatchComboCounter to be added here instead. return new DefaultSkinComponentsContainer(container => { diff --git a/osu.Game.Rulesets.Mania/Skinning/Argon/ManiaArgonSkinTransformer.cs b/osu.Game.Rulesets.Mania/Skinning/Argon/ManiaArgonSkinTransformer.cs index d13f0ca21b..4d9798b264 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Argon/ManiaArgonSkinTransformer.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Argon/ManiaArgonSkinTransformer.cs @@ -39,7 +39,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon switch (containerLookup.Target) { - case GlobalSkinnableContainerLookup.GlobalSkinnableContainers.MainHUDComponents: + case GlobalSkinnableContainers.MainHUDComponents: return new DefaultSkinComponentsContainer(container => { var combo = container.ChildrenOfType().FirstOrDefault(); diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs index c9fb55e9ce..2a79d58f22 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs @@ -95,7 +95,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy switch (containerLookup.Target) { - case GlobalSkinnableContainerLookup.GlobalSkinnableContainers.MainHUDComponents: + case GlobalSkinnableContainers.MainHUDComponents: return new DefaultSkinComponentsContainer(container => { var combo = container.ChildrenOfType().FirstOrDefault(); diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs index 6609a84be4..12dac18694 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs @@ -60,7 +60,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy // Our own ruleset components default. switch (containerLookup.Target) { - case GlobalSkinnableContainerLookup.GlobalSkinnableContainers.MainHUDComponents: + case GlobalSkinnableContainers.MainHUDComponents: return new DefaultSkinComponentsContainer(container => { var keyCounter = container.OfType().FirstOrDefault(); diff --git a/osu.Game.Tests/Skins/SkinDeserialisationTest.cs b/osu.Game.Tests/Skins/SkinDeserialisationTest.cs index ad01a057ad..82b46ee75f 100644 --- a/osu.Game.Tests/Skins/SkinDeserialisationTest.cs +++ b/osu.Game.Tests/Skins/SkinDeserialisationTest.cs @@ -107,7 +107,7 @@ namespace osu.Game.Tests.Skins var skin = new TestSkin(new SkinInfo(), null, storage); Assert.That(skin.LayoutInfos, Has.Count.EqualTo(2)); - Assert.That(skin.LayoutInfos[GlobalSkinnableContainerLookup.GlobalSkinnableContainers.MainHUDComponents].AllDrawables.ToArray(), Has.Length.EqualTo(9)); + Assert.That(skin.LayoutInfos[GlobalSkinnableContainers.MainHUDComponents].AllDrawables.ToArray(), Has.Length.EqualTo(9)); } } @@ -120,8 +120,8 @@ namespace osu.Game.Tests.Skins var skin = new TestSkin(new SkinInfo(), null, storage); Assert.That(skin.LayoutInfos, Has.Count.EqualTo(2)); - Assert.That(skin.LayoutInfos[GlobalSkinnableContainerLookup.GlobalSkinnableContainers.MainHUDComponents].AllDrawables.ToArray(), Has.Length.EqualTo(10)); - Assert.That(skin.LayoutInfos[GlobalSkinnableContainerLookup.GlobalSkinnableContainers.MainHUDComponents].AllDrawables.Select(i => i.Type), Contains.Item(typeof(PlayerName))); + Assert.That(skin.LayoutInfos[GlobalSkinnableContainers.MainHUDComponents].AllDrawables.ToArray(), Has.Length.EqualTo(10)); + Assert.That(skin.LayoutInfos[GlobalSkinnableContainers.MainHUDComponents].AllDrawables.Select(i => i.Type), Contains.Item(typeof(PlayerName))); } } @@ -134,10 +134,10 @@ namespace osu.Game.Tests.Skins var skin = new TestSkin(new SkinInfo(), null, storage); Assert.That(skin.LayoutInfos, Has.Count.EqualTo(2)); - Assert.That(skin.LayoutInfos[GlobalSkinnableContainerLookup.GlobalSkinnableContainers.MainHUDComponents].AllDrawables.ToArray(), Has.Length.EqualTo(6)); - Assert.That(skin.LayoutInfos[GlobalSkinnableContainerLookup.GlobalSkinnableContainers.SongSelect].AllDrawables.ToArray(), Has.Length.EqualTo(1)); + Assert.That(skin.LayoutInfos[GlobalSkinnableContainers.MainHUDComponents].AllDrawables.ToArray(), Has.Length.EqualTo(6)); + Assert.That(skin.LayoutInfos[GlobalSkinnableContainers.SongSelect].AllDrawables.ToArray(), Has.Length.EqualTo(1)); - var skinnableInfo = skin.LayoutInfos[GlobalSkinnableContainerLookup.GlobalSkinnableContainers.SongSelect].AllDrawables.First(); + var skinnableInfo = skin.LayoutInfos[GlobalSkinnableContainers.SongSelect].AllDrawables.First(); Assert.That(skinnableInfo.Type, Is.EqualTo(typeof(SkinnableSprite))); Assert.That(skinnableInfo.Settings.First().Key, Is.EqualTo("sprite_name")); @@ -148,10 +148,10 @@ namespace osu.Game.Tests.Skins using (var storage = new ZipArchiveReader(stream)) { var skin = new TestSkin(new SkinInfo(), null, storage); - Assert.That(skin.LayoutInfos[GlobalSkinnableContainerLookup.GlobalSkinnableContainers.MainHUDComponents].AllDrawables.ToArray(), Has.Length.EqualTo(8)); - Assert.That(skin.LayoutInfos[GlobalSkinnableContainerLookup.GlobalSkinnableContainers.MainHUDComponents].AllDrawables.Select(i => i.Type), Contains.Item(typeof(UnstableRateCounter))); - Assert.That(skin.LayoutInfos[GlobalSkinnableContainerLookup.GlobalSkinnableContainers.MainHUDComponents].AllDrawables.Select(i => i.Type), Contains.Item(typeof(ColourHitErrorMeter))); - Assert.That(skin.LayoutInfos[GlobalSkinnableContainerLookup.GlobalSkinnableContainers.MainHUDComponents].AllDrawables.Select(i => i.Type), Contains.Item(typeof(LegacySongProgress))); + Assert.That(skin.LayoutInfos[GlobalSkinnableContainers.MainHUDComponents].AllDrawables.ToArray(), Has.Length.EqualTo(8)); + Assert.That(skin.LayoutInfos[GlobalSkinnableContainers.MainHUDComponents].AllDrawables.Select(i => i.Type), Contains.Item(typeof(UnstableRateCounter))); + Assert.That(skin.LayoutInfos[GlobalSkinnableContainers.MainHUDComponents].AllDrawables.Select(i => i.Type), Contains.Item(typeof(ColourHitErrorMeter))); + Assert.That(skin.LayoutInfos[GlobalSkinnableContainers.MainHUDComponents].AllDrawables.Select(i => i.Type), Contains.Item(typeof(LegacySongProgress))); } } diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs index 1061f493d4..5230cea7a5 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs @@ -38,7 +38,7 @@ namespace osu.Game.Tests.Visual.Gameplay { CreateSkinTest(TrianglesSkin.CreateInfo(), () => new LegacyBeatmapSkin(new BeatmapInfo(), null)); AddUntilStep("wait for hud load", () => Player.ChildrenOfType().All(c => c.ComponentsLoaded)); - AddAssert("hud from default skin", () => AssertComponentsFromExpectedSource(GlobalSkinnableContainerLookup.GlobalSkinnableContainers.MainHUDComponents, skinManager.CurrentSkin.Value)); + AddAssert("hud from default skin", () => AssertComponentsFromExpectedSource(GlobalSkinnableContainers.MainHUDComponents, skinManager.CurrentSkin.Value)); } protected void CreateSkinTest(SkinInfo gameCurrentSkin, Func getBeatmapSkin) @@ -53,7 +53,7 @@ namespace osu.Game.Tests.Visual.Gameplay }); } - protected bool AssertComponentsFromExpectedSource(GlobalSkinnableContainerLookup.GlobalSkinnableContainers target, ISkin expectedSource) + protected bool AssertComponentsFromExpectedSource(GlobalSkinnableContainers target, ISkin expectedSource) { var targetContainer = Player.ChildrenOfType().First(s => s.Lookup.Target == target); var actualComponentsContainer = targetContainer.ChildrenOfType().SingleOrDefault(c => c.Parent == targetContainer); diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs index 9e53f86e33..2a2bff218a 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs @@ -378,10 +378,10 @@ namespace osu.Game.Tests.Visual.Gameplay } private SkinnableContainer globalHUDTarget => Player.ChildrenOfType() - .Single(c => c.Lookup.Target == GlobalSkinnableContainerLookup.GlobalSkinnableContainers.MainHUDComponents && c.Lookup.Ruleset == null); + .Single(c => c.Lookup.Target == GlobalSkinnableContainers.MainHUDComponents && c.Lookup.Ruleset == null); private SkinnableContainer rulesetHUDTarget => Player.ChildrenOfType() - .Single(c => c.Lookup.Target == GlobalSkinnableContainerLookup.GlobalSkinnableContainers.MainHUDComponents && c.Lookup.Ruleset != null); + .Single(c => c.Lookup.Target == GlobalSkinnableContainers.MainHUDComponents && c.Lookup.Ruleset != null); [Test] public void TestMigrationArgon() diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditorComponentsList.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditorComponentsList.cs index e4b6358600..b5fe6633b6 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditorComponentsList.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditorComponentsList.cs @@ -20,7 +20,7 @@ namespace osu.Game.Tests.Visual.Gameplay [Test] public void TestToggleEditor() { - var skinComponentsContainer = new SkinnableContainer(new GlobalSkinnableContainerLookup(GlobalSkinnableContainerLookup.GlobalSkinnableContainers.SongSelect)); + var skinComponentsContainer = new SkinnableContainer(new GlobalSkinnableContainerLookup(GlobalSkinnableContainers.SongSelect)); AddStep("show available components", () => SetContents(_ => new SkinComponentToolbox(skinComponentsContainer, null) { diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index 7bddef534c..ac1b9ce34f 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -109,7 +109,7 @@ namespace osu.Game.Screens.Play private readonly List hideTargets; /// - /// The container for skin components attached to + /// The container for skin components attached to /// internal readonly Drawable PlayfieldSkinLayer; @@ -132,7 +132,7 @@ namespace osu.Game.Screens.Play ? (rulesetComponents = new HUDComponentsContainer(drawableRuleset.Ruleset.RulesetInfo) { AlwaysPresent = true, }) : Empty(), PlayfieldSkinLayer = drawableRuleset != null - ? new SkinnableContainer(new GlobalSkinnableContainerLookup(GlobalSkinnableContainerLookup.GlobalSkinnableContainers.Playfield, drawableRuleset.Ruleset.RulesetInfo)) { AlwaysPresent = true, } + ? new SkinnableContainer(new GlobalSkinnableContainerLookup(GlobalSkinnableContainers.Playfield, drawableRuleset.Ruleset.RulesetInfo)) { AlwaysPresent = true, } : Empty(), topRightElements = new FillFlowContainer { @@ -448,7 +448,7 @@ namespace osu.Game.Screens.Play private OsuConfigManager config { get; set; } public HUDComponentsContainer([CanBeNull] RulesetInfo ruleset = null) - : base(new GlobalSkinnableContainerLookup(GlobalSkinnableContainerLookup.GlobalSkinnableContainers.MainHUDComponents, ruleset)) + : base(new GlobalSkinnableContainerLookup(GlobalSkinnableContainers.MainHUDComponents, ruleset)) { RelativeSizeAxes = Axes.Both; } diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index 162ab0aa42..2965aa383d 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -321,7 +321,7 @@ namespace osu.Game.Screens.Select } } }, - new SkinnableContainer(new GlobalSkinnableContainerLookup(GlobalSkinnableContainerLookup.GlobalSkinnableContainers.SongSelect)) + new SkinnableContainer(new GlobalSkinnableContainerLookup(GlobalSkinnableContainers.SongSelect)) { RelativeSizeAxes = Axes.Both, }, diff --git a/osu.Game/Skinning/ArgonSkin.cs b/osu.Game/Skinning/ArgonSkin.cs index 0155de588f..74ab3d885e 100644 --- a/osu.Game/Skinning/ArgonSkin.cs +++ b/osu.Game/Skinning/ArgonSkin.cs @@ -103,7 +103,7 @@ namespace osu.Game.Skinning switch (containerLookup.Target) { - case GlobalSkinnableContainerLookup.GlobalSkinnableContainers.SongSelect: + case GlobalSkinnableContainers.SongSelect: var songSelectComponents = new DefaultSkinComponentsContainer(_ => { // do stuff when we need to. @@ -111,7 +111,7 @@ namespace osu.Game.Skinning return songSelectComponents; - case GlobalSkinnableContainerLookup.GlobalSkinnableContainers.MainHUDComponents: + case GlobalSkinnableContainers.MainHUDComponents: if (containerLookup.Ruleset != null) { return new Container diff --git a/osu.Game/Skinning/GlobalSkinnableContainerLookup.cs b/osu.Game/Skinning/GlobalSkinnableContainerLookup.cs index 384b4aa23c..cac8c3bb2f 100644 --- a/osu.Game/Skinning/GlobalSkinnableContainerLookup.cs +++ b/osu.Game/Skinning/GlobalSkinnableContainerLookup.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.ComponentModel; using osu.Framework.Extensions; using osu.Game.Rulesets; @@ -58,20 +57,5 @@ namespace osu.Game.Skinning { return HashCode.Combine((int)Target, Ruleset); } - - /// - /// Represents a particular area or part of a game screen whose layout can be customised using the skin editor. - /// - public enum GlobalSkinnableContainers - { - [Description("HUD")] - MainHUDComponents, - - [Description("Song select")] - SongSelect, - - [Description("Playfield")] - Playfield - } } } diff --git a/osu.Game/Skinning/GlobalSkinnableContainers.cs b/osu.Game/Skinning/GlobalSkinnableContainers.cs new file mode 100644 index 0000000000..02f915895f --- /dev/null +++ b/osu.Game/Skinning/GlobalSkinnableContainers.cs @@ -0,0 +1,22 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.ComponentModel; + +namespace osu.Game.Skinning +{ + /// + /// Represents a particular area or part of a game screen whose layout can be customised using the skin editor. + /// + public enum GlobalSkinnableContainers + { + [Description("HUD")] + MainHUDComponents, + + [Description("Song select")] + SongSelect, + + [Description("Playfield")] + Playfield + } +} diff --git a/osu.Game/Skinning/LegacyBeatmapSkin.cs b/osu.Game/Skinning/LegacyBeatmapSkin.cs index 54e259a807..81dc79b25f 100644 --- a/osu.Game/Skinning/LegacyBeatmapSkin.cs +++ b/osu.Game/Skinning/LegacyBeatmapSkin.cs @@ -54,7 +54,7 @@ namespace osu.Game.Skinning { switch (containerLookup.Target) { - case GlobalSkinnableContainerLookup.GlobalSkinnableContainers.MainHUDComponents: + case GlobalSkinnableContainers.MainHUDComponents: // this should exist in LegacySkin instead, but there isn't a fallback skin for LegacySkins yet. // therefore keep the check here until fallback default legacy skin is supported. if (!this.HasFont(LegacyFont.Score)) diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index 078bef9d0d..0085bf62ac 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -364,7 +364,7 @@ namespace osu.Game.Skinning switch (containerLookup.Target) { - case GlobalSkinnableContainerLookup.GlobalSkinnableContainers.MainHUDComponents: + case GlobalSkinnableContainers.MainHUDComponents: if (containerLookup.Ruleset != null) { return new DefaultSkinComponentsContainer(container => diff --git a/osu.Game/Skinning/Skin.cs b/osu.Game/Skinning/Skin.cs index 581c47402f..449a30c022 100644 --- a/osu.Game/Skinning/Skin.cs +++ b/osu.Game/Skinning/Skin.cs @@ -43,10 +43,10 @@ namespace osu.Game.Skinning public SkinConfiguration Configuration { get; set; } - public IDictionary LayoutInfos => layoutInfos; + public IDictionary LayoutInfos => layoutInfos; - private readonly Dictionary layoutInfos = - new Dictionary(); + private readonly Dictionary layoutInfos = + new Dictionary(); public abstract ISample? GetSample(ISampleInfo sampleInfo); @@ -123,7 +123,7 @@ namespace osu.Game.Skinning } // skininfo files may be null for default skin. - foreach (GlobalSkinnableContainerLookup.GlobalSkinnableContainers skinnableTarget in Enum.GetValues()) + foreach (GlobalSkinnableContainers skinnableTarget in Enum.GetValues()) { string filename = $"{skinnableTarget}.json"; @@ -206,7 +206,7 @@ namespace osu.Game.Skinning #region Deserialisation & Migration - private SkinLayoutInfo? parseLayoutInfo(string jsonContent, GlobalSkinnableContainerLookup.GlobalSkinnableContainers target) + private SkinLayoutInfo? parseLayoutInfo(string jsonContent, GlobalSkinnableContainers target) { SkinLayoutInfo? layout = null; @@ -245,7 +245,7 @@ namespace osu.Game.Skinning return layout; } - private void applyMigration(SkinLayoutInfo layout, GlobalSkinnableContainerLookup.GlobalSkinnableContainers target, int version) + private void applyMigration(SkinLayoutInfo layout, GlobalSkinnableContainers target, int version) { switch (version) { @@ -253,7 +253,7 @@ namespace osu.Game.Skinning { // Combo counters were moved out of the global HUD components into per-ruleset. // This is to allow some rulesets to customise further (ie. mania and catch moving the combo to within their play area). - if (target != GlobalSkinnableContainerLookup.GlobalSkinnableContainers.MainHUDComponents || + if (target != GlobalSkinnableContainers.MainHUDComponents || !layout.TryGetDrawableInfo(null, out var globalHUDComponents) || resources == null) break; diff --git a/osu.Game/Skinning/TrianglesSkin.cs b/osu.Game/Skinning/TrianglesSkin.cs index 8e694b4c3f..b0cb54a6f9 100644 --- a/osu.Game/Skinning/TrianglesSkin.cs +++ b/osu.Game/Skinning/TrianglesSkin.cs @@ -76,7 +76,7 @@ namespace osu.Game.Skinning switch (containerLookup.Target) { - case GlobalSkinnableContainerLookup.GlobalSkinnableContainers.SongSelect: + case GlobalSkinnableContainers.SongSelect: var songSelectComponents = new DefaultSkinComponentsContainer(_ => { // do stuff when we need to. @@ -84,7 +84,7 @@ namespace osu.Game.Skinning return songSelectComponents; - case GlobalSkinnableContainerLookup.GlobalSkinnableContainers.MainHUDComponents: + case GlobalSkinnableContainers.MainHUDComponents: var skinnableTargetWrapper = new DefaultSkinComponentsContainer(container => { var score = container.OfType().FirstOrDefault(); From b57b8168a62c8ab481b4f2d790cf6a0213f08b89 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 22 Aug 2024 19:00:15 +0900 Subject: [PATCH 2064/2493] Rename `Target` lookup to `Component` --- .../Skinning/Legacy/CatchLegacySkinTransformer.cs | 2 +- .../Skinning/Argon/ManiaArgonSkinTransformer.cs | 2 +- .../Skinning/Legacy/ManiaLegacySkinTransformer.cs | 2 +- .../Skinning/Legacy/OsuLegacySkinTransformer.cs | 2 +- .../Gameplay/TestSceneBeatmapSkinFallbacks.cs | 2 +- .../Visual/Gameplay/TestSceneSkinEditor.cs | 4 ++-- osu.Game/Skinning/ArgonSkin.cs | 2 +- .../Skinning/GlobalSkinnableContainerLookup.cs | 14 +++++++------- osu.Game/Skinning/LegacyBeatmapSkin.cs | 2 +- osu.Game/Skinning/LegacySkin.cs | 2 +- osu.Game/Skinning/Skin.cs | 8 ++++---- osu.Game/Skinning/TrianglesSkin.cs | 2 +- 12 files changed, 22 insertions(+), 22 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs b/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs index 61ef1de2b9..a62a712001 100644 --- a/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs @@ -44,7 +44,7 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy return null; // Our own ruleset components default. - switch (containerLookup.Target) + switch (containerLookup.Component) { case GlobalSkinnableContainers.MainHUDComponents: // todo: remove CatchSkinComponents.CatchComboCounter and refactor LegacyCatchComboCounter to be added here instead. diff --git a/osu.Game.Rulesets.Mania/Skinning/Argon/ManiaArgonSkinTransformer.cs b/osu.Game.Rulesets.Mania/Skinning/Argon/ManiaArgonSkinTransformer.cs index 4d9798b264..2c361df8b1 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Argon/ManiaArgonSkinTransformer.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Argon/ManiaArgonSkinTransformer.cs @@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon if (base.GetDrawableComponent(lookup) is UserConfiguredLayoutContainer d) return d; - switch (containerLookup.Target) + switch (containerLookup.Component) { case GlobalSkinnableContainers.MainHUDComponents: return new DefaultSkinComponentsContainer(container => diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs index 2a79d58f22..895f5a7cc1 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs @@ -93,7 +93,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy if (!IsProvidingLegacyResources) return null; - switch (containerLookup.Target) + switch (containerLookup.Component) { case GlobalSkinnableContainers.MainHUDComponents: return new DefaultSkinComponentsContainer(container => diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs index 12dac18694..26708f6686 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs @@ -58,7 +58,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy return null; // Our own ruleset components default. - switch (containerLookup.Target) + switch (containerLookup.Component) { case GlobalSkinnableContainers.MainHUDComponents: return new DefaultSkinComponentsContainer(container => diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs index 5230cea7a5..9a4f084d10 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs @@ -55,7 +55,7 @@ namespace osu.Game.Tests.Visual.Gameplay protected bool AssertComponentsFromExpectedSource(GlobalSkinnableContainers target, ISkin expectedSource) { - var targetContainer = Player.ChildrenOfType().First(s => s.Lookup.Target == target); + var targetContainer = Player.ChildrenOfType().First(s => s.Lookup.Component == target); var actualComponentsContainer = targetContainer.ChildrenOfType().SingleOrDefault(c => c.Parent == targetContainer); if (actualComponentsContainer == null) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs index 2a2bff218a..4dca8c9001 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs @@ -378,10 +378,10 @@ namespace osu.Game.Tests.Visual.Gameplay } private SkinnableContainer globalHUDTarget => Player.ChildrenOfType() - .Single(c => c.Lookup.Target == GlobalSkinnableContainers.MainHUDComponents && c.Lookup.Ruleset == null); + .Single(c => c.Lookup.Component == GlobalSkinnableContainers.MainHUDComponents && c.Lookup.Ruleset == null); private SkinnableContainer rulesetHUDTarget => Player.ChildrenOfType() - .Single(c => c.Lookup.Target == GlobalSkinnableContainers.MainHUDComponents && c.Lookup.Ruleset != null); + .Single(c => c.Lookup.Component == GlobalSkinnableContainers.MainHUDComponents && c.Lookup.Ruleset != null); [Test] public void TestMigrationArgon() diff --git a/osu.Game/Skinning/ArgonSkin.cs b/osu.Game/Skinning/ArgonSkin.cs index 74ab3d885e..2489013c1e 100644 --- a/osu.Game/Skinning/ArgonSkin.cs +++ b/osu.Game/Skinning/ArgonSkin.cs @@ -101,7 +101,7 @@ namespace osu.Game.Skinning if (base.GetDrawableComponent(lookup) is UserConfiguredLayoutContainer c) return c; - switch (containerLookup.Target) + switch (containerLookup.Component) { case GlobalSkinnableContainers.SongSelect: var songSelectComponents = new DefaultSkinComponentsContainer(_ => diff --git a/osu.Game/Skinning/GlobalSkinnableContainerLookup.cs b/osu.Game/Skinning/GlobalSkinnableContainerLookup.cs index cac8c3bb2f..524d99197a 100644 --- a/osu.Game/Skinning/GlobalSkinnableContainerLookup.cs +++ b/osu.Game/Skinning/GlobalSkinnableContainerLookup.cs @@ -15,7 +15,7 @@ namespace osu.Game.Skinning /// /// The target area / layer of the game for which skin components will be returned. /// - public readonly GlobalSkinnableContainers Target; + public readonly GlobalSkinnableContainers Component; /// /// The ruleset for which skin components should be returned. @@ -23,17 +23,17 @@ namespace osu.Game.Skinning /// public readonly RulesetInfo? Ruleset; - public GlobalSkinnableContainerLookup(GlobalSkinnableContainers target, RulesetInfo? ruleset = null) + public GlobalSkinnableContainerLookup(GlobalSkinnableContainers component, RulesetInfo? ruleset = null) { - Target = target; + Component = component; Ruleset = ruleset; } public override string ToString() { - if (Ruleset == null) return Target.GetDescription(); + if (Ruleset == null) return Component.GetDescription(); - return $"{Target.GetDescription()} (\"{Ruleset.Name}\" only)"; + return $"{Component.GetDescription()} (\"{Ruleset.Name}\" only)"; } public bool Equals(GlobalSkinnableContainerLookup? other) @@ -41,7 +41,7 @@ namespace osu.Game.Skinning if (ReferenceEquals(null, other)) return false; if (ReferenceEquals(this, other)) return true; - return Target == other.Target && (ReferenceEquals(Ruleset, other.Ruleset) || Ruleset?.Equals(other.Ruleset) == true); + return Component == other.Component && (ReferenceEquals(Ruleset, other.Ruleset) || Ruleset?.Equals(other.Ruleset) == true); } public override bool Equals(object? obj) @@ -55,7 +55,7 @@ namespace osu.Game.Skinning public override int GetHashCode() { - return HashCode.Combine((int)Target, Ruleset); + return HashCode.Combine((int)Component, Ruleset); } } } diff --git a/osu.Game/Skinning/LegacyBeatmapSkin.cs b/osu.Game/Skinning/LegacyBeatmapSkin.cs index 81dc79b25f..c8a93f418f 100644 --- a/osu.Game/Skinning/LegacyBeatmapSkin.cs +++ b/osu.Game/Skinning/LegacyBeatmapSkin.cs @@ -52,7 +52,7 @@ namespace osu.Game.Skinning { if (lookup is GlobalSkinnableContainerLookup containerLookup) { - switch (containerLookup.Target) + switch (containerLookup.Component) { case GlobalSkinnableContainers.MainHUDComponents: // this should exist in LegacySkin instead, but there isn't a fallback skin for LegacySkins yet. diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index 0085bf62ac..16d9cf391c 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -362,7 +362,7 @@ namespace osu.Game.Skinning if (base.GetDrawableComponent(lookup) is UserConfiguredLayoutContainer c) return c; - switch (containerLookup.Target) + switch (containerLookup.Component) { case GlobalSkinnableContainers.MainHUDComponents: if (containerLookup.Ruleset != null) diff --git a/osu.Game/Skinning/Skin.cs b/osu.Game/Skinning/Skin.cs index 449a30c022..04a7fd53f7 100644 --- a/osu.Game/Skinning/Skin.cs +++ b/osu.Game/Skinning/Skin.cs @@ -164,7 +164,7 @@ namespace osu.Game.Skinning /// The target container to reset. public void ResetDrawableTarget(SkinnableContainer targetContainer) { - LayoutInfos.Remove(targetContainer.Lookup.Target); + LayoutInfos.Remove(targetContainer.Lookup.Component); } /// @@ -173,8 +173,8 @@ namespace osu.Game.Skinning /// The target container to serialise to this skin. public void UpdateDrawableTarget(SkinnableContainer targetContainer) { - if (!LayoutInfos.TryGetValue(targetContainer.Lookup.Target, out var layoutInfo)) - layoutInfos[targetContainer.Lookup.Target] = layoutInfo = new SkinLayoutInfo(); + if (!LayoutInfos.TryGetValue(targetContainer.Lookup.Component, out var layoutInfo)) + layoutInfos[targetContainer.Lookup.Component] = layoutInfo = new SkinLayoutInfo(); layoutInfo.Update(targetContainer.Lookup.Ruleset, ((ISerialisableDrawableContainer)targetContainer).CreateSerialisedInfo().ToArray()); } @@ -191,7 +191,7 @@ namespace osu.Game.Skinning // It is important to return null if the user has not configured this yet. // This allows skin transformers the opportunity to provide default components. - if (!LayoutInfos.TryGetValue(containerLookup.Target, out var layoutInfo)) return null; + if (!LayoutInfos.TryGetValue(containerLookup.Component, out var layoutInfo)) return null; if (!layoutInfo.TryGetDrawableInfo(containerLookup.Ruleset, out var drawableInfos)) return null; return new UserConfiguredLayoutContainer diff --git a/osu.Game/Skinning/TrianglesSkin.cs b/osu.Game/Skinning/TrianglesSkin.cs index b0cb54a6f9..c0d327a082 100644 --- a/osu.Game/Skinning/TrianglesSkin.cs +++ b/osu.Game/Skinning/TrianglesSkin.cs @@ -74,7 +74,7 @@ namespace osu.Game.Skinning if (containerLookup.Ruleset != null) return null; - switch (containerLookup.Target) + switch (containerLookup.Component) { case GlobalSkinnableContainers.SongSelect: var songSelectComponents = new DefaultSkinComponentsContainer(_ => From 1435fe24ae8076baa494652439785ab318009ea3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 22 Aug 2024 17:14:35 +0900 Subject: [PATCH 2065/2493] Remove requirement of `base` calls to ensure user skin container layouts are retrieved --- .../Legacy/CatchLegacySkinTransformer.cs | 4 --- .../Legacy/ManiaLegacySkinTransformer.cs | 4 --- .../Legacy/OsuLegacySkinTransformer.cs | 4 --- osu.Game/Skinning/Skin.cs | 27 +++++++++++-------- osu.Game/Skinning/SkinnableContainer.cs | 5 +++- osu.Game/Skinning/UserSkinComponentLookup.cs | 18 +++++++++++++ 6 files changed, 38 insertions(+), 24 deletions(-) create mode 100644 osu.Game/Skinning/UserSkinComponentLookup.cs diff --git a/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs b/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs index a62a712001..e64dcd4e75 100644 --- a/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs @@ -35,10 +35,6 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy if (containerLookup.Ruleset == null) return base.GetDrawableComponent(lookup); - // Skin has configuration. - if (base.GetDrawableComponent(lookup) is UserConfiguredLayoutContainer d) - return d; - // we don't have enough assets to display these components (this is especially the case on a "beatmap" skin). if (!IsProvidingLegacyResources) return null; diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs index 895f5a7cc1..3372cb70db 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs @@ -85,10 +85,6 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy if (containerLookup.Ruleset == null) return base.GetDrawableComponent(lookup); - // Skin has configuration. - if (base.GetDrawableComponent(lookup) is UserConfiguredLayoutContainer d) - return d; - // we don't have enough assets to display these components (this is especially the case on a "beatmap" skin). if (!IsProvidingLegacyResources) return null; diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs index 26708f6686..afccdcc3ac 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs @@ -49,10 +49,6 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy if (containerLookup.Ruleset == null) return base.GetDrawableComponent(lookup); - // Skin has configuration. - if (base.GetDrawableComponent(lookup) is UserConfiguredLayoutContainer d) - return d; - // we don't have enough assets to display these components (this is especially the case on a "beatmap" skin). if (!IsProvidingLegacyResources) return null; diff --git a/osu.Game/Skinning/Skin.cs b/osu.Game/Skinning/Skin.cs index 04a7fd53f7..694aaf882a 100644 --- a/osu.Game/Skinning/Skin.cs +++ b/osu.Game/Skinning/Skin.cs @@ -187,18 +187,23 @@ namespace osu.Game.Skinning case SkinnableSprite.SpriteComponentLookup sprite: return this.GetAnimation(sprite.LookupName, false, false, maxSize: sprite.MaxSize); - case GlobalSkinnableContainerLookup containerLookup: - - // It is important to return null if the user has not configured this yet. - // This allows skin transformers the opportunity to provide default components. - if (!LayoutInfos.TryGetValue(containerLookup.Component, out var layoutInfo)) return null; - if (!layoutInfo.TryGetDrawableInfo(containerLookup.Ruleset, out var drawableInfos)) return null; - - return new UserConfiguredLayoutContainer + case UserSkinComponentLookup userLookup: + switch (userLookup.Component) { - RelativeSizeAxes = Axes.Both, - ChildrenEnumerable = drawableInfos.Select(i => i.CreateInstance()) - }; + case GlobalSkinnableContainerLookup containerLookup: + // It is important to return null if the user has not configured this yet. + // This allows skin transformers the opportunity to provide default components. + if (!LayoutInfos.TryGetValue(containerLookup.Component, out var layoutInfo)) return null; + if (!layoutInfo.TryGetDrawableInfo(containerLookup.Ruleset, out var drawableInfos)) return null; + + return new UserConfiguredLayoutContainer + { + RelativeSizeAxes = Axes.Both, + ChildrenEnumerable = drawableInfos.Select(i => i.CreateInstance()) + }; + } + + break; } return null; diff --git a/osu.Game/Skinning/SkinnableContainer.cs b/osu.Game/Skinning/SkinnableContainer.cs index c58992c541..aad95ca779 100644 --- a/osu.Game/Skinning/SkinnableContainer.cs +++ b/osu.Game/Skinning/SkinnableContainer.cs @@ -43,7 +43,10 @@ namespace osu.Game.Skinning Lookup = lookup; } - public void Reload() => Reload(CurrentSkin.GetDrawableComponent(Lookup) as Container); + public void Reload() => Reload(( + CurrentSkin.GetDrawableComponent(new UserSkinComponentLookup(Lookup)) + ?? CurrentSkin.GetDrawableComponent(Lookup)) + as Container); public void Reload(Container? componentsContainer) { diff --git a/osu.Game/Skinning/UserSkinComponentLookup.cs b/osu.Game/Skinning/UserSkinComponentLookup.cs new file mode 100644 index 0000000000..1ecdc96b38 --- /dev/null +++ b/osu.Game/Skinning/UserSkinComponentLookup.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. + +namespace osu.Game.Skinning +{ + /// + /// A lookup class which is only for internal use, and explicitly to get a user-level configuration. + /// + internal class UserSkinComponentLookup : ISkinComponentLookup + { + public readonly ISkinComponentLookup Component; + + public UserSkinComponentLookup(ISkinComponentLookup component) + { + Component = component; + } + } +} From 58552e97680175ca74e2e7c2f0b0fcad2a711ecb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 22 Aug 2024 19:18:41 +0900 Subject: [PATCH 2066/2493] Add missing user ruleset to link copying for beatmap panels --- osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs index 66d1480fdc..359e0f6c78 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs @@ -295,7 +295,7 @@ namespace osu.Game.Screens.Select.Carousel items.Add(new OsuMenuItem("Collections") { Items = collectionItems }); - if (beatmapInfo.GetOnlineURL(api) is string url) + if (beatmapInfo.GetOnlineURL(api, ruleset.Value) is string url) items.Add(new OsuMenuItem("Copy link", MenuItemType.Standard, () => game?.CopyUrlToClipboard(url))); if (hideRequested != null) From f1adc6f98cc48024a0bb35811955fb5653c3fdf6 Mon Sep 17 00:00:00 2001 From: StanR Date: Thu, 22 Aug 2024 15:59:13 +0500 Subject: [PATCH 2067/2493] Don't cap max island size, make repetition nerf more lenient on high bpm, adjust balancing --- .../Difficulty/Evaluators/RhythmEvaluator.cs | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs index d2f58925cd..9ffb4fc3d7 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs @@ -60,8 +60,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators } private const int history_time_max = 5 * 1000; // 5 seconds of calculatingRhythmBonus max. - private const double rhythm_multiplier = 1.1; - private const int max_island_size = 7; + private const double rhythm_multiplier = 1.05; /// /// Calculates a rhythm multiplier for the difficulty of the tap associated with historic data of the current . @@ -103,7 +102,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators double prevDelta = prevObj.StrainTime; double lastDelta = lastObj.StrainTime; - double currRatio = 1.0 + 8.4 * Math.Min(0.5, Math.Pow(Math.Sin(Math.PI / (Math.Min(prevDelta, currDelta) / Math.Max(prevDelta, currDelta))), 2)); // fancy function to calculate rhythmbonuses. + double currRatio = 1.0 + 8.8 * Math.Min(0.5, Math.Pow(Math.Sin(Math.PI / (Math.Min(prevDelta, currDelta) / Math.Max(prevDelta, currDelta))), 2)); // fancy function to calculate rhythmbonuses. double windowPenalty = Math.Min(1, Math.Max(0, Math.Abs(prevDelta - currDelta) - currObj.HitWindowGreat * 0.3) / (currObj.HitWindowGreat * 0.3)); @@ -117,11 +116,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators { if (!(Math.Abs(prevDelta - currDelta) > deltaDifferenceEpsilon)) { - if (island.Deltas.Count < max_island_size) - { - // island is still progressing - island.AddDelta((int)currDelta, deltaDifferenceEpsilon); - } + // island is still progressing + island.AddDelta((int)currDelta, deltaDifferenceEpsilon); } else { @@ -143,7 +139,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators // repeated island (ex: triplet -> triplet) double power = logistic(island.AverageDelta(), 4, 0.165, 10); - effectiveRatio *= Math.Min(1.0 / islandCounts[island], Math.Pow(1.0 / islandCounts[island], power)); + effectiveRatio *= Math.Min(2.0 / islandCounts[island], Math.Pow(1.0 / islandCounts[island], power)); } rhythmComplexitySum += Math.Sqrt(effectiveRatio * startRatio) * currHistoricalDecay; From 46d55d5e61356b8e7a1771e9e9c72fb014713324 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 22 Aug 2024 20:13:24 +0900 Subject: [PATCH 2068/2493] Remove remaining early `base` lookup calls which were missed --- .../Skinning/Argon/ManiaArgonSkinTransformer.cs | 4 ---- osu.Game/Skinning/ArgonSkin.cs | 4 ---- osu.Game/Skinning/LegacySkin.cs | 3 --- osu.Game/Skinning/Skin.cs | 3 ++- osu.Game/Skinning/TrianglesSkin.cs | 3 --- .../Skinning/UserConfiguredLayoutContainer.cs | 15 --------------- 6 files changed, 2 insertions(+), 30 deletions(-) delete mode 100644 osu.Game/Skinning/UserConfiguredLayoutContainer.cs diff --git a/osu.Game.Rulesets.Mania/Skinning/Argon/ManiaArgonSkinTransformer.cs b/osu.Game.Rulesets.Mania/Skinning/Argon/ManiaArgonSkinTransformer.cs index 2c361df8b1..8707246402 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Argon/ManiaArgonSkinTransformer.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Argon/ManiaArgonSkinTransformer.cs @@ -33,10 +33,6 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon if (containerLookup.Ruleset == null) return base.GetDrawableComponent(lookup); - // Skin has configuration. - if (base.GetDrawableComponent(lookup) is UserConfiguredLayoutContainer d) - return d; - switch (containerLookup.Component) { case GlobalSkinnableContainers.MainHUDComponents: diff --git a/osu.Game/Skinning/ArgonSkin.cs b/osu.Game/Skinning/ArgonSkin.cs index 2489013c1e..6baba02d29 100644 --- a/osu.Game/Skinning/ArgonSkin.cs +++ b/osu.Game/Skinning/ArgonSkin.cs @@ -97,10 +97,6 @@ namespace osu.Game.Skinning switch (lookup) { case GlobalSkinnableContainerLookup containerLookup: - - if (base.GetDrawableComponent(lookup) is UserConfiguredLayoutContainer c) - return c; - switch (containerLookup.Component) { case GlobalSkinnableContainers.SongSelect: diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index 16d9cf391c..8706f24e61 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -359,9 +359,6 @@ namespace osu.Game.Skinning switch (lookup) { case GlobalSkinnableContainerLookup containerLookup: - if (base.GetDrawableComponent(lookup) is UserConfiguredLayoutContainer c) - return c; - switch (containerLookup.Component) { case GlobalSkinnableContainers.MainHUDComponents: diff --git a/osu.Game/Skinning/Skin.cs b/osu.Game/Skinning/Skin.cs index 694aaf882a..4c7dda50a9 100644 --- a/osu.Game/Skinning/Skin.cs +++ b/osu.Game/Skinning/Skin.cs @@ -14,6 +14,7 @@ using osu.Framework.Audio.Sample; using osu.Framework.Bindables; using osu.Framework.Extensions.TypeExtensions; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Textures; using osu.Framework.IO.Stores; using osu.Framework.Logging; @@ -196,7 +197,7 @@ namespace osu.Game.Skinning if (!LayoutInfos.TryGetValue(containerLookup.Component, out var layoutInfo)) return null; if (!layoutInfo.TryGetDrawableInfo(containerLookup.Ruleset, out var drawableInfos)) return null; - return new UserConfiguredLayoutContainer + return new Container { RelativeSizeAxes = Axes.Both, ChildrenEnumerable = drawableInfos.Select(i => i.CreateInstance()) diff --git a/osu.Game/Skinning/TrianglesSkin.cs b/osu.Game/Skinning/TrianglesSkin.cs index c0d327a082..ca0653ee12 100644 --- a/osu.Game/Skinning/TrianglesSkin.cs +++ b/osu.Game/Skinning/TrianglesSkin.cs @@ -67,9 +67,6 @@ namespace osu.Game.Skinning switch (lookup) { case GlobalSkinnableContainerLookup containerLookup: - if (base.GetDrawableComponent(lookup) is UserConfiguredLayoutContainer c) - return c; - // Only handle global level defaults for now. if (containerLookup.Ruleset != null) return null; diff --git a/osu.Game/Skinning/UserConfiguredLayoutContainer.cs b/osu.Game/Skinning/UserConfiguredLayoutContainer.cs deleted file mode 100644 index 1b5a27b53b..0000000000 --- a/osu.Game/Skinning/UserConfiguredLayoutContainer.cs +++ /dev/null @@ -1,15 +0,0 @@ -// 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.Graphics.Containers; - -namespace osu.Game.Skinning -{ - /// - /// This signifies that a call resolved a configuration created - /// by a user in their skin. Generally this should be given priority over any local defaults or overrides. - /// - public partial class UserConfiguredLayoutContainer : Container - { - } -} From 0db068e423024d35bb4e2145d134e8f7dc7e2988 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Thu, 22 Aug 2024 19:15:53 +0200 Subject: [PATCH 2069/2493] allow repeating on seek actions --- osu.Game/Screens/Edit/Editor.cs | 41 ++++++++++++++++++--------------- 1 file changed, 22 insertions(+), 19 deletions(-) diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 2933c89cd8..355d724434 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -714,6 +714,26 @@ namespace osu.Game.Screens.Edit public bool OnPressed(KeyBindingPressEvent e) { + // Repeatable actions + switch (e.Action) + { + case GlobalAction.EditorSeekToPreviousHitObject: + seekHitObject(-1); + return true; + + case GlobalAction.EditorSeekToNextHitObject: + seekHitObject(1); + return true; + + case GlobalAction.EditorSeekToPreviousSamplePoint: + seekSamplePoint(-1); + return true; + + case GlobalAction.EditorSeekToNextSamplePoint: + seekSamplePoint(1); + return true; + } + if (e.Repeat) return false; @@ -751,26 +771,9 @@ namespace osu.Game.Screens.Edit case GlobalAction.EditorTestGameplay: bottomBar.TestGameplayButton.TriggerClick(); return true; - - case GlobalAction.EditorSeekToPreviousHitObject: - seekHitObject(-1); - return true; - - case GlobalAction.EditorSeekToNextHitObject: - seekHitObject(1); - return true; - - case GlobalAction.EditorSeekToPreviousSamplePoint: - seekSamplePoint(-1); - return true; - - case GlobalAction.EditorSeekToNextSamplePoint: - seekSamplePoint(1); - return true; - - default: - return false; } + + return false; } public void OnReleased(KeyBindingReleaseEvent e) From adbdb39e9f57ce6e548ddba58798f797e9430de6 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Thu, 22 Aug 2024 19:18:38 +0200 Subject: [PATCH 2070/2493] move public member to top of file --- osu.Game/Screens/Edit/Editor.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 355d724434..6b8ea7e97e 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -225,6 +225,9 @@ namespace osu.Game.Screens.Edit /// public Bindable ComposerFocusMode { get; } = new Bindable(); + [CanBeNull] + public event Action ShowSampleEditPopoverRequested; + public Editor(EditorLoader loader = null) { this.loader = loader; @@ -1107,9 +1110,6 @@ namespace osu.Game.Screens.Edit clock.SeekSmoothlyTo(found.StartTime); } - [CanBeNull] - public event Action ShowSampleEditPopoverRequested; - private void seekSamplePoint(int direction) { double currentTime = clock.CurrentTimeAccurate; From 998b5fdc12122a538dadbd3b7afcda868eb3bdda Mon Sep 17 00:00:00 2001 From: OliBomby Date: Thu, 22 Aug 2024 19:53:34 +0200 Subject: [PATCH 2071/2493] Add property EditorShowScrollSpeed to Ruleset --- osu.Game.Rulesets.Catch/CatchRuleset.cs | 2 ++ osu.Game.Rulesets.Osu/OsuRuleset.cs | 2 ++ osu.Game/Rulesets/Ruleset.cs | 5 +++++ osu.Game/Rulesets/UI/Scrolling/DrawableScrollingRuleset.cs | 2 -- osu.Game/Rulesets/UI/Scrolling/IDrawableScrollingRuleset.cs | 4 ---- .../Timelines/Summary/Parts/EffectPointVisualisation.cs | 6 +----- osu.Game/Screens/Edit/Timing/EffectSection.cs | 5 +---- .../Screens/Edit/Timing/RowAttributes/EffectRowAttribute.cs | 6 +----- 8 files changed, 12 insertions(+), 20 deletions(-) diff --git a/osu.Game.Rulesets.Catch/CatchRuleset.cs b/osu.Game.Rulesets.Catch/CatchRuleset.cs index 3edc23a8b7..7eaf4f2b18 100644 --- a/osu.Game.Rulesets.Catch/CatchRuleset.cs +++ b/osu.Game.Rulesets.Catch/CatchRuleset.cs @@ -254,5 +254,7 @@ namespace osu.Game.Rulesets.Catch return adjustedDifficulty; } + + public override bool EditorShowScrollSpeed => false; } } diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index 7042ad0cd4..be48ef9acc 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -359,5 +359,7 @@ namespace osu.Game.Rulesets.Osu return adjustedDifficulty; } + + public override bool EditorShowScrollSpeed => false; } } diff --git a/osu.Game/Rulesets/Ruleset.cs b/osu.Game/Rulesets/Ruleset.cs index fb0e225c94..5af1fd386c 100644 --- a/osu.Game/Rulesets/Ruleset.cs +++ b/osu.Game/Rulesets/Ruleset.cs @@ -401,5 +401,10 @@ namespace osu.Game.Rulesets new DifficultySection(), new ColoursSection(), ]; + + /// + /// Can be overridden to avoid showing scroll speed changes in the editor. + /// + public virtual bool EditorShowScrollSpeed => true; } } diff --git a/osu.Game/Rulesets/UI/Scrolling/DrawableScrollingRuleset.cs b/osu.Game/Rulesets/UI/Scrolling/DrawableScrollingRuleset.cs index d23658ac33..ba3a9bd483 100644 --- a/osu.Game/Rulesets/UI/Scrolling/DrawableScrollingRuleset.cs +++ b/osu.Game/Rulesets/UI/Scrolling/DrawableScrollingRuleset.cs @@ -64,8 +64,6 @@ namespace osu.Game.Rulesets.UI.Scrolling MaxValue = time_span_max }; - ScrollVisualisationMethod IDrawableScrollingRuleset.VisualisationMethod => VisualisationMethod; - /// /// Whether the player can change . /// diff --git a/osu.Game/Rulesets/UI/Scrolling/IDrawableScrollingRuleset.cs b/osu.Game/Rulesets/UI/Scrolling/IDrawableScrollingRuleset.cs index b348a22009..27531492d6 100644 --- a/osu.Game/Rulesets/UI/Scrolling/IDrawableScrollingRuleset.cs +++ b/osu.Game/Rulesets/UI/Scrolling/IDrawableScrollingRuleset.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. -using osu.Game.Configuration; - namespace osu.Game.Rulesets.UI.Scrolling { /// @@ -10,8 +8,6 @@ namespace osu.Game.Rulesets.UI.Scrolling /// public interface IDrawableScrollingRuleset { - ScrollVisualisationMethod VisualisationMethod { get; } - IScrollingInfo ScrollingInfo { get; } } } diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/EffectPointVisualisation.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/EffectPointVisualisation.cs index e3f90558c5..f1a8dc5e35 100644 --- a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/EffectPointVisualisation.cs +++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/EffectPointVisualisation.cs @@ -9,10 +9,8 @@ using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Shapes; using osu.Framework.Localisation; using osu.Game.Beatmaps.ControlPoints; -using osu.Game.Configuration; using osu.Game.Extensions; using osu.Game.Graphics; -using osu.Game.Rulesets.UI.Scrolling; namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts { @@ -81,9 +79,7 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts { ClearInternal(); - var drawableRuleset = beatmap.BeatmapInfo.Ruleset.CreateInstance().CreateDrawableRulesetWith(beatmap.PlayableBeatmap); - - if (drawableRuleset is IDrawableScrollingRuleset scrollingRuleset && scrollingRuleset.VisualisationMethod != ScrollVisualisationMethod.Constant) + if (beatmap.BeatmapInfo.Ruleset.CreateInstance().EditorShowScrollSpeed) AddInternal(new ControlPointVisualisation(effect)); if (!kiai.Value) diff --git a/osu.Game/Screens/Edit/Timing/EffectSection.cs b/osu.Game/Screens/Edit/Timing/EffectSection.cs index f321f7eeb0..a4b9f37dff 100644 --- a/osu.Game/Screens/Edit/Timing/EffectSection.cs +++ b/osu.Game/Screens/Edit/Timing/EffectSection.cs @@ -5,9 +5,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Beatmaps.ControlPoints; -using osu.Game.Configuration; using osu.Game.Graphics.UserInterfaceV2; -using osu.Game.Rulesets.UI.Scrolling; namespace osu.Game.Screens.Edit.Timing { @@ -38,8 +36,7 @@ namespace osu.Game.Screens.Edit.Timing kiai.Current.BindValueChanged(_ => saveChanges()); scrollSpeedSlider.Current.BindValueChanged(_ => saveChanges()); - var drawableRuleset = Beatmap.BeatmapInfo.Ruleset.CreateInstance().CreateDrawableRulesetWith(Beatmap.PlayableBeatmap); - if (drawableRuleset is not IDrawableScrollingRuleset scrollingRuleset || scrollingRuleset.VisualisationMethod == ScrollVisualisationMethod.Constant) + if (!Beatmap.BeatmapInfo.Ruleset.CreateInstance().EditorShowScrollSpeed) scrollSpeedSlider.Hide(); void saveChanges() diff --git a/osu.Game/Screens/Edit/Timing/RowAttributes/EffectRowAttribute.cs b/osu.Game/Screens/Edit/Timing/RowAttributes/EffectRowAttribute.cs index 253bfdd73a..87ee675e7f 100644 --- a/osu.Game/Screens/Edit/Timing/RowAttributes/EffectRowAttribute.cs +++ b/osu.Game/Screens/Edit/Timing/RowAttributes/EffectRowAttribute.cs @@ -5,8 +5,6 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Beatmaps.ControlPoints; -using osu.Game.Configuration; -using osu.Game.Rulesets.UI.Scrolling; namespace osu.Game.Screens.Edit.Timing.RowAttributes { @@ -42,9 +40,7 @@ namespace osu.Game.Screens.Edit.Timing.RowAttributes kiaiModeBubble = new AttributeText(Point) { Text = "kiai" }, }); - var drawableRuleset = Beatmap.BeatmapInfo.Ruleset.CreateInstance().CreateDrawableRulesetWith(Beatmap.PlayableBeatmap); - - if (drawableRuleset is not IDrawableScrollingRuleset scrollingRuleset || scrollingRuleset.VisualisationMethod == ScrollVisualisationMethod.Constant) + if (!Beatmap.BeatmapInfo.Ruleset.CreateInstance().EditorShowScrollSpeed) { text.Hide(); progressBar.Hide(); From ad8e7f1897fbae3c76e8438bd2263217f67819fb Mon Sep 17 00:00:00 2001 From: OliBomby Date: Thu, 22 Aug 2024 20:17:09 +0200 Subject: [PATCH 2072/2493] Fix scroll speed visualisation missing on start kiai effect points They were being drawn far offscreen because the width of the kiai multiplied with the X coordinate of the scroll speed vis. --- .../Timelines/Summary/Parts/EffectPointVisualisation.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/EffectPointVisualisation.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/EffectPointVisualisation.cs index f1a8dc5e35..b4e6d1ece2 100644 --- a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/EffectPointVisualisation.cs +++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/EffectPointVisualisation.cs @@ -80,7 +80,13 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts ClearInternal(); if (beatmap.BeatmapInfo.Ruleset.CreateInstance().EditorShowScrollSpeed) - AddInternal(new ControlPointVisualisation(effect)); + { + AddInternal(new ControlPointVisualisation(effect) + { + // importantly, override the x position being set since we do that in the GroupVisualisation parent drawable. + X = 0, + }); + } if (!kiai.Value) return; From 48cfd77ee8d0ef359db019855ce6653103b23cef Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 23 Aug 2024 14:48:50 +0900 Subject: [PATCH 2073/2493] `Component` -> `Lookup` --- .../Skinning/Legacy/CatchLegacySkinTransformer.cs | 2 +- .../Skinning/Argon/ManiaArgonSkinTransformer.cs | 2 +- .../Skinning/Legacy/ManiaLegacySkinTransformer.cs | 2 +- .../Skinning/Legacy/OsuLegacySkinTransformer.cs | 2 +- .../Gameplay/TestSceneBeatmapSkinFallbacks.cs | 2 +- .../Visual/Gameplay/TestSceneSkinEditor.cs | 4 ++-- osu.Game/Skinning/ArgonSkin.cs | 2 +- .../Skinning/GlobalSkinnableContainerLookup.cs | 14 +++++++------- osu.Game/Skinning/LegacyBeatmapSkin.cs | 2 +- osu.Game/Skinning/LegacySkin.cs | 2 +- osu.Game/Skinning/Skin.cs | 8 ++++---- osu.Game/Skinning/TrianglesSkin.cs | 2 +- 12 files changed, 22 insertions(+), 22 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs b/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs index e64dcd4e75..69efb7fbca 100644 --- a/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs @@ -40,7 +40,7 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy return null; // Our own ruleset components default. - switch (containerLookup.Component) + switch (containerLookup.Lookup) { case GlobalSkinnableContainers.MainHUDComponents: // todo: remove CatchSkinComponents.CatchComboCounter and refactor LegacyCatchComboCounter to be added here instead. diff --git a/osu.Game.Rulesets.Mania/Skinning/Argon/ManiaArgonSkinTransformer.cs b/osu.Game.Rulesets.Mania/Skinning/Argon/ManiaArgonSkinTransformer.cs index 8707246402..afccb2e568 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Argon/ManiaArgonSkinTransformer.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Argon/ManiaArgonSkinTransformer.cs @@ -33,7 +33,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon if (containerLookup.Ruleset == null) return base.GetDrawableComponent(lookup); - switch (containerLookup.Component) + switch (containerLookup.Lookup) { case GlobalSkinnableContainers.MainHUDComponents: return new DefaultSkinComponentsContainer(container => diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs index 3372cb70db..cb42b2b62a 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs @@ -89,7 +89,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy if (!IsProvidingLegacyResources) return null; - switch (containerLookup.Component) + switch (containerLookup.Lookup) { case GlobalSkinnableContainers.MainHUDComponents: return new DefaultSkinComponentsContainer(container => diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs index afccdcc3ac..636a9ecb21 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs @@ -54,7 +54,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy return null; // Our own ruleset components default. - switch (containerLookup.Component) + switch (containerLookup.Lookup) { case GlobalSkinnableContainers.MainHUDComponents: return new DefaultSkinComponentsContainer(container => diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs index 9a4f084d10..5ec32f318c 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs @@ -55,7 +55,7 @@ namespace osu.Game.Tests.Visual.Gameplay protected bool AssertComponentsFromExpectedSource(GlobalSkinnableContainers target, ISkin expectedSource) { - var targetContainer = Player.ChildrenOfType().First(s => s.Lookup.Component == target); + var targetContainer = Player.ChildrenOfType().First(s => s.Lookup.Lookup == target); var actualComponentsContainer = targetContainer.ChildrenOfType().SingleOrDefault(c => c.Parent == targetContainer); if (actualComponentsContainer == null) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs index 4dca8c9001..3a7bc05300 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs @@ -378,10 +378,10 @@ namespace osu.Game.Tests.Visual.Gameplay } private SkinnableContainer globalHUDTarget => Player.ChildrenOfType() - .Single(c => c.Lookup.Component == GlobalSkinnableContainers.MainHUDComponents && c.Lookup.Ruleset == null); + .Single(c => c.Lookup.Lookup == GlobalSkinnableContainers.MainHUDComponents && c.Lookup.Ruleset == null); private SkinnableContainer rulesetHUDTarget => Player.ChildrenOfType() - .Single(c => c.Lookup.Component == GlobalSkinnableContainers.MainHUDComponents && c.Lookup.Ruleset != null); + .Single(c => c.Lookup.Lookup == GlobalSkinnableContainers.MainHUDComponents && c.Lookup.Ruleset != null); [Test] public void TestMigrationArgon() diff --git a/osu.Game/Skinning/ArgonSkin.cs b/osu.Game/Skinning/ArgonSkin.cs index 6baba02d29..771d10d73b 100644 --- a/osu.Game/Skinning/ArgonSkin.cs +++ b/osu.Game/Skinning/ArgonSkin.cs @@ -97,7 +97,7 @@ namespace osu.Game.Skinning switch (lookup) { case GlobalSkinnableContainerLookup containerLookup: - switch (containerLookup.Component) + switch (containerLookup.Lookup) { case GlobalSkinnableContainers.SongSelect: var songSelectComponents = new DefaultSkinComponentsContainer(_ => diff --git a/osu.Game/Skinning/GlobalSkinnableContainerLookup.cs b/osu.Game/Skinning/GlobalSkinnableContainerLookup.cs index 524d99197a..6d78981f0a 100644 --- a/osu.Game/Skinning/GlobalSkinnableContainerLookup.cs +++ b/osu.Game/Skinning/GlobalSkinnableContainerLookup.cs @@ -15,7 +15,7 @@ namespace osu.Game.Skinning /// /// The target area / layer of the game for which skin components will be returned. /// - public readonly GlobalSkinnableContainers Component; + public readonly GlobalSkinnableContainers Lookup; /// /// The ruleset for which skin components should be returned. @@ -23,17 +23,17 @@ namespace osu.Game.Skinning /// public readonly RulesetInfo? Ruleset; - public GlobalSkinnableContainerLookup(GlobalSkinnableContainers component, RulesetInfo? ruleset = null) + public GlobalSkinnableContainerLookup(GlobalSkinnableContainers lookup, RulesetInfo? ruleset = null) { - Component = component; + Lookup = lookup; Ruleset = ruleset; } public override string ToString() { - if (Ruleset == null) return Component.GetDescription(); + if (Ruleset == null) return Lookup.GetDescription(); - return $"{Component.GetDescription()} (\"{Ruleset.Name}\" only)"; + return $"{Lookup.GetDescription()} (\"{Ruleset.Name}\" only)"; } public bool Equals(GlobalSkinnableContainerLookup? other) @@ -41,7 +41,7 @@ namespace osu.Game.Skinning if (ReferenceEquals(null, other)) return false; if (ReferenceEquals(this, other)) return true; - return Component == other.Component && (ReferenceEquals(Ruleset, other.Ruleset) || Ruleset?.Equals(other.Ruleset) == true); + return Lookup == other.Lookup && (ReferenceEquals(Ruleset, other.Ruleset) || Ruleset?.Equals(other.Ruleset) == true); } public override bool Equals(object? obj) @@ -55,7 +55,7 @@ namespace osu.Game.Skinning public override int GetHashCode() { - return HashCode.Combine((int)Component, Ruleset); + return HashCode.Combine((int)Lookup, Ruleset); } } } diff --git a/osu.Game/Skinning/LegacyBeatmapSkin.cs b/osu.Game/Skinning/LegacyBeatmapSkin.cs index c8a93f418f..656c0e046f 100644 --- a/osu.Game/Skinning/LegacyBeatmapSkin.cs +++ b/osu.Game/Skinning/LegacyBeatmapSkin.cs @@ -52,7 +52,7 @@ namespace osu.Game.Skinning { if (lookup is GlobalSkinnableContainerLookup containerLookup) { - switch (containerLookup.Component) + switch (containerLookup.Lookup) { case GlobalSkinnableContainers.MainHUDComponents: // this should exist in LegacySkin instead, but there isn't a fallback skin for LegacySkins yet. diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index 8706f24e61..6faadfba9b 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -359,7 +359,7 @@ namespace osu.Game.Skinning switch (lookup) { case GlobalSkinnableContainerLookup containerLookup: - switch (containerLookup.Component) + switch (containerLookup.Lookup) { case GlobalSkinnableContainers.MainHUDComponents: if (containerLookup.Ruleset != null) diff --git a/osu.Game/Skinning/Skin.cs b/osu.Game/Skinning/Skin.cs index 4c7dda50a9..2382253036 100644 --- a/osu.Game/Skinning/Skin.cs +++ b/osu.Game/Skinning/Skin.cs @@ -165,7 +165,7 @@ namespace osu.Game.Skinning /// The target container to reset. public void ResetDrawableTarget(SkinnableContainer targetContainer) { - LayoutInfos.Remove(targetContainer.Lookup.Component); + LayoutInfos.Remove(targetContainer.Lookup.Lookup); } /// @@ -174,8 +174,8 @@ namespace osu.Game.Skinning /// The target container to serialise to this skin. public void UpdateDrawableTarget(SkinnableContainer targetContainer) { - if (!LayoutInfos.TryGetValue(targetContainer.Lookup.Component, out var layoutInfo)) - layoutInfos[targetContainer.Lookup.Component] = layoutInfo = new SkinLayoutInfo(); + if (!LayoutInfos.TryGetValue(targetContainer.Lookup.Lookup, out var layoutInfo)) + layoutInfos[targetContainer.Lookup.Lookup] = layoutInfo = new SkinLayoutInfo(); layoutInfo.Update(targetContainer.Lookup.Ruleset, ((ISerialisableDrawableContainer)targetContainer).CreateSerialisedInfo().ToArray()); } @@ -194,7 +194,7 @@ namespace osu.Game.Skinning case GlobalSkinnableContainerLookup containerLookup: // It is important to return null if the user has not configured this yet. // This allows skin transformers the opportunity to provide default components. - if (!LayoutInfos.TryGetValue(containerLookup.Component, out var layoutInfo)) return null; + if (!LayoutInfos.TryGetValue(containerLookup.Lookup, out var layoutInfo)) return null; if (!layoutInfo.TryGetDrawableInfo(containerLookup.Ruleset, out var drawableInfos)) return null; return new Container diff --git a/osu.Game/Skinning/TrianglesSkin.cs b/osu.Game/Skinning/TrianglesSkin.cs index ca0653ee12..d562fd3256 100644 --- a/osu.Game/Skinning/TrianglesSkin.cs +++ b/osu.Game/Skinning/TrianglesSkin.cs @@ -71,7 +71,7 @@ namespace osu.Game.Skinning if (containerLookup.Ruleset != null) return null; - switch (containerLookup.Component) + switch (containerLookup.Lookup) { case GlobalSkinnableContainers.SongSelect: var songSelectComponents = new DefaultSkinComponentsContainer(_ => From 2a479a84dce4d8d3840c8645118a82ac76f1be7d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 23 Aug 2024 18:21:31 +0900 Subject: [PATCH 2074/2493] Remove conditional inhibiting seek when beatmap change is not allowed In testing I can't find a reason for this to exist. Blaming back shows that it existed before we had `AllowTrackControl` and was likely being used as a stop-gap measure to achieve the same thing. It's existed since over 6 years ago. Let's give removing it a try to fix some usability concerns? Closes https://github.com/ppy/osu/issues/29563. --- osu.Game/Overlays/MusicController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/MusicController.cs b/osu.Game/Overlays/MusicController.cs index 27c7cd0f49..63efdd5381 100644 --- a/osu.Game/Overlays/MusicController.cs +++ b/osu.Game/Overlays/MusicController.cs @@ -115,7 +115,7 @@ namespace osu.Game.Overlays seekDelegate?.Cancel(); seekDelegate = Schedule(() => { - if (beatmap.Disabled || !AllowTrackControl.Value) + if (!AllowTrackControl.Value) return; CurrentTrack.Seek(position); From 1e39af8ac5f07c9992faf062572390d85ea7e981 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 23 Aug 2024 19:49:15 +0900 Subject: [PATCH 2075/2493] Add a bit of logging around medal awarding Might help with https://github.com/ppy/osu/issues/29119. --- osu.Game/Overlays/MedalAnimation.cs | 7 ++++--- osu.Game/Overlays/MedalOverlay.cs | 6 ++++++ 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/osu.Game/Overlays/MedalAnimation.cs b/osu.Game/Overlays/MedalAnimation.cs index 25776d50db..daceeedf47 100644 --- a/osu.Game/Overlays/MedalAnimation.cs +++ b/osu.Game/Overlays/MedalAnimation.cs @@ -30,7 +30,8 @@ namespace osu.Game.Overlays private const float border_width = 5; - private readonly Medal medal; + public readonly Medal Medal; + private readonly Box background; private readonly Container backgroundStrip, particleContainer; private readonly BackgroundStrip leftStrip, rightStrip; @@ -44,7 +45,7 @@ namespace osu.Game.Overlays public MedalAnimation(Medal medal) { - this.medal = medal; + Medal = medal; RelativeSizeAxes = Axes.Both; Child = content = new Container @@ -168,7 +169,7 @@ namespace osu.Game.Overlays { base.LoadComplete(); - LoadComponentAsync(drawableMedal = new DrawableMedal(medal) + LoadComponentAsync(drawableMedal = new DrawableMedal(Medal) { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, diff --git a/osu.Game/Overlays/MedalOverlay.cs b/osu.Game/Overlays/MedalOverlay.cs index 072d7db6c7..19f61cb910 100644 --- a/osu.Game/Overlays/MedalOverlay.cs +++ b/osu.Game/Overlays/MedalOverlay.cs @@ -8,6 +8,7 @@ using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Input.Events; +using osu.Framework.Logging; using osu.Game.Graphics.Containers; using osu.Game.Input.Bindings; using osu.Game.Online.API; @@ -81,7 +82,10 @@ namespace osu.Game.Overlays }; var medalAnimation = new MedalAnimation(medal); + queuedMedals.Enqueue(medalAnimation); + Logger.Log($"Queueing medal unlock for \"{medal.Name}\" ({queuedMedals.Count} to display)"); + if (OverlayActivationMode.Value == OverlayActivation.All) Scheduler.AddOnce(Show); } @@ -95,10 +99,12 @@ namespace osu.Game.Overlays if (!queuedMedals.TryDequeue(out lastAnimation)) { + Logger.Log("All queued medals have been displayed!"); Hide(); return; } + Logger.Log($"Preparing to display \"{lastAnimation.Medal.Name}\""); LoadComponentAsync(lastAnimation, medalContainer.Add); } From 3943fe96f4fe7084235c09fe1772bd6d23a9ac62 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 23 Aug 2024 20:44:35 +0900 Subject: [PATCH 2076/2493] Add failing test showing deserialise failing with some skins --- .../Archives/argon-invalid-drawable.osk | Bin 0 -> 2403 bytes osu.Game.Tests/Skins/SkinDeserialisationTest.cs | 13 +++++++++++++ 2 files changed, 13 insertions(+) create mode 100644 osu.Game.Tests/Resources/Archives/argon-invalid-drawable.osk diff --git a/osu.Game.Tests/Resources/Archives/argon-invalid-drawable.osk b/osu.Game.Tests/Resources/Archives/argon-invalid-drawable.osk new file mode 100644 index 0000000000000000000000000000000000000000..23c318149cbe3840c4219611ea22cdebf8a60a17 GIT binary patch literal 2403 zcmbtVdo)|=7C$1Xv}l8P6<1u()GM?`sX@Jpgho8$F`m(hM|9Pzbf_5G($b>TGgJp5 zwB^PSW)vOuhzjlKjCl2`*X>+d3DZ)>J?R~*R>vRiy8Byao$st~o&DQ;|MqWxc&soK z3BbVx{Z5t${An5r03ZXP0H;7Q#opbA_6Tc7V zR*z!Z3|tMmRjzue1d88NJ|ZvP(LIC1r6}_X8xvot#QEFTF-z9+?yoB7p76uAPq@i? zN3yD$Uo3y&yM@TC_a!YejzGFm`j*<9E!8RgMSENBa!k|ECCkV5ffoAW}Xf>9;jTJD=m!tQMmCL0}_OQr%?!M0Nb{Es6m3#Y_E23 zR-&9MJLuhg5JtF*zcJFCWKQbtk(tYp)FXjdQaqH2?qC5<)3 zsyE_UowF-5ZfxxM)83x3drQoEtMems<-!~Gb|(tRFDUY`SuJ>wT32Q6&~v7LfcRJo zKcpD9)^H|JOkSH<_RzvRF5@R_`H(fCUPw)v$gr%NEKN4e%h2=nO+Q3v!PwZryUi`x zM0w=DI*gE`H+ssxCnwMrmK7tPe~Hm;dBs|n-awNE`q8e=cQ={;o|q>+@=H#1D?=UH zJW#Ck*%FE}&kGCF1GwS`s7-&zGT;l4c(n;IX-=PS=lrnyR-656}Ewg<1~vbU%5Snn4yYNkEe?)~!93U9*Vn=SBI5je4;P6I6r z00PP3_Y`9is4EpvjQ;*kr>)Prdg<%=??ac?f7NAhfOPP=fS`c4PB@RZM?9|KQy{nG zgf$Tg1{h4DFd0?eSU}Z!DJLlj4W!1J3C$FJ?O0eb0}&G=^sloLGhi^2+C-C#9`&l8 zS~NOCy>>vT3*YRY(wax@QoO5Y+^i-+*BCiA{!5Rh0oq7&s&AsTj?OgHuKIi9gxOP_ znq%xix>n22^lm7f`0%~W(LU5Yr||cuS8IG4Aeu9bY%VdG!H7$ej9=_5vP-`YS<@am zZs9zwy0N{H{Sq~Dd3t_RN{|1QXQPAe+wDoCxYOp*7R_GC%kNkbTeZOy+33Eo`3sys z?p&RW)wrN+==p|a?9`vhuc>;%uiY#uWaw(VVf_-=lsIDEks|7n>U?SAkWGD>Rre)Q zL**a8nl(K2&u_DC(vkvc5P{P?@b|UA!V8N3iI(PoDX*Owi3T@^L0a$;feE2xQbfq7 z*?5JW5-JMT|IlAKV9XQn_zP%9owK+BhqXa0+p~#4KaeS8%fF7LmM(gs9uzQSK1QIJ zAC#y8VhC{dj-V4ffrAl5hm$FKWD5D?TozypaQDDgztMQ8+Y$l$@FBCt9cmycs#2C; zme0M`qtRk;HNO1<3&D$&NzQ!~kUH5Z#!tG;Q(Pz*p}#XdnNZ>3pmR_-w!)lTF&oB9 zh7#E_qwu8-ScLMzMEad4Ogr02HTU6@94l%L$CWKXl_EMa4O&!1&+y`-i!*r^-h^-H zIyutgm0l{MVScZEb#C=3vM%?|QrF`lm45OyiF2;9X&ft27F1sDyi{N%n3*8Ez4N{S zo{0aMH<=O|wg1BNu!BzSO$7XazMi|=H5W%6M-Jz@3t<>zh%tQTHr)60Y(I{Ta}qMl zGRlv!G%dYOkizn=TNYdKoA^97A2mZu=uyu|P%p|*FIrEaC6L$g!D|y#k44zIbMteg z`KsScQX_zs7-T@Mqi^#^8VB*gsWD){f}rtOAqZ0V({;OB`-1Cs$Cy;XZXY7wel{7+i>-1qLz5cK_yF{#y{@BOOa mbFaGz5_Hv$F{!iz|Mj}>9>Zf{;4c6G1J_F-0EiS3eEkQ&0G0>< literal 0 HcmV?d00001 diff --git a/osu.Game.Tests/Skins/SkinDeserialisationTest.cs b/osu.Game.Tests/Skins/SkinDeserialisationTest.cs index 82b46ee75f..7372557161 100644 --- a/osu.Game.Tests/Skins/SkinDeserialisationTest.cs +++ b/osu.Game.Tests/Skins/SkinDeserialisationTest.cs @@ -12,6 +12,7 @@ using osu.Framework.IO.Stores; using osu.Game.Audio; using osu.Game.IO; using osu.Game.IO.Archives; +using osu.Game.Screens.Menu; using osu.Game.Screens.Play.HUD; using osu.Game.Screens.Play.HUD.HitErrorMeters; using osu.Game.Skinning; @@ -125,6 +126,18 @@ namespace osu.Game.Tests.Skins } } + [Test] + public void TestDeserialiseInvalidDrawables() + { + using (var stream = TestResources.OpenResource("Archives/argon-invalid-drawable.osk")) + using (var storage = new ZipArchiveReader(stream)) + { + var skin = new TestSkin(new SkinInfo(), null, storage); + + Assert.That(skin.LayoutInfos.Any(kvp => kvp.Value.AllDrawables.Any(d => d.Type == typeof(StarFountain))), Is.False); + } + } + [Test] public void TestDeserialiseModifiedClassic() { From 885d832e9845ae1934993aa52322995fa6e4a56e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 23 Aug 2024 20:44:45 +0900 Subject: [PATCH 2077/2493] Fix deserialise failing with some old skins --- osu.Game/Skinning/Skin.cs | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/osu.Game/Skinning/Skin.cs b/osu.Game/Skinning/Skin.cs index 2382253036..e93a10d50b 100644 --- a/osu.Game/Skinning/Skin.cs +++ b/osu.Game/Skinning/Skin.cs @@ -248,9 +248,33 @@ namespace osu.Game.Skinning applyMigration(layout, target, i); layout.Version = SkinLayoutInfo.LATEST_VERSION; + + foreach (var kvp in layout.DrawableInfo.ToArray()) + { + foreach (var di in kvp.Value) + { + if (!isValidDrawable(di)) + layout.DrawableInfo[kvp.Key] = kvp.Value.Where(i => i.Type != di.Type).ToArray(); + } + } + return layout; } + private bool isValidDrawable(SerialisedDrawableInfo di) + { + if (!typeof(ISerialisableDrawable).IsAssignableFrom(di.Type)) + return false; + + foreach (var child in di.Children) + { + if (!isValidDrawable(child)) + return false; + } + + return true; + } + private void applyMigration(SkinLayoutInfo layout, GlobalSkinnableContainers target, int version) { switch (version) From f5e195a7ee2e6a4d2c72ca2ab982bf23adb160aa Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 24 Aug 2024 06:40:30 +0900 Subject: [PATCH 2078/2493] Remove existing test asserts --- .../Visual/UserInterface/TestSceneMainMenuButton.cs | 9 --------- 1 file changed, 9 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneMainMenuButton.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneMainMenuButton.cs index e534547c27..41543669eb 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneMainMenuButton.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneMainMenuButton.cs @@ -6,7 +6,6 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; -using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Localisation; using osu.Game.Online.API; @@ -72,7 +71,6 @@ namespace osu.Game.Tests.Visual.UserInterface NotificationOverlay notificationOverlay = null!; DependencyProvidingContainer buttonContainer = null!; - AddStep("set intro played flag", () => Dependencies.Get().SetValue(Static.DailyChallengeIntroPlayed, true)); AddStep("beatmap of the day active", () => metadataClient.DailyChallengeUpdated(new DailyChallengeInfo { RoomID = 1234, @@ -98,7 +96,6 @@ namespace osu.Game.Tests.Visual.UserInterface }, }; }); - AddAssert("intro played flag reset", () => !Dependencies.Get().Get(Static.DailyChallengeIntroPlayed)); AddAssert("notification posted", () => notificationOverlay.AllNotifications.Count(), () => Is.EqualTo(1)); AddStep("clear notifications", () => @@ -107,11 +104,8 @@ namespace osu.Game.Tests.Visual.UserInterface notification.Close(runFlingAnimation: false); }); - AddStep("set intro played flag", () => Dependencies.Get().SetValue(Static.DailyChallengeIntroPlayed, true)); - AddStep("beatmap of the day not active", () => metadataClient.DailyChallengeUpdated(null)); AddAssert("no notification posted", () => notificationOverlay.AllNotifications.Count(), () => Is.Zero); - AddAssert("intro played flag still set", () => Dependencies.Get().Get(Static.DailyChallengeIntroPlayed)); AddStep("hide button's parent", () => buttonContainer.Hide()); @@ -120,7 +114,6 @@ namespace osu.Game.Tests.Visual.UserInterface RoomID = 1234, })); AddAssert("no notification posted", () => notificationOverlay.AllNotifications.Count(), () => Is.Zero); - AddAssert("intro played flag still set", () => Dependencies.Get().Get(Static.DailyChallengeIntroPlayed)); } [Test] @@ -178,13 +171,11 @@ namespace osu.Game.Tests.Visual.UserInterface }; }); - AddStep("set intro played flag", () => Dependencies.Get().SetValue(Static.DailyChallengeIntroPlayed, true)); AddStep("beatmap of the day active", () => metadataClient.DailyChallengeUpdated(new DailyChallengeInfo { RoomID = 1234 })); AddAssert("no notification posted", () => notificationOverlay.AllNotifications.Count(), () => Is.Zero); - AddAssert("intro played flag reset", () => !Dependencies.Get().Get(Static.DailyChallengeIntroPlayed)); } } } From ce8286d299f11a9e7c96baf67ca38d5a143239d5 Mon Sep 17 00:00:00 2001 From: StanR Date: Sat, 24 Aug 2024 04:37:58 +0500 Subject: [PATCH 2079/2493] Scale difficulty with doubletapness, make kicksliders not reduce the difficulty of the next object, adjust balancing --- .../Difficulty/Evaluators/RhythmEvaluator.cs | 28 +++++++++++++------ .../Difficulty/Evaluators/SpeedEvaluator.cs | 14 +--------- .../Preprocessing/OsuDifficultyHitObject.cs | 18 ++++++++++++ 3 files changed, 38 insertions(+), 22 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs index 9ffb4fc3d7..22b330bcac 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs @@ -60,7 +60,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators } private const int history_time_max = 5 * 1000; // 5 seconds of calculatingRhythmBonus max. - private const double rhythm_multiplier = 1.05; + private const double rhythm_multiplier = 0.95; /// /// Calculates a rhythm multiplier for the difficulty of the tap associated with historic data of the current . @@ -102,7 +102,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators double prevDelta = prevObj.StrainTime; double lastDelta = lastObj.StrainTime; - double currRatio = 1.0 + 8.8 * Math.Min(0.5, Math.Pow(Math.Sin(Math.PI / (Math.Min(prevDelta, currDelta) / Math.Max(prevDelta, currDelta))), 2)); // fancy function to calculate rhythmbonuses. + double currRatio = 1.0 + 9.8 * Math.Min(0.5, Math.Pow(Math.Sin(Math.PI / (Math.Min(prevDelta, currDelta) / Math.Max(prevDelta, currDelta))), 2)); // fancy function to calculate rhythmbonuses. double windowPenalty = Math.Min(1, Math.Max(0, Math.Abs(prevDelta - currDelta) - currObj.HitWindowGreat * 0.3) / (currObj.HitWindowGreat * 0.3)); @@ -121,16 +121,22 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators } else { - if (currObj.BaseObject is Slider) // bpm change is into slider, this is easy acc window + // bpm change is into slider, this is easy acc window + if (currObj.BaseObject is Slider) effectiveRatio *= 0.125; - if (prevObj.BaseObject is Slider) // bpm change was from a slider, this is easier typically than circle -> circle - effectiveRatio *= 0.25; + // bpm change was from a slider, this is easier typically than circle -> circle + // unintentional side effect is that bursts with kicksliders at the ends might have lower difficulty than bursts without sliders + // therefore we're checking for quick sliders and don't lower the difficulty for them since they don't really make tapping easier (no time to adjust) + if (prevObj.BaseObject is Slider && prevObj.TravelTime > prevDelta * 1.5) + effectiveRatio *= 0.15; - if (island.IsSimilarPolarity(previousIsland, deltaDifferenceEpsilon)) // repeated island polartiy (2 -> 4, 3 -> 5) - effectiveRatio *= 0.35; + // repeated island polartiy (2 -> 4, 3 -> 5) + if (island.IsSimilarPolarity(previousIsland, deltaDifferenceEpsilon)) + effectiveRatio *= 0.3; - if (lastDelta > prevDelta + deltaDifferenceEpsilon && prevDelta > currDelta + deltaDifferenceEpsilon) // previous increase happened a note ago, 1/1->1/2-1/4, dont want to buff this. + // previous increase happened a note ago, 1/1->1/2-1/4, dont want to buff this. + if (lastDelta > prevDelta + deltaDifferenceEpsilon && prevDelta > currDelta + deltaDifferenceEpsilon) effectiveRatio *= 0.125; if (!islandCounts.TryAdd(island, 1)) @@ -142,6 +148,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators effectiveRatio *= Math.Min(2.0 / islandCounts[island], Math.Pow(1.0 / islandCounts[island], power)); } + // scale down the difficulty if the object is doubletappable + double doubletapness = prevObj.GetDoubletapness((OsuDifficultyHitObject?)prevObj.Next(0)); + effectiveRatio *= 1 - doubletapness * 0.75; + rhythmComplexitySum += Math.Sqrt(effectiveRatio * startRatio) * currHistoricalDecay; startRatio = effectiveRatio; @@ -167,7 +177,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators prevObj = currObj; } - return Math.Sqrt(4 + rhythmComplexitySum * rhythm_multiplier) / 2; //produces multiplier that can be applied to strain. range [1, infinity) (not really though) + return Math.Sqrt(4 + rhythmComplexitySum * rhythm_multiplier) / 2.0; //produces multiplier that can be applied to strain. range [1, infinity) (not really though) } private static double logistic(double x, double maxValue, double multiplier, double offset) => (maxValue / (1 + Math.Pow(Math.E, offset - (multiplier * x)))); diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/SpeedEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/SpeedEvaluator.cs index 2df383aaa8..3a264188f6 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/SpeedEvaluator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/SpeedEvaluator.cs @@ -30,21 +30,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators // derive strainTime for calculation var osuCurrObj = (OsuDifficultyHitObject)current; var osuPrevObj = current.Index > 0 ? (OsuDifficultyHitObject)current.Previous(0) : null; - var osuNextObj = (OsuDifficultyHitObject?)current.Next(0); double strainTime = osuCurrObj.StrainTime; - double doubletapness = 1; - - // Nerf doubletappable doubles. - if (osuNextObj != null) - { - double currDeltaTime = Math.Max(1, osuCurrObj.DeltaTime); - double nextDeltaTime = Math.Max(1, osuNextObj.DeltaTime); - double deltaDifference = Math.Abs(nextDeltaTime - currDeltaTime); - double speedRatio = currDeltaTime / Math.Max(currDeltaTime, deltaDifference); - double windowRatio = Math.Pow(Math.Min(1, currDeltaTime / osuCurrObj.HitWindowGreat), 2); - doubletapness = Math.Pow(speedRatio, 1 - windowRatio); - } + double doubletapness = 1.0 - osuCurrObj.GetDoubletapness((OsuDifficultyHitObject?)osuCurrObj.Next(0)); // Cap deltatime to the OD 300 hitwindow. // 0.93 is derived from making sure 260bpm OD8 streams aren't nerfed harshly, whilst 0.92 limits the effect of the cap. diff --git a/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs b/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs index 95535274c1..3eaf500ad7 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs @@ -137,6 +137,24 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing return Math.Clamp((time - fadeInStartTime) / fadeInDuration, 0.0, 1.0); } + /// + /// Returns how possible is it to doubletap this object together with the next one and get perfect judgement in range from 0 to 1 + /// + public double GetDoubletapness(OsuDifficultyHitObject? osuNextObj) + { + if (osuNextObj != null) + { + double currDeltaTime = Math.Max(1, DeltaTime); + double nextDeltaTime = Math.Max(1, osuNextObj.DeltaTime); + double deltaDifference = Math.Abs(nextDeltaTime - currDeltaTime); + double speedRatio = currDeltaTime / Math.Max(currDeltaTime, deltaDifference); + double windowRatio = Math.Pow(Math.Min(1, currDeltaTime / HitWindowGreat), 2); + return 1.0 - Math.Pow(speedRatio, 1 - windowRatio); + } + + return 0; + } + private void setDistances(double clockRate) { if (BaseObject is Slider currentSlider) From 89a4025c0109b3c96fb1265eab6485ea9a7ba9e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Brandst=C3=B6tter?= Date: Sat, 24 Aug 2024 10:40:51 +0200 Subject: [PATCH 2080/2493] Fix Daily Challenge play count using a different colour than osu-web --- .../Online/TestSceneUserProfileDailyChallenge.cs | 9 +++++++++ .../Header/Components/DailyChallengeStatsDisplay.cs | 12 ++++++++---- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserProfileDailyChallenge.cs b/osu.Game.Tests/Visual/Online/TestSceneUserProfileDailyChallenge.cs index f2135ec992..d7f5f65769 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneUserProfileDailyChallenge.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneUserProfileDailyChallenge.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -11,6 +12,7 @@ using osu.Game.Overlays; using osu.Game.Overlays.Profile; using osu.Game.Overlays.Profile.Header.Components; using osu.Game.Rulesets.Osu; +using osu.Game.Scoring; using osuTK; namespace osu.Game.Tests.Visual.Online @@ -60,5 +62,12 @@ namespace osu.Game.Tests.Visual.Online change.Invoke(User.Value!.User.DailyChallengeStatistics); User.Value = new UserProfileData(User.Value.User, User.Value.Ruleset); } + + [Test] + public void TestPlayCountRankingTier() + { + AddAssert("1 before silver", () => DailyChallengeStatsDisplay.TierForPlayCount(30) == RankingTier.Bronze); + AddAssert("first silver", () => DailyChallengeStatsDisplay.TierForPlayCount(31) == RankingTier.Silver); + } } } diff --git a/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsDisplay.cs b/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsDisplay.cs index 82d3cfafd7..41fd2be591 100644 --- a/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsDisplay.cs +++ b/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsDisplay.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.Extensions.LocalisationExtensions; @@ -11,9 +12,9 @@ using osu.Framework.Graphics.Shapes; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; +using osu.Game.Localisation; using osu.Game.Online.API.Requests.Responses; using osu.Game.Scoring; -using osu.Game.Localisation; namespace osu.Game.Overlays.Profile.Header.Components { @@ -107,15 +108,18 @@ namespace osu.Game.Overlays.Profile.Header.Components APIUserDailyChallengeStatistics stats = User.Value.User.DailyChallengeStatistics; dailyPlayCount.Text = DailyChallengeStatsDisplayStrings.UnitDay(stats.PlayCount.ToLocalisableString("N0")); - dailyPlayCount.Colour = colours.ForRankingTier(tierForPlayCount(stats.PlayCount)); + dailyPlayCount.Colour = colours.ForRankingTier(TierForPlayCount(stats.PlayCount)); TooltipContent = new DailyChallengeTooltipData(colourProvider, stats); Show(); - - static RankingTier tierForPlayCount(int playCount) => DailyChallengeStatsTooltip.TierForDaily(playCount / 3); } + // Rounding up is needed here to ensure the overlay shows the same colour as osu-web for the play count. + // This is because, for example, 31 / 3 > 10 in JavaScript because floats are used, while here it would + // get truncated to 10 with an integer division and show a lower tier. + public static RankingTier TierForPlayCount(int playCount) => DailyChallengeStatsTooltip.TierForDaily((int)Math.Ceiling(playCount / 3.0d)); + public ITooltip GetCustomTooltip() => new DailyChallengeStatsTooltip(); } } From b54487031ab01ffac4e6aa67c0fc33a7b3ac71c3 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 24 Aug 2024 06:40:51 +0900 Subject: [PATCH 2081/2493] Add `DailyChallengeButton` for intro played flag reset logic --- .../TestSceneDailyChallengeIntro.cs | 51 +++++++++++-------- .../OnlinePlay/TestRoomRequestsHandler.cs | 10 +++- osu.Game/Utils/Optional.cs | 2 +- 3 files changed, 40 insertions(+), 23 deletions(-) diff --git a/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeIntro.cs b/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeIntro.cs index 08d44d7405..f1a2d6b5f2 100644 --- a/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeIntro.cs +++ b/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeIntro.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; using osu.Game.Configuration; @@ -10,11 +9,14 @@ using osu.Game.Online.API; using osu.Game.Online.Metadata; using osu.Game.Online.Rooms; using osu.Game.Overlays; +using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Mods; +using osu.Game.Screens.Menu; using osu.Game.Screens.OnlinePlay.DailyChallenge; -using osu.Game.Tests.Resources; using osu.Game.Tests.Visual.Metadata; using osu.Game.Tests.Visual.OnlinePlay; +using osuTK.Graphics; +using osuTK.Input; using CreateRoomRequest = osu.Game.Online.Rooms.CreateRoomRequest; namespace osu.Game.Tests.Visual.DailyChallenge @@ -32,23 +34,27 @@ namespace osu.Game.Tests.Visual.DailyChallenge [BackgroundDependencyLoader] private void load() { - base.Content.Add(notificationOverlay); - base.Content.Add(metadataClient); + Add(notificationOverlay); + Add(metadataClient); + + // add button to observe for daily challenge changes and perform its logic. + Add(new DailyChallengeButton(@"button-default-select", new Color4(102, 68, 204, 255), _ => { }, 0, Key.D)); } [Test] public void TestDailyChallenge() { - startChallenge(); + startChallenge(1234); AddStep("push screen", () => LoadScreen(new DailyChallengeIntro(room))); } [Test] public void TestPlayIntroOnceFlag() { + startChallenge(1234); AddStep("set intro played flag", () => Dependencies.Get().SetValue(Static.DailyChallengeIntroPlayed, true)); - startChallenge(); + startChallenge(1235); AddAssert("intro played flag reset", () => Dependencies.Get().Get(Static.DailyChallengeIntroPlayed), () => Is.False); @@ -56,25 +62,28 @@ namespace osu.Game.Tests.Visual.DailyChallenge AddUntilStep("intro played flag set", () => Dependencies.Get().Get(Static.DailyChallengeIntroPlayed), () => Is.True); } - private void startChallenge() + private void startChallenge(int roomId) { - room = new Room + AddStep("add room", () => { - RoomID = { Value = 1234 }, - Name = { Value = "Daily Challenge: June 4, 2024" }, - Playlist = + API.Perform(new CreateRoomRequest(room = new Room { - new PlaylistItem(TestResources.CreateTestBeatmapSetInfo().Beatmaps.First()) + RoomID = { Value = roomId }, + Name = { Value = "Daily Challenge: June 4, 2024" }, + Playlist = { - RequiredMods = [new APIMod(new OsuModTraceable())], - AllowedMods = [new APIMod(new OsuModDoubleTime())] - } - }, - EndDate = { Value = DateTimeOffset.Now.AddHours(12) }, - Category = { Value = RoomCategory.DailyChallenge } - }; - - AddStep("add room", () => API.Perform(new CreateRoomRequest(room))); + new PlaylistItem(CreateAPIBeatmap(new OsuRuleset().RulesetInfo)) + { + RequiredMods = [new APIMod(new OsuModTraceable())], + AllowedMods = [new APIMod(new OsuModDoubleTime())] + } + }, + StartDate = { Value = DateTimeOffset.Now }, + EndDate = { Value = DateTimeOffset.Now.AddHours(24) }, + Category = { Value = RoomCategory.DailyChallenge } + })); + }); + AddStep("signal client", () => metadataClient.DailyChallengeUpdated(new DailyChallengeInfo { RoomID = roomId })); } } } diff --git a/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs b/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs index 91df38feb9..4ceb946b28 100644 --- a/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs +++ b/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs @@ -18,6 +18,7 @@ using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; using osu.Game.Screens.OnlinePlay.Components; using osu.Game.Tests.Beatmaps; +using osu.Game.Utils; namespace osu.Game.Tests.Visual.OnlinePlay { @@ -277,11 +278,18 @@ namespace osu.Game.Tests.Visual.OnlinePlay var result = JsonConvert.DeserializeObject(JsonConvert.SerializeObject(source)); Debug.Assert(result != null); - // Playlist item IDs aren't serialised. + // Playlist item IDs and beatmaps aren't serialised. if (source.CurrentPlaylistItem.Value != null) + { + result.CurrentPlaylistItem.Value = result.CurrentPlaylistItem.Value.With(new Optional(source.CurrentPlaylistItem.Value.Beatmap)); result.CurrentPlaylistItem.Value.ID = source.CurrentPlaylistItem.Value.ID; + } + for (int i = 0; i < source.Playlist.Count; i++) + { + result.Playlist[i] = result.Playlist[i].With(new Optional(source.Playlist[i].Beatmap)); result.Playlist[i].ID = source.Playlist[i].ID; + } return result; } diff --git a/osu.Game/Utils/Optional.cs b/osu.Game/Utils/Optional.cs index 301767ba08..f5749a513f 100644 --- a/osu.Game/Utils/Optional.cs +++ b/osu.Game/Utils/Optional.cs @@ -22,7 +22,7 @@ namespace osu.Game.Utils /// public readonly bool HasValue; - private Optional(T value) + public Optional(T value) { Value = value; HasValue = true; From 2bb72762ad6f18e9c7b3ad0e075731f1edcf75f9 Mon Sep 17 00:00:00 2001 From: clayton Date: Sun, 25 Aug 2024 20:42:34 -0700 Subject: [PATCH 2082/2493] Use more contrasting color for mod icon foreground --- osu.Game/Rulesets/UI/ModIcon.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/UI/ModIcon.cs b/osu.Game/Rulesets/UI/ModIcon.cs index 5d9fafd60c..6d91b85823 100644 --- a/osu.Game/Rulesets/UI/ModIcon.cs +++ b/osu.Game/Rulesets/UI/ModIcon.cs @@ -11,6 +11,7 @@ using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; using osu.Framework.Localisation; +using osu.Framework.Utils; using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; @@ -204,7 +205,7 @@ namespace osu.Game.Rulesets.UI private void updateColour() { - modAcronym.Colour = modIcon.Colour = OsuColour.Gray(84); + modAcronym.Colour = modIcon.Colour = Interpolation.ValueAt(0.1f, Colour4.Black, backgroundColour, 0, 1); extendedText.Colour = background.Colour = Selected.Value ? backgroundColour.Lighten(0.2f) : backgroundColour; extendedBackground.Colour = Selected.Value ? backgroundColour.Darken(2.4f) : backgroundColour.Darken(2.8f); From 70d08b9e976be5eef6de51d0088ca30130c20e71 Mon Sep 17 00:00:00 2001 From: clayton Date: Sun, 25 Aug 2024 20:42:57 -0700 Subject: [PATCH 2083/2493] Increase mod icon acronym font weight --- osu.Game/Rulesets/UI/ModIcon.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/UI/ModIcon.cs b/osu.Game/Rulesets/UI/ModIcon.cs index 6d91b85823..5237425075 100644 --- a/osu.Game/Rulesets/UI/ModIcon.cs +++ b/osu.Game/Rulesets/UI/ModIcon.cs @@ -140,7 +140,7 @@ namespace osu.Game.Rulesets.UI Origin = Anchor.Centre, Anchor = Anchor.Centre, Alpha = 0, - Font = OsuFont.Numeric.With(null, 22f), + Font = OsuFont.Numeric.With(size: 22f, weight: FontWeight.Black), UseFullGlyphHeight = false, Text = mod.Acronym }, From 84bceca7780c053743206a7cab4b2c5a98df4430 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 26 Aug 2024 15:38:58 +0900 Subject: [PATCH 2084/2493] Assume we can always fetch the ruleset --- .../Screens/OnlinePlay/DailyChallenge/DailyChallengeIntro.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeIntro.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeIntro.cs index 47785c8868..0a1ac7a5a7 100644 --- a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeIntro.cs +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeIntro.cs @@ -100,7 +100,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge StarRatingDisplay starRatingDisplay; IBeatmapInfo beatmap = item.Beatmap; - Ruleset ruleset = rulesets.GetRuleset(item.Beatmap.Ruleset.ShortName)?.CreateInstance() ?? Ruleset.Value.CreateInstance(); + Ruleset ruleset = rulesets.GetRuleset(item.Beatmap.Ruleset.ShortName)!.CreateInstance(); InternalChildren = new Drawable[] { From 4fc96ebfded8c454aaba5c77e63694af843410b3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 27 Aug 2024 13:13:22 +0900 Subject: [PATCH 2085/2493] Tidy some thing up --- .../Blueprints/Sliders/SliderCircleOverlay.cs | 80 +----------------- .../Blueprints/Sliders/SliderEndDragMarker.cs | 84 +++++++++++++++++++ .../Sliders/SliderSelectionBlueprint.cs | 23 +++-- 3 files changed, 101 insertions(+), 86 deletions(-) create mode 100644 osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderEndDragMarker.cs diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderCircleOverlay.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderCircleOverlay.cs index 6bc3926279..247ceb4078 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderCircleOverlay.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderCircleOverlay.cs @@ -2,22 +2,18 @@ // See the LICENCE file in the repository root for full licence text. using System; -using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Lines; using osu.Framework.Graphics.Primitives; -using osu.Framework.Input.Events; -using osu.Framework.Utils; -using osu.Game.Graphics; using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components; using osu.Game.Rulesets.Osu.Objects; -using osuTK; namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders { public partial class SliderCircleOverlay : CompositeDrawable { + public SliderEndDragMarker? EndDragMarker { get; } + public RectangleF VisibleQuad { get @@ -62,8 +58,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders } } - public SliderEndDragMarker? EndDragMarker { get; } - protected override void Update() { base.Update(); @@ -94,75 +88,5 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders CirclePiece.Show(); endDragMarkerContainer?.Show(); } - - public partial class SliderEndDragMarker : SmoothPath - { - public Action? StartDrag { get; set; } - public Action? Drag { get; set; } - public Action? EndDrag { get; set; } - - [Resolved] - private OsuColour colours { get; set; } = null!; - - [BackgroundDependencyLoader] - private void load() - { - var path = PathApproximator.CircularArcToPiecewiseLinear([ - new Vector2(0, OsuHitObject.OBJECT_RADIUS), - new Vector2(OsuHitObject.OBJECT_RADIUS, 0), - new Vector2(0, -OsuHitObject.OBJECT_RADIUS) - ]); - - Anchor = Anchor.CentreLeft; - Origin = Anchor.CentreLeft; - PathRadius = 5; - Vertices = path; - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - updateState(); - } - - protected override bool OnHover(HoverEvent e) - { - updateState(); - return true; - } - - protected override void OnHoverLost(HoverLostEvent e) - { - updateState(); - base.OnHoverLost(e); - } - - protected override bool OnDragStart(DragStartEvent e) - { - updateState(); - StartDrag?.Invoke(e); - return true; - } - - protected override void OnDrag(DragEvent e) - { - updateState(); - base.OnDrag(e); - Drag?.Invoke(e); - } - - protected override void OnDragEnd(DragEndEvent e) - { - updateState(); - EndDrag?.Invoke(); - base.OnDragEnd(e); - } - - private void updateState() - { - Colour = IsHovered || IsDragged ? colours.Red : colours.Yellow; - } - } } } diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderEndDragMarker.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderEndDragMarker.cs new file mode 100644 index 0000000000..37383544dc --- /dev/null +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderEndDragMarker.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; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Lines; +using osu.Framework.Input.Events; +using osu.Framework.Utils; +using osu.Game.Graphics; +using osu.Game.Rulesets.Osu.Objects; +using osuTK; + +namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders +{ + public partial class SliderEndDragMarker : SmoothPath + { + public Action? StartDrag { get; set; } + public Action? Drag { get; set; } + public Action? EndDrag { get; set; } + + [Resolved] + private OsuColour colours { get; set; } = null!; + + [BackgroundDependencyLoader] + private void load() + { + var path = PathApproximator.CircularArcToPiecewiseLinear([ + new Vector2(0, OsuHitObject.OBJECT_RADIUS), + new Vector2(OsuHitObject.OBJECT_RADIUS, 0), + new Vector2(0, -OsuHitObject.OBJECT_RADIUS) + ]); + + Anchor = Anchor.CentreLeft; + Origin = Anchor.CentreLeft; + PathRadius = 5; + Vertices = path; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + updateState(); + } + + protected override bool OnHover(HoverEvent e) + { + updateState(); + return true; + } + + protected override void OnHoverLost(HoverLostEvent e) + { + updateState(); + base.OnHoverLost(e); + } + + protected override bool OnDragStart(DragStartEvent e) + { + updateState(); + StartDrag?.Invoke(e); + return true; + } + + protected override void OnDrag(DragEvent e) + { + updateState(); + base.OnDrag(e); + Drag?.Invoke(e); + } + + protected override void OnDragEnd(DragEndEvent e) + { + updateState(); + EndDrag?.Invoke(); + base.OnDragEnd(e); + } + + private void updateState() + { + Colour = IsHovered || IsDragged ? colours.Red : colours.Yellow; + } + } +} diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs index 691c053e4d..aca704609a 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using JetBrains.Annotations; using osu.Framework.Allocation; @@ -57,6 +58,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders [Resolved(CanBeNull = true)] private BindableBeatDivisor beatDivisor { get; set; } + [CanBeNull] + private PathControlPoint placementControlPoint; + public override Quad SelectionQuad { get @@ -84,6 +88,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders // Cached slider path which ignored the expected distance value. private readonly Cached fullPathCache = new Cached(); + private Vector2 lastRightClickPosition; + public SliderSelectionBlueprint(Slider slider) : base(slider) { @@ -99,7 +105,10 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders TailOverlay = CreateCircleOverlay(HitObject, SliderPosition.End), }; - TailOverlay.EndDragMarker!.StartDrag += startAdjustingLength; + // tail will always have a non-null end drag marker. + Debug.Assert(TailOverlay.EndDragMarker != null); + + TailOverlay.EndDragMarker.StartDrag += startAdjustingLength; TailOverlay.EndDragMarker.Drag += adjustLength; TailOverlay.EndDragMarker.EndDrag += endAdjustLength; @@ -154,7 +163,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders protected override bool OnHover(HoverEvent e) { updateVisualDefinition(); - return base.OnHover(e); } @@ -199,14 +207,12 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders } } - private Vector2 rightClickPosition; - protected override bool OnMouseDown(MouseDownEvent e) { switch (e.Button) { case MouseButton.Right: - rightClickPosition = e.MouseDownPosition; + lastRightClickPosition = e.MouseDownPosition; return false; // Allow right click to be handled by context menu case MouseButton.Left: @@ -226,6 +232,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders return false; } + #region Length Adjustment (independent of path nodes) + private Vector2 lengthAdjustMouseOffset; private double oldDuration; private double oldVelocityMultiplier; @@ -351,8 +359,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders return bestValue; } - [CanBeNull] - private PathControlPoint placementControlPoint; + #endregion protected override bool OnDragStart(DragStartEvent e) { @@ -586,7 +593,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders new OsuMenuItem("Add control point", MenuItemType.Standard, () => { changeHandler?.BeginChange(); - addControlPoint(rightClickPosition); + addControlPoint(lastRightClickPosition); changeHandler?.EndChange(); }), new OsuMenuItem("Convert to stream", MenuItemType.Destructive, convertToStream), From 50a8348bf9680154451fa40034d20236dc510bc9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 27 Aug 2024 13:18:55 +0900 Subject: [PATCH 2086/2493] Apply NRT to remaining classes in slider blueprint namespace --- .../Sliders/SliderPlacementBlueprint.cs | 49 ++++++++----------- .../Sliders/SliderSelectionBlueprint.cs | 40 +++++++-------- 2 files changed, 40 insertions(+), 49 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs index 013f790f65..42945295b8 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs @@ -1,13 +1,9 @@ // 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.Collections.Generic; -using System.Diagnostics; using System.Linq; -using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Input; @@ -29,30 +25,29 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders { public new Slider HitObject => (Slider)base.HitObject; - private SliderBodyPiece bodyPiece; - private HitCirclePiece headCirclePiece; - private HitCirclePiece tailCirclePiece; - private PathControlPointVisualiser controlPointVisualiser; + private SliderBodyPiece bodyPiece = null!; + private HitCirclePiece headCirclePiece = null!; + private HitCirclePiece tailCirclePiece = null!; + private PathControlPointVisualiser controlPointVisualiser = null!; - private InputManager inputManager; + private InputManager inputManager = null!; + + private PathControlPoint? cursor; private SliderPlacementState state; private PathControlPoint segmentStart; - private PathControlPoint cursor; + private int currentSegmentLength; private bool usingCustomSegmentType; - [Resolved(CanBeNull = true)] - [CanBeNull] - private IPositionSnapProvider positionSnapProvider { get; set; } + [Resolved] + private IPositionSnapProvider? positionSnapProvider { get; set; } - [Resolved(CanBeNull = true)] - [CanBeNull] - private IDistanceSnapProvider distanceSnapProvider { get; set; } + [Resolved] + private IDistanceSnapProvider? distanceSnapProvider { get; set; } - [Resolved(CanBeNull = true)] - [CanBeNull] - private FreehandSliderToolboxGroup freehandToolboxGroup { get; set; } + [Resolved] + private FreehandSliderToolboxGroup? freehandToolboxGroup { get; set; } private readonly IncrementalBSplineBuilder bSplineBuilder = new IncrementalBSplineBuilder { Degree = 4 }; @@ -84,7 +79,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders protected override void LoadComplete() { base.LoadComplete(); - inputManager = GetContainingInputManager(); + + inputManager = GetContainingInputManager()!; if (freehandToolboxGroup != null) { @@ -108,7 +104,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders } [Resolved] - private EditorBeatmap editorBeatmap { get; set; } + private EditorBeatmap editorBeatmap { get; set; } = null!; public override void UpdateTimeAndPosition(SnapResult result) { @@ -151,7 +147,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders case SliderPlacementState.ControlPoints: if (canPlaceNewControlPoint(out var lastPoint)) placeNewControlPoint(); - else + else if (lastPoint != null) beginNewSegment(lastPoint); break; @@ -162,9 +158,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders private void beginNewSegment(PathControlPoint lastPoint) { - // Transform the last point into a new segment. - Debug.Assert(lastPoint != null); - segmentStart = lastPoint; segmentStart.Type = PathType.LINEAR; @@ -384,7 +377,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders /// /// The last-placed control point. May be null, but is not null if false is returned. /// Whether a new control point can be placed at the current position. - private bool canPlaceNewControlPoint([CanBeNull] out PathControlPoint lastPoint) + private bool canPlaceNewControlPoint(out PathControlPoint? lastPoint) { // We cannot rely on the ordering of drawable pieces, so find the respective drawable piece by searching for the last non-cursor control point. var last = HitObject.Path.ControlPoints.LastOrDefault(p => p != cursor); @@ -436,7 +429,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders // Replace this segment with a circular arc if it is a reasonable substitute. var circleArcSegment = tryCircleArc(segment); - if (circleArcSegment is not null) + if (circleArcSegment != null) { HitObject.Path.ControlPoints.Add(new PathControlPoint(circleArcSegment[0], PathType.PERFECT_CURVE)); HitObject.Path.ControlPoints.Add(new PathControlPoint(circleArcSegment[1])); @@ -453,7 +446,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders } } - private Vector2[] tryCircleArc(List segment) + private Vector2[]? tryCircleArc(List segment) { if (segment.Count < 3 || freehandToolboxGroup?.CircleThreshold.Value == 0) return null; diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs index aca704609a..1debb09099 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs @@ -1,13 +1,10 @@ // 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.Collections.Generic; using System.Diagnostics; using System.Linq; -using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Caching; @@ -36,30 +33,28 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders { protected new DrawableSlider DrawableObject => (DrawableSlider)base.DrawableObject; - protected SliderBodyPiece BodyPiece { get; private set; } - protected SliderCircleOverlay HeadOverlay { get; private set; } - protected SliderCircleOverlay TailOverlay { get; private set; } + protected SliderBodyPiece BodyPiece { get; private set; } = null!; + protected SliderCircleOverlay HeadOverlay { get; private set; } = null!; + protected SliderCircleOverlay TailOverlay { get; private set; } = null!; - [CanBeNull] - protected PathControlPointVisualiser ControlPointVisualiser { get; private set; } + protected PathControlPointVisualiser? ControlPointVisualiser { get; private set; } - [Resolved(CanBeNull = true)] - private IDistanceSnapProvider distanceSnapProvider { get; set; } + [Resolved] + private IDistanceSnapProvider? distanceSnapProvider { get; set; } - [Resolved(CanBeNull = true)] - private IPlacementHandler placementHandler { get; set; } + [Resolved] + private IPlacementHandler? placementHandler { get; set; } - [Resolved(CanBeNull = true)] - private EditorBeatmap editorBeatmap { get; set; } + [Resolved] + private EditorBeatmap? editorBeatmap { get; set; } - [Resolved(CanBeNull = true)] - private IEditorChangeHandler changeHandler { get; set; } + [Resolved] + private IEditorChangeHandler? changeHandler { get; set; } - [Resolved(CanBeNull = true)] - private BindableBeatDivisor beatDivisor { get; set; } + [Resolved] + private BindableBeatDivisor? beatDivisor { get; set; } - [CanBeNull] - private PathControlPoint placementControlPoint; + private PathControlPoint? placementControlPoint; public override Quad SelectionQuad { @@ -145,7 +140,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders return false; hoveredControlPoint.IsSelected.Value = true; - ControlPointVisualiser.DeleteSelected(); + ControlPointVisualiser?.DeleteSelected(); return true; } @@ -487,6 +482,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders private void splitControlPoints(List controlPointsToSplitAt) { + if (editorBeatmap == null) + return; + // Arbitrary gap in milliseconds to put between split slider pieces const double split_gap = 100; From 9840a07eaf86d9c9f652b089f0daf81cd282d2f9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 27 Aug 2024 14:03:14 +0900 Subject: [PATCH 2087/2493] Fix osu!mania hold notes playing a sound at their tail in the editor Closes #29584. --- .../Beatmaps/ManiaBeatmapConverter.cs | 13 +--------- osu.Game.Rulesets.Mania/Objects/HoldNote.cs | 24 ++++++++++++++++--- 2 files changed, 22 insertions(+), 15 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs index 39ee3d209b..970d68759f 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs @@ -6,7 +6,6 @@ using System; using System.Linq; using System.Collections.Generic; using System.Threading; -using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; @@ -271,7 +270,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps Duration = endTimeData.Duration, Column = column, Samples = HitObject.Samples, - NodeSamples = (HitObject as IHasRepeats)?.NodeSamples ?? defaultNodeSamples + NodeSamples = (HitObject as IHasRepeats)?.NodeSamples ?? HoldNote.CreateDefaultNodeSamples(HitObject) }); } else if (HitObject is IHasXPosition) @@ -286,16 +285,6 @@ namespace osu.Game.Rulesets.Mania.Beatmaps return pattern; } - - /// - /// osu!mania-specific beatmaps in stable only play samples at the start of the hold note. - /// - private List> defaultNodeSamples - => new List> - { - HitObject.Samples, - new List() - }; } } } diff --git a/osu.Game.Rulesets.Mania/Objects/HoldNote.cs b/osu.Game.Rulesets.Mania/Objects/HoldNote.cs index 6be0ee2d6b..98060dd226 100644 --- a/osu.Game.Rulesets.Mania/Objects/HoldNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/HoldNote.cs @@ -7,6 +7,7 @@ using System.Collections.Generic; using System.Threading; using osu.Game.Audio; using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Scoring; @@ -91,6 +92,10 @@ namespace osu.Game.Rulesets.Mania.Objects { base.CreateNestedHitObjects(cancellationToken); + // Generally node samples will be populated by ManiaBeatmapConverter, but in a case like the editor they may not be. + // Ensure they are set to a sane default here. + NodeSamples ??= CreateDefaultNodeSamples(this); + AddNested(Head = new HeadNote { StartTime = StartTime, @@ -102,7 +107,7 @@ namespace osu.Game.Rulesets.Mania.Objects { StartTime = EndTime, Column = Column, - Samples = GetNodeSamples((NodeSamples?.Count - 1) ?? 1), + Samples = GetNodeSamples(NodeSamples.Count - 1), }); AddNested(Body = new HoldNoteBody @@ -116,7 +121,20 @@ namespace osu.Game.Rulesets.Mania.Objects protected override HitWindows CreateHitWindows() => HitWindows.Empty; - public IList GetNodeSamples(int nodeIndex) => - nodeIndex < NodeSamples?.Count ? NodeSamples[nodeIndex] : Samples; + public IList GetNodeSamples(int nodeIndex) => nodeIndex < NodeSamples?.Count ? NodeSamples[nodeIndex] : Samples; + + /// + /// Create the default note samples for a hold note, based off their main sample. + /// + /// + /// By default, osu!mania beatmaps in only play samples at the start of the hold note. + /// + /// The object to use as a basis for the head sample. + /// Defaults for assigning to . + public static List> CreateDefaultNodeSamples(HitObject obj) => new List> + { + obj.Samples, + new List(), + }; } } From dce32a79830721345382d799629f2f6fee710cd8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 27 Aug 2024 14:30:07 +0900 Subject: [PATCH 2088/2493] Ban `Vortice.*` imports They have colours and boxes and other classes that conflict with our naming. We never use them. --- osu.sln.DotSettings | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.sln.DotSettings b/osu.sln.DotSettings index 0c52f8d82a..a792b956dd 100644 --- a/osu.sln.DotSettings +++ b/osu.sln.DotSettings @@ -845,6 +845,7 @@ See the LICENCE file in the repository root for full licence text. True True True + True True True True From c2c83fe73d7bb638c725dbb285589bd86aea0b4d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 27 Aug 2024 14:33:36 +0900 Subject: [PATCH 2089/2493] Fix `TestSceneBreakTracker` not removing old drawables Also adds a bright background for testing overlay display. --- .../Visual/Gameplay/TestSceneBreakTracker.cs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneBreakTracker.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneBreakTracker.cs index c010b2c809..ea21262fc0 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneBreakTracker.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneBreakTracker.cs @@ -7,9 +7,11 @@ using System.Collections.Generic; using System.Linq; using NUnit.Framework; using osu.Framework.Graphics; +using osu.Framework.Graphics.Shapes; using osu.Framework.Timing; using osu.Game.Beatmaps.Timing; using osu.Game.Screens.Play; +using osuTK.Graphics; namespace osu.Game.Tests.Visual.Gameplay { @@ -28,14 +30,19 @@ namespace osu.Game.Tests.Visual.Gameplay public TestSceneBreakTracker() { - AddRange(new Drawable[] + Children = new Drawable[] { + new Box + { + Colour = Color4.White, + RelativeSizeAxes = Axes.Both, + }, breakTracker = new TestBreakTracker(), breakOverlay = new BreakOverlay(true, null) { ProcessCustomClock = false, } - }); + }; } protected override void Update() From abdbe510b884f86e5e9a5d9f746b8bddc51caac6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 27 Aug 2024 14:52:08 +0900 Subject: [PATCH 2090/2493] Move break overlay (and cursor) further forward in depth I didn't really want to move the cursor in front of the HUD, but we face a bit of an impossible scenario otherwise (it should definitely be in front of the break overlay for visibility). So I'll deal with it for now. --- osu.Game/Screens/Play/Player.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 986c687960..91bd0a676b 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -446,14 +446,6 @@ namespace osu.Game.Screens.Play Children = new[] { DimmableStoryboard.OverlayLayerContainer.CreateProxy(), - BreakOverlay = new BreakOverlay(working.Beatmap.BeatmapInfo.LetterboxInBreaks, ScoreProcessor) - { - Clock = DrawableRuleset.FrameStableClock, - ProcessCustomClock = false, - Breaks = working.Beatmap.Breaks - }, - // display the cursor above some HUD elements. - DrawableRuleset.Cursor?.CreateProxy() ?? new Container(), HUDOverlay = new HUDOverlay(DrawableRuleset, GameplayState.Mods, Configuration.AlwaysShowLeaderboard) { HoldToQuit = @@ -472,6 +464,14 @@ namespace osu.Game.Screens.Play Anchor = Anchor.Centre, Origin = Anchor.Centre }, + BreakOverlay = new BreakOverlay(working.Beatmap.BeatmapInfo.LetterboxInBreaks, ScoreProcessor) + { + Clock = DrawableRuleset.FrameStableClock, + ProcessCustomClock = false, + Breaks = working.Beatmap.Breaks + }, + // display the cursor above some HUD elements. + DrawableRuleset.Cursor?.CreateProxy() ?? new Container(), skipIntroOverlay = new SkipOverlay(DrawableRuleset.GameplayStartTime) { RequestSkip = performUserRequestedSkip From 797b0207470f340456a46e55e0087fad3218d19b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 27 Aug 2024 14:53:49 +0900 Subject: [PATCH 2091/2493] Add shadow around break overlay middle content to make sure it remains visible --- .../Screens/Play/Break/LetterboxOverlay.cs | 4 +-- osu.Game/Screens/Play/BreakOverlay.cs | 27 +++++++++++++++++++ 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Play/Break/LetterboxOverlay.cs b/osu.Game/Screens/Play/Break/LetterboxOverlay.cs index c4e2dbf403..9308a02b07 100644 --- a/osu.Game/Screens/Play/Break/LetterboxOverlay.cs +++ b/osu.Game/Screens/Play/Break/LetterboxOverlay.cs @@ -11,12 +11,12 @@ namespace osu.Game.Screens.Play.Break { public partial class LetterboxOverlay : CompositeDrawable { - private const int height = 350; - private static readonly Color4 transparent_black = new Color4(0, 0, 0, 0); public LetterboxOverlay() { + const int height = 150; + RelativeSizeAxes = Axes.Both; InternalChildren = new Drawable[] { diff --git a/osu.Game/Screens/Play/BreakOverlay.cs b/osu.Game/Screens/Play/BreakOverlay.cs index ece3105b42..7480cec3a6 100644 --- a/osu.Game/Screens/Play/BreakOverlay.cs +++ b/osu.Game/Screens/Play/BreakOverlay.cs @@ -6,11 +6,14 @@ using System; using System.Collections.Generic; using osu.Framework.Bindables; +using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.UserInterface; using osu.Game.Beatmaps.Timing; +using osu.Game.Graphics; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; using osu.Game.Screens.Play.Break; @@ -69,6 +72,30 @@ namespace osu.Game.Screens.Play Anchor = Anchor.Centre, Origin = Anchor.Centre, }, + new CircularContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Width = 80, + Height = 4, + Masking = true, + EdgeEffect = new EdgeEffectParameters + { + Type = EdgeEffectType.Shadow, + Radius = 260, + Colour = OsuColour.Gray(0.2f).Opacity(0.8f), + Roundness = 12 + }, + Children = new Drawable[] + { + new Box + { + Alpha = 0, + AlwaysPresent = true, + RelativeSizeAxes = Axes.Both, + }, + } + }, remainingTimeAdjustmentBox = new Container { Anchor = Anchor.Centre, From 98faa07590e403706a2d82ca7ec656512df6114c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 27 Aug 2024 14:56:57 +0900 Subject: [PATCH 2092/2493] Apply NRT to `BreakOverlay` --- osu.Game/Screens/Play/BreakOverlay.cs | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/osu.Game/Screens/Play/BreakOverlay.cs b/osu.Game/Screens/Play/BreakOverlay.cs index 7480cec3a6..120d72a8e7 100644 --- a/osu.Game/Screens/Play/BreakOverlay.cs +++ b/osu.Game/Screens/Play/BreakOverlay.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.Collections.Generic; using osu.Framework.Bindables; @@ -32,7 +30,7 @@ namespace osu.Game.Screens.Play private readonly Container fadeContainer; - private IReadOnlyList breaks; + private IReadOnlyList breaks = Array.Empty(); public IReadOnlyList Breaks { @@ -138,11 +136,8 @@ namespace osu.Game.Screens.Play base.LoadComplete(); initializeBreaks(); - if (scoreProcessor != null) - { - info.AccuracyDisplay.Current.BindTo(scoreProcessor.Accuracy); - ((IBindable)info.GradeDisplay.Current).BindTo(scoreProcessor.Rank); - } + info.AccuracyDisplay.Current.BindTo(scoreProcessor.Accuracy); + ((IBindable)info.GradeDisplay.Current).BindTo(scoreProcessor.Rank); } protected override void Update() @@ -157,8 +152,6 @@ namespace osu.Game.Screens.Play FinishTransforms(true); Scheduler.CancelDelayedTasks(); - if (breaks == null) return; // we need breaks. - foreach (var b in breaks) { if (!b.HasEffect) From 6f1664f0a60fc08995d737e40272b61742fbe580 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 27 Aug 2024 16:30:49 +0900 Subject: [PATCH 2093/2493] Add beat-synced animation to break overlay I've been meaning to make the progress bar synchronise with the beat rather than a continuous countdown, just to give the overlay a bit more of a rhythmic feel. Not completely happy with how this feels but I think it's a start? I had to refactor how the break overlay works in the process. It no longer creates transforms for all breaks ahead-of-time, which could be argued as a better way of doing things. It's more dynamically able to handle breaks now (maybe useful for the future, who knows). --- .../Visual/Gameplay/TestSceneBreakTracker.cs | 13 ++- osu.Game/Screens/Play/BreakOverlay.cs | 104 ++++++++++-------- osu.Game/Screens/Play/BreakTracker.cs | 21 ++-- osu.Game/Screens/Play/Player.cs | 2 +- osu.Game/Utils/PeriodTracker.cs | 24 +++- 5 files changed, 102 insertions(+), 62 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneBreakTracker.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneBreakTracker.cs index ea21262fc0..21b6495865 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneBreakTracker.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneBreakTracker.cs @@ -10,6 +10,8 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Shapes; using osu.Framework.Timing; using osu.Game.Beatmaps.Timing; +using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.Scoring; using osu.Game.Screens.Play; using osuTK.Graphics; @@ -38,9 +40,10 @@ namespace osu.Game.Tests.Visual.Gameplay RelativeSizeAxes = Axes.Both, }, breakTracker = new TestBreakTracker(), - breakOverlay = new BreakOverlay(true, null) + breakOverlay = new BreakOverlay(true, new ScoreProcessor(new OsuRuleset())) { ProcessCustomClock = false, + BreakTracker = breakTracker, } }; } @@ -55,9 +58,6 @@ namespace osu.Game.Tests.Visual.Gameplay [Test] public void TestShowBreaks() { - setClock(false); - - addShowBreakStep(2); addShowBreakStep(5); addShowBreakStep(15); } @@ -122,7 +122,7 @@ namespace osu.Game.Tests.Visual.Gameplay { AddStep($"show '{seconds}s' break", () => { - breakOverlay.Breaks = breakTracker.Breaks = new List + breakTracker.Breaks = new List { new BreakPeriod(Clock.CurrentTime, Clock.CurrentTime + seconds * 1000) }; @@ -136,7 +136,7 @@ namespace osu.Game.Tests.Visual.Gameplay private void loadBreaksStep(string breakDescription, IReadOnlyList breaks) { - AddStep($"load {breakDescription}", () => breakOverlay.Breaks = breakTracker.Breaks = breaks); + AddStep($"load {breakDescription}", () => breakTracker.Breaks = breaks); seekAndAssertBreak("seek back to 0", 0, false); } @@ -182,6 +182,7 @@ namespace osu.Game.Tests.Visual.Gameplay } public TestBreakTracker() + : base(0, new ScoreProcessor(new OsuRuleset())) { FramedManualClock = new FramedClock(manualClock = new ManualClock()); ProcessCustomClock = false; diff --git a/osu.Game/Screens/Play/BreakOverlay.cs b/osu.Game/Screens/Play/BreakOverlay.cs index 120d72a8e7..7fc3dba3eb 100644 --- a/osu.Game/Screens/Play/BreakOverlay.cs +++ b/osu.Game/Screens/Play/BreakOverlay.cs @@ -2,7 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Collections.Generic; +using osu.Framework.Audio.Track; using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; @@ -10,15 +10,18 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.UserInterface; +using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.Timing; using osu.Game.Graphics; +using osu.Game.Graphics.Containers; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; using osu.Game.Screens.Play.Break; +using osu.Game.Utils; namespace osu.Game.Screens.Play { - public partial class BreakOverlay : Container + public partial class BreakOverlay : BeatSyncedContainer { /// /// The duration of the break overlay fading. @@ -26,26 +29,14 @@ namespace osu.Game.Screens.Play public const double BREAK_FADE_DURATION = BreakPeriod.MIN_BREAK_DURATION / 2; private const float remaining_time_container_max_size = 0.3f; - private const int vertical_margin = 25; + private const int vertical_margin = 15; private readonly Container fadeContainer; - private IReadOnlyList breaks = Array.Empty(); - - public IReadOnlyList Breaks - { - get => breaks; - set - { - breaks = value; - - if (IsLoaded) - initializeBreaks(); - } - } - public override bool RemoveCompletedTransforms => false; + public BreakTracker BreakTracker { get; init; } = null!; + private readonly Container remainingTimeAdjustmentBox; private readonly Container remainingTimeBox; private readonly RemainingTimeCounter remainingTimeCounter; @@ -53,11 +44,15 @@ namespace osu.Game.Screens.Play private readonly ScoreProcessor scoreProcessor; private readonly BreakInfo info; + private readonly IBindable currentPeriod = new Bindable(); + public BreakOverlay(bool letterboxing, ScoreProcessor scoreProcessor) { this.scoreProcessor = scoreProcessor; RelativeSizeAxes = Axes.Both; + MinimumBeatLength = 200; + Child = fadeContainer = new Container { Alpha = 0, @@ -114,13 +109,13 @@ namespace osu.Game.Screens.Play { Anchor = Anchor.Centre, Origin = Anchor.BottomCentre, - Margin = new MarginPadding { Bottom = vertical_margin }, + Y = -vertical_margin, }, info = new BreakInfo { Anchor = Anchor.Centre, Origin = Anchor.TopCentre, - Margin = new MarginPadding { Top = vertical_margin }, + Y = vertical_margin, }, breakArrows = new BreakArrows { @@ -134,51 +129,68 @@ namespace osu.Game.Screens.Play protected override void LoadComplete() { base.LoadComplete(); - initializeBreaks(); info.AccuracyDisplay.Current.BindTo(scoreProcessor.Accuracy); ((IBindable)info.GradeDisplay.Current).BindTo(scoreProcessor.Rank); + + currentPeriod.BindTo(BreakTracker.CurrentPeriod); + currentPeriod.BindValueChanged(updateDisplay, true); } + private float remainingTimeForCurrentPeriod => + currentPeriod.Value == null ? 0 : (float)Math.Max(0, (currentPeriod.Value.Value.End - Time.Current - BREAK_FADE_DURATION) / currentPeriod.Value.Value.Duration); + protected override void Update() { base.Update(); - remainingTimeBox.Height = Math.Min(8, remainingTimeBox.DrawWidth); + if (currentPeriod.Value != null) + { + remainingTimeBox.Height = Math.Min(8, remainingTimeBox.DrawWidth); + remainingTimeCounter.X = -(remainingTimeForCurrentPeriod - 0.5f) * 30; + info.X = (remainingTimeForCurrentPeriod - 0.5f) * 30; + } } - private void initializeBreaks() + protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes) + { + base.OnNewBeat(beatIndex, timingPoint, effectPoint, amplitudes); + + if (currentPeriod.Value == null) + return; + + float timeBoxTargetWidth = (float)Math.Max(0, (remainingTimeForCurrentPeriod - timingPoint.BeatLength / currentPeriod.Value.Value.Duration)); + remainingTimeBox.ResizeWidthTo(timeBoxTargetWidth, timingPoint.BeatLength * 2, Easing.OutQuint); + } + + private void updateDisplay(ValueChangedEvent period) { FinishTransforms(true); Scheduler.CancelDelayedTasks(); - foreach (var b in breaks) + if (period.NewValue == null) + return; + + var b = period.NewValue.Value; + + using (BeginAbsoluteSequence(b.Start)) { - if (!b.HasEffect) - continue; + fadeContainer.FadeIn(BREAK_FADE_DURATION); + breakArrows.Show(BREAK_FADE_DURATION); - using (BeginAbsoluteSequence(b.StartTime)) + remainingTimeAdjustmentBox + .ResizeWidthTo(remaining_time_container_max_size, BREAK_FADE_DURATION, Easing.OutQuint) + .Delay(b.Duration - BREAK_FADE_DURATION) + .ResizeWidthTo(0); + + remainingTimeBox.ResizeWidthTo(remainingTimeForCurrentPeriod); + + remainingTimeCounter.CountTo(b.Duration).CountTo(0, b.Duration); + + using (BeginDelayedSequence(b.Duration - BREAK_FADE_DURATION)) { - fadeContainer.FadeIn(BREAK_FADE_DURATION); - breakArrows.Show(BREAK_FADE_DURATION); - - remainingTimeAdjustmentBox - .ResizeWidthTo(remaining_time_container_max_size, BREAK_FADE_DURATION, Easing.OutQuint) - .Delay(b.Duration - BREAK_FADE_DURATION) - .ResizeWidthTo(0); - - remainingTimeBox - .ResizeWidthTo(0, b.Duration - BREAK_FADE_DURATION) - .Then() - .ResizeWidthTo(1); - - remainingTimeCounter.CountTo(b.Duration).CountTo(0, b.Duration); - - using (BeginDelayedSequence(b.Duration - BREAK_FADE_DURATION)) - { - fadeContainer.FadeOut(BREAK_FADE_DURATION); - breakArrows.Hide(BREAK_FADE_DURATION); - } + fadeContainer.FadeOut(BREAK_FADE_DURATION); + breakArrows.Hide(BREAK_FADE_DURATION); } } } diff --git a/osu.Game/Screens/Play/BreakTracker.cs b/osu.Game/Screens/Play/BreakTracker.cs index 20ef1dc4bf..3c3f31053a 100644 --- a/osu.Game/Screens/Play/BreakTracker.cs +++ b/osu.Game/Screens/Play/BreakTracker.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.Collections.Generic; using System.Linq; using osu.Framework.Bindables; @@ -18,7 +16,7 @@ namespace osu.Game.Screens.Play private readonly ScoreProcessor scoreProcessor; private readonly double gameplayStartTime; - private PeriodTracker breaks; + private PeriodTracker breaks = new PeriodTracker(Enumerable.Empty()); /// /// Whether the gameplay is currently in a break. @@ -27,6 +25,8 @@ namespace osu.Game.Screens.Play private readonly BindableBool isBreakTime = new BindableBool(true); + public readonly Bindable CurrentPeriod = new Bindable(); + public IReadOnlyList Breaks { set @@ -39,7 +39,7 @@ namespace osu.Game.Screens.Play } } - public BreakTracker(double gameplayStartTime = 0, ScoreProcessor scoreProcessor = null) + public BreakTracker(double gameplayStartTime, ScoreProcessor scoreProcessor) { this.gameplayStartTime = gameplayStartTime; this.scoreProcessor = scoreProcessor; @@ -55,9 +55,16 @@ namespace osu.Game.Screens.Play { double time = Clock.CurrentTime; - isBreakTime.Value = breaks?.IsInAny(time) == true - || time < gameplayStartTime - || scoreProcessor?.HasCompleted.Value == true; + if (breaks.IsInAny(time, out var currentBreak)) + { + CurrentPeriod.Value = currentBreak; + isBreakTime.Value = true; + } + else + { + CurrentPeriod.Value = null; + isBreakTime.Value = time < gameplayStartTime || scoreProcessor.HasCompleted.Value; + } } } } diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 91bd0a676b..2a66c3d5d3 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -468,7 +468,7 @@ namespace osu.Game.Screens.Play { Clock = DrawableRuleset.FrameStableClock, ProcessCustomClock = false, - Breaks = working.Beatmap.Breaks + BreakTracker = breakTracker, }, // display the cursor above some HUD elements. DrawableRuleset.Cursor?.CreateProxy() ?? new Container(), diff --git a/osu.Game/Utils/PeriodTracker.cs b/osu.Game/Utils/PeriodTracker.cs index ba77702247..2c62684ac4 100644 --- a/osu.Game/Utils/PeriodTracker.cs +++ b/osu.Game/Utils/PeriodTracker.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; namespace osu.Game.Utils @@ -24,8 +25,17 @@ namespace osu.Game.Utils /// Whether the provided time is in any of the added periods. /// /// The time value to check. - public bool IsInAny(double time) + public bool IsInAny(double time) => IsInAny(time, out _); + + /// + /// Whether the provided time is in any of the added periods. + /// + /// The time value to check. + /// The period which matched. + public bool IsInAny(double time, [NotNullWhen(true)] out Period? period) { + period = null; + if (periods.Count == 0) return false; @@ -41,7 +51,15 @@ namespace osu.Game.Utils } var nearest = periods[nearestIndex]; - return time >= nearest.Start && time <= nearest.End; + bool isInAny = time >= nearest.Start && time <= nearest.End; + + if (isInAny) + { + period = nearest; + return true; + } + + return false; } } @@ -57,6 +75,8 @@ namespace osu.Game.Utils /// public readonly double End; + public double Duration => End - Start; + public Period(double start, double end) { if (start >= end) From 90d06d4496d727baf5da700906ad20ce7e2a66d9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 27 Aug 2024 16:37:27 +0900 Subject: [PATCH 2094/2493] Add slight parallax to centre content --- osu.Game/Screens/Play/BreakOverlay.cs | 58 +++++++++++++++------------ 1 file changed, 33 insertions(+), 25 deletions(-) diff --git a/osu.Game/Screens/Play/BreakOverlay.cs b/osu.Game/Screens/Play/BreakOverlay.cs index 7fc3dba3eb..fd2a3cc62f 100644 --- a/osu.Game/Screens/Play/BreakOverlay.cs +++ b/osu.Game/Screens/Play/BreakOverlay.cs @@ -89,33 +89,41 @@ namespace osu.Game.Screens.Play }, } }, - remainingTimeAdjustmentBox = new Container + new ParallaxContainer { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - AutoSizeAxes = Axes.Y, - RelativeSizeAxes = Axes.X, - Width = 0, - Child = remainingTimeBox = new Circle + RelativeSizeAxes = Axes.Both, + ParallaxAmount = -0.008f, + Children = new Drawable[] { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.X, - Height = 8, - Masking = true, - } - }, - remainingTimeCounter = new RemainingTimeCounter - { - Anchor = Anchor.Centre, - Origin = Anchor.BottomCentre, - Y = -vertical_margin, - }, - info = new BreakInfo - { - Anchor = Anchor.Centre, - Origin = Anchor.TopCentre, - Y = vertical_margin, + remainingTimeAdjustmentBox = new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + AutoSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.X, + Width = 0, + Child = remainingTimeBox = new Circle + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.X, + Height = 8, + Masking = true, + } + }, + remainingTimeCounter = new RemainingTimeCounter + { + Anchor = Anchor.Centre, + Origin = Anchor.BottomCentre, + Y = -vertical_margin, + }, + info = new BreakInfo + { + Anchor = Anchor.Centre, + Origin = Anchor.TopCentre, + Y = vertical_margin, + }, + }, }, breakArrows = new BreakArrows { From e59689f31a26ff0896c5d3f8e46f2fd4598c8974 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 27 Aug 2024 09:49:49 +0200 Subject: [PATCH 2095/2493] Fix test and NRT failure --- osu.Game.Tests/Visual/Gameplay/TestSceneBreakTracker.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneBreakTracker.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneBreakTracker.cs index ea21262fc0..ba8f9971ba 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneBreakTracker.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneBreakTracker.cs @@ -10,6 +10,8 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Shapes; using osu.Framework.Timing; using osu.Game.Beatmaps.Timing; +using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.Scoring; using osu.Game.Screens.Play; using osuTK.Graphics; @@ -38,7 +40,7 @@ namespace osu.Game.Tests.Visual.Gameplay RelativeSizeAxes = Axes.Both, }, breakTracker = new TestBreakTracker(), - breakOverlay = new BreakOverlay(true, null) + breakOverlay = new BreakOverlay(true, new ScoreProcessor(new OsuRuleset())) { ProcessCustomClock = false, } From ed45c947fca29fd07439e1004ce17ec3069f9021 Mon Sep 17 00:00:00 2001 From: StanR Date: Tue, 27 Aug 2024 15:50:08 +0500 Subject: [PATCH 2096/2493] Adjust max history by clockrate to make rhythm calculation more consistent between rates --- .../Difficulty/Evaluators/RhythmEvaluator.cs | 28 ++++++++++--------- .../Difficulty/OsuDifficultyCalculator.cs | 2 +- .../Difficulty/Skills/Speed.cs | 6 ++-- 3 files changed, 20 insertions(+), 16 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs index 22b330bcac..26f50efc72 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs @@ -60,12 +60,13 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators } private const int history_time_max = 5 * 1000; // 5 seconds of calculatingRhythmBonus max. - private const double rhythm_multiplier = 0.95; + private const int history_objects_max = 32; + private const double rhythm_multiplier = 1.25; /// /// Calculates a rhythm multiplier for the difficulty of the tap associated with historic data of the current . /// - public static double EvaluateDifficultyOf(DifficultyHitObject current) + public static double EvaluateDifficultyOf(DifficultyHitObject current, double clockRate) { if (current.BaseObject is Spinner) return 0; @@ -76,15 +77,18 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators var previousIsland = new Island(); Dictionary islandCounts = new Dictionary(); + int historyTimeMaxAdjusted = (int)Math.Ceiling(history_time_max / clockRate); + int historyObjectsMaxAdjusted = (int)Math.Ceiling(history_objects_max / clockRate); + double startRatio = 0; // store the ratio of the current start of an island to buff for tighter rhythms bool firstDeltaSwitch = false; - int historicalNoteCount = Math.Min(current.Index, 32); + int historicalNoteCount = Math.Min(current.Index, historyObjectsMaxAdjusted); int rhythmStart = 0; - while (rhythmStart < historicalNoteCount - 2 && current.StartTime - current.Previous(rhythmStart).StartTime < history_time_max) + while (rhythmStart < historicalNoteCount - 2 && current.StartTime - current.Previous(rhythmStart).StartTime < historyTimeMaxAdjusted) rhythmStart++; OsuDifficultyHitObject prevObj = (OsuDifficultyHitObject)current.Previous(rhythmStart); @@ -94,7 +98,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators { OsuDifficultyHitObject currObj = (OsuDifficultyHitObject)current.Previous(i - 1); - double currHistoricalDecay = (history_time_max - (current.StartTime - currObj.StartTime)) / history_time_max; // scales note 0 to 1 from history to now + double currHistoricalDecay = (historyTimeMaxAdjusted - (current.StartTime - currObj.StartTime)) / historyTimeMaxAdjusted; // scales note 0 to 1 from history to now currHistoricalDecay = Math.Min((double)(historicalNoteCount - i) / historicalNoteCount, currHistoricalDecay); // either we're limited by time or limited by object count. @@ -102,16 +106,14 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators double prevDelta = prevObj.StrainTime; double lastDelta = lastObj.StrainTime; - double currRatio = 1.0 + 9.8 * Math.Min(0.5, Math.Pow(Math.Sin(Math.PI / (Math.Min(prevDelta, currDelta) / Math.Max(prevDelta, currDelta))), 2)); // fancy function to calculate rhythmbonuses. - - double windowPenalty = Math.Min(1, Math.Max(0, Math.Abs(prevDelta - currDelta) - currObj.HitWindowGreat * 0.3) / (currObj.HitWindowGreat * 0.3)); - - windowPenalty = Math.Min(1, windowPenalty); - - double effectiveRatio = windowPenalty * currRatio; + double currRatio = 1.0 + 7.27 * Math.Min(0.5, Math.Pow(Math.Sin(Math.PI / (Math.Min(prevDelta, currDelta) / Math.Max(prevDelta, currDelta))), 2)); // fancy function to calculate rhythmbonuses. double deltaDifferenceEpsilon = currObj.HitWindowGreat * 0.3; + double windowPenalty = Math.Min(1, Math.Max(0, Math.Abs(prevDelta - currDelta) - deltaDifferenceEpsilon) / deltaDifferenceEpsilon); + + double effectiveRatio = windowPenalty * currRatio; + if (firstDeltaSwitch) { if (!(Math.Abs(prevDelta - currDelta) > deltaDifferenceEpsilon)) @@ -145,7 +147,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators // repeated island (ex: triplet -> triplet) double power = logistic(island.AverageDelta(), 4, 0.165, 10); - effectiveRatio *= Math.Min(2.0 / islandCounts[island], Math.Pow(1.0 / islandCounts[island], power)); + effectiveRatio *= Math.Min(3.0 / islandCounts[island], Math.Pow(1.0 / islandCounts[island], power)); } // scale down the difficulty if the object is doubletappable diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs index 007cd977e5..a5ef0764bb 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs @@ -134,7 +134,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty { new Aim(mods, true), new Aim(mods, false), - new Speed(mods) + new Speed(mods, clockRate) }; if (mods.Any(h => h is OsuModFlashlight)) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs index 40aac013ab..ae85980815 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs @@ -16,6 +16,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills /// public class Speed : OsuStrainSkill { + private readonly double clockRate; private double skillMultiplier => 1375; private double strainDecayBase => 0.3; @@ -27,9 +28,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills private readonly List objectStrains = new List(); - public Speed(Mod[] mods) + public Speed(Mod[] mods, double clockRate) : base(mods) { + this.clockRate = clockRate; } private double strainDecay(double ms) => Math.Pow(strainDecayBase, ms / 1000); @@ -41,7 +43,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills currentStrain *= strainDecay(((OsuDifficultyHitObject)current).StrainTime); currentStrain += SpeedEvaluator.EvaluateDifficultyOf(current) * skillMultiplier; - currentRhythm = RhythmEvaluator.EvaluateDifficultyOf(current); + currentRhythm = RhythmEvaluator.EvaluateDifficultyOf(current, clockRate); double totalStrain = currentStrain * currentRhythm; From 71044a0766fdfe39274de1ea3c6babb8dfb78a39 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Tue, 27 Aug 2024 19:02:40 +0200 Subject: [PATCH 2097/2493] fix difference in sample time calculation --- osu.Game/Screens/Edit/Editor.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 6b8ea7e97e..9bb91af806 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -1131,7 +1131,7 @@ namespace osu.Game.Screens.Edit for (int i = 0; i < r.SpanCount(); i++) { - nodeSamplePointTimes[i + 2] = current.StartTime + r.Duration / r.SpanCount() * (i + 1); + nodeSamplePointTimes[i + 2] = current.StartTime + r.Duration * (i + 1) / r.SpanCount(); } double found = direction < 1 From daad4765938f5f3bd04148706af65e35e47620ce Mon Sep 17 00:00:00 2001 From: OliBomby Date: Tue, 27 Aug 2024 19:04:16 +0200 Subject: [PATCH 2098/2493] Add float comparison leniency just in case --- .../Edit/Compose/Components/Timeline/SamplePointPiece.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs index 6cd7044943..121cc0a301 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs @@ -14,6 +14,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; +using osu.Framework.Utils; using osu.Game.Audio; using osu.Game.Graphics; using osu.Game.Graphics.UserInterface; @@ -71,7 +72,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline private void onShowSampleEditPopoverRequested(double time) { - if (time == GetTime()) + if (Precision.AlmostEquals(time, GetTime())) this.ShowPopover(); } From 1117fd56a10c3b93a11f572d49b99e9533669f07 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Tue, 27 Aug 2024 19:40:18 +0200 Subject: [PATCH 2099/2493] change default seek hotkeys --- osu.Game/Input/Bindings/GlobalActionContainer.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Input/Bindings/GlobalActionContainer.cs b/osu.Game/Input/Bindings/GlobalActionContainer.cs index 542073476f..27d026ac9c 100644 --- a/osu.Game/Input/Bindings/GlobalActionContainer.cs +++ b/osu.Game/Input/Bindings/GlobalActionContainer.cs @@ -147,10 +147,10 @@ namespace osu.Game.Input.Bindings new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.MouseWheelLeft }, GlobalAction.EditorCycleNextBeatSnapDivisor), new KeyBinding(new[] { InputKey.Control, InputKey.R }, GlobalAction.EditorToggleRotateControl), new KeyBinding(new[] { InputKey.Control, InputKey.E }, GlobalAction.EditorToggleScaleControl), - new KeyBinding(new[] { InputKey.Alt, InputKey.Left }, GlobalAction.EditorSeekToPreviousHitObject), - new KeyBinding(new[] { InputKey.Alt, InputKey.Right }, GlobalAction.EditorSeekToNextHitObject), - new KeyBinding(new[] { InputKey.Alt, InputKey.Shift, InputKey.Left }, GlobalAction.EditorSeekToPreviousSamplePoint), - new KeyBinding(new[] { InputKey.Alt, InputKey.Shift, InputKey.Right }, GlobalAction.EditorSeekToNextSamplePoint), + new KeyBinding(new[] { InputKey.Control, InputKey.Left }, GlobalAction.EditorSeekToPreviousHitObject), + new KeyBinding(new[] { InputKey.Control, InputKey.Right }, GlobalAction.EditorSeekToNextHitObject), + new KeyBinding(new[] { InputKey.Alt, InputKey.Left }, GlobalAction.EditorSeekToPreviousSamplePoint), + new KeyBinding(new[] { InputKey.Alt, InputKey.Right }, GlobalAction.EditorSeekToNextSamplePoint), }; private static IEnumerable editorTestPlayKeyBindings => new[] From b5b4f915a94b19c40b1d2fae32954a0dcbd0047c Mon Sep 17 00:00:00 2001 From: OliBomby Date: Tue, 27 Aug 2024 19:40:33 +0200 Subject: [PATCH 2100/2493] Automatic seek to sample point on right-click --- .../Edit/Compose/Components/Timeline/SamplePointPiece.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs index 121cc0a301..488cd288e4 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs @@ -78,11 +78,18 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline protected override bool OnClick(ClickEvent e) { - editorClock?.SeekSmoothlyTo(GetTime()); this.ShowPopover(); return true; } + protected override void OnMouseUp(MouseUpEvent e) + { + if (e.Button != MouseButton.Right) return; + + editorClock?.SeekSmoothlyTo(GetTime()); + this.ShowPopover(); + } + private void updateText() { Label.Text = $"{abbreviateBank(GetBankValue(GetSamples()))} {GetVolumeValue(GetSamples())}"; From 7fda8bc95b8092124820306cab26561e66cda8fe Mon Sep 17 00:00:00 2001 From: StanR Date: Tue, 27 Aug 2024 23:48:15 +0500 Subject: [PATCH 2101/2493] Reduce repetition nerf for non-consecutive islands --- .../Difficulty/Evaluators/RhythmEvaluator.cs | 37 +++++++++++-------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs index 26f50efc72..9e851f11a6 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs @@ -14,19 +14,24 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators { private readonly struct Island : IEquatable { - public Island() + private readonly double deltaDifferenceEpsilon; + + public Island(double epsilon) { + deltaDifferenceEpsilon = epsilon; } public Island(int firstDelta, double epsilon) { - AddDelta(firstDelta, epsilon); + deltaDifferenceEpsilon = epsilon; + AddDelta(firstDelta); } public List Deltas { get; } = new List(); - public void AddDelta(int delta, double epsilon) + public void AddDelta(int delta) { + double epsilon = deltaDifferenceEpsilon; int existingDelta = Deltas.FirstOrDefault(x => Math.Abs(x - delta) >= epsilon); Deltas.Add(existingDelta == default ? delta : existingDelta); @@ -34,10 +39,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators public double AverageDelta() => Deltas.Count > 0 ? Math.Max(Deltas.Average(), OsuDifficultyHitObject.MIN_DELTA_TIME) : 0; - public bool IsSimilarPolarity(Island other, double epsilon) + public bool IsSimilarPolarity(Island other) { // consider islands to be of similar polarity only if they're having the same average delta (we don't want to consider 3 singletaps similar to a triple) - return Math.Abs(AverageDelta() - other.AverageDelta()) < epsilon && + return Math.Abs(AverageDelta() - other.AverageDelta()) < deltaDifferenceEpsilon && Deltas.Count % 2 == other.Deltas.Count % 2; } @@ -61,7 +66,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators private const int history_time_max = 5 * 1000; // 5 seconds of calculatingRhythmBonus max. private const int history_objects_max = 32; - private const double rhythm_multiplier = 1.25; + private const double rhythm_multiplier = 1.2; /// /// Calculates a rhythm multiplier for the difficulty of the tap associated with historic data of the current . @@ -73,8 +78,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators double rhythmComplexitySum = 0; - var island = new Island(); - var previousIsland = new Island(); + double deltaDifferenceEpsilon = ((OsuDifficultyHitObject)current).HitWindowGreat * 0.3; + + var island = new Island(deltaDifferenceEpsilon); + var previousIsland = new Island(deltaDifferenceEpsilon); Dictionary islandCounts = new Dictionary(); int historyTimeMaxAdjusted = (int)Math.Ceiling(history_time_max / clockRate); @@ -106,9 +113,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators double prevDelta = prevObj.StrainTime; double lastDelta = lastObj.StrainTime; - double currRatio = 1.0 + 7.27 * Math.Min(0.5, Math.Pow(Math.Sin(Math.PI / (Math.Min(prevDelta, currDelta) / Math.Max(prevDelta, currDelta))), 2)); // fancy function to calculate rhythmbonuses. - - double deltaDifferenceEpsilon = currObj.HitWindowGreat * 0.3; + double currRatio = 1.0 + 5.8 * Math.Min(0.5, Math.Pow(Math.Sin(Math.PI / (Math.Min(prevDelta, currDelta) / Math.Max(prevDelta, currDelta))), 2)); // fancy function to calculate rhythmbonuses. double windowPenalty = Math.Min(1, Math.Max(0, Math.Abs(prevDelta - currDelta) - deltaDifferenceEpsilon) / deltaDifferenceEpsilon); @@ -119,7 +124,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators if (!(Math.Abs(prevDelta - currDelta) > deltaDifferenceEpsilon)) { // island is still progressing - island.AddDelta((int)currDelta, deltaDifferenceEpsilon); + island.AddDelta((int)currDelta); } else { @@ -131,10 +136,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators // unintentional side effect is that bursts with kicksliders at the ends might have lower difficulty than bursts without sliders // therefore we're checking for quick sliders and don't lower the difficulty for them since they don't really make tapping easier (no time to adjust) if (prevObj.BaseObject is Slider && prevObj.TravelTime > prevDelta * 1.5) - effectiveRatio *= 0.15; + effectiveRatio *= 0.2; // repeated island polartiy (2 -> 4, 3 -> 5) - if (island.IsSimilarPolarity(previousIsland, deltaDifferenceEpsilon)) + if (island.IsSimilarPolarity(previousIsland)) effectiveRatio *= 0.3; // previous increase happened a note ago, 1/1->1/2-1/4, dont want to buff this. @@ -143,7 +148,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators if (!islandCounts.TryAdd(island, 1)) { - islandCounts[island]++; + // only add island to island counts if they're going one after another + if (previousIsland.Equals(island)) + islandCounts[island]++; // repeated island (ex: triplet -> triplet) double power = logistic(island.AverageDelta(), 4, 0.165, 10); From 47a52d10ebb488b3d9e8feaee29d113a548bd916 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 28 Aug 2024 15:32:59 +0900 Subject: [PATCH 2102/2493] Revert "Add slight parallax to centre content" This reverts commit 90d06d4496d727baf5da700906ad20ce7e2a66d9. --- osu.Game/Screens/Play/BreakOverlay.cs | 58 ++++++++++++--------------- 1 file changed, 25 insertions(+), 33 deletions(-) diff --git a/osu.Game/Screens/Play/BreakOverlay.cs b/osu.Game/Screens/Play/BreakOverlay.cs index fd2a3cc62f..7fc3dba3eb 100644 --- a/osu.Game/Screens/Play/BreakOverlay.cs +++ b/osu.Game/Screens/Play/BreakOverlay.cs @@ -89,41 +89,33 @@ namespace osu.Game.Screens.Play }, } }, - new ParallaxContainer + remainingTimeAdjustmentBox = new Container { - RelativeSizeAxes = Axes.Both, - ParallaxAmount = -0.008f, - Children = new Drawable[] + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + AutoSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.X, + Width = 0, + Child = remainingTimeBox = new Circle { - remainingTimeAdjustmentBox = new Container - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - AutoSizeAxes = Axes.Y, - RelativeSizeAxes = Axes.X, - Width = 0, - Child = remainingTimeBox = new Circle - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.X, - Height = 8, - Masking = true, - } - }, - remainingTimeCounter = new RemainingTimeCounter - { - Anchor = Anchor.Centre, - Origin = Anchor.BottomCentre, - Y = -vertical_margin, - }, - info = new BreakInfo - { - Anchor = Anchor.Centre, - Origin = Anchor.TopCentre, - Y = vertical_margin, - }, - }, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.X, + Height = 8, + Masking = true, + } + }, + remainingTimeCounter = new RemainingTimeCounter + { + Anchor = Anchor.Centre, + Origin = Anchor.BottomCentre, + Y = -vertical_margin, + }, + info = new BreakInfo + { + Anchor = Anchor.Centre, + Origin = Anchor.TopCentre, + Y = vertical_margin, }, breakArrows = new BreakArrows { From eb70a1b72d9d9476fb3a151a4af102fefd3d1593 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 28 Aug 2024 15:57:42 +0900 Subject: [PATCH 2103/2493] Change middle text to only animate initially --- osu.Game/Screens/Play/BreakOverlay.cs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/Play/BreakOverlay.cs b/osu.Game/Screens/Play/BreakOverlay.cs index 7fc3dba3eb..7f9e879b44 100644 --- a/osu.Game/Screens/Play/BreakOverlay.cs +++ b/osu.Game/Screens/Play/BreakOverlay.cs @@ -144,12 +144,7 @@ namespace osu.Game.Screens.Play { base.Update(); - if (currentPeriod.Value != null) - { - remainingTimeBox.Height = Math.Min(8, remainingTimeBox.DrawWidth); - remainingTimeCounter.X = -(remainingTimeForCurrentPeriod - 0.5f) * 30; - info.X = (remainingTimeForCurrentPeriod - 0.5f) * 30; - } + remainingTimeBox.Height = Math.Min(8, remainingTimeBox.DrawWidth); } protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes) @@ -187,6 +182,12 @@ namespace osu.Game.Screens.Play remainingTimeCounter.CountTo(b.Duration).CountTo(0, b.Duration); + remainingTimeCounter.MoveToX(-50) + .MoveToX(0, BREAK_FADE_DURATION, Easing.OutQuint); + + info.MoveToX(50) + .MoveToX(0, BREAK_FADE_DURATION, Easing.OutQuint); + using (BeginDelayedSequence(b.Duration - BREAK_FADE_DURATION)) { fadeContainer.FadeOut(BREAK_FADE_DURATION); From 466ed5de785e2f2f70a5077bdb8f1d527aad788d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 27 Aug 2024 17:37:15 +0900 Subject: [PATCH 2104/2493] Add basic detached beatmap store --- osu.Game/Database/DetachedBeatmapStore.cs | 117 +++++++++++++++++++++ osu.Game/OsuGame.cs | 1 + osu.Game/Screens/Select/BeatmapCarousel.cs | 92 ++-------------- 3 files changed, 129 insertions(+), 81 deletions(-) create mode 100644 osu.Game/Database/DetachedBeatmapStore.cs diff --git a/osu.Game/Database/DetachedBeatmapStore.cs b/osu.Game/Database/DetachedBeatmapStore.cs new file mode 100644 index 0000000000..0acc38a5a1 --- /dev/null +++ b/osu.Game/Database/DetachedBeatmapStore.cs @@ -0,0 +1,117 @@ +// 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; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Logging; +using osu.Game.Beatmaps; +using Realms; + +namespace osu.Game.Database +{ + // TODO: handle realm migration + public partial class DetachedBeatmapStore : Component + { + private readonly ManualResetEventSlim loaded = new ManualResetEventSlim(); + + private List originalBeatmapSetsDetached = new List(); + + private IDisposable? subscriptionSets; + + /// + /// Track GUIDs of all sets in realm to allow handling deletions. + /// + private readonly List realmBeatmapSets = new List(); + + [Resolved] + private RealmAccess realm { get; set; } = null!; + + public IReadOnlyList GetDetachedBeatmaps() + { + if (!loaded.Wait(60000)) + Logger.Error(new TimeoutException("Beatmaps did not load in an acceptable time"), $"{nameof(DetachedBeatmapStore)} fell over"); + + return originalBeatmapSetsDetached; + } + + [BackgroundDependencyLoader] + private void load() + { + subscriptionSets = realm.RegisterForNotifications(getBeatmapSets, beatmapSetsChanged); + } + + private void beatmapSetsChanged(IRealmCollection sender, ChangeSet? changes) + { + if (changes == null) + { + if (originalBeatmapSetsDetached.Count > 0 && sender.Count == 0) + { + // Usually we'd reset stuff here, but doing so triggers a silly flow which ends up deadlocking realm. + // Additionally, user should not be at song select when realm is blocking all operations in the first place. + // + // Note that due to the catch-up logic below, once operations are restored we will still be in a roughly + // correct state. The only things that this return will change is the carousel will not empty *during* the blocking + // operation. + return; + } + + originalBeatmapSetsDetached = sender.Detach(); + + realmBeatmapSets.Clear(); + realmBeatmapSets.AddRange(sender.Select(r => r.ID)); + + loaded.Set(); + return; + } + + HashSet setsRequiringUpdate = new HashSet(); + HashSet setsRequiringRemoval = new HashSet(); + + foreach (int i in changes.DeletedIndices.OrderDescending()) + { + Guid id = realmBeatmapSets[i]; + + setsRequiringRemoval.Add(id); + setsRequiringUpdate.Remove(id); + + realmBeatmapSets.RemoveAt(i); + } + + foreach (int i in changes.InsertedIndices) + { + Guid id = sender[i].ID; + + setsRequiringRemoval.Remove(id); + setsRequiringUpdate.Add(id); + + realmBeatmapSets.Insert(i, id); + } + + foreach (int i in changes.NewModifiedIndices) + setsRequiringUpdate.Add(sender[i].ID); + + // deletions + foreach (Guid g in setsRequiringRemoval) + originalBeatmapSetsDetached.RemoveAll(set => set.ID == g); + + // updates + foreach (Guid g in setsRequiringUpdate) + { + originalBeatmapSetsDetached.RemoveAll(set => set.ID == g); + originalBeatmapSetsDetached.Add(fetchFromID(g)!); + } + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + subscriptionSets?.Dispose(); + } + + private IQueryable getBeatmapSets(Realm realm) => realm.All().Where(s => !s.DeletePending && !s.Protected); + } +} diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 089db3b698..0ef6a94679 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -1141,6 +1141,7 @@ namespace osu.Game loadComponentSingleFile(new MedalOverlay(), topMostOverlayContent.Add); loadComponentSingleFile(new BackgroundDataStoreProcessor(), Add); + loadComponentSingleFile(new DetachedBeatmapStore(), Add, true); Add(difficultyRecommender); Add(externalLinkOpener = new ExternalLinkOpener()); diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index b0f198d486..d06023258a 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -76,8 +76,6 @@ namespace osu.Game.Screens.Select private CarouselBeatmapSet? selectedBeatmapSet; - private List originalBeatmapSetsDetached = new List(); - /// /// Raised when the is changed. /// @@ -109,6 +107,9 @@ namespace osu.Game.Screens.Select [Cached] protected readonly CarouselScrollContainer Scroll; + [Resolved] + private DetachedBeatmapStore detachedBeatmapStore { get; set; } = null!; + private readonly NoResultsPlaceholder noResultsPlaceholder; private IEnumerable beatmapSets => root.Items.OfType(); @@ -128,9 +129,7 @@ namespace osu.Game.Screens.Select private void loadBeatmapSets(IEnumerable beatmapSets) { - originalBeatmapSetsDetached = beatmapSets.Detach(); - - if (selectedBeatmapSet != null && !originalBeatmapSetsDetached.Contains(selectedBeatmapSet.BeatmapSet)) + if (selectedBeatmapSet != null && !beatmapSets.Contains(selectedBeatmapSet.BeatmapSet)) selectedBeatmapSet = null; var selectedBeatmapBefore = selectedBeatmap?.BeatmapInfo; @@ -139,7 +138,7 @@ namespace osu.Game.Screens.Select if (beatmapsSplitOut) { - var carouselBeatmapSets = originalBeatmapSetsDetached.SelectMany(s => s.Beatmaps).Select(b => + var carouselBeatmapSets = beatmapSets.SelectMany(s => s.Beatmaps).Select(b => { return createCarouselSet(new BeatmapSetInfo(new[] { b }) { @@ -153,7 +152,7 @@ namespace osu.Game.Screens.Select } else { - var carouselBeatmapSets = originalBeatmapSetsDetached.Select(createCarouselSet).OfType(); + var carouselBeatmapSets = beatmapSets.Select(createCarouselSet).OfType(); newRoot.AddItems(carouselBeatmapSets); } @@ -230,7 +229,7 @@ namespace osu.Game.Screens.Select } [BackgroundDependencyLoader] - private void load(OsuConfigManager config, AudioManager audio) + private void load(OsuConfigManager config, AudioManager audio, DetachedBeatmapStore beatmapStore) { spinSample = audio.Samples.Get("SongSelect/random-spin"); randomSelectSample = audio.Samples.Get(@"SongSelect/select-random"); @@ -246,18 +245,13 @@ namespace osu.Game.Screens.Select // This is performing an unnecessary second lookup on realm (in addition to the subscription), but for performance reasons // we require it to be separate: the subscription's initial callback (with `ChangeSet` of `null`) will run on the update // thread. If we attempt to detach beatmaps in this callback the game will fall over (it takes time). - realm.Run(r => loadBeatmapSets(getBeatmapSets(r))); + loadBeatmapSets(detachedBeatmapStore.GetDetachedBeatmaps()); } } [Resolved] private RealmAccess realm { get; set; } = null!; - /// - /// Track GUIDs of all sets in realm to allow handling deletions. - /// - private readonly List realmBeatmapSets = new List(); - protected override void LoadComplete() { base.LoadComplete(); @@ -266,6 +260,8 @@ namespace osu.Game.Screens.Select subscriptionBeatmaps = realm.RegisterForNotifications(r => r.All().Where(b => !b.Hidden), beatmapsChanged); } + private IQueryable getBeatmapSets(Realm realm) => realm.All().Where(s => !s.DeletePending && !s.Protected); + private readonly HashSet setsRequiringUpdate = new HashSet(); private readonly HashSet setsRequiringRemoval = new HashSet(); @@ -275,65 +271,6 @@ namespace osu.Game.Screens.Select if (loadedTestBeatmaps) return; - if (changes == null) - { - realmBeatmapSets.Clear(); - realmBeatmapSets.AddRange(sender.Select(r => r.ID)); - - if (originalBeatmapSetsDetached.Count > 0 && sender.Count == 0) - { - // Usually we'd reset stuff here, but doing so triggers a silly flow which ends up deadlocking realm. - // Additionally, user should not be at song select when realm is blocking all operations in the first place. - // - // Note that due to the catch-up logic below, once operations are restored we will still be in a roughly - // correct state. The only things that this return will change is the carousel will not empty *during* the blocking - // operation. - return; - } - - // Do a full two-way check for missing (or incorrectly present) beatmaps. - // Let's assume that the worst that can happen is deletions or additions. - setsRequiringRemoval.Clear(); - setsRequiringUpdate.Clear(); - - foreach (Guid id in realmBeatmapSets) - { - if (!root.BeatmapSetsByID.ContainsKey(id)) - setsRequiringUpdate.Add(id); - } - - foreach (Guid id in root.BeatmapSetsByID.Keys) - { - if (!realmBeatmapSets.Contains(id)) - setsRequiringRemoval.Add(id); - } - } - else - { - foreach (int i in changes.DeletedIndices.OrderDescending()) - { - Guid id = realmBeatmapSets[i]; - - setsRequiringRemoval.Add(id); - setsRequiringUpdate.Remove(id); - - realmBeatmapSets.RemoveAt(i); - } - - foreach (int i in changes.InsertedIndices) - { - Guid id = sender[i].ID; - - setsRequiringRemoval.Remove(id); - setsRequiringUpdate.Add(id); - - realmBeatmapSets.Insert(i, id); - } - - foreach (int i in changes.NewModifiedIndices) - setsRequiringUpdate.Add(sender[i].ID); - } - Scheduler.AddOnce(processBeatmapChanges); } @@ -425,8 +362,6 @@ namespace osu.Game.Screens.Select invalidateAfterChange(); } - private IQueryable getBeatmapSets(Realm realm) => realm.All().Where(s => !s.DeletePending && !s.Protected); - public void RemoveBeatmapSet(BeatmapSetInfo beatmapSet) => Schedule(() => { removeBeatmapSet(beatmapSet.ID); @@ -438,8 +373,6 @@ namespace osu.Game.Screens.Select if (!root.BeatmapSetsByID.TryGetValue(beatmapSetID, out var existingSets)) return; - originalBeatmapSetsDetached.RemoveAll(set => set.ID == beatmapSetID); - foreach (var set in existingSets) { foreach (var beatmap in set.Beatmaps) @@ -465,9 +398,6 @@ namespace osu.Game.Screens.Select { beatmapSet = beatmapSet.Detach(); - originalBeatmapSetsDetached.RemoveAll(set => set.ID == beatmapSet.ID); - originalBeatmapSetsDetached.Add(beatmapSet); - var newSets = new List(); if (beatmapsSplitOut) @@ -766,7 +696,7 @@ namespace osu.Game.Screens.Select if (activeCriteria.SplitOutDifficulties != beatmapsSplitOut) { beatmapsSplitOut = activeCriteria.SplitOutDifficulties; - loadBeatmapSets(originalBeatmapSetsDetached); + loadBeatmapSets(detachedBeatmapStore.GetDetachedBeatmaps()); return; } From 4d42274771b770c4cb36e05a0611a2e20a4db324 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 27 Aug 2024 18:13:52 +0900 Subject: [PATCH 2105/2493] Use bindable list implementation --- osu.Game/Database/DetachedBeatmapStore.cs | 60 +++--------- osu.Game/Screens/Select/BeatmapCarousel.cs | 101 +++++++++++++-------- 2 files changed, 78 insertions(+), 83 deletions(-) diff --git a/osu.Game/Database/DetachedBeatmapStore.cs b/osu.Game/Database/DetachedBeatmapStore.cs index 0acc38a5a1..ff81784745 100644 --- a/osu.Game/Database/DetachedBeatmapStore.cs +++ b/osu.Game/Database/DetachedBeatmapStore.cs @@ -2,10 +2,10 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Collections.Generic; using System.Linq; using System.Threading; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Logging; using osu.Game.Beatmaps; @@ -13,42 +13,36 @@ using Realms; namespace osu.Game.Database { - // TODO: handle realm migration public partial class DetachedBeatmapStore : Component { private readonly ManualResetEventSlim loaded = new ManualResetEventSlim(); - private List originalBeatmapSetsDetached = new List(); + private readonly BindableList detachedBeatmapSets = new BindableList(); - private IDisposable? subscriptionSets; - - /// - /// Track GUIDs of all sets in realm to allow handling deletions. - /// - private readonly List realmBeatmapSets = new List(); + private IDisposable? realmSubscription; [Resolved] private RealmAccess realm { get; set; } = null!; - public IReadOnlyList GetDetachedBeatmaps() + public IBindableList GetDetachedBeatmaps() { if (!loaded.Wait(60000)) Logger.Error(new TimeoutException("Beatmaps did not load in an acceptable time"), $"{nameof(DetachedBeatmapStore)} fell over"); - return originalBeatmapSetsDetached; + return detachedBeatmapSets.GetBoundCopy(); } [BackgroundDependencyLoader] private void load() { - subscriptionSets = realm.RegisterForNotifications(getBeatmapSets, beatmapSetsChanged); + realmSubscription = realm.RegisterForNotifications(getBeatmapSets, beatmapSetsChanged); } private void beatmapSetsChanged(IRealmCollection sender, ChangeSet? changes) { if (changes == null) { - if (originalBeatmapSetsDetached.Count > 0 && sender.Count == 0) + if (detachedBeatmapSets.Count > 0 && sender.Count == 0) { // Usually we'd reset stuff here, but doing so triggers a silly flow which ends up deadlocking realm. // Additionally, user should not be at song select when realm is blocking all operations in the first place. @@ -59,57 +53,29 @@ namespace osu.Game.Database return; } - originalBeatmapSetsDetached = sender.Detach(); - - realmBeatmapSets.Clear(); - realmBeatmapSets.AddRange(sender.Select(r => r.ID)); + detachedBeatmapSets.Clear(); + detachedBeatmapSets.AddRange(sender.Detach()); loaded.Set(); return; } - HashSet setsRequiringUpdate = new HashSet(); - HashSet setsRequiringRemoval = new HashSet(); - foreach (int i in changes.DeletedIndices.OrderDescending()) - { - Guid id = realmBeatmapSets[i]; - - setsRequiringRemoval.Add(id); - setsRequiringUpdate.Remove(id); - - realmBeatmapSets.RemoveAt(i); - } + detachedBeatmapSets.RemoveAt(i); foreach (int i in changes.InsertedIndices) { - Guid id = sender[i].ID; - - setsRequiringRemoval.Remove(id); - setsRequiringUpdate.Add(id); - - realmBeatmapSets.Insert(i, id); + detachedBeatmapSets.Insert(i, sender[i].Detach()); } foreach (int i in changes.NewModifiedIndices) - setsRequiringUpdate.Add(sender[i].ID); - - // deletions - foreach (Guid g in setsRequiringRemoval) - originalBeatmapSetsDetached.RemoveAll(set => set.ID == g); - - // updates - foreach (Guid g in setsRequiringUpdate) - { - originalBeatmapSetsDetached.RemoveAll(set => set.ID == g); - originalBeatmapSetsDetached.Add(fetchFromID(g)!); - } + detachedBeatmapSets.ReplaceRange(i, 1, new[] { sender[i].Detach() }); } protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); - subscriptionSets?.Dispose(); + realmSubscription?.Dispose(); } private IQueryable getBeatmapSets(Realm realm) => realm.All().Where(s => !s.DeletePending && !s.Protected); diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index d06023258a..118ea45e45 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Collections.Specialized; using System.Diagnostics; using System.Linq; using osu.Framework.Allocation; @@ -21,6 +22,7 @@ using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.Database; +using osu.Game.Extensions; using osu.Game.Graphics.Containers; using osu.Game.Input.Bindings; using osu.Game.Screens.Select.Carousel; @@ -108,7 +110,12 @@ namespace osu.Game.Screens.Select protected readonly CarouselScrollContainer Scroll; [Resolved] - private DetachedBeatmapStore detachedBeatmapStore { get; set; } = null!; + private RealmAccess realm { get; set; } = null!; + + [Resolved] + private DetachedBeatmapStore? detachedBeatmapStore { get; set; } + + private IBindableList detachedBeatmapSets = null!; private readonly NoResultsPlaceholder noResultsPlaceholder; @@ -165,12 +172,6 @@ namespace osu.Game.Screens.Select applyActiveCriteria(false); - if (loadedTestBeatmaps) - { - invalidateAfterChange(); - BeatmapSetsLoaded = true; - } - // Restore selection if (selectedBeatmapBefore != null && newRoot.BeatmapSetsByID.TryGetValue(selectedBeatmapBefore.BeatmapSet!.ID, out var newSelectionCandidates)) { @@ -179,6 +180,12 @@ namespace osu.Game.Screens.Select if (found != null) found.State.Value = CarouselItemState.Selected; } + + Schedule(() => + { + invalidateAfterChange(); + BeatmapSetsLoaded = true; + }); } private readonly List visibleItems = new List(); @@ -194,7 +201,6 @@ namespace osu.Game.Screens.Select private CarouselRoot root; - private IDisposable? subscriptionSets; private IDisposable? subscriptionBeatmaps; private readonly DrawablePool setPool = new DrawablePool(100); @@ -245,32 +251,62 @@ namespace osu.Game.Screens.Select // This is performing an unnecessary second lookup on realm (in addition to the subscription), but for performance reasons // we require it to be separate: the subscription's initial callback (with `ChangeSet` of `null`) will run on the update // thread. If we attempt to detach beatmaps in this callback the game will fall over (it takes time). - loadBeatmapSets(detachedBeatmapStore.GetDetachedBeatmaps()); + detachedBeatmapSets = detachedBeatmapStore!.GetDetachedBeatmaps(); + detachedBeatmapSets.BindCollectionChanged(beatmapSetsChanged); + loadBeatmapSets(detachedBeatmapSets); } } - [Resolved] - private RealmAccess realm { get; set; } = null!; - protected override void LoadComplete() { base.LoadComplete(); - subscriptionSets = realm.RegisterForNotifications(getBeatmapSets, beatmapSetsChanged); subscriptionBeatmaps = realm.RegisterForNotifications(r => r.All().Where(b => !b.Hidden), beatmapsChanged); } - private IQueryable getBeatmapSets(Realm realm) => realm.All().Where(s => !s.DeletePending && !s.Protected); + private readonly HashSet setsRequiringUpdate = new HashSet(); + private readonly HashSet setsRequiringRemoval = new HashSet(); - private readonly HashSet setsRequiringUpdate = new HashSet(); - private readonly HashSet setsRequiringRemoval = new HashSet(); - - private void beatmapSetsChanged(IRealmCollection sender, ChangeSet? changes) + private void beatmapSetsChanged(object? beatmaps, NotifyCollectionChangedEventArgs changed) { // If loading test beatmaps, avoid overwriting with realm subscription callbacks. if (loadedTestBeatmaps) return; + var newBeatmapSets = changed.NewItems!.Cast(); + var newBeatmapSetIDs = newBeatmapSets.Select(s => s.ID).ToHashSet(); + + var oldBeatmapSets = changed.OldItems!.Cast(); + var oldBeatmapSetIDs = oldBeatmapSets.Select(s => s.ID).ToHashSet(); + + switch (changed.Action) + { + case NotifyCollectionChangedAction.Add: + setsRequiringRemoval.RemoveWhere(s => newBeatmapSetIDs.Contains(s.ID)); + setsRequiringUpdate.AddRange(newBeatmapSets); + break; + + case NotifyCollectionChangedAction.Remove: + setsRequiringUpdate.RemoveWhere(s => oldBeatmapSetIDs.Contains(s.ID)); + setsRequiringRemoval.AddRange(oldBeatmapSets); + break; + + case NotifyCollectionChangedAction.Replace: + setsRequiringUpdate.AddRange(newBeatmapSets); + break; + + case NotifyCollectionChangedAction.Move: + setsRequiringUpdate.AddRange(newBeatmapSets); + break; + + case NotifyCollectionChangedAction.Reset: + setsRequiringRemoval.Clear(); + setsRequiringUpdate.Clear(); + + loadBeatmapSets(detachedBeatmapSets); + break; + } + Scheduler.AddOnce(processBeatmapChanges); } @@ -282,9 +318,10 @@ namespace osu.Game.Screens.Select { try { - foreach (var set in setsRequiringRemoval) removeBeatmapSet(set); + // TODO: chekc whether we still need beatmap sets by ID + foreach (var set in setsRequiringRemoval) removeBeatmapSet(set.ID); - foreach (var set in setsRequiringUpdate) updateBeatmapSet(fetchFromID(set)!); + foreach (var set in setsRequiringUpdate) updateBeatmapSet(set); if (setsRequiringRemoval.Count > 0 && SelectedBeatmapInfo != null) { @@ -302,7 +339,7 @@ namespace osu.Game.Screens.Select // This relies on the full update operation being in a single transaction, so please don't change that. foreach (var set in setsRequiringUpdate) { - foreach (var beatmapInfo in fetchFromID(set)!.Beatmaps) + foreach (var beatmapInfo in set.Beatmaps) { if (!((IBeatmapMetadataInfo)beatmapInfo.Metadata).Equals(SelectedBeatmapInfo.Metadata)) continue; @@ -317,7 +354,7 @@ namespace osu.Game.Screens.Select // If a direct selection couldn't be made, it's feasible that the difficulty name (or beatmap metadata) changed. // Let's attempt to follow set-level selection anyway. - SelectBeatmap(fetchFromID(setsRequiringUpdate.First())!.Beatmaps.First()); + SelectBeatmap(setsRequiringUpdate.First().Beatmaps.First()); } } } @@ -353,7 +390,7 @@ namespace osu.Game.Screens.Select if (root.BeatmapSetsByID.TryGetValue(beatmapSet.ID, out var existingSets) && existingSets.SelectMany(s => s.Beatmaps).All(b => b.BeatmapInfo.ID != beatmapInfo.ID)) { - updateBeatmapSet(beatmapSet.Detach()); + updateBeatmapSet(beatmapSet); changed = true; } } @@ -383,21 +420,14 @@ namespace osu.Game.Screens.Select } } - public void UpdateBeatmapSet(BeatmapSetInfo beatmapSet) + public void UpdateBeatmapSet(BeatmapSetInfo beatmapSet) => Schedule(() => { - beatmapSet = beatmapSet.Detach(); - - Schedule(() => - { - updateBeatmapSet(beatmapSet); - invalidateAfterChange(); - }); - } + updateBeatmapSet(beatmapSet); + invalidateAfterChange(); + }); private void updateBeatmapSet(BeatmapSetInfo beatmapSet) { - beatmapSet = beatmapSet.Detach(); - var newSets = new List(); if (beatmapsSplitOut) @@ -696,7 +726,7 @@ namespace osu.Game.Screens.Select if (activeCriteria.SplitOutDifficulties != beatmapsSplitOut) { beatmapsSplitOut = activeCriteria.SplitOutDifficulties; - loadBeatmapSets(detachedBeatmapStore.GetDetachedBeatmaps()); + loadBeatmapSets(detachedBeatmapSets); return; } @@ -1245,7 +1275,6 @@ namespace osu.Game.Screens.Select { base.Dispose(isDisposing); - subscriptionSets?.Dispose(); subscriptionBeatmaps?.Dispose(); } } From be0e2efda2b37a31abc5318303b418709856ceb1 Mon Sep 17 00:00:00 2001 From: Fabep Date: Wed, 28 Aug 2024 09:51:17 +0200 Subject: [PATCH 2106/2493] Removed on click event for expanding the Mod Customisation Header. --- osu.Game/Overlays/Mods/ModCustomisationHeader.cs | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModCustomisationHeader.cs b/osu.Game/Overlays/Mods/ModCustomisationHeader.cs index abd48a0dcb..57fe99ce4a 100644 --- a/osu.Game/Overlays/Mods/ModCustomisationHeader.cs +++ b/osu.Game/Overlays/Mods/ModCustomisationHeader.cs @@ -112,20 +112,6 @@ namespace osu.Game.Overlays.Mods }, true); } - protected override bool OnClick(ClickEvent e) - { - if (Enabled.Value) - { - ExpandedState.Value = ExpandedState.Value switch - { - ModCustomisationPanelState.Collapsed => ModCustomisationPanelState.Expanded, - _ => ModCustomisationPanelState.Collapsed - }; - } - - return base.OnClick(e); - } - private bool touchedThisFrame; protected override bool OnTouchDown(TouchDownEvent e) From cadbb0f27ab2937e930e86083a42a8e76101b613 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Wed, 28 Aug 2024 09:57:13 +0200 Subject: [PATCH 2107/2493] change sample seek keybind to ctrl shift --- osu.Game/Input/Bindings/GlobalActionContainer.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Input/Bindings/GlobalActionContainer.cs b/osu.Game/Input/Bindings/GlobalActionContainer.cs index 27d026ac9c..aca0984e0f 100644 --- a/osu.Game/Input/Bindings/GlobalActionContainer.cs +++ b/osu.Game/Input/Bindings/GlobalActionContainer.cs @@ -149,8 +149,8 @@ namespace osu.Game.Input.Bindings new KeyBinding(new[] { InputKey.Control, InputKey.E }, GlobalAction.EditorToggleScaleControl), new KeyBinding(new[] { InputKey.Control, InputKey.Left }, GlobalAction.EditorSeekToPreviousHitObject), new KeyBinding(new[] { InputKey.Control, InputKey.Right }, GlobalAction.EditorSeekToNextHitObject), - new KeyBinding(new[] { InputKey.Alt, InputKey.Left }, GlobalAction.EditorSeekToPreviousSamplePoint), - new KeyBinding(new[] { InputKey.Alt, InputKey.Right }, GlobalAction.EditorSeekToNextSamplePoint), + new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.Left }, GlobalAction.EditorSeekToPreviousSamplePoint), + new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.Right }, GlobalAction.EditorSeekToNextSamplePoint), }; private static IEnumerable editorTestPlayKeyBindings => new[] From 6adaf6a41faeeb09e9f53993c6ac2b91e9c9a0bf Mon Sep 17 00:00:00 2001 From: Fabep Date: Wed, 28 Aug 2024 10:09:47 +0200 Subject: [PATCH 2108/2493] Changed ModCustomisationPanelState names --- .../Visual/UserInterface/TestSceneModCustomisationPanel.cs | 6 +++--- osu.Game/Overlays/Mods/ModCustomisationHeader.cs | 2 +- osu.Game/Overlays/Mods/ModCustomisationPanel.cs | 6 +++--- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModCustomisationPanel.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModCustomisationPanel.cs index 0d8ea05612..d93f62935a 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModCustomisationPanel.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModCustomisationPanel.cs @@ -58,19 +58,19 @@ namespace osu.Game.Tests.Visual.UserInterface { SelectedMods.Value = new[] { new OsuModDoubleTime() }; panel.Enabled.Value = true; - panel.ExpandedState.Value = ModCustomisationPanel.ModCustomisationPanelState.Expanded; + panel.ExpandedState.Value = ModCustomisationPanel.ModCustomisationPanelState.ExpandedByMod; }); AddStep("set DA", () => { SelectedMods.Value = new Mod[] { new OsuModDifficultyAdjust() }; panel.Enabled.Value = true; - panel.ExpandedState.Value = ModCustomisationPanel.ModCustomisationPanelState.Expanded; + panel.ExpandedState.Value = ModCustomisationPanel.ModCustomisationPanelState.ExpandedByMod; }); AddStep("set FL+WU+DA+AD", () => { SelectedMods.Value = new Mod[] { new OsuModFlashlight(), new ModWindUp(), new OsuModDifficultyAdjust(), new OsuModApproachDifferent() }; panel.Enabled.Value = true; - panel.ExpandedState.Value = ModCustomisationPanel.ModCustomisationPanelState.Expanded; + panel.ExpandedState.Value = ModCustomisationPanel.ModCustomisationPanelState.ExpandedByMod; }); AddStep("set empty", () => { diff --git a/osu.Game/Overlays/Mods/ModCustomisationHeader.cs b/osu.Game/Overlays/Mods/ModCustomisationHeader.cs index 57fe99ce4a..5ddcf01b88 100644 --- a/osu.Game/Overlays/Mods/ModCustomisationHeader.cs +++ b/osu.Game/Overlays/Mods/ModCustomisationHeader.cs @@ -130,7 +130,7 @@ namespace osu.Game.Overlays.Mods if (Enabled.Value) { if (!touchedThisFrame && panel.ExpandedState.Value == ModCustomisationPanelState.Collapsed) - panel.ExpandedState.Value = ModCustomisationPanelState.ExpandedByHover; + panel.ExpandedState.Value = ModCustomisationPanelState.Expanded; } return base.OnHover(e); diff --git a/osu.Game/Overlays/Mods/ModCustomisationPanel.cs b/osu.Game/Overlays/Mods/ModCustomisationPanel.cs index 522481bc6b..03a1b3d0dd 100644 --- a/osu.Game/Overlays/Mods/ModCustomisationPanel.cs +++ b/osu.Game/Overlays/Mods/ModCustomisationPanel.cs @@ -227,7 +227,7 @@ namespace osu.Game.Overlays.Mods { base.Update(); - if (ExpandedState.Value == ModCustomisationPanelState.ExpandedByHover + if (ExpandedState.Value == ModCustomisationPanelState.Expanded && !ReceivePositionalInputAt(inputManager.CurrentState.Mouse.Position) && inputManager.DraggedDrawable == null) { @@ -239,8 +239,8 @@ namespace osu.Game.Overlays.Mods public enum ModCustomisationPanelState { Collapsed = 0, - ExpandedByHover = 1, - Expanded = 2, + Expanded = 1, + ExpandedByMod = 2, } } } diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index 74890df5d9..cdc0fbbd96 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -368,7 +368,7 @@ namespace osu.Game.Overlays.Mods customisationPanel.Enabled.Value = true; if (anyModPendingConfiguration) - customisationPanel.ExpandedState.Value = ModCustomisationPanel.ModCustomisationPanelState.Expanded; + customisationPanel.ExpandedState.Value = ModCustomisationPanel.ModCustomisationPanelState.ExpandedByMod; } else { From 081c9eb21bca77fb98094fe02463d01eb73b69d4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 28 Aug 2024 16:17:03 +0900 Subject: [PATCH 2109/2493] Fix incorrect cancellation / disposal handling of `DetachedBeatmapStore` --- osu.Game/Database/DetachedBeatmapStore.cs | 8 +++----- osu.Game/Screens/Select/BeatmapCarousel.cs | 5 +++-- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/osu.Game/Database/DetachedBeatmapStore.cs b/osu.Game/Database/DetachedBeatmapStore.cs index ff81784745..55ab836dd9 100644 --- a/osu.Game/Database/DetachedBeatmapStore.cs +++ b/osu.Game/Database/DetachedBeatmapStore.cs @@ -7,7 +7,6 @@ using System.Threading; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; -using osu.Framework.Logging; using osu.Game.Beatmaps; using Realms; @@ -24,11 +23,9 @@ namespace osu.Game.Database [Resolved] private RealmAccess realm { get; set; } = null!; - public IBindableList GetDetachedBeatmaps() + public IBindableList GetDetachedBeatmaps(CancellationToken cancellationToken) { - if (!loaded.Wait(60000)) - Logger.Error(new TimeoutException("Beatmaps did not load in an acceptable time"), $"{nameof(DetachedBeatmapStore)} fell over"); - + loaded.Wait(cancellationToken); return detachedBeatmapSets.GetBoundCopy(); } @@ -75,6 +72,7 @@ namespace osu.Game.Database protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); + loaded.Set(); realmSubscription?.Dispose(); } diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 118ea45e45..94a6087741 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Collections.Specialized; using System.Diagnostics; using System.Linq; +using System.Threading; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; @@ -235,7 +236,7 @@ namespace osu.Game.Screens.Select } [BackgroundDependencyLoader] - private void load(OsuConfigManager config, AudioManager audio, DetachedBeatmapStore beatmapStore) + private void load(OsuConfigManager config, AudioManager audio, CancellationToken cancellationToken) { spinSample = audio.Samples.Get("SongSelect/random-spin"); randomSelectSample = audio.Samples.Get(@"SongSelect/select-random"); @@ -251,7 +252,7 @@ namespace osu.Game.Screens.Select // This is performing an unnecessary second lookup on realm (in addition to the subscription), but for performance reasons // we require it to be separate: the subscription's initial callback (with `ChangeSet` of `null`) will run on the update // thread. If we attempt to detach beatmaps in this callback the game will fall over (it takes time). - detachedBeatmapSets = detachedBeatmapStore!.GetDetachedBeatmaps(); + detachedBeatmapSets = detachedBeatmapStore!.GetDetachedBeatmaps(cancellationToken); detachedBeatmapSets.BindCollectionChanged(beatmapSetsChanged); loadBeatmapSets(detachedBeatmapSets); } From 81b36d897d5869184a1bc6b397718b71f4e143d6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 28 Aug 2024 16:19:17 +0900 Subject: [PATCH 2110/2493] Fix null reference in change handling code --- osu.Game/Screens/Select/BeatmapCarousel.cs | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 94a6087741..05e567b693 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -274,30 +274,31 @@ namespace osu.Game.Screens.Select if (loadedTestBeatmaps) return; - var newBeatmapSets = changed.NewItems!.Cast(); - var newBeatmapSetIDs = newBeatmapSets.Select(s => s.ID).ToHashSet(); - - var oldBeatmapSets = changed.OldItems!.Cast(); - var oldBeatmapSetIDs = oldBeatmapSets.Select(s => s.ID).ToHashSet(); + IEnumerable? newBeatmapSets = changed.NewItems?.Cast(); switch (changed.Action) { case NotifyCollectionChangedAction.Add: + HashSet newBeatmapSetIDs = newBeatmapSets!.Select(s => s.ID).ToHashSet(); + setsRequiringRemoval.RemoveWhere(s => newBeatmapSetIDs.Contains(s.ID)); - setsRequiringUpdate.AddRange(newBeatmapSets); + setsRequiringUpdate.AddRange(newBeatmapSets!); break; case NotifyCollectionChangedAction.Remove: + IEnumerable oldBeatmapSets = changed.OldItems!.Cast(); + HashSet oldBeatmapSetIDs = oldBeatmapSets.Select(s => s.ID).ToHashSet(); + setsRequiringUpdate.RemoveWhere(s => oldBeatmapSetIDs.Contains(s.ID)); setsRequiringRemoval.AddRange(oldBeatmapSets); break; case NotifyCollectionChangedAction.Replace: - setsRequiringUpdate.AddRange(newBeatmapSets); + setsRequiringUpdate.AddRange(newBeatmapSets!); break; case NotifyCollectionChangedAction.Move: - setsRequiringUpdate.AddRange(newBeatmapSets); + setsRequiringUpdate.AddRange(newBeatmapSets!); break; case NotifyCollectionChangedAction.Reset: From b1f653899c59cb8ef488e0e2ae09d44102f221db Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 28 Aug 2024 16:30:09 +0900 Subject: [PATCH 2111/2493] Fix enumeration over modified collection --- osu.Game/Screens/Select/BeatmapCarousel.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 05e567b693..305deb4ba9 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -137,6 +137,10 @@ namespace osu.Game.Screens.Select private void loadBeatmapSets(IEnumerable beatmapSets) { + // Ensure no changes are made to the list while we are initialising items. + // We'll catch up on changes via subscriptions anyway. + beatmapSets = beatmapSets.ToArray(); + if (selectedBeatmapSet != null && !beatmapSets.Contains(selectedBeatmapSet.BeatmapSet)) selectedBeatmapSet = null; From c2c1dccf2db2de855e9d76280eef4eed5cdcc845 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 28 Aug 2024 17:46:36 +0900 Subject: [PATCH 2112/2493] Detach beatmap sets asynchronously --- osu.Game/Database/DetachedBeatmapStore.cs | 57 ++++++++++++++++++----- 1 file changed, 45 insertions(+), 12 deletions(-) diff --git a/osu.Game/Database/DetachedBeatmapStore.cs b/osu.Game/Database/DetachedBeatmapStore.cs index 55ab836dd9..4e5ff23f7c 100644 --- a/osu.Game/Database/DetachedBeatmapStore.cs +++ b/osu.Game/Database/DetachedBeatmapStore.cs @@ -4,6 +4,7 @@ using System; using System.Linq; using System.Threading; +using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -30,9 +31,10 @@ namespace osu.Game.Database } [BackgroundDependencyLoader] - private void load() + private void load(CancellationToken cancellationToken) { - realmSubscription = realm.RegisterForNotifications(getBeatmapSets, beatmapSetsChanged); + realmSubscription = realm.RegisterForNotifications(r => r.All().Where(s => !s.DeletePending && !s.Protected), beatmapSetsChanged); + loaded.Wait(cancellationToken); } private void beatmapSetsChanged(IRealmCollection sender, ChangeSet? changes) @@ -50,23 +52,56 @@ namespace osu.Game.Database return; } - detachedBeatmapSets.Clear(); - detachedBeatmapSets.AddRange(sender.Detach()); + // Detaching beatmaps takes some time, so let's make sure it doesn't run on the update thread. + var frozenSets = sender.Freeze(); + + Task.Factory.StartNew(() => + { + realm.Run(_ => + { + var detached = frozenSets.Detach(); + + detachedBeatmapSets.Clear(); + detachedBeatmapSets.AddRange(detached); + loaded.Set(); + }); + }, TaskCreationOptions.LongRunning); - loaded.Set(); return; } foreach (int i in changes.DeletedIndices.OrderDescending()) - detachedBeatmapSets.RemoveAt(i); + removeAt(i); foreach (int i in changes.InsertedIndices) - { - detachedBeatmapSets.Insert(i, sender[i].Detach()); - } + insert(sender[i].Detach(), i); foreach (int i in changes.NewModifiedIndices) - detachedBeatmapSets.ReplaceRange(i, 1, new[] { sender[i].Detach() }); + replaceRange(sender[i].Detach(), i); + } + + private void replaceRange(BeatmapSetInfo set, int i) + { + if (loaded.IsSet) + detachedBeatmapSets.ReplaceRange(i, 1, new[] { set }); + else + Schedule(() => { detachedBeatmapSets.ReplaceRange(i, 1, new[] { set }); }); + } + + private void insert(BeatmapSetInfo set, int i) + { + if (loaded.IsSet) + detachedBeatmapSets.Insert(i, set); + else + Schedule(() => { detachedBeatmapSets.Insert(i, set); }); + } + + private void removeAt(int i) + { + if (loaded.IsSet) + detachedBeatmapSets.RemoveAt(i); + else + Schedule(() => { detachedBeatmapSets.RemoveAt(i); }); } protected override void Dispose(bool isDisposing) @@ -75,7 +110,5 @@ namespace osu.Game.Database loaded.Set(); realmSubscription?.Dispose(); } - - private IQueryable getBeatmapSets(Realm realm) => realm.All().Where(s => !s.DeletePending && !s.Protected); } } From 5ed0c6e91a9ea05c751ec9bb81b56bf17b919400 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 28 Aug 2024 17:48:17 +0900 Subject: [PATCH 2113/2493] Remove song select preloading Really unnecessary now. --- osu.Game/Screens/Menu/MainMenu.cs | 22 +--------------------- 1 file changed, 1 insertion(+), 21 deletions(-) diff --git a/osu.Game/Screens/Menu/MainMenu.cs b/osu.Game/Screens/Menu/MainMenu.cs index dfe5460aee..c1d502bd41 100644 --- a/osu.Game/Screens/Menu/MainMenu.cs +++ b/osu.Game/Screens/Menu/MainMenu.cs @@ -54,8 +54,6 @@ namespace osu.Game.Screens.Menu public override bool? AllowGlobalTrackControl => true; - private Screen songSelect; - private MenuSideFlashes sideFlashes; protected ButtonSystem Buttons; @@ -220,26 +218,11 @@ namespace osu.Game.Screens.Menu Buttons.OnBeatmapListing = () => beatmapListing?.ToggleVisibility(); reappearSampleSwoosh = audio.Samples.Get(@"Menu/reappear-swoosh"); - - preloadSongSelect(); } public void ReturnToOsuLogo() => Buttons.State = ButtonSystemState.Initial; - private void preloadSongSelect() - { - if (songSelect == null) - LoadComponentAsync(songSelect = new PlaySongSelect()); - } - - private void loadSoloSongSelect() => this.Push(consumeSongSelect()); - - private Screen consumeSongSelect() - { - var s = songSelect; - songSelect = null; - return s; - } + private void loadSoloSongSelect() => this.Push(new PlaySongSelect()); public override void OnEntering(ScreenTransitionEvent e) { @@ -373,9 +356,6 @@ namespace osu.Game.Screens.Menu ApplyToBackground(b => (b as BackgroundScreenDefault)?.Next()); - // we may have consumed our preloaded instance, so let's make another. - preloadSongSelect(); - musicController.EnsurePlayingSomething(); // Cycle tip on resuming From 336abadbd1b9e36f2aa2f2ea6c05916494a19685 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 28 Aug 2024 18:22:05 +0900 Subject: [PATCH 2114/2493] Allow running initial filter criteria asynchronously This reverts a portion of https://github.com/ppy/osu/pull/9539. The rearrangement in `SongSelect` is required to get the initial filter into `BeatmapCarousel` (and avoid the `FilterChanged` event firing, causing a delayed/scheduled filter application). --- .../SongSelect/TestSceneBeatmapCarousel.cs | 5 +++ .../TestSceneUpdateBeatmapSetButton.cs | 2 +- osu.Game/Screens/Select/BeatmapCarousel.cs | 9 ++--- osu.Game/Screens/Select/SongSelect.cs | 35 ++++++++++--------- 4 files changed, 30 insertions(+), 21 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs index c0102b238c..24be242013 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs @@ -1389,6 +1389,11 @@ namespace osu.Game.Tests.Visual.SongSelect private partial class TestBeatmapCarousel : BeatmapCarousel { + public TestBeatmapCarousel() + : base(new FilterCriteria()) + { + } + public bool PendingFilterTask => PendingFilter != null; public IEnumerable Items diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneUpdateBeatmapSetButton.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneUpdateBeatmapSetButton.cs index 6d97be730b..0b0cd0317a 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneUpdateBeatmapSetButton.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneUpdateBeatmapSetButton.cs @@ -246,7 +246,7 @@ namespace osu.Game.Tests.Visual.SongSelect private BeatmapCarousel createCarousel() { - return carousel = new BeatmapCarousel + return carousel = new BeatmapCarousel(new FilterCriteria()) { RelativeSizeAxes = Axes.Both, BeatmapSets = new List diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 305deb4ba9..5e79a8202e 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -170,13 +170,12 @@ namespace osu.Game.Screens.Select } root = newRoot; + root.Filter(activeCriteria); Scroll.Clear(false); itemsCache.Invalidate(); ScrollToSelected(); - applyActiveCriteria(false); - // Restore selection if (selectedBeatmapBefore != null && newRoot.BeatmapSetsByID.TryGetValue(selectedBeatmapBefore.BeatmapSet!.ID, out var newSelectionCandidates)) { @@ -215,7 +214,7 @@ namespace osu.Game.Screens.Select private int visibleSetsCount; - public BeatmapCarousel() + public BeatmapCarousel(FilterCriteria initialCriterial) { root = new CarouselRoot(this); InternalChild = new Container @@ -237,6 +236,8 @@ namespace osu.Game.Screens.Select noResultsPlaceholder = new NoResultsPlaceholder() } }; + + activeCriteria = initialCriterial; } [BackgroundDependencyLoader] @@ -662,7 +663,7 @@ namespace osu.Game.Screens.Select item.State.Value = CarouselItemState.Selected; } - private FilterCriteria activeCriteria = new FilterCriteria(); + private FilterCriteria activeCriteria; protected ScheduledDelegate? PendingFilter; diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index 2965aa383d..3cfc7623b9 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -162,20 +162,6 @@ namespace osu.Game.Screens.Select ApplyToBackground(applyBlurToBackground); }); - LoadComponentAsync(Carousel = new BeatmapCarousel - { - AllowSelection = false, // delay any selection until our bindables are ready to make a good choice. - Anchor = Anchor.CentreRight, - Origin = Anchor.CentreRight, - RelativeSizeAxes = Axes.Both, - BleedTop = FilterControl.HEIGHT, - BleedBottom = Select.Footer.HEIGHT, - SelectionChanged = updateSelectedBeatmap, - BeatmapSetsChanged = carouselBeatmapsLoaded, - FilterApplied = () => Scheduler.AddOnce(updateVisibleBeatmapCount), - GetRecommendedBeatmap = s => recommender?.GetRecommendedBeatmap(s), - }, c => carouselContainer.Child = c); - // initial value transfer is required for FilterControl (it uses our re-cached bindables in its async load for the initial filter). transferRulesetValue(); @@ -227,7 +213,6 @@ namespace osu.Game.Screens.Select { RelativeSizeAxes = Axes.X, Height = FilterControl.HEIGHT, - FilterChanged = ApplyFilterToCarousel, }, new GridContainer // used for max width implementation { @@ -328,6 +313,23 @@ namespace osu.Game.Screens.Select modSpeedHotkeyHandler = new ModSpeedHotkeyHandler(), }); + // Important to load this after the filter control is loaded (so we have initial filter criteria prepared). + LoadComponentAsync(Carousel = new BeatmapCarousel(FilterControl.CreateCriteria()) + { + AllowSelection = false, // delay any selection until our bindables are ready to make a good choice. + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + RelativeSizeAxes = Axes.Both, + BleedTop = FilterControl.HEIGHT, + BleedBottom = Select.Footer.HEIGHT, + SelectionChanged = updateSelectedBeatmap, + BeatmapSetsChanged = carouselBeatmapsLoaded, + FilterApplied = () => Scheduler.AddOnce(updateVisibleBeatmapCount), + GetRecommendedBeatmap = s => recommender?.GetRecommendedBeatmap(s), + }, c => carouselContainer.Child = c); + + FilterControl.FilterChanged = ApplyFilterToCarousel; + if (ShowSongSelectFooter) { AddRangeInternal(new Drawable[] @@ -992,7 +994,8 @@ namespace osu.Game.Screens.Select // if we have a pending filter operation, we want to run it now. // it could change selection (ie. if the ruleset has been changed). - Carousel.FlushPendingFilterOperations(); + if (IsLoaded) + Carousel.FlushPendingFilterOperations(); return true; } From dd4a1104e45ddd50015608555227fe17afdb6754 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 28 Aug 2024 18:56:09 +0900 Subject: [PATCH 2115/2493] Always debounce external `Filter` requests (except for tests) The only exception to the rule here was "when screen isn't active apply without debounce" but I'm not sure we want this. It would cause a stutter on returning to song select and I'm not even sure this is a common scenario. I'd rather remove it and see if someone finds an actual case where this is an issue. --- .../SongSelect/TestSceneBeatmapCarousel.cs | 88 ++++++++++--------- .../SongSelect/TestScenePlaySongSelect.cs | 12 ++- osu.Game/Screens/Select/BeatmapCarousel.cs | 4 +- osu.Game/Screens/Select/SongSelect.cs | 10 +-- 4 files changed, 55 insertions(+), 59 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs index 24be242013..a075559f6a 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs @@ -52,11 +52,11 @@ namespace osu.Game.Tests.Visual.SongSelect { createCarousel(new List()); - AddStep("filter to ruleset 0", () => carousel.Filter(new FilterCriteria + AddStep("filter to ruleset 0", () => carousel.FilterImmediately(new FilterCriteria { Ruleset = rulesets.AvailableRulesets.ElementAt(0), AllowConvertedBeatmaps = true, - }, false)); + })); AddStep("add mixed ruleset beatmapset", () => { @@ -78,11 +78,11 @@ namespace osu.Game.Tests.Visual.SongSelect && visibleBeatmapPanels.Count(p => ((CarouselBeatmap)p.Item)!.BeatmapInfo.Ruleset.OnlineID == 0) == 1; }); - AddStep("filter to ruleset 1", () => carousel.Filter(new FilterCriteria + AddStep("filter to ruleset 1", () => carousel.FilterImmediately(new FilterCriteria { Ruleset = rulesets.AvailableRulesets.ElementAt(1), AllowConvertedBeatmaps = true, - }, false)); + })); AddUntilStep("wait for filtered difficulties", () => { @@ -93,11 +93,11 @@ namespace osu.Game.Tests.Visual.SongSelect && visibleBeatmapPanels.Count(p => ((CarouselBeatmap)p.Item)!.BeatmapInfo.Ruleset.OnlineID == 1) == 1; }); - AddStep("filter to ruleset 2", () => carousel.Filter(new FilterCriteria + AddStep("filter to ruleset 2", () => carousel.FilterImmediately(new FilterCriteria { Ruleset = rulesets.AvailableRulesets.ElementAt(2), AllowConvertedBeatmaps = true, - }, false)); + })); AddUntilStep("wait for filtered difficulties", () => { @@ -344,7 +344,7 @@ namespace osu.Game.Tests.Visual.SongSelect // basic filtering setSelected(1, 1); - AddStep("Filter", () => carousel.Filter(new FilterCriteria { SearchText = carousel.BeatmapSets.ElementAt(2).Metadata.Title }, false)); + AddStep("Filter", () => carousel.FilterImmediately(new FilterCriteria { SearchText = carousel.BeatmapSets.ElementAt(2).Metadata.Title })); checkVisibleItemCount(diff: false, count: 1); checkVisibleItemCount(diff: true, count: 3); waitForSelection(3, 1); @@ -360,13 +360,13 @@ namespace osu.Game.Tests.Visual.SongSelect // test filtering some difficulties (and keeping current beatmap set selected). setSelected(1, 2); - AddStep("Filter some difficulties", () => carousel.Filter(new FilterCriteria { SearchText = "Normal" }, false)); + AddStep("Filter some difficulties", () => carousel.FilterImmediately(new FilterCriteria { SearchText = "Normal" })); waitForSelection(1, 1); - AddStep("Un-filter", () => carousel.Filter(new FilterCriteria(), false)); + AddStep("Un-filter", () => carousel.FilterImmediately(new FilterCriteria())); waitForSelection(1, 1); - AddStep("Filter all", () => carousel.Filter(new FilterCriteria { SearchText = "Dingo" }, false)); + AddStep("Filter all", () => carousel.FilterImmediately(new FilterCriteria { SearchText = "Dingo" })); checkVisibleItemCount(false, 0); checkVisibleItemCount(true, 0); @@ -378,7 +378,7 @@ namespace osu.Game.Tests.Visual.SongSelect advanceSelection(false); AddAssert("Selection is null", () => currentSelection == null); - AddStep("Un-filter", () => carousel.Filter(new FilterCriteria(), false)); + AddStep("Un-filter", () => carousel.FilterImmediately(new FilterCriteria())); AddAssert("Selection is non-null", () => currentSelection != null); @@ -399,7 +399,7 @@ namespace osu.Game.Tests.Visual.SongSelect setSelected(1, 3); - AddStep("Apply a range filter", () => carousel.Filter(new FilterCriteria + AddStep("Apply a range filter", () => carousel.FilterImmediately(new FilterCriteria { SearchText = searchText, StarDifficulty = new FilterCriteria.OptionalRange @@ -408,7 +408,7 @@ namespace osu.Game.Tests.Visual.SongSelect Max = 5.5, IsLowerInclusive = true } - }, false)); + })); // should reselect the buffered selection. waitForSelection(3, 2); @@ -445,13 +445,13 @@ namespace osu.Game.Tests.Visual.SongSelect AddAssert("ensure repeat", () => selectedSets.Contains(carousel.SelectedBeatmapSet)); AddStep("Add set with 100 difficulties", () => carousel.UpdateBeatmapSet(TestResources.CreateTestBeatmapSetInfo(100, rulesets.AvailableRulesets.ToArray()))); - AddStep("Filter Extra", () => carousel.Filter(new FilterCriteria { SearchText = "Extra 10" }, false)); + AddStep("Filter Extra", () => carousel.FilterImmediately(new FilterCriteria { SearchText = "Extra 10" })); checkInvisibleDifficultiesUnselectable(); checkInvisibleDifficultiesUnselectable(); checkInvisibleDifficultiesUnselectable(); checkInvisibleDifficultiesUnselectable(); checkInvisibleDifficultiesUnselectable(); - AddStep("Un-filter", () => carousel.Filter(new FilterCriteria(), false)); + AddStep("Un-filter", () => carousel.FilterImmediately(new FilterCriteria())); } [Test] @@ -527,7 +527,7 @@ namespace osu.Game.Tests.Visual.SongSelect loadBeatmaps(setCount: local_set_count, diffCount: local_diff_count); - AddStep("Sort by difficulty", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Difficulty }, false)); + AddStep("Sort by difficulty", () => carousel.FilterImmediately(new FilterCriteria { Sort = SortMode.Difficulty })); checkVisibleItemCount(false, local_set_count * local_diff_count); @@ -566,7 +566,7 @@ namespace osu.Game.Tests.Visual.SongSelect loadBeatmaps(sets, () => new FilterCriteria { Ruleset = rulesets.AvailableRulesets.ElementAt(0) }); AddStep("Set non-empty mode filter", () => - carousel.Filter(new FilterCriteria { Ruleset = rulesets.AvailableRulesets.ElementAt(1) }, false)); + carousel.FilterImmediately(new FilterCriteria { Ruleset = rulesets.AvailableRulesets.ElementAt(1) })); AddAssert("Something is selected", () => carousel.SelectedBeatmapInfo != null); } @@ -601,7 +601,7 @@ namespace osu.Game.Tests.Visual.SongSelect loadBeatmaps(sets); - AddStep("Sort by date submitted", () => carousel.Filter(new FilterCriteria { Sort = SortMode.DateSubmitted }, false)); + AddStep("Sort by date submitted", () => carousel.FilterImmediately(new FilterCriteria { Sort = SortMode.DateSubmitted })); checkVisibleItemCount(diff: false, count: 10); checkVisibleItemCount(diff: true, count: 5); @@ -610,11 +610,11 @@ namespace osu.Game.Tests.Visual.SongSelect AddAssert("rest are at start", () => carousel.Items.OfType().TakeWhile(i => i.Item is CarouselBeatmapSet s && s.BeatmapSet.DateSubmitted != null).Count(), () => Is.EqualTo(6)); - AddStep("Sort by date submitted and string", () => carousel.Filter(new FilterCriteria + AddStep("Sort by date submitted and string", () => carousel.FilterImmediately(new FilterCriteria { Sort = SortMode.DateSubmitted, SearchText = zzz_string - }, false)); + })); checkVisibleItemCount(diff: false, count: 5); checkVisibleItemCount(diff: true, count: 5); @@ -658,10 +658,10 @@ namespace osu.Game.Tests.Visual.SongSelect loadBeatmaps(sets); - AddStep("Sort by author", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Author }, false)); + AddStep("Sort by author", () => carousel.FilterImmediately(new FilterCriteria { Sort = SortMode.Author })); AddAssert($"Check {zzz_uppercase} is last", () => carousel.BeatmapSets.Last().Metadata.Author.Username == zzz_uppercase); AddAssert($"Check {zzz_lowercase} is second last", () => carousel.BeatmapSets.SkipLast(1).Last().Metadata.Author.Username == zzz_lowercase); - AddStep("Sort by artist", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Artist }, false)); + AddStep("Sort by artist", () => carousel.FilterImmediately(new FilterCriteria { Sort = SortMode.Artist })); AddAssert($"Check {zzz_uppercase} is last", () => carousel.BeatmapSets.Last().Metadata.Artist == zzz_uppercase); AddAssert($"Check {zzz_lowercase} is second last", () => carousel.BeatmapSets.SkipLast(1).Last().Metadata.Artist == zzz_lowercase); } @@ -703,7 +703,7 @@ namespace osu.Game.Tests.Visual.SongSelect loadBeatmaps(sets); - AddStep("Sort by artist", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Artist }, false)); + AddStep("Sort by artist", () => carousel.FilterImmediately(new FilterCriteria { Sort = SortMode.Artist })); AddAssert("Check last item", () => { var lastItem = carousel.BeatmapSets.Last(); @@ -746,10 +746,10 @@ namespace osu.Game.Tests.Visual.SongSelect loadBeatmaps(sets); - AddStep("Sort by title", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Title }, false)); + AddStep("Sort by title", () => carousel.FilterImmediately(new FilterCriteria { Sort = SortMode.Title })); AddAssert("Items remain in descending added order", () => carousel.BeatmapSets.Select(s => s.DateAdded), () => Is.Ordered.Descending); - AddStep("Sort by artist", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Artist }, false)); + AddStep("Sort by artist", () => carousel.FilterImmediately(new FilterCriteria { Sort = SortMode.Artist })); AddAssert("Items remain in descending added order", () => carousel.BeatmapSets.Select(s => s.DateAdded), () => Is.Ordered.Descending); } @@ -786,7 +786,7 @@ namespace osu.Game.Tests.Visual.SongSelect loadBeatmaps(sets); - AddStep("Sort by artist", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Artist }, false)); + AddStep("Sort by artist", () => carousel.FilterImmediately(new FilterCriteria { Sort = SortMode.Artist })); AddAssert("Items in descending added order", () => carousel.BeatmapSets.Select(s => s.DateAdded), () => Is.Ordered.Descending); AddStep("Save order", () => originalOrder = carousel.BeatmapSets.Select(s => s.ID).ToArray()); @@ -796,7 +796,7 @@ namespace osu.Game.Tests.Visual.SongSelect AddAssert("Order didn't change", () => carousel.BeatmapSets.Select(s => s.ID), () => Is.EqualTo(originalOrder)); - AddStep("Sort by title", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Title }, false)); + AddStep("Sort by title", () => carousel.FilterImmediately(new FilterCriteria { Sort = SortMode.Title })); AddAssert("Order didn't change", () => carousel.BeatmapSets.Select(s => s.ID), () => Is.EqualTo(originalOrder)); } @@ -833,7 +833,7 @@ namespace osu.Game.Tests.Visual.SongSelect loadBeatmaps(sets); - AddStep("Sort by artist", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Artist }, false)); + AddStep("Sort by artist", () => carousel.FilterImmediately(new FilterCriteria { Sort = SortMode.Artist })); AddAssert("Items in descending added order", () => carousel.BeatmapSets.Select(s => s.DateAdded), () => Is.Ordered.Descending); AddStep("Save order", () => originalOrder = carousel.BeatmapSets.Select(s => s.ID).ToArray()); @@ -858,7 +858,7 @@ namespace osu.Game.Tests.Visual.SongSelect AddAssert("Order didn't change", () => carousel.BeatmapSets.Select(s => s.ID), () => Is.EqualTo(originalOrder)); - AddStep("Sort by title", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Title }, false)); + AddStep("Sort by title", () => carousel.FilterImmediately(new FilterCriteria { Sort = SortMode.Title })); AddAssert("Order didn't change", () => carousel.BeatmapSets.Select(s => s.ID), () => Is.EqualTo(originalOrder)); } @@ -885,12 +885,12 @@ namespace osu.Game.Tests.Visual.SongSelect loadBeatmaps(sets); - AddStep("Sort by difficulty", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Difficulty }, false)); + AddStep("Sort by difficulty", () => carousel.FilterImmediately(new FilterCriteria { Sort = SortMode.Difficulty })); checkVisibleItemCount(false, local_set_count * local_diff_count); checkVisibleItemCount(true, 1); - AddStep("Filter to normal", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Difficulty, SearchText = "Normal" }, false)); + AddStep("Filter to normal", () => carousel.FilterImmediately(new FilterCriteria { Sort = SortMode.Difficulty, SearchText = "Normal" })); checkVisibleItemCount(false, local_set_count); checkVisibleItemCount(true, 1); @@ -901,7 +901,7 @@ namespace osu.Game.Tests.Visual.SongSelect .Count(p => ((CarouselBeatmapSet)p.Item)!.Beatmaps.Single().BeatmapInfo.DifficultyName.StartsWith("Normal", StringComparison.Ordinal)) == local_set_count; }); - AddStep("Filter to insane", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Difficulty, SearchText = "Insane" }, false)); + AddStep("Filter to insane", () => carousel.FilterImmediately(new FilterCriteria { Sort = SortMode.Difficulty, SearchText = "Insane" })); checkVisibleItemCount(false, local_set_count); checkVisibleItemCount(true, 1); @@ -1022,7 +1022,7 @@ namespace osu.Game.Tests.Visual.SongSelect carousel.UpdateBeatmapSet(testMixed); }); AddStep("filter to ruleset 0", () => - carousel.Filter(new FilterCriteria { Ruleset = rulesets.AvailableRulesets.ElementAt(0) }, false)); + carousel.FilterImmediately(new FilterCriteria { Ruleset = rulesets.AvailableRulesets.ElementAt(0) })); AddStep("select filtered map skipping filtered", () => carousel.SelectBeatmap(testMixed.Beatmaps[1], false)); AddAssert("unfiltered beatmap not selected", () => carousel.SelectedBeatmapInfo?.Ruleset.OnlineID == 0); @@ -1068,12 +1068,12 @@ namespace osu.Game.Tests.Visual.SongSelect { AddStep("Toggle non-matching filter", () => { - carousel.Filter(new FilterCriteria { SearchText = Guid.NewGuid().ToString() }, false); + carousel.FilterImmediately(new FilterCriteria { SearchText = Guid.NewGuid().ToString() }); }); AddStep("Restore no filter", () => { - carousel.Filter(new FilterCriteria(), false); + carousel.FilterImmediately(new FilterCriteria()); eagerSelectedIDs.Add(carousel.SelectedBeatmapSet!.ID); }); } @@ -1097,7 +1097,7 @@ namespace osu.Game.Tests.Visual.SongSelect loadBeatmaps(manySets); - AddStep("Sort by difficulty", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Difficulty }, false)); + AddStep("Sort by difficulty", () => carousel.FilterImmediately(new FilterCriteria { Sort = SortMode.Difficulty })); advanceSelection(direction: 1, diff: false); @@ -1105,12 +1105,12 @@ namespace osu.Game.Tests.Visual.SongSelect { AddStep("Toggle non-matching filter", () => { - carousel.Filter(new FilterCriteria { SearchText = Guid.NewGuid().ToString() }, false); + carousel.FilterImmediately(new FilterCriteria { SearchText = Guid.NewGuid().ToString() }); }); AddStep("Restore no filter", () => { - carousel.Filter(new FilterCriteria(), false); + carousel.FilterImmediately(new FilterCriteria()); eagerSelectedIDs.Add(carousel.SelectedBeatmapSet!.ID); }); } @@ -1185,7 +1185,7 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep($"Set ruleset to {rulesetInfo.ShortName}", () => { - carousel.Filter(new FilterCriteria { Ruleset = rulesetInfo, Sort = SortMode.Title }, false); + carousel.FilterImmediately(new FilterCriteria { Ruleset = rulesetInfo, Sort = SortMode.Title }); }); waitForSelection(i + 1, 1); } @@ -1223,12 +1223,12 @@ namespace osu.Game.Tests.Visual.SongSelect setSelected(i, 1); AddStep("Set ruleset to taiko", () => { - carousel.Filter(new FilterCriteria { Ruleset = rulesets.AvailableRulesets.ElementAt(1), Sort = SortMode.Title }, false); + carousel.FilterImmediately(new FilterCriteria { Ruleset = rulesets.AvailableRulesets.ElementAt(1), Sort = SortMode.Title }); }); waitForSelection(i - 1, 1); AddStep("Remove ruleset filter", () => { - carousel.Filter(new FilterCriteria { Sort = SortMode.Title }, false); + carousel.FilterImmediately(new FilterCriteria { Sort = SortMode.Title }); }); } @@ -1415,6 +1415,12 @@ namespace osu.Game.Tests.Visual.SongSelect } } } + + public void FilterImmediately(FilterCriteria newCriteria) + { + Filter(newCriteria); + FlushPendingFilterOperations(); + } } } } diff --git a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs index 4c6a5c93d9..9df26e0da5 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs @@ -1397,8 +1397,6 @@ namespace osu.Game.Tests.Visual.SongSelect { public Action? StartRequested; - public new Bindable Ruleset => base.Ruleset; - public new FilterControl FilterControl => base.FilterControl; public WorkingBeatmap CurrentBeatmap => Beatmap.Value; @@ -1408,18 +1406,18 @@ namespace osu.Game.Tests.Visual.SongSelect public new void PresentScore(ScoreInfo score) => base.PresentScore(score); + public int FilterCount; + protected override bool OnStart() { StartRequested?.Invoke(); return base.OnStart(); } - public int FilterCount; - - protected override void ApplyFilterToCarousel(FilterCriteria criteria) + [BackgroundDependencyLoader] + private void load() { - FilterCount++; - base.ApplyFilterToCarousel(criteria); + FilterControl.FilterChanged += _ => FilterCount++; } } } diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 5e79a8202e..ddc8f22c95 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -700,12 +700,12 @@ namespace osu.Game.Screens.Select } } - public void Filter(FilterCriteria? newCriteria, bool debounce = true) + public void Filter(FilterCriteria? newCriteria) { if (newCriteria != null) activeCriteria = newCriteria; - applyActiveCriteria(debounce); + applyActiveCriteria(true); } private bool beatmapsSplitOut; diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index 3cfc7623b9..bfbc50378a 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -328,7 +328,7 @@ namespace osu.Game.Screens.Select GetRecommendedBeatmap = s => recommender?.GetRecommendedBeatmap(s), }, c => carouselContainer.Child = c); - FilterControl.FilterChanged = ApplyFilterToCarousel; + FilterControl.FilterChanged = Carousel.Filter; if (ShowSongSelectFooter) { @@ -403,14 +403,6 @@ namespace osu.Game.Screens.Select protected virtual ModSelectOverlay CreateModSelectOverlay() => new SoloModSelectOverlay(); - protected virtual void ApplyFilterToCarousel(FilterCriteria criteria) - { - // if not the current screen, we want to get carousel in a good presentation state before displaying (resume or enter). - bool shouldDebounce = this.IsCurrentScreen(); - - Carousel.Filter(criteria, shouldDebounce); - } - private DependencyContainer dependencies = null!; protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) From 9123d2cb7f18962f805b8a941f762f1c6583b73a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 28 Aug 2024 19:19:04 +0900 Subject: [PATCH 2116/2493] Fix multiple test failures --- .../Visual/Background/TestSceneUserDimBackgrounds.cs | 6 ++++++ osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs | 6 ++++++ osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs | 6 ++++++ .../Multiplayer/TestSceneMultiplayerMatchSongSelect.cs | 5 +++++ .../Visual/Multiplayer/TestScenePlaylistsSongSelect.cs | 6 ++++++ .../Visual/Navigation/TestScenePresentBeatmap.cs | 6 ++++++ .../Visual/SongSelect/TestScenePlaySongSelect.cs | 4 ++++ osu.Game/Database/DetachedBeatmapStore.cs | 7 +++---- osu.Game/Screens/Select/BeatmapCarousel.cs | 6 +++--- 9 files changed, 45 insertions(+), 7 deletions(-) diff --git a/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs b/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs index aac7689b1b..d8be57382f 100644 --- a/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs +++ b/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs @@ -18,6 +18,7 @@ using osu.Framework.Screens; using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Configuration; +using osu.Game.Database; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; @@ -48,13 +49,18 @@ namespace osu.Game.Tests.Visual.Background [BackgroundDependencyLoader] private void load(GameHost host, AudioManager audio) { + DetachedBeatmapStore detachedBeatmapStore; + Dependencies.Cache(rulesets = new RealmRulesetStore(Realm)); Dependencies.Cache(manager = new BeatmapManager(LocalStorage, Realm, null, audio, Resources, host, Beatmap.Default)); Dependencies.Cache(new OsuConfigManager(LocalStorage)); + Dependencies.Cache(detachedBeatmapStore = new DetachedBeatmapStore()); Dependencies.Cache(Realm); manager.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely(); + Add(detachedBeatmapStore); + Beatmap.SetDefault(); } diff --git a/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs b/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs index 0f1ba9ba75..8bcd5aab1c 100644 --- a/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs +++ b/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs @@ -12,6 +12,7 @@ using osu.Framework.Platform; using osu.Framework.Screens; using osu.Framework.Testing; using osu.Game.Beatmaps; +using osu.Game.Database; using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; using osu.Game.Rulesets; @@ -45,9 +46,14 @@ namespace osu.Game.Tests.Visual.Multiplayer [BackgroundDependencyLoader] private void load(GameHost host, AudioManager audio) { + DetachedBeatmapStore detachedBeatmapStore; + Dependencies.Cache(new RealmRulesetStore(Realm)); Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, Realm, null, audio, Resources, host, Beatmap.Default)); + Dependencies.Cache(detachedBeatmapStore = new DetachedBeatmapStore()); Dependencies.Cache(Realm); + + Add(detachedBeatmapStore); } public override void SetUpSteps() diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs index ad7e211354..df2021dbaf 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs @@ -19,6 +19,7 @@ using osu.Framework.Testing; using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Configuration; +using osu.Game.Database; using osu.Game.Graphics.UserInterface; using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; @@ -65,9 +66,14 @@ namespace osu.Game.Tests.Visual.Multiplayer [BackgroundDependencyLoader] private void load(GameHost host, AudioManager audio) { + DetachedBeatmapStore detachedBeatmapStore; + Dependencies.Cache(new RealmRulesetStore(Realm)); Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, Realm, API, audio, Resources, host, Beatmap.Default)); + Dependencies.Cache(detachedBeatmapStore = new DetachedBeatmapStore()); Dependencies.Cache(Realm); + + Add(detachedBeatmapStore); } public override void SetUpSteps() diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs index 8dc41cd707..88cc7eb9b3 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs @@ -45,11 +45,16 @@ namespace osu.Game.Tests.Visual.Multiplayer [BackgroundDependencyLoader] private void load(GameHost host, AudioManager audio) { + DetachedBeatmapStore detachedBeatmapStore; + Dependencies.Cache(rulesets = new RealmRulesetStore(Realm)); Dependencies.Cache(manager = new BeatmapManager(LocalStorage, Realm, null, audio, Resources, host, Beatmap.Default)); + Dependencies.Cache(detachedBeatmapStore = new DetachedBeatmapStore()); Dependencies.Cache(Realm); importedBeatmapSet = manager.Import(TestResources.CreateTestBeatmapSetInfo(8, rulesets.AvailableRulesets.ToArray())); + + Add(detachedBeatmapStore); } public override void SetUpSteps() diff --git a/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsSongSelect.cs b/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsSongSelect.cs index b0b753fc22..cc78bed5de 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsSongSelect.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsSongSelect.cs @@ -12,6 +12,7 @@ using osu.Framework.Platform; using osu.Framework.Screens; using osu.Framework.Utils; using osu.Game.Beatmaps; +using osu.Game.Database; using osu.Game.Online.Rooms; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; @@ -33,13 +34,18 @@ namespace osu.Game.Tests.Visual.Multiplayer [BackgroundDependencyLoader] private void load(GameHost host, AudioManager audio) { + DetachedBeatmapStore detachedBeatmapStore; + Dependencies.Cache(new RealmRulesetStore(Realm)); Dependencies.Cache(manager = new BeatmapManager(LocalStorage, Realm, null, audio, Resources, host, Beatmap.Default)); + Dependencies.Cache(detachedBeatmapStore = new DetachedBeatmapStore()); Dependencies.Cache(Realm); var beatmapSet = TestResources.CreateTestBeatmapSetInfo(); manager.Import(beatmapSet); + + Add(detachedBeatmapStore); } public override void SetUpSteps() diff --git a/osu.Game.Tests/Visual/Navigation/TestScenePresentBeatmap.cs b/osu.Game.Tests/Visual/Navigation/TestScenePresentBeatmap.cs index c054792168..fc711473f2 100644 --- a/osu.Game.Tests/Visual/Navigation/TestScenePresentBeatmap.cs +++ b/osu.Game.Tests/Visual/Navigation/TestScenePresentBeatmap.cs @@ -176,6 +176,12 @@ namespace osu.Game.Tests.Visual.Navigation private void confirmBeatmapInSongSelect(Func getImport) { + AddUntilStep("wait for carousel loaded", () => + { + var songSelect = (Screens.Select.SongSelect)Game.ScreenStack.CurrentScreen; + return songSelect.ChildrenOfType().SingleOrDefault()?.IsLoaded == true; + }); + AddUntilStep("beatmap in song select", () => { var songSelect = (Screens.Select.SongSelect)Game.ScreenStack.CurrentScreen; diff --git a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs index 9df26e0da5..1f298d2d2d 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs @@ -56,16 +56,20 @@ namespace osu.Game.Tests.Visual.SongSelect [BackgroundDependencyLoader] private void load(GameHost host, AudioManager audio) { + DetachedBeatmapStore detachedBeatmapStore; + // These DI caches are required to ensure for interactive runs this test scene doesn't nuke all user beatmaps in the local install. // At a point we have isolated interactive test runs enough, this can likely be removed. Dependencies.Cache(rulesets = new RealmRulesetStore(Realm)); Dependencies.Cache(Realm); Dependencies.Cache(manager = new BeatmapManager(LocalStorage, Realm, null, audio, Resources, host, defaultBeatmap = Beatmap.Default)); + Dependencies.Cache(detachedBeatmapStore = new DetachedBeatmapStore()); Dependencies.Cache(music = new MusicController()); // required to get bindables attached Add(music); + Add(detachedBeatmapStore); Dependencies.Cache(config = new OsuConfigManager(LocalStorage)); } diff --git a/osu.Game/Database/DetachedBeatmapStore.cs b/osu.Game/Database/DetachedBeatmapStore.cs index 4e5ff23f7c..39f0bdaafe 100644 --- a/osu.Game/Database/DetachedBeatmapStore.cs +++ b/osu.Game/Database/DetachedBeatmapStore.cs @@ -24,17 +24,16 @@ namespace osu.Game.Database [Resolved] private RealmAccess realm { get; set; } = null!; - public IBindableList GetDetachedBeatmaps(CancellationToken cancellationToken) + public IBindableList GetDetachedBeatmaps(CancellationToken? cancellationToken) { - loaded.Wait(cancellationToken); + loaded.Wait(cancellationToken ?? CancellationToken.None); return detachedBeatmapSets.GetBoundCopy(); } [BackgroundDependencyLoader] - private void load(CancellationToken cancellationToken) + private void load() { realmSubscription = realm.RegisterForNotifications(r => r.All().Where(s => !s.DeletePending && !s.Protected), beatmapSetsChanged); - loaded.Wait(cancellationToken); } private void beatmapSetsChanged(IRealmCollection sender, ChangeSet? changes) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index ddc8f22c95..63b2bcf7b1 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -241,7 +241,7 @@ namespace osu.Game.Screens.Select } [BackgroundDependencyLoader] - private void load(OsuConfigManager config, AudioManager audio, CancellationToken cancellationToken) + private void load(OsuConfigManager config, AudioManager audio, CancellationToken? cancellationToken) { spinSample = audio.Samples.Get("SongSelect/random-spin"); randomSelectSample = audio.Samples.Get(@"SongSelect/select-random"); @@ -252,12 +252,12 @@ namespace osu.Game.Screens.Select RightClickScrollingEnabled.ValueChanged += enabled => Scroll.RightMouseScrollbar = enabled.NewValue; RightClickScrollingEnabled.TriggerChange(); - if (!loadedTestBeatmaps) + if (!loadedTestBeatmaps && detachedBeatmapStore != null) { // This is performing an unnecessary second lookup on realm (in addition to the subscription), but for performance reasons // we require it to be separate: the subscription's initial callback (with `ChangeSet` of `null`) will run on the update // thread. If we attempt to detach beatmaps in this callback the game will fall over (it takes time). - detachedBeatmapSets = detachedBeatmapStore!.GetDetachedBeatmaps(cancellationToken); + detachedBeatmapSets = detachedBeatmapStore.GetDetachedBeatmaps(cancellationToken); detachedBeatmapSets.BindCollectionChanged(beatmapSetsChanged); loadBeatmapSets(detachedBeatmapSets); } From 853023dfbac4a328d9281eb229fc51d917c84bbe Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 28 Aug 2024 20:14:33 +0900 Subject: [PATCH 2117/2493] Reduce test filter count expectation by one due to initial filter being implicit --- .../SongSelect/TestScenePlaySongSelect.cs | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs index 1f298d2d2d..6b8fa94336 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs @@ -246,7 +246,7 @@ namespace osu.Game.Tests.Visual.SongSelect createSongSelect(); - AddAssert("filter count is 1", () => songSelect?.FilterCount == 1); + AddAssert("filter count is 0", () => songSelect?.FilterCount, () => Is.EqualTo(0)); } [Test] @@ -366,7 +366,7 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep("return", () => songSelect!.MakeCurrent()); AddUntilStep("wait for current", () => songSelect!.IsCurrentScreen()); - AddAssert("filter count is 1", () => songSelect!.FilterCount == 1); + AddAssert("filter count is 0", () => songSelect!.FilterCount, () => Is.EqualTo(0)); } [Test] @@ -386,7 +386,7 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep("return", () => songSelect!.MakeCurrent()); AddUntilStep("wait for current", () => songSelect!.IsCurrentScreen()); - AddAssert("filter count is 2", () => songSelect!.FilterCount == 2); + AddAssert("filter count is 1", () => songSelect!.FilterCount, () => Is.EqualTo(1)); } [Test] @@ -1274,11 +1274,11 @@ namespace osu.Game.Tests.Visual.SongSelect // Mod that is guaranteed to never re-filter. AddStep("add non-filterable mod", () => SelectedMods.Value = new Mod[] { new OsuModCinema() }); - AddAssert("filter count is 1", () => songSelect!.FilterCount, () => Is.EqualTo(1)); + AddAssert("filter count is 0", () => songSelect!.FilterCount, () => Is.EqualTo(0)); // Removing the mod should still not re-filter. AddStep("remove non-filterable mod", () => SelectedMods.Value = Array.Empty()); - AddAssert("filter count is 1", () => songSelect!.FilterCount, () => Is.EqualTo(1)); + AddAssert("filter count is 0", () => songSelect!.FilterCount, () => Is.EqualTo(0)); } [Test] @@ -1290,35 +1290,35 @@ namespace osu.Game.Tests.Visual.SongSelect // Change to mania ruleset. AddStep("filter to mania ruleset", () => Ruleset.Value = rulesets.AvailableRulesets.First(r => r.OnlineID == 3)); - AddAssert("filter count is 2", () => songSelect!.FilterCount, () => Is.EqualTo(2)); + AddAssert("filter count is 2", () => songSelect!.FilterCount, () => Is.EqualTo(1)); // Apply a mod, but this should NOT re-filter because there's no search text. AddStep("add filterable mod", () => SelectedMods.Value = new Mod[] { new ManiaModKey3() }); - AddAssert("filter count is 2", () => songSelect!.FilterCount, () => Is.EqualTo(2)); + AddAssert("filter count is 1", () => songSelect!.FilterCount, () => Is.EqualTo(1)); // Set search text. Should re-filter. AddStep("set search text to match mods", () => songSelect!.FilterControl.CurrentTextSearch.Value = "keys=3"); - AddAssert("filter count is 3", () => songSelect!.FilterCount, () => Is.EqualTo(3)); + AddAssert("filter count is 2", () => songSelect!.FilterCount, () => Is.EqualTo(2)); // Change filterable mod. Should re-filter. AddStep("change new filterable mod", () => SelectedMods.Value = new Mod[] { new ManiaModKey5() }); - AddAssert("filter count is 4", () => songSelect!.FilterCount, () => Is.EqualTo(4)); + AddAssert("filter count is 3", () => songSelect!.FilterCount, () => Is.EqualTo(3)); // Add non-filterable mod. Should NOT re-filter. AddStep("apply non-filterable mod", () => SelectedMods.Value = new Mod[] { new ManiaModNoFail(), new ManiaModKey5() }); - AddAssert("filter count is 4", () => songSelect!.FilterCount, () => Is.EqualTo(4)); + AddAssert("filter count is 3", () => songSelect!.FilterCount, () => Is.EqualTo(3)); // Remove filterable mod. Should re-filter. AddStep("remove filterable mod", () => SelectedMods.Value = new Mod[] { new ManiaModNoFail() }); - AddAssert("filter count is 5", () => songSelect!.FilterCount, () => Is.EqualTo(5)); + AddAssert("filter count is 4", () => songSelect!.FilterCount, () => Is.EqualTo(4)); // Remove non-filterable mod. Should NOT re-filter. AddStep("remove filterable mod", () => SelectedMods.Value = Array.Empty()); - AddAssert("filter count is 5", () => songSelect!.FilterCount, () => Is.EqualTo(5)); + AddAssert("filter count is 4", () => songSelect!.FilterCount, () => Is.EqualTo(4)); // Add filterable mod. Should re-filter. AddStep("add filterable mod", () => SelectedMods.Value = new Mod[] { new ManiaModKey3() }); - AddAssert("filter count is 6", () => songSelect!.FilterCount, () => Is.EqualTo(6)); + AddAssert("filter count is 5", () => songSelect!.FilterCount, () => Is.EqualTo(5)); } private void waitForInitialSelection() From e04b5bb3f260dd32794c00081263b6f7f61b3791 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 28 Aug 2024 19:35:28 +0900 Subject: [PATCH 2118/2493] Tidy up test beatmap loading --- .../SongSelect/TestSceneBeatmapCarousel.cs | 16 ++++++------- osu.Game/Screens/Select/BeatmapCarousel.cs | 23 +++++++++++-------- 2 files changed, 20 insertions(+), 19 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs index a075559f6a..ec072a3dd2 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs @@ -6,6 +6,7 @@ using System; using System.Collections.Generic; using System.Linq; +using JetBrains.Annotations; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Extensions.IEnumerableExtensions; @@ -1268,26 +1269,23 @@ namespace osu.Game.Tests.Visual.SongSelect } } - createCarousel(beatmapSets, c => + createCarousel(beatmapSets, initialCriteria, c => { - carouselAdjust?.Invoke(c); - - carousel.Filter(initialCriteria?.Invoke() ?? new FilterCriteria()); carousel.BeatmapSetsChanged = () => changed = true; - carousel.BeatmapSets = beatmapSets; + carouselAdjust?.Invoke(c); }); AddUntilStep("Wait for load", () => changed); } - private void createCarousel(List beatmapSets, Action carouselAdjust = null, Container target = null) + private void createCarousel(List beatmapSets, [CanBeNull] Func initialCriteria = null, Action carouselAdjust = null, Container target = null) { AddStep("Create carousel", () => { selectedSets.Clear(); eagerSelectedIDs.Clear(); - carousel = new TestBeatmapCarousel + carousel = new TestBeatmapCarousel(initialCriteria?.Invoke() ?? new FilterCriteria()) { RelativeSizeAxes = Axes.Both, }; @@ -1389,8 +1387,8 @@ namespace osu.Game.Tests.Visual.SongSelect private partial class TestBeatmapCarousel : BeatmapCarousel { - public TestBeatmapCarousel() - : base(new FilterCriteria()) + public TestBeatmapCarousel(FilterCriteria criteria) + : base(criteria) { } diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 63b2bcf7b1..20899d1869 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -130,18 +130,22 @@ namespace osu.Game.Screens.Select get => beatmapSets.Select(g => g.BeatmapSet); set { + if (LoadState != LoadState.NotLoaded) + throw new InvalidOperationException("If not using a realm source, beatmap sets must be set before load."); + loadedTestBeatmaps = true; - Schedule(() => loadBeatmapSets(value)); + detachedBeatmapSets = new BindableList(value); + Schedule(loadNewRoot); } } - private void loadBeatmapSets(IEnumerable beatmapSets) + private void loadNewRoot() { // Ensure no changes are made to the list while we are initialising items. // We'll catch up on changes via subscriptions anyway. - beatmapSets = beatmapSets.ToArray(); + BeatmapSetInfo[] loadableSets = detachedBeatmapSets.ToArray(); - if (selectedBeatmapSet != null && !beatmapSets.Contains(selectedBeatmapSet.BeatmapSet)) + if (selectedBeatmapSet != null && !loadableSets.Contains(selectedBeatmapSet.BeatmapSet)) selectedBeatmapSet = null; var selectedBeatmapBefore = selectedBeatmap?.BeatmapInfo; @@ -150,7 +154,7 @@ namespace osu.Game.Screens.Select if (beatmapsSplitOut) { - var carouselBeatmapSets = beatmapSets.SelectMany(s => s.Beatmaps).Select(b => + var carouselBeatmapSets = loadableSets.SelectMany(s => s.Beatmaps).Select(b => { return createCarouselSet(new BeatmapSetInfo(new[] { b }) { @@ -164,7 +168,7 @@ namespace osu.Game.Screens.Select } else { - var carouselBeatmapSets = beatmapSets.Select(createCarouselSet).OfType(); + var carouselBeatmapSets = loadableSets.Select(createCarouselSet).OfType(); newRoot.AddItems(carouselBeatmapSets); } @@ -259,7 +263,7 @@ namespace osu.Game.Screens.Select // thread. If we attempt to detach beatmaps in this callback the game will fall over (it takes time). detachedBeatmapSets = detachedBeatmapStore.GetDetachedBeatmaps(cancellationToken); detachedBeatmapSets.BindCollectionChanged(beatmapSetsChanged); - loadBeatmapSets(detachedBeatmapSets); + loadNewRoot(); } } @@ -309,8 +313,7 @@ namespace osu.Game.Screens.Select case NotifyCollectionChangedAction.Reset: setsRequiringRemoval.Clear(); setsRequiringUpdate.Clear(); - - loadBeatmapSets(detachedBeatmapSets); + loadNewRoot(); break; } @@ -733,7 +736,7 @@ namespace osu.Game.Screens.Select if (activeCriteria.SplitOutDifficulties != beatmapsSplitOut) { beatmapsSplitOut = activeCriteria.SplitOutDifficulties; - loadBeatmapSets(detachedBeatmapSets); + loadNewRoot(); return; } From 1776d38809fbea7994614c34c489a7d740832089 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 28 Aug 2024 20:06:44 +0900 Subject: [PATCH 2119/2493] Remove `loadedTestBeatmaps` flag --- osu.Game/Screens/Select/BeatmapCarousel.cs | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 20899d1869..7f6921d768 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -116,15 +116,12 @@ namespace osu.Game.Screens.Select [Resolved] private DetachedBeatmapStore? detachedBeatmapStore { get; set; } - private IBindableList detachedBeatmapSets = null!; + private IBindableList? detachedBeatmapSets; private readonly NoResultsPlaceholder noResultsPlaceholder; private IEnumerable beatmapSets => root.Items.OfType(); - // todo: only used for testing, maybe remove. - private bool loadedTestBeatmaps; - public IEnumerable BeatmapSets { get => beatmapSets.Select(g => g.BeatmapSet); @@ -133,7 +130,6 @@ namespace osu.Game.Screens.Select if (LoadState != LoadState.NotLoaded) throw new InvalidOperationException("If not using a realm source, beatmap sets must be set before load."); - loadedTestBeatmaps = true; detachedBeatmapSets = new BindableList(value); Schedule(loadNewRoot); } @@ -143,7 +139,7 @@ namespace osu.Game.Screens.Select { // Ensure no changes are made to the list while we are initialising items. // We'll catch up on changes via subscriptions anyway. - BeatmapSetInfo[] loadableSets = detachedBeatmapSets.ToArray(); + BeatmapSetInfo[] loadableSets = detachedBeatmapSets!.ToArray(); if (selectedBeatmapSet != null && !loadableSets.Contains(selectedBeatmapSet.BeatmapSet)) selectedBeatmapSet = null; @@ -256,7 +252,7 @@ namespace osu.Game.Screens.Select RightClickScrollingEnabled.ValueChanged += enabled => Scroll.RightMouseScrollbar = enabled.NewValue; RightClickScrollingEnabled.TriggerChange(); - if (!loadedTestBeatmaps && detachedBeatmapStore != null) + if (detachedBeatmapStore != null && detachedBeatmapSets == null) { // This is performing an unnecessary second lookup on realm (in addition to the subscription), but for performance reasons // we require it to be separate: the subscription's initial callback (with `ChangeSet` of `null`) will run on the update @@ -279,10 +275,6 @@ namespace osu.Game.Screens.Select private void beatmapSetsChanged(object? beatmaps, NotifyCollectionChangedEventArgs changed) { - // If loading test beatmaps, avoid overwriting with realm subscription callbacks. - if (loadedTestBeatmaps) - return; - IEnumerable? newBeatmapSets = changed.NewItems?.Cast(); switch (changed.Action) From f0b2176c300cf121a2502211e8b5835a7e78a03b Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Wed, 28 Aug 2024 22:58:57 -0700 Subject: [PATCH 2120/2493] Add failing pinned comment replies state test --- .../Visual/Online/TestSceneCommentsContainer.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneCommentsContainer.cs b/osu.Game.Tests/Visual/Online/TestSceneCommentsContainer.cs index acc3c9b8b4..eb805b27cb 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneCommentsContainer.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneCommentsContainer.cs @@ -17,6 +17,7 @@ using osu.Game.Online.API; using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays.Comments; +using osu.Game.Overlays.Comments.Buttons; namespace osu.Game.Tests.Visual.Online { @@ -58,6 +59,11 @@ namespace osu.Game.Tests.Visual.Online AddStep("show comments", () => commentsContainer.ShowComments(CommentableType.Beatmapset, 123)); AddUntilStep("show more button hidden", () => commentsContainer.ChildrenOfType().Single().Alpha == 0); + + if (withPinned) + AddAssert("pinned comment replies collapsed", () => commentsContainer.ChildrenOfType().First().Expanded.Value, () => Is.False); + else + AddAssert("first comment replies expanded", () => commentsContainer.ChildrenOfType().First().Expanded.Value, () => Is.True); } [TestCase(false)] @@ -302,7 +308,7 @@ namespace osu.Game.Tests.Visual.Online bundle.Comments.Add(new Comment { Id = 20, - Message = "Reply to pinned comment", + Message = "Reply to pinned comment initially hidden", LegacyName = "AbandonedUser", CreatedAt = DateTimeOffset.Now, VotesCount = 0, From ef443b0b5d191110947c23e151c0878607fb2b0b Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Wed, 28 Aug 2024 23:00:16 -0700 Subject: [PATCH 2121/2493] Hide pinned comment replies initially to match web --- osu.Game/Overlays/Comments/DrawableComment.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Comments/DrawableComment.cs b/osu.Game/Overlays/Comments/DrawableComment.cs index afd4b96c68..296f90872e 100644 --- a/osu.Game/Overlays/Comments/DrawableComment.cs +++ b/osu.Game/Overlays/Comments/DrawableComment.cs @@ -47,7 +47,7 @@ namespace osu.Game.Overlays.Comments public readonly BindableList Replies = new BindableList(); - private readonly BindableBool childrenExpanded = new BindableBool(true); + private readonly BindableBool childrenExpanded; private int currentPage; @@ -92,6 +92,8 @@ namespace osu.Game.Overlays.Comments { Comment = comment; Meta = meta; + + childrenExpanded = new BindableBool(!comment.Pinned); } [BackgroundDependencyLoader] From def1abaeca06161faee6422d8efbe1c68b03c4f3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 28 Aug 2024 23:29:32 +0900 Subject: [PATCH 2122/2493] Fix some tests not always waiting long enough for beatmap loading These used to work because there was a huge blocking load operation, which is now more asynchronous. Note that the change made in `SongSelect` is not required, but defensive (feels it should have been doing this the whole time). --- .../Visual/Editing/TestSceneOpenEditorTimestamp.cs | 4 ++-- .../Navigation/TestSceneBeatmapEditorNavigation.cs | 14 ++++++++------ .../Visual/Navigation/TestScenePresentBeatmap.cs | 4 ++-- .../Visual/Navigation/TestSceneScreenNavigation.cs | 6 ++++-- osu.Game/Screens/Select/SongSelect.cs | 3 ++- 5 files changed, 18 insertions(+), 13 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneOpenEditorTimestamp.cs b/osu.Game.Tests/Visual/Editing/TestSceneOpenEditorTimestamp.cs index 1f46a08831..971eb223eb 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneOpenEditorTimestamp.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneOpenEditorTimestamp.cs @@ -36,7 +36,7 @@ namespace osu.Game.Tests.Visual.Editing () => Is.EqualTo(1)); AddStep("enter song select", () => Game.ChildrenOfType().Single().OnSolo?.Invoke()); - AddUntilStep("entered song select", () => Game.ScreenStack.CurrentScreen is PlaySongSelect); + AddUntilStep("entered song select", () => Game.ScreenStack.CurrentScreen is PlaySongSelect songSelect && songSelect.BeatmapSetsLoaded); addStepClickLink("00:00:000 (1)", waitForSeek: false); AddUntilStep("received 'must be in edit'", @@ -138,7 +138,7 @@ namespace osu.Game.Tests.Visual.Editing AddUntilStep("Wait for song select", () => Game.Beatmap.Value.BeatmapSetInfo.Equals(beatmapSet) && Game.ScreenStack.CurrentScreen is PlaySongSelect songSelect - && songSelect.IsLoaded + && songSelect.BeatmapSetsLoaded ); AddStep("Switch ruleset", () => Game.Ruleset.Value = ruleset); AddStep("Open editor for ruleset", () => diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneBeatmapEditorNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneBeatmapEditorNavigation.cs index 5640682d06..d76e0290ef 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneBeatmapEditorNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneBeatmapEditorNavigation.cs @@ -165,16 +165,19 @@ namespace osu.Game.Tests.Visual.Navigation } [Test] + [Solo] public void TestEditorGameplayTestAlwaysUsesOriginalRuleset() { prepareBeatmap(); - AddStep("switch ruleset", () => Game.Ruleset.Value = new ManiaRuleset().RulesetInfo); + AddStep("switch ruleset at song select", () => Game.Ruleset.Value = new ManiaRuleset().RulesetInfo); AddStep("open editor", () => ((PlaySongSelect)Game.ScreenStack.CurrentScreen).Edit(beatmapSet.Beatmaps.First(beatmap => beatmap.Ruleset.OnlineID == 0))); - AddUntilStep("wait for editor open", () => Game.ScreenStack.CurrentScreen is Editor editor && editor.ReadyForUse); - AddStep("test gameplay", () => getEditor().TestGameplay()); + AddUntilStep("wait for editor open", () => Game.ScreenStack.CurrentScreen is Editor editor && editor.ReadyForUse); + AddAssert("editor ruleset is osu!", () => Game.Ruleset.Value, () => Is.EqualTo(new OsuRuleset().RulesetInfo)); + + AddStep("test gameplay", () => getEditor().TestGameplay()); AddUntilStep("wait for player", () => { // notifications may fire at almost any inopportune time and cause annoying test failures. @@ -183,8 +186,7 @@ namespace osu.Game.Tests.Visual.Navigation Game.CloseAllOverlays(); return Game.ScreenStack.CurrentScreen is EditorPlayer editorPlayer && editorPlayer.IsLoaded; }); - - AddAssert("current ruleset is osu!", () => Game.Ruleset.Value.Equals(new OsuRuleset().RulesetInfo)); + AddAssert("gameplay ruleset is osu!", () => Game.Ruleset.Value, () => Is.EqualTo(new OsuRuleset().RulesetInfo)); AddStep("exit to song select", () => Game.PerformFromScreen(_ => { }, typeof(PlaySongSelect).Yield())); AddUntilStep("wait for song select", () => Game.ScreenStack.CurrentScreen is PlaySongSelect); @@ -352,7 +354,7 @@ namespace osu.Game.Tests.Visual.Navigation AddUntilStep("wait for song select", () => Game.Beatmap.Value.BeatmapSetInfo.Equals(beatmapSet) && Game.ScreenStack.CurrentScreen is PlaySongSelect songSelect - && songSelect.IsLoaded); + && songSelect.BeatmapSetsLoaded); } private void openEditor() diff --git a/osu.Game.Tests/Visual/Navigation/TestScenePresentBeatmap.cs b/osu.Game.Tests/Visual/Navigation/TestScenePresentBeatmap.cs index fc711473f2..f036b4b3ef 100644 --- a/osu.Game.Tests/Visual/Navigation/TestScenePresentBeatmap.cs +++ b/osu.Game.Tests/Visual/Navigation/TestScenePresentBeatmap.cs @@ -193,7 +193,7 @@ namespace osu.Game.Tests.Visual.Navigation { AddStep("present beatmap", () => Game.PresentBeatmap(getImport())); - AddUntilStep("wait for song select", () => Game.ScreenStack.CurrentScreen is Screens.Select.SongSelect songSelect && songSelect.IsLoaded); + AddUntilStep("wait for song select", () => Game.ScreenStack.CurrentScreen is Screens.Select.SongSelect songSelect && songSelect.BeatmapSetsLoaded); AddUntilStep("correct beatmap displayed", () => Game.Beatmap.Value.BeatmapSetInfo.OnlineID, () => Is.EqualTo(getImport().OnlineID)); AddAssert("correct ruleset selected", () => Game.Ruleset.Value, () => Is.EqualTo(getImport().Beatmaps.First().Ruleset)); } @@ -203,7 +203,7 @@ namespace osu.Game.Tests.Visual.Navigation Predicate pred = b => b.OnlineID == importedID * 1024 + 2; AddStep("present difficulty", () => Game.PresentBeatmap(getImport(), pred)); - AddUntilStep("wait for song select", () => Game.ScreenStack.CurrentScreen is Screens.Select.SongSelect songSelect && songSelect.IsLoaded); + AddUntilStep("wait for song select", () => Game.ScreenStack.CurrentScreen is Screens.Select.SongSelect songSelect && songSelect.BeatmapSetsLoaded); AddUntilStep("correct beatmap displayed", () => Game.Beatmap.Value.BeatmapInfo.OnlineID, () => Is.EqualTo(importedID * 1024 + 2)); AddAssert("correct ruleset selected", () => Game.Ruleset.Value.OnlineID, () => Is.EqualTo(expectedRulesetOnlineID ?? getImport().Beatmaps.First().Ruleset.OnlineID)); } diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs index db9ecd90b9..f02c2fd4f0 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs @@ -1035,9 +1035,11 @@ namespace osu.Game.Tests.Visual.Navigation [Test] public void TestTouchScreenDetectionInGame() { + BeatmapSetInfo beatmapSet = null; + PushAndConfirm(() => new TestPlaySongSelect()); - AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely()); - AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault); + AddStep("import beatmap", () => beatmapSet = BeatmapImportHelper.LoadQuickOszIntoOsu(Game).GetResultSafely()); + AddUntilStep("wait for selected", () => Game.Beatmap.Value.BeatmapSetInfo.Equals(beatmapSet)); AddStep("select", () => InputManager.Key(Key.Enter)); Player player = null; diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index bfbc50378a..6da72ee660 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -428,7 +428,8 @@ namespace osu.Game.Screens.Select // Forced refetch is important here to guarantee correct invalidation across all difficulties. Beatmap.Value = beatmaps.GetWorkingBeatmap(beatmapInfo ?? beatmapInfoNoDebounce, true); - this.Push(new EditorLoader()); + + FinaliseSelection(customStartAction: () => this.Push(new EditorLoader())); } /// From d1d2591b6737c9fa6ee5806d6c2c1038db0aba57 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 29 Aug 2024 18:29:58 +0900 Subject: [PATCH 2123/2493] Fix realm changes being applied before detach finishes --- osu.Game/Database/DetachedBeatmapStore.cs | 85 +++++++++++++++++------ 1 file changed, 63 insertions(+), 22 deletions(-) diff --git a/osu.Game/Database/DetachedBeatmapStore.cs b/osu.Game/Database/DetachedBeatmapStore.cs index 39f0bdaafe..17d2dd15b6 100644 --- a/osu.Game/Database/DetachedBeatmapStore.cs +++ b/osu.Game/Database/DetachedBeatmapStore.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -21,6 +22,8 @@ namespace osu.Game.Database private IDisposable? realmSubscription; + private readonly Queue pendingOperations = new Queue(); + [Resolved] private RealmAccess realm { get; set; } = null!; @@ -70,37 +73,61 @@ namespace osu.Game.Database } foreach (int i in changes.DeletedIndices.OrderDescending()) - removeAt(i); + { + pendingOperations.Enqueue(new OperationArgs + { + Type = OperationType.Remove, + Index = i, + }); + } foreach (int i in changes.InsertedIndices) - insert(sender[i].Detach(), i); + { + pendingOperations.Enqueue(new OperationArgs + { + Type = OperationType.Insert, + BeatmapSet = sender[i].Detach(), + Index = i, + }); + } foreach (int i in changes.NewModifiedIndices) - replaceRange(sender[i].Detach(), i); + { + pendingOperations.Enqueue(new OperationArgs + { + Type = OperationType.Update, + BeatmapSet = sender[i].Detach(), + Index = i, + }); + } } - private void replaceRange(BeatmapSetInfo set, int i) + protected override void Update() { - if (loaded.IsSet) - detachedBeatmapSets.ReplaceRange(i, 1, new[] { set }); - else - Schedule(() => { detachedBeatmapSets.ReplaceRange(i, 1, new[] { set }); }); - } + base.Update(); - private void insert(BeatmapSetInfo set, int i) - { - if (loaded.IsSet) - detachedBeatmapSets.Insert(i, set); - else - Schedule(() => { detachedBeatmapSets.Insert(i, set); }); - } + // We can't start processing operations until we have finished detaching the initial list. + if (!loaded.IsSet) + return; - private void removeAt(int i) - { - if (loaded.IsSet) - detachedBeatmapSets.RemoveAt(i); - else - Schedule(() => { detachedBeatmapSets.RemoveAt(i); }); + // If this ever leads to performance issues, we could dequeue a limited number of operations per update frame. + while (pendingOperations.TryDequeue(out var op)) + { + switch (op.Type) + { + case OperationType.Insert: + detachedBeatmapSets.Insert(op.Index, op.BeatmapSet!); + break; + + case OperationType.Update: + detachedBeatmapSets.ReplaceRange(op.Index, 1, new[] { op.BeatmapSet! }); + break; + + case OperationType.Remove: + detachedBeatmapSets.RemoveAt(op.Index); + break; + } + } } protected override void Dispose(bool isDisposing) @@ -109,5 +136,19 @@ namespace osu.Game.Database loaded.Set(); realmSubscription?.Dispose(); } + + private record OperationArgs + { + public OperationType Type; + public BeatmapSetInfo? BeatmapSet; + public int Index; + } + + private enum OperationType + { + Insert, + Update, + Remove + } } } From 97adac2e0ae235eff15213e6f89dc6dcddd245a6 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 29 Aug 2024 15:31:02 +0900 Subject: [PATCH 2124/2493] Add test + adjust existing ones with new semantics --- .../Filtering/FilterQueryParserTest.cs | 35 +++++++++++++++---- 1 file changed, 29 insertions(+), 6 deletions(-) diff --git a/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs b/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs index 7897b3d8c0..e6006b7fd2 100644 --- a/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs +++ b/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs @@ -537,7 +537,7 @@ namespace osu.Game.Tests.NonVisual.Filtering [TestCaseSource(nameof(correct_date_query_examples))] public void TestValidDateQueries(string dateQuery) { - string query = $"played<{dateQuery} time"; + string query = $"lastplayed<{dateQuery} time"; var filterCriteria = new FilterCriteria(); FilterQueryParser.ApplyQueries(filterCriteria, query); Assert.AreEqual(true, filterCriteria.LastPlayed.HasFilter); @@ -571,7 +571,7 @@ namespace osu.Game.Tests.NonVisual.Filtering [Test] public void TestGreaterDateQuery() { - const string query = "played>50"; + const string query = "lastplayed>50"; var filterCriteria = new FilterCriteria(); FilterQueryParser.ApplyQueries(filterCriteria, query); Assert.That(filterCriteria.LastPlayed.Max, Is.Not.Null); @@ -584,7 +584,7 @@ namespace osu.Game.Tests.NonVisual.Filtering [Test] public void TestLowerDateQuery() { - const string query = "played<50"; + const string query = "lastplayed<50"; var filterCriteria = new FilterCriteria(); FilterQueryParser.ApplyQueries(filterCriteria, query); Assert.That(filterCriteria.LastPlayed.Max, Is.Null); @@ -597,7 +597,7 @@ namespace osu.Game.Tests.NonVisual.Filtering [Test] public void TestBothSidesDateQuery() { - const string query = "played>3M played<1y6M"; + const string query = "lastplayed>3M lastplayed<1y6M"; var filterCriteria = new FilterCriteria(); FilterQueryParser.ApplyQueries(filterCriteria, query); Assert.That(filterCriteria.LastPlayed.Min, Is.Not.Null); @@ -611,7 +611,7 @@ namespace osu.Game.Tests.NonVisual.Filtering [Test] public void TestEqualDateQuery() { - const string query = "played=50"; + const string query = "lastplayed=50"; var filterCriteria = new FilterCriteria(); FilterQueryParser.ApplyQueries(filterCriteria, query); Assert.AreEqual(false, filterCriteria.LastPlayed.HasFilter); @@ -620,11 +620,34 @@ namespace osu.Game.Tests.NonVisual.Filtering [Test] public void TestOutOfRangeDateQuery() { - const string query = "played<10000y"; + const string query = "lastplayed<10000y"; var filterCriteria = new FilterCriteria(); FilterQueryParser.ApplyQueries(filterCriteria, query); Assert.AreEqual(true, filterCriteria.LastPlayed.HasFilter); Assert.AreEqual(DateTimeOffset.MinValue.AddMilliseconds(1), filterCriteria.LastPlayed.Min); } + + private static readonly object[] played_query_tests = + { + new object[] { "0", DateTimeOffset.MinValue, true }, + new object[] { "0", DateTimeOffset.Now, false }, + new object[] { "false", DateTimeOffset.MinValue, true }, + new object[] { "false", DateTimeOffset.Now, false }, + + new object[] { "1", DateTimeOffset.MinValue, false }, + new object[] { "1", DateTimeOffset.Now, true }, + new object[] { "true", DateTimeOffset.MinValue, false }, + new object[] { "true", DateTimeOffset.Now, true }, + }; + + [Test] + [TestCaseSource(nameof(played_query_tests))] + public void TestPlayedQuery(string query, DateTimeOffset reference, bool matched) + { + var filterCriteria = new FilterCriteria(); + FilterQueryParser.ApplyQueries(filterCriteria, $"played={query}"); + Assert.AreEqual(true, filterCriteria.LastPlayed.HasFilter); + Assert.AreEqual(matched, filterCriteria.LastPlayed.IsInRange(reference)); + } } } From fde790c014179ab88a381918dc3b8e5354f8173d Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 29 Aug 2024 15:32:35 +0900 Subject: [PATCH 2125/2493] Rework `played` filter to a boolean value --- osu.Game/Screens/Select/FilterQueryParser.cs | 40 +++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Select/FilterQueryParser.cs b/osu.Game/Screens/Select/FilterQueryParser.cs index 40fd289be6..3e0dba59f0 100644 --- a/osu.Game/Screens/Select/FilterQueryParser.cs +++ b/osu.Game/Screens/Select/FilterQueryParser.cs @@ -62,10 +62,31 @@ namespace osu.Game.Screens.Select case "length": return tryUpdateLengthRange(criteria, op, value); - case "played": case "lastplayed": return tryUpdateDateAgoRange(ref criteria.LastPlayed, op, value); + case "played": + if (!tryParseBool(value, out bool played)) + return false; + + // Unplayed beatmaps are filtered on DateTimeOffset.MinValue. + + if (played) + { + criteria.LastPlayed.Min = DateTimeOffset.MinValue; + criteria.LastPlayed.Max = DateTimeOffset.MaxValue; + criteria.LastPlayed.IsLowerInclusive = false; + } + else + { + criteria.LastPlayed.Min = DateTimeOffset.MinValue; + criteria.LastPlayed.Max = DateTimeOffset.MinValue; + criteria.LastPlayed.IsLowerInclusive = true; + criteria.LastPlayed.IsUpperInclusive = true; + } + + return true; + case "divisor": return TryUpdateCriteriaRange(ref criteria.BeatDivisor, op, value, tryParseInt); @@ -133,6 +154,23 @@ namespace osu.Game.Screens.Select private static bool tryParseInt(string value, out int result) => int.TryParse(value, NumberStyles.None, CultureInfo.InvariantCulture, out result); + private static bool tryParseBool(string value, out bool result) + { + switch (value) + { + case "1": + result = true; + return true; + + case "0": + result = false; + return true; + + default: + return bool.TryParse(value, out result); + } + } + private static bool tryParseEnum(string value, out TEnum result) where TEnum : struct { // First try an exact match. From 7435e8aa00a35e91b5334126384eae7182ad1ed2 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 30 Aug 2024 00:48:53 +0900 Subject: [PATCH 2126/2493] Fix catch auto generator not considering circle size --- osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs b/osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs index 7c84cb24f3..7c62f9692f 100644 --- a/osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs +++ b/osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs @@ -16,9 +16,12 @@ namespace osu.Game.Rulesets.Catch.Replays { public new CatchBeatmap Beatmap => (CatchBeatmap)base.Beatmap; + private readonly float halfCatcherWidth; + public CatchAutoGenerator(IBeatmap beatmap) : base(beatmap) { + halfCatcherWidth = Catcher.CalculateCatchWidth(beatmap.Difficulty) * 0.5f; } protected override void GenerateFrames() @@ -47,10 +50,7 @@ namespace osu.Game.Rulesets.Catch.Replays bool dashRequired = speedRequired > Catcher.BASE_WALK_SPEED; bool impossibleJump = speedRequired > Catcher.BASE_DASH_SPEED; - // todo: get correct catcher size, based on difficulty CS. - const float catcher_width_half = Catcher.BASE_SIZE * 0.3f * 0.5f; - - if (lastPosition - catcher_width_half < h.EffectiveX && lastPosition + catcher_width_half > h.EffectiveX) + if (lastPosition - halfCatcherWidth < h.EffectiveX && lastPosition + halfCatcherWidth > h.EffectiveX) { // we are already in the correct range. lastTime = h.StartTime; From 8fe7ab131ca810d3397603aa6dee0e67237b5911 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Thu, 29 Aug 2024 19:34:14 +0200 Subject: [PATCH 2127/2493] dont seek on right-click, only on keyboard request --- .../Components/Timeline/SamplePointPiece.cs | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs index 488cd288e4..a8cf8723f2 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs @@ -72,8 +72,10 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline private void onShowSampleEditPopoverRequested(double time) { - if (Precision.AlmostEquals(time, GetTime())) - this.ShowPopover(); + if (!Precision.AlmostEquals(time, GetTime())) return; + + editorClock?.SeekSmoothlyTo(GetTime()); + this.ShowPopover(); } protected override bool OnClick(ClickEvent e) @@ -82,14 +84,6 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline return true; } - protected override void OnMouseUp(MouseUpEvent e) - { - if (e.Button != MouseButton.Right) return; - - editorClock?.SeekSmoothlyTo(GetTime()); - this.ShowPopover(); - } - private void updateText() { Label.Text = $"{abbreviateBank(GetBankValue(GetSamples()))} {GetVolumeValue(GetSamples())}"; From 3a1afda2b3c41a9756675b85d878b9d21901fdeb Mon Sep 17 00:00:00 2001 From: OliBomby Date: Thu, 29 Aug 2024 22:22:15 +0200 Subject: [PATCH 2128/2493] fix test --- .../Editing/TestSceneHitObjectSampleAdjustments.cs | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneHitObjectSampleAdjustments.cs b/osu.Game.Tests/Visual/Editing/TestSceneHitObjectSampleAdjustments.cs index 3e663aea0f..3c5277a4d9 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneHitObjectSampleAdjustments.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneHitObjectSampleAdjustments.cs @@ -332,15 +332,6 @@ namespace osu.Game.Tests.Visual.Editing }); }); - clickNodeSamplePiece(0, 0); - editorTimeIs(0); - clickNodeSamplePiece(0, 1); - editorTimeIs(813); - clickNodeSamplePiece(0, 2); - editorTimeIs(1627); - clickSamplePiece(0); - editorTimeIs(406); - seekSamplePiece(-1); editorTimeIs(0); samplePopoverIsOpen(); @@ -692,11 +683,11 @@ namespace osu.Game.Tests.Visual.Editing private void seekSamplePiece(int direction) => AddStep($"seek sample piece {direction}", () => { + InputManager.PressKey(Key.ControlLeft); InputManager.PressKey(Key.ShiftLeft); - InputManager.PressKey(Key.AltLeft); InputManager.Key(direction < 1 ? Key.Left : Key.Right); - InputManager.ReleaseKey(Key.AltLeft); InputManager.ReleaseKey(Key.ShiftLeft); + InputManager.ReleaseKey(Key.ControlLeft); }); private void samplePopoverIsOpen() => AddUntilStep("sample popover is open", () => From 3bc42db3a612a0fdd97aeeaf0a94d4588b088326 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 30 Aug 2024 16:13:30 +0900 Subject: [PATCH 2129/2493] Fix event leak in `Multiplayer` implementation Very likely closes #29088. It's the only thing I could find odd in the memory dump. --- osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs index 7d27725775..bf316bb3da 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs @@ -108,7 +108,10 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer base.Dispose(isDisposing); if (client.IsNotNull()) + { client.RoomUpdated -= onRoomUpdated; + client.GameplayAborted -= onGameplayAborted; + } } } } From 7f41d5f4e7e7fa0b27192a2eb5ba85045508e8a1 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 30 Aug 2024 16:32:15 +0900 Subject: [PATCH 2130/2493] Remove mouse input from mania touch controls --- .../UI/ManiaTouchInputArea.cs | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/osu.Game.Rulesets.Mania/UI/ManiaTouchInputArea.cs b/osu.Game.Rulesets.Mania/UI/ManiaTouchInputArea.cs index 453b75ac84..8c4a71cf24 100644 --- a/osu.Game.Rulesets.Mania/UI/ManiaTouchInputArea.cs +++ b/osu.Game.Rulesets.Mania/UI/ManiaTouchInputArea.cs @@ -99,12 +99,6 @@ namespace osu.Game.Rulesets.Mania.UI return false; } - protected override bool OnMouseDown(MouseDownEvent e) - { - Show(); - return true; - } - protected override bool OnTouchDown(TouchDownEvent e) { Show(); @@ -172,17 +166,6 @@ namespace osu.Game.Rulesets.Mania.UI updateButton(false); } - protected override bool OnMouseDown(MouseDownEvent e) - { - updateButton(true); - return false; // handled by parent container to show overlay. - } - - protected override void OnMouseUp(MouseUpEvent e) - { - updateButton(false); - } - private void updateButton(bool press) { if (press == isPressed) From 5836f497ac13d168ab077946a67f8d349079794f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 30 Aug 2024 18:03:30 +0900 Subject: [PATCH 2131/2493] Provide API context earlier to api requests in order to fix missing schedules Closes https://github.com/ppy/osu/issues/29546. --- osu.Game/Online/API/APIAccess.cs | 8 +++++-- osu.Game/Online/API/APIRequest.cs | 37 +++++++++++++++++++++---------- 2 files changed, 31 insertions(+), 14 deletions(-) diff --git a/osu.Game/Online/API/APIAccess.cs b/osu.Game/Online/API/APIAccess.cs index 716d1e4466..a9ad561163 100644 --- a/osu.Game/Online/API/APIAccess.cs +++ b/osu.Game/Online/API/APIAccess.cs @@ -385,7 +385,8 @@ namespace osu.Game.Online.API { try { - request.Perform(this); + request.AttachAPI(this); + request.Perform(); } catch (Exception e) { @@ -483,7 +484,8 @@ namespace osu.Game.Online.API { try { - req.Perform(this); + req.AttachAPI(this); + req.Perform(); if (req.CompletionState != APIRequestCompletionState.Completed) return false; @@ -568,6 +570,8 @@ namespace osu.Game.Online.API { lock (queue) { + request.AttachAPI(this); + if (state.Value == APIState.Offline) { request.Fail(new WebException(@"User not logged in")); diff --git a/osu.Game/Online/API/APIRequest.cs b/osu.Game/Online/API/APIRequest.cs index 6b6b222043..d062b8f3de 100644 --- a/osu.Game/Online/API/APIRequest.cs +++ b/osu.Game/Online/API/APIRequest.cs @@ -4,6 +4,7 @@ #nullable disable using System; +using System.Diagnostics; using System.Globalization; using JetBrains.Annotations; using Newtonsoft.Json; @@ -74,6 +75,7 @@ namespace osu.Game.Online.API protected virtual string Uri => $@"{API.APIEndpointUrl}/api/v2/{Target}"; protected APIAccess API; + protected WebRequest WebRequest; /// @@ -101,16 +103,29 @@ namespace osu.Game.Online.API /// public APIRequestCompletionState CompletionState { get; private set; } - public void Perform(IAPIProvider api) + /// + /// Should be called before to give API context. + /// + /// + /// This allows scheduling of operations back to the correct thread (which may be required before is called). + /// + public void AttachAPI(APIAccess apiAccess) { - if (!(api is APIAccess apiAccess)) + if (API != null && API != apiAccess) + throw new InvalidOperationException("Attached API cannot be changed after initial set."); + + API = apiAccess; + } + + public void Perform() + { + if (API == null) { Fail(new NotSupportedException($"A {nameof(APIAccess)} is required to perform requests.")); return; } - API = apiAccess; - User = apiAccess.LocalUser.Value; + User = API.LocalUser.Value; if (isFailing) return; @@ -153,6 +168,8 @@ namespace osu.Game.Online.API internal void TriggerSuccess() { + Debug.Assert(API != null); + lock (completionStateLock) { if (CompletionState != APIRequestCompletionState.Waiting) @@ -161,14 +178,13 @@ namespace osu.Game.Online.API CompletionState = APIRequestCompletionState.Completed; } - if (API == null) - Success?.Invoke(); - else - API.Schedule(() => Success?.Invoke()); + API.Schedule(() => Success?.Invoke()); } internal void TriggerFailure(Exception e) { + Debug.Assert(API != null); + lock (completionStateLock) { if (CompletionState != APIRequestCompletionState.Waiting) @@ -177,10 +193,7 @@ namespace osu.Game.Online.API CompletionState = APIRequestCompletionState.Failed; } - if (API == null) - Failure?.Invoke(e); - else - API.Schedule(() => Failure?.Invoke(e)); + API.Schedule(() => Failure?.Invoke(e)); } public void Cancel() => Fail(new OperationCanceledException(@"Request cancelled")); From 07611bd8f5f30617a78649cc9eb513c89844a552 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 30 Aug 2024 18:10:33 +0900 Subject: [PATCH 2132/2493] Use `IAPIProvider` interface and correctly support scheduling from `DummyAPIAccess` --- osu.Game/Online/API/APIAccess.cs | 2 +- osu.Game/Online/API/APIRequest.cs | 4 ++-- osu.Game/Online/API/DummyAPIAccess.cs | 13 ++++++++++++- osu.Game/Online/API/IAPIProvider.cs | 5 +++++ 4 files changed, 20 insertions(+), 4 deletions(-) diff --git a/osu.Game/Online/API/APIAccess.cs b/osu.Game/Online/API/APIAccess.cs index a9ad561163..a9ccbf9b18 100644 --- a/osu.Game/Online/API/APIAccess.cs +++ b/osu.Game/Online/API/APIAccess.cs @@ -159,7 +159,7 @@ namespace osu.Game.Online.API private void onTokenChanged(ValueChangedEvent e) => config.SetValue(OsuSetting.Token, config.Get(OsuSetting.SavePassword) ? authentication.TokenString : string.Empty); - internal new void Schedule(Action action) => base.Schedule(action); + void IAPIProvider.Schedule(Action action) => base.Schedule(action); public string AccessToken => authentication.RequestAccessToken(); diff --git a/osu.Game/Online/API/APIRequest.cs b/osu.Game/Online/API/APIRequest.cs index d062b8f3de..37ad5fff0e 100644 --- a/osu.Game/Online/API/APIRequest.cs +++ b/osu.Game/Online/API/APIRequest.cs @@ -74,7 +74,7 @@ namespace osu.Game.Online.API protected virtual string Uri => $@"{API.APIEndpointUrl}/api/v2/{Target}"; - protected APIAccess API; + protected IAPIProvider API; protected WebRequest WebRequest; @@ -109,7 +109,7 @@ namespace osu.Game.Online.API /// /// This allows scheduling of operations back to the correct thread (which may be required before is called). /// - public void AttachAPI(APIAccess apiAccess) + public void AttachAPI(IAPIProvider apiAccess) { if (API != null && API != apiAccess) throw new InvalidOperationException("Attached API cannot be changed after initial set."); diff --git a/osu.Game/Online/API/DummyAPIAccess.cs b/osu.Game/Online/API/DummyAPIAccess.cs index 0af76537cd..7ac5c45fad 100644 --- a/osu.Game/Online/API/DummyAPIAccess.cs +++ b/osu.Game/Online/API/DummyAPIAccess.cs @@ -82,6 +82,8 @@ namespace osu.Game.Online.API public virtual void Queue(APIRequest request) { + request.AttachAPI(this); + Schedule(() => { if (HandleRequest?.Invoke(request) != true) @@ -98,10 +100,17 @@ namespace osu.Game.Online.API }); } - public void Perform(APIRequest request) => HandleRequest?.Invoke(request); + void IAPIProvider.Schedule(Action action) => base.Schedule(action); + + public void Perform(APIRequest request) + { + request.AttachAPI(this); + HandleRequest?.Invoke(request); + } public Task PerformAsync(APIRequest request) { + request.AttachAPI(this); HandleRequest?.Invoke(request); return Task.CompletedTask; } @@ -155,6 +164,8 @@ namespace osu.Game.Online.API state.Value = APIState.Connecting; LastLoginError = null; + request.AttachAPI(this); + // if no handler installed / handler can't handle verification, just assume that the server would verify for simplicity. if (HandleRequest?.Invoke(request) != true) onSuccessfulLogin(); diff --git a/osu.Game/Online/API/IAPIProvider.cs b/osu.Game/Online/API/IAPIProvider.cs index d8194dc32b..eccfb36546 100644 --- a/osu.Game/Online/API/IAPIProvider.cs +++ b/osu.Game/Online/API/IAPIProvider.cs @@ -134,6 +134,11 @@ namespace osu.Game.Online.API /// void UpdateStatistics(UserStatistics newStatistics); + /// + /// Schedule a callback to run on the update thread. + /// + internal void Schedule(Action action); + /// /// Constructs a new . May be null if not supported. /// From dd7133657dbe57c3aa99ef8266b52ca6bebf62a9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 30 Aug 2024 18:14:10 +0900 Subject: [PATCH 2133/2493] Fix weird test critical failure if exception happens too early in execution Noticed in passing. ``` Exit code is 134 (Unhandled exception. System.NullReferenceException: Object reference not set to an instance of an object. at osu.Game.OsuGameBase.onExceptionThrown(Exception ex) in /Users/dean/Projects/osu/osu.Game/OsuGameBase.cs:line 695 at osu.Framework.Platform.GameHost.abortExecutionFromException(Object sender, Exception exception, Boolean isTerminating) at osu.Framework.Platform.GameHost.unobservedExceptionHandler(Object sender, UnobservedTaskExceptionEventArgs args) at System.Threading.Tasks.TaskExceptionHolder.Finalize()) ``` --- 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 1988a06503..ce0c288934 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -692,7 +692,7 @@ namespace osu.Game if (Interlocked.Decrement(ref allowableExceptions) < 0) { Logger.Log("Too many unhandled exceptions, crashing out."); - RulesetStore.TryDisableCustomRulesetsCausing(ex); + RulesetStore?.TryDisableCustomRulesetsCausing(ex); return false; } From 2d745fb67e9210ae4eb7d0b5a702729ca4ac8ce3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 30 Aug 2024 18:21:30 +0900 Subject: [PATCH 2134/2493] Apply NRT to `APIRequest` --- osu.Game/Online/API/APIRequest.cs | 26 ++++++++----------- .../Online/API/Requests/JoinChannelRequest.cs | 2 +- .../API/Requests/LeaveChannelRequest.cs | 2 +- osu.Game/Online/Chat/WebSocketChatClient.cs | 2 +- osu.Game/Online/Rooms/JoinRoomRequest.cs | 2 +- osu.Game/Online/Rooms/PartRoomRequest.cs | 2 +- osu.Game/Tests/PollingChatClient.cs | 2 +- 7 files changed, 17 insertions(+), 21 deletions(-) diff --git a/osu.Game/Online/API/APIRequest.cs b/osu.Game/Online/API/APIRequest.cs index 37ad5fff0e..45ebbcd76d 100644 --- a/osu.Game/Online/API/APIRequest.cs +++ b/osu.Game/Online/API/APIRequest.cs @@ -1,12 +1,9 @@ // 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.Diagnostics; using System.Globalization; -using JetBrains.Annotations; using Newtonsoft.Json; using osu.Framework.Extensions.TypeExtensions; using osu.Framework.IO.Network; @@ -27,18 +24,17 @@ namespace osu.Game.Online.API /// /// The deserialised response object. May be null if the request or deserialisation failed. /// - [CanBeNull] - public T Response { get; private set; } + public T? Response { get; private set; } /// /// Invoked on successful completion of an API request. /// This will be scheduled to the API's internal scheduler (run on update thread automatically). /// - public new event APISuccessHandler Success; + public new event APISuccessHandler? Success; protected APIRequest() { - base.Success += () => Success?.Invoke(Response); + base.Success += () => Success?.Invoke(Response!); } protected override void PostProcess() @@ -72,28 +68,28 @@ namespace osu.Game.Online.API protected virtual WebRequest CreateWebRequest() => new OsuWebRequest(Uri); - protected virtual string Uri => $@"{API.APIEndpointUrl}/api/v2/{Target}"; + protected virtual string Uri => $@"{API!.APIEndpointUrl}/api/v2/{Target}"; - protected IAPIProvider API; + protected IAPIProvider? API; - protected WebRequest WebRequest; + protected WebRequest? WebRequest; /// /// The currently logged in user. Note that this will only be populated during . /// - protected APIUser User { get; private set; } + protected APIUser? User { get; private set; } /// /// Invoked on successful completion of an API request. /// This will be scheduled to the API's internal scheduler (run on update thread automatically). /// - public event APISuccessHandler Success; + public event APISuccessHandler? Success; /// /// Invoked on failure to complete an API request. /// This will be scheduled to the API's internal scheduler (run on update thread automatically). /// - public event APIFailureHandler Failure; + public event APIFailureHandler? Failure; private readonly object completionStateLock = new object(); @@ -210,7 +206,7 @@ namespace osu.Game.Online.API // in the case of a cancellation we don't care about whether there's an error in the response. if (!(e is OperationCanceledException)) { - string responseString = WebRequest?.GetResponseString(); + string? responseString = WebRequest?.GetResponseString(); // naive check whether there's an error in the response to avoid unnecessary JSON deserialisation. if (!string.IsNullOrEmpty(responseString) && responseString.Contains(@"""error""")) @@ -248,7 +244,7 @@ namespace osu.Game.Online.API private class DisplayableError { [JsonProperty("error")] - public string ErrorMessage { get; set; } + public string ErrorMessage { get; set; } = string.Empty; } } diff --git a/osu.Game/Online/API/Requests/JoinChannelRequest.cs b/osu.Game/Online/API/Requests/JoinChannelRequest.cs index 33eab7e355..0109e653d9 100644 --- a/osu.Game/Online/API/Requests/JoinChannelRequest.cs +++ b/osu.Game/Online/API/Requests/JoinChannelRequest.cs @@ -23,6 +23,6 @@ namespace osu.Game.Online.API.Requests return req; } - protected override string Target => $@"chat/channels/{channel.Id}/users/{User.Id}"; + protected override string Target => $@"chat/channels/{channel.Id}/users/{User!.Id}"; } } diff --git a/osu.Game/Online/API/Requests/LeaveChannelRequest.cs b/osu.Game/Online/API/Requests/LeaveChannelRequest.cs index 7dfc9a0aed..36cfd79c60 100644 --- a/osu.Game/Online/API/Requests/LeaveChannelRequest.cs +++ b/osu.Game/Online/API/Requests/LeaveChannelRequest.cs @@ -23,6 +23,6 @@ namespace osu.Game.Online.API.Requests return req; } - protected override string Target => $@"chat/channels/{channel.Id}/users/{User.Id}"; + protected override string Target => $@"chat/channels/{channel.Id}/users/{User!.Id}"; } } diff --git a/osu.Game/Online/Chat/WebSocketChatClient.cs b/osu.Game/Online/Chat/WebSocketChatClient.cs index 37774a1f5d..a74f0222f2 100644 --- a/osu.Game/Online/Chat/WebSocketChatClient.cs +++ b/osu.Game/Online/Chat/WebSocketChatClient.cs @@ -80,7 +80,7 @@ namespace osu.Game.Online.Chat fetchReq.Success += updates => { - if (updates?.Presence != null) + if (updates.Presence != null) { foreach (var channel in updates.Presence) joinChannel(channel); diff --git a/osu.Game/Online/Rooms/JoinRoomRequest.cs b/osu.Game/Online/Rooms/JoinRoomRequest.cs index 8645f2a2c0..9a73104b60 100644 --- a/osu.Game/Online/Rooms/JoinRoomRequest.cs +++ b/osu.Game/Online/Rooms/JoinRoomRequest.cs @@ -27,6 +27,6 @@ namespace osu.Game.Online.Rooms return req; } - protected override string Target => $@"rooms/{Room.RoomID.Value}/users/{User.Id}"; + protected override string Target => $@"rooms/{Room.RoomID.Value}/users/{User!.Id}"; } } diff --git a/osu.Game/Online/Rooms/PartRoomRequest.cs b/osu.Game/Online/Rooms/PartRoomRequest.cs index 09ba6f65c3..2416833a1e 100644 --- a/osu.Game/Online/Rooms/PartRoomRequest.cs +++ b/osu.Game/Online/Rooms/PartRoomRequest.cs @@ -23,6 +23,6 @@ namespace osu.Game.Online.Rooms return req; } - protected override string Target => $"rooms/{room.RoomID.Value}/users/{User.Id}"; + protected override string Target => $"rooms/{room.RoomID.Value}/users/{User!.Id}"; } } diff --git a/osu.Game/Tests/PollingChatClient.cs b/osu.Game/Tests/PollingChatClient.cs index eb29b35c1d..75975c716b 100644 --- a/osu.Game/Tests/PollingChatClient.cs +++ b/osu.Game/Tests/PollingChatClient.cs @@ -48,7 +48,7 @@ namespace osu.Game.Tests fetchReq.Success += updates => { - if (updates?.Presence != null) + if (updates.Presence != null) { foreach (var channel in updates.Presence) handleChannelJoined(channel); From 291dd5b1016081e534b78e0d894a688bd9dec74a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 30 Aug 2024 18:37:27 +0900 Subject: [PATCH 2135/2493] Remove TODO --- osu.Game/Screens/Select/BeatmapCarousel.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 7f6921d768..32f85824fa 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -320,7 +320,6 @@ namespace osu.Game.Screens.Select { try { - // TODO: chekc whether we still need beatmap sets by ID foreach (var set in setsRequiringRemoval) removeBeatmapSet(set.ID); foreach (var set in setsRequiringUpdate) updateBeatmapSet(set); From 1b9942cb3092b7f5a3f256e611d1e33c4455a76d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 30 Aug 2024 18:44:04 +0900 Subject: [PATCH 2136/2493] Mark `BeatmapSets` as `internal` --- osu.Game/Screens/Select/BeatmapCarousel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 32f85824fa..ed3fbc4054 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -122,7 +122,7 @@ namespace osu.Game.Screens.Select private IEnumerable beatmapSets => root.Items.OfType(); - public IEnumerable BeatmapSets + internal IEnumerable BeatmapSets { get => beatmapSets.Select(g => g.BeatmapSet); set From 2033a5e1579ff8cd3264b9f65c148ea700005929 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 30 Aug 2024 18:44:51 +0900 Subject: [PATCH 2137/2493] Add disposal of `ManualResetEventSlim` --- osu.Game/Database/DetachedBeatmapStore.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Database/DetachedBeatmapStore.cs b/osu.Game/Database/DetachedBeatmapStore.cs index 17d2dd15b6..7920f24a0b 100644 --- a/osu.Game/Database/DetachedBeatmapStore.cs +++ b/osu.Game/Database/DetachedBeatmapStore.cs @@ -133,7 +133,9 @@ namespace osu.Game.Database protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); + loaded.Set(); + loaded.Dispose(); realmSubscription?.Dispose(); } From de208fd5c385fe128968eeab4f182dfaf4c3abd3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 30 Aug 2024 18:45:45 +0900 Subject: [PATCH 2138/2493] Add very basic error handling for failed beatmap detach --- osu.Game/Database/DetachedBeatmapStore.cs | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/osu.Game/Database/DetachedBeatmapStore.cs b/osu.Game/Database/DetachedBeatmapStore.cs index 7920f24a0b..64aeeccd9a 100644 --- a/osu.Game/Database/DetachedBeatmapStore.cs +++ b/osu.Game/Database/DetachedBeatmapStore.cs @@ -10,6 +10,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Beatmaps; +using osu.Game.Online.Multiplayer; using Realms; namespace osu.Game.Database @@ -59,15 +60,21 @@ namespace osu.Game.Database Task.Factory.StartNew(() => { - realm.Run(_ => + try { - var detached = frozenSets.Detach(); + realm.Run(_ => + { + var detached = frozenSets.Detach(); - detachedBeatmapSets.Clear(); - detachedBeatmapSets.AddRange(detached); + detachedBeatmapSets.Clear(); + detachedBeatmapSets.AddRange(detached); + }); + } + finally + { loaded.Set(); - }); - }, TaskCreationOptions.LongRunning); + } + }, TaskCreationOptions.LongRunning).FireAndForget(); return; } From 7b6e62283ffa0a56a99581863b74735957ebacca Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 30 Aug 2024 18:50:00 +0900 Subject: [PATCH 2139/2493] Fix beatmap not being detached on hide/unhide The explicit detach call was removed from `updateBeatmapSet`, causing this to occur. We could optionally add it back (it will be a noop in all cases though). --- osu.Game/Screens/Select/BeatmapCarousel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index ed3fbc4054..87cea45e87 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -391,7 +391,7 @@ namespace osu.Game.Screens.Select if (root.BeatmapSetsByID.TryGetValue(beatmapSet.ID, out var existingSets) && existingSets.SelectMany(s => s.Beatmaps).All(b => b.BeatmapInfo.ID != beatmapInfo.ID)) { - updateBeatmapSet(beatmapSet); + updateBeatmapSet(beatmapSet.Detach()); changed = true; } } From 8ffd4aa82c5e1afd4b4fe56773d6043248b54a12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 30 Aug 2024 13:41:34 +0200 Subject: [PATCH 2140/2493] Fix NRT inspections --- .../TestSceneOnlinePlayBeatmapAvailabilityTracker.cs | 2 +- osu.Game/Online/API/APIDownloadRequest.cs | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs b/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs index 585fd516bd..ae3451c3e0 100644 --- a/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs +++ b/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs @@ -257,7 +257,7 @@ namespace osu.Game.Tests.Online { } - protected override string Target => null; + protected override string Target => string.Empty; } } } diff --git a/osu.Game/Online/API/APIDownloadRequest.cs b/osu.Game/Online/API/APIDownloadRequest.cs index c48372278a..f8db52139d 100644 --- a/osu.Game/Online/API/APIDownloadRequest.cs +++ b/osu.Game/Online/API/APIDownloadRequest.cs @@ -4,6 +4,7 @@ #nullable disable using System; +using System.Diagnostics; using System.IO; using osu.Framework.IO.Network; @@ -34,7 +35,11 @@ namespace osu.Game.Online.API return request; } - private void request_Progress(long current, long total) => API.Schedule(() => Progressed?.Invoke(current, total)); + private void request_Progress(long current, long total) + { + Debug.Assert(API != null); + API.Schedule(() => Progressed?.Invoke(current, total)); + } protected void TriggerSuccess(string filename) { From 8b04455c29fd41dfab49974f8883acb7ef60d8fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 30 Aug 2024 14:57:15 +0200 Subject: [PATCH 2141/2493] Fix chat overlay tests Not entirely sure why they were failing previously, but the most likely explanation is that by freak accident some mock requests would previously execute immediately rather than be scheduled on the API thread, which would change execution ordering and ensure that `ChannelManager.CurrentChannel` would become the joined channel, rather than remaining at the channel listing. --- osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs index a47205094e..b6445dec6b 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs @@ -446,7 +446,7 @@ namespace osu.Game.Tests.Visual.Online { AddStep("Show overlay with channel 1", () => { - channelManager.JoinChannel(testChannel1); + channelManager.CurrentChannel.Value = channelManager.JoinChannel(testChannel1); chatOverlay.Show(); }); waitForChannel1Visible(); @@ -462,7 +462,7 @@ namespace osu.Game.Tests.Visual.Online { AddStep("Show overlay with channel 1", () => { - channelManager.JoinChannel(testChannel1); + channelManager.CurrentChannel.Value = channelManager.JoinChannel(testChannel1); chatOverlay.Show(); }); waitForChannel1Visible(); From f5a2b5ea03caa71e4122926ccea6d0b53e1b781f Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Wed, 28 Aug 2024 02:20:11 +0300 Subject: [PATCH 2142/2493] Use FastCircle in demanding places in the editor --- .../Blueprints/Sliders/Components/PathControlPointPiece.cs | 4 ++-- .../Timelines/Summary/Parts/EffectPointVisualisation.cs | 2 +- .../Timelines/Summary/Visualisations/PointVisualisation.cs | 2 +- osu.Game/Screens/Loader.cs | 1 + 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs index 9d819f6cc0..3337e99215 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs @@ -40,7 +40,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components public readonly PathControlPoint ControlPoint; private readonly T hitObject; - private readonly Circle circle; + private readonly FastCircle circle; private readonly Drawable markerRing; [Resolved] @@ -62,7 +62,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components InternalChildren = new[] { - circle = new Circle + circle = new FastCircle { Anchor = Anchor.Centre, Origin = Anchor.Centre, diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/EffectPointVisualisation.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/EffectPointVisualisation.cs index 17fedb933a..1d71bc100c 100644 --- a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/EffectPointVisualisation.cs +++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/EffectPointVisualisation.cs @@ -105,7 +105,7 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts } } - private partial class KiaiVisualisation : Circle, IHasTooltip + private partial class KiaiVisualisation : FastCircle, IHasTooltip { private readonly double startTime; private readonly double endTime; diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/Visualisations/PointVisualisation.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/Visualisations/PointVisualisation.cs index 9c16f457f7..6c9af53964 100644 --- a/osu.Game/Screens/Edit/Components/Timelines/Summary/Visualisations/PointVisualisation.cs +++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/Visualisations/PointVisualisation.cs @@ -9,7 +9,7 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Visualisations /// /// Represents a singular point on a timeline part. /// - public partial class PointVisualisation : Circle + public partial class PointVisualisation : FastCircle { public readonly double StartTime; diff --git a/osu.Game/Screens/Loader.cs b/osu.Game/Screens/Loader.cs index 4dba512cbd..57e3998646 100644 --- a/osu.Game/Screens/Loader.cs +++ b/osu.Game/Screens/Loader.cs @@ -122,6 +122,7 @@ namespace osu.Game.Screens loadTargets.Add(manager.Load(@"CursorTrail", FragmentShaderDescriptor.TEXTURE)); loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_2, "TriangleBorder")); + loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_2, "FastCircle")); loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_3, FragmentShaderDescriptor.TEXTURE)); } From 225418dbb36db38ac9d97c7b7bd960380018be4f Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Sat, 31 Aug 2024 01:59:40 +0300 Subject: [PATCH 2143/2493] Rework kiai handling in summary timeline --- .../Summary/Parts/EffectPointVisualisation.cs | 93 +------------ .../Timelines/Summary/Parts/KiaiPart.cs | 123 ++++++++++++++++++ .../Timelines/Summary/SummaryTimeline.cs | 6 + 3 files changed, 130 insertions(+), 92 deletions(-) create mode 100644 osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/KiaiPart.cs diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/EffectPointVisualisation.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/EffectPointVisualisation.cs index b4e6d1ece2..25d50a97be 100644 --- a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/EffectPointVisualisation.cs +++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/EffectPointVisualisation.cs @@ -2,29 +2,19 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; -using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Cursor; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Localisation; using osu.Game.Beatmaps.ControlPoints; -using osu.Game.Extensions; -using osu.Game.Graphics; namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts { public partial class EffectPointVisualisation : CompositeDrawable, IControlPointVisualisation { private readonly EffectControlPoint effect; - private Bindable kiai = null!; [Resolved] private EditorBeatmap beatmap { get; set; } = null!; - [Resolved] - private OsuColour colours { get; set; } = null!; - public EffectPointVisualisation(EffectControlPoint point) { RelativePositionAxes = Axes.Both; @@ -36,49 +26,6 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts [BackgroundDependencyLoader] private void load() { - kiai = effect.KiaiModeBindable.GetBoundCopy(); - kiai.BindValueChanged(_ => refreshDisplay(), true); - } - - private EffectControlPoint? nextControlPoint; - - protected override void LoadComplete() - { - base.LoadComplete(); - - // Due to the limitations of ControlPointInfo, it's impossible to know via event flow when the next kiai point has changed. - // This is due to the fact that an EffectPoint can be added to an existing group. We would need to bind to ItemAdded on *every* - // future group to track this. - // - // I foresee this being a potential performance issue on beatmaps with many control points, so let's limit how often we check - // for changes. ControlPointInfo needs a refactor to make this flow better, but it should do for now. - Scheduler.AddDelayed(() => - { - EffectControlPoint? next = null; - - for (int i = 0; i < beatmap.ControlPointInfo.EffectPoints.Count; i++) - { - var point = beatmap.ControlPointInfo.EffectPoints[i]; - - if (point.Time > effect.Time) - { - next = point; - break; - } - } - - if (!ReferenceEquals(nextControlPoint, next)) - { - nextControlPoint = next; - refreshDisplay(); - } - }, 100, true); - } - - private void refreshDisplay() - { - ClearInternal(); - if (beatmap.BeatmapInfo.Ruleset.CreateInstance().EditorShowScrollSpeed) { AddInternal(new ControlPointVisualisation(effect) @@ -87,46 +34,8 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts X = 0, }); } - - if (!kiai.Value) - return; - - // handle kiai duration - // eventually this will be simpler when we have control points with durations. - if (nextControlPoint != null) - { - RelativeSizeAxes = Axes.Both; - Origin = Anchor.TopLeft; - - Width = (float)(nextControlPoint.Time - effect.Time); - - AddInternal(new KiaiVisualisation(effect.Time, nextControlPoint.Time) - { - RelativeSizeAxes = Axes.Both, - Anchor = Anchor.BottomLeft, - Origin = Anchor.CentreLeft, - Height = 0.4f, - Depth = float.MaxValue, - Colour = colours.Purple1, - }); - } } - private partial class KiaiVisualisation : Circle, IHasTooltip - { - private readonly double startTime; - private readonly double endTime; - - public KiaiVisualisation(double startTime, double endTime) - { - this.startTime = startTime; - this.endTime = endTime; - } - - public LocalisableString TooltipText => $"{startTime.ToEditorFormattedString()} - {endTime.ToEditorFormattedString()} kiai time"; - } - - // kiai sections display duration, so are required to be visualised. - public bool IsVisuallyRedundant(ControlPoint other) => other is EffectControlPoint otherEffect && effect.KiaiMode == otherEffect.KiaiMode; + public bool IsVisuallyRedundant(ControlPoint other) => other is EffectControlPoint; } } diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/KiaiPart.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/KiaiPart.cs new file mode 100644 index 0000000000..d61d4580fe --- /dev/null +++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/KiaiPart.cs @@ -0,0 +1,123 @@ +// 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.Cursor; +using osu.Framework.Graphics.Pooling; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Localisation; +using osu.Game.Extensions; +using osu.Game.Graphics; + +namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts +{ + /// + /// The part of the timeline that displays kiai sections in the song. + /// + public partial class KiaiPart : TimelinePart + { + private DrawablePool pool = null!; + + [BackgroundDependencyLoader] + private void load() + { + AddInternal(pool = new DrawablePool(10)); + } + + protected override void LoadBeatmap(EditorBeatmap beatmap) + { + base.LoadBeatmap(beatmap); + EditorBeatmap.ControlPointInfo.ControlPointsChanged += updateParts; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + updateParts(); + } + + private void updateParts() => Scheduler.AddOnce(() => + { + Clear(disposeChildren: false); + + double? startTime = null; + + foreach (var effectPoint in EditorBeatmap.ControlPointInfo.EffectPoints) + { + if (startTime.HasValue) + { + if (effectPoint.KiaiMode) + continue; + + var section = new KiaiSection + { + StartTime = startTime.Value, + EndTime = effectPoint.Time + }; + + Add(pool.Get(v => v.Section = section)); + + startTime = null; + } + else + { + if (!effectPoint.KiaiMode) + continue; + + startTime = effectPoint.Time; + } + } + + // last effect point has kiai enabled, kiai should last until the end of the map + if (startTime.HasValue) + { + Add(pool.Get(v => v.Section = new KiaiSection + { + StartTime = startTime.Value, + EndTime = Content.RelativeChildSize.X + })); + } + }); + + private partial class KiaiVisualisation : PoolableDrawable, IHasTooltip + { + private KiaiSection section; + + public KiaiSection Section + { + set + { + section = value; + + X = (float)value.StartTime; + Width = (float)value.Duration; + } + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + RelativePositionAxes = Axes.X; + RelativeSizeAxes = Axes.Both; + Anchor = Anchor.CentreLeft; + Origin = Anchor.CentreLeft; + Height = 0.2f; + AddInternal(new Circle + { + RelativeSizeAxes = Axes.Both, + Colour = colours.Purple1 + }); + } + + public LocalisableString TooltipText => $"{section.StartTime.ToEditorFormattedString()} - {section.EndTime.ToEditorFormattedString()} kiai time"; + } + + private readonly struct KiaiSection + { + public double StartTime { get; init; } + public double EndTime { get; init; } + public double Duration => EndTime - StartTime; + } + } +} diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/SummaryTimeline.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/SummaryTimeline.cs index 4ab7c88178..c01481e840 100644 --- a/osu.Game/Screens/Edit/Components/Timelines/Summary/SummaryTimeline.cs +++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/SummaryTimeline.cs @@ -65,6 +65,12 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary Origin = Anchor.Centre, RelativeSizeAxes = Axes.Both, }, + new KiaiPart + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + }, new ControlPointPart { Anchor = Anchor.Centre, From 6b8b49e4f181cdf0feed5952d5a8f50f583ebd71 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 31 Aug 2024 13:14:56 +0900 Subject: [PATCH 2144/2493] Simplify scroll speed point display code now that it only serves one purpose --- .../Summary/Parts/EffectPointVisualisation.cs | 41 ------------------- .../Summary/Parts/GroupVisualisation.cs | 21 ++++++++-- 2 files changed, 18 insertions(+), 44 deletions(-) delete mode 100644 osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/EffectPointVisualisation.cs diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/EffectPointVisualisation.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/EffectPointVisualisation.cs deleted file mode 100644 index 25d50a97be..0000000000 --- a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/EffectPointVisualisation.cs +++ /dev/null @@ -1,41 +0,0 @@ -// 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.Game.Beatmaps.ControlPoints; - -namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts -{ - public partial class EffectPointVisualisation : CompositeDrawable, IControlPointVisualisation - { - private readonly EffectControlPoint effect; - - [Resolved] - private EditorBeatmap beatmap { get; set; } = null!; - - public EffectPointVisualisation(EffectControlPoint point) - { - RelativePositionAxes = Axes.Both; - RelativeSizeAxes = Axes.Y; - - effect = point; - } - - [BackgroundDependencyLoader] - private void load() - { - if (beatmap.BeatmapInfo.Ruleset.CreateInstance().EditorShowScrollSpeed) - { - AddInternal(new ControlPointVisualisation(effect) - { - // importantly, override the x position being set since we do that in the GroupVisualisation parent drawable. - X = 0, - }); - } - } - - public bool IsVisuallyRedundant(ControlPoint other) => other is EffectControlPoint; - } -} diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/GroupVisualisation.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/GroupVisualisation.cs index b872c3725c..0dd945805b 100644 --- a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/GroupVisualisation.cs +++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/GroupVisualisation.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System.Linq; +using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -15,6 +16,8 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts private readonly IBindableList controlPoints = new BindableList(); + private bool showScrollSpeed; + public GroupVisualisation(ControlPointGroup group) { RelativePositionAxes = Axes.X; @@ -24,8 +27,13 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts Group = group; X = (float)group.Time; + } + + [BackgroundDependencyLoader] + private void load(EditorBeatmap beatmap) + { + showScrollSpeed = beatmap.BeatmapInfo.Ruleset.CreateInstance().EditorShowScrollSpeed; - // Run in constructor so IsRedundant calls can work correctly. controlPoints.BindTo(Group.ControlPoints); controlPoints.BindCollectionChanged((_, _) => { @@ -47,8 +55,15 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts }); break; - case EffectControlPoint effect: - AddInternal(new EffectPointVisualisation(effect)); + case EffectControlPoint: + if (!showScrollSpeed) + return; + + AddInternal(new ControlPointVisualisation(point) + { + // importantly, override the x position being set since we do that above. + X = 0, + }); break; } } From 636ee50eb9cbdc2d5d75de884b4be79ddd914e45 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Sat, 31 Aug 2024 23:03:10 +0900 Subject: [PATCH 2145/2493] Rename to VelopackUpdateManager --- osu.Desktop/OsuGameDesktop.cs | 2 +- .../{VeloUpdateManager.cs => VelopackUpdateManager.cs} | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) rename osu.Desktop/Updater/{VeloUpdateManager.cs => VelopackUpdateManager.cs} (96%) diff --git a/osu.Desktop/OsuGameDesktop.cs b/osu.Desktop/OsuGameDesktop.cs index ee73c84ba3..94f6ef0fc3 100644 --- a/osu.Desktop/OsuGameDesktop.cs +++ b/osu.Desktop/OsuGameDesktop.cs @@ -100,7 +100,7 @@ namespace osu.Desktop if (!string.IsNullOrEmpty(packageManaged)) return new NoActionUpdateManager(); - return new VeloUpdateManager(); + return new VelopackUpdateManager(); } public override bool RestartAppWhenExited() diff --git a/osu.Desktop/Updater/VeloUpdateManager.cs b/osu.Desktop/Updater/VelopackUpdateManager.cs similarity index 96% rename from osu.Desktop/Updater/VeloUpdateManager.cs rename to osu.Desktop/Updater/VelopackUpdateManager.cs index 6d3eb3f3f0..5cdc87e539 100644 --- a/osu.Desktop/Updater/VeloUpdateManager.cs +++ b/osu.Desktop/Updater/VelopackUpdateManager.cs @@ -11,11 +11,10 @@ using osu.Game.Overlays.Notifications; using osu.Game.Screens.Play; using Velopack; using Velopack.Sources; -using UpdateManager = Velopack.UpdateManager; namespace osu.Desktop.Updater { - public partial class VeloUpdateManager : Game.Updater.UpdateManager + public partial class VelopackUpdateManager : Game.Updater.UpdateManager { private readonly UpdateManager updateManager; private INotificationOverlay notificationOverlay = null!; @@ -26,7 +25,7 @@ namespace osu.Desktop.Updater [Resolved] private ILocalUserPlayInfo? localUserInfo { get; set; } - public VeloUpdateManager() + public VelopackUpdateManager() { const string? github_token = null; // TODO: populate. updateManager = new UpdateManager(new GithubSource(@"https://github.com/ppy/osu", github_token, false), new UpdateOptions From 837fa1b8dc397c370bd43bc640610405f6fcffc9 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Sat, 31 Aug 2024 17:32:24 +0300 Subject: [PATCH 2146/2493] Use FastCircle for kiai visualisation --- .../Screens/Edit/Components/Timelines/Summary/Parts/KiaiPart.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/KiaiPart.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/KiaiPart.cs index d61d4580fe..ee44df8598 100644 --- a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/KiaiPart.cs +++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/KiaiPart.cs @@ -103,7 +103,7 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts Anchor = Anchor.CentreLeft; Origin = Anchor.CentreLeft; Height = 0.2f; - AddInternal(new Circle + AddInternal(new FastCircle { RelativeSizeAxes = Axes.Both, Colour = colours.Purple1 From a038799c4745bf8f47189d13beec664f708785ac Mon Sep 17 00:00:00 2001 From: smallketchup82 Date: Sat, 31 Aug 2024 17:14:53 -0400 Subject: [PATCH 2147/2493] Update osu.Desktop.csproj bump version --- 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 ffa0c30b0c..3588317b8a 100644 --- a/osu.Desktop/osu.Desktop.csproj +++ b/osu.Desktop/osu.Desktop.csproj @@ -25,7 +25,7 @@ - + From f7da7193ff683c5fb7b9ced964fff3090e2b00af Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 1 Sep 2024 19:10:08 +0900 Subject: [PATCH 2148/2493] 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 b5a355a77f..2609fd42c3 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -10,7 +10,7 @@ true - + diff --git a/osu.iOS.props b/osu.iOS.props index a94b9375c9..1056f4b441 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -17,6 +17,6 @@ -all - + From b990af6adad67f239c31f3cc28ccfe657c9428d6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 2 Sep 2024 13:08:14 +0900 Subject: [PATCH 2149/2493] Use full name --- osu.Desktop/Program.cs | 4 ++-- osu.sln.DotSettings | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Desktop/Program.cs b/osu.Desktop/Program.cs index 92c8f2104c..609af2a8a7 100644 --- a/osu.Desktop/Program.cs +++ b/osu.Desktop/Program.cs @@ -31,7 +31,7 @@ namespace osu.Desktop public static void Main(string[] args) { // Velopack needs to run before anything else - setupVelo(); + setupVelopack(); if (OperatingSystem.IsWindows()) { @@ -164,7 +164,7 @@ namespace osu.Desktop return false; } - private static void setupVelo() + private static void setupVelopack() { VelopackApp .Build() diff --git a/osu.sln.DotSettings b/osu.sln.DotSettings index a792b956dd..38686d8508 100644 --- a/osu.sln.DotSettings +++ b/osu.sln.DotSettings @@ -1060,5 +1060,6 @@ private void load() True True True + True True True From 42e1168b35072115839e266ada4baebb9cfb6915 Mon Sep 17 00:00:00 2001 From: smallketchup82 Date: Mon, 2 Sep 2024 01:23:05 -0400 Subject: [PATCH 2150/2493] Remove github token variable & pass null for the github token --- osu.Desktop/Updater/VelopackUpdateManager.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Desktop/Updater/VelopackUpdateManager.cs b/osu.Desktop/Updater/VelopackUpdateManager.cs index 5cdc87e539..eb1463483f 100644 --- a/osu.Desktop/Updater/VelopackUpdateManager.cs +++ b/osu.Desktop/Updater/VelopackUpdateManager.cs @@ -27,8 +27,7 @@ namespace osu.Desktop.Updater public VelopackUpdateManager() { - const string? github_token = null; // TODO: populate. - updateManager = new UpdateManager(new GithubSource(@"https://github.com/ppy/osu", github_token, false), new UpdateOptions + updateManager = new UpdateManager(new GithubSource(@"https://github.com/ppy/osu", null, false), new UpdateOptions { AllowVersionDowngrade = true }); From 30fb3c3999c9b5a230865377a8721bef6e54545b Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 2 Sep 2024 15:23:40 +0900 Subject: [PATCH 2151/2493] Fix osu!catch fruits not resizing on texture change --- .../Legacy/LegacyCatchHitObjectPiece.cs | 22 ++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyCatchHitObjectPiece.cs b/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyCatchHitObjectPiece.cs index 2184ecc363..15b168b8c2 100644 --- a/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyCatchHitObjectPiece.cs +++ b/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyCatchHitObjectPiece.cs @@ -85,9 +85,25 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy protected void SetTexture(Texture? texture, Texture? overlayTexture) { - colouredSprite.Texture = texture; - overlaySprite.Texture = overlayTexture; - hyperSprite.Texture = texture; + // Sizes are reset due to an arguable osu!framework bug where Sprite retains the size of the first set texture. + + if (colouredSprite.Texture != texture) + { + colouredSprite.Size = Vector2.Zero; + colouredSprite.Texture = texture; + } + + if (overlaySprite.Texture != overlayTexture) + { + overlaySprite.Size = Vector2.Zero; + overlaySprite.Texture = overlayTexture; + } + + if (hyperSprite.Texture != texture) + { + hyperSprite.Size = Vector2.Zero; + hyperSprite.Texture = texture; + } } } } From 38a62eed4458ea0897105537e5cfc28830798019 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 2 Sep 2024 16:29:41 +0900 Subject: [PATCH 2152/2493] Add automatic downloading support when spectating a multiplayer room --- .../Match/MultiplayerSpectateButton.cs | 74 ++++++++++++++++++- 1 file changed, 73 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerSpectateButton.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerSpectateButton.cs index 1d308ed39c..8bc3704261 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerSpectateButton.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerSpectateButton.cs @@ -3,12 +3,19 @@ #nullable disable +using System.Linq; +using System.Threading; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Extensions; using osu.Framework.Graphics; +using osu.Game.Beatmaps; +using osu.Game.Configuration; +using osu.Game.Database; using osu.Game.Graphics; using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Online.Multiplayer; +using osu.Game.Online.Rooms; using osuTK; namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match @@ -46,10 +53,11 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match } [BackgroundDependencyLoader] - private void load() + private void load(OsuConfigManager config) { operationInProgress = ongoingOperationTracker.InProgress.GetBoundCopy(); operationInProgress.BindValueChanged(_ => updateState()); + automaticallyDownload = config.GetBindable(OsuSetting.AutomaticallyDownloadMissingBeatmaps); } protected override void OnRoomUpdated() @@ -77,6 +85,70 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match button.Enabled.Value = Client.Room != null && Client.Room.State != MultiplayerRoomState.Closed && !operationInProgress.Value; + + Scheduler.AddOnce(checkForAutomaticDownload); } + + #region Automatic download handling + + [Resolved] + private BeatmapLookupCache beatmapLookupCache { get; set; } + + [Resolved] + private BeatmapModelDownloader beatmapDownloader { get; set; } = null!; + + [Resolved] + private BeatmapManager beatmaps { get; set; } + + private CancellationTokenSource downloadCheckCancellation; + + private Bindable automaticallyDownload; + + protected override void PlaylistItemChanged(MultiplayerPlaylistItem item) + { + base.PlaylistItemChanged(item); + Scheduler.AddOnce(checkForAutomaticDownload); + } + + private void checkForAutomaticDownload() + { + MultiplayerPlaylistItem item = Client.Room?.Playlist.FirstOrDefault(i => !i.Expired); + + downloadCheckCancellation?.Cancel(); + + if (item == null) + return; + + if (!automaticallyDownload.Value) + return; + + // While we can support automatic downloads when not spectating, there are some usability concerns. + // - In host rotate mode, this could potentially be unwanted by some users (even though they want automatic downloads everywhere else). + // - When first joining a room, the expectation should be that the user is checking out the room, and they may not immediately want to download the selected beatmap. + // + // Rather than over-complicating this flow, let's only auto-download when spectating for the time being. + // A potential path forward would be to have a local auto-download checkbox above the playlist item list area. + if (Client.LocalUser?.State != MultiplayerUserState.Spectating) + return; + + // In a perfect world we'd use BeatmapAvailability, but there's no event-driven flow for when a selection changes. + // ie. if selection changes from "not downloaded" to another "not downloaded" we wouldn't get a value changed raised. + beatmapLookupCache + .GetBeatmapAsync(item.BeatmapID, (downloadCheckCancellation = new CancellationTokenSource()).Token) + .ContinueWith(resolved => Schedule(() => + { + var beatmapSet = resolved.GetResultSafely()?.BeatmapSet; + + if (beatmapSet == null) + return; + + if (beatmaps.IsAvailableLocally(new BeatmapSetInfo { OnlineID = beatmapSet.OnlineID })) + return; + + beatmapDownloader.Download(beatmapSet); + })); + } + + #endregion } } From 6227e4f848ce0fc24c54cd81d47e9bb9268242d7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 2 Sep 2024 16:35:10 +0900 Subject: [PATCH 2153/2493] Apply NRT to `MultiplayerSpectateButton` --- .../Match/MultiplayerSpectateButton.cs | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerSpectateButton.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerSpectateButton.cs index 8bc3704261..bb6cd6cdaa 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerSpectateButton.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerSpectateButton.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.Linq; using System.Threading; using osu.Framework.Allocation; @@ -23,12 +21,12 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match public partial class MultiplayerSpectateButton : MultiplayerRoomComposite { [Resolved] - private OngoingOperationTracker ongoingOperationTracker { get; set; } + private OngoingOperationTracker ongoingOperationTracker { get; set; } = null!; [Resolved] - private OsuColour colours { get; set; } + private OsuColour colours { get; set; } = null!; - private IBindable operationInProgress; + private IBindable operationInProgress = null!; private readonly RoundedButton button; @@ -92,17 +90,17 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match #region Automatic download handling [Resolved] - private BeatmapLookupCache beatmapLookupCache { get; set; } + private BeatmapLookupCache beatmapLookupCache { get; set; } = null!; [Resolved] private BeatmapModelDownloader beatmapDownloader { get; set; } = null!; [Resolved] - private BeatmapManager beatmaps { get; set; } + private BeatmapManager beatmaps { get; set; } = null!; - private CancellationTokenSource downloadCheckCancellation; + private Bindable automaticallyDownload = null!; - private Bindable automaticallyDownload; + private CancellationTokenSource? downloadCheckCancellation; protected override void PlaylistItemChanged(MultiplayerPlaylistItem item) { @@ -112,7 +110,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match private void checkForAutomaticDownload() { - MultiplayerPlaylistItem item = Client.Room?.Playlist.FirstOrDefault(i => !i.Expired); + MultiplayerPlaylistItem? item = Client.Room?.Playlist.FirstOrDefault(i => !i.Expired); downloadCheckCancellation?.Cancel(); From f8a6a6a8aef5db35a3d5cdf7aca50247d4cfedd4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 2 Sep 2024 16:43:46 +0900 Subject: [PATCH 2154/2493] Request restart asynchronously to avoid blocking update thread --- osu.Desktop/OsuGameDesktop.cs | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/osu.Desktop/OsuGameDesktop.cs b/osu.Desktop/OsuGameDesktop.cs index 94f6ef0fc3..c75a3f0a1a 100644 --- a/osu.Desktop/OsuGameDesktop.cs +++ b/osu.Desktop/OsuGameDesktop.cs @@ -5,6 +5,7 @@ using System; using System.IO; using System.Reflection; using System.Runtime.Versioning; +using System.Threading.Tasks; using Microsoft.Win32; using osu.Desktop.Performance; using osu.Desktop.Security; @@ -18,6 +19,7 @@ using osu.Desktop.Windows; using osu.Framework.Allocation; using osu.Game.IO; using osu.Game.IPC; +using osu.Game.Online.Multiplayer; using osu.Game.Performance; using osu.Game.Utils; @@ -105,16 +107,8 @@ namespace osu.Desktop public override bool RestartAppWhenExited() { - try - { - Velopack.UpdateExe.Start(); - return true; - } - catch (Exception e) - { - Logger.Error(e, "Failed to restart application"); - return base.RestartAppWhenExited(); - } + Task.Run(() => Velopack.UpdateExe.Start()).FireAndForget(); + return true; } protected override void LoadComplete() From 68e6fa286e00c0fef777d5d2fcfa49f503a5bd45 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 2 Sep 2024 16:46:29 +0900 Subject: [PATCH 2155/2493] Make comment about velopack's init a bit more loud --- osu.Desktop/Program.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Desktop/Program.cs b/osu.Desktop/Program.cs index 609af2a8a7..5103663815 100644 --- a/osu.Desktop/Program.cs +++ b/osu.Desktop/Program.cs @@ -30,7 +30,9 @@ namespace osu.Desktop [STAThread] public static void Main(string[] args) { - // Velopack needs to run before anything else + // IMPORTANT DON'T IGNORE: For general sanity, velopack's setup needs to run before anything else. + // This has bitten us in the rear before (bricked updater), and although the underlying issue from + // last time has been fixed, let's not tempt fate. setupVelopack(); if (OperatingSystem.IsWindows()) From cd9b82253e4639d3a94dc049244c9ccbcc59ea47 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 2 Sep 2024 17:20:33 +0900 Subject: [PATCH 2156/2493] Pass through correct update to apply when calling `WaitExitThenApplyUpdates` --- osu.Desktop/Updater/VelopackUpdateManager.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Desktop/Updater/VelopackUpdateManager.cs b/osu.Desktop/Updater/VelopackUpdateManager.cs index eb1463483f..4d2535ed32 100644 --- a/osu.Desktop/Updater/VelopackUpdateManager.cs +++ b/osu.Desktop/Updater/VelopackUpdateManager.cs @@ -52,7 +52,7 @@ namespace osu.Desktop.Updater if (localUserInfo?.IsPlaying.Value == true) return false; - var info = await updateManager.CheckForUpdatesAsync().ConfigureAwait(false); + UpdateInfo? info = await updateManager.CheckForUpdatesAsync().ConfigureAwait(false); // Handle no updates available. if (info == null) @@ -65,7 +65,7 @@ namespace osu.Desktop.Updater { Activated = () => { - restartToApplyUpdate(); + restartToApplyUpdate(null); return true; } }); @@ -78,7 +78,7 @@ namespace osu.Desktop.Updater { notification = new UpdateProgressNotification { - CompletionClickAction = restartToApplyUpdate, + CompletionClickAction = () => restartToApplyUpdate(info), }; Schedule(() => notificationOverlay.Post(notification)); @@ -117,9 +117,9 @@ namespace osu.Desktop.Updater return true; } - private bool restartToApplyUpdate() + private bool restartToApplyUpdate(UpdateInfo? info) { - updateManager.WaitExitThenApplyUpdates(null); + updateManager.WaitExitThenApplyUpdates(info?.TargetFullRelease); Schedule(() => game.AttemptExit()); return true; } From 171ac0f5104efea07c262cffa1622d6d758ed289 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 2 Sep 2024 17:26:14 +0900 Subject: [PATCH 2157/2493] Fix incorrect osu!catch snap display when last object is a juice stream Addresses https://github.com/ppy/osu/discussions/29678. --- osu.Game.Rulesets.Catch/Edit/CatchDistanceSnapProvider.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Catch/Edit/CatchDistanceSnapProvider.cs b/osu.Game.Rulesets.Catch/Edit/CatchDistanceSnapProvider.cs index c3103bd204..6f5b32a41d 100644 --- a/osu.Game.Rulesets.Catch/Edit/CatchDistanceSnapProvider.cs +++ b/osu.Game.Rulesets.Catch/Edit/CatchDistanceSnapProvider.cs @@ -18,7 +18,9 @@ namespace osu.Game.Rulesets.Catch.Edit // The implementation below is probably correct but should be checked if/when exposed via controls. float expectedDistance = DurationToDistance(before, after.StartTime - before.GetEndTime()); - float actualDistance = Math.Abs(((CatchHitObject)before).EffectiveX - ((CatchHitObject)after).EffectiveX); + + float previousEndX = (before as JuiceStream)?.EndX ?? ((CatchHitObject)before).EffectiveX; + float actualDistance = Math.Abs(previousEndX - ((CatchHitObject)after).EffectiveX); return actualDistance / expectedDistance; } From 10901075bef07ba3abba1a22c0166a5bcd23c6de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 2 Sep 2024 11:27:44 +0200 Subject: [PATCH 2158/2493] Modify existing test coverage to demonstrate failure with touch --- .../TestSceneModCustomisationPanel.cs | 37 ++++++------------- 1 file changed, 12 insertions(+), 25 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModCustomisationPanel.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModCustomisationPanel.cs index d93f62935a..04cb129630 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModCustomisationPanel.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModCustomisationPanel.cs @@ -7,6 +7,7 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Input; using osu.Framework.Testing; using osu.Game.Graphics.UserInterface; using osu.Game.Overlays; @@ -120,7 +121,7 @@ namespace osu.Game.Tests.Visual.UserInterface } [Test] - public void TestExpandedStatePersistsWhenClicked() + public void TestHoverExpandsAndCollapsesWhenHeaderTouched() { AddStep("add customisable mod", () => { @@ -128,34 +129,20 @@ namespace osu.Game.Tests.Visual.UserInterface panel.Enabled.Value = true; }); - AddStep("hover header", () => InputManager.MoveMouseTo(header)); - checkExpanded(true); - - AddStep("click", () => InputManager.Click(MouseButton.Left)); - checkExpanded(false); - AddStep("click", () => InputManager.Click(MouseButton.Left)); - checkExpanded(true); - - AddStep("move away", () => InputManager.MoveMouseTo(Vector2.One)); - checkExpanded(true); - - AddStep("click", () => InputManager.Click(MouseButton.Left)); - checkExpanded(false); - } - - [Test] - public void TestHoverExpandsAndCollapsesWhenHeaderClicked() - { - AddStep("add customisable mod", () => + AddStep("touch header", () => { - SelectedMods.Value = new[] { new OsuModDoubleTime() }; - panel.Enabled.Value = true; + var touch = new Touch(TouchSource.Touch1, header.ScreenSpaceDrawQuad.Centre); + InputManager.BeginTouch(touch); + Schedule(() => InputManager.EndTouch(touch)); }); - - AddStep("hover header", () => InputManager.MoveMouseTo(header)); checkExpanded(true); - AddStep("click", () => InputManager.Click(MouseButton.Left)); + AddStep("touch away from header", () => + { + var touch = new Touch(TouchSource.Touch1, header.ScreenSpaceDrawQuad.TopLeft - new Vector2(10)); + InputManager.BeginTouch(touch); + Schedule(() => InputManager.EndTouch(touch)); + }); checkExpanded(false); } From 5211e606b5b0c9919a2675c09a263ec88acb8c0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 2 Sep 2024 11:33:43 +0200 Subject: [PATCH 2159/2493] Fix mod customisation header being non-functional on mobile As expected the previous touch handling would prevent the header from working entirely when click handling was removed from the header. --- .../Overlays/Mods/ModCustomisationHeader.cs | 20 ++----------------- 1 file changed, 2 insertions(+), 18 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModCustomisationHeader.cs b/osu.Game/Overlays/Mods/ModCustomisationHeader.cs index 5ddcf01b88..32fd5a37aa 100644 --- a/osu.Game/Overlays/Mods/ModCustomisationHeader.cs +++ b/osu.Game/Overlays/Mods/ModCustomisationHeader.cs @@ -112,26 +112,10 @@ namespace osu.Game.Overlays.Mods }, true); } - private bool touchedThisFrame; - - protected override bool OnTouchDown(TouchDownEvent e) - { - if (Enabled.Value) - { - touchedThisFrame = true; - Schedule(() => touchedThisFrame = false); - } - - return base.OnTouchDown(e); - } - protected override bool OnHover(HoverEvent e) { - if (Enabled.Value) - { - if (!touchedThisFrame && panel.ExpandedState.Value == ModCustomisationPanelState.Collapsed) - panel.ExpandedState.Value = ModCustomisationPanelState.Expanded; - } + if (Enabled.Value && panel.ExpandedState.Value == ModCustomisationPanelState.Collapsed) + panel.ExpandedState.Value = ModCustomisationPanelState.Expanded; return base.OnHover(e); } From 872d14ed88b8daf04121cd1a5483bbf8cb5dbd79 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 2 Sep 2024 19:18:43 +0900 Subject: [PATCH 2160/2493] Fix incorrect handling of ordered playlist items --- .../Match/MultiplayerSpectateButton.cs | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerSpectateButton.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerSpectateButton.cs index bb6cd6cdaa..fa26a85786 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerSpectateButton.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerSpectateButton.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.Linq; using System.Threading; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -58,6 +57,13 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match automaticallyDownload = config.GetBindable(OsuSetting.AutomaticallyDownloadMissingBeatmaps); } + protected override void LoadComplete() + { + base.LoadComplete(); + + CurrentPlaylistItem.BindValueChanged(_ => Scheduler.AddOnce(checkForAutomaticDownload), true); + } + protected override void OnRoomUpdated() { base.OnRoomUpdated(); @@ -102,19 +108,13 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match private CancellationTokenSource? downloadCheckCancellation; - protected override void PlaylistItemChanged(MultiplayerPlaylistItem item) - { - base.PlaylistItemChanged(item); - Scheduler.AddOnce(checkForAutomaticDownload); - } - private void checkForAutomaticDownload() { - MultiplayerPlaylistItem? item = Client.Room?.Playlist.FirstOrDefault(i => !i.Expired); + PlaylistItem? currentItem = CurrentPlaylistItem.Value; downloadCheckCancellation?.Cancel(); - if (item == null) + if (currentItem == null) return; if (!automaticallyDownload.Value) @@ -132,7 +132,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match // In a perfect world we'd use BeatmapAvailability, but there's no event-driven flow for when a selection changes. // ie. if selection changes from "not downloaded" to another "not downloaded" we wouldn't get a value changed raised. beatmapLookupCache - .GetBeatmapAsync(item.BeatmapID, (downloadCheckCancellation = new CancellationTokenSource()).Token) + .GetBeatmapAsync(currentItem.Beatmap.OnlineID, (downloadCheckCancellation = new CancellationTokenSource()).Token) .ContinueWith(resolved => Schedule(() => { var beatmapSet = resolved.GetResultSafely()?.BeatmapSet; From 7b139433772804c5c5f044abc57589e9cf638af5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 2 Sep 2024 19:20:05 +0900 Subject: [PATCH 2161/2493] Handle changes to the automatic download setting immediately --- .../OnlinePlay/Multiplayer/Match/MultiplayerSpectateButton.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerSpectateButton.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerSpectateButton.cs index fa26a85786..ea7ab2dce3 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerSpectateButton.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerSpectateButton.cs @@ -54,7 +54,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match { operationInProgress = ongoingOperationTracker.InProgress.GetBoundCopy(); operationInProgress.BindValueChanged(_ => updateState()); + automaticallyDownload = config.GetBindable(OsuSetting.AutomaticallyDownloadMissingBeatmaps); + automaticallyDownload.BindValueChanged(_ => Scheduler.AddOnce(checkForAutomaticDownload)); } protected override void LoadComplete() From 16c2c140377286c2a90430d23a37e5c7da43cc34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 2 Sep 2024 14:45:29 +0200 Subject: [PATCH 2162/2493] Adjust tests further to match new UX --- .../TestSceneModSelectOverlay.cs | 20 +++++++------------ 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs index f21c64f7fe..280497e861 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs @@ -241,12 +241,8 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep("select difficulty adjust via panel", () => getPanelForMod(typeof(OsuModDifficultyAdjust)).TriggerClick()); assertCustomisationToggleState(disabled: false, active: true); - AddStep("dismiss mod customisation via toggle", () => - { - InputManager.MoveMouseTo(modSelectOverlay.ChildrenOfType().Single()); - InputManager.Click(MouseButton.Left); - }); - assertCustomisationToggleState(disabled: false, active: false); + AddStep("move mouse away", () => InputManager.MoveMouseTo(Vector2.Zero)); + assertCustomisationToggleState(disabled: false, active: true); AddStep("reset mods", () => SelectedMods.SetDefault()); AddStep("select difficulty adjust via panel", () => getPanelForMod(typeof(OsuModDifficultyAdjust)).TriggerClick()); @@ -664,7 +660,7 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep("select DT", () => SelectedMods.Value = new Mod[] { new OsuModDoubleTime() }); AddAssert("DT selected", () => modSelectOverlay.ChildrenOfType().Count(panel => panel.Active.Value), () => Is.EqualTo(1)); - AddStep("open customisation area", () => modSelectOverlay.ChildrenOfType().Single().TriggerClick()); + AddStep("open customisation area", () => InputManager.MoveMouseTo(modSelectOverlay.ChildrenOfType().Single())); assertCustomisationToggleState(disabled: false, active: true); AddStep("hover over mod settings slider", () => @@ -976,7 +972,7 @@ namespace osu.Game.Tests.Visual.UserInterface createScreen(); AddStep("select DT", () => SelectedMods.Value = new Mod[] { new OsuModDoubleTime() }); - AddStep("open customisation panel", () => this.ChildrenOfType().Single().TriggerClick()); + AddStep("open customisation panel", () => InputManager.MoveMouseTo(this.ChildrenOfType().Single())); AddAssert("search lost focus", () => !this.ChildrenOfType().Single().HasFocus); AddStep("press tab", () => InputManager.Key(Key.Tab)); @@ -991,15 +987,13 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep("press backspace", () => InputManager.Key(Key.BackSpace)); AddAssert("mods not deselected", () => SelectedMods.Value.Single() is OsuModDoubleTime); - AddStep("move mouse to scroll bar", () => InputManager.MoveMouseTo(modSelectOverlay.ChildrenOfType().Single().ScreenSpaceDrawQuad.BottomLeft + new Vector2(10f, -5f))); + AddStep("move mouse to customisation panel", () => InputManager.MoveMouseTo(modSelectOverlay.ChildrenOfType().First())); AddStep("scroll down", () => InputManager.ScrollVerticalBy(-10f)); AddAssert("column not scrolled", () => modSelectOverlay.ChildrenOfType().Single().IsScrolledToStart()); - AddStep("press mouse", () => InputManager.PressButton(MouseButton.Left)); - AddAssert("search still not focused", () => !this.ChildrenOfType().Single().HasFocus); - AddStep("release mouse", () => InputManager.ReleaseButton(MouseButton.Left)); - AddAssert("customisation panel closed by click", + AddStep("move mouse away", () => InputManager.MoveMouseTo(Vector2.Zero)); + AddAssert("customisation panel closed", () => this.ChildrenOfType().Single().ExpandedState.Value, () => Is.EqualTo(ModCustomisationPanel.ModCustomisationPanelState.Collapsed)); From e3457d850157808928b7ef912b8ecc562826f5a2 Mon Sep 17 00:00:00 2001 From: Fabep Date: Mon, 2 Sep 2024 19:14:08 +0200 Subject: [PATCH 2163/2493] Mod customisation header's color is now based on the state of the panel rather than the hover of the container. --- .../Overlays/Mods/ModCustomisationHeader.cs | 28 ++++++++++++++----- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModCustomisationHeader.cs b/osu.Game/Overlays/Mods/ModCustomisationHeader.cs index 32fd5a37aa..d8191f5ba5 100644 --- a/osu.Game/Overlays/Mods/ModCustomisationHeader.cs +++ b/osu.Game/Overlays/Mods/ModCustomisationHeader.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Collections.Generic; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; @@ -20,7 +19,7 @@ using static osu.Game.Overlays.Mods.ModCustomisationPanel; namespace osu.Game.Overlays.Mods { - public partial class ModCustomisationHeader : OsuHoverContainer + public partial class ModCustomisationHeader : OsuClickableContainer { private Box background = null!; private Box backgroundFlash = null!; @@ -29,8 +28,6 @@ namespace osu.Game.Overlays.Mods [Resolved] private OverlayColourProvider colourProvider { get; set; } = null!; - protected override IEnumerable EffectTargets => new[] { background }; - public readonly Bindable ExpandedState = new Bindable(ModCustomisationPanelState.Collapsed); private readonly ModCustomisationPanel panel; @@ -52,6 +49,7 @@ namespace osu.Game.Overlays.Mods background = new Box { RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Dark3, }, backgroundFlash = new Box { @@ -84,9 +82,6 @@ namespace osu.Game.Overlays.Mods } } }; - - IdleColour = colourProvider.Dark3; - HoverColour = colourProvider.Light4; } protected override void LoadComplete() @@ -110,6 +105,20 @@ namespace osu.Game.Overlays.Mods { icon.ScaleTo(v.NewValue > ModCustomisationPanelState.Collapsed ? new Vector2(1, -1) : Vector2.One, 300, Easing.OutQuint); }, true); + + panel.ExpandedState.BindValueChanged(v => + { + switch (v.NewValue) + { + case ModCustomisationPanelState.Expanded: + case ModCustomisationPanelState.ExpandedByMod: + fadeBackgroundColor(colourProvider.Light4); + break; + default: + fadeBackgroundColor(colourProvider.Dark3); + break; + }; + }, false); } protected override bool OnHover(HoverEvent e) @@ -119,5 +128,10 @@ namespace osu.Game.Overlays.Mods return base.OnHover(e); } + + private void fadeBackgroundColor(Color4 color) + { + background.FadeColour(color, 500, Easing.OutQuint); + } } } From 582ffcfc9722342f73d83778fc950cacc1bb9439 Mon Sep 17 00:00:00 2001 From: Fabep Date: Mon, 2 Sep 2024 19:17:07 +0200 Subject: [PATCH 2164/2493] Woopsie! I accidentally added one too many semi-colons, so I moved it here into the commit instead ; --- osu.Game/Overlays/Mods/ModCustomisationHeader.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Mods/ModCustomisationHeader.cs b/osu.Game/Overlays/Mods/ModCustomisationHeader.cs index d8191f5ba5..da77d8dac8 100644 --- a/osu.Game/Overlays/Mods/ModCustomisationHeader.cs +++ b/osu.Game/Overlays/Mods/ModCustomisationHeader.cs @@ -117,7 +117,7 @@ namespace osu.Game.Overlays.Mods default: fadeBackgroundColor(colourProvider.Dark3); break; - }; + } }, false); } From a2b15fcdee7feccabe605e06984c0b851843de31 Mon Sep 17 00:00:00 2001 From: Sheppsu Date: Tue, 3 Sep 2024 00:59:42 -0400 Subject: [PATCH 2165/2493] rework code logic to make more sense analysis container creates settings and the settings items are created more nicely also removed use of localized strings because that can be done separately --- osu.Game.Rulesets.Osu/OsuRuleset.cs | 4 +- .../UI/OsuAnalysisContainer.cs | 35 +++++++----- .../UI/OsuAnalysisSettings.cs | 53 ++++--------------- .../PlayerSettingsOverlayStrings.cs | 20 ------- osu.Game/Rulesets/Ruleset.cs | 4 +- osu.Game/Rulesets/UI/AnalysisContainer.cs | 13 ++++- .../Play/PlayerSettings/AnalysisSettings.cs | 13 ++--- osu.Game/Screens/Play/ReplayPlayer.cs | 8 +-- 8 files changed, 55 insertions(+), 95 deletions(-) diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index 8beeeac34e..a8a1d98bf3 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -13,6 +13,7 @@ using osu.Game.Beatmaps.Legacy; using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Overlays.Settings; +using osu.Game.Replays; using osu.Game.Rulesets.Configuration; using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Edit; @@ -37,7 +38,6 @@ using osu.Game.Rulesets.Scoring.Legacy; using osu.Game.Rulesets.UI; using osu.Game.Scoring; using osu.Game.Screens.Edit.Setup; -using osu.Game.Screens.Play.PlayerSettings; using osu.Game.Screens.Ranking.Statistics; using osu.Game.Skinning; @@ -361,7 +361,7 @@ namespace osu.Game.Rulesets.Osu return adjustedDifficulty; } - public override AnalysisSettings CreateAnalysisSettings(DrawableRuleset drawableRuleset) => new OsuAnalysisSettings(drawableRuleset); + public override OsuAnalysisContainer CreateAnalysisContainer(Replay replay, Playfield playfield) => new OsuAnalysisContainer(replay, playfield); public override bool EditorShowScrollSpeed => false; } diff --git a/osu.Game.Rulesets.Osu/UI/OsuAnalysisContainer.cs b/osu.Game.Rulesets.Osu/UI/OsuAnalysisContainer.cs index 7d8ae6980c..3bddc479ef 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuAnalysisContainer.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuAnalysisContainer.cs @@ -1,10 +1,10 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. + +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. using System; using System.Collections.Generic; using osu.Framework.Allocation; -using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Lines; using osu.Framework.Graphics.Performance; @@ -21,27 +21,33 @@ namespace osu.Game.Rulesets.Osu.UI { public partial class OsuAnalysisContainer : AnalysisContainer { - public Bindable HitMarkerEnabled = new BindableBool(); - public Bindable AimMarkersEnabled = new BindableBool(); - public Bindable AimLinesEnabled = new BindableBool(); + public new OsuAnalysisSettings AnalysisSettings => (OsuAnalysisSettings)base.AnalysisSettings; + + protected new OsuPlayfield Playfield => (OsuPlayfield)base.Playfield; protected HitMarkersContainer HitMarkers; protected AimMarkersContainer AimMarkers; protected AimLinesContainer AimLines; - public OsuAnalysisContainer(Replay replay) - : base(replay) + public OsuAnalysisContainer(Replay replay, Playfield playfield) + : base(replay, playfield) { InternalChildren = new Drawable[] { + AimLines = new AimLinesContainer { Depth = float.MaxValue }, HitMarkers = new HitMarkersContainer(), - AimMarkers = new AimMarkersContainer { Depth = float.MinValue }, - AimLines = new AimLinesContainer { Depth = float.MaxValue } + AimMarkers = new AimMarkersContainer { Depth = float.MinValue } }; + } - HitMarkerEnabled.ValueChanged += e => HitMarkers.FadeTo(e.NewValue ? 1 : 0); - AimMarkersEnabled.ValueChanged += e => AimMarkers.FadeTo(e.NewValue ? 1 : 0); - AimLinesEnabled.ValueChanged += e => AimLines.FadeTo(e.NewValue ? 1 : 0); + protected override OsuAnalysisSettings CreateAnalysisSettings() + { + var settings = new OsuAnalysisSettings(); + settings.HitMarkersEnabled.ValueChanged += e => HitMarkers.FadeTo(e.NewValue ? 1 : 0); + settings.AimMarkersEnabled.ValueChanged += e => AimMarkers.FadeTo(e.NewValue ? 1 : 0); + settings.AimLinesEnabled.ValueChanged += e => AimLines.FadeTo(e.NewValue ? 1 : 0); + settings.CursorHideEnabled.ValueChanged += e => Playfield.Cursor.FadeTo(e.NewValue ? 0 : 1); + return settings; } [BackgroundDependencyLoader] @@ -51,6 +57,11 @@ namespace osu.Game.Rulesets.Osu.UI AimMarkers.Hide(); AimLines.Hide(); + LoadReplay(); + } + + protected void LoadReplay() + { bool leftHeld = false; bool rightHeld = false; diff --git a/osu.Game.Rulesets.Osu/UI/OsuAnalysisSettings.cs b/osu.Game.Rulesets.Osu/UI/OsuAnalysisSettings.cs index b3d5231ade..c45b893a1c 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuAnalysisSettings.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuAnalysisSettings.cs @@ -2,58 +2,23 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Bindables; -using osu.Framework.Graphics; -using osu.Game.Localisation; -using osu.Game.Replays; -using osu.Game.Rulesets.UI; +using osu.Game.Configuration; using osu.Game.Screens.Play.PlayerSettings; namespace osu.Game.Rulesets.Osu.UI { public partial class OsuAnalysisSettings : AnalysisSettings { - protected new DrawableOsuRuleset DrawableRuleset => (DrawableOsuRuleset)base.DrawableRuleset; + [SettingSource("Hit markers", SettingControlType = typeof(PlayerCheckbox))] + public BindableBool HitMarkersEnabled { get; } = new BindableBool(); - private readonly PlayerCheckbox hitMarkerToggle; - private readonly PlayerCheckbox aimMarkerToggle; - private readonly PlayerCheckbox aimLinesToggle; + [SettingSource("Aim markers", SettingControlType = typeof(PlayerCheckbox))] + public BindableBool AimMarkersEnabled { get; } = new BindableBool(); - public OsuAnalysisSettings(DrawableRuleset drawableRuleset) - : base(drawableRuleset) - { - PlayerCheckbox hideCursorToggle; + [SettingSource("Aim lines", SettingControlType = typeof(PlayerCheckbox))] + public BindableBool AimLinesEnabled { get; } = new BindableBool(); - Children = new Drawable[] - { - hitMarkerToggle = new PlayerCheckbox { LabelText = PlayerSettingsOverlayStrings.HitMarkers }, - aimMarkerToggle = new PlayerCheckbox { LabelText = PlayerSettingsOverlayStrings.AimMarkers }, - aimLinesToggle = new PlayerCheckbox { LabelText = PlayerSettingsOverlayStrings.AimLines }, - hideCursorToggle = new PlayerCheckbox { LabelText = PlayerSettingsOverlayStrings.HideCursor } - }; - - hideCursorToggle.Current.BindValueChanged(onCursorToggle); - } - - private void onCursorToggle(ValueChangedEvent hide) - { - // this only hides half the cursor - if (hide.NewValue) - { - DrawableRuleset.Playfield.Cursor.FadeOut(); - } - else - { - DrawableRuleset.Playfield.Cursor.FadeIn(); - } - } - - public override AnalysisContainer CreateAnalysisContainer(Replay replay) - { - var analysisContainer = new OsuAnalysisContainer(replay); - analysisContainer.HitMarkerEnabled.BindTo(hitMarkerToggle.Current); - analysisContainer.AimMarkersEnabled.BindTo(aimMarkerToggle.Current); - analysisContainer.AimLinesEnabled.BindTo(aimLinesToggle.Current); - return analysisContainer; - } + [SettingSource("Hide cursor", SettingControlType = typeof(PlayerCheckbox))] + public BindableBool CursorHideEnabled { get; } = new BindableBool(); } } diff --git a/osu.Game/Localisation/PlayerSettingsOverlayStrings.cs b/osu.Game/Localisation/PlayerSettingsOverlayStrings.cs index 017cc9bf82..60874da561 100644 --- a/osu.Game/Localisation/PlayerSettingsOverlayStrings.cs +++ b/osu.Game/Localisation/PlayerSettingsOverlayStrings.cs @@ -19,26 +19,6 @@ namespace osu.Game.Localisation /// public static LocalisableString StepForward => new TranslatableString(getKey(@"step_forward_frame"), @"Step forward one frame"); - /// - /// "Hit markers" - /// - public static LocalisableString HitMarkers => new TranslatableString(getKey(@"hit_markers"), @"Hit markers"); - - /// - /// "Aim markers" - /// - public static LocalisableString AimMarkers => new TranslatableString(getKey(@"aim_markers"), @"Aim markers"); - - /// - /// "Hide cursor" - /// - public static LocalisableString HideCursor => new TranslatableString(getKey(@"hide_cursor"), @"Hide cursor"); - - /// - /// "Aim lines" - /// - public static LocalisableString AimLines => new TranslatableString(getKey(@"aim_lines"), @"Aim lines"); - /// /// "Seek backward {0} seconds" /// diff --git a/osu.Game/Rulesets/Ruleset.cs b/osu.Game/Rulesets/Ruleset.cs index 4626698190..fdf43c2f09 100644 --- a/osu.Game/Rulesets/Ruleset.cs +++ b/osu.Game/Rulesets/Ruleset.cs @@ -17,6 +17,7 @@ using osu.Game.Beatmaps.Legacy; using osu.Game.Configuration; using osu.Game.Extensions; using osu.Game.Overlays.Settings; +using osu.Game.Replays; using osu.Game.Rulesets.Configuration; using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Edit; @@ -27,7 +28,6 @@ using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI; using osu.Game.Scoring; using osu.Game.Screens.Edit.Setup; -using osu.Game.Screens.Play.PlayerSettings; using osu.Game.Screens.Ranking.Statistics; using osu.Game.Skinning; using osu.Game.Users; @@ -410,6 +410,6 @@ namespace osu.Game.Rulesets public virtual DifficultySection? CreateEditorDifficultySection() => null; - public virtual AnalysisSettings? CreateAnalysisSettings(DrawableRuleset drawableRuleset) => null; + public virtual AnalysisContainer? CreateAnalysisContainer(Replay replay, Playfield playfield) => null; } } diff --git a/osu.Game/Rulesets/UI/AnalysisContainer.cs b/osu.Game/Rulesets/UI/AnalysisContainer.cs index 62d54374e7..69a71cf06e 100644 --- a/osu.Game/Rulesets/UI/AnalysisContainer.cs +++ b/osu.Game/Rulesets/UI/AnalysisContainer.cs @@ -3,16 +3,25 @@ using osu.Framework.Graphics.Containers; using osu.Game.Replays; +using osu.Game.Screens.Play.PlayerSettings; namespace osu.Game.Rulesets.UI { - public partial class AnalysisContainer : Container + public abstract partial class AnalysisContainer : Container { protected Replay Replay; + protected Playfield Playfield; - public AnalysisContainer(Replay replay) + public AnalysisSettings AnalysisSettings; + + public AnalysisContainer(Replay replay, Playfield playfield) { Replay = replay; + Playfield = playfield; + + AnalysisSettings = CreateAnalysisSettings(); } + + protected abstract AnalysisSettings CreateAnalysisSettings(); } } diff --git a/osu.Game/Screens/Play/PlayerSettings/AnalysisSettings.cs b/osu.Game/Screens/Play/PlayerSettings/AnalysisSettings.cs index 91531b28b4..e1f77cef12 100644 --- a/osu.Game/Screens/Play/PlayerSettings/AnalysisSettings.cs +++ b/osu.Game/Screens/Play/PlayerSettings/AnalysisSettings.cs @@ -1,21 +1,16 @@ // 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.Replays; -using osu.Game.Rulesets.UI; +using osu.Game.Configuration; namespace osu.Game.Screens.Play.PlayerSettings { - public abstract partial class AnalysisSettings : PlayerSettingsGroup + public partial class AnalysisSettings : PlayerSettingsGroup { - protected DrawableRuleset DrawableRuleset; - - protected AnalysisSettings(DrawableRuleset drawableRuleset) + public AnalysisSettings() : base("Analysis Settings") { - DrawableRuleset = drawableRuleset; + AddRange(this.CreateSettingsControls()); } - - public abstract AnalysisContainer CreateAnalysisContainer(Replay replay); } } diff --git a/osu.Game/Screens/Play/ReplayPlayer.cs b/osu.Game/Screens/Play/ReplayPlayer.cs index 5d604003c8..af9568c08c 100644 --- a/osu.Game/Screens/Play/ReplayPlayer.cs +++ b/osu.Game/Screens/Play/ReplayPlayer.cs @@ -72,12 +72,12 @@ namespace osu.Game.Screens.Play HUDOverlay.PlayerSettingsOverlay.AddAtStart(playbackSettings); - var analysisSettings = DrawableRuleset.Ruleset.CreateAnalysisSettings(DrawableRuleset); + var analysisContainer = DrawableRuleset.Ruleset.CreateAnalysisContainer(GameplayState.Score.Replay, DrawableRuleset.Playfield); - if (analysisSettings != null) + if (analysisContainer != null) { - HUDOverlay.PlayerSettingsOverlay.AddAtStart(analysisSettings); - DrawableRuleset.Playfield.AddAnalysisContainer(analysisSettings.CreateAnalysisContainer(GameplayState.Score.Replay)); + HUDOverlay.PlayerSettingsOverlay.AddAtStart(analysisContainer.AnalysisSettings); + DrawableRuleset.Playfield.AddAnalysisContainer(analysisContainer); } } From 56db29d0f5c7d798144660186ededd38a8ff03ae Mon Sep 17 00:00:00 2001 From: Sheppsu Date: Tue, 3 Sep 2024 01:38:54 -0400 Subject: [PATCH 2166/2493] make test go indefinitely --- .../TestSceneHitMarker.cs | 91 ------------ .../TestSceneOsuAnalysisContainer.cs | 134 ++++++++++++++++++ .../UI/OsuAnalysisContainer.cs | 2 + 3 files changed, 136 insertions(+), 91 deletions(-) delete mode 100644 osu.Game.Rulesets.Osu.Tests/TestSceneHitMarker.cs create mode 100644 osu.Game.Rulesets.Osu.Tests/TestSceneOsuAnalysisContainer.cs diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneHitMarker.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneHitMarker.cs deleted file mode 100644 index e4c48f96b8..0000000000 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneHitMarker.cs +++ /dev/null @@ -1,91 +0,0 @@ -// 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.Collections.Generic; -using System.Linq; -using NUnit.Framework; -using osu.Game.Replays; -using osu.Game.Rulesets.Osu.Replays; -using osu.Game.Rulesets.Osu.UI; -using osu.Game.Rulesets.Replays; -using osu.Game.Tests.Visual; -using osuTK; - -namespace osu.Game.Rulesets.Osu.Tests -{ - public partial class TestSceneHitMarker : OsuTestScene - { - private TestOsuAnalysisContainer analysisContainer; - - [Test] - public void TestHitMarkers() - { - createAnalysisContainer(); - AddStep("enable hit markers", () => analysisContainer.HitMarkerEnabled.Value = true); - AddAssert("hit markers visible", () => analysisContainer.HitMarkersVisible); - AddStep("disable hit markers", () => analysisContainer.HitMarkerEnabled.Value = false); - AddAssert("hit markers not visible", () => !analysisContainer.HitMarkersVisible); - } - - [Test] - public void TestAimMarker() - { - createAnalysisContainer(); - AddStep("enable aim markers", () => analysisContainer.AimMarkersEnabled.Value = true); - AddAssert("aim markers visible", () => analysisContainer.AimMarkersVisible); - AddStep("disable aim markers", () => analysisContainer.AimMarkersEnabled.Value = false); - AddAssert("aim markers not visible", () => !analysisContainer.AimMarkersVisible); - } - - [Test] - public void TestAimLines() - { - createAnalysisContainer(); - AddStep("enable aim lines", () => analysisContainer.AimLinesEnabled.Value = true); - AddAssert("aim lines visible", () => analysisContainer.AimLinesVisible); - AddStep("disable aim lines", () => analysisContainer.AimLinesEnabled.Value = false); - AddAssert("aim lines not visible", () => !analysisContainer.AimLinesVisible); - } - - private void createAnalysisContainer() - { - AddStep("create new analysis container", () => Child = analysisContainer = new TestOsuAnalysisContainer(fabricateReplay())); - } - - private Replay fabricateReplay() - { - var frames = new List(); - - for (int i = 0; i < 50; i++) - { - frames.Add(new OsuReplayFrame - { - Time = Time.Current + i * 15, - Position = new Vector2(20 + i * 10, 20), - Actions = - { - i % 2 == 0 ? OsuAction.LeftButton : OsuAction.RightButton - } - }); - } - - return new Replay { Frames = frames }; - } - - private partial class TestOsuAnalysisContainer : OsuAnalysisContainer - { - public TestOsuAnalysisContainer(Replay replay) - : base(replay) - { - } - - public bool HitMarkersVisible => HitMarkers.Alpha > 0 && HitMarkers.Entries.Any(); - - public bool AimMarkersVisible => AimMarkers.Alpha > 0 && AimMarkers.Entries.Any(); - - public bool AimLinesVisible => AimLines.Alpha > 0 && AimLines.Vertices.Count > 1; - } - } -} diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuAnalysisContainer.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuAnalysisContainer.cs new file mode 100644 index 0000000000..a173256557 --- /dev/null +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuAnalysisContainer.cs @@ -0,0 +1,134 @@ +// 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.Collections.Generic; +using System.Linq; +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Threading; +using osu.Game.Replays; +using osu.Game.Rulesets.Osu.Replays; +using osu.Game.Rulesets.Osu.UI; +using osu.Game.Rulesets.Replays; +using osu.Game.Tests.Visual; +using osuTK; + +namespace osu.Game.Rulesets.Osu.Tests +{ + public partial class TestSceneOsuAnalysisContainer : OsuTestScene + { + private TestOsuAnalysisContainer analysisContainer; + + [BackgroundDependencyLoader] + private void load() + { + Child = analysisContainer = createAnalysisContainer(); + } + + [Test] + public void TestHitMarkers() + { + var loop = createAnalysisContainer(); + AddStep("enable hit markers", () => analysisContainer.AnalysisSettings.HitMarkersEnabled.Value = true); + AddAssert("hit markers visible", () => analysisContainer.HitMarkersVisible); + AddStep("disable hit markers", () => analysisContainer.AnalysisSettings.HitMarkersEnabled.Value = false); + AddAssert("hit markers not visible", () => !analysisContainer.HitMarkersVisible); + } + + [Test] + public void TestAimMarker() + { + var loop = createAnalysisContainer(); + AddStep("enable aim markers", () => analysisContainer.AnalysisSettings.AimMarkersEnabled.Value = true); + AddAssert("aim markers visible", () => analysisContainer.AimMarkersVisible); + AddStep("disable aim markers", () => analysisContainer.AnalysisSettings.AimMarkersEnabled.Value = false); + AddAssert("aim markers not visible", () => !analysisContainer.AimMarkersVisible); + } + + [Test] + public void TestAimLines() + { + var loop = createAnalysisContainer(); + AddStep("enable aim lines", () => analysisContainer.AnalysisSettings.AimLinesEnabled.Value = true); + AddAssert("aim lines visible", () => analysisContainer.AimLinesVisible); + AddStep("disable aim lines", () => analysisContainer.AnalysisSettings.AimLinesEnabled.Value = false); + AddAssert("aim lines not visible", () => !analysisContainer.AimLinesVisible); + } + + private TestOsuAnalysisContainer createAnalysisContainer() => new TestOsuAnalysisContainer(); + + private partial class TestOsuAnalysisContainer : OsuAnalysisContainer + { + public TestOsuAnalysisContainer() + : base(new Replay(), new OsuPlayfield()) + { + } + + [BackgroundDependencyLoader] + private void load() + { + Replay = fabricateReplay(); + LoadReplay(); + + makeReplayLoop(); + } + + private void makeReplayLoop() + { + Scheduler.AddDelayed(() => + { + Replay = fabricateReplay(); + + HitMarkers.Clear(); + AimMarkers.Clear(); + AimLines.Clear(); + + LoadReplay(); + + makeReplayLoop(); + }, 15000); + } + + public bool HitMarkersVisible => HitMarkers.Alpha > 0 && HitMarkers.Entries.Any(); + + public bool AimMarkersVisible => AimMarkers.Alpha > 0 && AimMarkers.Entries.Any(); + + public bool AimLinesVisible => AimLines.Alpha > 0 && AimLines.Vertices.Count > 1; + + private Replay fabricateReplay() + { + var frames = new List(); + var random = new Random(); + int posX = 250; + int posY = 250; + bool leftOrRight = false; + + for (int i = 0; i < 1000; i++) + { + posX = Math.Clamp(posX + random.Next(-10, 11), 0, 500); + posY = Math.Clamp(posY + random.Next(-10, 11), 0, 500); + + var actions = new List(); + + if (i % 20 == 0) + { + actions.Add(leftOrRight ? OsuAction.LeftButton : OsuAction.RightButton); + leftOrRight = !leftOrRight; + } + + frames.Add(new OsuReplayFrame + { + Time = Time.Current + i * 15, + Position = new Vector2(posX, posY), + Actions = actions + }); + } + + return new Replay { Frames = frames }; + } + } + } +} diff --git a/osu.Game.Rulesets.Osu/UI/OsuAnalysisContainer.cs b/osu.Game.Rulesets.Osu/UI/OsuAnalysisContainer.cs index 3bddc479ef..2f341a35bb 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuAnalysisContainer.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuAnalysisContainer.cs @@ -142,6 +142,8 @@ namespace osu.Game.Rulesets.Osu.UI public void Add(AimPointEntry entry) => lifetimeManager.AddEntry(entry); + public void Clear() => lifetimeManager.ClearEntries(); + private void entryBecameAlive(LifetimeEntry entry) { aliveEntries.Add((AimPointEntry)entry); From a549cdd5b9f1b8a968e9a3e5d57c926db22b8675 Mon Sep 17 00:00:00 2001 From: Sheppsu Date: Tue, 3 Sep 2024 04:49:50 -0400 Subject: [PATCH 2167/2493] persist analysis settings --- .../UI/OsuAnalysisContainer.cs | 27 ++++++++++++------- .../UI/OsuAnalysisSettings.cs | 10 +++++++ osu.Game/Configuration/OsuConfigManager.cs | 10 +++++++ 3 files changed, 38 insertions(+), 9 deletions(-) diff --git a/osu.Game.Rulesets.Osu/UI/OsuAnalysisContainer.cs b/osu.Game.Rulesets.Osu/UI/OsuAnalysisContainer.cs index 2f341a35bb..4eff147772 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuAnalysisContainer.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuAnalysisContainer.cs @@ -1,5 +1,4 @@ - -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. using System; @@ -38,28 +37,38 @@ namespace osu.Game.Rulesets.Osu.UI HitMarkers = new HitMarkersContainer(), AimMarkers = new AimMarkersContainer { Depth = float.MinValue } }; + } protected override OsuAnalysisSettings CreateAnalysisSettings() { var settings = new OsuAnalysisSettings(); - settings.HitMarkersEnabled.ValueChanged += e => HitMarkers.FadeTo(e.NewValue ? 1 : 0); - settings.AimMarkersEnabled.ValueChanged += e => AimMarkers.FadeTo(e.NewValue ? 1 : 0); - settings.AimLinesEnabled.ValueChanged += e => AimLines.FadeTo(e.NewValue ? 1 : 0); - settings.CursorHideEnabled.ValueChanged += e => Playfield.Cursor.FadeTo(e.NewValue ? 0 : 1); + settings.HitMarkersEnabled.ValueChanged += e => toggleHitMarkers(e.NewValue); + settings.AimMarkersEnabled.ValueChanged += e => toggleAimMarkers(e.NewValue); + settings.AimLinesEnabled.ValueChanged += e => toggleAimLines(e.NewValue); + settings.CursorHideEnabled.ValueChanged += e => toggleCursorHidden(e.NewValue); return settings; } [BackgroundDependencyLoader] private void load() { - HitMarkers.Hide(); - AimMarkers.Hide(); - AimLines.Hide(); + toggleHitMarkers(AnalysisSettings.HitMarkersEnabled.Value); + toggleAimMarkers(AnalysisSettings.AimMarkersEnabled.Value); + toggleAimLines(AnalysisSettings.AimLinesEnabled.Value); + toggleCursorHidden(AnalysisSettings.CursorHideEnabled.Value); LoadReplay(); } + private void toggleHitMarkers(bool value) => HitMarkers.FadeTo(value ? 1 : 0); + + private void toggleAimMarkers(bool value) => AimMarkers.FadeTo(value ? 1 : 0); + + private void toggleAimLines(bool value) => AimLines.FadeTo(value ? 1 : 0); + + private void toggleCursorHidden(bool value) => Playfield.Cursor.FadeTo(value ? 0 : 1); + protected void LoadReplay() { bool leftHeld = false; diff --git a/osu.Game.Rulesets.Osu/UI/OsuAnalysisSettings.cs b/osu.Game.Rulesets.Osu/UI/OsuAnalysisSettings.cs index c45b893a1c..ae81b2c0b8 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuAnalysisSettings.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuAnalysisSettings.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.Bindables; using osu.Game.Configuration; using osu.Game.Screens.Play.PlayerSettings; @@ -20,5 +21,14 @@ namespace osu.Game.Rulesets.Osu.UI [SettingSource("Hide cursor", SettingControlType = typeof(PlayerCheckbox))] public BindableBool CursorHideEnabled { get; } = new BindableBool(); + + [BackgroundDependencyLoader] + private void load(OsuConfigManager config) + { + config.BindWith(OsuSetting.ReplayHitMarkersEnabled, HitMarkersEnabled); + config.BindWith(OsuSetting.ReplayAimMarkersEnabled, AimMarkersEnabled); + config.BindWith(OsuSetting.ReplayAimLinesEnabled, AimLinesEnabled); + config.BindWith(OsuSetting.ReplayCursorHideEnabled, CursorHideEnabled); + } } } diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index 8d6c244b35..8b75c9c934 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -154,6 +154,12 @@ namespace osu.Game.Configuration SetDefault(OsuSetting.IncreaseFirstObjectVisibility, true); SetDefault(OsuSetting.GameplayDisableWinKey, true); + // Replay + SetDefault(OsuSetting.ReplayHitMarkersEnabled, false); + SetDefault(OsuSetting.ReplayAimMarkersEnabled, false); + SetDefault(OsuSetting.ReplayAimLinesEnabled, false); + SetDefault(OsuSetting.ReplayCursorHideEnabled, false); + // Update SetDefault(OsuSetting.ReleaseStream, ReleaseStream.Lazer); @@ -413,6 +419,10 @@ namespace osu.Game.Configuration EditorShowHitMarkers, EditorAutoSeekOnPlacement, DiscordRichPresence, + ReplayHitMarkersEnabled, + ReplayAimMarkersEnabled, + ReplayAimLinesEnabled, + ReplayCursorHideEnabled, ShowOnlineExplicitContent, LastProcessedMetadataId, From 6c89c4eed6f2e619b2932324c9173d0dcaf9f39d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 3 Sep 2024 18:50:57 +0900 Subject: [PATCH 2168/2493] Fix rewind causing weirdness with progress bar animation --- osu.Game/Screens/Play/BreakOverlay.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/osu.Game/Screens/Play/BreakOverlay.cs b/osu.Game/Screens/Play/BreakOverlay.cs index 7f9e879b44..4ed8b69a77 100644 --- a/osu.Game/Screens/Play/BreakOverlay.cs +++ b/osu.Game/Screens/Play/BreakOverlay.cs @@ -145,6 +145,13 @@ namespace osu.Game.Screens.Play base.Update(); remainingTimeBox.Height = Math.Min(8, remainingTimeBox.DrawWidth); + + // Keep things simple by resetting beat synced transforms on a rewind. + if (Clock.ElapsedFrameTime < 0) + { + remainingTimeBox.ClearTransforms(targetMember: nameof(Width)); + remainingTimeBox.Width = remainingTimeForCurrentPeriod; + } } protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes) From 08224b416e9664f264366a4280edd891d1439782 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 3 Sep 2024 19:11:34 +0900 Subject: [PATCH 2169/2493] Simplify update process by caching pending update info and early-handling edge case --- osu.Desktop/Updater/VelopackUpdateManager.cs | 28 +++++++++++--------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/osu.Desktop/Updater/VelopackUpdateManager.cs b/osu.Desktop/Updater/VelopackUpdateManager.cs index 4d2535ed32..bf7122d720 100644 --- a/osu.Desktop/Updater/VelopackUpdateManager.cs +++ b/osu.Desktop/Updater/VelopackUpdateManager.cs @@ -25,11 +25,13 @@ namespace osu.Desktop.Updater [Resolved] private ILocalUserPlayInfo? localUserInfo { get; set; } + private UpdateInfo? pendingUpdate; + public VelopackUpdateManager() { updateManager = new UpdateManager(new GithubSource(@"https://github.com/ppy/osu", null, false), new UpdateOptions { - AllowVersionDowngrade = true + AllowVersionDowngrade = true, }); } @@ -52,33 +54,33 @@ namespace osu.Desktop.Updater if (localUserInfo?.IsPlaying.Value == true) return false; - UpdateInfo? info = await updateManager.CheckForUpdatesAsync().ConfigureAwait(false); - - // Handle no updates available. - if (info == null) + if (pendingUpdate != null) { - // If there's no updates pending restart, bail and retry later. - if (!updateManager.IsUpdatePendingRestart) return false; - // If there is an update pending restart, show the notification to restart again. notificationOverlay.Post(new UpdateApplicationCompleteNotification { Activated = () => { - restartToApplyUpdate(null); + restartToApplyUpdate(); return true; } }); return true; } + pendingUpdate = await updateManager.CheckForUpdatesAsync().ConfigureAwait(false); + + // Handle no updates available. + if (pendingUpdate == null) + return false; + scheduleRecheck = false; if (notification == null) { notification = new UpdateProgressNotification { - CompletionClickAction = () => restartToApplyUpdate(info), + CompletionClickAction = restartToApplyUpdate, }; Schedule(() => notificationOverlay.Post(notification)); @@ -88,7 +90,7 @@ namespace osu.Desktop.Updater try { - await updateManager.DownloadUpdatesAsync(info, p => notification.Progress = p / 100f).ConfigureAwait(false); + await updateManager.DownloadUpdatesAsync(pendingUpdate, p => notification.Progress = p / 100f).ConfigureAwait(false); notification.State = ProgressNotificationState.Completed; } @@ -117,9 +119,9 @@ namespace osu.Desktop.Updater return true; } - private bool restartToApplyUpdate(UpdateInfo? info) + private bool restartToApplyUpdate() { - updateManager.WaitExitThenApplyUpdates(info?.TargetFullRelease); + updateManager.WaitExitThenApplyUpdates(pendingUpdate?.TargetFullRelease); Schedule(() => game.AttemptExit()); return true; } From b61023385a85a4508cd28b153d94fa233cc251a6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 3 Sep 2024 19:16:10 +0900 Subject: [PATCH 2170/2493] Don't log probable network failures to sentry --- osu.Desktop/Updater/VelopackUpdateManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Desktop/Updater/VelopackUpdateManager.cs b/osu.Desktop/Updater/VelopackUpdateManager.cs index bf7122d720..5b4d281f80 100644 --- a/osu.Desktop/Updater/VelopackUpdateManager.cs +++ b/osu.Desktop/Updater/VelopackUpdateManager.cs @@ -105,7 +105,7 @@ namespace osu.Desktop.Updater { // we'll ignore this and retry later. can be triggered by no internet connection or thread abortion. scheduleRecheck = true; - Logger.Error(e, @"update check failed!"); + Logger.Log($@"update check failed ({e.Message})"); } finally { From 1f122ab38de24e819697fba9c1eb2c3b6b8f4ac1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 3 Sep 2024 23:57:18 +0900 Subject: [PATCH 2171/2493] Apply new rider migration --- osu.sln.DotSettings | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.sln.DotSettings b/osu.sln.DotSettings index a792b956dd..2d0b7446f3 100644 --- a/osu.sln.DotSettings +++ b/osu.sln.DotSettings @@ -853,6 +853,7 @@ See the LICENCE file in the repository root for full licence text. True True True + True True True True From 421f245c3102bb2ab3719b7ff4717b401a0db11a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 4 Sep 2024 14:31:59 +0900 Subject: [PATCH 2172/2493] Fix per-frame allocations in `BeatmapCarousel` --- .../SongSelect/TestSceneBeatmapCarousel.cs | 4 ++-- osu.Game/Screens/Select/BeatmapCarousel.cs | 11 +++++++---- .../Carousel/DrawableCarouselBeatmapSet.cs | 18 ++++++++---------- 3 files changed, 17 insertions(+), 16 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs index ec072a3dd2..218a67e818 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs @@ -1405,9 +1405,9 @@ namespace osu.Game.Tests.Visual.SongSelect yield return item; - if (item is DrawableCarouselBeatmapSet set) + if (item is DrawableCarouselBeatmapSet set && set.Beatmaps?.IsLoaded == true) { - foreach (var difficulty in set.DrawableBeatmaps) + foreach (var difficulty in set.Beatmaps) yield return difficulty; } } diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 87cea45e87..f7e0eae4a5 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -857,8 +857,9 @@ namespace osu.Game.Screens.Select // Add those items within the previously found index range that should be displayed. foreach (var item in toDisplay) { - var panel = setPool.Get(p => p.Item = item); + var panel = setPool.Get(); + panel.Item = item; panel.Y = item.CarouselYPosition; Scroll.Add(panel); @@ -898,10 +899,12 @@ namespace osu.Game.Screens.Select Scroll.ChangeChildDepth(item, hasPassedSelection ? -item.Item.CarouselYPosition : item.Item.CarouselYPosition); } - if (item is DrawableCarouselBeatmapSet set) + if (item is DrawableCarouselBeatmapSet set && set.Beatmaps?.IsLoaded == true) { - foreach (var diff in set.DrawableBeatmaps) + foreach (var diff in set.Beatmaps) + { updateItem(diff, item); + } } } } @@ -1101,7 +1104,7 @@ namespace osu.Game.Screens.Select } /// - /// Update a item's x position and multiplicative alpha based on its y position and + /// Update an item's x position and multiplicative alpha based on its y position and /// the current scroll position. /// /// The item to be updated. diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs index 1cd8b065fc..9a01b46216 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs @@ -51,9 +51,7 @@ namespace osu.Game.Screens.Select.Carousel [Resolved] private IBindable ruleset { get; set; } = null!; - public IEnumerable DrawableBeatmaps => beatmapContainer?.IsLoaded != true ? Enumerable.Empty() : beatmapContainer.AliveChildren; - - private Container? beatmapContainer; + public Container? Beatmaps; private BeatmapSetInfo beatmapSet = null!; @@ -126,7 +124,7 @@ namespace osu.Game.Screens.Select.Carousel Content.Clear(); Header.Clear(); - beatmapContainer = null; + Beatmaps = null; beatmapsLoadTask = null; if (Item == null) @@ -164,7 +162,7 @@ namespace osu.Game.Screens.Select.Carousel // if we are already displaying all the correct beatmaps, only run animation updates. // note that the displayed beatmaps may change due to the applied filter. // a future optimisation could add/remove only changed difficulties rather than reinitialise. - if (beatmapContainer != null && visibleBeatmaps.Length == beatmapContainer.Count && visibleBeatmaps.All(b => beatmapContainer.Any(c => c.Item == b))) + if (Beatmaps != null && visibleBeatmaps.Length == Beatmaps.Count && visibleBeatmaps.All(b => Beatmaps.Any(c => c.Item == b))) { updateBeatmapYPositions(); } @@ -173,17 +171,17 @@ namespace osu.Game.Screens.Select.Carousel // on selection we show our child beatmaps. // for now this is a simple drawable construction each selection. // can be improved in the future. - beatmapContainer = new Container + Beatmaps = new Container { X = 100, RelativeSizeAxes = Axes.Both, ChildrenEnumerable = visibleBeatmaps.Select(c => c.CreateDrawableRepresentation()!) }; - beatmapsLoadTask = LoadComponentAsync(beatmapContainer, loaded => + beatmapsLoadTask = LoadComponentAsync(Beatmaps, loaded => { // make sure the pooled target hasn't changed. - if (beatmapContainer != loaded) + if (Beatmaps != loaded) return; Content.Child = loaded; @@ -244,7 +242,7 @@ namespace osu.Game.Screens.Select.Carousel private void updateBeatmapYPositions() { - if (beatmapContainer == null) + if (Beatmaps == null) return; if (beatmapsLoadTask == null || !beatmapsLoadTask.IsCompleted) @@ -254,7 +252,7 @@ namespace osu.Game.Screens.Select.Carousel bool isSelected = Item?.State.Value == CarouselItemState.Selected; - foreach (var panel in beatmapContainer) + foreach (var panel in Beatmaps) { Debug.Assert(panel.Item != null); From 97a51af5a06351d64944951963d82dba4df65c16 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 4 Sep 2024 14:52:52 +0900 Subject: [PATCH 2173/2493] Fix one more unnecessary enumerator allocation --- .../Overlays/NotificationOverlayToastTray.cs | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/NotificationOverlayToastTray.cs b/osu.Game/Overlays/NotificationOverlayToastTray.cs index d2899f29b8..df07b4f138 100644 --- a/osu.Game/Overlays/NotificationOverlayToastTray.cs +++ b/osu.Game/Overlays/NotificationOverlayToastTray.cs @@ -153,8 +153,22 @@ namespace osu.Game.Overlays { base.Update(); - float height = toastFlow.Count > 0 ? toastFlow.DrawHeight + 120 : 0; - float alpha = toastFlow.Count > 0 ? MathHelper.Clamp(toastFlow.DrawHeight / 41, 0, 1) * toastFlow.Children.Max(n => n.Alpha) : 0; + float height = 0; + float alpha = 0; + + if (toastFlow.Count > 0) + { + float maxNotificationAlpha = 0; + + foreach (var t in toastFlow) + { + if (t.Alpha > maxNotificationAlpha) + maxNotificationAlpha = t.Alpha; + } + + height = toastFlow.DrawHeight + 120; + alpha = MathHelper.Clamp(toastFlow.DrawHeight / 41, 0, 1) * maxNotificationAlpha; + } toastContentBackground.Height = (float)Interpolation.DampContinuously(toastContentBackground.Height, height, 10, Clock.ElapsedFrameTime); toastContentBackground.Alpha = (float)Interpolation.DampContinuously(toastContentBackground.Alpha, alpha, 10, Clock.ElapsedFrameTime); From dfe11dc68a4f381a3d6ec78d30462929dd3efe5c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 4 Sep 2024 15:25:36 +0900 Subject: [PATCH 2174/2493] Use `for` with exposed `IReadOnlyList` rather than making internal container public --- .../SongSelect/TestSceneBeatmapCarousel.cs | 4 ++-- osu.Game/Screens/Select/BeatmapCarousel.cs | 8 +++----- .../Carousel/DrawableCarouselBeatmapSet.cs | 18 ++++++++++-------- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs index 218a67e818..ec072a3dd2 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs @@ -1405,9 +1405,9 @@ namespace osu.Game.Tests.Visual.SongSelect yield return item; - if (item is DrawableCarouselBeatmapSet set && set.Beatmaps?.IsLoaded == true) + if (item is DrawableCarouselBeatmapSet set) { - foreach (var difficulty in set.Beatmaps) + foreach (var difficulty in set.DrawableBeatmaps) yield return difficulty; } } diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index f7e0eae4a5..a6a6a2f585 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -899,12 +899,10 @@ namespace osu.Game.Screens.Select Scroll.ChangeChildDepth(item, hasPassedSelection ? -item.Item.CarouselYPosition : item.Item.CarouselYPosition); } - if (item is DrawableCarouselBeatmapSet set && set.Beatmaps?.IsLoaded == true) + if (item is DrawableCarouselBeatmapSet set) { - foreach (var diff in set.Beatmaps) - { - updateItem(diff, item); - } + for (int i = 0; i < set.DrawableBeatmaps.Count; i++) + updateItem(set.DrawableBeatmaps[i], item); } } } diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs index 9a01b46216..eba40994e2 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs @@ -51,7 +51,9 @@ namespace osu.Game.Screens.Select.Carousel [Resolved] private IBindable ruleset { get; set; } = null!; - public Container? Beatmaps; + public IReadOnlyList DrawableBeatmaps => beatmapContainer?.IsLoaded != true ? Array.Empty() : beatmapContainer; + + private Container? beatmapContainer; private BeatmapSetInfo beatmapSet = null!; @@ -124,7 +126,7 @@ namespace osu.Game.Screens.Select.Carousel Content.Clear(); Header.Clear(); - Beatmaps = null; + beatmapContainer = null; beatmapsLoadTask = null; if (Item == null) @@ -162,7 +164,7 @@ namespace osu.Game.Screens.Select.Carousel // if we are already displaying all the correct beatmaps, only run animation updates. // note that the displayed beatmaps may change due to the applied filter. // a future optimisation could add/remove only changed difficulties rather than reinitialise. - if (Beatmaps != null && visibleBeatmaps.Length == Beatmaps.Count && visibleBeatmaps.All(b => Beatmaps.Any(c => c.Item == b))) + if (beatmapContainer != null && visibleBeatmaps.Length == beatmapContainer.Count && visibleBeatmaps.All(b => beatmapContainer.Any(c => c.Item == b))) { updateBeatmapYPositions(); } @@ -171,17 +173,17 @@ namespace osu.Game.Screens.Select.Carousel // on selection we show our child beatmaps. // for now this is a simple drawable construction each selection. // can be improved in the future. - Beatmaps = new Container + beatmapContainer = new Container { X = 100, RelativeSizeAxes = Axes.Both, ChildrenEnumerable = visibleBeatmaps.Select(c => c.CreateDrawableRepresentation()!) }; - beatmapsLoadTask = LoadComponentAsync(Beatmaps, loaded => + beatmapsLoadTask = LoadComponentAsync(beatmapContainer, loaded => { // make sure the pooled target hasn't changed. - if (Beatmaps != loaded) + if (beatmapContainer != loaded) return; Content.Child = loaded; @@ -242,7 +244,7 @@ namespace osu.Game.Screens.Select.Carousel private void updateBeatmapYPositions() { - if (Beatmaps == null) + if (beatmapContainer == null) return; if (beatmapsLoadTask == null || !beatmapsLoadTask.IsCompleted) @@ -252,7 +254,7 @@ namespace osu.Game.Screens.Select.Carousel bool isSelected = Item?.State.Value == CarouselItemState.Selected; - foreach (var panel in Beatmaps) + foreach (var panel in beatmapContainer) { Debug.Assert(panel.Item != null); From e564e8c04895336559a16b093cbc905de8162b9c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 4 Sep 2024 16:08:18 +0900 Subject: [PATCH 2175/2493] Add todo about fixing stutter on update application --- osu.Desktop/Updater/VelopackUpdateManager.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Desktop/Updater/VelopackUpdateManager.cs b/osu.Desktop/Updater/VelopackUpdateManager.cs index 5b4d281f80..527892413a 100644 --- a/osu.Desktop/Updater/VelopackUpdateManager.cs +++ b/osu.Desktop/Updater/VelopackUpdateManager.cs @@ -121,6 +121,8 @@ namespace osu.Desktop.Updater private bool restartToApplyUpdate() { + // TODO: Migrate this to async flow whenever available (see https://github.com/ppy/osu/pull/28743#discussion_r1740505665). + // Currently there's an internal Thread.Sleep(300) which will cause a stutter when the user clicks to restart. updateManager.WaitExitThenApplyUpdates(pendingUpdate?.TargetFullRelease); Schedule(() => game.AttemptExit()); return true; From c89597b060cae084899db82c3b6c5040a17f794a Mon Sep 17 00:00:00 2001 From: Sheppsu Date: Wed, 4 Sep 2024 03:37:52 -0400 Subject: [PATCH 2176/2493] fix config mistake --- .../Configuration/OsuRulesetConfigManager.cs | 11 +++++++++++ osu.Game.Rulesets.Osu/OsuRuleset.cs | 3 --- osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs | 2 ++ .../UI/OsuAnalysisContainer.cs | 13 ++++++------- osu.Game.Rulesets.Osu/UI/OsuAnalysisSettings.cs | 17 ++++++++++++----- osu.Game/Configuration/OsuConfigManager.cs | 10 ---------- osu.Game/Rulesets/Ruleset.cs | 3 --- osu.Game/Rulesets/UI/AnalysisContainer.cs | 10 +++++----- osu.Game/Rulesets/UI/DrawableRuleset.cs | 2 ++ .../Play/PlayerSettings/AnalysisSettings.cs | 7 ++++++- osu.Game/Screens/Play/ReplayPlayer.cs | 2 +- 11 files changed, 45 insertions(+), 35 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Configuration/OsuRulesetConfigManager.cs b/osu.Game.Rulesets.Osu/Configuration/OsuRulesetConfigManager.cs index 2056a50eda..23b7b9c1fa 100644 --- a/osu.Game.Rulesets.Osu/Configuration/OsuRulesetConfigManager.cs +++ b/osu.Game.Rulesets.Osu/Configuration/OsuRulesetConfigManager.cs @@ -24,6 +24,11 @@ namespace osu.Game.Rulesets.Osu.Configuration SetDefault(OsuRulesetSetting.ShowCursorTrail, true); SetDefault(OsuRulesetSetting.ShowCursorRipples, false); SetDefault(OsuRulesetSetting.PlayfieldBorderStyle, PlayfieldBorderStyle.None); + + SetDefault(OsuRulesetSetting.ReplayHitMarkersEnabled, false); + SetDefault(OsuRulesetSetting.ReplayAimMarkersEnabled, false); + SetDefault(OsuRulesetSetting.ReplayAimLinesEnabled, false); + SetDefault(OsuRulesetSetting.ReplayCursorHideEnabled, false); } } @@ -34,5 +39,11 @@ namespace osu.Game.Rulesets.Osu.Configuration ShowCursorTrail, ShowCursorRipples, PlayfieldBorderStyle, + + // Replay + ReplayHitMarkersEnabled, + ReplayAimMarkersEnabled, + ReplayAimLinesEnabled, + ReplayCursorHideEnabled, } } diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index a8a1d98bf3..be48ef9acc 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -13,7 +13,6 @@ using osu.Game.Beatmaps.Legacy; using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Overlays.Settings; -using osu.Game.Replays; using osu.Game.Rulesets.Configuration; using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Edit; @@ -361,8 +360,6 @@ namespace osu.Game.Rulesets.Osu return adjustedDifficulty; } - public override OsuAnalysisContainer CreateAnalysisContainer(Replay replay, Playfield playfield) => new OsuAnalysisContainer(replay, playfield); - public override bool EditorShowScrollSpeed => false; } } diff --git a/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs b/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs index f0390ad716..3c6456957b 100644 --- a/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs @@ -68,5 +68,7 @@ namespace osu.Game.Rulesets.Osu.UI return 0; } } + + public override AnalysisContainer CreateAnalysisContainer(Replay replay) => new OsuAnalysisContainer(replay, this); } } diff --git a/osu.Game.Rulesets.Osu/UI/OsuAnalysisContainer.cs b/osu.Game.Rulesets.Osu/UI/OsuAnalysisContainer.cs index 4eff147772..57401edece 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuAnalysisContainer.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuAnalysisContainer.cs @@ -22,14 +22,14 @@ namespace osu.Game.Rulesets.Osu.UI { public new OsuAnalysisSettings AnalysisSettings => (OsuAnalysisSettings)base.AnalysisSettings; - protected new OsuPlayfield Playfield => (OsuPlayfield)base.Playfield; + protected new DrawableOsuRuleset DrawableRuleset => (DrawableOsuRuleset)base.DrawableRuleset; protected HitMarkersContainer HitMarkers; protected AimMarkersContainer AimMarkers; protected AimLinesContainer AimLines; - public OsuAnalysisContainer(Replay replay, Playfield playfield) - : base(replay, playfield) + public OsuAnalysisContainer(Replay replay, DrawableRuleset drawableRuleset) + : base(replay, drawableRuleset) { InternalChildren = new Drawable[] { @@ -37,12 +37,11 @@ namespace osu.Game.Rulesets.Osu.UI HitMarkers = new HitMarkersContainer(), AimMarkers = new AimMarkersContainer { Depth = float.MinValue } }; - } - protected override OsuAnalysisSettings CreateAnalysisSettings() + protected override OsuAnalysisSettings CreateAnalysisSettings(Ruleset ruleset) { - var settings = new OsuAnalysisSettings(); + var settings = new OsuAnalysisSettings((OsuRuleset)ruleset); settings.HitMarkersEnabled.ValueChanged += e => toggleHitMarkers(e.NewValue); settings.AimMarkersEnabled.ValueChanged += e => toggleAimMarkers(e.NewValue); settings.AimLinesEnabled.ValueChanged += e => toggleAimLines(e.NewValue); @@ -67,7 +66,7 @@ namespace osu.Game.Rulesets.Osu.UI private void toggleAimLines(bool value) => AimLines.FadeTo(value ? 1 : 0); - private void toggleCursorHidden(bool value) => Playfield.Cursor.FadeTo(value ? 0 : 1); + private void toggleCursorHidden(bool value) => DrawableRuleset.Playfield.Cursor.FadeTo(value ? 0 : 1); protected void LoadReplay() { diff --git a/osu.Game.Rulesets.Osu/UI/OsuAnalysisSettings.cs b/osu.Game.Rulesets.Osu/UI/OsuAnalysisSettings.cs index ae81b2c0b8..6e11c87c3a 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuAnalysisSettings.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuAnalysisSettings.cs @@ -4,12 +4,18 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Game.Configuration; +using osu.Game.Rulesets.Osu.Configuration; using osu.Game.Screens.Play.PlayerSettings; namespace osu.Game.Rulesets.Osu.UI { public partial class OsuAnalysisSettings : AnalysisSettings { + public OsuAnalysisSettings(Ruleset ruleset) + : base(ruleset) + { + } + [SettingSource("Hit markers", SettingControlType = typeof(PlayerCheckbox))] public BindableBool HitMarkersEnabled { get; } = new BindableBool(); @@ -23,12 +29,13 @@ namespace osu.Game.Rulesets.Osu.UI public BindableBool CursorHideEnabled { get; } = new BindableBool(); [BackgroundDependencyLoader] - private void load(OsuConfigManager config) + private void load(IRulesetConfigCache cache) { - config.BindWith(OsuSetting.ReplayHitMarkersEnabled, HitMarkersEnabled); - config.BindWith(OsuSetting.ReplayAimMarkersEnabled, AimMarkersEnabled); - config.BindWith(OsuSetting.ReplayAimLinesEnabled, AimLinesEnabled); - config.BindWith(OsuSetting.ReplayCursorHideEnabled, CursorHideEnabled); + var config = (OsuRulesetConfigManager)cache.GetConfigFor(Ruleset)!; + config.BindWith(OsuRulesetSetting.ReplayHitMarkersEnabled, HitMarkersEnabled); + config.BindWith(OsuRulesetSetting.ReplayAimMarkersEnabled, AimMarkersEnabled); + config.BindWith(OsuRulesetSetting.ReplayAimLinesEnabled, AimLinesEnabled); + config.BindWith(OsuRulesetSetting.ReplayCursorHideEnabled, CursorHideEnabled); } } } diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index 8b75c9c934..8d6c244b35 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -154,12 +154,6 @@ namespace osu.Game.Configuration SetDefault(OsuSetting.IncreaseFirstObjectVisibility, true); SetDefault(OsuSetting.GameplayDisableWinKey, true); - // Replay - SetDefault(OsuSetting.ReplayHitMarkersEnabled, false); - SetDefault(OsuSetting.ReplayAimMarkersEnabled, false); - SetDefault(OsuSetting.ReplayAimLinesEnabled, false); - SetDefault(OsuSetting.ReplayCursorHideEnabled, false); - // Update SetDefault(OsuSetting.ReleaseStream, ReleaseStream.Lazer); @@ -419,10 +413,6 @@ namespace osu.Game.Configuration EditorShowHitMarkers, EditorAutoSeekOnPlacement, DiscordRichPresence, - ReplayHitMarkersEnabled, - ReplayAimMarkersEnabled, - ReplayAimLinesEnabled, - ReplayCursorHideEnabled, ShowOnlineExplicitContent, LastProcessedMetadataId, diff --git a/osu.Game/Rulesets/Ruleset.cs b/osu.Game/Rulesets/Ruleset.cs index fdf43c2f09..2e48b8e16f 100644 --- a/osu.Game/Rulesets/Ruleset.cs +++ b/osu.Game/Rulesets/Ruleset.cs @@ -17,7 +17,6 @@ using osu.Game.Beatmaps.Legacy; using osu.Game.Configuration; using osu.Game.Extensions; using osu.Game.Overlays.Settings; -using osu.Game.Replays; using osu.Game.Rulesets.Configuration; using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Edit; @@ -409,7 +408,5 @@ namespace osu.Game.Rulesets public virtual bool EditorShowScrollSpeed => true; public virtual DifficultySection? CreateEditorDifficultySection() => null; - - public virtual AnalysisContainer? CreateAnalysisContainer(Replay replay, Playfield playfield) => null; } } diff --git a/osu.Game/Rulesets/UI/AnalysisContainer.cs b/osu.Game/Rulesets/UI/AnalysisContainer.cs index 69a71cf06e..b6c2a8c1c8 100644 --- a/osu.Game/Rulesets/UI/AnalysisContainer.cs +++ b/osu.Game/Rulesets/UI/AnalysisContainer.cs @@ -10,18 +10,18 @@ namespace osu.Game.Rulesets.UI public abstract partial class AnalysisContainer : Container { protected Replay Replay; - protected Playfield Playfield; + protected DrawableRuleset DrawableRuleset; public AnalysisSettings AnalysisSettings; - public AnalysisContainer(Replay replay, Playfield playfield) + protected AnalysisContainer(Replay replay, DrawableRuleset drawableRuleset) { Replay = replay; - Playfield = playfield; + DrawableRuleset = drawableRuleset; - AnalysisSettings = CreateAnalysisSettings(); + AnalysisSettings = CreateAnalysisSettings(drawableRuleset.Ruleset); } - protected abstract AnalysisSettings CreateAnalysisSettings(); + protected abstract AnalysisSettings CreateAnalysisSettings(Ruleset ruleset); } } diff --git a/osu.Game/Rulesets/UI/DrawableRuleset.cs b/osu.Game/Rulesets/UI/DrawableRuleset.cs index a28b2716cb..5b1f59d549 100644 --- a/osu.Game/Rulesets/UI/DrawableRuleset.cs +++ b/osu.Game/Rulesets/UI/DrawableRuleset.cs @@ -596,6 +596,8 @@ namespace osu.Game.Rulesets.UI /// Invoked when the user requests to pause while the resume overlay is active. /// public abstract void CancelResume(); + + public virtual AnalysisContainer CreateAnalysisContainer(Replay replay) => null; } public class BeatmapInvalidForRulesetException : ArgumentException diff --git a/osu.Game/Screens/Play/PlayerSettings/AnalysisSettings.cs b/osu.Game/Screens/Play/PlayerSettings/AnalysisSettings.cs index e1f77cef12..4c64eef92f 100644 --- a/osu.Game/Screens/Play/PlayerSettings/AnalysisSettings.cs +++ b/osu.Game/Screens/Play/PlayerSettings/AnalysisSettings.cs @@ -2,14 +2,19 @@ // See the LICENCE file in the repository root for full licence text. using osu.Game.Configuration; +using osu.Game.Rulesets; namespace osu.Game.Screens.Play.PlayerSettings { public partial class AnalysisSettings : PlayerSettingsGroup { - public AnalysisSettings() + protected Ruleset Ruleset; + + public AnalysisSettings(Ruleset ruleset) : base("Analysis Settings") { + Ruleset = ruleset; + AddRange(this.CreateSettingsControls()); } } diff --git a/osu.Game/Screens/Play/ReplayPlayer.cs b/osu.Game/Screens/Play/ReplayPlayer.cs index af9568c08c..82a2f09250 100644 --- a/osu.Game/Screens/Play/ReplayPlayer.cs +++ b/osu.Game/Screens/Play/ReplayPlayer.cs @@ -72,7 +72,7 @@ namespace osu.Game.Screens.Play HUDOverlay.PlayerSettingsOverlay.AddAtStart(playbackSettings); - var analysisContainer = DrawableRuleset.Ruleset.CreateAnalysisContainer(GameplayState.Score.Replay, DrawableRuleset.Playfield); + var analysisContainer = DrawableRuleset.CreateAnalysisContainer(GameplayState.Score.Replay); if (analysisContainer != null) { From 59ff8c498417f97760b83ccf55fdfb65fd454428 Mon Sep 17 00:00:00 2001 From: Sheppsu Date: Wed, 4 Sep 2024 03:38:13 -0400 Subject: [PATCH 2177/2493] fix analysis container creation --- .../TestSceneOsuAnalysisContainer.cs | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuAnalysisContainer.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuAnalysisContainer.cs index a173256557..0fc91513e6 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuAnalysisContainer.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuAnalysisContainer.cs @@ -8,11 +8,12 @@ using System.Collections.Generic; using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; -using osu.Framework.Threading; using osu.Game.Replays; +using osu.Game.Rulesets.Osu.Beatmaps; using osu.Game.Rulesets.Osu.Replays; using osu.Game.Rulesets.Osu.UI; using osu.Game.Rulesets.Replays; +using osu.Game.Rulesets.UI; using osu.Game.Tests.Visual; using osuTK; @@ -31,7 +32,6 @@ namespace osu.Game.Rulesets.Osu.Tests [Test] public void TestHitMarkers() { - var loop = createAnalysisContainer(); AddStep("enable hit markers", () => analysisContainer.AnalysisSettings.HitMarkersEnabled.Value = true); AddAssert("hit markers visible", () => analysisContainer.HitMarkersVisible); AddStep("disable hit markers", () => analysisContainer.AnalysisSettings.HitMarkersEnabled.Value = false); @@ -41,7 +41,6 @@ namespace osu.Game.Rulesets.Osu.Tests [Test] public void TestAimMarker() { - var loop = createAnalysisContainer(); AddStep("enable aim markers", () => analysisContainer.AnalysisSettings.AimMarkersEnabled.Value = true); AddAssert("aim markers visible", () => analysisContainer.AimMarkersVisible); AddStep("disable aim markers", () => analysisContainer.AnalysisSettings.AimMarkersEnabled.Value = false); @@ -51,19 +50,28 @@ namespace osu.Game.Rulesets.Osu.Tests [Test] public void TestAimLines() { - var loop = createAnalysisContainer(); AddStep("enable aim lines", () => analysisContainer.AnalysisSettings.AimLinesEnabled.Value = true); AddAssert("aim lines visible", () => analysisContainer.AimLinesVisible); AddStep("disable aim lines", () => analysisContainer.AnalysisSettings.AimLinesEnabled.Value = false); AddAssert("aim lines not visible", () => !analysisContainer.AimLinesVisible); } - private TestOsuAnalysisContainer createAnalysisContainer() => new TestOsuAnalysisContainer(); + private TestOsuAnalysisContainer createAnalysisContainer() + { + var replay = new Replay(); + var ruleset = new OsuRuleset(); + var beatmap = new OsuBeatmap(); + var drawableRuleset = new DrawableOsuRuleset(ruleset, beatmap); + // Load playfield cursor to avoid errors + Add(drawableRuleset); + + return new TestOsuAnalysisContainer(replay, drawableRuleset); + } private partial class TestOsuAnalysisContainer : OsuAnalysisContainer { - public TestOsuAnalysisContainer() - : base(new Replay(), new OsuPlayfield()) + public TestOsuAnalysisContainer(Replay replay, DrawableRuleset drawableRuleset) + : base(replay, drawableRuleset) { } From 86309f4b4605b7ac82853006acf08bec8cb2a84f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 4 Sep 2024 17:25:36 +0900 Subject: [PATCH 2178/2493] Revert "Woopsie! I accidentally added one too many semi-colons, so I moved it here into the commit instead ;" This reverts commit 582ffcfc9722342f73d83778fc950cacc1bb9439. Revert "Mod customisation header's color is now based on the state of the panel rather than the hover of the container." This reverts commit e3457d850157808928b7ef912b8ecc562826f5a2. --- .../Overlays/Mods/ModCustomisationHeader.cs | 28 +++++-------------- 1 file changed, 7 insertions(+), 21 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModCustomisationHeader.cs b/osu.Game/Overlays/Mods/ModCustomisationHeader.cs index da77d8dac8..32fd5a37aa 100644 --- a/osu.Game/Overlays/Mods/ModCustomisationHeader.cs +++ b/osu.Game/Overlays/Mods/ModCustomisationHeader.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 osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; @@ -19,7 +20,7 @@ using static osu.Game.Overlays.Mods.ModCustomisationPanel; namespace osu.Game.Overlays.Mods { - public partial class ModCustomisationHeader : OsuClickableContainer + public partial class ModCustomisationHeader : OsuHoverContainer { private Box background = null!; private Box backgroundFlash = null!; @@ -28,6 +29,8 @@ namespace osu.Game.Overlays.Mods [Resolved] private OverlayColourProvider colourProvider { get; set; } = null!; + protected override IEnumerable EffectTargets => new[] { background }; + public readonly Bindable ExpandedState = new Bindable(ModCustomisationPanelState.Collapsed); private readonly ModCustomisationPanel panel; @@ -49,7 +52,6 @@ namespace osu.Game.Overlays.Mods background = new Box { RelativeSizeAxes = Axes.Both, - Colour = colourProvider.Dark3, }, backgroundFlash = new Box { @@ -82,6 +84,9 @@ namespace osu.Game.Overlays.Mods } } }; + + IdleColour = colourProvider.Dark3; + HoverColour = colourProvider.Light4; } protected override void LoadComplete() @@ -105,20 +110,6 @@ namespace osu.Game.Overlays.Mods { icon.ScaleTo(v.NewValue > ModCustomisationPanelState.Collapsed ? new Vector2(1, -1) : Vector2.One, 300, Easing.OutQuint); }, true); - - panel.ExpandedState.BindValueChanged(v => - { - switch (v.NewValue) - { - case ModCustomisationPanelState.Expanded: - case ModCustomisationPanelState.ExpandedByMod: - fadeBackgroundColor(colourProvider.Light4); - break; - default: - fadeBackgroundColor(colourProvider.Dark3); - break; - } - }, false); } protected override bool OnHover(HoverEvent e) @@ -128,10 +119,5 @@ namespace osu.Game.Overlays.Mods return base.OnHover(e); } - - private void fadeBackgroundColor(Color4 color) - { - background.FadeColour(color, 500, Easing.OutQuint); - } } } From 7cd24ba58ecedd2971e4f06b637e1dab4bdeb00d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 4 Sep 2024 18:00:07 +0900 Subject: [PATCH 2179/2493] Disallow mistimed firing of beat sync for break overlay for now It doesn't work well with pause/resume. --- osu.Game/Screens/Play/BreakOverlay.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/Screens/Play/BreakOverlay.cs b/osu.Game/Screens/Play/BreakOverlay.cs index 4ed8b69a77..1fdb9402bc 100644 --- a/osu.Game/Screens/Play/BreakOverlay.cs +++ b/osu.Game/Screens/Play/BreakOverlay.cs @@ -53,6 +53,10 @@ namespace osu.Game.Screens.Play MinimumBeatLength = 200; + // Doesn't play well with pause/unpause. + // This might mean that some beats don't animate if the user is running <60fps, but we'll deal with that if anyone notices. + AllowMistimedEventFiring = false; + Child = fadeContainer = new Container { Alpha = 0, From 045096b08ac771c649809546a53290d01d46857b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 4 Sep 2024 18:00:48 +0900 Subject: [PATCH 2180/2493] 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 2609fd42c3..5f3dd2f6f4 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -10,7 +10,7 @@ true - + diff --git a/osu.iOS.props b/osu.iOS.props index 1056f4b441..9d9b42a163 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -17,6 +17,6 @@ -all - + From cb9d1d49a225cbf58c253ea87ae600f9cb0716c2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 4 Sep 2024 18:01:06 +0900 Subject: [PATCH 2181/2493] 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 6952de2fa5..b2bf8c7eb9 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -36,7 +36,7 @@ - + From 3531f646f232eb21ed14b27f31a43884e425d547 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 18 Jul 2024 09:46:06 +0200 Subject: [PATCH 2182/2493] Refactor `DrawableOsuMenuItem` to remove a hack --- .../UserInterface/DrawableOsuMenuItem.cs | 41 +++++++++++-------- .../UserInterface/DrawableStatefulMenuItem.cs | 10 +---- 2 files changed, 25 insertions(+), 26 deletions(-) diff --git a/osu.Game/Graphics/UserInterface/DrawableOsuMenuItem.cs b/osu.Game/Graphics/UserInterface/DrawableOsuMenuItem.cs index 06ef75cf58..703dcbf3b7 100644 --- a/osu.Game/Graphics/UserInterface/DrawableOsuMenuItem.cs +++ b/osu.Game/Graphics/UserInterface/DrawableOsuMenuItem.cs @@ -124,7 +124,7 @@ namespace osu.Game.Graphics.UserInterface protected sealed override Drawable CreateContent() => text = CreateTextContainer(); protected virtual TextContainer CreateTextContainer() => new TextContainer(); - protected partial class TextContainer : Container, IHasText + protected partial class TextContainer : FillFlowContainer, IHasText { public LocalisableString Text { @@ -145,25 +145,32 @@ namespace osu.Game.Graphics.UserInterface Origin = Anchor.CentreLeft; AutoSizeAxes = Axes.Both; + Spacing = new Vector2(10); + Direction = FillDirection.Horizontal; - Children = new Drawable[] + Child = new Container { - NormalText = new OsuSpriteText + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + AutoSizeAxes = Axes.Both, + Margin = new MarginPadding { Horizontal = MARGIN_HORIZONTAL, Vertical = MARGIN_VERTICAL }, + Children = new Drawable[] { - AlwaysPresent = true, // ensures that the menu item does not change width when switching between normal and bold text. - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Font = OsuFont.GetFont(size: text_size), - Margin = new MarginPadding { Horizontal = MARGIN_HORIZONTAL, Vertical = MARGIN_VERTICAL }, - }, - BoldText = new OsuSpriteText - { - AlwaysPresent = true, // ensures that the menu item does not change width when switching between normal and bold text. - Alpha = 0, - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Font = OsuFont.GetFont(size: text_size, weight: FontWeight.Bold), - Margin = new MarginPadding { Horizontal = MARGIN_HORIZONTAL, Vertical = MARGIN_VERTICAL }, + NormalText = new OsuSpriteText + { + AlwaysPresent = true, // ensures that the menu item does not change width when switching between normal and bold text. + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Font = OsuFont.GetFont(size: text_size), + }, + BoldText = new OsuSpriteText + { + AlwaysPresent = true, // ensures that the menu item does not change width when switching between normal and bold text. + Alpha = 0, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Font = OsuFont.GetFont(size: text_size, weight: FontWeight.Bold), + } } }; } diff --git a/osu.Game/Graphics/UserInterface/DrawableStatefulMenuItem.cs b/osu.Game/Graphics/UserInterface/DrawableStatefulMenuItem.cs index b9e81e1bf2..6888c2c71b 100644 --- a/osu.Game/Graphics/UserInterface/DrawableStatefulMenuItem.cs +++ b/osu.Game/Graphics/UserInterface/DrawableStatefulMenuItem.cs @@ -51,7 +51,7 @@ namespace osu.Game.Graphics.UserInterface Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, Size = new Vector2(10), - Margin = new MarginPadding { Horizontal = MARGIN_HORIZONTAL }, + Margin = new MarginPadding { Left = -MARGIN_HORIZONTAL, Right = MARGIN_HORIZONTAL }, AlwaysPresent = true, }); } @@ -62,14 +62,6 @@ namespace osu.Game.Graphics.UserInterface state.BindValueChanged(updateState, true); } - protected override void Update() - { - base.Update(); - - // Todo: This is bad. This can maybe be done better with a refactor of DrawableOsuMenuItem. - stateIcon.X = BoldText.DrawWidth + 10; - } - private void updateState(ValueChangedEvent state) { var icon = menuItem.GetIconForState(state.NewValue); From 3c6c49187a30dbba31bd1f705d5e6c9aeab0a1c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 18 Jul 2024 10:21:04 +0200 Subject: [PATCH 2183/2493] Implement component for displaying hotkeys --- .../UserInterface/TestSceneHotkeyDisplay.cs | 28 +++++ osu.Game/Graphics/UserInterface/Hotkey.cs | 53 +++++++++ .../Graphics/UserInterface/HotkeyDisplay.cs | 110 ++++++++++++++++++ osu.Game/OsuGameBase.cs | 1 + 4 files changed, 192 insertions(+) create mode 100644 osu.Game.Tests/Visual/UserInterface/TestSceneHotkeyDisplay.cs create mode 100644 osu.Game/Graphics/UserInterface/Hotkey.cs create mode 100644 osu.Game/Graphics/UserInterface/HotkeyDisplay.cs diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneHotkeyDisplay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneHotkeyDisplay.cs new file mode 100644 index 0000000000..1c2c94dbf1 --- /dev/null +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneHotkeyDisplay.cs @@ -0,0 +1,28 @@ +// 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.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Input; +using osu.Framework.Input.Bindings; +using osu.Game.Graphics.UserInterface; +using osu.Game.Input.Bindings; + +namespace osu.Game.Tests.Visual.UserInterface +{ + public partial class TestSceneHotkeyDisplay : ThemeComparisonTestScene + { + protected override Drawable CreateContent() => new FillFlowContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Direction = FillDirection.Vertical, + Children = new[] + { + new HotkeyDisplay { Hotkey = new Hotkey(new KeyCombination(InputKey.MouseLeft)) }, + new HotkeyDisplay { Hotkey = new Hotkey(GlobalAction.EditorDecreaseDistanceSpacing) }, + new HotkeyDisplay { Hotkey = new Hotkey(PlatformAction.Save) }, + } + }; + } +} diff --git a/osu.Game/Graphics/UserInterface/Hotkey.cs b/osu.Game/Graphics/UserInterface/Hotkey.cs new file mode 100644 index 0000000000..811d385466 --- /dev/null +++ b/osu.Game/Graphics/UserInterface/Hotkey.cs @@ -0,0 +1,53 @@ +// 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.Input; +using osu.Framework.Input.Bindings; +using osu.Framework.Platform; +using osu.Game.Input; +using osu.Game.Input.Bindings; + +namespace osu.Game.Graphics.UserInterface +{ + public struct Hotkey + { + public KeyCombination[]? KeyCombinations { get; } + public GlobalAction? GlobalAction { get; } + public PlatformAction? PlatformAction { get; } + + public Hotkey(params KeyCombination[] keyCombinations) + { + KeyCombinations = keyCombinations; + } + + public Hotkey(GlobalAction globalAction) + { + GlobalAction = globalAction; + } + + public Hotkey(PlatformAction platformAction) + { + PlatformAction = platformAction; + } + + public IEnumerable ResolveKeyCombination(ReadableKeyCombinationProvider keyCombinationProvider, RealmKeyBindingStore keyBindingStore, GameHost gameHost) + { + if (KeyCombinations != null) + return KeyCombinations.Select(keyCombinationProvider.GetReadableString); + + if (GlobalAction != null) + return keyBindingStore.GetReadableKeyCombinationsFor(GlobalAction.Value); + + if (PlatformAction != null) + { + var action = PlatformAction.Value; + var bindings = gameHost.PlatformKeyBindings.Where(kb => (PlatformAction)kb.Action == action); + return bindings.Select(b => keyCombinationProvider.GetReadableString(b.KeyCombination)); + } + + return Enumerable.Empty(); + } + } +} diff --git a/osu.Game/Graphics/UserInterface/HotkeyDisplay.cs b/osu.Game/Graphics/UserInterface/HotkeyDisplay.cs new file mode 100644 index 0000000000..63970249d1 --- /dev/null +++ b/osu.Game/Graphics/UserInterface/HotkeyDisplay.cs @@ -0,0 +1,110 @@ +// 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.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Input; +using osu.Framework.Platform; +using osu.Game.Graphics.Sprites; +using osu.Game.Input; +using osu.Game.Overlays; +using osuTK; + +namespace osu.Game.Graphics.UserInterface +{ + public partial class HotkeyDisplay : CompositeDrawable + { + private Hotkey hotkey; + + public Hotkey Hotkey + { + get => hotkey; + set + { + if (EqualityComparer.Default.Equals(hotkey, value)) + return; + + hotkey = value; + + if (IsLoaded) + updateState(); + } + } + + private FillFlowContainer flow = null!; + + [Resolved] + private ReadableKeyCombinationProvider readableKeyCombinationProvider { get; set; } = null!; + + [Resolved] + private RealmKeyBindingStore realmKeyBindingStore { get; set; } = null!; + + [Resolved] + private GameHost gameHost { get; set; } = null!; + + [BackgroundDependencyLoader] + private void load() + { + AutoSizeAxes = Axes.Both; + + InternalChild = flow = new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(5) + }; + + updateState(); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + updateState(); + } + + private void updateState() + { + flow.Clear(); + foreach (string h in hotkey.ResolveKeyCombination(readableKeyCombinationProvider, realmKeyBindingStore, gameHost)) + flow.Add(new HotkeyBox(h)); + } + + private partial class HotkeyBox : CompositeDrawable + { + private readonly string hotkey; + + public HotkeyBox(string hotkey) + { + this.hotkey = hotkey; + } + + [BackgroundDependencyLoader] + private void load(OverlayColourProvider? colourProvider, OsuColour colours) + { + AutoSizeAxes = Axes.Both; + Masking = true; + CornerRadius = 3; + + InternalChildren = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider?.Background6 ?? Colour4.Black.Opacity(0.7f), + }, + new OsuSpriteText + { + Margin = new MarginPadding { Horizontal = 5, Bottom = 1, }, + Text = hotkey.ToUpperInvariant(), + Font = OsuFont.Default.With(size: 12, weight: FontWeight.Bold), + Colour = colourProvider?.Light1 ?? colours.GrayA, + } + }; + } + } + } +} diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index ce0c288934..e317fac25d 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -409,6 +409,7 @@ namespace osu.Game KeyBindingStore = new RealmKeyBindingStore(realm, keyCombinationProvider); KeyBindingStore.Register(globalBindings, RulesetStore.AvailableRulesets); + dependencies.Cache(KeyBindingStore); dependencies.Cache(globalBindings); From 3acc5fe5a0f10d24228579f393202dae42a6c1e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 18 Jul 2024 11:20:22 +0200 Subject: [PATCH 2184/2493] Integrate hotkey display into drawable menu items --- .../UserInterface/DrawableOsuMenuItem.cs | 117 +++++++++++------- .../UserInterface/DrawableStatefulMenuItem.cs | 7 +- osu.Game/Graphics/UserInterface/OsuMenu.cs | 2 +- .../Graphics/UserInterface/OsuMenuItem.cs | 2 + .../Edit/Components/Menus/EditorMenuBar.cs | 1 + 5 files changed, 82 insertions(+), 47 deletions(-) diff --git a/osu.Game/Graphics/UserInterface/DrawableOsuMenuItem.cs b/osu.Game/Graphics/UserInterface/DrawableOsuMenuItem.cs index 703dcbf3b7..3ecda50537 100644 --- a/osu.Game/Graphics/UserInterface/DrawableOsuMenuItem.cs +++ b/osu.Game/Graphics/UserInterface/DrawableOsuMenuItem.cs @@ -3,6 +3,7 @@ #nullable disable +using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; @@ -20,12 +21,13 @@ namespace osu.Game.Graphics.UserInterface { public partial class DrawableOsuMenuItem : Menu.DrawableMenuItem { - public const int MARGIN_HORIZONTAL = 17; + public const int MARGIN_HORIZONTAL = 10; public const int MARGIN_VERTICAL = 4; private const int text_size = 17; private const int transition_length = 80; - private TextContainer text; + protected TextContainer Text { get; private set; } + private HotkeyDisplay hotkey; private HoverClickSounds hoverClickSounds; public DrawableOsuMenuItem(MenuItem item) @@ -39,32 +41,33 @@ namespace osu.Game.Graphics.UserInterface BackgroundColour = Color4.Transparent; BackgroundColourHover = Color4Extensions.FromHex(@"172023"); + AddInternal(hotkey = new HotkeyDisplay + { + Alpha = 0, + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + Margin = new MarginPadding { Right = 10, Top = 1 }, + }); AddInternal(hoverClickSounds = new HoverClickSounds()); - updateTextColour(); + updateText(); - bool hasSubmenu = Item.Items.Any(); - - // Only add right chevron if direction of menu items is vertical (i.e. width is relative size, see `DrawableMenuItem.SetFlowDirection()`). - if (hasSubmenu && RelativeSizeAxes == Axes.X) + if (showChevron) { AddInternal(new SpriteIcon { - Margin = new MarginPadding(6), + Margin = new MarginPadding { Horizontal = 10, }, Size = new Vector2(8), Icon = FontAwesome.Solid.ChevronRight, Anchor = Anchor.CentreRight, Origin = Anchor.CentreRight, }); - - text.Padding = new MarginPadding - { - // Add some padding for the chevron above. - Right = 5, - }; } } + // Only add right chevron if direction of menu items is vertical (i.e. width is relative size, see `DrawableMenuItem.SetFlowDirection()`). + private bool showChevron => Item.Items.Any() && RelativeSizeAxes == Axes.X; + protected override void LoadComplete() { base.LoadComplete(); @@ -73,23 +76,39 @@ namespace osu.Game.Graphics.UserInterface FinishTransforms(); } - private void updateTextColour() + private void updateText() { - switch ((Item as OsuMenuItem)?.Type) + var osuMenuItem = Item as OsuMenuItem; + + switch (osuMenuItem?.Type) { default: case MenuItemType.Standard: - text.Colour = Color4.White; + Text.Colour = Color4.White; break; case MenuItemType.Destructive: - text.Colour = Color4.Red; + Text.Colour = Color4.Red; break; case MenuItemType.Highlighted: - text.Colour = Color4Extensions.FromHex(@"ffcc22"); + Text.Colour = Color4Extensions.FromHex(@"ffcc22"); break; } + + hotkey.Hotkey = osuMenuItem?.Hotkey ?? default; + hotkey.Alpha = EqualityComparer.Default.Equals(hotkey.Hotkey, default) ? 0 : 1; + } + + protected override void UpdateAfterChildren() + { + base.UpdateAfterChildren(); + + // this hack ensures that the menu can auto-size while leaving enough space for the hotkey display. + // the gist of it is that while the hotkey display is not in the text / "content" that determines sizing + // (because it cannot be, because we want the hotkey display to align to the *right* and not the left), + // enough padding to fit the hotkey with _its_ spacing is added as padding of the text to compensate. + Text.Padding = new MarginPadding { Right = hotkey.Alpha > 0 || showChevron ? hotkey.DrawWidth + 15 : 0 }; } protected override bool OnHover(HoverEvent e) @@ -111,20 +130,20 @@ namespace osu.Game.Graphics.UserInterface if (IsHovered && IsActionable) { - text.BoldText.FadeIn(transition_length, Easing.OutQuint); - text.NormalText.FadeOut(transition_length, Easing.OutQuint); + Text.BoldText.FadeIn(transition_length, Easing.OutQuint); + Text.NormalText.FadeOut(transition_length, Easing.OutQuint); } else { - text.BoldText.FadeOut(transition_length, Easing.OutQuint); - text.NormalText.FadeIn(transition_length, Easing.OutQuint); + Text.BoldText.FadeOut(transition_length, Easing.OutQuint); + Text.NormalText.FadeIn(transition_length, Easing.OutQuint); } } - protected sealed override Drawable CreateContent() => text = CreateTextContainer(); + protected sealed override Drawable CreateContent() => Text = CreateTextContainer(); protected virtual TextContainer CreateTextContainer() => new TextContainer(); - protected partial class TextContainer : FillFlowContainer, IHasText + protected partial class TextContainer : Container, IHasText { public LocalisableString Text { @@ -138,39 +157,53 @@ namespace osu.Game.Graphics.UserInterface public readonly SpriteText NormalText; public readonly SpriteText BoldText; + public readonly Container CheckboxContainer; public TextContainer() { - Anchor = Anchor.CentreLeft; - Origin = Anchor.CentreLeft; - AutoSizeAxes = Axes.Both; - Spacing = new Vector2(10); - Direction = FillDirection.Horizontal; - Child = new Container + Child = new FillFlowContainer { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, + AutoSizeAxes = Axes.Both, - Margin = new MarginPadding { Horizontal = MARGIN_HORIZONTAL, Vertical = MARGIN_VERTICAL }, + Spacing = new Vector2(10), + Direction = FillDirection.Horizontal, + Padding = new MarginPadding { Horizontal = MARGIN_HORIZONTAL, Vertical = MARGIN_VERTICAL, }, + Children = new Drawable[] { - NormalText = new OsuSpriteText + CheckboxContainer = new Container { - AlwaysPresent = true, // ensures that the menu item does not change width when switching between normal and bold text. - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Font = OsuFont.GetFont(size: text_size), + RelativeSizeAxes = Axes.Y, + Width = MARGIN_HORIZONTAL, }, - BoldText = new OsuSpriteText + new Container { - AlwaysPresent = true, // ensures that the menu item does not change width when switching between normal and bold text. - Alpha = 0, Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, - Font = OsuFont.GetFont(size: text_size, weight: FontWeight.Bold), - } + AutoSizeAxes = Axes.Both, + Children = new Drawable[] + { + NormalText = new OsuSpriteText + { + AlwaysPresent = true, // ensures that the menu item does not change width when switching between normal and bold text. + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Font = OsuFont.GetFont(size: text_size), + }, + BoldText = new OsuSpriteText + { + AlwaysPresent = true, // ensures that the menu item does not change width when switching between normal and bold text. + Alpha = 0, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Font = OsuFont.GetFont(size: text_size, weight: FontWeight.Bold), + } + } + }, } }; } diff --git a/osu.Game/Graphics/UserInterface/DrawableStatefulMenuItem.cs b/osu.Game/Graphics/UserInterface/DrawableStatefulMenuItem.cs index 6888c2c71b..4206f77c98 100644 --- a/osu.Game/Graphics/UserInterface/DrawableStatefulMenuItem.cs +++ b/osu.Game/Graphics/UserInterface/DrawableStatefulMenuItem.cs @@ -46,12 +46,11 @@ namespace osu.Game.Graphics.UserInterface state = menuItem.State.GetBoundCopy(); - Add(stateIcon = new SpriteIcon + CheckboxContainer.Add(stateIcon = new SpriteIcon { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, Size = new Vector2(10), - Margin = new MarginPadding { Left = -MARGIN_HORIZONTAL, Right = MARGIN_HORIZONTAL }, AlwaysPresent = true, }); } diff --git a/osu.Game/Graphics/UserInterface/OsuMenu.cs b/osu.Game/Graphics/UserInterface/OsuMenu.cs index e2aac297e3..2b9a26166f 100644 --- a/osu.Game/Graphics/UserInterface/OsuMenu.cs +++ b/osu.Game/Graphics/UserInterface/OsuMenu.cs @@ -109,7 +109,7 @@ namespace osu.Game.Graphics.UserInterface Colour = BackgroundColourHover, RelativeSizeAxes = Axes.X, Height = 2f, - Width = 0.8f, + Width = 0.9f, }); } diff --git a/osu.Game/Graphics/UserInterface/OsuMenuItem.cs b/osu.Game/Graphics/UserInterface/OsuMenuItem.cs index 20461de08f..f122990a0f 100644 --- a/osu.Game/Graphics/UserInterface/OsuMenuItem.cs +++ b/osu.Game/Graphics/UserInterface/OsuMenuItem.cs @@ -11,6 +11,8 @@ namespace osu.Game.Graphics.UserInterface { public readonly MenuItemType Type; + public Hotkey Hotkey { get; init; } + public OsuMenuItem(LocalisableString text, MenuItemType type = MenuItemType.Standard) : this(text, type, null) { diff --git a/osu.Game/Screens/Edit/Components/Menus/EditorMenuBar.cs b/osu.Game/Screens/Edit/Components/Menus/EditorMenuBar.cs index ee954a7ea0..101e5e31b0 100644 --- a/osu.Game/Screens/Edit/Components/Menus/EditorMenuBar.cs +++ b/osu.Game/Screens/Edit/Components/Menus/EditorMenuBar.cs @@ -92,6 +92,7 @@ namespace osu.Game.Screens.Edit.Components.Menus BackgroundColour = colourProvider.Background2; ForegroundColourHover = colourProvider.Content1; BackgroundColourHover = colourProvider.Background1; + Text.CheckboxContainer.Alpha = 0; } protected override void LoadComplete() From a417fec2347a01bb4e2ab1be308c474f4a240ab3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 4 Sep 2024 18:35:27 +0900 Subject: [PATCH 2185/2493] Move analysis container implementation completely local to osu! ruleset --- .../TestSceneOsuAnalysisContainer.cs | 129 +++++++----------- .../UI/DrawableOsuRuleset.cs | 10 +- .../UI/OsuAnalysisContainer.cs | 51 +++---- .../UI/OsuAnalysisSettings.cs | 17 ++- osu.Game/Rulesets/Ruleset.cs | 2 - osu.Game/Rulesets/UI/AnalysisContainer.cs | 27 ---- osu.Game/Rulesets/UI/DrawableRuleset.cs | 2 - osu.Game/Rulesets/UI/Playfield.cs | 6 - .../Play/PlayerSettings/AnalysisSettings.cs | 21 --- osu.Game/Screens/Play/ReplayPlayer.cs | 8 -- 10 files changed, 93 insertions(+), 180 deletions(-) delete mode 100644 osu.Game/Rulesets/UI/AnalysisContainer.cs delete mode 100644 osu.Game/Screens/Play/PlayerSettings/AnalysisSettings.cs diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuAnalysisContainer.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuAnalysisContainer.cs index 0fc91513e6..536de8b41a 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuAnalysisContainer.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuAnalysisContainer.cs @@ -1,13 +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 System; using System.Collections.Generic; using System.Linq; using NUnit.Framework; -using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Testing; using osu.Game.Replays; using osu.Game.Rulesets.Osu.Beatmaps; using osu.Game.Rulesets.Osu.Replays; @@ -21,51 +20,80 @@ namespace osu.Game.Rulesets.Osu.Tests { public partial class TestSceneOsuAnalysisContainer : OsuTestScene { - private TestOsuAnalysisContainer analysisContainer; + private TestOsuAnalysisContainer analysisContainer = null!; - [BackgroundDependencyLoader] - private void load() + [SetUpSteps] + public void SetUpSteps() { - Child = analysisContainer = createAnalysisContainer(); + AddStep("create analysis container", () => + { + DrawableOsuRuleset drawableRuleset = new DrawableOsuRuleset(new OsuRuleset(), new OsuBeatmap()); + + Children = new Drawable[] + { + drawableRuleset, + analysisContainer = new TestOsuAnalysisContainer(fabricateReplay(), drawableRuleset), + }; + }); } [Test] public void TestHitMarkers() { - AddStep("enable hit markers", () => analysisContainer.AnalysisSettings.HitMarkersEnabled.Value = true); + AddStep("enable hit markers", () => analysisContainer.Settings.HitMarkersEnabled.Value = true); AddAssert("hit markers visible", () => analysisContainer.HitMarkersVisible); - AddStep("disable hit markers", () => analysisContainer.AnalysisSettings.HitMarkersEnabled.Value = false); + AddStep("disable hit markers", () => analysisContainer.Settings.HitMarkersEnabled.Value = false); AddAssert("hit markers not visible", () => !analysisContainer.HitMarkersVisible); } [Test] public void TestAimMarker() { - AddStep("enable aim markers", () => analysisContainer.AnalysisSettings.AimMarkersEnabled.Value = true); + AddStep("enable aim markers", () => analysisContainer.Settings.AimMarkersEnabled.Value = true); AddAssert("aim markers visible", () => analysisContainer.AimMarkersVisible); - AddStep("disable aim markers", () => analysisContainer.AnalysisSettings.AimMarkersEnabled.Value = false); + AddStep("disable aim markers", () => analysisContainer.Settings.AimMarkersEnabled.Value = false); AddAssert("aim markers not visible", () => !analysisContainer.AimMarkersVisible); } [Test] public void TestAimLines() { - AddStep("enable aim lines", () => analysisContainer.AnalysisSettings.AimLinesEnabled.Value = true); + AddStep("enable aim lines", () => analysisContainer.Settings.AimLinesEnabled.Value = true); AddAssert("aim lines visible", () => analysisContainer.AimLinesVisible); - AddStep("disable aim lines", () => analysisContainer.AnalysisSettings.AimLinesEnabled.Value = false); + AddStep("disable aim lines", () => analysisContainer.Settings.AimLinesEnabled.Value = false); AddAssert("aim lines not visible", () => !analysisContainer.AimLinesVisible); } - private TestOsuAnalysisContainer createAnalysisContainer() + private Replay fabricateReplay() { - var replay = new Replay(); - var ruleset = new OsuRuleset(); - var beatmap = new OsuBeatmap(); - var drawableRuleset = new DrawableOsuRuleset(ruleset, beatmap); - // Load playfield cursor to avoid errors - Add(drawableRuleset); + var frames = new List(); + var random = new Random(); + int posX = 250; + int posY = 250; + bool leftOrRight = false; - return new TestOsuAnalysisContainer(replay, drawableRuleset); + for (int i = 0; i < 1000; i++) + { + posX = Math.Clamp(posX + random.Next(-10, 11), 0, 500); + posY = Math.Clamp(posY + random.Next(-10, 11), 0, 500); + + var actions = new List(); + + if (i % 20 == 0) + { + actions.Add(leftOrRight ? OsuAction.LeftButton : OsuAction.RightButton); + leftOrRight = !leftOrRight; + } + + frames.Add(new OsuReplayFrame + { + Time = Time.Current + i * 15, + Position = new Vector2(posX, posY), + Actions = actions + }); + } + + return new Replay { Frames = frames }; } private partial class TestOsuAnalysisContainer : OsuAnalysisContainer @@ -75,68 +103,9 @@ namespace osu.Game.Rulesets.Osu.Tests { } - [BackgroundDependencyLoader] - private void load() - { - Replay = fabricateReplay(); - LoadReplay(); - - makeReplayLoop(); - } - - private void makeReplayLoop() - { - Scheduler.AddDelayed(() => - { - Replay = fabricateReplay(); - - HitMarkers.Clear(); - AimMarkers.Clear(); - AimLines.Clear(); - - LoadReplay(); - - makeReplayLoop(); - }, 15000); - } - public bool HitMarkersVisible => HitMarkers.Alpha > 0 && HitMarkers.Entries.Any(); - public bool AimMarkersVisible => AimMarkers.Alpha > 0 && AimMarkers.Entries.Any(); - public bool AimLinesVisible => AimLines.Alpha > 0 && AimLines.Vertices.Count > 1; - - private Replay fabricateReplay() - { - var frames = new List(); - var random = new Random(); - int posX = 250; - int posY = 250; - bool leftOrRight = false; - - for (int i = 0; i < 1000; i++) - { - posX = Math.Clamp(posX + random.Next(-10, 11), 0, 500); - posY = Math.Clamp(posY + random.Next(-10, 11), 0, 500); - - var actions = new List(); - - if (i % 20 == 0) - { - actions.Add(leftOrRight ? OsuAction.LeftButton : OsuAction.RightButton); - leftOrRight = !leftOrRight; - } - - frames.Add(new OsuReplayFrame - { - Time = Time.Current + i * 15, - Position = new Vector2(posX, posY), - Actions = actions - }); - } - - return new Replay { Frames = frames }; - } } } } diff --git a/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs b/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs index 3c6456957b..ba0768db5d 100644 --- a/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs @@ -36,6 +36,14 @@ namespace osu.Game.Rulesets.Osu.UI { } + protected override void LoadComplete() + { + if (HasReplayLoaded.Value) + LoadComponentAsync(new OsuAnalysisContainer(ReplayScore.Replay, this), PlayfieldAdjustmentContainer.Add); + + base.LoadComplete(); + } + public override DrawableHitObject CreateDrawableRepresentation(OsuHitObject h) => null; public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; // always show the gameplay cursor @@ -68,7 +76,5 @@ namespace osu.Game.Rulesets.Osu.UI return 0; } } - - public override AnalysisContainer CreateAnalysisContainer(Replay replay) => new OsuAnalysisContainer(replay, this); } } diff --git a/osu.Game.Rulesets.Osu/UI/OsuAnalysisContainer.cs b/osu.Game.Rulesets.Osu/UI/OsuAnalysisContainer.cs index 57401edece..19e53944c9 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuAnalysisContainer.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuAnalysisContainer.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using osu.Framework.Allocation; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Lines; using osu.Framework.Graphics.Performance; using osu.Framework.Graphics.Pooling; @@ -18,62 +19,62 @@ using osuTK.Graphics; namespace osu.Game.Rulesets.Osu.UI { - public partial class OsuAnalysisContainer : AnalysisContainer + public partial class OsuAnalysisContainer : CompositeDrawable { - public new OsuAnalysisSettings AnalysisSettings => (OsuAnalysisSettings)base.AnalysisSettings; + protected readonly HitMarkersContainer HitMarkers; + protected readonly AimMarkersContainer AimMarkers; + protected readonly AimLinesContainer AimLines; - protected new DrawableOsuRuleset DrawableRuleset => (DrawableOsuRuleset)base.DrawableRuleset; + public OsuAnalysisSettings Settings = null!; - protected HitMarkersContainer HitMarkers; - protected AimMarkersContainer AimMarkers; - protected AimLinesContainer AimLines; + private readonly Replay replay; + private readonly DrawableRuleset drawableRuleset; public OsuAnalysisContainer(Replay replay, DrawableRuleset drawableRuleset) - : base(replay, drawableRuleset) { + this.replay = replay; + this.drawableRuleset = drawableRuleset; + InternalChildren = new Drawable[] { - AimLines = new AimLinesContainer { Depth = float.MaxValue }, HitMarkers = new HitMarkersContainer(), - AimMarkers = new AimMarkersContainer { Depth = float.MinValue } + AimLines = new AimLinesContainer(), + AimMarkers = new AimMarkersContainer(), }; } - protected override OsuAnalysisSettings CreateAnalysisSettings(Ruleset ruleset) - { - var settings = new OsuAnalysisSettings((OsuRuleset)ruleset); - settings.HitMarkersEnabled.ValueChanged += e => toggleHitMarkers(e.NewValue); - settings.AimMarkersEnabled.ValueChanged += e => toggleAimMarkers(e.NewValue); - settings.AimLinesEnabled.ValueChanged += e => toggleAimLines(e.NewValue); - settings.CursorHideEnabled.ValueChanged += e => toggleCursorHidden(e.NewValue); - return settings; - } - [BackgroundDependencyLoader] private void load() { - toggleHitMarkers(AnalysisSettings.HitMarkersEnabled.Value); - toggleAimMarkers(AnalysisSettings.AimMarkersEnabled.Value); - toggleAimLines(AnalysisSettings.AimLinesEnabled.Value); - toggleCursorHidden(AnalysisSettings.CursorHideEnabled.Value); + AddInternal(Settings = new OsuAnalysisSettings()); LoadReplay(); } + protected override void LoadComplete() + { + base.LoadComplete(); + + Settings.HitMarkersEnabled.BindValueChanged(e => toggleHitMarkers(e.NewValue), true); + Settings.AimMarkersEnabled.BindValueChanged(e => toggleAimMarkers(e.NewValue), true); + Settings.AimLinesEnabled.BindValueChanged(e => toggleAimLines(e.NewValue), true); + Settings.CursorHideEnabled.BindValueChanged(e => toggleCursorHidden(e.NewValue), true); + } + private void toggleHitMarkers(bool value) => HitMarkers.FadeTo(value ? 1 : 0); private void toggleAimMarkers(bool value) => AimMarkers.FadeTo(value ? 1 : 0); private void toggleAimLines(bool value) => AimLines.FadeTo(value ? 1 : 0); - private void toggleCursorHidden(bool value) => DrawableRuleset.Playfield.Cursor.FadeTo(value ? 0 : 1); + private void toggleCursorHidden(bool value) => drawableRuleset.Playfield.Cursor.FadeTo(value ? 0 : 1); protected void LoadReplay() { bool leftHeld = false; bool rightHeld = false; - foreach (var frame in Replay.Frames) + foreach (var frame in replay.Frames) { var osuFrame = (OsuReplayFrame)frame; diff --git a/osu.Game.Rulesets.Osu/UI/OsuAnalysisSettings.cs b/osu.Game.Rulesets.Osu/UI/OsuAnalysisSettings.cs index 6e11c87c3a..5419d4e17a 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuAnalysisSettings.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuAnalysisSettings.cs @@ -9,13 +9,8 @@ using osu.Game.Screens.Play.PlayerSettings; namespace osu.Game.Rulesets.Osu.UI { - public partial class OsuAnalysisSettings : AnalysisSettings + public partial class OsuAnalysisSettings : PlayerSettingsGroup { - public OsuAnalysisSettings(Ruleset ruleset) - : base(ruleset) - { - } - [SettingSource("Hit markers", SettingControlType = typeof(PlayerCheckbox))] public BindableBool HitMarkersEnabled { get; } = new BindableBool(); @@ -28,10 +23,18 @@ namespace osu.Game.Rulesets.Osu.UI [SettingSource("Hide cursor", SettingControlType = typeof(PlayerCheckbox))] public BindableBool CursorHideEnabled { get; } = new BindableBool(); + public OsuAnalysisSettings() + : base("Analysis Settings") + { + } + [BackgroundDependencyLoader] private void load(IRulesetConfigCache cache) { - var config = (OsuRulesetConfigManager)cache.GetConfigFor(Ruleset)!; + AddRange(this.CreateSettingsControls()); + + var config = (OsuRulesetConfigManager)cache.GetConfigFor(new OsuRuleset())!; + config.BindWith(OsuRulesetSetting.ReplayHitMarkersEnabled, HitMarkersEnabled); config.BindWith(OsuRulesetSetting.ReplayAimMarkersEnabled, AimMarkersEnabled); config.BindWith(OsuRulesetSetting.ReplayAimLinesEnabled, AimLinesEnabled); diff --git a/osu.Game/Rulesets/Ruleset.cs b/osu.Game/Rulesets/Ruleset.cs index 2e48b8e16f..5af1fd386c 100644 --- a/osu.Game/Rulesets/Ruleset.cs +++ b/osu.Game/Rulesets/Ruleset.cs @@ -406,7 +406,5 @@ namespace osu.Game.Rulesets /// Can be overridden to avoid showing scroll speed changes in the editor. /// public virtual bool EditorShowScrollSpeed => true; - - public virtual DifficultySection? CreateEditorDifficultySection() => null; } } diff --git a/osu.Game/Rulesets/UI/AnalysisContainer.cs b/osu.Game/Rulesets/UI/AnalysisContainer.cs deleted file mode 100644 index b6c2a8c1c8..0000000000 --- a/osu.Game/Rulesets/UI/AnalysisContainer.cs +++ /dev/null @@ -1,27 +0,0 @@ -// 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.Graphics.Containers; -using osu.Game.Replays; -using osu.Game.Screens.Play.PlayerSettings; - -namespace osu.Game.Rulesets.UI -{ - public abstract partial class AnalysisContainer : Container - { - protected Replay Replay; - protected DrawableRuleset DrawableRuleset; - - public AnalysisSettings AnalysisSettings; - - protected AnalysisContainer(Replay replay, DrawableRuleset drawableRuleset) - { - Replay = replay; - DrawableRuleset = drawableRuleset; - - AnalysisSettings = CreateAnalysisSettings(drawableRuleset.Ruleset); - } - - protected abstract AnalysisSettings CreateAnalysisSettings(Ruleset ruleset); - } -} diff --git a/osu.Game/Rulesets/UI/DrawableRuleset.cs b/osu.Game/Rulesets/UI/DrawableRuleset.cs index 5b1f59d549..a28b2716cb 100644 --- a/osu.Game/Rulesets/UI/DrawableRuleset.cs +++ b/osu.Game/Rulesets/UI/DrawableRuleset.cs @@ -596,8 +596,6 @@ namespace osu.Game.Rulesets.UI /// Invoked when the user requests to pause while the resume overlay is active. /// public abstract void CancelResume(); - - public virtual AnalysisContainer CreateAnalysisContainer(Replay replay) => null; } public class BeatmapInvalidForRulesetException : ArgumentException diff --git a/osu.Game/Rulesets/UI/Playfield.cs b/osu.Game/Rulesets/UI/Playfield.cs index e116acdc19..90a2f63faa 100644 --- a/osu.Game/Rulesets/UI/Playfield.cs +++ b/osu.Game/Rulesets/UI/Playfield.cs @@ -291,12 +291,6 @@ namespace osu.Game.Rulesets.UI /// protected virtual HitObjectContainer CreateHitObjectContainer() => new HitObjectContainer(); - /// - /// Adds an analysis container to internal children for replays. - /// - /// - public virtual void AddAnalysisContainer(AnalysisContainer analysisContainer) => AddInternal(analysisContainer); - #region Pooling support private readonly Dictionary pools = new Dictionary(); diff --git a/osu.Game/Screens/Play/PlayerSettings/AnalysisSettings.cs b/osu.Game/Screens/Play/PlayerSettings/AnalysisSettings.cs deleted file mode 100644 index 4c64eef92f..0000000000 --- a/osu.Game/Screens/Play/PlayerSettings/AnalysisSettings.cs +++ /dev/null @@ -1,21 +0,0 @@ -// 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.Configuration; -using osu.Game.Rulesets; - -namespace osu.Game.Screens.Play.PlayerSettings -{ - public partial class AnalysisSettings : PlayerSettingsGroup - { - protected Ruleset Ruleset; - - public AnalysisSettings(Ruleset ruleset) - : base("Analysis Settings") - { - Ruleset = ruleset; - - AddRange(this.CreateSettingsControls()); - } - } -} diff --git a/osu.Game/Screens/Play/ReplayPlayer.cs b/osu.Game/Screens/Play/ReplayPlayer.cs index 82a2f09250..ff60dbc0d0 100644 --- a/osu.Game/Screens/Play/ReplayPlayer.cs +++ b/osu.Game/Screens/Play/ReplayPlayer.cs @@ -71,14 +71,6 @@ namespace osu.Game.Screens.Play playbackSettings.UserPlaybackRate.BindTo(master.UserPlaybackRate); HUDOverlay.PlayerSettingsOverlay.AddAtStart(playbackSettings); - - var analysisContainer = DrawableRuleset.CreateAnalysisContainer(GameplayState.Score.Replay); - - if (analysisContainer != null) - { - HUDOverlay.PlayerSettingsOverlay.AddAtStart(analysisContainer.AnalysisSettings); - DrawableRuleset.Playfield.AddAnalysisContainer(analysisContainer); - } } protected override void PrepareReplay() From 992a0da95727c3d574289d6b3664d2be0729166c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 4 Sep 2024 18:43:33 +0900 Subject: [PATCH 2186/2493] Rename classes slightly --- .../TestSceneOsuAnalysisContainer.cs | 8 ++++---- osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs | 2 +- .../{OsuAnalysisContainer.cs => ReplayAnalysisOverlay.cs} | 8 ++++---- .../{OsuAnalysisSettings.cs => ReplayAnalysisSettings.cs} | 4 ++-- 4 files changed, 11 insertions(+), 11 deletions(-) rename osu.Game.Rulesets.Osu/UI/{OsuAnalysisContainer.cs => ReplayAnalysisOverlay.cs} (96%) rename osu.Game.Rulesets.Osu/UI/{OsuAnalysisSettings.cs => ReplayAnalysisSettings.cs} (93%) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuAnalysisContainer.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuAnalysisContainer.cs index 536de8b41a..548e40487b 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuAnalysisContainer.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuAnalysisContainer.cs @@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Osu.Tests { public partial class TestSceneOsuAnalysisContainer : OsuTestScene { - private TestOsuAnalysisContainer analysisContainer = null!; + private TestReplayAnalysisOverlay analysisContainer = null!; [SetUpSteps] public void SetUpSteps() @@ -32,7 +32,7 @@ namespace osu.Game.Rulesets.Osu.Tests Children = new Drawable[] { drawableRuleset, - analysisContainer = new TestOsuAnalysisContainer(fabricateReplay(), drawableRuleset), + analysisContainer = new TestReplayAnalysisOverlay(fabricateReplay(), drawableRuleset), }; }); } @@ -96,9 +96,9 @@ namespace osu.Game.Rulesets.Osu.Tests return new Replay { Frames = frames }; } - private partial class TestOsuAnalysisContainer : OsuAnalysisContainer + private partial class TestReplayAnalysisOverlay : ReplayAnalysisOverlay { - public TestOsuAnalysisContainer(Replay replay, DrawableRuleset drawableRuleset) + public TestReplayAnalysisOverlay(Replay replay, DrawableRuleset drawableRuleset) : base(replay, drawableRuleset) { } diff --git a/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs b/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs index ba0768db5d..ee16fa91f4 100644 --- a/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs @@ -39,7 +39,7 @@ namespace osu.Game.Rulesets.Osu.UI protected override void LoadComplete() { if (HasReplayLoaded.Value) - LoadComponentAsync(new OsuAnalysisContainer(ReplayScore.Replay, this), PlayfieldAdjustmentContainer.Add); + LoadComponentAsync(new ReplayAnalysisOverlay(ReplayScore.Replay, this), PlayfieldAdjustmentContainer.Add); base.LoadComplete(); } diff --git a/osu.Game.Rulesets.Osu/UI/OsuAnalysisContainer.cs b/osu.Game.Rulesets.Osu/UI/ReplayAnalysisOverlay.cs similarity index 96% rename from osu.Game.Rulesets.Osu/UI/OsuAnalysisContainer.cs rename to osu.Game.Rulesets.Osu/UI/ReplayAnalysisOverlay.cs index 19e53944c9..521f67a77c 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuAnalysisContainer.cs +++ b/osu.Game.Rulesets.Osu/UI/ReplayAnalysisOverlay.cs @@ -19,18 +19,18 @@ using osuTK.Graphics; namespace osu.Game.Rulesets.Osu.UI { - public partial class OsuAnalysisContainer : CompositeDrawable + public partial class ReplayAnalysisOverlay : CompositeDrawable { protected readonly HitMarkersContainer HitMarkers; protected readonly AimMarkersContainer AimMarkers; protected readonly AimLinesContainer AimLines; - public OsuAnalysisSettings Settings = null!; + public ReplayAnalysisSettings Settings = null!; private readonly Replay replay; private readonly DrawableRuleset drawableRuleset; - public OsuAnalysisContainer(Replay replay, DrawableRuleset drawableRuleset) + public ReplayAnalysisOverlay(Replay replay, DrawableRuleset drawableRuleset) { this.replay = replay; this.drawableRuleset = drawableRuleset; @@ -46,7 +46,7 @@ namespace osu.Game.Rulesets.Osu.UI [BackgroundDependencyLoader] private void load() { - AddInternal(Settings = new OsuAnalysisSettings()); + AddInternal(Settings = new ReplayAnalysisSettings()); LoadReplay(); } diff --git a/osu.Game.Rulesets.Osu/UI/OsuAnalysisSettings.cs b/osu.Game.Rulesets.Osu/UI/ReplayAnalysisSettings.cs similarity index 93% rename from osu.Game.Rulesets.Osu/UI/OsuAnalysisSettings.cs rename to osu.Game.Rulesets.Osu/UI/ReplayAnalysisSettings.cs index 5419d4e17a..87180e155b 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuAnalysisSettings.cs +++ b/osu.Game.Rulesets.Osu/UI/ReplayAnalysisSettings.cs @@ -9,7 +9,7 @@ using osu.Game.Screens.Play.PlayerSettings; namespace osu.Game.Rulesets.Osu.UI { - public partial class OsuAnalysisSettings : PlayerSettingsGroup + public partial class ReplayAnalysisSettings : PlayerSettingsGroup { [SettingSource("Hit markers", SettingControlType = typeof(PlayerCheckbox))] public BindableBool HitMarkersEnabled { get; } = new BindableBool(); @@ -23,7 +23,7 @@ namespace osu.Game.Rulesets.Osu.UI [SettingSource("Hide cursor", SettingControlType = typeof(PlayerCheckbox))] public BindableBool CursorHideEnabled { get; } = new BindableBool(); - public OsuAnalysisSettings() + public ReplayAnalysisSettings() : base("Analysis Settings") { } From cc3d220f6f421603fda86800025fe98a859c7c8d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 4 Sep 2024 18:58:19 +0900 Subject: [PATCH 2187/2493] Allow settings to be added to replay HUD from ruleset --- osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs | 16 +++++++--------- .../UI/ReplayAnalysisOverlay.cs | 5 +++-- osu.Game/Screens/Play/ReplayPlayer.cs | 10 ++++++++++ 3 files changed, 20 insertions(+), 11 deletions(-) diff --git a/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs b/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs index ee16fa91f4..880b2dbe6f 100644 --- a/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs @@ -1,11 +1,10 @@ // 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.Collections.Generic; using System.Linq; +using osu.Framework.Allocation; using osu.Framework.Input; using osu.Game.Beatmaps; using osu.Game.Input.Handlers; @@ -31,20 +30,19 @@ namespace osu.Game.Rulesets.Osu.UI public new OsuPlayfield Playfield => (OsuPlayfield)base.Playfield; - public DrawableOsuRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList mods = null) + public DrawableOsuRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList? mods = null) : base(ruleset, beatmap, mods) { } - protected override void LoadComplete() + [BackgroundDependencyLoader] + private void load(ReplayPlayer? replayPlayer) { - if (HasReplayLoaded.Value) - LoadComponentAsync(new ReplayAnalysisOverlay(ReplayScore.Replay, this), PlayfieldAdjustmentContainer.Add); - - base.LoadComplete(); + if (replayPlayer != null) + PlayfieldAdjustmentContainer.Add(new ReplayAnalysisOverlay(replayPlayer.Score.Replay, this)); } - public override DrawableHitObject CreateDrawableRepresentation(OsuHitObject h) => null; + public override DrawableHitObject? CreateDrawableRepresentation(OsuHitObject h) => null; public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; // always show the gameplay cursor diff --git a/osu.Game.Rulesets.Osu/UI/ReplayAnalysisOverlay.cs b/osu.Game.Rulesets.Osu/UI/ReplayAnalysisOverlay.cs index 521f67a77c..398ab54981 100644 --- a/osu.Game.Rulesets.Osu/UI/ReplayAnalysisOverlay.cs +++ b/osu.Game.Rulesets.Osu/UI/ReplayAnalysisOverlay.cs @@ -14,6 +14,7 @@ using osu.Game.Rulesets.Objects.Pooling; using osu.Game.Rulesets.Osu.Replays; using osu.Game.Rulesets.Osu.Skinning.Default; using osu.Game.Rulesets.UI; +using osu.Game.Screens.Play; using osuTK; using osuTK.Graphics; @@ -44,9 +45,9 @@ namespace osu.Game.Rulesets.Osu.UI } [BackgroundDependencyLoader] - private void load() + private void load(ReplayPlayer replayPlayer) { - AddInternal(Settings = new ReplayAnalysisSettings()); + replayPlayer.AddSettings(Settings = new ReplayAnalysisSettings()); LoadReplay(); } diff --git a/osu.Game/Screens/Play/ReplayPlayer.cs b/osu.Game/Screens/Play/ReplayPlayer.cs index ff60dbc0d0..0c125264a1 100644 --- a/osu.Game/Screens/Play/ReplayPlayer.cs +++ b/osu.Game/Screens/Play/ReplayPlayer.cs @@ -55,6 +55,16 @@ namespace osu.Game.Screens.Play this.createScore = createScore; } + /// + /// Add a settings group to the HUD overlay. Intended to be used by rulesets to add replay-specific settings. + /// + /// The settings group to be shown. + public void AddSettings(PlayerSettingsGroup settings) => Schedule(() => + { + settings.Expanded.Value = false; + HUDOverlay.PlayerSettingsOverlay.Add(settings); + }); + [BackgroundDependencyLoader] private void load(OsuConfigManager config) { From 9b81deb3ac95988d72afb96b509eb6785855281d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 4 Sep 2024 19:09:23 +0900 Subject: [PATCH 2188/2493] Fix settings not working if `ReplayPlayer` is not available --- .../TestSceneOsuAnalysisContainer.cs | 2 ++ .../UI/ReplayAnalysisOverlay.cs | 32 +++++++++---------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuAnalysisContainer.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuAnalysisContainer.cs index 548e40487b..fc2255a9cf 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuAnalysisContainer.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuAnalysisContainer.cs @@ -103,6 +103,8 @@ namespace osu.Game.Rulesets.Osu.Tests { } + public new ReplayAnalysisSettings Settings => base.Settings; + public bool HitMarkersVisible => HitMarkers.Alpha > 0 && HitMarkers.Entries.Any(); public bool AimMarkersVisible => AimMarkers.Alpha > 0 && AimMarkers.Entries.Any(); public bool AimLinesVisible => AimLines.Alpha > 0 && AimLines.Vertices.Count > 1; diff --git a/osu.Game.Rulesets.Osu/UI/ReplayAnalysisOverlay.cs b/osu.Game.Rulesets.Osu/UI/ReplayAnalysisOverlay.cs index 398ab54981..d16f161370 100644 --- a/osu.Game.Rulesets.Osu/UI/ReplayAnalysisOverlay.cs +++ b/osu.Game.Rulesets.Osu/UI/ReplayAnalysisOverlay.cs @@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Osu.UI protected readonly AimMarkersContainer AimMarkers; protected readonly AimLinesContainer AimLines; - public ReplayAnalysisSettings Settings = null!; + protected ReplayAnalysisSettings Settings = null!; private readonly Replay replay; private readonly DrawableRuleset drawableRuleset; @@ -45,32 +45,30 @@ namespace osu.Game.Rulesets.Osu.UI } [BackgroundDependencyLoader] - private void load(ReplayPlayer replayPlayer) + private void load(ReplayPlayer? replayPlayer) { - replayPlayer.AddSettings(Settings = new ReplayAnalysisSettings()); + Settings = new ReplayAnalysisSettings(); - LoadReplay(); + if (replayPlayer != null) + replayPlayer.AddSettings(Settings); + else + // only in test + AddInternal(Settings); + + loadReplay(); } protected override void LoadComplete() { base.LoadComplete(); - Settings.HitMarkersEnabled.BindValueChanged(e => toggleHitMarkers(e.NewValue), true); - Settings.AimMarkersEnabled.BindValueChanged(e => toggleAimMarkers(e.NewValue), true); - Settings.AimLinesEnabled.BindValueChanged(e => toggleAimLines(e.NewValue), true); - Settings.CursorHideEnabled.BindValueChanged(e => toggleCursorHidden(e.NewValue), true); + Settings.HitMarkersEnabled.BindValueChanged(enabled => HitMarkers.FadeTo(enabled.NewValue ? 1 : 0), true); + Settings.AimMarkersEnabled.BindValueChanged(enabled => AimMarkers.FadeTo(enabled.NewValue ? 1 : 0), true); + Settings.AimLinesEnabled.BindValueChanged(enabled => AimLines.FadeTo(enabled.NewValue ? 1 : 0), true); + Settings.CursorHideEnabled.BindValueChanged(enabled => drawableRuleset.Playfield.Cursor.FadeTo(enabled.NewValue ? 0 : 1), true); } - private void toggleHitMarkers(bool value) => HitMarkers.FadeTo(value ? 1 : 0); - - private void toggleAimMarkers(bool value) => AimMarkers.FadeTo(value ? 1 : 0); - - private void toggleAimLines(bool value) => AimLines.FadeTo(value ? 1 : 0); - - private void toggleCursorHidden(bool value) => drawableRuleset.Playfield.Cursor.FadeTo(value ? 0 : 1); - - protected void LoadReplay() + private void loadReplay() { bool leftHeld = false; bool rightHeld = false; From 0c4f5bcdaad0fbffe62a37a1351819482fe93923 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 4 Sep 2024 11:45:29 +0200 Subject: [PATCH 2189/2493] Decouple editor main menu items from `DrawableOsuMenuItem` It didn't ever really make sense for it to be sharing the implementation details of that (e.g. colouring of primary/dangerous actions), and with the hotkey display things got outright hacky, so I'm decoupling it entirely. --- .../Editing/TestSceneDifficultyDelete.cs | 5 +- .../UserInterface/DrawableOsuMenuItem.cs | 28 +++--- .../Edit/Components/Menus/EditorMenuBar.cs | 91 +++++++++++++++++-- 3 files changed, 101 insertions(+), 23 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneDifficultyDelete.cs b/osu.Game.Tests/Visual/Editing/TestSceneDifficultyDelete.cs index d4bd77642c..62ff59c6b3 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneDifficultyDelete.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneDifficultyDelete.cs @@ -13,6 +13,7 @@ using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets; using osu.Game.Rulesets.Osu; using osu.Game.Screens.Edit; +using osu.Game.Screens.Edit.Components.Menus; using osu.Game.Storyboards; using osu.Game.Tests.Beatmaps.IO; using osuTK.Input; @@ -60,7 +61,7 @@ namespace osu.Game.Tests.Visual.Editing beatmapSetHashBefore = Beatmap.Value.BeatmapSetInfo.Hash; }); - AddStep("click File", () => this.ChildrenOfType().First().TriggerClick()); + AddStep("click File", () => this.ChildrenOfType().First().TriggerClick()); if (i == 11) { @@ -107,7 +108,7 @@ namespace osu.Game.Tests.Visual.Editing EditorBeatmap.EndChange(); }); - AddStep("click File", () => this.ChildrenOfType().First().TriggerClick()); + AddStep("click File", () => this.ChildrenOfType().First().TriggerClick()); AddStep("click delete", () => getDeleteMenuItem().TriggerClick()); AddUntilStep("wait for dialog", () => DialogOverlay.CurrentDialog != null); diff --git a/osu.Game/Graphics/UserInterface/DrawableOsuMenuItem.cs b/osu.Game/Graphics/UserInterface/DrawableOsuMenuItem.cs index 3ecda50537..20de8e3c9f 100644 --- a/osu.Game/Graphics/UserInterface/DrawableOsuMenuItem.cs +++ b/osu.Game/Graphics/UserInterface/DrawableOsuMenuItem.cs @@ -23,10 +23,10 @@ namespace osu.Game.Graphics.UserInterface { public const int MARGIN_HORIZONTAL = 10; public const int MARGIN_VERTICAL = 4; - private const int text_size = 17; - private const int transition_length = 80; + public const int TEXT_SIZE = 17; + public const int TRANSITION_LENGTH = 80; - protected TextContainer Text { get; private set; } + private TextContainer text; private HotkeyDisplay hotkey; private HoverClickSounds hoverClickSounds; @@ -84,15 +84,15 @@ namespace osu.Game.Graphics.UserInterface { default: case MenuItemType.Standard: - Text.Colour = Color4.White; + text.Colour = Color4.White; break; case MenuItemType.Destructive: - Text.Colour = Color4.Red; + text.Colour = Color4.Red; break; case MenuItemType.Highlighted: - Text.Colour = Color4Extensions.FromHex(@"ffcc22"); + text.Colour = Color4Extensions.FromHex(@"ffcc22"); break; } @@ -108,7 +108,7 @@ namespace osu.Game.Graphics.UserInterface // the gist of it is that while the hotkey display is not in the text / "content" that determines sizing // (because it cannot be, because we want the hotkey display to align to the *right* and not the left), // enough padding to fit the hotkey with _its_ spacing is added as padding of the text to compensate. - Text.Padding = new MarginPadding { Right = hotkey.Alpha > 0 || showChevron ? hotkey.DrawWidth + 15 : 0 }; + text.Padding = new MarginPadding { Right = hotkey.Alpha > 0 || showChevron ? hotkey.DrawWidth + 15 : 0 }; } protected override bool OnHover(HoverEvent e) @@ -130,17 +130,17 @@ namespace osu.Game.Graphics.UserInterface if (IsHovered && IsActionable) { - Text.BoldText.FadeIn(transition_length, Easing.OutQuint); - Text.NormalText.FadeOut(transition_length, Easing.OutQuint); + text.BoldText.FadeIn(TRANSITION_LENGTH, Easing.OutQuint); + text.NormalText.FadeOut(TRANSITION_LENGTH, Easing.OutQuint); } else { - Text.BoldText.FadeOut(transition_length, Easing.OutQuint); - Text.NormalText.FadeIn(transition_length, Easing.OutQuint); + text.BoldText.FadeOut(TRANSITION_LENGTH, Easing.OutQuint); + text.NormalText.FadeIn(TRANSITION_LENGTH, Easing.OutQuint); } } - protected sealed override Drawable CreateContent() => Text = CreateTextContainer(); + protected sealed override Drawable CreateContent() => text = CreateTextContainer(); protected virtual TextContainer CreateTextContainer() => new TextContainer(); protected partial class TextContainer : Container, IHasText @@ -192,7 +192,7 @@ namespace osu.Game.Graphics.UserInterface AlwaysPresent = true, // ensures that the menu item does not change width when switching between normal and bold text. Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, - Font = OsuFont.GetFont(size: text_size), + Font = OsuFont.GetFont(size: TEXT_SIZE), }, BoldText = new OsuSpriteText { @@ -200,7 +200,7 @@ namespace osu.Game.Graphics.UserInterface Alpha = 0, Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, - Font = OsuFont.GetFont(size: text_size, weight: FontWeight.Bold), + Font = OsuFont.GetFont(size: TEXT_SIZE, weight: FontWeight.Bold), } } }, diff --git a/osu.Game/Screens/Edit/Components/Menus/EditorMenuBar.cs b/osu.Game/Screens/Edit/Components/Menus/EditorMenuBar.cs index 101e5e31b0..47a13dcfba 100644 --- a/osu.Game/Screens/Edit/Components/Menus/EditorMenuBar.cs +++ b/osu.Game/Screens/Edit/Components/Menus/EditorMenuBar.cs @@ -7,7 +7,10 @@ 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.Framework.Localisation; using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Overlays; using osuTK; @@ -78,8 +81,11 @@ namespace osu.Game.Screens.Edit.Components.Menus protected override DrawableMenuItem CreateDrawableMenuItem(MenuItem item) => new DrawableEditorBarMenuItem(item); - private partial class DrawableEditorBarMenuItem : DrawableOsuMenuItem + internal partial class DrawableEditorBarMenuItem : DrawableMenuItem { + private HoverClickSounds hoverClickSounds = null!; + private TextContainer text = null!; + public DrawableEditorBarMenuItem(MenuItem item) : base(item) { @@ -92,7 +98,8 @@ namespace osu.Game.Screens.Edit.Components.Menus BackgroundColour = colourProvider.Background2; ForegroundColourHover = colourProvider.Content1; BackgroundColourHover = colourProvider.Background1; - Text.CheckboxContainer.Alpha = 0; + + AddInternal(hoverClickSounds = new HoverClickSounds()); } protected override void LoadComplete() @@ -101,6 +108,36 @@ namespace osu.Game.Screens.Edit.Components.Menus Foreground.Anchor = Anchor.CentreLeft; Foreground.Origin = Anchor.CentreLeft; + Item.Action.BindDisabledChanged(_ => updateState(), true); + } + + protected override bool OnHover(HoverEvent e) + { + updateState(); + return base.OnHover(e); + } + + protected override void OnHoverLost(HoverLostEvent e) + { + updateState(); + base.OnHoverLost(e); + } + + private void updateState() + { + hoverClickSounds.Enabled.Value = IsActionable; + Alpha = IsActionable ? 1 : 0.2f; + + if (IsHovered && IsActionable) + { + text.BoldText.FadeIn(DrawableOsuMenuItem.TRANSITION_LENGTH, Easing.OutQuint); + text.NormalText.FadeOut(DrawableOsuMenuItem.TRANSITION_LENGTH, Easing.OutQuint); + } + else + { + text.BoldText.FadeOut(DrawableOsuMenuItem.TRANSITION_LENGTH, Easing.OutQuint); + text.NormalText.FadeIn(DrawableOsuMenuItem.TRANSITION_LENGTH, Easing.OutQuint); + } } protected override void UpdateBackgroundColour() @@ -119,16 +156,56 @@ namespace osu.Game.Screens.Edit.Components.Menus base.UpdateForegroundColour(); } - protected override DrawableOsuMenuItem.TextContainer CreateTextContainer() => new TextContainer(); + protected sealed override Drawable CreateContent() => text = new TextContainer(); + } - private new partial class TextContainer : DrawableOsuMenuItem.TextContainer + private partial class TextContainer : Container, IHasText + { + public LocalisableString Text { - public TextContainer() + get => NormalText.Text; + set { - NormalText.Font = OsuFont.TorusAlternate; - BoldText.Font = OsuFont.TorusAlternate.With(weight: FontWeight.Bold); + NormalText.Text = value; + BoldText.Text = value; } } + + public readonly SpriteText NormalText; + public readonly SpriteText BoldText; + + public TextContainer() + { + AutoSizeAxes = Axes.Both; + + Child = new Container + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + + AutoSizeAxes = Axes.Both, + Padding = new MarginPadding { Horizontal = 17, Vertical = DrawableOsuMenuItem.MARGIN_VERTICAL, }, + + Children = new Drawable[] + { + NormalText = new OsuSpriteText + { + AlwaysPresent = true, // ensures that the menu item does not change width when switching between normal and bold text. + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Font = OsuFont.GetFont(size: DrawableOsuMenuItem.TEXT_SIZE), + }, + BoldText = new OsuSpriteText + { + AlwaysPresent = true, // ensures that the menu item does not change width when switching between normal and bold text. + Alpha = 0, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Font = OsuFont.GetFont(size: DrawableOsuMenuItem.TEXT_SIZE, weight: FontWeight.Bold), + } + } + }; + } } private partial class SubMenu : OsuMenu From 130802e48048c134c6c8f19c77e3e032834acf72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 18 Jul 2024 11:20:31 +0200 Subject: [PATCH 2190/2493] Add hotkey hints to editor menus --- .../JuiceStreamSelectionBlueprint.cs | 6 +- .../Components/PathControlPointVisualiser.cs | 16 ++++- .../Sliders/SliderSelectionBlueprint.cs | 11 +++- .../Edit/TaikoSelectionHandler.cs | 17 ++++- osu.Game/Graphics/UserInterface/Hotkey.cs | 20 ++++-- .../Components/EditorSelectionHandler.cs | 65 +++++++++++++++++-- .../Compose/Components/SelectionHandler.cs | 5 +- osu.Game/Screens/Edit/Editor.cs | 14 ++-- 8 files changed, 124 insertions(+), 30 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/JuiceStreamSelectionBlueprint.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/JuiceStreamSelectionBlueprint.cs index a492920d3a..3eb8d6c018 100644 --- a/osu.Game.Rulesets.Catch/Edit/Blueprints/JuiceStreamSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/JuiceStreamSelectionBlueprint.cs @@ -8,6 +8,7 @@ using osu.Framework.Caching; using osu.Framework.Graphics; using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.UserInterface; +using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets.Catch.Edit.Blueprints.Components; @@ -172,7 +173,10 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints yield return new OsuMenuItem("Add vertex", MenuItemType.Standard, () => { editablePath.AddVertex(rightMouseDownPosition); - }); + }) + { + Hotkey = new Hotkey(new KeyCombination(InputKey.Control, InputKey.MouseLeft)) + }; } protected override void Dispose(bool isDisposing) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs index df369dcef5..21d63a0ea3 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs @@ -488,8 +488,10 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components curveTypeItems = new List(); - foreach (PathType? type in path_types) + for (int i = 0; i < path_types.Length; ++i) { + var type = path_types[i]; + // special inherit case if (type == null) { @@ -499,7 +501,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components curveTypeItems.Add(new OsuMenuItemSpacer()); } - curveTypeItems.Add(createMenuItemForPathType(type)); + curveTypeItems.Add(createMenuItemForPathType(type, InputKey.Number1 + i)); } if (selectedPieces.Any(piece => piece.ControlPoint.Type?.Type == SplineType.Catmull)) @@ -533,7 +535,15 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components return menuItems.ToArray(); - CurveTypeMenuItem createMenuItemForPathType(PathType? type) => new CurveTypeMenuItem(type, _ => updatePathTypeOfSelectedPieces(type)); + CurveTypeMenuItem createMenuItemForPathType(PathType? type, InputKey? key = null) + { + Hotkey hotkey = default; + + if (key != null) + hotkey = new Hotkey(new KeyCombination(InputKey.Alt, key.Value)); + + return new CurveTypeMenuItem(type, _ => updatePathTypeOfSelectedPieces(type)) { Hotkey = hotkey }; + } } } diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs index 1debb09099..25b3012d8f 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs @@ -11,6 +11,7 @@ using osu.Framework.Caching; using osu.Framework.Graphics; using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.UserInterface; +using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Framework.Utils; using osu.Game.Audio; @@ -593,8 +594,14 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders changeHandler?.BeginChange(); addControlPoint(lastRightClickPosition); changeHandler?.EndChange(); - }), - new OsuMenuItem("Convert to stream", MenuItemType.Destructive, convertToStream), + }) + { + Hotkey = new Hotkey(new KeyCombination(InputKey.Control, InputKey.MouseLeft)) + }, + new OsuMenuItem("Convert to stream", MenuItemType.Destructive, convertToStream) + { + Hotkey = new Hotkey(new KeyCombination(InputKey.Control, InputKey.Shift, InputKey.F)) + }, }; // Always refer to the drawable object's slider body so subsequent movement deltas are calculated with updated positions. diff --git a/osu.Game.Rulesets.Taiko/Edit/TaikoSelectionHandler.cs b/osu.Game.Rulesets.Taiko/Edit/TaikoSelectionHandler.cs index ae6dced9aa..b706e96bdb 100644 --- a/osu.Game.Rulesets.Taiko/Edit/TaikoSelectionHandler.cs +++ b/osu.Game.Rulesets.Taiko/Edit/TaikoSelectionHandler.cs @@ -6,6 +6,7 @@ using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics.UserInterface; +using osu.Framework.Input.Bindings; using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects; @@ -86,10 +87,22 @@ namespace osu.Game.Rulesets.Taiko.Edit protected override IEnumerable GetContextMenuItemsForSelection(IEnumerable> selection) { if (selection.All(s => s.Item is Hit)) - yield return new TernaryStateToggleMenuItem("Rim") { State = { BindTarget = selectionRimState } }; + { + yield return new TernaryStateToggleMenuItem("Rim") + { + State = { BindTarget = selectionRimState }, + Hotkey = new Hotkey(new KeyCombination(InputKey.W), new KeyCombination(InputKey.R)), + }; + } if (selection.All(s => s.Item is TaikoHitObject)) - yield return new TernaryStateToggleMenuItem("Strong") { State = { BindTarget = selectionStrongState } }; + { + yield return new TernaryStateToggleMenuItem("Strong") + { + State = { BindTarget = selectionStrongState }, + Hotkey = new Hotkey(new KeyCombination(InputKey.E)), + }; + } foreach (var item in base.GetContextMenuItemsForSelection(selection)) yield return item; diff --git a/osu.Game/Graphics/UserInterface/Hotkey.cs b/osu.Game/Graphics/UserInterface/Hotkey.cs index 811d385466..c4c0eb63c1 100644 --- a/osu.Game/Graphics/UserInterface/Hotkey.cs +++ b/osu.Game/Graphics/UserInterface/Hotkey.cs @@ -13,9 +13,9 @@ namespace osu.Game.Graphics.UserInterface { public struct Hotkey { - public KeyCombination[]? KeyCombinations { get; } - public GlobalAction? GlobalAction { get; } - public PlatformAction? PlatformAction { get; } + public KeyCombination[]? KeyCombinations { get; init; } + public GlobalAction? GlobalAction { get; init; } + public PlatformAction? PlatformAction { get; init; } public Hotkey(params KeyCombination[] keyCombinations) { @@ -34,20 +34,26 @@ namespace osu.Game.Graphics.UserInterface public IEnumerable ResolveKeyCombination(ReadableKeyCombinationProvider keyCombinationProvider, RealmKeyBindingStore keyBindingStore, GameHost gameHost) { + var result = new List(); + if (KeyCombinations != null) - return KeyCombinations.Select(keyCombinationProvider.GetReadableString); + { + result.AddRange(KeyCombinations.Select(keyCombinationProvider.GetReadableString)); + } if (GlobalAction != null) - return keyBindingStore.GetReadableKeyCombinationsFor(GlobalAction.Value); + { + result.AddRange(keyBindingStore.GetReadableKeyCombinationsFor(GlobalAction.Value)); + } if (PlatformAction != null) { var action = PlatformAction.Value; var bindings = gameHost.PlatformKeyBindings.Where(kb => (PlatformAction)kb.Action == action); - return bindings.Select(b => keyCombinationProvider.GetReadableString(b.KeyCombination)); + result.AddRange(bindings.Select(b => keyCombinationProvider.GetReadableString(b.KeyCombination))); } - return Enumerable.Empty(); + return result; } } } diff --git a/osu.Game/Screens/Edit/Compose/Components/EditorSelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/EditorSelectionHandler.cs index 472b48425f..dbbf767a7d 100644 --- a/osu.Game/Screens/Edit/Compose/Components/EditorSelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/EditorSelectionHandler.cs @@ -8,6 +8,7 @@ using Humanizer; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics.UserInterface; +using osu.Framework.Input.Bindings; using osu.Game.Audio; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Graphics.UserInterface; @@ -350,19 +351,69 @@ namespace osu.Game.Screens.Edit.Compose.Components { if (SelectedBlueprints.All(b => b.Item is IHasComboInformation)) { - yield return new TernaryStateToggleMenuItem("New combo") { State = { BindTarget = SelectionNewComboState } }; + yield return new TernaryStateToggleMenuItem("New combo") + { + State = { BindTarget = SelectionNewComboState }, + Hotkey = new Hotkey(new KeyCombination(InputKey.Q)) + }; } - yield return new OsuMenuItem("Sample") + yield return new OsuMenuItem("Sample") { Items = getSampleSubmenuItems().ToArray(), }; + yield return new OsuMenuItem("Bank") { Items = getBankSubmenuItems().ToArray(), }; + } + + private IEnumerable getSampleSubmenuItems() + { + var whistle = SelectionSampleStates[HitSampleInfo.HIT_WHISTLE]; + yield return new TernaryStateToggleMenuItem(whistle.Description) { - Items = SelectionSampleStates.Select(kvp => - new TernaryStateToggleMenuItem(kvp.Value.Description) { State = { BindTarget = kvp.Value } }).ToArray() + State = { BindTarget = whistle }, + Hotkey = new Hotkey(new KeyCombination(InputKey.W)) }; - yield return new OsuMenuItem("Bank") + var finish = SelectionSampleStates[HitSampleInfo.HIT_FINISH]; + yield return new TernaryStateToggleMenuItem(finish.Description) { - Items = SelectionBankStates.Select(kvp => - new TernaryStateToggleMenuItem(kvp.Value.Description) { State = { BindTarget = kvp.Value } }).ToArray() + State = { BindTarget = finish }, + Hotkey = new Hotkey(new KeyCombination(InputKey.E)) + }; + + var clap = SelectionSampleStates[HitSampleInfo.HIT_CLAP]; + yield return new TernaryStateToggleMenuItem(clap.Description) + { + State = { BindTarget = clap }, + Hotkey = new Hotkey(new KeyCombination(InputKey.R)) + }; + } + + private IEnumerable getBankSubmenuItems() + { + var auto = SelectionBankStates[HIT_BANK_AUTO]; + yield return new TernaryStateToggleMenuItem(auto.Description) + { + State = { BindTarget = auto }, + Hotkey = new Hotkey(new KeyCombination(InputKey.Shift, InputKey.Q)) + }; + + var normal = SelectionBankStates[HitSampleInfo.BANK_NORMAL]; + yield return new TernaryStateToggleMenuItem(normal.Description) + { + State = { BindTarget = normal }, + Hotkey = new Hotkey(new KeyCombination(InputKey.Shift, InputKey.W)) + }; + + var soft = SelectionBankStates[HitSampleInfo.BANK_SOFT]; + yield return new TernaryStateToggleMenuItem(soft.Description) + { + State = { BindTarget = soft }, + Hotkey = new Hotkey(new KeyCombination(InputKey.Shift, InputKey.E)) + }; + + var drum = SelectionBankStates[HitSampleInfo.BANK_DRUM]; + yield return new TernaryStateToggleMenuItem(drum.Description) + { + State = { BindTarget = drum }, + Hotkey = new Hotkey(new KeyCombination(InputKey.Shift, InputKey.R)) }; } diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs index 98807ad85d..39fff169b7 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs @@ -415,7 +415,10 @@ namespace osu.Game.Screens.Edit.Compose.Components if (SelectedBlueprints.Count == 1) items.AddRange(SelectedBlueprints[0].ContextMenuItems); - items.Add(new OsuMenuItem(CommonStrings.ButtonsDelete, MenuItemType.Destructive, DeleteSelected)); + items.Add(new OsuMenuItem(CommonStrings.ButtonsDelete, MenuItemType.Destructive, DeleteSelected) + { + Hotkey = new Hotkey { PlatformAction = PlatformAction.Delete, KeyCombinations = [new KeyCombination(InputKey.Shift, InputKey.MouseRight), new KeyCombination(InputKey.MouseMiddle)] } + }); return items.ToArray(); } diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 9bb91af806..de74fd87cc 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -362,13 +362,13 @@ namespace osu.Game.Screens.Edit { Items = new[] { - undoMenuItem = new EditorMenuItem(CommonStrings.Undo, MenuItemType.Standard, Undo), - redoMenuItem = new EditorMenuItem(CommonStrings.Redo, MenuItemType.Standard, Redo), + undoMenuItem = new EditorMenuItem(CommonStrings.Undo, MenuItemType.Standard, Undo) { Hotkey = new Hotkey(PlatformAction.Undo) }, + redoMenuItem = new EditorMenuItem(CommonStrings.Redo, MenuItemType.Standard, Redo) { Hotkey = new Hotkey(PlatformAction.Redo) }, new OsuMenuItemSpacer(), - cutMenuItem = new EditorMenuItem(CommonStrings.Cut, MenuItemType.Standard, Cut), - copyMenuItem = new EditorMenuItem(CommonStrings.Copy, MenuItemType.Standard, Copy), - pasteMenuItem = new EditorMenuItem(CommonStrings.Paste, MenuItemType.Standard, Paste), - cloneMenuItem = new EditorMenuItem(CommonStrings.Clone, MenuItemType.Standard, Clone), + cutMenuItem = new EditorMenuItem(CommonStrings.Cut, MenuItemType.Standard, Cut) { Hotkey = new Hotkey(PlatformAction.Cut) }, + copyMenuItem = new EditorMenuItem(CommonStrings.Copy, MenuItemType.Standard, Copy) { Hotkey = new Hotkey(PlatformAction.Copy) }, + pasteMenuItem = new EditorMenuItem(CommonStrings.Paste, MenuItemType.Standard, Paste) { Hotkey = new Hotkey(PlatformAction.Paste) }, + cloneMenuItem = new EditorMenuItem(CommonStrings.Clone, MenuItemType.Standard, Clone) { Hotkey = new Hotkey(GlobalAction.EditorCloneSelection) }, } }, new MenuItem(CommonStrings.MenuBarView) @@ -1194,7 +1194,7 @@ namespace osu.Game.Screens.Edit yield return new EditorMenuItem(EditorStrings.DeleteDifficulty, MenuItemType.Standard, deleteDifficulty) { Action = { Disabled = Beatmap.Value.BeatmapSetInfo.Beatmaps.Count < 2 } }; yield return new OsuMenuItemSpacer(); - var save = new EditorMenuItem(WebCommonStrings.ButtonsSave, MenuItemType.Standard, () => attemptMutationOperation(Save)); + var save = new EditorMenuItem(WebCommonStrings.ButtonsSave, MenuItemType.Standard, () => attemptMutationOperation(Save)) { Hotkey = new Hotkey(PlatformAction.Save) }; saveRelatedMenuItems.Add(save); yield return save; From 6c07b873af174461888006d87420264b8f349109 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 4 Sep 2024 19:28:07 +0900 Subject: [PATCH 2191/2493] Isolate configuration container from analysis overlay --- .../TestSceneOsuAnalysisContainer.cs | 32 ++++++++--------- .../Configuration/OsuRulesetConfigManager.cs | 4 +-- .../UI/DrawableOsuRuleset.cs | 12 +++++-- .../UI/ReplayAnalysisOverlay.cs | 35 ++++++++----------- .../UI/ReplayAnalysisSettings.cs | 9 ++--- 5 files changed, 47 insertions(+), 45 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuAnalysisContainer.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuAnalysisContainer.cs index fc2255a9cf..04fcb36cad 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuAnalysisContainer.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuAnalysisContainer.cs @@ -5,14 +5,14 @@ using System; using System.Collections.Generic; using System.Linq; using NUnit.Framework; +using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Testing; using osu.Game.Replays; -using osu.Game.Rulesets.Osu.Beatmaps; +using osu.Game.Rulesets.Osu.Configuration; using osu.Game.Rulesets.Osu.Replays; using osu.Game.Rulesets.Osu.UI; using osu.Game.Rulesets.Replays; -using osu.Game.Rulesets.UI; using osu.Game.Tests.Visual; using osuTK; @@ -21,18 +21,20 @@ namespace osu.Game.Rulesets.Osu.Tests public partial class TestSceneOsuAnalysisContainer : OsuTestScene { private TestReplayAnalysisOverlay analysisContainer = null!; + private ReplayAnalysisSettings settings = null!; + + [Cached] + private OsuRulesetConfigManager config = new OsuRulesetConfigManager(null, new OsuRuleset().RulesetInfo); [SetUpSteps] public void SetUpSteps() { AddStep("create analysis container", () => { - DrawableOsuRuleset drawableRuleset = new DrawableOsuRuleset(new OsuRuleset(), new OsuBeatmap()); - Children = new Drawable[] { - drawableRuleset, - analysisContainer = new TestReplayAnalysisOverlay(fabricateReplay(), drawableRuleset), + analysisContainer = new TestReplayAnalysisOverlay(fabricateReplay()), + settings = new ReplayAnalysisSettings(config), }; }); } @@ -40,27 +42,27 @@ namespace osu.Game.Rulesets.Osu.Tests [Test] public void TestHitMarkers() { - AddStep("enable hit markers", () => analysisContainer.Settings.HitMarkersEnabled.Value = true); + AddStep("enable hit markers", () => settings.HitMarkersEnabled.Value = true); AddAssert("hit markers visible", () => analysisContainer.HitMarkersVisible); - AddStep("disable hit markers", () => analysisContainer.Settings.HitMarkersEnabled.Value = false); + AddStep("disable hit markers", () => settings.HitMarkersEnabled.Value = false); AddAssert("hit markers not visible", () => !analysisContainer.HitMarkersVisible); } [Test] public void TestAimMarker() { - AddStep("enable aim markers", () => analysisContainer.Settings.AimMarkersEnabled.Value = true); + AddStep("enable aim markers", () => settings.AimMarkersEnabled.Value = true); AddAssert("aim markers visible", () => analysisContainer.AimMarkersVisible); - AddStep("disable aim markers", () => analysisContainer.Settings.AimMarkersEnabled.Value = false); + AddStep("disable aim markers", () => settings.AimMarkersEnabled.Value = false); AddAssert("aim markers not visible", () => !analysisContainer.AimMarkersVisible); } [Test] public void TestAimLines() { - AddStep("enable aim lines", () => analysisContainer.Settings.AimLinesEnabled.Value = true); + AddStep("enable aim lines", () => settings.AimLinesEnabled.Value = true); AddAssert("aim lines visible", () => analysisContainer.AimLinesVisible); - AddStep("disable aim lines", () => analysisContainer.Settings.AimLinesEnabled.Value = false); + AddStep("disable aim lines", () => settings.AimLinesEnabled.Value = false); AddAssert("aim lines not visible", () => !analysisContainer.AimLinesVisible); } @@ -98,13 +100,11 @@ namespace osu.Game.Rulesets.Osu.Tests private partial class TestReplayAnalysisOverlay : ReplayAnalysisOverlay { - public TestReplayAnalysisOverlay(Replay replay, DrawableRuleset drawableRuleset) - : base(replay, drawableRuleset) + public TestReplayAnalysisOverlay(Replay replay) + : base(replay) { } - public new ReplayAnalysisSettings Settings => base.Settings; - public bool HitMarkersVisible => HitMarkers.Alpha > 0 && HitMarkers.Entries.Any(); public bool AimMarkersVisible => AimMarkers.Alpha > 0 && AimMarkers.Entries.Any(); public bool AimLinesVisible => AimLines.Alpha > 0 && AimLines.Vertices.Count > 1; diff --git a/osu.Game.Rulesets.Osu/Configuration/OsuRulesetConfigManager.cs b/osu.Game.Rulesets.Osu/Configuration/OsuRulesetConfigManager.cs index 23b7b9c1fa..df5cd55c33 100644 --- a/osu.Game.Rulesets.Osu/Configuration/OsuRulesetConfigManager.cs +++ b/osu.Game.Rulesets.Osu/Configuration/OsuRulesetConfigManager.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.Game.Configuration; using osu.Game.Rulesets.Configuration; using osu.Game.Rulesets.UI; @@ -11,7 +9,7 @@ namespace osu.Game.Rulesets.Osu.Configuration { public class OsuRulesetConfigManager : RulesetConfigManager { - public OsuRulesetConfigManager(SettingsStore settings, RulesetInfo ruleset, int? variant = null) + public OsuRulesetConfigManager(SettingsStore? settings, RulesetInfo ruleset, int? variant = null) : base(settings, ruleset, variant) { } diff --git a/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs b/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs index 880b2dbe6f..09dcd54c3c 100644 --- a/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs @@ -5,6 +5,8 @@ using System; using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; using osu.Framework.Input; using osu.Game.Beatmaps; using osu.Game.Input.Handlers; @@ -24,7 +26,7 @@ namespace osu.Game.Rulesets.Osu.UI { public partial class DrawableOsuRuleset : DrawableRuleset { - protected new OsuRulesetConfigManager Config => (OsuRulesetConfigManager)base.Config; + private Bindable? cursorHideEnabled; public new OsuInputManager KeyBindingInputManager => (OsuInputManager)base.KeyBindingInputManager; @@ -39,7 +41,13 @@ namespace osu.Game.Rulesets.Osu.UI private void load(ReplayPlayer? replayPlayer) { if (replayPlayer != null) - PlayfieldAdjustmentContainer.Add(new ReplayAnalysisOverlay(replayPlayer.Score.Replay, this)); + { + PlayfieldAdjustmentContainer.Add(new ReplayAnalysisOverlay(replayPlayer.Score.Replay)); + replayPlayer.AddSettings(new ReplayAnalysisSettings((OsuRulesetConfigManager)Config)); + + cursorHideEnabled = ((OsuRulesetConfigManager)Config).GetBindable(OsuRulesetSetting.ReplayCursorHideEnabled); + cursorHideEnabled.BindValueChanged(enabled => Playfield.Cursor.FadeTo(enabled.NewValue ? 0 : 1), true); + } } public override DrawableHitObject? CreateDrawableRepresentation(OsuHitObject h) => null; diff --git a/osu.Game.Rulesets.Osu/UI/ReplayAnalysisOverlay.cs b/osu.Game.Rulesets.Osu/UI/ReplayAnalysisOverlay.cs index d16f161370..80fb561ed1 100644 --- a/osu.Game.Rulesets.Osu/UI/ReplayAnalysisOverlay.cs +++ b/osu.Game.Rulesets.Osu/UI/ReplayAnalysisOverlay.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Lines; @@ -11,10 +12,9 @@ using osu.Framework.Graphics.Performance; using osu.Framework.Graphics.Pooling; using osu.Game.Replays; using osu.Game.Rulesets.Objects.Pooling; +using osu.Game.Rulesets.Osu.Configuration; using osu.Game.Rulesets.Osu.Replays; using osu.Game.Rulesets.Osu.Skinning.Default; -using osu.Game.Rulesets.UI; -using osu.Game.Screens.Play; using osuTK; using osuTK.Graphics; @@ -22,19 +22,19 @@ namespace osu.Game.Rulesets.Osu.UI { public partial class ReplayAnalysisOverlay : CompositeDrawable { + private BindableBool hitMarkersEnabled { get; } = new BindableBool(); + private BindableBool aimMarkersEnabled { get; } = new BindableBool(); + private BindableBool aimLinesEnabled { get; } = new BindableBool(); + protected readonly HitMarkersContainer HitMarkers; protected readonly AimMarkersContainer AimMarkers; protected readonly AimLinesContainer AimLines; - protected ReplayAnalysisSettings Settings = null!; - private readonly Replay replay; - private readonly DrawableRuleset drawableRuleset; - public ReplayAnalysisOverlay(Replay replay, DrawableRuleset drawableRuleset) + public ReplayAnalysisOverlay(Replay replay) { this.replay = replay; - this.drawableRuleset = drawableRuleset; InternalChildren = new Drawable[] { @@ -45,27 +45,22 @@ namespace osu.Game.Rulesets.Osu.UI } [BackgroundDependencyLoader] - private void load(ReplayPlayer? replayPlayer) + private void load(OsuRulesetConfigManager config) { - Settings = new ReplayAnalysisSettings(); - - if (replayPlayer != null) - replayPlayer.AddSettings(Settings); - else - // only in test - AddInternal(Settings); - loadReplay(); + + config.BindWith(OsuRulesetSetting.ReplayHitMarkersEnabled, hitMarkersEnabled); + config.BindWith(OsuRulesetSetting.ReplayAimMarkersEnabled, aimMarkersEnabled); + config.BindWith(OsuRulesetSetting.ReplayAimLinesEnabled, aimLinesEnabled); } protected override void LoadComplete() { base.LoadComplete(); - Settings.HitMarkersEnabled.BindValueChanged(enabled => HitMarkers.FadeTo(enabled.NewValue ? 1 : 0), true); - Settings.AimMarkersEnabled.BindValueChanged(enabled => AimMarkers.FadeTo(enabled.NewValue ? 1 : 0), true); - Settings.AimLinesEnabled.BindValueChanged(enabled => AimLines.FadeTo(enabled.NewValue ? 1 : 0), true); - Settings.CursorHideEnabled.BindValueChanged(enabled => drawableRuleset.Playfield.Cursor.FadeTo(enabled.NewValue ? 0 : 1), true); + hitMarkersEnabled.BindValueChanged(enabled => HitMarkers.FadeTo(enabled.NewValue ? 1 : 0), true); + aimMarkersEnabled.BindValueChanged(enabled => AimMarkers.FadeTo(enabled.NewValue ? 1 : 0), true); + aimLinesEnabled.BindValueChanged(enabled => AimLines.FadeTo(enabled.NewValue ? 1 : 0), true); } private void loadReplay() diff --git a/osu.Game.Rulesets.Osu/UI/ReplayAnalysisSettings.cs b/osu.Game.Rulesets.Osu/UI/ReplayAnalysisSettings.cs index 87180e155b..dd09ee146b 100644 --- a/osu.Game.Rulesets.Osu/UI/ReplayAnalysisSettings.cs +++ b/osu.Game.Rulesets.Osu/UI/ReplayAnalysisSettings.cs @@ -11,6 +11,8 @@ namespace osu.Game.Rulesets.Osu.UI { public partial class ReplayAnalysisSettings : PlayerSettingsGroup { + private readonly OsuRulesetConfigManager config; + [SettingSource("Hit markers", SettingControlType = typeof(PlayerCheckbox))] public BindableBool HitMarkersEnabled { get; } = new BindableBool(); @@ -23,18 +25,17 @@ namespace osu.Game.Rulesets.Osu.UI [SettingSource("Hide cursor", SettingControlType = typeof(PlayerCheckbox))] public BindableBool CursorHideEnabled { get; } = new BindableBool(); - public ReplayAnalysisSettings() + public ReplayAnalysisSettings(OsuRulesetConfigManager config) : base("Analysis Settings") { + this.config = config; } [BackgroundDependencyLoader] - private void load(IRulesetConfigCache cache) + private void load() { AddRange(this.CreateSettingsControls()); - var config = (OsuRulesetConfigManager)cache.GetConfigFor(new OsuRuleset())!; - config.BindWith(OsuRulesetSetting.ReplayHitMarkersEnabled, HitMarkersEnabled); config.BindWith(OsuRulesetSetting.ReplayAimMarkersEnabled, AimMarkersEnabled); config.BindWith(OsuRulesetSetting.ReplayAimLinesEnabled, AimLinesEnabled); From 6f99d839b05602285a9ede38d00666a9db31e9d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 4 Sep 2024 12:41:02 +0200 Subject: [PATCH 2192/2493] Make struct readonly --- osu.Game/Graphics/UserInterface/Hotkey.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Graphics/UserInterface/Hotkey.cs b/osu.Game/Graphics/UserInterface/Hotkey.cs index c4c0eb63c1..0b5176a02e 100644 --- a/osu.Game/Graphics/UserInterface/Hotkey.cs +++ b/osu.Game/Graphics/UserInterface/Hotkey.cs @@ -11,7 +11,7 @@ using osu.Game.Input.Bindings; namespace osu.Game.Graphics.UserInterface { - public struct Hotkey + public readonly struct Hotkey { public KeyCombination[]? KeyCombinations { get; init; } public GlobalAction? GlobalAction { get; init; } From abd74ab41cb8e2b6b34c0786bc88105f0b4378f8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 4 Sep 2024 19:41:40 +0900 Subject: [PATCH 2193/2493] Add note about being able to apply a newer update during runtime --- osu.Desktop/Updater/VelopackUpdateManager.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Desktop/Updater/VelopackUpdateManager.cs b/osu.Desktop/Updater/VelopackUpdateManager.cs index 527892413a..e550755fff 100644 --- a/osu.Desktop/Updater/VelopackUpdateManager.cs +++ b/osu.Desktop/Updater/VelopackUpdateManager.cs @@ -54,6 +54,8 @@ namespace osu.Desktop.Updater if (localUserInfo?.IsPlaying.Value == true) return false; + // TODO: we should probably be checking if there's a more recent update, rather than shortcutting here. + // Velopack does support this scenario (see https://github.com/ppy/osu/pull/28743#discussion_r1743495975). if (pendingUpdate != null) { // If there is an update pending restart, show the notification to restart again. From 6a309725ed62370accbf45784d15a57af52c00d1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 4 Sep 2024 19:50:45 +0900 Subject: [PATCH 2194/2493] Make test more usable --- .../TestSceneOsuAnalysisContainer.cs | 24 ++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuAnalysisContainer.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuAnalysisContainer.cs index 04fcb36cad..d31b27e00d 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuAnalysisContainer.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuAnalysisContainer.cs @@ -33,9 +33,27 @@ namespace osu.Game.Rulesets.Osu.Tests { Children = new Drawable[] { - analysisContainer = new TestReplayAnalysisOverlay(fabricateReplay()), + new OsuPlayfieldAdjustmentContainer + { + Child = analysisContainer = new TestReplayAnalysisOverlay(fabricateReplay()), + }, settings = new ReplayAnalysisSettings(config), }; + + settings.HitMarkersEnabled.Value = false; + settings.AimMarkersEnabled.Value = false; + settings.AimLinesEnabled.Value = false; + }); + } + + [Test] + public void TestEverythingOn() + { + AddStep("enable everything", () => + { + settings.HitMarkersEnabled.Value = true; + settings.AimMarkersEnabled.Value = true; + settings.AimLinesEnabled.Value = true; }); } @@ -76,8 +94,8 @@ namespace osu.Game.Rulesets.Osu.Tests for (int i = 0; i < 1000; i++) { - posX = Math.Clamp(posX + random.Next(-10, 11), 0, 500); - posY = Math.Clamp(posY + random.Next(-10, 11), 0, 500); + posX = Math.Clamp(posX + random.Next(-20, 21), 0, 500); + posY = Math.Clamp(posY + random.Next(-20, 21), 0, 500); var actions = new List(); From b7a56c8a4587fd197624c27ef8fba367edc8bebf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 4 Sep 2024 13:57:53 +0200 Subject: [PATCH 2195/2493] Implement "form" text box control --- .../UserInterface/TestSceneFormControls.cs | 61 +++++ .../UserInterface/ThemeComparisonTestScene.cs | 2 +- .../UserInterfaceV2/FormFieldCaption.cs | 68 +++++ .../Graphics/UserInterfaceV2/FormNumberBox.cs | 23 ++ .../Graphics/UserInterfaceV2/FormTextBox.cs | 238 ++++++++++++++++++ 5 files changed, 391 insertions(+), 1 deletion(-) create mode 100644 osu.Game.Tests/Visual/UserInterface/TestSceneFormControls.cs create mode 100644 osu.Game/Graphics/UserInterfaceV2/FormFieldCaption.cs create mode 100644 osu.Game/Graphics/UserInterfaceV2/FormNumberBox.cs create mode 100644 osu.Game/Graphics/UserInterfaceV2/FormTextBox.cs diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneFormControls.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneFormControls.cs new file mode 100644 index 0000000000..f5bc40c869 --- /dev/null +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneFormControls.cs @@ -0,0 +1,61 @@ +// 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.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; +using osu.Game.Graphics.Cursor; +using osu.Game.Graphics.UserInterfaceV2; +using osuTK; + +namespace osu.Game.Tests.Visual.UserInterface +{ + public partial class TestSceneFormControls : ThemeComparisonTestScene + { + public TestSceneFormControls() + : base(false) + { + } + + protected override Drawable CreateContent() => new OsuContextMenuContainer + { + RelativeSizeAxes = Axes.Both, + Child = new PopoverContainer + { + RelativeSizeAxes = Axes.Both, + Child = new FillFlowContainer + { + RelativeSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Spacing = new Vector2(5), + Padding = new MarginPadding(10), + Children = new Drawable[] + { + new FormTextBox + { + Caption = "Artist", + HintText = "Poot artist here!", + PlaceholderText = "Here is an artist", + TabbableContentContainer = this, + }, + new FormTextBox + { + Caption = "Artist", + HintText = "Poot artist here!", + PlaceholderText = "Here is an artist", + Current = { Disabled = true }, + TabbableContentContainer = this, + }, + new FormNumberBox + { + Caption = "Number", + HintText = "Insert your favourite number", + PlaceholderText = "Mine is 42!", + TabbableContentContainer = this, + }, + }, + }, + }, + }; + } +} diff --git a/osu.Game.Tests/Visual/UserInterface/ThemeComparisonTestScene.cs b/osu.Game.Tests/Visual/UserInterface/ThemeComparisonTestScene.cs index 4700ef72d9..44133d89f8 100644 --- a/osu.Game.Tests/Visual/UserInterface/ThemeComparisonTestScene.cs +++ b/osu.Game.Tests/Visual/UserInterface/ThemeComparisonTestScene.cs @@ -75,7 +75,7 @@ namespace osu.Game.Tests.Visual.UserInterface new Box { RelativeSizeAxes = Axes.Both, - Colour = colourProvider.Background4 + Colour = colourProvider.Background3 }, CreateContent() } diff --git a/osu.Game/Graphics/UserInterfaceV2/FormFieldCaption.cs b/osu.Game/Graphics/UserInterfaceV2/FormFieldCaption.cs new file mode 100644 index 0000000000..75c27618e9 --- /dev/null +++ b/osu.Game/Graphics/UserInterfaceV2/FormFieldCaption.cs @@ -0,0 +1,68 @@ +// 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.Extensions.ObjectExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Localisation; +using osu.Game.Graphics.Sprites; +using osuTK; + +namespace osu.Game.Graphics.UserInterfaceV2 +{ + public partial class FormFieldCaption : CompositeDrawable, IHasTooltip + { + private LocalisableString caption; + + public LocalisableString Caption + { + get => caption; + set + { + caption = value; + + if (captionText.IsNotNull()) + captionText.Text = value; + } + } + + private OsuSpriteText captionText = null!; + + public LocalisableString TooltipText { get; set; } + + [BackgroundDependencyLoader] + private void load() + { + AutoSizeAxes = Axes.Both; + + InternalChild = new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(5), + Children = new Drawable[] + { + captionText = new OsuSpriteText + { + Text = caption, + Font = OsuFont.Default.With(size: 12, weight: FontWeight.SemiBold), + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + }, + new SpriteIcon + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Alpha = TooltipText == default ? 0 : 1, + Size = new Vector2(10), + Icon = FontAwesome.Solid.QuestionCircle, + Margin = new MarginPadding { Top = 1, }, + } + }, + }; + } + } +} diff --git a/osu.Game/Graphics/UserInterfaceV2/FormNumberBox.cs b/osu.Game/Graphics/UserInterfaceV2/FormNumberBox.cs new file mode 100644 index 0000000000..66f1a45210 --- /dev/null +++ b/osu.Game/Graphics/UserInterfaceV2/FormNumberBox.cs @@ -0,0 +1,23 @@ +// 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.Graphics.UserInterfaceV2 +{ + public partial class FormNumberBox : FormTextBox + { + public bool AllowDecimals { get; init; } + + internal override InnerTextBox CreateTextBox() => new InnerNumberBox + { + AllowDecimals = AllowDecimals, + }; + + internal partial class InnerNumberBox : InnerTextBox + { + public bool AllowDecimals { get; init; } + + protected override bool CanAddCharacter(char character) + => char.IsAsciiDigit(character) || (AllowDecimals && character == '.'); + } + } +} diff --git a/osu.Game/Graphics/UserInterfaceV2/FormTextBox.cs b/osu.Game/Graphics/UserInterfaceV2/FormTextBox.cs new file mode 100644 index 0000000000..985eb74662 --- /dev/null +++ b/osu.Game/Graphics/UserInterfaceV2/FormTextBox.cs @@ -0,0 +1,238 @@ +// 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.Extensions.Color4Extensions; +using osu.Framework.Extensions.ObjectExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.UserInterface; +using osu.Framework.Input; +using osu.Framework.Input.Events; +using osu.Framework.Localisation; +using osu.Game.Graphics.UserInterface; +using osu.Game.Overlays; + +namespace osu.Game.Graphics.UserInterfaceV2 +{ + public partial class FormTextBox : CompositeDrawable, IHasCurrentValue + { + public Bindable Current + { + get => current.Current; + set => current.Current = value; + } + + private bool readOnly; + + public bool ReadOnly + { + get => readOnly; + set + { + readOnly = value; + + if (textBox.IsNotNull()) + updateState(); + } + } + + private CompositeDrawable? tabbableContentContainer; + + public CompositeDrawable? TabbableContentContainer + { + set + { + tabbableContentContainer = value; + + if (textBox.IsNotNull()) + textBox.TabbableContentContainer = tabbableContentContainer; + } + } + + public event TextBox.OnCommitHandler? OnCommit; + + private readonly BindableWithCurrent current = new BindableWithCurrent(); + + public LocalisableString Caption { get; init; } + public LocalisableString HintText { get; init; } + public LocalisableString PlaceholderText { get; init; } + + private Box background = null!; + private Box flashLayer = null!; + private InnerTextBox textBox = null!; + private FormFieldCaption caption = null!; + private IFocusManager focusManager = null!; + + [Resolved] + private OverlayColourProvider colourProvider { get; set; } = null!; + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + RelativeSizeAxes = Axes.X; + Height = 50; + + Masking = true; + CornerRadius = 5; + + InternalChildren = new Drawable[] + { + background = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background5, + }, + flashLayer = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Colour4.Transparent, + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding(9), + Children = new Drawable[] + { + caption = new FormFieldCaption + { + Anchor = Anchor.TopLeft, + Origin = Anchor.TopLeft, + Caption = Caption, + TooltipText = HintText, + }, + textBox = CreateTextBox().With(t => + { + t.Anchor = Anchor.BottomRight; + t.Origin = Anchor.BottomRight; + t.RelativeSizeAxes = Axes.X; + t.Width = 1; + t.PlaceholderText = PlaceholderText; + t.Current = Current; + t.CommitOnFocusLost = true; + t.OnCommit += (textBox, newText) => + { + OnCommit?.Invoke(textBox, newText); + + if (!current.Disabled && !ReadOnly) + { + flashLayer.Colour = ColourInfo.GradientVertical(colourProvider.Dark1.Opacity(0), colourProvider.Dark2); + flashLayer.FadeOutFromOne(800, Easing.OutQuint); + } + }; + t.OnInputError = () => + { + flashLayer.Colour = ColourInfo.GradientVertical(colours.Red3.Opacity(0), colours.Red3); + flashLayer.FadeOutFromOne(200, Easing.OutQuint); + }; + t.TabbableContentContainer = tabbableContentContainer; + }), + }, + }, + }; + } + + internal virtual InnerTextBox CreateTextBox() => new InnerTextBox(); + + protected override void LoadComplete() + { + base.LoadComplete(); + + focusManager = GetContainingFocusManager()!; + textBox.Focused.BindValueChanged(_ => updateState()); + current.BindDisabledChanged(_ => updateState(), true); + } + + protected override bool OnHover(HoverEvent e) + { + updateState(); + return true; + } + + protected override void OnHoverLost(HoverLostEvent e) + { + base.OnHoverLost(e); + updateState(); + } + + protected override bool OnClick(ClickEvent e) + { + focusManager.ChangeFocus(textBox); + return true; + } + + private void updateState() + { + bool disabled = Current.Disabled || ReadOnly; + + textBox.ReadOnly = disabled; + textBox.Alpha = 1; + + caption.Colour = disabled ? colourProvider.Foreground1 : colourProvider.Content2; + textBox.Colour = disabled ? colourProvider.Foreground1 : colourProvider.Content1; + + if (!disabled) + { + BorderThickness = IsHovered || textBox.Focused.Value ? 3 : 0; + BorderColour = textBox.Focused.Value ? colourProvider.Highlight1 : colourProvider.Light4; + + if (textBox.Focused.Value) + background.Colour = ColourInfo.GradientVertical(colourProvider.Background5, colourProvider.Dark3); + else if (IsHovered) + background.Colour = ColourInfo.GradientVertical(colourProvider.Background5, colourProvider.Dark4); + else + background.Colour = colourProvider.Background5; + } + else + { + background.Colour = colourProvider.Background4; + } + } + + internal partial class InnerTextBox : OsuTextBox + { + public BindableBool Focused { get; } = new BindableBool(); + + public Action? OnInputError { get; set; } + + protected override float LeftRightPadding => 0; + + [BackgroundDependencyLoader] + private void load() + { + Height = 16; + TextContainer.Height = 1; + Masking = false; + BackgroundUnfocused = BackgroundFocused = BackgroundCommit = Colour4.Transparent; + } + + protected override SpriteText CreatePlaceholder() => base.CreatePlaceholder().With(t => t.Margin = default); + + protected override void OnFocus(FocusEvent e) + { + base.OnFocus(e); + + Focused.Value = true; + } + + protected override void OnFocusLost(FocusLostEvent e) + { + base.OnFocusLost(e); + + Focused.Value = false; + } + + protected override void NotifyInputError() + { + PlayFeedbackSample(FeedbackSampleType.TextInvalid); + // base call intentionally suppressed + OnInputError?.Invoke(); + } + } + } +} From 17760afa6023a9eca18051b4a1e2f22ed0f131aa Mon Sep 17 00:00:00 2001 From: Fabep Date: Wed, 4 Sep 2024 15:29:48 +0200 Subject: [PATCH 2196/2493] Changed ModCustomisationHeader to inherit from OsuClickableContainer. ModCustomisationHeader changes color depending on state. --- .../Overlays/Mods/ModCustomisationHeader.cs | 43 +++++++++++++++---- 1 file changed, 35 insertions(+), 8 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModCustomisationHeader.cs b/osu.Game/Overlays/Mods/ModCustomisationHeader.cs index 32fd5a37aa..1d40fb3f5c 100644 --- a/osu.Game/Overlays/Mods/ModCustomisationHeader.cs +++ b/osu.Game/Overlays/Mods/ModCustomisationHeader.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Collections.Generic; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; @@ -20,17 +19,16 @@ using static osu.Game.Overlays.Mods.ModCustomisationPanel; namespace osu.Game.Overlays.Mods { - public partial class ModCustomisationHeader : OsuHoverContainer + public partial class ModCustomisationHeader : OsuClickableContainer { private Box background = null!; + private Box hoverBackground = null!; private Box backgroundFlash = null!; private SpriteIcon icon = null!; [Resolved] private OverlayColourProvider colourProvider { get; set; } = null!; - protected override IEnumerable EffectTargets => new[] { background }; - public readonly Bindable ExpandedState = new Bindable(ModCustomisationPanelState.Collapsed); private readonly ModCustomisationPanel panel; @@ -53,6 +51,13 @@ namespace osu.Game.Overlays.Mods { RelativeSizeAxes = Axes.Both, }, + hoverBackground = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = OsuColour.Gray(80).Opacity(180), + Blending = BlendingParameters.Additive, + Alpha = 0, + }, backgroundFlash = new Box { RelativeSizeAxes = Axes.Both, @@ -84,9 +89,6 @@ namespace osu.Game.Overlays.Mods } } }; - - IdleColour = colourProvider.Dark3; - HoverColour = colourProvider.Light4; } protected override void LoadComplete() @@ -109,15 +111,40 @@ namespace osu.Game.Overlays.Mods ExpandedState.BindValueChanged(v => { icon.ScaleTo(v.NewValue > ModCustomisationPanelState.Collapsed ? new Vector2(1, -1) : Vector2.One, 300, Easing.OutQuint); + + switch (v.NewValue) + { + case ModCustomisationPanelState.Collapsed: + background.FadeColour(colourProvider.Dark3, 500, Easing.OutQuint); + break; + + case ModCustomisationPanelState.Expanded: + case ModCustomisationPanelState.ExpandedByMod: + background.FadeColour(colourProvider.Light4, 500, Easing.OutQuint); + break; + } }, true); } protected override bool OnHover(HoverEvent e) { - if (Enabled.Value && panel.ExpandedState.Value == ModCustomisationPanelState.Collapsed) + if (!Enabled.Value) + return base.OnHover(e); + + if (panel.ExpandedState.Value == ModCustomisationPanelState.Collapsed) panel.ExpandedState.Value = ModCustomisationPanelState.Expanded; + hoverBackground.FadeIn(200); + return base.OnHover(e); } + + protected override void OnHoverLost(HoverLostEvent e) + { + if (Enabled.Value) + hoverBackground.FadeOut(200); + + base.OnHoverLost(e); + } } } From 6fc60908c01816a986792e31918de381944342e3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 5 Sep 2024 01:00:23 +0900 Subject: [PATCH 2197/2493] Trigger request failure on receiving a null response for a typed `APIRequest` --- osu.Game/Online/API/APIRequest.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/osu.Game/Online/API/APIRequest.cs b/osu.Game/Online/API/APIRequest.cs index 45ebbcd76d..5cbe9040ba 100644 --- a/osu.Game/Online/API/APIRequest.cs +++ b/osu.Game/Online/API/APIRequest.cs @@ -46,6 +46,9 @@ namespace osu.Game.Online.API Response = ((OsuJsonWebRequest)WebRequest).ResponseObject; Logger.Log($"{GetType().ReadableName()} finished with response size of {WebRequest.ResponseStream.Length:#,0} bytes", LoggingTarget.Network); } + + if (Response == null) + TriggerFailure(new ArgumentNullException(nameof(Response))); } internal void TriggerSuccess(T result) @@ -152,6 +155,8 @@ namespace osu.Game.Online.API PostProcess(); + if (isFailing) return; + TriggerSuccess(); } From dcb463acafddefe68c2ed90fab193f599c9458f5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 4 Sep 2024 20:07:31 +0900 Subject: [PATCH 2198/2493] Split out classes and avoid weird configuration stuff --- .../Skinning/Default/HitMarker.cs | 74 -------- .../UI/ReplayAnalysis/AimLinesContainer.cs | 70 ++++++++ .../UI/ReplayAnalysis/AimMarkersContainer.cs | 20 +++ .../UI/ReplayAnalysis/AimPointEntry.cs | 20 +++ .../UI/ReplayAnalysis/HitMarker.cs | 27 +++ .../UI/ReplayAnalysis/HitMarkerEntry.cs | 18 ++ .../UI/ReplayAnalysis/HitMarkerLeftClick.cs | 49 ++++++ .../UI/ReplayAnalysis/HitMarkerMovement.cs | 50 ++++++ .../UI/ReplayAnalysis/HitMarkerRightClick.cs | 49 ++++++ .../UI/ReplayAnalysis/HitMarkersContainer.cs | 28 +++ .../UI/ReplayAnalysisOverlay.cs | 160 +----------------- 11 files changed, 334 insertions(+), 231 deletions(-) delete mode 100644 osu.Game.Rulesets.Osu/Skinning/Default/HitMarker.cs create mode 100644 osu.Game.Rulesets.Osu/UI/ReplayAnalysis/AimLinesContainer.cs create mode 100644 osu.Game.Rulesets.Osu/UI/ReplayAnalysis/AimMarkersContainer.cs create mode 100644 osu.Game.Rulesets.Osu/UI/ReplayAnalysis/AimPointEntry.cs create mode 100644 osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarker.cs create mode 100644 osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarkerEntry.cs create mode 100644 osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarkerLeftClick.cs create mode 100644 osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarkerMovement.cs create mode 100644 osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarkerRightClick.cs create mode 100644 osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarkersContainer.cs diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/HitMarker.cs b/osu.Game.Rulesets.Osu/Skinning/Default/HitMarker.cs deleted file mode 100644 index fe662470bc..0000000000 --- a/osu.Game.Rulesets.Osu/Skinning/Default/HitMarker.cs +++ /dev/null @@ -1,74 +0,0 @@ -// 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.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; -using osuTK; - -namespace osu.Game.Rulesets.Osu.Skinning.Default -{ - public partial class HitMarker : CompositeDrawable - { - public HitMarker(OsuAction? action) - { - var (colour, length, hasBorder) = getConfig(action); - - if (hasBorder) - { - InternalChildren = new Drawable[] - { - new Box - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Size = new Vector2(3, length), - Colour = Colour4.Black.Opacity(0.5F) - }, - new Box - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Size = new Vector2(3, length), - Rotation = 90, - Colour = Colour4.Black.Opacity(0.5F) - } - }; - } - - AddRangeInternal(new Drawable[] - { - new Box - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Size = new Vector2(1, length), - Colour = colour - }, - new Box - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Size = new Vector2(1, length), - Rotation = 90, - Colour = colour - } - }); - } - - private (Colour4 colour, float length, bool hasBorder) getConfig(OsuAction? action) - { - switch (action) - { - case OsuAction.LeftButton: - return (Colour4.Orange, 20, true); - - case OsuAction.RightButton: - return (Colour4.LightGreen, 20, true); - - default: - return (Colour4.Gray.Opacity(0.5F), 8, false); - } - } - } -} diff --git a/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/AimLinesContainer.cs b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/AimLinesContainer.cs new file mode 100644 index 0000000000..db747e2775 --- /dev/null +++ b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/AimLinesContainer.cs @@ -0,0 +1,70 @@ +// 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 osu.Framework.Graphics.Lines; +using osu.Framework.Graphics.Performance; +using osuTK.Graphics; + +namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis +{ + public partial class AimLinesContainer : Path + { + private readonly LifetimeEntryManager lifetimeManager = new LifetimeEntryManager(); + private readonly SortedSet aliveEntries = new SortedSet(new AimLinePointComparator()); + + public AimLinesContainer() + { + lifetimeManager.EntryBecameAlive += entryBecameAlive; + lifetimeManager.EntryBecameDead += entryBecameDead; + + PathRadius = 1f; + Colour = new Color4(255, 255, 255, 127); + } + + protected override void Update() + { + base.Update(); + + lifetimeManager.Update(Time.Current); + } + + public void Add(AimPointEntry entry) => lifetimeManager.AddEntry(entry); + + public void Clear() => lifetimeManager.ClearEntries(); + + private void entryBecameAlive(LifetimeEntry entry) + { + aliveEntries.Add((AimPointEntry)entry); + updateVertices(); + } + + private void entryBecameDead(LifetimeEntry entry) + { + aliveEntries.Remove((AimPointEntry)entry); + updateVertices(); + } + + private void updateVertices() + { + ClearVertices(); + + foreach (var entry in aliveEntries) + { + AddVertex(entry.Position); + } + } + + private sealed class AimLinePointComparator : IComparer + { + public int Compare(AimPointEntry? x, AimPointEntry? y) + { + ArgumentNullException.ThrowIfNull(x); + ArgumentNullException.ThrowIfNull(y); + + return x.LifetimeStart.CompareTo(y.LifetimeStart); + } + } + } +} diff --git a/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/AimMarkersContainer.cs b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/AimMarkersContainer.cs new file mode 100644 index 0000000000..76e88ad5b0 --- /dev/null +++ b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/AimMarkersContainer.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.Graphics.Pooling; +using osu.Game.Rulesets.Objects.Pooling; + +namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis +{ + public partial class AimMarkersContainer : PooledDrawableWithLifetimeContainer + { + private readonly DrawablePool pool; + + public AimMarkersContainer() + { + AddInternal(pool = new DrawablePool(80)); + } + + protected override HitMarker GetDrawable(AimPointEntry entry) => pool.Get(d => d.Apply(entry)); + } +} diff --git a/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/AimPointEntry.cs b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/AimPointEntry.cs new file mode 100644 index 0000000000..948568eb12 --- /dev/null +++ b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/AimPointEntry.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.Graphics.Performance; +using osuTK; + +namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis +{ + public partial class AimPointEntry : LifetimeEntry + { + public Vector2 Position { get; } + + public AimPointEntry(double time, Vector2 position) + { + LifetimeStart = time; + LifetimeEnd = time + 1_000; + Position = position; + } + } +} diff --git a/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarker.cs b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarker.cs new file mode 100644 index 0000000000..339bdae5da --- /dev/null +++ b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarker.cs @@ -0,0 +1,27 @@ +// 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.Graphics; +using osu.Game.Rulesets.Objects.Pooling; + +namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis +{ + public partial class HitMarker : PoolableDrawableWithLifetime + { + public HitMarker() + { + Origin = Anchor.Centre; + } + + protected override void OnApply(AimPointEntry entry) + { + Position = entry.Position; + + using (BeginAbsoluteSequence(LifetimeStart)) + Show(); + + using (BeginAbsoluteSequence(LifetimeEnd - 200)) + this.FadeOut(200); + } + } +} diff --git a/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarkerEntry.cs b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarkerEntry.cs new file mode 100644 index 0000000000..80c268910d --- /dev/null +++ b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarkerEntry.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 osuTK; + +namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis +{ + public partial class HitMarkerEntry : AimPointEntry + { + public bool IsLeftMarker { get; } + + public HitMarkerEntry(double lifetimeStart, Vector2 position, bool isLeftMarker) + : base(lifetimeStart, position) + { + IsLeftMarker = isLeftMarker; + } + } +} diff --git a/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarkerLeftClick.cs b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarkerLeftClick.cs new file mode 100644 index 0000000000..988fc28371 --- /dev/null +++ b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarkerLeftClick.cs @@ -0,0 +1,49 @@ +using osu.Framework.Graphics; +using osu.Framework.Graphics.Shapes; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis +{ + public partial class HitMarkerLeftClick : HitMarker + { + public HitMarkerLeftClick() + { + const float length = 20; + + Colour = Color4.OrangeRed; + + InternalChildren = new Drawable[] + { + new Box + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(3, length), + Colour = Colour4.Black.Opacity(0.5F) + }, + new Box + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(3, length), + Rotation = 90, + Colour = Colour4.Black.Opacity(0.5F) + }, + new Box + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(1, length), + }, + new Box + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(1, length), + Rotation = 90, + } + }; + } + } +} diff --git a/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarkerMovement.cs b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarkerMovement.cs new file mode 100644 index 0000000000..4f9b6f8790 --- /dev/null +++ b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarkerMovement.cs @@ -0,0 +1,50 @@ +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Shapes; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis +{ + public partial class HitMarkerMovement : HitMarker + { + public HitMarkerMovement() + { + const float length = 5; + + Colour = Color4.Gray.Opacity(0.4f); + + InternalChildren = new Drawable[] + { + new Box + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(3, length), + Colour = Colour4.Black.Opacity(0.5F) + }, + new Box + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(3, length), + Rotation = 90, + Colour = Colour4.Black.Opacity(0.5F) + }, + new Box + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(1, length), + }, + new Box + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(1, length), + Rotation = 90, + } + }; + } + } +} diff --git a/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarkerRightClick.cs b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarkerRightClick.cs new file mode 100644 index 0000000000..32cdd2d0b5 --- /dev/null +++ b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarkerRightClick.cs @@ -0,0 +1,49 @@ +using osu.Framework.Graphics; +using osu.Framework.Graphics.Shapes; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis +{ + public partial class HitMarkerRightClick : HitMarker + { + public HitMarkerRightClick() + { + const float length = 20; + + Colour = Color4.GreenYellow; + + InternalChildren = new Drawable[] + { + new Box + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(3, length), + Colour = Colour4.Black.Opacity(0.5F) + }, + new Box + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(3, length), + Rotation = 90, + Colour = Colour4.Black.Opacity(0.5F) + }, + new Box + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(1, length), + }, + new Box + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(1, length), + Rotation = 90, + } + }; + } + } +} diff --git a/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarkersContainer.cs b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarkersContainer.cs new file mode 100644 index 0000000000..fcc5fa28c2 --- /dev/null +++ b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarkersContainer.cs @@ -0,0 +1,28 @@ +// 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.Graphics.Pooling; +using osu.Game.Rulesets.Objects.Pooling; + +namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis +{ + public partial class HitMarkersContainer : PooledDrawableWithLifetimeContainer + { + private readonly DrawablePool leftPool; + private readonly DrawablePool rightPool; + + public HitMarkersContainer() + { + AddInternal(leftPool = new DrawablePool(15)); + AddInternal(rightPool = new DrawablePool(15)); + } + + protected override HitMarker GetDrawable(HitMarkerEntry entry) + { + if (entry.IsLeftMarker) + return leftPool.Get(d => d.Apply(entry)); + + return rightPool.Get(d => d.Apply(entry)); + } + } +} diff --git a/osu.Game.Rulesets.Osu/UI/ReplayAnalysisOverlay.cs b/osu.Game.Rulesets.Osu/UI/ReplayAnalysisOverlay.cs index 80fb561ed1..2cc1948474 100644 --- a/osu.Game.Rulesets.Osu/UI/ReplayAnalysisOverlay.cs +++ b/osu.Game.Rulesets.Osu/UI/ReplayAnalysisOverlay.cs @@ -1,22 +1,14 @@ // 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 osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Lines; -using osu.Framework.Graphics.Performance; -using osu.Framework.Graphics.Pooling; using osu.Game.Replays; -using osu.Game.Rulesets.Objects.Pooling; using osu.Game.Rulesets.Osu.Configuration; using osu.Game.Rulesets.Osu.Replays; -using osu.Game.Rulesets.Osu.Skinning.Default; -using osuTK; -using osuTK.Graphics; +using osu.Game.Rulesets.Osu.UI.ReplayAnalysis; namespace osu.Game.Rulesets.Osu.UI { @@ -34,6 +26,8 @@ namespace osu.Game.Rulesets.Osu.UI public ReplayAnalysisOverlay(Replay replay) { + RelativeSizeAxes = Axes.Both; + this.replay = replay; InternalChildren = new Drawable[] @@ -95,153 +89,5 @@ namespace osu.Game.Rulesets.Osu.UI } } } - - protected partial class HitMarkersContainer : PooledDrawableWithLifetimeContainer - { - private readonly HitMarkerPool leftPool; - private readonly HitMarkerPool rightPool; - - public HitMarkersContainer() - { - AddInternal(leftPool = new HitMarkerPool(OsuAction.LeftButton, 15)); - AddInternal(rightPool = new HitMarkerPool(OsuAction.RightButton, 15)); - } - - protected override HitMarkerDrawable GetDrawable(HitMarkerEntry entry) => (entry.IsLeftMarker ? leftPool : rightPool).Get(d => d.Apply(entry)); - } - - protected partial class AimMarkersContainer : PooledDrawableWithLifetimeContainer - { - private readonly HitMarkerPool pool; - - public AimMarkersContainer() - { - AddInternal(pool = new HitMarkerPool(null, 80)); - } - - protected override HitMarkerDrawable GetDrawable(AimPointEntry entry) => pool.Get(d => d.Apply(entry)); - } - - protected partial class AimLinesContainer : Path - { - private readonly LifetimeEntryManager lifetimeManager = new LifetimeEntryManager(); - private readonly SortedSet aliveEntries = new SortedSet(new AimLinePointComparator()); - - public AimLinesContainer() - { - lifetimeManager.EntryBecameAlive += entryBecameAlive; - lifetimeManager.EntryBecameDead += entryBecameDead; - - PathRadius = 1f; - Colour = new Color4(255, 255, 255, 127); - } - - protected override void Update() - { - base.Update(); - - lifetimeManager.Update(Time.Current); - } - - public void Add(AimPointEntry entry) => lifetimeManager.AddEntry(entry); - - public void Clear() => lifetimeManager.ClearEntries(); - - private void entryBecameAlive(LifetimeEntry entry) - { - aliveEntries.Add((AimPointEntry)entry); - updateVertices(); - } - - private void entryBecameDead(LifetimeEntry entry) - { - aliveEntries.Remove((AimPointEntry)entry); - updateVertices(); - } - - private void updateVertices() - { - ClearVertices(); - - foreach (var entry in aliveEntries) - { - AddVertex(entry.Position); - } - } - - private sealed class AimLinePointComparator : IComparer - { - public int Compare(AimPointEntry? x, AimPointEntry? y) - { - ArgumentNullException.ThrowIfNull(x); - ArgumentNullException.ThrowIfNull(y); - - return x.LifetimeStart.CompareTo(y.LifetimeStart); - } - } - } - - protected partial class HitMarkerDrawable : PoolableDrawableWithLifetime - { - /// - /// This constructor only exists to meet the new() type constraint of . - /// - public HitMarkerDrawable() - { - } - - public HitMarkerDrawable(OsuAction? action) - { - Origin = Anchor.Centre; - InternalChild = new HitMarker(action); - } - - protected override void OnApply(AimPointEntry entry) - { - Position = entry.Position; - - using (BeginAbsoluteSequence(LifetimeStart)) - Show(); - - using (BeginAbsoluteSequence(LifetimeEnd - 200)) - this.FadeOut(200); - } - } - - protected partial class HitMarkerPool : DrawablePool - { - private readonly OsuAction? action; - - public HitMarkerPool(OsuAction? action, int initialSize) - : base(initialSize) - { - this.action = action; - } - - protected override HitMarkerDrawable CreateNewDrawable() => new HitMarkerDrawable(action); - } - - protected partial class AimPointEntry : LifetimeEntry - { - public Vector2 Position { get; } - - public AimPointEntry(double time, Vector2 position) - { - LifetimeStart = time; - LifetimeEnd = time + 1_000; - Position = position; - } - } - - protected partial class HitMarkerEntry : AimPointEntry - { - public bool IsLeftMarker { get; } - - public HitMarkerEntry(double lifetimeStart, Vector2 position, bool isLeftMarker) - : base(lifetimeStart, position) - { - IsLeftMarker = isLeftMarker; - } - } } } From 7f9a98a7aa01307c15ee48d3d74408482b58d6a1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 4 Sep 2024 20:17:22 +0900 Subject: [PATCH 2199/2493] More renames --- .../TestSceneOsuAnalysisContainer.cs | 6 ++--- ...rsContainer.cs => ClickMarkerContainer.cs} | 4 +-- ...ontainer.cs => MovementMarkerContainer.cs} | 4 +-- ...sContainer.cs => MovementPathContainer.cs} | 6 ++--- .../UI/ReplayAnalysisOverlay.cs | 26 +++++++++---------- 5 files changed, 22 insertions(+), 24 deletions(-) rename osu.Game.Rulesets.Osu/UI/ReplayAnalysis/{HitMarkersContainer.cs => ClickMarkerContainer.cs} (85%) rename osu.Game.Rulesets.Osu/UI/ReplayAnalysis/{AimMarkersContainer.cs => MovementMarkerContainer.cs} (78%) rename osu.Game.Rulesets.Osu/UI/ReplayAnalysis/{AimLinesContainer.cs => MovementPathContainer.cs} (92%) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuAnalysisContainer.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuAnalysisContainer.cs index d31b27e00d..fb6d59ddba 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuAnalysisContainer.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuAnalysisContainer.cs @@ -123,9 +123,9 @@ namespace osu.Game.Rulesets.Osu.Tests { } - public bool HitMarkersVisible => HitMarkers.Alpha > 0 && HitMarkers.Entries.Any(); - public bool AimMarkersVisible => AimMarkers.Alpha > 0 && AimMarkers.Entries.Any(); - public bool AimLinesVisible => AimLines.Alpha > 0 && AimLines.Vertices.Count > 1; + public bool HitMarkersVisible => ClickMarkers.Alpha > 0 && ClickMarkers.Entries.Any(); + public bool AimMarkersVisible => MovementMarkers.Alpha > 0 && MovementMarkers.Entries.Any(); + public bool AimLinesVisible => MovementPath.Alpha > 0 && MovementPath.Vertices.Count > 1; } } } diff --git a/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarkersContainer.cs b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/ClickMarkerContainer.cs similarity index 85% rename from osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarkersContainer.cs rename to osu.Game.Rulesets.Osu/UI/ReplayAnalysis/ClickMarkerContainer.cs index fcc5fa28c2..be56e26caf 100644 --- a/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarkersContainer.cs +++ b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/ClickMarkerContainer.cs @@ -6,12 +6,12 @@ using osu.Game.Rulesets.Objects.Pooling; namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis { - public partial class HitMarkersContainer : PooledDrawableWithLifetimeContainer + public partial class ClickMarkerContainer : PooledDrawableWithLifetimeContainer { private readonly DrawablePool leftPool; private readonly DrawablePool rightPool; - public HitMarkersContainer() + public ClickMarkerContainer() { AddInternal(leftPool = new DrawablePool(15)); AddInternal(rightPool = new DrawablePool(15)); diff --git a/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/AimMarkersContainer.cs b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/MovementMarkerContainer.cs similarity index 78% rename from osu.Game.Rulesets.Osu/UI/ReplayAnalysis/AimMarkersContainer.cs rename to osu.Game.Rulesets.Osu/UI/ReplayAnalysis/MovementMarkerContainer.cs index 76e88ad5b0..8c2bdd5908 100644 --- a/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/AimMarkersContainer.cs +++ b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/MovementMarkerContainer.cs @@ -6,11 +6,11 @@ using osu.Game.Rulesets.Objects.Pooling; namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis { - public partial class AimMarkersContainer : PooledDrawableWithLifetimeContainer + public partial class MovementMarkerContainer : PooledDrawableWithLifetimeContainer { private readonly DrawablePool pool; - public AimMarkersContainer() + public MovementMarkerContainer() { AddInternal(pool = new DrawablePool(80)); } diff --git a/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/AimLinesContainer.cs b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/MovementPathContainer.cs similarity index 92% rename from osu.Game.Rulesets.Osu/UI/ReplayAnalysis/AimLinesContainer.cs rename to osu.Game.Rulesets.Osu/UI/ReplayAnalysis/MovementPathContainer.cs index db747e2775..58c5993f34 100644 --- a/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/AimLinesContainer.cs +++ b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/MovementPathContainer.cs @@ -9,12 +9,12 @@ using osuTK.Graphics; namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis { - public partial class AimLinesContainer : Path + public partial class MovementPathContainer : Path { private readonly LifetimeEntryManager lifetimeManager = new LifetimeEntryManager(); private readonly SortedSet aliveEntries = new SortedSet(new AimLinePointComparator()); - public AimLinesContainer() + public MovementPathContainer() { lifetimeManager.EntryBecameAlive += entryBecameAlive; lifetimeManager.EntryBecameDead += entryBecameDead; @@ -32,8 +32,6 @@ namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis public void Add(AimPointEntry entry) => lifetimeManager.AddEntry(entry); - public void Clear() => lifetimeManager.ClearEntries(); - private void entryBecameAlive(LifetimeEntry entry) { aliveEntries.Add((AimPointEntry)entry); diff --git a/osu.Game.Rulesets.Osu/UI/ReplayAnalysisOverlay.cs b/osu.Game.Rulesets.Osu/UI/ReplayAnalysisOverlay.cs index 2cc1948474..d33d468c7e 100644 --- a/osu.Game.Rulesets.Osu/UI/ReplayAnalysisOverlay.cs +++ b/osu.Game.Rulesets.Osu/UI/ReplayAnalysisOverlay.cs @@ -18,9 +18,9 @@ namespace osu.Game.Rulesets.Osu.UI private BindableBool aimMarkersEnabled { get; } = new BindableBool(); private BindableBool aimLinesEnabled { get; } = new BindableBool(); - protected readonly HitMarkersContainer HitMarkers; - protected readonly AimMarkersContainer AimMarkers; - protected readonly AimLinesContainer AimLines; + protected readonly ClickMarkerContainer ClickMarkers; + protected readonly MovementMarkerContainer MovementMarkers; + protected readonly MovementPathContainer MovementPath; private readonly Replay replay; @@ -32,9 +32,9 @@ namespace osu.Game.Rulesets.Osu.UI InternalChildren = new Drawable[] { - HitMarkers = new HitMarkersContainer(), - AimLines = new AimLinesContainer(), - AimMarkers = new AimMarkersContainer(), + ClickMarkers = new ClickMarkerContainer(), + MovementPath = new MovementPathContainer(), + MovementMarkers = new MovementMarkerContainer(), }; } @@ -52,9 +52,9 @@ namespace osu.Game.Rulesets.Osu.UI { base.LoadComplete(); - hitMarkersEnabled.BindValueChanged(enabled => HitMarkers.FadeTo(enabled.NewValue ? 1 : 0), true); - aimMarkersEnabled.BindValueChanged(enabled => AimMarkers.FadeTo(enabled.NewValue ? 1 : 0), true); - aimLinesEnabled.BindValueChanged(enabled => AimLines.FadeTo(enabled.NewValue ? 1 : 0), true); + hitMarkersEnabled.BindValueChanged(enabled => ClickMarkers.FadeTo(enabled.NewValue ? 1 : 0), true); + aimMarkersEnabled.BindValueChanged(enabled => MovementMarkers.FadeTo(enabled.NewValue ? 1 : 0), true); + aimLinesEnabled.BindValueChanged(enabled => MovementPath.FadeTo(enabled.NewValue ? 1 : 0), true); } private void loadReplay() @@ -66,8 +66,8 @@ namespace osu.Game.Rulesets.Osu.UI { var osuFrame = (OsuReplayFrame)frame; - AimMarkers.Add(new AimPointEntry(osuFrame.Time, osuFrame.Position)); - AimLines.Add(new AimPointEntry(osuFrame.Time, osuFrame.Position)); + MovementMarkers.Add(new AimPointEntry(osuFrame.Time, osuFrame.Position)); + MovementPath.Add(new AimPointEntry(osuFrame.Time, osuFrame.Position)); bool leftButton = osuFrame.Actions.Contains(OsuAction.LeftButton); bool rightButton = osuFrame.Actions.Contains(OsuAction.RightButton); @@ -76,7 +76,7 @@ namespace osu.Game.Rulesets.Osu.UI leftHeld = false; else if (!leftHeld && leftButton) { - HitMarkers.Add(new HitMarkerEntry(osuFrame.Time, osuFrame.Position, true)); + ClickMarkers.Add(new HitMarkerEntry(osuFrame.Time, osuFrame.Position, true)); leftHeld = true; } @@ -84,7 +84,7 @@ namespace osu.Game.Rulesets.Osu.UI rightHeld = false; else if (!rightHeld && rightButton) { - HitMarkers.Add(new HitMarkerEntry(osuFrame.Time, osuFrame.Position, false)); + ClickMarkers.Add(new HitMarkerEntry(osuFrame.Time, osuFrame.Position, false)); rightHeld = true; } } From a4a37c5ba64773ef5cae4626ca76220ca66ce87d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 4 Sep 2024 20:25:29 +0900 Subject: [PATCH 2200/2493] Simplify lifetime entries and stuff --- ...AimPointEntry.cs => AnalysisFrameEntry.cs} | 7 ++- .../UI/ReplayAnalysis/ClickMarkerContainer.cs | 16 ++---- .../UI/ReplayAnalysis/HitMarker.cs | 4 +- ...itMarkerLeftClick.cs => HitMarkerClick.cs} | 37 +++++++++++--- .../UI/ReplayAnalysis/HitMarkerEntry.cs | 18 ------- .../UI/ReplayAnalysis/HitMarkerRightClick.cs | 49 ------------------- .../ReplayAnalysis/MovementMarkerContainer.cs | 4 +- .../ReplayAnalysis/MovementPathContainer.cs | 22 ++++++--- .../UI/ReplayAnalysisOverlay.cs | 8 +-- 9 files changed, 61 insertions(+), 104 deletions(-) rename osu.Game.Rulesets.Osu/UI/ReplayAnalysis/{AimPointEntry.cs => AnalysisFrameEntry.cs} (66%) rename osu.Game.Rulesets.Osu/UI/ReplayAnalysis/{HitMarkerLeftClick.cs => HitMarkerClick.cs} (55%) delete mode 100644 osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarkerEntry.cs delete mode 100644 osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarkerRightClick.cs diff --git a/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/AimPointEntry.cs b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/AnalysisFrameEntry.cs similarity index 66% rename from osu.Game.Rulesets.Osu/UI/ReplayAnalysis/AimPointEntry.cs rename to osu.Game.Rulesets.Osu/UI/ReplayAnalysis/AnalysisFrameEntry.cs index 948568eb12..ca11e6aff1 100644 --- a/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/AimPointEntry.cs +++ b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/AnalysisFrameEntry.cs @@ -6,15 +6,18 @@ using osuTK; namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis { - public partial class AimPointEntry : LifetimeEntry + public partial class AnalysisFrameEntry : LifetimeEntry { + public OsuAction? Action { get; } + public Vector2 Position { get; } - public AimPointEntry(double time, Vector2 position) + public AnalysisFrameEntry(double time, Vector2 position, OsuAction? action = null) { LifetimeStart = time; LifetimeEnd = time + 1_000; Position = position; + Action = action; } } } diff --git a/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/ClickMarkerContainer.cs b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/ClickMarkerContainer.cs index be56e26caf..3de1a70d7c 100644 --- a/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/ClickMarkerContainer.cs +++ b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/ClickMarkerContainer.cs @@ -6,23 +6,15 @@ using osu.Game.Rulesets.Objects.Pooling; namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis { - public partial class ClickMarkerContainer : PooledDrawableWithLifetimeContainer + public partial class ClickMarkerContainer : PooledDrawableWithLifetimeContainer { - private readonly DrawablePool leftPool; - private readonly DrawablePool rightPool; + private readonly DrawablePool clickMarkerPool; public ClickMarkerContainer() { - AddInternal(leftPool = new DrawablePool(15)); - AddInternal(rightPool = new DrawablePool(15)); + AddInternal(clickMarkerPool = new DrawablePool(30)); } - protected override HitMarker GetDrawable(HitMarkerEntry entry) - { - if (entry.IsLeftMarker) - return leftPool.Get(d => d.Apply(entry)); - - return rightPool.Get(d => d.Apply(entry)); - } + protected override HitMarker GetDrawable(AnalysisFrameEntry entry) => clickMarkerPool.Get(d => d.Apply(entry)); } } diff --git a/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarker.cs b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarker.cs index 339bdae5da..e2ecba44fc 100644 --- a/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarker.cs +++ b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarker.cs @@ -6,14 +6,14 @@ using osu.Game.Rulesets.Objects.Pooling; namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis { - public partial class HitMarker : PoolableDrawableWithLifetime + public partial class HitMarker : PoolableDrawableWithLifetime { public HitMarker() { Origin = Anchor.Centre; } - protected override void OnApply(AimPointEntry entry) + protected override void OnApply(AnalysisFrameEntry entry) { Position = entry.Position; diff --git a/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarkerLeftClick.cs b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarkerClick.cs similarity index 55% rename from osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarkerLeftClick.cs rename to osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarkerClick.cs index 988fc28371..4652ea3416 100644 --- a/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarkerLeftClick.cs +++ b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarkerClick.cs @@ -1,17 +1,18 @@ +using System; +using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Shapes; +using osu.Game.Graphics; using osuTK; -using osuTK.Graphics; namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis { - public partial class HitMarkerLeftClick : HitMarker + public partial class HitMarkerClick : HitMarker { - public HitMarkerLeftClick() + public HitMarkerClick() { const float length = 20; - - Colour = Color4.OrangeRed; + const float border_size = 3; InternalChildren = new Drawable[] { @@ -19,14 +20,14 @@ namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Size = new Vector2(3, length), + Size = new Vector2(border_size, length + border_size), Colour = Colour4.Black.Opacity(0.5F) }, new Box { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Size = new Vector2(3, length), + Size = new Vector2(border_size, length + border_size), Rotation = 90, Colour = Colour4.Black.Opacity(0.5F) }, @@ -45,5 +46,27 @@ namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis } }; } + + [Resolved] + private OsuColour colours { get; set; } = null!; + + protected override void OnApply(AnalysisFrameEntry entry) + { + base.OnApply(entry); + + switch (entry.Action) + { + case OsuAction.LeftButton: + Colour = colours.BlueLight; + break; + + case OsuAction.RightButton: + Colour = colours.YellowLight; + break; + + default: + throw new ArgumentOutOfRangeException(); + } + } } } diff --git a/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarkerEntry.cs b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarkerEntry.cs deleted file mode 100644 index 80c268910d..0000000000 --- a/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarkerEntry.cs +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osuTK; - -namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis -{ - public partial class HitMarkerEntry : AimPointEntry - { - public bool IsLeftMarker { get; } - - public HitMarkerEntry(double lifetimeStart, Vector2 position, bool isLeftMarker) - : base(lifetimeStart, position) - { - IsLeftMarker = isLeftMarker; - } - } -} diff --git a/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarkerRightClick.cs b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarkerRightClick.cs deleted file mode 100644 index 32cdd2d0b5..0000000000 --- a/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarkerRightClick.cs +++ /dev/null @@ -1,49 +0,0 @@ -using osu.Framework.Graphics; -using osu.Framework.Graphics.Shapes; -using osuTK; -using osuTK.Graphics; - -namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis -{ - public partial class HitMarkerRightClick : HitMarker - { - public HitMarkerRightClick() - { - const float length = 20; - - Colour = Color4.GreenYellow; - - InternalChildren = new Drawable[] - { - new Box - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Size = new Vector2(3, length), - Colour = Colour4.Black.Opacity(0.5F) - }, - new Box - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Size = new Vector2(3, length), - Rotation = 90, - Colour = Colour4.Black.Opacity(0.5F) - }, - new Box - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Size = new Vector2(1, length), - }, - new Box - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Size = new Vector2(1, length), - Rotation = 90, - } - }; - } - } -} diff --git a/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/MovementMarkerContainer.cs b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/MovementMarkerContainer.cs index 8c2bdd5908..d52f54650d 100644 --- a/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/MovementMarkerContainer.cs +++ b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/MovementMarkerContainer.cs @@ -6,7 +6,7 @@ using osu.Game.Rulesets.Objects.Pooling; namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis { - public partial class MovementMarkerContainer : PooledDrawableWithLifetimeContainer + public partial class MovementMarkerContainer : PooledDrawableWithLifetimeContainer { private readonly DrawablePool pool; @@ -15,6 +15,6 @@ namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis AddInternal(pool = new DrawablePool(80)); } - protected override HitMarker GetDrawable(AimPointEntry entry) => pool.Get(d => d.Apply(entry)); + protected override HitMarker GetDrawable(AnalysisFrameEntry entry) => pool.Get(d => d.Apply(entry)); } } diff --git a/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/MovementPathContainer.cs b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/MovementPathContainer.cs index 58c5993f34..1d52157036 100644 --- a/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/MovementPathContainer.cs +++ b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/MovementPathContainer.cs @@ -3,16 +3,17 @@ using System; using System.Collections.Generic; +using osu.Framework.Allocation; using osu.Framework.Graphics.Lines; using osu.Framework.Graphics.Performance; -using osuTK.Graphics; +using osu.Game.Graphics; namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis { public partial class MovementPathContainer : Path { private readonly LifetimeEntryManager lifetimeManager = new LifetimeEntryManager(); - private readonly SortedSet aliveEntries = new SortedSet(new AimLinePointComparator()); + private readonly SortedSet aliveEntries = new SortedSet(new AimLinePointComparator()); public MovementPathContainer() { @@ -20,7 +21,12 @@ namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis lifetimeManager.EntryBecameDead += entryBecameDead; PathRadius = 1f; - Colour = new Color4(255, 255, 255, 127); + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + Colour = colours.Pink2; } protected override void Update() @@ -30,17 +36,17 @@ namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis lifetimeManager.Update(Time.Current); } - public void Add(AimPointEntry entry) => lifetimeManager.AddEntry(entry); + public void Add(AnalysisFrameEntry entry) => lifetimeManager.AddEntry(entry); private void entryBecameAlive(LifetimeEntry entry) { - aliveEntries.Add((AimPointEntry)entry); + aliveEntries.Add((AnalysisFrameEntry)entry); updateVertices(); } private void entryBecameDead(LifetimeEntry entry) { - aliveEntries.Remove((AimPointEntry)entry); + aliveEntries.Remove((AnalysisFrameEntry)entry); updateVertices(); } @@ -54,9 +60,9 @@ namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis } } - private sealed class AimLinePointComparator : IComparer + private sealed class AimLinePointComparator : IComparer { - public int Compare(AimPointEntry? x, AimPointEntry? y) + public int Compare(AnalysisFrameEntry? x, AnalysisFrameEntry? y) { ArgumentNullException.ThrowIfNull(x); ArgumentNullException.ThrowIfNull(y); diff --git a/osu.Game.Rulesets.Osu/UI/ReplayAnalysisOverlay.cs b/osu.Game.Rulesets.Osu/UI/ReplayAnalysisOverlay.cs index d33d468c7e..a42625a9c4 100644 --- a/osu.Game.Rulesets.Osu/UI/ReplayAnalysisOverlay.cs +++ b/osu.Game.Rulesets.Osu/UI/ReplayAnalysisOverlay.cs @@ -66,8 +66,8 @@ namespace osu.Game.Rulesets.Osu.UI { var osuFrame = (OsuReplayFrame)frame; - MovementMarkers.Add(new AimPointEntry(osuFrame.Time, osuFrame.Position)); - MovementPath.Add(new AimPointEntry(osuFrame.Time, osuFrame.Position)); + MovementMarkers.Add(new AnalysisFrameEntry(osuFrame.Time, osuFrame.Position)); + MovementPath.Add(new AnalysisFrameEntry(osuFrame.Time, osuFrame.Position)); bool leftButton = osuFrame.Actions.Contains(OsuAction.LeftButton); bool rightButton = osuFrame.Actions.Contains(OsuAction.RightButton); @@ -76,7 +76,7 @@ namespace osu.Game.Rulesets.Osu.UI leftHeld = false; else if (!leftHeld && leftButton) { - ClickMarkers.Add(new HitMarkerEntry(osuFrame.Time, osuFrame.Position, true)); + ClickMarkers.Add(new AnalysisFrameEntry(osuFrame.Time, osuFrame.Position, OsuAction.LeftButton)); leftHeld = true; } @@ -84,7 +84,7 @@ namespace osu.Game.Rulesets.Osu.UI rightHeld = false; else if (!rightHeld && rightButton) { - ClickMarkers.Add(new HitMarkerEntry(osuFrame.Time, osuFrame.Position, false)); + ClickMarkers.Add(new AnalysisFrameEntry(osuFrame.Time, osuFrame.Position, OsuAction.RightButton)); rightHeld = true; } } From a6ed719454dc3c0c2784f49d4d8ef627e424e9e1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 4 Sep 2024 21:04:59 +0900 Subject: [PATCH 2201/2493] Visual design pass --- .../UI/ReplayAnalysis/HitMarker.cs | 20 ++++++ .../UI/ReplayAnalysis/HitMarkerClick.cs | 66 ++++--------------- .../UI/ReplayAnalysis/HitMarkerMovement.cs | 40 ++++------- .../ReplayAnalysis/MovementPathContainer.cs | 2 +- .../UI/ReplayAnalysisOverlay.cs | 17 +++-- 5 files changed, 61 insertions(+), 84 deletions(-) diff --git a/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarker.cs b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarker.cs index e2ecba44fc..4fa992ff15 100644 --- a/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarker.cs +++ b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarker.cs @@ -1,7 +1,9 @@ // 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.Game.Graphics; using osu.Game.Rulesets.Objects.Pooling; namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis @@ -13,6 +15,9 @@ namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis Origin = Anchor.Centre; } + [Resolved] + private OsuColour colours { get; set; } = null!; + protected override void OnApply(AnalysisFrameEntry entry) { Position = entry.Position; @@ -22,6 +27,21 @@ namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis using (BeginAbsoluteSequence(LifetimeEnd - 200)) this.FadeOut(200); + + switch (entry.Action) + { + case OsuAction.LeftButton: + Colour = colours.BlueLight; + break; + + case OsuAction.RightButton: + Colour = colours.YellowLight; + break; + + default: + Colour = colours.Pink2; + break; + } } } } diff --git a/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarkerClick.cs b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarkerClick.cs index 4652ea3416..ba41de7caa 100644 --- a/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarkerClick.cs +++ b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarkerClick.cs @@ -1,9 +1,8 @@ -using System; -using osu.Framework.Allocation; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; -using osu.Game.Graphics; using osuTK; +using osuTK.Graphics; namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis { @@ -11,62 +10,25 @@ namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis { public HitMarkerClick() { - const float length = 20; - const float border_size = 3; - InternalChildren = new Drawable[] { - new Box + new CircularContainer { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Size = new Vector2(border_size, length + border_size), - Colour = Colour4.Black.Opacity(0.5F) + Size = new Vector2(15), + Masking = true, + BorderThickness = 2, + BorderColour = Color4.White, + Child = new Box + { + Colour = Color4.Black, + RelativeSizeAxes = Axes.Both, + AlwaysPresent = true, + Alpha = 0, + }, }, - new Box - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Size = new Vector2(border_size, length + border_size), - Rotation = 90, - Colour = Colour4.Black.Opacity(0.5F) - }, - new Box - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Size = new Vector2(1, length), - }, - new Box - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Size = new Vector2(1, length), - Rotation = 90, - } }; } - - [Resolved] - private OsuColour colours { get; set; } = null!; - - protected override void OnApply(AnalysisFrameEntry entry) - { - base.OnApply(entry); - - switch (entry.Action) - { - case OsuAction.LeftButton: - Colour = colours.BlueLight; - break; - - case OsuAction.RightButton: - Colour = colours.YellowLight; - break; - - default: - throw new ArgumentOutOfRangeException(); - } - } } } diff --git a/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarkerMovement.cs b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarkerMovement.cs index 4f9b6f8790..0cda732b39 100644 --- a/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarkerMovement.cs +++ b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarkerMovement.cs @@ -1,8 +1,7 @@ -using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Shapes; +using osu.Game.Graphics; using osuTK; -using osuTK.Graphics; namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis { @@ -10,41 +9,30 @@ namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis { public HitMarkerMovement() { - const float length = 5; - - Colour = Color4.Gray.Opacity(0.4f); - InternalChildren = new Drawable[] { - new Box + new Circle { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Size = new Vector2(3, length), - Colour = Colour4.Black.Opacity(0.5F) + Colour = OsuColour.Gray(0.2f), + RelativeSizeAxes = Axes.Both, + Size = new Vector2(1.2f) }, - new Box + new Circle { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Size = new Vector2(3, length), - Rotation = 90, - Colour = Colour4.Black.Opacity(0.5F) + RelativeSizeAxes = Axes.Both, }, - new Box - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Size = new Vector2(1, length), - }, - new Box - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Size = new Vector2(1, length), - Rotation = 90, - } }; } + + protected override void OnApply(AnalysisFrameEntry entry) + { + base.OnApply(entry); + + Size = new Vector2(entry.Action != null ? 4 : 3); + } } } diff --git a/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/MovementPathContainer.cs b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/MovementPathContainer.cs index 1d52157036..ff662c4dfa 100644 --- a/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/MovementPathContainer.cs +++ b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/MovementPathContainer.cs @@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis lifetimeManager.EntryBecameAlive += entryBecameAlive; lifetimeManager.EntryBecameDead += entryBecameDead; - PathRadius = 1f; + PathRadius = 0.5f; } [BackgroundDependencyLoader] diff --git a/osu.Game.Rulesets.Osu/UI/ReplayAnalysisOverlay.cs b/osu.Game.Rulesets.Osu/UI/ReplayAnalysisOverlay.cs index a42625a9c4..682842c56f 100644 --- a/osu.Game.Rulesets.Osu/UI/ReplayAnalysisOverlay.cs +++ b/osu.Game.Rulesets.Osu/UI/ReplayAnalysisOverlay.cs @@ -62,13 +62,12 @@ namespace osu.Game.Rulesets.Osu.UI bool leftHeld = false; bool rightHeld = false; + OsuAction? lastAction = null; + foreach (var frame in replay.Frames) { var osuFrame = (OsuReplayFrame)frame; - MovementMarkers.Add(new AnalysisFrameEntry(osuFrame.Time, osuFrame.Position)); - MovementPath.Add(new AnalysisFrameEntry(osuFrame.Time, osuFrame.Position)); - bool leftButton = osuFrame.Actions.Contains(OsuAction.LeftButton); bool rightButton = osuFrame.Actions.Contains(OsuAction.RightButton); @@ -76,17 +75,25 @@ namespace osu.Game.Rulesets.Osu.UI leftHeld = false; else if (!leftHeld && leftButton) { - ClickMarkers.Add(new AnalysisFrameEntry(osuFrame.Time, osuFrame.Position, OsuAction.LeftButton)); leftHeld = true; + lastAction = OsuAction.LeftButton; + ClickMarkers.Add(new AnalysisFrameEntry(osuFrame.Time, osuFrame.Position, OsuAction.LeftButton)); } if (rightHeld && !rightButton) rightHeld = false; else if (!rightHeld && rightButton) { - ClickMarkers.Add(new AnalysisFrameEntry(osuFrame.Time, osuFrame.Position, OsuAction.RightButton)); rightHeld = true; + lastAction = OsuAction.RightButton; + ClickMarkers.Add(new AnalysisFrameEntry(osuFrame.Time, osuFrame.Position, OsuAction.RightButton)); } + + if (!leftButton && !rightButton) + lastAction = null; + + MovementMarkers.Add(new AnalysisFrameEntry(osuFrame.Time, osuFrame.Position, lastAction)); + MovementPath.Add(new AnalysisFrameEntry(osuFrame.Time, osuFrame.Position)); } } } From 21146c35015b2aa6bbd9e015b97313061090c75b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 4 Sep 2024 21:16:59 +0900 Subject: [PATCH 2202/2493] Add back shadow cast --- osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs b/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs index 09dcd54c3c..c6ad10c062 100644 --- a/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs @@ -32,6 +32,8 @@ namespace osu.Game.Rulesets.Osu.UI public new OsuPlayfield Playfield => (OsuPlayfield)base.Playfield; + protected new OsuRulesetConfigManager Config => (OsuRulesetConfigManager)base.Config; + public DrawableOsuRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList? mods = null) : base(ruleset, beatmap, mods) { @@ -43,9 +45,9 @@ namespace osu.Game.Rulesets.Osu.UI if (replayPlayer != null) { PlayfieldAdjustmentContainer.Add(new ReplayAnalysisOverlay(replayPlayer.Score.Replay)); - replayPlayer.AddSettings(new ReplayAnalysisSettings((OsuRulesetConfigManager)Config)); + replayPlayer.AddSettings(new ReplayAnalysisSettings(Config)); - cursorHideEnabled = ((OsuRulesetConfigManager)Config).GetBindable(OsuRulesetSetting.ReplayCursorHideEnabled); + cursorHideEnabled = Config.GetBindable(OsuRulesetSetting.ReplayCursorHideEnabled); cursorHideEnabled.BindValueChanged(enabled => Playfield.Cursor.FadeTo(enabled.NewValue ? 0 : 1), true); } } From 08ebc83a89d25ad869ef372e4735ebfe0dcaefaf Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 5 Sep 2024 12:07:16 +0900 Subject: [PATCH 2203/2493] Fix path getting misaligned with negative position values --- .../UI/ReplayAnalysis/MovementPathContainer.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/MovementPathContainer.cs b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/MovementPathContainer.cs index ff662c4dfa..dbe87b7ea7 100644 --- a/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/MovementPathContainer.cs +++ b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/MovementPathContainer.cs @@ -7,6 +7,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics.Lines; using osu.Framework.Graphics.Performance; using osu.Game.Graphics; +using osuTK; namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis { @@ -54,10 +55,19 @@ namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis { ClearVertices(); + Vector2 min = Vector2.Zero; + foreach (var entry in aliveEntries) { AddVertex(entry.Position); + if (entry.Position.X < min.X) + min.X = entry.Position.X; + + if (entry.Position.Y < min.Y) + min.Y = entry.Position.Y; } + + Position = min; } private sealed class AimLinePointComparator : IComparer From ee26ff2e2901bbb51cf477b6c1c3289dcc19b9f7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 5 Sep 2024 13:07:49 +0900 Subject: [PATCH 2204/2493] Add out-of-bounds tests to test case --- osu.Game.Rulesets.Osu.Tests/TestSceneOsuAnalysisContainer.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuAnalysisContainer.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuAnalysisContainer.cs index fb6d59ddba..b2cb8c5468 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuAnalysisContainer.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuAnalysisContainer.cs @@ -94,8 +94,8 @@ namespace osu.Game.Rulesets.Osu.Tests for (int i = 0; i < 1000; i++) { - posX = Math.Clamp(posX + random.Next(-20, 21), 0, 500); - posY = Math.Clamp(posY + random.Next(-20, 21), 0, 500); + posX = Math.Clamp(posX + random.Next(-20, 21), -100, 600); + posY = Math.Clamp(posY + random.Next(-20, 21), -100, 600); var actions = new List(); From 2d198e57e1ee2c7f7e6f71009c384301c650317c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 5 Sep 2024 14:02:05 +0900 Subject: [PATCH 2205/2493] Second visual design pass --- .../UI/ReplayAnalysis/HitMarker.cs | 32 ++--------- .../UI/ReplayAnalysis/HitMarkerClick.cs | 57 ++++++++++++++++++- .../UI/ReplayAnalysis/HitMarkerMovement.cs | 38 ++++++++++--- .../ReplayAnalysis/MovementPathContainer.cs | 2 + .../UI/ReplayAnalysisOverlay.cs | 2 +- 5 files changed, 93 insertions(+), 38 deletions(-) diff --git a/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarker.cs b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarker.cs index 4fa992ff15..2187fdfe57 100644 --- a/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarker.cs +++ b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarker.cs @@ -8,40 +8,20 @@ using osu.Game.Rulesets.Objects.Pooling; namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis { - public partial class HitMarker : PoolableDrawableWithLifetime + public abstract partial class HitMarker : PoolableDrawableWithLifetime { - public HitMarker() + [Resolved] + protected OsuColour Colours { get; private set; } = null!; + + [BackgroundDependencyLoader] + private void load() { Origin = Anchor.Centre; } - [Resolved] - private OsuColour colours { get; set; } = null!; - protected override void OnApply(AnalysisFrameEntry entry) { Position = entry.Position; - - using (BeginAbsoluteSequence(LifetimeStart)) - Show(); - - using (BeginAbsoluteSequence(LifetimeEnd - 200)) - this.FadeOut(200); - - switch (entry.Action) - { - case OsuAction.LeftButton: - Colour = colours.BlueLight; - break; - - case OsuAction.RightButton: - Colour = colours.YellowLight; - break; - - default: - Colour = colours.Pink2; - break; - } } } } diff --git a/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarkerClick.cs b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarkerClick.cs index ba41de7caa..a1024d1930 100644 --- a/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarkerClick.cs +++ b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarkerClick.cs @@ -1,3 +1,4 @@ +using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; @@ -8,17 +9,30 @@ namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis { public partial class HitMarkerClick : HitMarker { - public HitMarkerClick() + private Container clickDisplay = null!; + + [BackgroundDependencyLoader] + private void load() { InternalChildren = new Drawable[] { + new Circle + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(0.125f), + RelativeSizeAxes = Axes.Both, + Blending = BlendingParameters.Additive, + Colour = Colours.Gray5, + }, new CircularContainer { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Size = new Vector2(15), + RelativeSizeAxes = Axes.Both, + Colour = Colours.Gray5, Masking = true, - BorderThickness = 2, + BorderThickness = 2.2f, BorderColour = Color4.White, Child = new Box { @@ -28,7 +42,44 @@ namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis Alpha = 0, }, }, + clickDisplay = new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.CentreRight, + RelativeSizeAxes = Axes.Both, + Colour = Colours.Yellow, + Scale = new Vector2(0.95f), + Width = 0.5f, + Masking = true, + BorderThickness = 2, + BorderColour = Color4.White, + Child = new CircularContainer + { + RelativeSizeAxes = Axes.Both, + Width = 2, + Masking = true, + BorderThickness = 2, + BorderColour = Color4.White, + Child = new Box + { + Colour = Color4.Black, + RelativeSizeAxes = Axes.Both, + AlwaysPresent = true, + Alpha = 0, + }, + } + } }; + + Size = new Vector2(16); + } + + protected override void OnApply(AnalysisFrameEntry entry) + { + base.OnApply(entry); + + clickDisplay.Alpha = entry.Action != null ? 1 : 0; + clickDisplay.Rotation = entry.Action == OsuAction.LeftButton ? 0 : 180; } } } diff --git a/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarkerMovement.cs b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarkerMovement.cs index 0cda732b39..b2bbb47c4b 100644 --- a/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarkerMovement.cs +++ b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarkerMovement.cs @@ -1,29 +1,47 @@ +using osu.Framework.Allocation; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; -using osu.Game.Graphics; using osuTK; +using osuTK.Graphics; namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis { public partial class HitMarkerMovement : HitMarker { - public HitMarkerMovement() + private Container clickDisplay = null!; + private Circle mainCircle = null!; + + [BackgroundDependencyLoader] + private void load() { InternalChildren = new Drawable[] { - new Circle + mainCircle = new Circle { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Colour = OsuColour.Gray(0.2f), RelativeSizeAxes = Axes.Both, - Size = new Vector2(1.2f) + Colour = Colours.Pink2, }, - new Circle + clickDisplay = new Container { + Colour = Colours.Yellow, + Scale = new Vector2(0.8f), Anchor = Anchor.Centre, - Origin = Anchor.Centre, + Origin = Anchor.CentreRight, RelativeSizeAxes = Axes.Both, + Width = 0.5f, + Masking = true, + Children = new Drawable[] + { + new Circle + { + RelativeSizeAxes = Axes.Both, + Width = 2, + Colour = Color4.White, + }, + }, }, }; } @@ -31,8 +49,12 @@ namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis protected override void OnApply(AnalysisFrameEntry entry) { base.OnApply(entry); + Size = new Vector2(entry.Action != null ? 4 : 2.5f); - Size = new Vector2(entry.Action != null ? 4 : 3); + mainCircle.Colour = entry.Action != null ? Colours.Gray4 : Colours.Pink2; + + clickDisplay.Alpha = entry.Action != null ? 1 : 0; + clickDisplay.Rotation = entry.Action == OsuAction.LeftButton ? 0 : 180; } } } diff --git a/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/MovementPathContainer.cs b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/MovementPathContainer.cs index dbe87b7ea7..2ffc6ffe4a 100644 --- a/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/MovementPathContainer.cs +++ b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/MovementPathContainer.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using osu.Framework.Allocation; +using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics.Lines; using osu.Framework.Graphics.Performance; using osu.Game.Graphics; @@ -28,6 +29,7 @@ namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis private void load(OsuColour colours) { Colour = colours.Pink2; + BackgroundColour = colours.Pink2.Opacity(0); } protected override void Update() diff --git a/osu.Game.Rulesets.Osu/UI/ReplayAnalysisOverlay.cs b/osu.Game.Rulesets.Osu/UI/ReplayAnalysisOverlay.cs index 682842c56f..06a1930fa3 100644 --- a/osu.Game.Rulesets.Osu/UI/ReplayAnalysisOverlay.cs +++ b/osu.Game.Rulesets.Osu/UI/ReplayAnalysisOverlay.cs @@ -32,8 +32,8 @@ namespace osu.Game.Rulesets.Osu.UI InternalChildren = new Drawable[] { - ClickMarkers = new ClickMarkerContainer(), MovementPath = new MovementPathContainer(), + ClickMarkers = new ClickMarkerContainer(), MovementMarkers = new MovementMarkerContainer(), }; } From 4f719b9fec4973a62514c6c8a5619dbae3350203 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 5 Sep 2024 14:28:20 +0900 Subject: [PATCH 2206/2493] One more rename pass --- .../TestSceneOsuAnalysisContainer.cs | 4 ++-- .../{HitMarker.cs => AnalysisMarker.cs} | 2 +- .../{HitMarkerClick.cs => ClickMarker.cs} | 5 ++++- .../UI/ReplayAnalysis/ClickMarkerContainer.cs | 8 ++++---- ...athContainer.cs => CursorPathContainer.cs} | 4 ++-- .../{HitMarkerMovement.cs => FrameMarker.cs} | 5 ++++- .../UI/ReplayAnalysis/FrameMarkerContainer.cs | 20 +++++++++++++++++++ .../ReplayAnalysis/MovementMarkerContainer.cs | 20 ------------------- .../UI/ReplayAnalysisOverlay.cs | 16 +++++++-------- 9 files changed, 45 insertions(+), 39 deletions(-) rename osu.Game.Rulesets.Osu/UI/ReplayAnalysis/{HitMarker.cs => AnalysisMarker.cs} (87%) rename osu.Game.Rulesets.Osu/UI/ReplayAnalysis/{HitMarkerClick.cs => ClickMarker.cs} (92%) rename osu.Game.Rulesets.Osu/UI/ReplayAnalysis/{MovementPathContainer.cs => CursorPathContainer.cs} (96%) rename osu.Game.Rulesets.Osu/UI/ReplayAnalysis/{HitMarkerMovement.cs => FrameMarker.cs} (91%) create mode 100644 osu.Game.Rulesets.Osu/UI/ReplayAnalysis/FrameMarkerContainer.cs delete mode 100644 osu.Game.Rulesets.Osu/UI/ReplayAnalysis/MovementMarkerContainer.cs diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuAnalysisContainer.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuAnalysisContainer.cs index b2cb8c5468..bea4f430ea 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuAnalysisContainer.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuAnalysisContainer.cs @@ -124,8 +124,8 @@ namespace osu.Game.Rulesets.Osu.Tests } public bool HitMarkersVisible => ClickMarkers.Alpha > 0 && ClickMarkers.Entries.Any(); - public bool AimMarkersVisible => MovementMarkers.Alpha > 0 && MovementMarkers.Entries.Any(); - public bool AimLinesVisible => MovementPath.Alpha > 0 && MovementPath.Vertices.Count > 1; + public bool AimMarkersVisible => FrameMarkers.Alpha > 0 && FrameMarkers.Entries.Any(); + public bool AimLinesVisible => CursorPath.Alpha > 0 && CursorPath.Vertices.Count > 1; } } } diff --git a/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarker.cs b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/AnalysisMarker.cs similarity index 87% rename from osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarker.cs rename to osu.Game.Rulesets.Osu/UI/ReplayAnalysis/AnalysisMarker.cs index 2187fdfe57..9b602c88a8 100644 --- a/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarker.cs +++ b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/AnalysisMarker.cs @@ -8,7 +8,7 @@ using osu.Game.Rulesets.Objects.Pooling; namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis { - public abstract partial class HitMarker : PoolableDrawableWithLifetime + public abstract partial class AnalysisMarker : PoolableDrawableWithLifetime { [Resolved] protected OsuColour Colours { get; private set; } = null!; diff --git a/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarkerClick.cs b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/ClickMarker.cs similarity index 92% rename from osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarkerClick.cs rename to osu.Game.Rulesets.Osu/UI/ReplayAnalysis/ClickMarker.cs index a1024d1930..5386a80d9d 100644 --- a/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarkerClick.cs +++ b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/ClickMarker.cs @@ -7,7 +7,10 @@ using osuTK.Graphics; namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis { - public partial class HitMarkerClick : HitMarker + /// + /// A marker which shows one click, with visuals focusing on the button which was clicked and the precise location of the click. + /// + public partial class ClickMarker : AnalysisMarker { private Container clickDisplay = null!; diff --git a/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/ClickMarkerContainer.cs b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/ClickMarkerContainer.cs index 3de1a70d7c..ff94449521 100644 --- a/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/ClickMarkerContainer.cs +++ b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/ClickMarkerContainer.cs @@ -6,15 +6,15 @@ using osu.Game.Rulesets.Objects.Pooling; namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis { - public partial class ClickMarkerContainer : PooledDrawableWithLifetimeContainer + public partial class ClickMarkerContainer : PooledDrawableWithLifetimeContainer { - private readonly DrawablePool clickMarkerPool; + private readonly DrawablePool clickMarkerPool; public ClickMarkerContainer() { - AddInternal(clickMarkerPool = new DrawablePool(30)); + AddInternal(clickMarkerPool = new DrawablePool(30)); } - protected override HitMarker GetDrawable(AnalysisFrameEntry entry) => clickMarkerPool.Get(d => d.Apply(entry)); + protected override AnalysisMarker GetDrawable(AnalysisFrameEntry entry) => clickMarkerPool.Get(d => d.Apply(entry)); } } diff --git a/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/MovementPathContainer.cs b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/CursorPathContainer.cs similarity index 96% rename from osu.Game.Rulesets.Osu/UI/ReplayAnalysis/MovementPathContainer.cs rename to osu.Game.Rulesets.Osu/UI/ReplayAnalysis/CursorPathContainer.cs index 2ffc6ffe4a..1951d467e2 100644 --- a/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/MovementPathContainer.cs +++ b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/CursorPathContainer.cs @@ -12,12 +12,12 @@ using osuTK; namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis { - public partial class MovementPathContainer : Path + public partial class CursorPathContainer : Path { private readonly LifetimeEntryManager lifetimeManager = new LifetimeEntryManager(); private readonly SortedSet aliveEntries = new SortedSet(new AimLinePointComparator()); - public MovementPathContainer() + public CursorPathContainer() { lifetimeManager.EntryBecameAlive += entryBecameAlive; lifetimeManager.EntryBecameDead += entryBecameDead; diff --git a/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarkerMovement.cs b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/FrameMarker.cs similarity index 91% rename from osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarkerMovement.cs rename to osu.Game.Rulesets.Osu/UI/ReplayAnalysis/FrameMarker.cs index b2bbb47c4b..6d44307075 100644 --- a/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarkerMovement.cs +++ b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/FrameMarker.cs @@ -7,7 +7,10 @@ using osuTK.Graphics; namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis { - public partial class HitMarkerMovement : HitMarker + /// + /// A marker which shows one movement frame, include any buttons which are pressed. + /// + public partial class FrameMarker : AnalysisMarker { private Container clickDisplay = null!; private Circle mainCircle = null!; diff --git a/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/FrameMarkerContainer.cs b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/FrameMarkerContainer.cs new file mode 100644 index 0000000000..63aea259f7 --- /dev/null +++ b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/FrameMarkerContainer.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.Graphics.Pooling; +using osu.Game.Rulesets.Objects.Pooling; + +namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis +{ + public partial class FrameMarkerContainer : PooledDrawableWithLifetimeContainer + { + private readonly DrawablePool pool; + + public FrameMarkerContainer() + { + AddInternal(pool = new DrawablePool(80)); + } + + protected override AnalysisMarker GetDrawable(AnalysisFrameEntry entry) => pool.Get(d => d.Apply(entry)); + } +} diff --git a/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/MovementMarkerContainer.cs b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/MovementMarkerContainer.cs deleted file mode 100644 index d52f54650d..0000000000 --- a/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/MovementMarkerContainer.cs +++ /dev/null @@ -1,20 +0,0 @@ -// 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.Graphics.Pooling; -using osu.Game.Rulesets.Objects.Pooling; - -namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis -{ - public partial class MovementMarkerContainer : PooledDrawableWithLifetimeContainer - { - private readonly DrawablePool pool; - - public MovementMarkerContainer() - { - AddInternal(pool = new DrawablePool(80)); - } - - protected override HitMarker GetDrawable(AnalysisFrameEntry entry) => pool.Get(d => d.Apply(entry)); - } -} diff --git a/osu.Game.Rulesets.Osu/UI/ReplayAnalysisOverlay.cs b/osu.Game.Rulesets.Osu/UI/ReplayAnalysisOverlay.cs index 06a1930fa3..6d52e3d975 100644 --- a/osu.Game.Rulesets.Osu/UI/ReplayAnalysisOverlay.cs +++ b/osu.Game.Rulesets.Osu/UI/ReplayAnalysisOverlay.cs @@ -19,8 +19,8 @@ namespace osu.Game.Rulesets.Osu.UI private BindableBool aimLinesEnabled { get; } = new BindableBool(); protected readonly ClickMarkerContainer ClickMarkers; - protected readonly MovementMarkerContainer MovementMarkers; - protected readonly MovementPathContainer MovementPath; + protected readonly FrameMarkerContainer FrameMarkers; + protected readonly CursorPathContainer CursorPath; private readonly Replay replay; @@ -32,9 +32,9 @@ namespace osu.Game.Rulesets.Osu.UI InternalChildren = new Drawable[] { - MovementPath = new MovementPathContainer(), + CursorPath = new CursorPathContainer(), ClickMarkers = new ClickMarkerContainer(), - MovementMarkers = new MovementMarkerContainer(), + FrameMarkers = new FrameMarkerContainer(), }; } @@ -53,8 +53,8 @@ namespace osu.Game.Rulesets.Osu.UI base.LoadComplete(); hitMarkersEnabled.BindValueChanged(enabled => ClickMarkers.FadeTo(enabled.NewValue ? 1 : 0), true); - aimMarkersEnabled.BindValueChanged(enabled => MovementMarkers.FadeTo(enabled.NewValue ? 1 : 0), true); - aimLinesEnabled.BindValueChanged(enabled => MovementPath.FadeTo(enabled.NewValue ? 1 : 0), true); + aimMarkersEnabled.BindValueChanged(enabled => FrameMarkers.FadeTo(enabled.NewValue ? 1 : 0), true); + aimLinesEnabled.BindValueChanged(enabled => CursorPath.FadeTo(enabled.NewValue ? 1 : 0), true); } private void loadReplay() @@ -92,8 +92,8 @@ namespace osu.Game.Rulesets.Osu.UI if (!leftButton && !rightButton) lastAction = null; - MovementMarkers.Add(new AnalysisFrameEntry(osuFrame.Time, osuFrame.Position, lastAction)); - MovementPath.Add(new AnalysisFrameEntry(osuFrame.Time, osuFrame.Position)); + FrameMarkers.Add(new AnalysisFrameEntry(osuFrame.Time, osuFrame.Position, lastAction)); + CursorPath.Add(new AnalysisFrameEntry(osuFrame.Time, osuFrame.Position)); } } } From 7390d89c75f45bc86b7e638ed3eddbae3d829ddc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 5 Sep 2024 15:12:02 +0900 Subject: [PATCH 2207/2493] Switch to using `CircularProgress` for more consistent sizing Also show both mouse buttons at once on frame markers. --- .../UI/ReplayAnalysis/AnalysisFrameEntry.cs | 4 +- .../UI/ReplayAnalysis/ClickMarker.cs | 56 +++++++++---------- .../UI/ReplayAnalysis/FrameMarker.cs | 48 +++++++++------- .../UI/ReplayAnalysisOverlay.cs | 9 +-- 4 files changed, 58 insertions(+), 59 deletions(-) diff --git a/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/AnalysisFrameEntry.cs b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/AnalysisFrameEntry.cs index ca11e6aff1..d44def1b67 100644 --- a/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/AnalysisFrameEntry.cs +++ b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/AnalysisFrameEntry.cs @@ -8,11 +8,11 @@ namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis { public partial class AnalysisFrameEntry : LifetimeEntry { - public OsuAction? Action { get; } + public OsuAction[] Action { get; } public Vector2 Position { get; } - public AnalysisFrameEntry(double time, Vector2 position, OsuAction? action = null) + public AnalysisFrameEntry(double time, Vector2 position, params OsuAction[] action) { LifetimeStart = time; LifetimeEnd = time + 1_000; diff --git a/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/ClickMarker.cs b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/ClickMarker.cs index 5386a80d9d..9788ea1aa9 100644 --- a/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/ClickMarker.cs +++ b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/ClickMarker.cs @@ -1,7 +1,12 @@ +// 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.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.UserInterface; using osuTK; using osuTK.Graphics; @@ -12,7 +17,8 @@ namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis /// public partial class ClickMarker : AnalysisMarker { - private Container clickDisplay = null!; + private CircularProgress leftClickDisplay = null!; + private CircularProgress rightClickDisplay = null!; [BackgroundDependencyLoader] private void load() @@ -45,33 +51,27 @@ namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis Alpha = 0, }, }, - clickDisplay = new Container + leftClickDisplay = new CircularProgress { - Anchor = Anchor.Centre, - Origin = Anchor.CentreRight, - RelativeSizeAxes = Axes.Both, Colour = Colours.Yellow, - Scale = new Vector2(0.95f), - Width = 0.5f, - Masking = true, - BorderThickness = 2, - BorderColour = Color4.White, - Child = new CircularContainer - { - RelativeSizeAxes = Axes.Both, - Width = 2, - Masking = true, - BorderThickness = 2, - BorderColour = Color4.White, - Child = new Box - { - Colour = Color4.Black, - RelativeSizeAxes = Axes.Both, - AlwaysPresent = true, - Alpha = 0, - }, - } - } + Size = new Vector2(0.95f), + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreRight, + Rotation = 180, + Progress = 0.5f, + InnerRadius = 0.18f, + RelativeSizeAxes = Axes.Both, + }, + rightClickDisplay = new CircularProgress + { + Colour = Colours.Yellow, + Size = new Vector2(0.95f), + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + Progress = 0.5f, + InnerRadius = 0.18f, + RelativeSizeAxes = Axes.Both, + }, }; Size = new Vector2(16); @@ -81,8 +81,8 @@ namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis { base.OnApply(entry); - clickDisplay.Alpha = entry.Action != null ? 1 : 0; - clickDisplay.Rotation = entry.Action == OsuAction.LeftButton ? 0 : 180; + leftClickDisplay.Alpha = entry.Action.Contains(OsuAction.LeftButton) ? 1 : 0; + rightClickDisplay.Alpha = entry.Action.Contains(OsuAction.RightButton) ? 1 : 0; } } } diff --git a/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/FrameMarker.cs b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/FrameMarker.cs index 6d44307075..35ee144568 100644 --- a/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/FrameMarker.cs +++ b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/FrameMarker.cs @@ -1,9 +1,12 @@ +// 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.Framework.Graphics; -using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.UserInterface; using osuTK; -using osuTK.Graphics; namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis { @@ -12,7 +15,8 @@ namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis /// public partial class FrameMarker : AnalysisMarker { - private Container clickDisplay = null!; + private CircularProgress leftClickDisplay = null!; + private CircularProgress rightClickDisplay = null!; private Circle mainCircle = null!; [BackgroundDependencyLoader] @@ -27,24 +31,26 @@ namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis RelativeSizeAxes = Axes.Both, Colour = Colours.Pink2, }, - clickDisplay = new Container + leftClickDisplay = new CircularProgress { Colour = Colours.Yellow, - Scale = new Vector2(0.8f), - Anchor = Anchor.Centre, + Size = new Vector2(0.8f), + Anchor = Anchor.CentreLeft, Origin = Anchor.CentreRight, + Rotation = 180, + Progress = 0.5f, + InnerRadius = 0.5f, + RelativeSizeAxes = Axes.Both, + }, + rightClickDisplay = new CircularProgress + { + Colour = Colours.Yellow, + Size = new Vector2(0.8f), + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + Progress = 0.5f, + InnerRadius = 0.5f, RelativeSizeAxes = Axes.Both, - Width = 0.5f, - Masking = true, - Children = new Drawable[] - { - new Circle - { - RelativeSizeAxes = Axes.Both, - Width = 2, - Colour = Color4.White, - }, - }, }, }; } @@ -52,12 +58,12 @@ namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis protected override void OnApply(AnalysisFrameEntry entry) { base.OnApply(entry); - Size = new Vector2(entry.Action != null ? 4 : 2.5f); + Size = new Vector2(entry.Action.Any() ? 4 : 2.5f); - mainCircle.Colour = entry.Action != null ? Colours.Gray4 : Colours.Pink2; + mainCircle.Colour = entry.Action.Any() ? Colours.Gray4 : Colours.Pink2; - clickDisplay.Alpha = entry.Action != null ? 1 : 0; - clickDisplay.Rotation = entry.Action == OsuAction.LeftButton ? 0 : 180; + leftClickDisplay.Alpha = entry.Action.Contains(OsuAction.LeftButton) ? 1 : 0; + rightClickDisplay.Alpha = entry.Action.Contains(OsuAction.RightButton) ? 1 : 0; } } } diff --git a/osu.Game.Rulesets.Osu/UI/ReplayAnalysisOverlay.cs b/osu.Game.Rulesets.Osu/UI/ReplayAnalysisOverlay.cs index 6d52e3d975..eb1ef49e5d 100644 --- a/osu.Game.Rulesets.Osu/UI/ReplayAnalysisOverlay.cs +++ b/osu.Game.Rulesets.Osu/UI/ReplayAnalysisOverlay.cs @@ -62,8 +62,6 @@ namespace osu.Game.Rulesets.Osu.UI bool leftHeld = false; bool rightHeld = false; - OsuAction? lastAction = null; - foreach (var frame in replay.Frames) { var osuFrame = (OsuReplayFrame)frame; @@ -76,7 +74,6 @@ namespace osu.Game.Rulesets.Osu.UI else if (!leftHeld && leftButton) { leftHeld = true; - lastAction = OsuAction.LeftButton; ClickMarkers.Add(new AnalysisFrameEntry(osuFrame.Time, osuFrame.Position, OsuAction.LeftButton)); } @@ -85,14 +82,10 @@ namespace osu.Game.Rulesets.Osu.UI else if (!rightHeld && rightButton) { rightHeld = true; - lastAction = OsuAction.RightButton; ClickMarkers.Add(new AnalysisFrameEntry(osuFrame.Time, osuFrame.Position, OsuAction.RightButton)); } - if (!leftButton && !rightButton) - lastAction = null; - - FrameMarkers.Add(new AnalysisFrameEntry(osuFrame.Time, osuFrame.Position, lastAction)); + FrameMarkers.Add(new AnalysisFrameEntry(osuFrame.Time, osuFrame.Position, osuFrame.Actions.ToArray())); CursorPath.Add(new AnalysisFrameEntry(osuFrame.Time, osuFrame.Position)); } } From 7983a765ab09c47fd0cf908aa6b758420e98f9ce Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 5 Sep 2024 15:12:16 +0900 Subject: [PATCH 2208/2493] Update test scene to show more button holds (including both buttons sometimes) --- .../TestSceneOsuAnalysisContainer.cs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuAnalysisContainer.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuAnalysisContainer.cs index bea4f430ea..292ecb7fde 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuAnalysisContainer.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuAnalysisContainer.cs @@ -90,26 +90,28 @@ namespace osu.Game.Rulesets.Osu.Tests var random = new Random(); int posX = 250; int posY = 250; - bool leftOrRight = false; + + var actions = new HashSet(); for (int i = 0; i < 1000; i++) { posX = Math.Clamp(posX + random.Next(-20, 21), -100, 600); posY = Math.Clamp(posY + random.Next(-20, 21), -100, 600); - var actions = new List(); - - if (i % 20 == 0) + if (random.NextDouble() > (actions.Count == 0 ? 0.9 : 0.95)) { - actions.Add(leftOrRight ? OsuAction.LeftButton : OsuAction.RightButton); - leftOrRight = !leftOrRight; + actions.Add(random.NextDouble() > 0.5 ? OsuAction.LeftButton : OsuAction.RightButton); + } + else if (random.NextDouble() > 0.7) + { + actions.Remove(random.NextDouble() > 0.5 ? OsuAction.LeftButton : OsuAction.RightButton); } frames.Add(new OsuReplayFrame { Time = Time.Current + i * 15, Position = new Vector2(posX, posY), - Actions = actions + Actions = actions.ToList(), }); } From 47a9b345ebc52cdecd8e772510014d372f9376c1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 5 Sep 2024 15:15:15 +0900 Subject: [PATCH 2209/2493] Rename config variables and setting strings --- .../TestSceneOsuAnalysisContainer.cs | 24 +++++++++---------- .../Configuration/OsuRulesetConfigManager.cs | 12 +++++----- .../UI/ReplayAnalysisOverlay.cs | 18 +++++++------- .../UI/ReplayAnalysisSettings.cs | 24 +++++++++---------- 4 files changed, 39 insertions(+), 39 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuAnalysisContainer.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuAnalysisContainer.cs index 292ecb7fde..fb8ac81b2c 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuAnalysisContainer.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuAnalysisContainer.cs @@ -40,9 +40,9 @@ namespace osu.Game.Rulesets.Osu.Tests settings = new ReplayAnalysisSettings(config), }; - settings.HitMarkersEnabled.Value = false; - settings.AimMarkersEnabled.Value = false; - settings.AimLinesEnabled.Value = false; + settings.ShowClickMarkers.Value = false; + settings.ShowAimMarkers.Value = false; + settings.ShowCursorPath.Value = false; }); } @@ -51,36 +51,36 @@ namespace osu.Game.Rulesets.Osu.Tests { AddStep("enable everything", () => { - settings.HitMarkersEnabled.Value = true; - settings.AimMarkersEnabled.Value = true; - settings.AimLinesEnabled.Value = true; + settings.ShowClickMarkers.Value = true; + settings.ShowAimMarkers.Value = true; + settings.ShowCursorPath.Value = true; }); } [Test] public void TestHitMarkers() { - AddStep("enable hit markers", () => settings.HitMarkersEnabled.Value = true); + AddStep("enable hit markers", () => settings.ShowClickMarkers.Value = true); AddAssert("hit markers visible", () => analysisContainer.HitMarkersVisible); - AddStep("disable hit markers", () => settings.HitMarkersEnabled.Value = false); + AddStep("disable hit markers", () => settings.ShowClickMarkers.Value = false); AddAssert("hit markers not visible", () => !analysisContainer.HitMarkersVisible); } [Test] public void TestAimMarker() { - AddStep("enable aim markers", () => settings.AimMarkersEnabled.Value = true); + AddStep("enable aim markers", () => settings.ShowAimMarkers.Value = true); AddAssert("aim markers visible", () => analysisContainer.AimMarkersVisible); - AddStep("disable aim markers", () => settings.AimMarkersEnabled.Value = false); + AddStep("disable aim markers", () => settings.ShowAimMarkers.Value = false); AddAssert("aim markers not visible", () => !analysisContainer.AimMarkersVisible); } [Test] public void TestAimLines() { - AddStep("enable aim lines", () => settings.AimLinesEnabled.Value = true); + AddStep("enable aim lines", () => settings.ShowCursorPath.Value = true); AddAssert("aim lines visible", () => analysisContainer.AimLinesVisible); - AddStep("disable aim lines", () => settings.AimLinesEnabled.Value = false); + AddStep("disable aim lines", () => settings.ShowCursorPath.Value = false); AddAssert("aim lines not visible", () => !analysisContainer.AimLinesVisible); } diff --git a/osu.Game.Rulesets.Osu/Configuration/OsuRulesetConfigManager.cs b/osu.Game.Rulesets.Osu/Configuration/OsuRulesetConfigManager.cs index df5cd55c33..e6002523b1 100644 --- a/osu.Game.Rulesets.Osu/Configuration/OsuRulesetConfigManager.cs +++ b/osu.Game.Rulesets.Osu/Configuration/OsuRulesetConfigManager.cs @@ -23,9 +23,9 @@ namespace osu.Game.Rulesets.Osu.Configuration SetDefault(OsuRulesetSetting.ShowCursorRipples, false); SetDefault(OsuRulesetSetting.PlayfieldBorderStyle, PlayfieldBorderStyle.None); - SetDefault(OsuRulesetSetting.ReplayHitMarkersEnabled, false); - SetDefault(OsuRulesetSetting.ReplayAimMarkersEnabled, false); - SetDefault(OsuRulesetSetting.ReplayAimLinesEnabled, false); + SetDefault(OsuRulesetSetting.ReplayClickMarkersEnabled, false); + SetDefault(OsuRulesetSetting.ReplayFrameMarkersEnabled, false); + SetDefault(OsuRulesetSetting.ReplayCursorPathEnabled, false); SetDefault(OsuRulesetSetting.ReplayCursorHideEnabled, false); } } @@ -39,9 +39,9 @@ namespace osu.Game.Rulesets.Osu.Configuration PlayfieldBorderStyle, // Replay - ReplayHitMarkersEnabled, - ReplayAimMarkersEnabled, - ReplayAimLinesEnabled, + ReplayClickMarkersEnabled, + ReplayFrameMarkersEnabled, + ReplayCursorPathEnabled, ReplayCursorHideEnabled, } } diff --git a/osu.Game.Rulesets.Osu/UI/ReplayAnalysisOverlay.cs b/osu.Game.Rulesets.Osu/UI/ReplayAnalysisOverlay.cs index eb1ef49e5d..622c32c51e 100644 --- a/osu.Game.Rulesets.Osu/UI/ReplayAnalysisOverlay.cs +++ b/osu.Game.Rulesets.Osu/UI/ReplayAnalysisOverlay.cs @@ -14,9 +14,9 @@ namespace osu.Game.Rulesets.Osu.UI { public partial class ReplayAnalysisOverlay : CompositeDrawable { - private BindableBool hitMarkersEnabled { get; } = new BindableBool(); - private BindableBool aimMarkersEnabled { get; } = new BindableBool(); - private BindableBool aimLinesEnabled { get; } = new BindableBool(); + private BindableBool showClickMarkers { get; } = new BindableBool(); + private BindableBool showFrameMarkers { get; } = new BindableBool(); + private BindableBool showCursorPath { get; } = new BindableBool(); protected readonly ClickMarkerContainer ClickMarkers; protected readonly FrameMarkerContainer FrameMarkers; @@ -43,18 +43,18 @@ namespace osu.Game.Rulesets.Osu.UI { loadReplay(); - config.BindWith(OsuRulesetSetting.ReplayHitMarkersEnabled, hitMarkersEnabled); - config.BindWith(OsuRulesetSetting.ReplayAimMarkersEnabled, aimMarkersEnabled); - config.BindWith(OsuRulesetSetting.ReplayAimLinesEnabled, aimLinesEnabled); + config.BindWith(OsuRulesetSetting.ReplayClickMarkersEnabled, showClickMarkers); + config.BindWith(OsuRulesetSetting.ReplayFrameMarkersEnabled, showFrameMarkers); + config.BindWith(OsuRulesetSetting.ReplayCursorPathEnabled, showCursorPath); } protected override void LoadComplete() { base.LoadComplete(); - hitMarkersEnabled.BindValueChanged(enabled => ClickMarkers.FadeTo(enabled.NewValue ? 1 : 0), true); - aimMarkersEnabled.BindValueChanged(enabled => FrameMarkers.FadeTo(enabled.NewValue ? 1 : 0), true); - aimLinesEnabled.BindValueChanged(enabled => CursorPath.FadeTo(enabled.NewValue ? 1 : 0), true); + showClickMarkers.BindValueChanged(enabled => ClickMarkers.FadeTo(enabled.NewValue ? 1 : 0), true); + showFrameMarkers.BindValueChanged(enabled => FrameMarkers.FadeTo(enabled.NewValue ? 1 : 0), true); + showCursorPath.BindValueChanged(enabled => CursorPath.FadeTo(enabled.NewValue ? 1 : 0), true); } private void loadReplay() diff --git a/osu.Game.Rulesets.Osu/UI/ReplayAnalysisSettings.cs b/osu.Game.Rulesets.Osu/UI/ReplayAnalysisSettings.cs index dd09ee146b..7daab68180 100644 --- a/osu.Game.Rulesets.Osu/UI/ReplayAnalysisSettings.cs +++ b/osu.Game.Rulesets.Osu/UI/ReplayAnalysisSettings.cs @@ -13,17 +13,17 @@ namespace osu.Game.Rulesets.Osu.UI { private readonly OsuRulesetConfigManager config; - [SettingSource("Hit markers", SettingControlType = typeof(PlayerCheckbox))] - public BindableBool HitMarkersEnabled { get; } = new BindableBool(); + [SettingSource("Show click markers", SettingControlType = typeof(PlayerCheckbox))] + public BindableBool ShowClickMarkers { get; } = new BindableBool(); - [SettingSource("Aim markers", SettingControlType = typeof(PlayerCheckbox))] - public BindableBool AimMarkersEnabled { get; } = new BindableBool(); + [SettingSource("Show frame markers", SettingControlType = typeof(PlayerCheckbox))] + public BindableBool ShowAimMarkers { get; } = new BindableBool(); - [SettingSource("Aim lines", SettingControlType = typeof(PlayerCheckbox))] - public BindableBool AimLinesEnabled { get; } = new BindableBool(); + [SettingSource("Show cursor path", SettingControlType = typeof(PlayerCheckbox))] + public BindableBool ShowCursorPath { get; } = new BindableBool(); - [SettingSource("Hide cursor", SettingControlType = typeof(PlayerCheckbox))] - public BindableBool CursorHideEnabled { get; } = new BindableBool(); + [SettingSource("Hide gameplay cursor", SettingControlType = typeof(PlayerCheckbox))] + public BindableBool HideSkinCursor { get; } = new BindableBool(); public ReplayAnalysisSettings(OsuRulesetConfigManager config) : base("Analysis Settings") @@ -36,10 +36,10 @@ namespace osu.Game.Rulesets.Osu.UI { AddRange(this.CreateSettingsControls()); - config.BindWith(OsuRulesetSetting.ReplayHitMarkersEnabled, HitMarkersEnabled); - config.BindWith(OsuRulesetSetting.ReplayAimMarkersEnabled, AimMarkersEnabled); - config.BindWith(OsuRulesetSetting.ReplayAimLinesEnabled, AimLinesEnabled); - config.BindWith(OsuRulesetSetting.ReplayCursorHideEnabled, CursorHideEnabled); + config.BindWith(OsuRulesetSetting.ReplayClickMarkersEnabled, ShowClickMarkers); + config.BindWith(OsuRulesetSetting.ReplayFrameMarkersEnabled, ShowAimMarkers); + config.BindWith(OsuRulesetSetting.ReplayCursorPathEnabled, ShowCursorPath); + config.BindWith(OsuRulesetSetting.ReplayCursorHideEnabled, HideSkinCursor); } } } From 0f01a855afd2a4585065eb41f4ee1ab49cdbc0d7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 5 Sep 2024 15:30:42 +0900 Subject: [PATCH 2210/2493] Add note about cursor hiding being potentially flaky --- osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs b/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs index c6ad10c062..16edc654a7 100644 --- a/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs @@ -48,6 +48,9 @@ namespace osu.Game.Rulesets.Osu.UI replayPlayer.AddSettings(new ReplayAnalysisSettings(Config)); cursorHideEnabled = Config.GetBindable(OsuRulesetSetting.ReplayCursorHideEnabled); + + // I have little faith in this working (other things touch cursor visibility) but haven't broken it yet. + // Let's wait for someone to report an issue before spending too much time on it. cursorHideEnabled.BindValueChanged(enabled => Playfield.Cursor.FadeTo(enabled.NewValue ? 0 : 1), true); } } From a1cf67be629e7f3c4dedaac9aadab6b3f0f73598 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 5 Sep 2024 15:53:53 +0900 Subject: [PATCH 2211/2493] Add setting to adjust replay analysis display length --- .../Configuration/OsuRulesetConfigManager.cs | 2 + .../UI/ReplayAnalysis/AnalysisFrameEntry.cs | 4 +- .../UI/ReplayAnalysisOverlay.cs | 70 +++++++++++++------ .../UI/ReplayAnalysisSettings.cs | 10 +++ 4 files changed, 64 insertions(+), 22 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Configuration/OsuRulesetConfigManager.cs b/osu.Game.Rulesets.Osu/Configuration/OsuRulesetConfigManager.cs index e6002523b1..8a8b78b645 100644 --- a/osu.Game.Rulesets.Osu/Configuration/OsuRulesetConfigManager.cs +++ b/osu.Game.Rulesets.Osu/Configuration/OsuRulesetConfigManager.cs @@ -27,6 +27,7 @@ namespace osu.Game.Rulesets.Osu.Configuration SetDefault(OsuRulesetSetting.ReplayFrameMarkersEnabled, false); SetDefault(OsuRulesetSetting.ReplayCursorPathEnabled, false); SetDefault(OsuRulesetSetting.ReplayCursorHideEnabled, false); + SetDefault(OsuRulesetSetting.ReplayAnalysisDisplayLength, 750); } } @@ -43,5 +44,6 @@ namespace osu.Game.Rulesets.Osu.Configuration ReplayFrameMarkersEnabled, ReplayCursorPathEnabled, ReplayCursorHideEnabled, + ReplayAnalysisDisplayLength, } } diff --git a/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/AnalysisFrameEntry.cs b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/AnalysisFrameEntry.cs index d44def1b67..116bccc747 100644 --- a/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/AnalysisFrameEntry.cs +++ b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/AnalysisFrameEntry.cs @@ -12,10 +12,10 @@ namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis public Vector2 Position { get; } - public AnalysisFrameEntry(double time, Vector2 position, params OsuAction[] action) + public AnalysisFrameEntry(double time, double displayLength, Vector2 position, params OsuAction[] action) { LifetimeStart = time; - LifetimeEnd = time + 1_000; + LifetimeEnd = time + displayLength; Position = position; Action = action; } diff --git a/osu.Game.Rulesets.Osu/UI/ReplayAnalysisOverlay.cs b/osu.Game.Rulesets.Osu/UI/ReplayAnalysisOverlay.cs index 622c32c51e..0c257a68c5 100644 --- a/osu.Game.Rulesets.Osu/UI/ReplayAnalysisOverlay.cs +++ b/osu.Game.Rulesets.Osu/UI/ReplayAnalysisOverlay.cs @@ -17,10 +17,11 @@ namespace osu.Game.Rulesets.Osu.UI private BindableBool showClickMarkers { get; } = new BindableBool(); private BindableBool showFrameMarkers { get; } = new BindableBool(); private BindableBool showCursorPath { get; } = new BindableBool(); + private BindableInt displayLength { get; } = new BindableInt(); - protected readonly ClickMarkerContainer ClickMarkers; - protected readonly FrameMarkerContainer FrameMarkers; - protected readonly CursorPathContainer CursorPath; + protected ClickMarkerContainer ClickMarkers = null!; + protected FrameMarkerContainer FrameMarkers = null!; + protected CursorPathContainer CursorPath = null!; private readonly Replay replay; @@ -29,36 +30,65 @@ namespace osu.Game.Rulesets.Osu.UI RelativeSizeAxes = Axes.Both; this.replay = replay; - - InternalChildren = new Drawable[] - { - CursorPath = new CursorPathContainer(), - ClickMarkers = new ClickMarkerContainer(), - FrameMarkers = new FrameMarkerContainer(), - }; } + private bool requireDisplay => showClickMarkers.Value || showFrameMarkers.Value || showCursorPath.Value; + [BackgroundDependencyLoader] private void load(OsuRulesetConfigManager config) { - loadReplay(); - config.BindWith(OsuRulesetSetting.ReplayClickMarkersEnabled, showClickMarkers); config.BindWith(OsuRulesetSetting.ReplayFrameMarkersEnabled, showFrameMarkers); config.BindWith(OsuRulesetSetting.ReplayCursorPathEnabled, showCursorPath); + config.BindWith(OsuRulesetSetting.ReplayAnalysisDisplayLength, displayLength); } protected override void LoadComplete() { base.LoadComplete(); - showClickMarkers.BindValueChanged(enabled => ClickMarkers.FadeTo(enabled.NewValue ? 1 : 0), true); - showFrameMarkers.BindValueChanged(enabled => FrameMarkers.FadeTo(enabled.NewValue ? 1 : 0), true); - showCursorPath.BindValueChanged(enabled => CursorPath.FadeTo(enabled.NewValue ? 1 : 0), true); + showClickMarkers.BindValueChanged(enabled => + { + initialise(); + ClickMarkers.FadeTo(enabled.NewValue ? 1 : 0); + }, true); + showFrameMarkers.BindValueChanged(enabled => + { + initialise(); + FrameMarkers.FadeTo(enabled.NewValue ? 1 : 0); + }, true); + showCursorPath.BindValueChanged(enabled => + { + initialise(); + CursorPath.FadeTo(enabled.NewValue ? 1 : 0); + }, true); + displayLength.BindValueChanged(_ => + { + isLoaded = false; + initialise(); + }, true); } - private void loadReplay() + private bool isLoaded; + + private void initialise() { + if (!requireDisplay) + return; + + if (isLoaded) + return; + + isLoaded = true; + + // It's faster to reinitialise the whole drawable stack than use `Clear` on `PooledDrawableWithLifetimeContainer` + InternalChildren = new Drawable[] + { + CursorPath = new CursorPathContainer(), + ClickMarkers = new ClickMarkerContainer(), + FrameMarkers = new FrameMarkerContainer(), + }; + bool leftHeld = false; bool rightHeld = false; @@ -74,7 +104,7 @@ namespace osu.Game.Rulesets.Osu.UI else if (!leftHeld && leftButton) { leftHeld = true; - ClickMarkers.Add(new AnalysisFrameEntry(osuFrame.Time, osuFrame.Position, OsuAction.LeftButton)); + ClickMarkers.Add(new AnalysisFrameEntry(osuFrame.Time, displayLength.Value, osuFrame.Position, OsuAction.LeftButton)); } if (rightHeld && !rightButton) @@ -82,11 +112,11 @@ namespace osu.Game.Rulesets.Osu.UI else if (!rightHeld && rightButton) { rightHeld = true; - ClickMarkers.Add(new AnalysisFrameEntry(osuFrame.Time, osuFrame.Position, OsuAction.RightButton)); + ClickMarkers.Add(new AnalysisFrameEntry(osuFrame.Time, displayLength.Value, osuFrame.Position, OsuAction.RightButton)); } - FrameMarkers.Add(new AnalysisFrameEntry(osuFrame.Time, osuFrame.Position, osuFrame.Actions.ToArray())); - CursorPath.Add(new AnalysisFrameEntry(osuFrame.Time, osuFrame.Position)); + FrameMarkers.Add(new AnalysisFrameEntry(osuFrame.Time, displayLength.Value, osuFrame.Position, osuFrame.Actions.ToArray())); + CursorPath.Add(new AnalysisFrameEntry(osuFrame.Time, displayLength.Value, osuFrame.Position)); } } } diff --git a/osu.Game.Rulesets.Osu/UI/ReplayAnalysisSettings.cs b/osu.Game.Rulesets.Osu/UI/ReplayAnalysisSettings.cs index 7daab68180..6acafb5d3b 100644 --- a/osu.Game.Rulesets.Osu/UI/ReplayAnalysisSettings.cs +++ b/osu.Game.Rulesets.Osu/UI/ReplayAnalysisSettings.cs @@ -25,6 +25,15 @@ namespace osu.Game.Rulesets.Osu.UI [SettingSource("Hide gameplay cursor", SettingControlType = typeof(PlayerCheckbox))] public BindableBool HideSkinCursor { get; } = new BindableBool(); + [SettingSource("Display length", SettingControlType = typeof(PlayerSliderBar))] + public BindableInt DisplayLength { get; } = new BindableInt + { + MinValue = 100, + Default = 800, + MaxValue = 2000, + Precision = 100, + }; + public ReplayAnalysisSettings(OsuRulesetConfigManager config) : base("Analysis Settings") { @@ -40,6 +49,7 @@ namespace osu.Game.Rulesets.Osu.UI config.BindWith(OsuRulesetSetting.ReplayFrameMarkersEnabled, ShowAimMarkers); config.BindWith(OsuRulesetSetting.ReplayCursorPathEnabled, ShowCursorPath); config.BindWith(OsuRulesetSetting.ReplayCursorHideEnabled, HideSkinCursor); + config.BindWith(OsuRulesetSetting.ReplayAnalysisDisplayLength, DisplayLength); } } } From 167e3a337796d925025cba8497300734d6f76a14 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 5 Sep 2024 16:01:28 +0900 Subject: [PATCH 2212/2493] Make loading asynchronous --- .../UI/ReplayAnalysisOverlay.cs | 59 +++++++++++-------- .../UI/ReplayAnalysisSettings.cs | 6 +- 2 files changed, 36 insertions(+), 29 deletions(-) diff --git a/osu.Game.Rulesets.Osu/UI/ReplayAnalysisOverlay.cs b/osu.Game.Rulesets.Osu/UI/ReplayAnalysisOverlay.cs index 0c257a68c5..8a48e81111 100644 --- a/osu.Game.Rulesets.Osu/UI/ReplayAnalysisOverlay.cs +++ b/osu.Game.Rulesets.Osu/UI/ReplayAnalysisOverlay.cs @@ -1,8 +1,10 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Threading; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Caching; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Replays; @@ -19,9 +21,9 @@ namespace osu.Game.Rulesets.Osu.UI private BindableBool showCursorPath { get; } = new BindableBool(); private BindableInt displayLength { get; } = new BindableInt(); - protected ClickMarkerContainer ClickMarkers = null!; - protected FrameMarkerContainer FrameMarkers = null!; - protected CursorPathContainer CursorPath = null!; + protected ClickMarkerContainer? ClickMarkers; + protected FrameMarkerContainer? FrameMarkers; + protected CursorPathContainer? CursorPath; private readonly Replay replay; @@ -47,42 +49,43 @@ namespace osu.Game.Rulesets.Osu.UI { base.LoadComplete(); - showClickMarkers.BindValueChanged(enabled => - { - initialise(); - ClickMarkers.FadeTo(enabled.NewValue ? 1 : 0); - }, true); - showFrameMarkers.BindValueChanged(enabled => - { - initialise(); - FrameMarkers.FadeTo(enabled.NewValue ? 1 : 0); - }, true); - showCursorPath.BindValueChanged(enabled => - { - initialise(); - CursorPath.FadeTo(enabled.NewValue ? 1 : 0); - }, true); displayLength.BindValueChanged(_ => { - isLoaded = false; - initialise(); + // Need to fully reload to make this work. + loaded.Invalidate(); }, true); } - private bool isLoaded; + private readonly Cached loaded = new Cached(); + + private CancellationTokenSource? generationCancellationSource; + + protected override void Update() + { + base.Update(); + + if (requireDisplay) + { + initialise(); + + if (ClickMarkers != null) ClickMarkers.Alpha = showClickMarkers.Value ? 1 : 0; + if (FrameMarkers != null) FrameMarkers.Alpha = showFrameMarkers.Value ? 1 : 0; + if (CursorPath != null) CursorPath.Alpha = showCursorPath.Value ? 1 : 0; + } + } private void initialise() { - if (!requireDisplay) + if (loaded.IsValid) return; - if (isLoaded) - return; + loaded.Validate(); - isLoaded = true; + generationCancellationSource?.Cancel(); + generationCancellationSource = new CancellationTokenSource(); // It's faster to reinitialise the whole drawable stack than use `Clear` on `PooledDrawableWithLifetimeContainer` - InternalChildren = new Drawable[] + var newDrawables = new Drawable[] { CursorPath = new CursorPathContainer(), ClickMarkers = new ClickMarkerContainer(), @@ -92,6 +95,8 @@ namespace osu.Game.Rulesets.Osu.UI bool leftHeld = false; bool rightHeld = false; + // This should probably be async as well, but it's a bit of a pain to debounce and everything. + // Let's address concerns when they are raised. foreach (var frame in replay.Frames) { var osuFrame = (OsuReplayFrame)frame; @@ -118,6 +123,8 @@ namespace osu.Game.Rulesets.Osu.UI FrameMarkers.Add(new AnalysisFrameEntry(osuFrame.Time, displayLength.Value, osuFrame.Position, osuFrame.Actions.ToArray())); CursorPath.Add(new AnalysisFrameEntry(osuFrame.Time, displayLength.Value, osuFrame.Position)); } + + LoadComponentsAsync(newDrawables, drawables => InternalChildrenEnumerable = drawables, generationCancellationSource.Token); } } } diff --git a/osu.Game.Rulesets.Osu/UI/ReplayAnalysisSettings.cs b/osu.Game.Rulesets.Osu/UI/ReplayAnalysisSettings.cs index 6acafb5d3b..dc4730d76a 100644 --- a/osu.Game.Rulesets.Osu/UI/ReplayAnalysisSettings.cs +++ b/osu.Game.Rulesets.Osu/UI/ReplayAnalysisSettings.cs @@ -28,10 +28,10 @@ namespace osu.Game.Rulesets.Osu.UI [SettingSource("Display length", SettingControlType = typeof(PlayerSliderBar))] public BindableInt DisplayLength { get; } = new BindableInt { - MinValue = 100, - Default = 800, + MinValue = 200, MaxValue = 2000, - Precision = 100, + Default = 800, + Precision = 200, }; public ReplayAnalysisSettings(OsuRulesetConfigManager config) From 7136483f85461c73d714a7588cb9f8a6a193632d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 5 Sep 2024 09:45:34 +0200 Subject: [PATCH 2213/2493] Fix nullability inspections --- .../TestSceneOsuAnalysisContainer.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuAnalysisContainer.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuAnalysisContainer.cs index fb8ac81b2c..d72a347675 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuAnalysisContainer.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuAnalysisContainer.cs @@ -125,9 +125,9 @@ namespace osu.Game.Rulesets.Osu.Tests { } - public bool HitMarkersVisible => ClickMarkers.Alpha > 0 && ClickMarkers.Entries.Any(); - public bool AimMarkersVisible => FrameMarkers.Alpha > 0 && FrameMarkers.Entries.Any(); - public bool AimLinesVisible => CursorPath.Alpha > 0 && CursorPath.Vertices.Count > 1; + public bool HitMarkersVisible => ClickMarkers?.Alpha > 0 && ClickMarkers.Entries.Any(); + public bool AimMarkersVisible => FrameMarkers?.Alpha > 0 && FrameMarkers.Entries.Any(); + public bool AimLinesVisible => CursorPath?.Alpha > 0 && CursorPath.Vertices.Count > 1; } } } From b9ddac420171e1ecfbcb4374538d8c1c117a92a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 5 Sep 2024 09:45:37 +0200 Subject: [PATCH 2214/2493] Fix test failures --- .../TestSceneOsuAnalysisContainer.cs | 12 ++++++------ osu.Game.Rulesets.Osu/UI/ReplayAnalysisOverlay.cs | 8 +++----- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuAnalysisContainer.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuAnalysisContainer.cs index d72a347675..184938ceda 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuAnalysisContainer.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuAnalysisContainer.cs @@ -61,27 +61,27 @@ namespace osu.Game.Rulesets.Osu.Tests public void TestHitMarkers() { AddStep("enable hit markers", () => settings.ShowClickMarkers.Value = true); - AddAssert("hit markers visible", () => analysisContainer.HitMarkersVisible); + AddUntilStep("hit markers visible", () => analysisContainer.HitMarkersVisible); AddStep("disable hit markers", () => settings.ShowClickMarkers.Value = false); - AddAssert("hit markers not visible", () => !analysisContainer.HitMarkersVisible); + AddUntilStep("hit markers not visible", () => !analysisContainer.HitMarkersVisible); } [Test] public void TestAimMarker() { AddStep("enable aim markers", () => settings.ShowAimMarkers.Value = true); - AddAssert("aim markers visible", () => analysisContainer.AimMarkersVisible); + AddUntilStep("aim markers visible", () => analysisContainer.AimMarkersVisible); AddStep("disable aim markers", () => settings.ShowAimMarkers.Value = false); - AddAssert("aim markers not visible", () => !analysisContainer.AimMarkersVisible); + AddUntilStep("aim markers not visible", () => !analysisContainer.AimMarkersVisible); } [Test] public void TestAimLines() { AddStep("enable aim lines", () => settings.ShowCursorPath.Value = true); - AddAssert("aim lines visible", () => analysisContainer.AimLinesVisible); + AddUntilStep("aim lines visible", () => analysisContainer.AimLinesVisible); AddStep("disable aim lines", () => settings.ShowCursorPath.Value = false); - AddAssert("aim lines not visible", () => !analysisContainer.AimLinesVisible); + AddUntilStep("aim lines not visible", () => !analysisContainer.AimLinesVisible); } private Replay fabricateReplay() diff --git a/osu.Game.Rulesets.Osu/UI/ReplayAnalysisOverlay.cs b/osu.Game.Rulesets.Osu/UI/ReplayAnalysisOverlay.cs index 8a48e81111..2b7f6c9fc9 100644 --- a/osu.Game.Rulesets.Osu/UI/ReplayAnalysisOverlay.cs +++ b/osu.Game.Rulesets.Osu/UI/ReplayAnalysisOverlay.cs @@ -65,13 +65,11 @@ namespace osu.Game.Rulesets.Osu.UI base.Update(); if (requireDisplay) - { initialise(); - if (ClickMarkers != null) ClickMarkers.Alpha = showClickMarkers.Value ? 1 : 0; - if (FrameMarkers != null) FrameMarkers.Alpha = showFrameMarkers.Value ? 1 : 0; - if (CursorPath != null) CursorPath.Alpha = showCursorPath.Value ? 1 : 0; - } + if (ClickMarkers != null) ClickMarkers.Alpha = showClickMarkers.Value ? 1 : 0; + if (FrameMarkers != null) FrameMarkers.Alpha = showFrameMarkers.Value ? 1 : 0; + if (CursorPath != null) CursorPath.Alpha = showCursorPath.Value ? 1 : 0; } private void initialise() From 86a06c7e103a0a8a4584bdeb0229e5b1631e0869 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 5 Sep 2024 17:19:53 +0900 Subject: [PATCH 2215/2493] Fix high performance session potentially getting stuck after multiplayer spectator --- osu.Game/Screens/Play/PlayerLoader.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs index 12048ecbbe..7682bba9a6 100644 --- a/osu.Game/Screens/Play/PlayerLoader.cs +++ b/osu.Game/Screens/Play/PlayerLoader.cs @@ -573,6 +573,9 @@ namespace osu.Game.Screens.Play // if the player never got pushed, we should explicitly dispose it. DisposalTask = LoadTask?.ContinueWith(_ => CurrentPlayer?.Dispose()); } + + highPerformanceSession?.Dispose(); + highPerformanceSession = null; } #endregion From e1b763ff0db4de395f93171aa567cb41e7dd9171 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 5 Sep 2024 11:21:59 +0200 Subject: [PATCH 2216/2493] Apply review suggestions wrt border appearance --- osu.Game/Graphics/UserInterfaceV2/FormTextBox.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Graphics/UserInterfaceV2/FormTextBox.cs b/osu.Game/Graphics/UserInterfaceV2/FormTextBox.cs index 985eb74662..044576c635 100644 --- a/osu.Game/Graphics/UserInterfaceV2/FormTextBox.cs +++ b/osu.Game/Graphics/UserInterfaceV2/FormTextBox.cs @@ -80,6 +80,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 Masking = true; CornerRadius = 5; + CornerExponent = 2.5f; InternalChildren = new Drawable[] { @@ -178,7 +179,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 if (!disabled) { - BorderThickness = IsHovered || textBox.Focused.Value ? 3 : 0; + BorderThickness = IsHovered || textBox.Focused.Value ? 2 : 0; BorderColour = textBox.Focused.Value ? colourProvider.Highlight1 : colourProvider.Light4; if (textBox.Focused.Value) From 791ce218fcd4c8bff495f869d92d1d25a63d23bc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 5 Sep 2024 18:55:11 +0900 Subject: [PATCH 2217/2493] Add test coverage of beatmap offset edge case failure --- .../Gameplay/TestSceneBeatmapOffsetControl.cs | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapOffsetControl.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapOffsetControl.cs index 3b88750013..c7f1eabab2 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapOffsetControl.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapOffsetControl.cs @@ -5,6 +5,7 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Graphics; using osu.Framework.Testing; +using osu.Game.Beatmaps; using osu.Game.Overlays.Settings; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.Mods; @@ -136,6 +137,59 @@ namespace osu.Game.Tests.Visual.Gameplay AddAssert("No calibration button", () => !offsetControl.ChildrenOfType().Any()); } + [Test] + public void TestCalibrationFromNonZeroWithImmediateReferenceScore() + { + const double average_error = -4.5; + const double initial_offset = -2; + + AddStep("Set beatmap offset non-neutral", () => Realm.Write(r => + { + r.Add(new BeatmapInfo + { + ID = Beatmap.Value.BeatmapInfo.ID, + Ruleset = Beatmap.Value.BeatmapInfo.Ruleset, + UserSettings = + { + Offset = initial_offset, + } + }); + })); + + AddStep("Create control with preloaded reference score", () => + { + Child = new PlayerSettingsGroup("Some settings") + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Children = new Drawable[] + { + offsetControl = new BeatmapOffsetControl + { + ReferenceScore = + { + Value = new ScoreInfo + { + HitEvents = TestSceneHitEventTimingDistributionGraph.CreateDistributedHitEvents(average_error), + BeatmapInfo = Beatmap.Value.BeatmapInfo, + } + } + } + } + }; + }); + + AddUntilStep("Has calibration button", () => offsetControl.ChildrenOfType().Any()); + AddStep("Press button", () => offsetControl.ChildrenOfType().Single().TriggerClick()); + AddAssert("Offset is adjusted", () => offsetControl.Current.Value, () => Is.EqualTo(initial_offset - average_error)); + + AddUntilStep("Button is disabled", () => !offsetControl.ChildrenOfType().Single().Enabled.Value); + AddStep("Remove reference score", () => offsetControl.ReferenceScore.Value = null); + AddAssert("No calibration button", () => !offsetControl.ChildrenOfType().Any()); + + AddStep("Clean up beatmap", () => Realm.Write(r => r.RemoveAll())); + } + [Test] public void TestCalibrationNoChange() { From 37f61b26ea858b53e29aefc784b1563b8ed56c59 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 5 Sep 2024 18:45:49 +0900 Subject: [PATCH 2218/2493] Fix offset adjust control not correctly applying changes after song select quit This is an interesting scenario where we arrive at a fresh `BeatmapOffsetControl` but with a reference score (from the last play). Our best assumption here is that the beatmap's offset hasn't changed since the last play, so we want to use it for the `lastPlayBeatmapOffset`. But due to unfortunate order of execution, `Current.Value` was not yet initialised. --- osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs index 7668d3e635..f312fb0ec5 100644 --- a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs +++ b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs @@ -104,8 +104,6 @@ namespace osu.Game.Screens.Play.PlayerSettings { base.LoadComplete(); - ReferenceScore.BindValueChanged(scoreChanged, true); - beatmapOffsetSubscription = realm.SubscribeToPropertyChanged( r => r.Find(beatmap.Value.BeatmapInfo.ID)?.UserSettings, settings => settings.Offset, @@ -124,6 +122,7 @@ namespace osu.Game.Screens.Play.PlayerSettings }); Current.BindValueChanged(currentChanged); + ReferenceScore.BindValueChanged(scoreChanged, true); } private void currentChanged(ValueChangedEvent offset) From e94e08fec3448012c041a301c5d76f1ff0776ee6 Mon Sep 17 00:00:00 2001 From: Sheppsu <49356627+Sheppsu@users.noreply.github.com> Date: Thu, 5 Sep 2024 20:14:36 -0400 Subject: [PATCH 2219/2493] fix marker depth when rewinding --- osu.Game.Rulesets.Osu/UI/ReplayAnalysis/AnalysisMarker.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/AnalysisMarker.cs b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/AnalysisMarker.cs index 9b602c88a8..187876d691 100644 --- a/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/AnalysisMarker.cs +++ b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/AnalysisMarker.cs @@ -22,6 +22,7 @@ namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis protected override void OnApply(AnalysisFrameEntry entry) { Position = entry.Position; + Depth = -(float)entry.LifetimeEnd; } } } From 36a30cf0772d67743032b168886b1f05fd08bc36 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 6 Sep 2024 16:01:47 +0900 Subject: [PATCH 2220/2493] Add note about using hard links in the future --- osu.Game/Database/RealmArchiveModelImporter.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Database/RealmArchiveModelImporter.cs b/osu.Game/Database/RealmArchiveModelImporter.cs index 38df2ac1dc..cf0625c51c 100644 --- a/osu.Game/Database/RealmArchiveModelImporter.cs +++ b/osu.Game/Database/RealmArchiveModelImporter.cs @@ -195,6 +195,7 @@ namespace osu.Game.Database Directory.CreateDirectory(Path.GetDirectoryName(destinationPath)!); + // Consider using hard links here to make this instant. using (var inStream = Files.Storage.GetStream(sourcePath)) using (var outStream = File.Create(destinationPath)) await inStream.CopyToAsync(outStream).ConfigureAwait(false); From 9f834ca1a2db21eea77dd687e8dbdef1ad7932eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 6 Sep 2024 10:24:05 +0200 Subject: [PATCH 2221/2493] Silence beatmap retrieval failures from results screen favourite button As reported (very poorly) in https://github.com/ppy/osu/pull/28991#issuecomment-2331854970. I believe this is a total edge case and is mostly visible on dev due to some beatmaps existing on `osu.ppy.sh` and not on `dev.ppy.sh`, but I tend to agree in general that these types of failures should not be firing very loud error notifications; logging to network and disabling the button with a tooltip adjustment should be enough. --- osu.Game/Screens/Ranking/FavouriteButton.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Ranking/FavouriteButton.cs b/osu.Game/Screens/Ranking/FavouriteButton.cs index aecaf7c5b9..019b80dde9 100644 --- a/osu.Game/Screens/Ranking/FavouriteButton.cs +++ b/osu.Game/Screens/Ranking/FavouriteButton.cs @@ -76,12 +76,13 @@ namespace osu.Game.Screens.Ranking }; beatmapSetRequest.Failure += e => { - Logger.Error(e, $"Failed to fetch beatmap info: {e.Message}"); + Logger.Log($"Favourite button failed to fetch beatmap info: {e}", LoggingTarget.Network); Schedule(() => { loading.Hide(); Enabled.Value = false; + TooltipText = "this beatmap cannot be favourited"; }); }; api.Queue(beatmapSetRequest); From 6913d75792585bab7f0c649dd6b5687e05753d33 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 6 Sep 2024 18:04:11 +0900 Subject: [PATCH 2222/2493] Add 'yes'/'no' acronyms to the `played=` filter --- osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs | 4 ++++ osu.Game/Screens/Select/FilterQueryParser.cs | 2 ++ 2 files changed, 6 insertions(+) diff --git a/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs b/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs index e6006b7fd2..9ecfa72947 100644 --- a/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs +++ b/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs @@ -633,11 +633,15 @@ namespace osu.Game.Tests.NonVisual.Filtering new object[] { "0", DateTimeOffset.Now, false }, new object[] { "false", DateTimeOffset.MinValue, true }, new object[] { "false", DateTimeOffset.Now, false }, + new object[] { "no", DateTimeOffset.MinValue, true }, + new object[] { "no", DateTimeOffset.Now, false }, new object[] { "1", DateTimeOffset.MinValue, false }, new object[] { "1", DateTimeOffset.Now, true }, new object[] { "true", DateTimeOffset.MinValue, false }, new object[] { "true", DateTimeOffset.Now, true }, + new object[] { "yes", DateTimeOffset.MinValue, false }, + new object[] { "yes", DateTimeOffset.Now, true }, }; [Test] diff --git a/osu.Game/Screens/Select/FilterQueryParser.cs b/osu.Game/Screens/Select/FilterQueryParser.cs index 3e0dba59f0..6c9a95a250 100644 --- a/osu.Game/Screens/Select/FilterQueryParser.cs +++ b/osu.Game/Screens/Select/FilterQueryParser.cs @@ -159,10 +159,12 @@ namespace osu.Game.Screens.Select switch (value) { case "1": + case "yes": result = true; return true; case "0": + case "no": result = false; return true; From 2c19b7994c70a3b1d0799add0b1018bf9ad7fa6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 6 Sep 2024 11:38:50 +0200 Subject: [PATCH 2223/2493] Implement "form" check box control --- .../UserInterface/TestSceneFormControls.cs | 16 +- .../Graphics/UserInterfaceV2/FormCheckBox.cs | 155 ++++++++++++++++++ 2 files changed, 170 insertions(+), 1 deletion(-) create mode 100644 osu.Game/Graphics/UserInterfaceV2/FormCheckBox.cs diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneFormControls.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneFormControls.cs index f5bc40c869..9c05a34010 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneFormControls.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneFormControls.cs @@ -6,6 +6,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; using osu.Game.Graphics.Cursor; using osu.Game.Graphics.UserInterfaceV2; +using osu.Game.Localisation; using osuTK; namespace osu.Game.Tests.Visual.UserInterface @@ -53,9 +54,22 @@ namespace osu.Game.Tests.Visual.UserInterface PlaceholderText = "Mine is 42!", TabbableContentContainer = this, }, + new FormCheckBox + { + Caption = EditorSetupStrings.LetterboxDuringBreaks, + HintText = EditorSetupStrings.LetterboxDuringBreaksDescription, + OnText = "Letterbox", + OffText = "Do not letterbox", + }, + new FormCheckBox + { + Caption = EditorSetupStrings.LetterboxDuringBreaks, + HintText = EditorSetupStrings.LetterboxDuringBreaksDescription, + Current = { Disabled = true }, + }, }, }, - }, + } }; } } diff --git a/osu.Game/Graphics/UserInterfaceV2/FormCheckBox.cs b/osu.Game/Graphics/UserInterfaceV2/FormCheckBox.cs new file mode 100644 index 0000000000..587aa921f5 --- /dev/null +++ b/osu.Game/Graphics/UserInterfaceV2/FormCheckBox.cs @@ -0,0 +1,155 @@ +// 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.Audio; +using osu.Framework.Audio.Sample; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.UserInterface; +using osu.Framework.Input.Events; +using osu.Framework.Localisation; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osu.Game.Overlays; + +namespace osu.Game.Graphics.UserInterfaceV2 +{ + public partial class FormCheckBox : CompositeDrawable, IHasCurrentValue + { + public Bindable Current + { + get => current.Current; + set => current.Current = value; + } + + private readonly BindableWithCurrent current = new BindableWithCurrent(); + + public LocalisableString Caption { get; init; } + public LocalisableString HintText { get; init; } + public LocalisableString OnText { get; init; } = "On"; + public LocalisableString OffText { get; init; } = "Off"; + + private Box background = null!; + private FormFieldCaption caption = null!; + private OsuSpriteText text = null!; + private Nub checkbox = null!; + + private Sample? sampleChecked; + private Sample? sampleUnchecked; + + [Resolved] + private OverlayColourProvider colourProvider { get; set; } = null!; + + [BackgroundDependencyLoader] + private void load(AudioManager audio) + { + RelativeSizeAxes = Axes.X; + Height = 50; + + Masking = true; + CornerRadius = 5; + CornerExponent = 2.5f; + + InternalChildren = new Drawable[] + { + background = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background5, + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding(9), + Children = new Drawable[] + { + caption = new FormFieldCaption + { + Caption = Caption, + TooltipText = HintText, + Anchor = Anchor.TopLeft, + Origin = Anchor.TopLeft, + }, + text = new OsuSpriteText + { + RelativeSizeAxes = Axes.X, + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + }, + checkbox = new Nub + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + Current = Current, + } + }, + }, + }; + + sampleChecked = audio.Samples.Get(@"UI/check-on"); + sampleUnchecked = audio.Samples.Get(@"UI/check-off"); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + current.BindValueChanged(_ => + { + updateState(); + playSamples(); + background.FlashColour(ColourInfo.GradientVertical(colourProvider.Background5, colourProvider.Dark2), 800, Easing.OutQuint); + }); + current.BindDisabledChanged(_ => updateState(), true); + } + + private void playSamples() + { + if (Current.Value) + sampleChecked?.Play(); + else + sampleUnchecked?.Play(); + } + + protected override bool OnHover(HoverEvent e) + { + updateState(); + return true; + } + + protected override void OnHoverLost(HoverLostEvent e) + { + base.OnHoverLost(e); + updateState(); + } + + protected override bool OnClick(ClickEvent e) + { + if (!Current.Disabled) + Current.Value = !Current.Value; + return true; + } + + private void updateState() + { + background.Colour = Current.Disabled ? colourProvider.Background4 : colourProvider.Background5; + caption.Colour = Current.Disabled ? colourProvider.Foreground1 : colourProvider.Content2; + checkbox.Colour = Current.Disabled ? colourProvider.Foreground1 : colourProvider.Content1; + text.Colour = Current.Disabled ? colourProvider.Foreground1 : colourProvider.Content1; + + text.Text = Current.Value ? OnText : OffText; + + if (!Current.Disabled) + { + BorderThickness = IsHovered ? 2 : 0; + + if (IsHovered) + BorderColour = colourProvider.Light4; + } + } + } +} From 15f73a3dfb3dcc838809dd48813c40cfd8d6a784 Mon Sep 17 00:00:00 2001 From: Michael Bui Date: Fri, 6 Sep 2024 21:52:42 +1200 Subject: [PATCH 2224/2493] show participation count in tooltip --- .../Header/Components/DailyChallengeStatsTooltip.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsTooltip.cs b/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsTooltip.cs index 64a8d67c5b..5d89406c34 100644 --- a/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsTooltip.cs +++ b/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsTooltip.cs @@ -26,6 +26,7 @@ namespace osu.Game.Overlays.Profile.Header.Components { private StreakPiece currentDaily = null!; private StreakPiece currentWeekly = null!; + private StreakPiece totalParticipation = null!; private StatisticsPiece bestDaily = null!; private StatisticsPiece bestWeekly = null!; private StatisticsPiece topTen = null!; @@ -70,7 +71,7 @@ namespace osu.Game.Overlays.Profile.Header.Components { topBackground = new Box { - RelativeSizeAxes = Axes.Both, + RelativeSizeAxes = Axes.None, }, new FillFlowContainer { @@ -78,8 +79,9 @@ namespace osu.Game.Overlays.Profile.Header.Components Direction = FillDirection.Horizontal, Padding = new MarginPadding(15f), Spacing = new Vector2(30f), - Children = new[] + Children = new Drawable[] { + totalParticipation = new StreakPiece(UsersStrings.ShowDailyChallengePlaycount), currentDaily = new StreakPiece(UsersStrings.ShowDailyChallengeDailyStreakCurrent), currentWeekly = new StreakPiece(UsersStrings.ShowDailyChallengeWeeklyStreakCurrent), } @@ -113,6 +115,9 @@ namespace osu.Game.Overlays.Profile.Header.Components background.Colour = colourProvider.Background4; topBackground.Colour = colourProvider.Background5; + totalParticipation.Value = DailyChallengeStatsDisplayStrings.UnitDay(statistics.PlayCount.ToLocalisableString(@"N0")); + totalParticipation.ValueColour = colourProvider.Content2; + currentDaily.Value = DailyChallengeStatsDisplayStrings.UnitDay(content.Statistics.DailyStreakCurrent.ToLocalisableString(@"N0")); currentDaily.ValueColour = colours.ForRankingTier(TierForDaily(statistics.DailyStreakCurrent)); From f59895aa3444eca3a167d2c3b315bb054626d091 Mon Sep 17 00:00:00 2001 From: Michael Bui Date: Fri, 6 Sep 2024 21:54:41 +1200 Subject: [PATCH 2225/2493] take out drawable --- .../Profile/Header/Components/DailyChallengeStatsTooltip.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsTooltip.cs b/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsTooltip.cs index 5d89406c34..df52fea158 100644 --- a/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsTooltip.cs +++ b/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsTooltip.cs @@ -79,7 +79,7 @@ namespace osu.Game.Overlays.Profile.Header.Components Direction = FillDirection.Horizontal, Padding = new MarginPadding(15f), Spacing = new Vector2(30f), - Children = new Drawable[] + Children = new[] { totalParticipation = new StreakPiece(UsersStrings.ShowDailyChallengePlaycount), currentDaily = new StreakPiece(UsersStrings.ShowDailyChallengeDailyStreakCurrent), From 34a9d60c190c2caf0a20c35508f8e592af6c414f Mon Sep 17 00:00:00 2001 From: Michael Bui Date: Fri, 6 Sep 2024 22:02:35 +1200 Subject: [PATCH 2226/2493] revert back to axes.both --- .../Profile/Header/Components/DailyChallengeStatsTooltip.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsTooltip.cs b/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsTooltip.cs index df52fea158..93ec3b941a 100644 --- a/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsTooltip.cs +++ b/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsTooltip.cs @@ -71,7 +71,7 @@ namespace osu.Game.Overlays.Profile.Header.Components { topBackground = new Box { - RelativeSizeAxes = Axes.None, + RelativeSizeAxes = Axes.Both, }, new FillFlowContainer { From ab8771900a44a2a3f01fd5494091866f3ebe7443 Mon Sep 17 00:00:00 2001 From: Michael Bui Date: Fri, 6 Sep 2024 22:04:10 +1200 Subject: [PATCH 2227/2493] change colour --- .../Profile/Header/Components/DailyChallengeStatsTooltip.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsTooltip.cs b/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsTooltip.cs index 93ec3b941a..bc389c5569 100644 --- a/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsTooltip.cs +++ b/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsTooltip.cs @@ -116,7 +116,7 @@ namespace osu.Game.Overlays.Profile.Header.Components topBackground.Colour = colourProvider.Background5; totalParticipation.Value = DailyChallengeStatsDisplayStrings.UnitDay(statistics.PlayCount.ToLocalisableString(@"N0")); - totalParticipation.ValueColour = colourProvider.Content2; + totalParticipation.ValueColour = colours.ForRankingTier(TierForDaily(statistics.PlayCount)); currentDaily.Value = DailyChallengeStatsDisplayStrings.UnitDay(content.Statistics.DailyStreakCurrent.ToLocalisableString(@"N0")); currentDaily.ValueColour = colours.ForRankingTier(TierForDaily(statistics.DailyStreakCurrent)); From 362b4bbc566ee0a32391d81f4936ef16a9b8744f Mon Sep 17 00:00:00 2001 From: Michael Bui Date: Fri, 6 Sep 2024 23:01:05 +1200 Subject: [PATCH 2228/2493] Hide daily challenge stats if there are no plays --- .../Profile/Header/Components/DailyChallengeStatsDisplay.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsDisplay.cs b/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsDisplay.cs index 41fd2be591..80487b19c6 100644 --- a/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsDisplay.cs +++ b/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsDisplay.cs @@ -107,6 +107,12 @@ namespace osu.Game.Overlays.Profile.Header.Components APIUserDailyChallengeStatistics stats = User.Value.User.DailyChallengeStatistics; + if (stats.PlayCount == 0) + { + Hide(); + return; + } + dailyPlayCount.Text = DailyChallengeStatsDisplayStrings.UnitDay(stats.PlayCount.ToLocalisableString("N0")); dailyPlayCount.Colour = colours.ForRankingTier(TierForPlayCount(stats.PlayCount)); From a31ea24c6d2ebe7937e2148f6ba754cace417e9d Mon Sep 17 00:00:00 2001 From: Michael Bui Date: Fri, 6 Sep 2024 23:05:04 +1200 Subject: [PATCH 2229/2493] show stats on all rulesets --- .../Profile/Header/Components/DailyChallengeStatsDisplay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsDisplay.cs b/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsDisplay.cs index 80487b19c6..cdc460e1a8 100644 --- a/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsDisplay.cs +++ b/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsDisplay.cs @@ -99,7 +99,7 @@ namespace osu.Game.Overlays.Profile.Header.Components private void updateDisplay() { - if (User.Value == null || User.Value.Ruleset.OnlineID != 0) + if (User.Value == null) { Hide(); return; From 7e53df5226667d6b7c1ba13bc9898a066e722ddf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 6 Sep 2024 13:01:50 +0200 Subject: [PATCH 2230/2493] Add failing test coverage --- .../TestScenePlayerScoreSubmission.cs | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerScoreSubmission.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerScoreSubmission.cs index 5e22e47572..c382f0828b 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerScoreSubmission.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerScoreSubmission.cs @@ -8,7 +8,9 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using NUnit.Framework; +using osu.Framework.Graphics.Containers; using osu.Framework.Screens; +using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Online.API; using osu.Game.Online.Rooms; @@ -26,6 +28,7 @@ using osu.Game.Scoring; using osu.Game.Screens.Play; using osu.Game.Screens.Ranking; using osu.Game.Tests.Beatmaps; +using osuTK.Input; namespace osu.Game.Tests.Visual.Gameplay { @@ -177,6 +180,30 @@ namespace osu.Game.Tests.Visual.Gameplay AddAssert("ensure no submission", () => Player.SubmittedScore == null); } + [Test] + public void TestEmptyFailStillImports() + { + prepareTestAPI(true); + + createPlayerTest(true); + + AddUntilStep("wait for token request", () => Player.TokenCreationRequested); + + AddUntilStep("wait for fail", () => Player.GameplayState.HasFailed); + AddUntilStep("wait for fail overlay", () => Player.FailOverlay.State.Value, () => Is.EqualTo(Visibility.Visible)); + + AddStep("attempt import", () => + { + InputManager.MoveMouseTo(Player.ChildrenOfType().Single()); + InputManager.Click(MouseButton.Left); + }); + AddUntilStep("wait for import to start", () => Player.ScoreImportStarted); + AddStep("allow import", () => Player.AllowImportCompletion.Release()); + + AddUntilStep("import completed", () => Player.ImportedScore, () => Is.Not.Null); + AddAssert("ensure no submission", () => Player.SubmittedScore, () => Is.Null); + } + [Test] public void TestSubmissionOnFail() { @@ -378,6 +405,8 @@ namespace osu.Game.Tests.Visual.Gameplay public SemaphoreSlim AllowImportCompletion { get; } public Score ImportedScore { get; private set; } + public new FailOverlay FailOverlay => base.FailOverlay; + public FakeImportingPlayer(bool allowPause = true, bool showResults = true, bool pauseOnFocusLost = false) : base(allowPause, showResults, pauseOnFocusLost) { From 4e9ad1388fb0de72c6197faa9ad6d56fb1a87087 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 6 Sep 2024 13:16:27 +0200 Subject: [PATCH 2231/2493] Fix stall when attempting to import replay after hitting nothing --- osu.Game/Screens/Play/SubmittingPlayer.cs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Play/SubmittingPlayer.cs b/osu.Game/Screens/Play/SubmittingPlayer.cs index 6c5f7fab9e..aea3bf6d5c 100644 --- a/osu.Game/Screens/Play/SubmittingPlayer.cs +++ b/osu.Game/Screens/Play/SubmittingPlayer.cs @@ -274,6 +274,16 @@ namespace osu.Game.Screens.Play return Task.CompletedTask; } + // if the user never hit anything, this score should not be counted in any way. + if (!score.ScoreInfo.Statistics.Any(s => s.Key.IsHit() && s.Value > 0)) + { + Logger.Log("No hits registered, skipping score submission"); + return Task.CompletedTask; + } + + // mind the timing of this. + // once `scoreSubmissionSource` is created, it is presumed that submission is taking place in the background, + // so all exceptional circumstances that would disallow submission must be handled above. lock (scoreSubmissionLock) { if (scoreSubmissionSource != null) @@ -282,10 +292,6 @@ namespace osu.Game.Screens.Play scoreSubmissionSource = new TaskCompletionSource(); } - // if the user never hit anything, this score should not be counted in any way. - if (!score.ScoreInfo.Statistics.Any(s => s.Key.IsHit() && s.Value > 0)) - return Task.CompletedTask; - Logger.Log($"Beginning score submission (token:{token.Value})..."); var request = CreateSubmissionRequest(score, token.Value); From 575da0992fa2f07792368b294cb1430684ea2a44 Mon Sep 17 00:00:00 2001 From: smallketchup82 Date: Fri, 6 Sep 2024 16:16:40 -0400 Subject: [PATCH 2232/2493] Fix file associations not updating & uninstalling --- osu.Desktop/Program.cs | 28 +++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/osu.Desktop/Program.cs b/osu.Desktop/Program.cs index 5103663815..5100eef3d9 100644 --- a/osu.Desktop/Program.cs +++ b/osu.Desktop/Program.cs @@ -168,12 +168,30 @@ namespace osu.Desktop private static void setupVelopack() { - VelopackApp - .Build() - .WithFirstRun(v => + var app = VelopackApp.Build(); + + app.WithFirstRun(_ => + { + if (OperatingSystem.IsWindows()) + WindowsAssociationManager.InstallAssociations(); + }); + + if (OperatingSystem.IsWindows()) + { + app.WithAfterUpdateFastCallback(_ => { - if (OperatingSystem.IsWindows()) WindowsAssociationManager.InstallAssociations(); - }).Run(); + if (OperatingSystem.IsWindows()) + WindowsAssociationManager.UpdateAssociations(); + }); + + app.WithBeforeUninstallFastCallback(_ => + { + if (OperatingSystem.IsWindows()) + WindowsAssociationManager.UninstallAssociations(); + }); + } + + app.Run(); } } } From ed044d5b85d80b1cdf03555e0be1530ffd166626 Mon Sep 17 00:00:00 2001 From: schiavoanto Date: Fri, 6 Sep 2024 22:58:18 +0200 Subject: [PATCH 2233/2493] Fix proposal for #29736 --- osu.Game/Screens/Ranking/CollectionPopover.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Screens/Ranking/CollectionPopover.cs b/osu.Game/Screens/Ranking/CollectionPopover.cs index 6617ac334f..ffc448d7a9 100644 --- a/osu.Game/Screens/Ranking/CollectionPopover.cs +++ b/osu.Game/Screens/Ranking/CollectionPopover.cs @@ -39,6 +39,7 @@ namespace osu.Game.Screens.Ranking new OsuMenu(Direction.Vertical, true) { Items = items, + MaxHeight = 375, }, }; } From 581f190856274ad1c521fb8ae32a503ebeed78ef Mon Sep 17 00:00:00 2001 From: Ianlucht Date: Fri, 6 Sep 2024 16:31:48 -0600 Subject: [PATCH 2234/2493] fixed issues with search by adding the double quotation marks in the BeatmapSetHeaderContents links. --- osu.Game/Overlays/BeatmapSet/BeatmapSetHeaderContent.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/BeatmapSet/BeatmapSetHeaderContent.cs b/osu.Game/Overlays/BeatmapSet/BeatmapSetHeaderContent.cs index 168056ea58..d9747d1f44 100644 --- a/osu.Game/Overlays/BeatmapSet/BeatmapSetHeaderContent.cs +++ b/osu.Game/Overlays/BeatmapSet/BeatmapSetHeaderContent.cs @@ -242,7 +242,7 @@ namespace osu.Game.Overlays.BeatmapSet title.Clear(); artist.Clear(); - title.AddLink(titleText, LinkAction.SearchBeatmapSet, titleText); + title.AddLink(titleText, LinkAction.SearchBeatmapSet, "title=\"\"" + titleText + "\"\""); title.AddArbitraryDrawable(Empty().With(d => d.Width = 5)); title.AddArbitraryDrawable(externalLink = new ExternalLinkButton()); @@ -259,7 +259,7 @@ namespace osu.Game.Overlays.BeatmapSet title.AddArbitraryDrawable(new SpotlightBeatmapBadge()); } - artist.AddLink(artistText, LinkAction.SearchBeatmapSet, artistText); + artist.AddLink(artistText, LinkAction.SearchBeatmapSet, "artist=\"\"" + artistText + "\"\""); if (setInfo.NewValue.TrackId != null) { From 3b81ad4cbffe88ff0ca16a0f26e74fc3c30b7c5b Mon Sep 17 00:00:00 2001 From: Bruno Heredia <111712756+Bruno5430@users.noreply.github.com> Date: Sat, 7 Sep 2024 01:42:47 -0300 Subject: [PATCH 2235/2493] Fix scroll speed slider defaulting to 0.01 --- osu.Game/Screens/Edit/Timing/EffectSection.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Timing/EffectSection.cs b/osu.Game/Screens/Edit/Timing/EffectSection.cs index a4b9f37dff..f9ef460232 100644 --- a/osu.Game/Screens/Edit/Timing/EffectSection.cs +++ b/osu.Game/Screens/Edit/Timing/EffectSection.cs @@ -56,7 +56,7 @@ namespace osu.Game.Screens.Edit.Timing isRebinding = true; kiai.Current = newEffectPoint.KiaiModeBindable; - scrollSpeedSlider.Current = new BindableDouble + scrollSpeedSlider.Current = new BindableDouble(1) { MinValue = 0.01, MaxValue = 10, From 41d32ab2ca0a79772e1ba8a3e21ba14fe863f30a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 7 Sep 2024 13:54:12 +0900 Subject: [PATCH 2236/2493] Fix display length not resetting to default because default was wrong Closes https://github.com/ppy/osu/issues/29757. --- osu.Game.Rulesets.Osu/Configuration/OsuRulesetConfigManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Configuration/OsuRulesetConfigManager.cs b/osu.Game.Rulesets.Osu/Configuration/OsuRulesetConfigManager.cs index 8a8b78b645..580c7e6bd8 100644 --- a/osu.Game.Rulesets.Osu/Configuration/OsuRulesetConfigManager.cs +++ b/osu.Game.Rulesets.Osu/Configuration/OsuRulesetConfigManager.cs @@ -27,7 +27,7 @@ namespace osu.Game.Rulesets.Osu.Configuration SetDefault(OsuRulesetSetting.ReplayFrameMarkersEnabled, false); SetDefault(OsuRulesetSetting.ReplayCursorPathEnabled, false); SetDefault(OsuRulesetSetting.ReplayCursorHideEnabled, false); - SetDefault(OsuRulesetSetting.ReplayAnalysisDisplayLength, 750); + SetDefault(OsuRulesetSetting.ReplayAnalysisDisplayLength, 800); } } From 9b189fd244f10613f616b077d1f83b6e1396cf85 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 7 Sep 2024 13:58:17 +0900 Subject: [PATCH 2237/2493] Fix windows check weirdness --- osu.Desktop/Program.cs | 29 ++++++++++------------------- 1 file changed, 10 insertions(+), 19 deletions(-) diff --git a/osu.Desktop/Program.cs b/osu.Desktop/Program.cs index 5100eef3d9..e78c2ca636 100644 --- a/osu.Desktop/Program.cs +++ b/osu.Desktop/Program.cs @@ -3,6 +3,7 @@ using System; using System.IO; +using System.Runtime.Versioning; using osu.Desktop.LegacyIpc; using osu.Desktop.Windows; using osu.Framework; @@ -170,28 +171,18 @@ namespace osu.Desktop { var app = VelopackApp.Build(); - app.WithFirstRun(_ => - { - if (OperatingSystem.IsWindows()) - WindowsAssociationManager.InstallAssociations(); - }); - if (OperatingSystem.IsWindows()) - { - app.WithAfterUpdateFastCallback(_ => - { - if (OperatingSystem.IsWindows()) - WindowsAssociationManager.UpdateAssociations(); - }); - - app.WithBeforeUninstallFastCallback(_ => - { - if (OperatingSystem.IsWindows()) - WindowsAssociationManager.UninstallAssociations(); - }); - } + configureWindows(app); app.Run(); } + + [SupportedOSPlatform("windows")] + private static void configureWindows(VelopackApp app) + { + app.WithFirstRun(_ => WindowsAssociationManager.InstallAssociations()); + app.WithAfterUpdateFastCallback(_ => WindowsAssociationManager.UpdateAssociations()); + app.WithBeforeUninstallFastCallback(_ => WindowsAssociationManager.UninstallAssociations()); + } } } From ac6cce5911d78a4321b679c1d93213954865a5a3 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Sat, 7 Sep 2024 17:40:33 +0900 Subject: [PATCH 2238/2493] Refactor to string interpolation --- osu.Game/Overlays/BeatmapSet/BeatmapSetHeaderContent.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/BeatmapSet/BeatmapSetHeaderContent.cs b/osu.Game/Overlays/BeatmapSet/BeatmapSetHeaderContent.cs index d9747d1f44..f9e0c6c380 100644 --- a/osu.Game/Overlays/BeatmapSet/BeatmapSetHeaderContent.cs +++ b/osu.Game/Overlays/BeatmapSet/BeatmapSetHeaderContent.cs @@ -242,7 +242,7 @@ namespace osu.Game.Overlays.BeatmapSet title.Clear(); artist.Clear(); - title.AddLink(titleText, LinkAction.SearchBeatmapSet, "title=\"\"" + titleText + "\"\""); + title.AddLink(titleText, LinkAction.SearchBeatmapSet, $@"title=""""{titleText}"""""); title.AddArbitraryDrawable(Empty().With(d => d.Width = 5)); title.AddArbitraryDrawable(externalLink = new ExternalLinkButton()); @@ -259,7 +259,7 @@ namespace osu.Game.Overlays.BeatmapSet title.AddArbitraryDrawable(new SpotlightBeatmapBadge()); } - artist.AddLink(artistText, LinkAction.SearchBeatmapSet, "artist=\"\"" + artistText + "\"\""); + artist.AddLink(artistText, LinkAction.SearchBeatmapSet, $@"artist=""""{artistText}"""""); if (setInfo.NewValue.TrackId != null) { From 10ef5a6d6dec110525cf59f7c9e14a0d11709c37 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 7 Sep 2024 21:46:43 +0900 Subject: [PATCH 2239/2493] 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 5f3dd2f6f4..7b45b9dec4 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -10,7 +10,7 @@ true - + diff --git a/osu.iOS.props b/osu.iOS.props index 9d9b42a163..1d76deddac 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -17,6 +17,6 @@ -all - + From 3e3ee3757c7688046b77fe4ee947d8ebf7e68dbe Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 7 Sep 2024 22:13:54 +0900 Subject: [PATCH 2240/2493] Add failing test case for difficulty splitting --- .../Visual/SongSelect/TestSceneBeatmapCarousel.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs index ec072a3dd2..fbed577ed2 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs @@ -520,6 +520,18 @@ namespace osu.Game.Tests.Visual.SongSelect waitForSelection(set_count); } + [Solo] + [Test] + public void TestDifficultiesSplitOutOnLoad() + { + loadBeatmaps(new List { TestResources.CreateTestBeatmapSetInfo(diff_count) }, () => new FilterCriteria + { + Sort = SortMode.Difficulty, + }); + + checkVisibleItemCount(false, 3); + } + [Test] public void TestAddRemoveDifficultySort() { From 4c6eb895309c33653bf0e2798ec41920e3567c7e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 7 Sep 2024 22:05:33 +0900 Subject: [PATCH 2241/2493] Fix beatmap difficulties not being split out on first load Closes https://github.com/ppy/osu/issues/29728. --- osu.Game/Screens/Select/BeatmapCarousel.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index a6a6a2f585..2486b26f25 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -137,6 +137,8 @@ namespace osu.Game.Screens.Select private void loadNewRoot() { + beatmapsSplitOut = activeCriteria.SplitOutDifficulties; + // Ensure no changes are made to the list while we are initialising items. // We'll catch up on changes via subscriptions anyway. BeatmapSetInfo[] loadableSets = detachedBeatmapSets!.ToArray(); @@ -726,7 +728,6 @@ namespace osu.Game.Screens.Select if (activeCriteria.SplitOutDifficulties != beatmapsSplitOut) { - beatmapsSplitOut = activeCriteria.SplitOutDifficulties; loadNewRoot(); return; } From 32de8e9b2da88e2edbcb06ae8434c15a342dc3fe Mon Sep 17 00:00:00 2001 From: schiavoanto Date: Sat, 7 Sep 2024 16:15:00 +0200 Subject: [PATCH 2242/2493] Fixed ControlPointTable items being blocked by buttons --- osu.Game/Screens/Edit/Timing/ControlPointTable.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Screens/Edit/Timing/ControlPointTable.cs b/osu.Game/Screens/Edit/Timing/ControlPointTable.cs index 501d8c0e41..c0b9ccb2be 100644 --- a/osu.Game/Screens/Edit/Timing/ControlPointTable.cs +++ b/osu.Game/Screens/Edit/Timing/ControlPointTable.cs @@ -53,6 +53,7 @@ namespace osu.Game.Screens.Edit.Timing private void load(OverlayColourProvider colours) { RelativeSizeAxes = Axes.Both; + Padding = new() { Bottom = 50 }; InternalChildren = new Drawable[] { From 2bc6547d49e3578c8bbb5590dafcaf93781eccf5 Mon Sep 17 00:00:00 2001 From: schiavoanto Date: Sat, 7 Sep 2024 16:23:23 +0200 Subject: [PATCH 2243/2493] Code quality fix: added type --- osu.Game/Screens/Edit/Timing/ControlPointTable.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Timing/ControlPointTable.cs b/osu.Game/Screens/Edit/Timing/ControlPointTable.cs index c0b9ccb2be..dd0cf2116e 100644 --- a/osu.Game/Screens/Edit/Timing/ControlPointTable.cs +++ b/osu.Game/Screens/Edit/Timing/ControlPointTable.cs @@ -53,7 +53,7 @@ namespace osu.Game.Screens.Edit.Timing private void load(OverlayColourProvider colours) { RelativeSizeAxes = Axes.Both; - Padding = new() { Bottom = 50 }; + Padding = new MarginPadding { Bottom = 50 }; InternalChildren = new Drawable[] { From 958bfde51d49f526d928a5976a6c0c4f21250bbb Mon Sep 17 00:00:00 2001 From: Ianlucht <90893791+Ianlucht@users.noreply.github.com> Date: Sat, 7 Sep 2024 13:46:42 -0600 Subject: [PATCH 2244/2493] added DailyChallengeIntro to notification --- .../DailyChallenge/NewDailyChallengeNotification.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/NewDailyChallengeNotification.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/NewDailyChallengeNotification.cs index ea19828a21..e305de0aaf 100644 --- a/osu.Game/Screens/OnlinePlay/DailyChallenge/NewDailyChallengeNotification.cs +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/NewDailyChallengeNotification.cs @@ -5,12 +5,14 @@ using System.Linq; using osu.Framework.Allocation; using osu.Framework.Screens; using osu.Game.Beatmaps.Drawables.Cards; +using osu.Game.Configuration; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Rooms; using osu.Game.Overlays.Notifications; using osu.Game.Screens.Menu; using osu.Game.Localisation; + namespace osu.Game.Screens.OnlinePlay.DailyChallenge { public partial class NewDailyChallengeNotification : SimpleNotification @@ -24,14 +26,18 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge this.room = room; } + [BackgroundDependencyLoader] - private void load(OsuGame? game) + private void load(OsuGame? game, SessionStatics statics) { Text = DailyChallengeStrings.ChallengeLiveNotification; Content.Add(card = new BeatmapCardNano((APIBeatmapSet)room.Playlist.Single().Beatmap.BeatmapSet!)); Activated = () => { - game?.PerformFromScreen(s => s.Push(new DailyChallenge(room)), [typeof(MainMenu)]); + if(statics.Get(Static.DailyChallengeIntroPlayed)) + game?.PerformFromScreen(s => s.Push(new DailyChallenge(room)), [typeof(MainMenu)]); + else + game?.PerformFromScreen(s => s.Push(new DailyChallengeIntro(room)), [typeof(MainMenu)]); return true; }; } From 170737b68f76d9269a81b7c91125c7518933f0b2 Mon Sep 17 00:00:00 2001 From: Ianlucht <90893791+Ianlucht@users.noreply.github.com> Date: Sat, 7 Sep 2024 13:48:14 -0600 Subject: [PATCH 2245/2493] added DailyChallengeIntro to notification --- .../OnlinePlay/DailyChallenge/NewDailyChallengeNotification.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/NewDailyChallengeNotification.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/NewDailyChallengeNotification.cs index e305de0aaf..8e4337274f 100644 --- a/osu.Game/Screens/OnlinePlay/DailyChallenge/NewDailyChallengeNotification.cs +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/NewDailyChallengeNotification.cs @@ -12,7 +12,6 @@ using osu.Game.Overlays.Notifications; using osu.Game.Screens.Menu; using osu.Game.Localisation; - namespace osu.Game.Screens.OnlinePlay.DailyChallenge { public partial class NewDailyChallengeNotification : SimpleNotification @@ -38,6 +37,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge game?.PerformFromScreen(s => s.Push(new DailyChallenge(room)), [typeof(MainMenu)]); else game?.PerformFromScreen(s => s.Push(new DailyChallengeIntro(room)), [typeof(MainMenu)]); + return true; }; } From e6f81abc3bcaaa82540931d30ed42485165231e8 Mon Sep 17 00:00:00 2001 From: Ianlucht <90893791+Ianlucht@users.noreply.github.com> Date: Sat, 7 Sep 2024 13:57:12 -0600 Subject: [PATCH 2246/2493] cleaned up whitespace --- .../OnlinePlay/DailyChallenge/NewDailyChallengeNotification.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/NewDailyChallengeNotification.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/NewDailyChallengeNotification.cs index 8e4337274f..35191f4ffa 100644 --- a/osu.Game/Screens/OnlinePlay/DailyChallenge/NewDailyChallengeNotification.cs +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/NewDailyChallengeNotification.cs @@ -25,7 +25,6 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge this.room = room; } - [BackgroundDependencyLoader] private void load(OsuGame? game, SessionStatics statics) { From cd94d6e2bcad42b630930621fa95bb3c54761e40 Mon Sep 17 00:00:00 2001 From: Ianlucht <90893791+Ianlucht@users.noreply.github.com> Date: Sat, 7 Sep 2024 14:01:38 -0600 Subject: [PATCH 2247/2493] fixed if statement format --- .../OnlinePlay/DailyChallenge/NewDailyChallengeNotification.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/NewDailyChallengeNotification.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/NewDailyChallengeNotification.cs index 35191f4ffa..7ae6992bec 100644 --- a/osu.Game/Screens/OnlinePlay/DailyChallenge/NewDailyChallengeNotification.cs +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/NewDailyChallengeNotification.cs @@ -32,7 +32,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge Content.Add(card = new BeatmapCardNano((APIBeatmapSet)room.Playlist.Single().Beatmap.BeatmapSet!)); Activated = () => { - if(statics.Get(Static.DailyChallengeIntroPlayed)) + if (statics.Get(Static.DailyChallengeIntroPlayed)) game?.PerformFromScreen(s => s.Push(new DailyChallenge(room)), [typeof(MainMenu)]); else game?.PerformFromScreen(s => s.Push(new DailyChallengeIntro(room)), [typeof(MainMenu)]); From 4cf057db8f62b82ffac6d954670e3bcbe0711e72 Mon Sep 17 00:00:00 2001 From: smallketchup82 Date: Sun, 8 Sep 2024 01:13:48 -0400 Subject: [PATCH 2248/2493] Completely disable velopack when using external update manager --- osu.Desktop/Program.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/osu.Desktop/Program.cs b/osu.Desktop/Program.cs index 5103663815..117ba66784 100644 --- a/osu.Desktop/Program.cs +++ b/osu.Desktop/Program.cs @@ -168,6 +168,14 @@ namespace osu.Desktop private static void setupVelopack() { + string? packageManaged = Environment.GetEnvironmentVariable("OSU_EXTERNAL_UPDATE_PROVIDER"); + + if (!string.IsNullOrEmpty(packageManaged)) + { + Logger.Log("Updates are being managed by an external provider. Skipping Velopack setup"); + return; + } + VelopackApp .Build() .WithFirstRun(v => From 7f814d3106b67158c16a336de7e01910dee4ba7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 8 Sep 2024 14:16:37 +0200 Subject: [PATCH 2249/2493] Fix incorrect tiers being used for tooltip total participation display Compare: https://github.com/ppy/osu-web/pull/11457/commits/95e4561a54353016f25c3fc859b176038b82088a --- .../Online/TestSceneUserProfileDailyChallenge.cs | 4 ++-- .../Header/Components/DailyChallengeStatsDisplay.cs | 9 +-------- .../Header/Components/DailyChallengeStatsTooltip.cs | 11 +++++++++-- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserProfileDailyChallenge.cs b/osu.Game.Tests/Visual/Online/TestSceneUserProfileDailyChallenge.cs index d7f5f65769..9db30380f6 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneUserProfileDailyChallenge.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneUserProfileDailyChallenge.cs @@ -66,8 +66,8 @@ namespace osu.Game.Tests.Visual.Online [Test] public void TestPlayCountRankingTier() { - AddAssert("1 before silver", () => DailyChallengeStatsDisplay.TierForPlayCount(30) == RankingTier.Bronze); - AddAssert("first silver", () => DailyChallengeStatsDisplay.TierForPlayCount(31) == RankingTier.Silver); + AddAssert("1 before silver", () => DailyChallengeStatsTooltip.TierForPlayCount(30) == RankingTier.Bronze); + AddAssert("first silver", () => DailyChallengeStatsTooltip.TierForPlayCount(31) == RankingTier.Silver); } } } diff --git a/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsDisplay.cs b/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsDisplay.cs index cdc460e1a8..3e86b2268f 100644 --- a/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsDisplay.cs +++ b/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsDisplay.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 osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.LocalisationExtensions; @@ -14,7 +13,6 @@ using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Localisation; using osu.Game.Online.API.Requests.Responses; -using osu.Game.Scoring; namespace osu.Game.Overlays.Profile.Header.Components { @@ -114,18 +112,13 @@ namespace osu.Game.Overlays.Profile.Header.Components } dailyPlayCount.Text = DailyChallengeStatsDisplayStrings.UnitDay(stats.PlayCount.ToLocalisableString("N0")); - dailyPlayCount.Colour = colours.ForRankingTier(TierForPlayCount(stats.PlayCount)); + dailyPlayCount.Colour = colours.ForRankingTier(DailyChallengeStatsTooltip.TierForPlayCount(stats.PlayCount)); TooltipContent = new DailyChallengeTooltipData(colourProvider, stats); Show(); } - // Rounding up is needed here to ensure the overlay shows the same colour as osu-web for the play count. - // This is because, for example, 31 / 3 > 10 in JavaScript because floats are used, while here it would - // get truncated to 10 with an integer division and show a lower tier. - public static RankingTier TierForPlayCount(int playCount) => DailyChallengeStatsTooltip.TierForDaily((int)Math.Ceiling(playCount / 3.0d)); - public ITooltip GetCustomTooltip() => new DailyChallengeStatsTooltip(); } } diff --git a/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsTooltip.cs b/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsTooltip.cs index bc389c5569..24e531bd87 100644 --- a/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsTooltip.cs +++ b/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsTooltip.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.Extensions.Color4Extensions; using osu.Framework.Extensions.LocalisationExtensions; @@ -116,7 +117,7 @@ namespace osu.Game.Overlays.Profile.Header.Components topBackground.Colour = colourProvider.Background5; totalParticipation.Value = DailyChallengeStatsDisplayStrings.UnitDay(statistics.PlayCount.ToLocalisableString(@"N0")); - totalParticipation.ValueColour = colours.ForRankingTier(TierForDaily(statistics.PlayCount)); + totalParticipation.ValueColour = colours.ForRankingTier(TierForPlayCount(statistics.PlayCount)); currentDaily.Value = DailyChallengeStatsDisplayStrings.UnitDay(content.Statistics.DailyStreakCurrent.ToLocalisableString(@"N0")); currentDaily.ValueColour = colours.ForRankingTier(TierForDaily(statistics.DailyStreakCurrent)); @@ -137,7 +138,13 @@ namespace osu.Game.Overlays.Profile.Header.Components topFifty.ValueColour = colourProvider.Content2; } - // reference: https://github.com/ppy/osu-web/blob/8206e0e91eeea80ccf92f0586561346dd40e085e/resources/js/profile-page/daily-challenge.tsx#L13-L43 + // reference: https://github.com/ppy/osu-web/blob/adf1e94754ba9625b85eba795f4a310caf169eec/resources/js/profile-page/daily-challenge.tsx#L13-L47 + + // Rounding up is needed here to ensure the overlay shows the same colour as osu-web for the play count. + // This is because, for example, 31 / 3 > 10 in JavaScript because floats are used, while here it would + // get truncated to 10 with an integer division and show a lower tier. + public static RankingTier TierForPlayCount(int playCount) => TierForDaily((int)Math.Ceiling(playCount / 3.0d)); + public static RankingTier TierForDaily(int daily) { if (daily > 360) From cf23c6668c3281e5644721665e68ec1265e26868 Mon Sep 17 00:00:00 2001 From: schiavoanto Date: Sun, 8 Sep 2024 15:59:23 +0200 Subject: [PATCH 2250/2493] Added background color to hide beatmap background --- .../Screens/Edit/Timing/ControlPointList.cs | 88 +++++++++++-------- 1 file changed, 52 insertions(+), 36 deletions(-) diff --git a/osu.Game/Screens/Edit/Timing/ControlPointList.cs b/osu.Game/Screens/Edit/Timing/ControlPointList.cs index 8699c388b3..6a21ff0053 100644 --- a/osu.Game/Screens/Edit/Timing/ControlPointList.cs +++ b/osu.Game/Screens/Edit/Timing/ControlPointList.cs @@ -7,11 +7,13 @@ 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.Input.Events; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Graphics; using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterfaceV2; +using osu.Game.Overlays; using osuTK; namespace osu.Game.Screens.Edit.Timing @@ -30,6 +32,9 @@ namespace osu.Game.Screens.Edit.Timing [Resolved] private Bindable selectedGroup { get; set; } = null!; + [Cached] + private OverlayColourProvider overlayColourProvider = new OverlayColourProvider(OverlayColourScheme.Green); + [BackgroundDependencyLoader] private void load(OsuColour colours) { @@ -43,51 +48,62 @@ namespace osu.Game.Screens.Edit.Timing RelativeSizeAxes = Axes.Both, Groups = { BindTarget = Beatmap.ControlPointInfo.Groups, }, }, - new FillFlowContainer + new Container { - AutoSizeAxes = Axes.Both, - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - Direction = FillDirection.Horizontal, - Margin = new MarginPadding(margins), - Spacing = new Vector2(5), + AutoSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.X, + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, Children = new Drawable[] { - new RoundedButton + new Box { - Text = "Select closest to current time", - Action = goToCurrentGroup, - Size = new Vector2(220, 30), - Anchor = Anchor.BottomRight, - Origin = Anchor.BottomRight, + Height = 50, + RelativeSizeAxes = Axes.X, + Anchor = Anchor.CentreLeft, + Colour = overlayColourProvider.Background2, }, - } - }, - new FillFlowContainer - { - AutoSizeAxes = Axes.Both, - Anchor = Anchor.BottomRight, - Origin = Anchor.BottomRight, - Direction = FillDirection.Horizontal, - Margin = new MarginPadding(margins), - Spacing = new Vector2(5), - Children = new Drawable[] - { - deleteButton = new RoundedButton + new FillFlowContainer { - Text = "-", - Size = new Vector2(30, 30), - Action = delete, - Anchor = Anchor.BottomRight, - Origin = Anchor.BottomRight, - BackgroundColour = colours.Red3, + Anchor = Anchor.BottomLeft, + Padding = new MarginPadding { Left = margins, Bottom = margins }, + Children = new Drawable[] + { + new RoundedButton + { + Text = "Select closest to current time", + Action = goToCurrentGroup, + Size = new Vector2(220, 30), + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + }, + } }, - addButton = new RoundedButton + new FillFlowContainer { - Action = addNew, - Size = new Vector2(160, 30), + Direction = FillDirection.Horizontal, Anchor = Anchor.BottomRight, - Origin = Anchor.BottomRight, + Spacing = new Vector2(5), + Padding = new MarginPadding { Right = margins, Bottom = margins }, + Children = new Drawable[] + { + deleteButton = new RoundedButton + { + Text = "-", + Size = new Vector2(30, 30), + Action = delete, + Anchor = Anchor.BottomRight, + Origin = Anchor.BottomRight, + BackgroundColour = colours.Red3, + }, + addButton = new RoundedButton + { + Action = addNew, + Size = new Vector2(160, 30), + Anchor = Anchor.BottomRight, + Origin = Anchor.BottomRight, + }, + } }, } }, From 2e6f17f25399684681c22f5701717037808c97aa Mon Sep 17 00:00:00 2001 From: schiavoanto Date: Sun, 8 Sep 2024 16:04:10 +0200 Subject: [PATCH 2251/2493] Fixed wrong OverlayColourScheme --- osu.Game/Screens/Edit/Timing/ControlPointList.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Timing/ControlPointList.cs b/osu.Game/Screens/Edit/Timing/ControlPointList.cs index 6a21ff0053..03ad1a631a 100644 --- a/osu.Game/Screens/Edit/Timing/ControlPointList.cs +++ b/osu.Game/Screens/Edit/Timing/ControlPointList.cs @@ -33,7 +33,7 @@ namespace osu.Game.Screens.Edit.Timing private Bindable selectedGroup { get; set; } = null!; [Cached] - private OverlayColourProvider overlayColourProvider = new OverlayColourProvider(OverlayColourScheme.Green); + private OverlayColourProvider overlayColourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine); [BackgroundDependencyLoader] private void load(OsuColour colours) From 134bcc85b76748fc5e5b4678f53498a853bd7a67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 8 Sep 2024 14:53:43 +0200 Subject: [PATCH 2252/2493] Add failing test case --- .../SongSelect/TestSceneBeatmapCarousel.cs | 27 ++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs index fbed577ed2..97c46a11fc 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs @@ -520,7 +520,6 @@ namespace osu.Game.Tests.Visual.SongSelect waitForSelection(set_count); } - [Solo] [Test] public void TestDifficultiesSplitOutOnLoad() { @@ -1132,6 +1131,32 @@ namespace osu.Game.Tests.Visual.SongSelect AddAssert("Selection was remembered", () => eagerSelectedIDs.Count == 1); } + [Test] + public void TestCarouselRetainsSelectionFromDifficultySort() + { + List manySets = new List(); + + AddStep("Populate beatmap sets", () => + { + manySets.Clear(); + + for (int i = 1; i <= 50; i++) + manySets.Add(TestResources.CreateTestBeatmapSetInfo(diff_count)); + }); + + loadBeatmaps(manySets); + + BeatmapInfo chosenBeatmap = null!; + AddStep("select given beatmap", () => carousel.SelectBeatmap(chosenBeatmap = manySets[20].Beatmaps[0])); + AddUntilStep("selection changed", () => carousel.SelectedBeatmapInfo, () => Is.EqualTo(chosenBeatmap)); + + AddStep("sort by difficulty", () => carousel.FilterImmediately(new FilterCriteria { Sort = SortMode.Difficulty })); + AddAssert("selection retained", () => carousel.SelectedBeatmapInfo, () => Is.EqualTo(chosenBeatmap)); + + AddStep("sort by title", () => carousel.FilterImmediately(new FilterCriteria { Sort = SortMode.Title })); + AddAssert("selection retained", () => carousel.SelectedBeatmapInfo, () => Is.EqualTo(chosenBeatmap)); + } + [Test] public void TestFilteringByUserStarDifficulty() { From cefbc76490d5b17f5607fd579b86f3c0b89104fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 8 Sep 2024 15:51:59 +0200 Subject: [PATCH 2253/2493] Fix selection being dropped when changing carousel sort mode from difficulty sort Closes https://github.com/ppy/osu/issues/29738. This "regressed" in https://github.com/ppy/osu/pull/29639, but if I didn't know better, I'd go as far as saying that this looks like a .NET bug, because the fact that PR broke it looks not sane. The TL;DR on this is that before the pull in question, the offending `.Contains()` check that this commit modifies was called on a `List`. The pull changed the collection type to `BeatmapSetInfo[]`. That said, the call is a LINQ call, so all good, right? Not really. First off, the default overload resolution order means that the previous code would call `List.Contains()`, and not `Enumerable.Contains()`. Then again, why would that matter? In both cases `T` is still `BeatmapSetInfo`, right? Well... about that... It is difficult to tell for sure what precisely is happening here, because of what looks like runtime magic. The end *symptom* is that the old code ended up calling `Array.IndexOf()`, and the new code ends up calling... `Array.IndexOf()`. So while yes, `BeatmapSetInfo` implements `IEquatable` and the expectation would be that `Equals()` should be getting called, the type elision to `object` means that we're back to reference equality semantics, because that's what `EqualityComparer.Default` is. A five-minute github search across dotnet/runtime yields this: https://github.com/dotnet/runtime/blob/c4792a228ea36792b90f87ddf7fce2477e827822/src/coreclr/vm/array.cpp#L984-L990 Now again, if I didn't know better, I'd see that "OPTIMIZATION:" comment, see what transpired in this scenario, and call that optimisation invalid, because it changes semantics. But I *probably* know that the dotnet team knows better and am probably just going to take it for what it is, because blame on that code looks to be years old and it's probably not a new behaviour. (I haven't tested empirically if it is.) Instead the fix is just to tell the `.Contains()` method to use the correct comparer. --- osu.Game/Screens/Select/BeatmapCarousel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 2486b26f25..525884c413 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -143,7 +143,7 @@ namespace osu.Game.Screens.Select // We'll catch up on changes via subscriptions anyway. BeatmapSetInfo[] loadableSets = detachedBeatmapSets!.ToArray(); - if (selectedBeatmapSet != null && !loadableSets.Contains(selectedBeatmapSet.BeatmapSet)) + if (selectedBeatmapSet != null && !loadableSets.Contains(selectedBeatmapSet.BeatmapSet, EqualityComparer.Default)) selectedBeatmapSet = null; var selectedBeatmapBefore = selectedBeatmap?.BeatmapInfo; From 10e84d72e566e0f9188985ac1ae1adfd03865e22 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 8 Sep 2024 23:07:17 +0900 Subject: [PATCH 2254/2493] Fix restart notifications appearing every 30 minutes If a user was to manually check for updates via the button, the recheck would have been fired. This is a recent regression. I kinda want to reorganise this code (the button press for check for udpates shouldn't even get close to the recheck code IMO) but for now this seems like one we should quickly fix. Addresses https://github.com/ppy/osu/discussions/29774. --- osu.Desktop/Updater/VelopackUpdateManager.cs | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/osu.Desktop/Updater/VelopackUpdateManager.cs b/osu.Desktop/Updater/VelopackUpdateManager.cs index e550755fff..c2965428f7 100644 --- a/osu.Desktop/Updater/VelopackUpdateManager.cs +++ b/osu.Desktop/Updater/VelopackUpdateManager.cs @@ -45,14 +45,17 @@ namespace osu.Desktop.Updater private async Task checkForUpdateAsync(UpdateProgressNotification? notification = null) { - // should we schedule a retry on completion of this check? - bool scheduleRecheck = true; + // whether to check again in 30 minutes. generally only if there's an error or no update was found (yet). + bool scheduleRecheck = false; try { // Avoid any kind of update checking while gameplay is running. if (localUserInfo?.IsPlaying.Value == true) + { + scheduleRecheck = true; return false; + } // TODO: we should probably be checking if there's a more recent update, rather than shortcutting here. // Velopack does support this scenario (see https://github.com/ppy/osu/pull/28743#discussion_r1743495975). @@ -67,17 +70,20 @@ namespace osu.Desktop.Updater return true; } }); + return true; } pendingUpdate = await updateManager.CheckForUpdatesAsync().ConfigureAwait(false); - // Handle no updates available. + // No update is available. We'll check again later. if (pendingUpdate == null) + { + scheduleRecheck = true; return false; + } - scheduleRecheck = false; - + // An update is found, let's notify the user and start downloading it. if (notification == null) { notification = new UpdateProgressNotification @@ -113,7 +119,6 @@ namespace osu.Desktop.Updater { if (scheduleRecheck) { - // check again in 30 minutes. Scheduler.AddDelayed(() => Task.Run(async () => await checkForUpdateAsync().ConfigureAwait(false)), 60000 * 30); } } From f5c5614eef02feb0c816f31ce1d4e9dae163ecdd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 8 Sep 2024 16:29:53 +0200 Subject: [PATCH 2255/2493] Resolve existing colour provider instead of re-caching own one --- osu.Game/Screens/Edit/Timing/ControlPointList.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/osu.Game/Screens/Edit/Timing/ControlPointList.cs b/osu.Game/Screens/Edit/Timing/ControlPointList.cs index 03ad1a631a..4cc356012f 100644 --- a/osu.Game/Screens/Edit/Timing/ControlPointList.cs +++ b/osu.Game/Screens/Edit/Timing/ControlPointList.cs @@ -32,11 +32,8 @@ namespace osu.Game.Screens.Edit.Timing [Resolved] private Bindable selectedGroup { get; set; } = null!; - [Cached] - private OverlayColourProvider overlayColourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine); - [BackgroundDependencyLoader] - private void load(OsuColour colours) + private void load(OsuColour colours, OverlayColourProvider colourProvider) { RelativeSizeAxes = Axes.Both; From 7ec2e0e86696eb63b6b1e4995af2263d91d214f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 8 Sep 2024 16:30:09 +0200 Subject: [PATCH 2256/2493] Refactor layout code to be a bit less haphazard Visually the same, functionally much saner. --- .../Screens/Edit/Timing/ControlPointList.cs | 38 +++++++++++-------- .../Screens/Edit/Timing/ControlPointTable.cs | 7 +++- 2 files changed, 28 insertions(+), 17 deletions(-) diff --git a/osu.Game/Screens/Edit/Timing/ControlPointList.cs b/osu.Game/Screens/Edit/Timing/ControlPointList.cs index 4cc356012f..49e5b76dd6 100644 --- a/osu.Game/Screens/Edit/Timing/ControlPointList.cs +++ b/osu.Game/Screens/Edit/Timing/ControlPointList.cs @@ -20,6 +20,8 @@ namespace osu.Game.Screens.Edit.Timing { public partial class ControlPointList : CompositeDrawable { + private ControlPointTable table = null!; + private Container controls = null!; private OsuButton deleteButton = null!; private RoundedButton addButton = null!; @@ -40,12 +42,12 @@ namespace osu.Game.Screens.Edit.Timing const float margins = 10; InternalChildren = new Drawable[] { - new ControlPointTable + table = new ControlPointTable { RelativeSizeAxes = Axes.Both, Groups = { BindTarget = Beatmap.ControlPointInfo.Groups, }, }, - new Container + controls = new Container { AutoSizeAxes = Axes.Y, RelativeSizeAxes = Axes.X, @@ -55,15 +57,16 @@ namespace osu.Game.Screens.Edit.Timing { new Box { - Height = 50, - RelativeSizeAxes = Axes.X, - Anchor = Anchor.CentreLeft, - Colour = overlayColourProvider.Background2, + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background2, }, new FillFlowContainer { - Anchor = Anchor.BottomLeft, - Padding = new MarginPadding { Left = margins, Bottom = margins }, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Padding = new MarginPadding { Left = margins, Vertical = margins, }, Children = new Drawable[] { new RoundedButton @@ -71,17 +74,19 @@ namespace osu.Game.Screens.Edit.Timing Text = "Select closest to current time", Action = goToCurrentGroup, Size = new Vector2(220, 30), - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, }, } }, new FillFlowContainer { + AutoSizeAxes = Axes.Both, Direction = FillDirection.Horizontal, - Anchor = Anchor.BottomRight, + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, Spacing = new Vector2(5), - Padding = new MarginPadding { Right = margins, Bottom = margins }, + Padding = new MarginPadding { Right = margins, Vertical = margins, }, Children = new Drawable[] { deleteButton = new RoundedButton @@ -89,16 +94,16 @@ namespace osu.Game.Screens.Edit.Timing Text = "-", Size = new Vector2(30, 30), Action = delete, - Anchor = Anchor.BottomRight, - Origin = Anchor.BottomRight, + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, BackgroundColour = colours.Red3, }, addButton = new RoundedButton { Action = addNew, Size = new Vector2(160, 30), - Anchor = Anchor.BottomRight, - Origin = Anchor.BottomRight, + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, }, } }, @@ -132,6 +137,7 @@ namespace osu.Game.Screens.Edit.Timing base.Update(); addButton.Enabled.Value = clock.CurrentTimeAccurate != selectedGroup.Value?.Time; + table.Padding = new MarginPadding { Bottom = controls.DrawHeight }; } private void goToCurrentGroup() diff --git a/osu.Game/Screens/Edit/Timing/ControlPointTable.cs b/osu.Game/Screens/Edit/Timing/ControlPointTable.cs index dd0cf2116e..fd812cfe2b 100644 --- a/osu.Game/Screens/Edit/Timing/ControlPointTable.cs +++ b/osu.Game/Screens/Edit/Timing/ControlPointTable.cs @@ -28,6 +28,12 @@ namespace osu.Game.Screens.Edit.Timing { public BindableList Groups { get; } = new BindableList(); + public new MarginPadding Padding + { + get => base.Padding; + set => base.Padding = value; + } + [Cached] private Bindable activeTimingPoint { get; } = new Bindable(); @@ -53,7 +59,6 @@ namespace osu.Game.Screens.Edit.Timing private void load(OverlayColourProvider colours) { RelativeSizeAxes = Axes.Both; - Padding = new MarginPadding { Bottom = 50 }; InternalChildren = new Drawable[] { From 19e4cc84d58f50d8e10e9c6d3c78133f47047830 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 9 Sep 2024 01:58:09 +0900 Subject: [PATCH 2257/2493] Also schedule a re-check on download failure --- osu.Desktop/Updater/VelopackUpdateManager.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Desktop/Updater/VelopackUpdateManager.cs b/osu.Desktop/Updater/VelopackUpdateManager.cs index c2965428f7..ae58a8793c 100644 --- a/osu.Desktop/Updater/VelopackUpdateManager.cs +++ b/osu.Desktop/Updater/VelopackUpdateManager.cs @@ -105,6 +105,7 @@ namespace osu.Desktop.Updater catch (Exception e) { // In the case of an error, a separate notification will be displayed. + scheduleRecheck = true; notification.FailDownload(); Logger.Error(e, @"update failed!"); } From 4ff72c5331c3f1d4007f08d5defa2ea0131cb94c Mon Sep 17 00:00:00 2001 From: smallketchup82 Date: Mon, 9 Sep 2024 03:04:16 -0400 Subject: [PATCH 2258/2493] Add beatmap icon to windows beatmap files --- osu.Desktop/Windows/Icons.cs | 2 ++ .../Windows/WindowsAssociationManager.cs | 4 ++-- osu.Desktop/beatmap.ico | Bin 0 -> 59403 bytes 3 files changed, 4 insertions(+), 2 deletions(-) create mode 100644 osu.Desktop/beatmap.ico diff --git a/osu.Desktop/Windows/Icons.cs b/osu.Desktop/Windows/Icons.cs index 67915c101a..9d37a21b49 100644 --- a/osu.Desktop/Windows/Icons.cs +++ b/osu.Desktop/Windows/Icons.cs @@ -13,5 +13,7 @@ namespace osu.Desktop.Windows private static readonly string icon_directory = Path.GetDirectoryName(typeof(Icons).Assembly.Location)!; public static string Lazer => Path.Join(icon_directory, "lazer.ico"); + + public static string Beatmap => Path.Join(icon_directory, "beatmap.ico"); } } diff --git a/osu.Desktop/Windows/WindowsAssociationManager.cs b/osu.Desktop/Windows/WindowsAssociationManager.cs index b32c01433d..92cffd0987 100644 --- a/osu.Desktop/Windows/WindowsAssociationManager.cs +++ b/osu.Desktop/Windows/WindowsAssociationManager.cs @@ -40,8 +40,8 @@ namespace osu.Desktop.Windows private static readonly FileAssociation[] file_associations = { - new FileAssociation(@".osz", WindowsAssociationManagerStrings.OsuBeatmap, Icons.Lazer), - new FileAssociation(@".olz", WindowsAssociationManagerStrings.OsuBeatmap, Icons.Lazer), + new FileAssociation(@".osz", WindowsAssociationManagerStrings.OsuBeatmap, Icons.Beatmap), + new FileAssociation(@".olz", WindowsAssociationManagerStrings.OsuBeatmap, Icons.Beatmap), new FileAssociation(@".osr", WindowsAssociationManagerStrings.OsuReplay, Icons.Lazer), new FileAssociation(@".osk", WindowsAssociationManagerStrings.OsuSkin, Icons.Lazer), }; diff --git a/osu.Desktop/beatmap.ico b/osu.Desktop/beatmap.ico new file mode 100644 index 0000000000000000000000000000000000000000..410ccfd73d6e42edbf3fc6ccc01bc9e00d6cd9a9 GIT binary patch literal 59403 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 DfNejV literal 0 HcmV?d00001 From d451415d5c6b726a10dcedebca6872f63a8273f1 Mon Sep 17 00:00:00 2001 From: smallketchup82 Date: Mon, 9 Sep 2024 03:28:50 -0400 Subject: [PATCH 2259/2493] Move environment variable logic to static bool in OsuGameDesktop --- osu.Desktop/OsuGameDesktop.cs | 6 +++--- osu.Desktop/Program.cs | 4 +--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/osu.Desktop/OsuGameDesktop.cs b/osu.Desktop/OsuGameDesktop.cs index c75a3f0a1a..46bd894c07 100644 --- a/osu.Desktop/OsuGameDesktop.cs +++ b/osu.Desktop/OsuGameDesktop.cs @@ -95,11 +95,11 @@ namespace osu.Desktop return key?.OpenSubKey(WindowsAssociationManager.SHELL_OPEN_COMMAND)?.GetValue(string.Empty)?.ToString()?.Split('"')[1].Replace("osu!.exe", ""); } + public static bool IsPackageManaged => !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("OSU_EXTERNAL_UPDATE_PROVIDER")); + protected override UpdateManager CreateUpdateManager() { - string? packageManaged = Environment.GetEnvironmentVariable("OSU_EXTERNAL_UPDATE_PROVIDER"); - - if (!string.IsNullOrEmpty(packageManaged)) + if (IsPackageManaged) return new NoActionUpdateManager(); return new VelopackUpdateManager(); diff --git a/osu.Desktop/Program.cs b/osu.Desktop/Program.cs index 78a20e32dc..6e0234f387 100644 --- a/osu.Desktop/Program.cs +++ b/osu.Desktop/Program.cs @@ -169,9 +169,7 @@ namespace osu.Desktop private static void setupVelopack() { - string? packageManaged = Environment.GetEnvironmentVariable("OSU_EXTERNAL_UPDATE_PROVIDER"); - - if (!string.IsNullOrEmpty(packageManaged)) + if (OsuGameDesktop.IsPackageManaged) { Logger.Log("Updates are being managed by an external provider. Skipping Velopack setup"); return; From 63b6f36a29e7e1f4d8d19f945155c29c1013c4af Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 9 Sep 2024 18:09:28 +0900 Subject: [PATCH 2260/2493] Add missing `.` --- osu.Desktop/Program.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Desktop/Program.cs b/osu.Desktop/Program.cs index 6e0234f387..ebc7509af6 100644 --- a/osu.Desktop/Program.cs +++ b/osu.Desktop/Program.cs @@ -171,7 +171,7 @@ namespace osu.Desktop { if (OsuGameDesktop.IsPackageManaged) { - Logger.Log("Updates are being managed by an external provider. Skipping Velopack setup"); + Logger.Log("Updates are being managed by an external provider. Skipping Velopack setup."); return; } From 4ada0bf787c1d1a17a14eadaac2f464c66f7227c Mon Sep 17 00:00:00 2001 From: smallketchup82 Date: Mon, 9 Sep 2024 12:48:15 -0400 Subject: [PATCH 2261/2493] Differentiate lazer in menus --- osu.Desktop/osu.Desktop.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Desktop/osu.Desktop.csproj b/osu.Desktop/osu.Desktop.csproj index 3588317b8a..841672b581 100644 --- a/osu.Desktop/osu.Desktop.csproj +++ b/osu.Desktop/osu.Desktop.csproj @@ -5,6 +5,7 @@ true A free-to-win rhythm game. Rhythm is just a *click* away! osu! + osu!lazer osu! osu!(lazer) lazer.ico From f716cb4a7cd0b1edfd52afa4d9533ed2337372cf Mon Sep 17 00:00:00 2001 From: smallketchup82 Date: Mon, 9 Sep 2024 16:11:28 -0400 Subject: [PATCH 2262/2493] Change to using osu!(lazer) --- 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 841672b581..bf5f26b352 100644 --- a/osu.Desktop/osu.Desktop.csproj +++ b/osu.Desktop/osu.Desktop.csproj @@ -5,7 +5,7 @@ true A free-to-win rhythm game. Rhythm is just a *click* away! osu! - osu!lazer + osu!(lazer) osu! osu!(lazer) lazer.ico From d8a745ec045d95fb5a8ca7785185a70b43054baa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 11 Sep 2024 11:24:11 +0200 Subject: [PATCH 2263/2493] Decouple legacy mania combo counter from abstract eldritch entity --- .../Legacy/LegacyManiaComboCounter.cs | 147 +++++++++++++++--- 1 file changed, 125 insertions(+), 22 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyManiaComboCounter.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyManiaComboCounter.cs index 07d014b416..889e6326f7 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyManiaComboCounter.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyManiaComboCounter.cs @@ -2,9 +2,12 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Globalization; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Skinning; using osuTK; @@ -12,17 +15,76 @@ using osuTK.Graphics; namespace osu.Game.Rulesets.Mania.Skinning.Legacy { - public partial class LegacyManiaComboCounter : LegacyComboCounter + public partial class LegacyManiaComboCounter : CompositeDrawable, ISerialisableDrawable { - [BackgroundDependencyLoader] - private void load(ISkinSource skin) - { - DisplayedCountText.Anchor = Anchor.Centre; - DisplayedCountText.Origin = Anchor.Centre; + public bool UsesFixedAnchor { get; set; } - PopOutCountText.Anchor = Anchor.Centre; - PopOutCountText.Origin = Anchor.Centre; - PopOutCountText.Colour = skin.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.ComboBreakColour)?.Value ?? Color4.Red; + public Bindable Current { get; } = new BindableInt { MinValue = 0 }; + + /// + /// Value shown at the current moment. + /// + public virtual int DisplayedCount + { + get => displayedCount; + private set + { + if (displayedCount.Equals(value)) + return; + + displayedCountText.FadeTo(value == 0 ? 0 : 1); + displayedCountText.Text = value.ToString(CultureInfo.InvariantCulture); + counterContainer.Size = displayedCountText.Size; + + displayedCount = value; + } + } + + private int displayedCount; + + private int previousValue; + + private const double fade_out_duration = 100; + private const double rolling_duration = 20; + + private Container counterContainer = null!; + private LegacySpriteText popOutCountText = null!; + private LegacySpriteText displayedCountText = null!; + + [BackgroundDependencyLoader] + private void load(ISkinSource skin, ScoreProcessor scoreProcessor) + { + AutoSizeAxes = Axes.Both; + + InternalChildren = new[] + { + counterContainer = new Container + { + AlwaysPresent = true, + Children = new[] + { + popOutCountText = new LegacySpriteText(LegacyFont.Combo) + { + Alpha = 0, + Blending = BlendingParameters.Additive, + BypassAutoSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Colour = skin.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.ComboBreakColour)?.Value ?? Color4.Red, + }, + displayedCountText = new LegacySpriteText(LegacyFont.Combo) + { + Alpha = 0, + AlwaysPresent = true, + BypassAutoSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }, + } + } + }; + + Current.BindTo(scoreProcessor.Combo); } [Resolved] @@ -34,6 +96,12 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy { base.LoadComplete(); + displayedCountText.Text = popOutCountText.Text = Current.Value.ToString(CultureInfo.InvariantCulture); + + Current.BindValueChanged(combo => updateCount(combo.NewValue == 0), true); + + counterContainer.Size = displayedCountText.Size; + direction = scrollingInfo.Direction.GetBoundCopy(); direction.BindValueChanged(_ => updateAnchor()); @@ -56,36 +124,71 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy Y = Math.Abs(Y) * (direction.Value == ScrollingDirection.Up ? -1 : 1); } - protected override void OnCountIncrement() + private void updateCount(bool rolling) { - base.OnCountIncrement(); + int prev = previousValue; + previousValue = Current.Value; - PopOutCountText.Hide(); - DisplayedCountText.ScaleTo(new Vector2(1f, 1.4f)) + if (!IsLoaded) + return; + + if (!rolling) + { + FinishTransforms(false, nameof(DisplayedCount)); + + if (prev + 1 == Current.Value) + onCountIncrement(); + else + onCountChange(); + } + else + onCountRolling(); + } + + private void onCountIncrement() + { + popOutCountText.Hide(); + + DisplayedCount = Current.Value; + displayedCountText.ScaleTo(new Vector2(1f, 1.4f)) .ScaleTo(new Vector2(1f), 300, Easing.Out) .FadeIn(120); } - protected override void OnCountChange() + private void onCountChange() { - base.OnCountChange(); + popOutCountText.Hide(); - PopOutCountText.Hide(); - DisplayedCountText.ScaleTo(1f); + if (Current.Value == 0) + displayedCountText.FadeOut(); + + DisplayedCount = Current.Value; + + displayedCountText.ScaleTo(1f); } - protected override void OnCountRolling() + private void onCountRolling() { if (DisplayedCount > 0) { - PopOutCountText.Text = FormatCount(DisplayedCount); - PopOutCountText.FadeTo(0.8f).FadeOut(200) + popOutCountText.Text = DisplayedCount.ToString(CultureInfo.InvariantCulture); + popOutCountText.FadeTo(0.8f).FadeOut(200) .ScaleTo(1f).ScaleTo(4f, 200); - DisplayedCountText.FadeTo(0.5f, 300); + displayedCountText.FadeTo(0.5f, 300); } - base.OnCountRolling(); + // Hides displayed count if was increasing from 0 to 1 but didn't finish + if (DisplayedCount == 0 && Current.Value == 0) + displayedCountText.FadeOut(fade_out_duration); + + this.TransformTo(nameof(DisplayedCount), Current.Value, getProportionalDuration(DisplayedCount, Current.Value)); + } + + private double getProportionalDuration(int currentValue, int newValue) + { + double difference = currentValue > newValue ? currentValue - newValue : newValue - currentValue; + return difference * rolling_duration; } } } From 0e663d18014436c6bcb1d3ba99c6e66f7d299a87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 11 Sep 2024 11:27:21 +0200 Subject: [PATCH 2264/2493] Revert default combo counter code to pre-abstractification (and nuke eldritch abstract entity) --- .../Visual/Gameplay/TestSceneSkinEditor.cs | 8 +- osu.Game/Skinning/LegacyComboCounter.cs | 203 -------------- .../Skinning/LegacyDefaultComboCounter.cs | 264 +++++++++++++++--- 3 files changed, 234 insertions(+), 241 deletions(-) delete mode 100644 osu.Game/Skinning/LegacyComboCounter.cs diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs index 3a7bc05300..91188f5bac 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs @@ -440,8 +440,8 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("import old classic skin", () => skins.CurrentSkinInfo.Value = importedSkin = importSkinFromArchives(@"classic-layout-version-0.osk").SkinInfo); AddUntilStep("wait for load", () => globalHUDTarget.ComponentsLoaded && rulesetHUDTarget.ComponentsLoaded); - AddAssert("no combo in global target", () => !globalHUDTarget.Components.OfType().Any()); - AddAssert("combo placed in ruleset target", () => rulesetHUDTarget.Components.OfType().Count() == 1); + AddAssert("no combo in global target", () => !globalHUDTarget.Components.OfType().Any()); + AddAssert("combo placed in ruleset target", () => rulesetHUDTarget.Components.OfType().Count() == 1); AddStep("add combo to global target", () => globalHUDTarget.Add(new LegacyDefaultComboCounter { @@ -454,8 +454,8 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("select another skin", () => skins.CurrentSkinInfo.SetDefault()); AddStep("select skin again", () => skins.CurrentSkinInfo.Value = importedSkin); AddUntilStep("wait for load", () => globalHUDTarget.ComponentsLoaded && rulesetHUDTarget.ComponentsLoaded); - AddAssert("combo placed in global target", () => globalHUDTarget.Components.OfType().Count() == 1); - AddAssert("combo placed in ruleset target", () => rulesetHUDTarget.Components.OfType().Count() == 1); + AddAssert("combo placed in global target", () => globalHUDTarget.Components.OfType().Count() == 1); + AddAssert("combo placed in ruleset target", () => rulesetHUDTarget.Components.OfType().Count() == 1); } private Skin importSkinFromArchives(string filename) diff --git a/osu.Game/Skinning/LegacyComboCounter.cs b/osu.Game/Skinning/LegacyComboCounter.cs deleted file mode 100644 index 7003e0d3c8..0000000000 --- a/osu.Game/Skinning/LegacyComboCounter.cs +++ /dev/null @@ -1,203 +0,0 @@ -// 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.Bindables; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Game.Rulesets.Scoring; - -namespace osu.Game.Skinning -{ - /// - /// Uses the 'x' symbol and has a pop-out effect while rolling over. - /// - public abstract partial class LegacyComboCounter : CompositeDrawable, ISerialisableDrawable - { - public Bindable Current { get; } = new BindableInt { MinValue = 0 }; - - private const double fade_out_duration = 100; - - /// - /// Duration in milliseconds for the counter roll-up animation for each element. - /// - private const double rolling_duration = 20; - - protected readonly LegacySpriteText PopOutCountText; - protected readonly LegacySpriteText DisplayedCountText; - - private int previousValue; - - private int displayedCount; - - private bool isRolling; - - private readonly Container counterContainer; - - public bool UsesFixedAnchor { get; set; } - - protected LegacyComboCounter() - { - AutoSizeAxes = Axes.Both; - - InternalChildren = new[] - { - counterContainer = new Container - { - AlwaysPresent = true, - Children = new[] - { - PopOutCountText = new LegacySpriteText(LegacyFont.Combo) - { - Alpha = 0, - Blending = BlendingParameters.Additive, - BypassAutoSizeAxes = Axes.Both, - }, - DisplayedCountText = new LegacySpriteText(LegacyFont.Combo) - { - Alpha = 0, - AlwaysPresent = true, - BypassAutoSizeAxes = Axes.Both, - }, - } - } - }; - } - - /// - /// Value shown at the current moment. - /// - public virtual int DisplayedCount - { - get => displayedCount; - private set - { - if (displayedCount.Equals(value)) - return; - - if (isRolling) - onDisplayedCountRolling(value); - else if (displayedCount + 1 == value) - onDisplayedCountIncrement(value); - else - onDisplayedCountChange(value); - - displayedCount = value; - } - } - - [BackgroundDependencyLoader] - private void load(ScoreProcessor scoreProcessor) - { - Current.BindTo(scoreProcessor.Combo); - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - DisplayedCountText.Text = FormatCount(Current.Value); - PopOutCountText.Text = FormatCount(Current.Value); - - Current.BindValueChanged(combo => updateCount(combo.NewValue == 0), true); - - counterContainer.Size = DisplayedCountText.Size; - } - - private void updateCount(bool rolling) - { - int prev = previousValue; - previousValue = Current.Value; - - if (!IsLoaded) - return; - - if (!rolling) - { - FinishTransforms(false, nameof(DisplayedCount)); - - isRolling = false; - DisplayedCount = prev; - - if (prev + 1 == Current.Value) - OnCountIncrement(); - else - OnCountChange(); - } - else - { - OnCountRolling(); - isRolling = true; - } - } - - /// - /// Raised when the counter should display the new value with transitions. - /// - protected virtual void OnCountIncrement() - { - if (DisplayedCount < Current.Value - 1) - DisplayedCount++; - - DisplayedCount++; - } - - /// - /// Raised when the counter should roll to the new combo value (usually roll back to zero). - /// - protected virtual void OnCountRolling() - { - // Hides displayed count if was increasing from 0 to 1 but didn't finish - if (DisplayedCount == 0 && Current.Value == 0) - DisplayedCountText.FadeOut(fade_out_duration); - - transformRoll(DisplayedCount, Current.Value); - } - - /// - /// Raised when the counter should display the new combo value without any transitions. - /// - protected virtual void OnCountChange() - { - if (Current.Value == 0) - DisplayedCountText.FadeOut(); - - DisplayedCount = Current.Value; - } - - private void onDisplayedCountRolling(int newValue) - { - if (newValue == 0) - DisplayedCountText.FadeOut(fade_out_duration); - - DisplayedCountText.Text = FormatCount(newValue); - counterContainer.Size = DisplayedCountText.Size; - } - - private void onDisplayedCountChange(int newValue) - { - DisplayedCountText.FadeTo(newValue == 0 ? 0 : 1); - DisplayedCountText.Text = FormatCount(newValue); - - counterContainer.Size = DisplayedCountText.Size; - } - - private void onDisplayedCountIncrement(int newValue) - { - DisplayedCountText.Text = FormatCount(newValue); - - counterContainer.Size = DisplayedCountText.Size; - } - - private void transformRoll(int currentValue, int newValue) => - this.TransformTo(nameof(DisplayedCount), newValue, getProportionalDuration(currentValue, newValue)); - - protected virtual string FormatCount(int count) => $@"{count}"; - - private double getProportionalDuration(int currentValue, int newValue) - { - double difference = currentValue > newValue ? currentValue - newValue : newValue - currentValue; - return difference * rolling_duration; - } - } -} diff --git a/osu.Game/Skinning/LegacyDefaultComboCounter.cs b/osu.Game/Skinning/LegacyDefaultComboCounter.cs index 6c81b1f959..7de4aee656 100644 --- a/osu.Game/Skinning/LegacyDefaultComboCounter.cs +++ b/osu.Game/Skinning/LegacyDefaultComboCounter.cs @@ -1,8 +1,12 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; -using osu.Framework.Threading; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osu.Game.Rulesets.Scoring; using osuTK; namespace osu.Game.Skinning @@ -10,73 +14,265 @@ namespace osu.Game.Skinning /// /// Uses the 'x' symbol and has a pop-out effect while rolling over. /// - public partial class LegacyDefaultComboCounter : LegacyComboCounter + public partial class LegacyDefaultComboCounter : CompositeDrawable, ISerialisableDrawable { + public Bindable Current { get; } = new BindableInt { MinValue = 0 }; + + private uint scheduledPopOutCurrentId; + private const double big_pop_out_duration = 300; + private const double small_pop_out_duration = 100; - private ScheduledDelegate? scheduledPopOut; + private const double fade_out_duration = 100; + + /// + /// Duration in milliseconds for the counter roll-up animation for each element. + /// + private const double rolling_duration = 20; + + private readonly Drawable popOutCount; + + private readonly Drawable displayedCountSpriteText; + + private int previousValue; + + private int displayedCount; + + private bool isRolling; + + private readonly Container counterContainer; + + public bool UsesFixedAnchor { get; set; } public LegacyDefaultComboCounter() { + AutoSizeAxes = Axes.Both; + + Anchor = Anchor.BottomLeft; + Origin = Anchor.BottomLeft; + Margin = new MarginPadding(10); - PopOutCountText.Anchor = Anchor.BottomLeft; - DisplayedCountText.Anchor = Anchor.BottomLeft; + Scale = new Vector2(1.28f); + + InternalChildren = new[] + { + counterContainer = new Container + { + AlwaysPresent = true, + Children = new[] + { + popOutCount = new LegacySpriteText(LegacyFont.Combo) + { + Alpha = 0, + Blending = BlendingParameters.Additive, + Anchor = Anchor.BottomLeft, + BypassAutoSizeAxes = Axes.Both, + }, + displayedCountSpriteText = new LegacySpriteText(LegacyFont.Combo) + { + Alpha = 0, + AlwaysPresent = true, + Anchor = Anchor.BottomLeft, + BypassAutoSizeAxes = Axes.Both, + }, + } + } + }; + } + + /// + /// Value shown at the current moment. + /// + public virtual int DisplayedCount + { + get => displayedCount; + private set + { + if (displayedCount.Equals(value)) + return; + + if (isRolling) + onDisplayedCountRolling(value); + else if (displayedCount + 1 == value) + onDisplayedCountIncrement(value); + else + onDisplayedCountChange(value); + + displayedCount = value; + } + } + + [BackgroundDependencyLoader] + private void load(ScoreProcessor scoreProcessor) + { + Current.BindTo(scoreProcessor.Combo); } protected override void LoadComplete() { base.LoadComplete(); + ((IHasText)displayedCountSpriteText).Text = formatCount(Current.Value); + ((IHasText)popOutCount).Text = formatCount(Current.Value); + + Current.BindValueChanged(combo => updateCount(combo.NewValue == 0), true); + + updateLayout(); + } + + private void updateLayout() + { const float font_height_ratio = 0.625f; const float vertical_offset = 9; - DisplayedCountText.OriginPosition = new Vector2(0, font_height_ratio * DisplayedCountText.Height + vertical_offset); - DisplayedCountText.Position = new Vector2(0, -(1 - font_height_ratio) * DisplayedCountText.Height + vertical_offset); + displayedCountSpriteText.OriginPosition = new Vector2(0, font_height_ratio * displayedCountSpriteText.Height + vertical_offset); + displayedCountSpriteText.Position = new Vector2(0, -(1 - font_height_ratio) * displayedCountSpriteText.Height + vertical_offset); - PopOutCountText.OriginPosition = new Vector2(3, font_height_ratio * PopOutCountText.Height + vertical_offset); // In stable, the bigger pop out scales a bit to the left - PopOutCountText.Position = new Vector2(0, -(1 - font_height_ratio) * PopOutCountText.Height + vertical_offset); + popOutCount.OriginPosition = new Vector2(3, font_height_ratio * popOutCount.Height + vertical_offset); // In stable, the bigger pop out scales a bit to the left + popOutCount.Position = new Vector2(0, -(1 - font_height_ratio) * popOutCount.Height + vertical_offset); + + counterContainer.Size = displayedCountSpriteText.Size; } - protected override void OnCountIncrement() + private void updateCount(bool rolling) { - DisplayedCountText.Show(); + int prev = previousValue; + previousValue = Current.Value; - PopOutCountText.Text = FormatCount(Current.Value); + if (!IsLoaded) + return; - PopOutCountText.ScaleTo(1.56f) - .ScaleTo(1, big_pop_out_duration); - - PopOutCountText.FadeTo(0.6f) - .FadeOut(big_pop_out_duration); - - this.Delay(big_pop_out_duration - 140).Schedule(() => + if (!rolling) { - base.OnCountIncrement(); + FinishTransforms(false, nameof(DisplayedCount)); + isRolling = false; + DisplayedCount = prev; - DisplayedCountText.ScaleTo(1).Then() - .ScaleTo(1.1f, small_pop_out_duration / 2, Easing.In).Then() - .ScaleTo(1, small_pop_out_duration / 2, Easing.Out); - }, out scheduledPopOut); + if (prev + 1 == Current.Value) + onCountIncrement(prev, Current.Value); + else + onCountChange(Current.Value); + } + else + { + onCountRolling(displayedCount, Current.Value); + isRolling = true; + } } - protected override void OnCountRolling() + private void transformPopOut(int newValue) { - scheduledPopOut?.Cancel(); - scheduledPopOut = null; + ((IHasText)popOutCount).Text = formatCount(newValue); - base.OnCountRolling(); + popOutCount.ScaleTo(1.56f) + .ScaleTo(1, big_pop_out_duration); + + popOutCount.FadeTo(0.6f) + .FadeOut(big_pop_out_duration); } - protected override void OnCountChange() + private void transformNoPopOut(int newValue) { - scheduledPopOut?.Cancel(); - scheduledPopOut = null; + ((IHasText)displayedCountSpriteText).Text = formatCount(newValue); - base.OnCountChange(); + counterContainer.Size = displayedCountSpriteText.Size; + + displayedCountSpriteText.ScaleTo(1); } - protected override string FormatCount(int count) => $@"{count}x"; + private void transformPopOutSmall(int newValue) + { + ((IHasText)displayedCountSpriteText).Text = formatCount(newValue); + + counterContainer.Size = displayedCountSpriteText.Size; + + displayedCountSpriteText.ScaleTo(1).Then() + .ScaleTo(1.1f, small_pop_out_duration / 2, Easing.In).Then() + .ScaleTo(1, small_pop_out_duration / 2, Easing.Out); + } + + private void scheduledPopOutSmall(uint id) + { + // Too late; scheduled task invalidated + if (id != scheduledPopOutCurrentId) + return; + + DisplayedCount++; + } + + private void onCountIncrement(int currentValue, int newValue) + { + scheduledPopOutCurrentId++; + + if (DisplayedCount < currentValue) + DisplayedCount++; + + displayedCountSpriteText.Show(); + + transformPopOut(newValue); + + uint newTaskId = scheduledPopOutCurrentId; + + Scheduler.AddDelayed(delegate + { + scheduledPopOutSmall(newTaskId); + }, big_pop_out_duration - 140); + } + + private void onCountRolling(int currentValue, int newValue) + { + scheduledPopOutCurrentId++; + + // Hides displayed count if was increasing from 0 to 1 but didn't finish + if (currentValue == 0 && newValue == 0) + displayedCountSpriteText.FadeOut(fade_out_duration); + + transformRoll(currentValue, newValue); + } + + private void onCountChange(int newValue) + { + scheduledPopOutCurrentId++; + + if (newValue == 0) + displayedCountSpriteText.FadeOut(); + + DisplayedCount = newValue; + } + + private void onDisplayedCountRolling(int newValue) + { + if (newValue == 0) + displayedCountSpriteText.FadeOut(fade_out_duration); + else + displayedCountSpriteText.Show(); + + transformNoPopOut(newValue); + } + + private void onDisplayedCountChange(int newValue) + { + displayedCountSpriteText.FadeTo(newValue == 0 ? 0 : 1); + transformNoPopOut(newValue); + } + + private void onDisplayedCountIncrement(int newValue) + { + displayedCountSpriteText.Show(); + transformPopOutSmall(newValue); + } + + private void transformRoll(int currentValue, int newValue) => + this.TransformTo(nameof(DisplayedCount), newValue, getProportionalDuration(currentValue, newValue)); + + private string formatCount(int count) => $@"{count}x"; + + private double getProportionalDuration(int currentValue, int newValue) + { + double difference = currentValue > newValue ? currentValue - newValue : newValue - currentValue; + return difference * rolling_duration; + } } } From a7e1d35f648c449e749fa1346d3de704b87fe4ce Mon Sep 17 00:00:00 2001 From: apollo-dw <83023433+apollo-dw@users.noreply.github.com> Date: Wed, 11 Sep 2024 12:29:17 +0100 Subject: [PATCH 2265/2493] Update osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs Co-authored-by: James Wilson --- osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs b/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs index 8dfaaca7b2..a0f2a04f40 100644 --- a/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs +++ b/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs @@ -25,9 +25,9 @@ namespace osu.Game.Rulesets.Difficulty protected const int ATTRIB_ID_SCORE_MULTIPLIER = 15; protected const int ATTRIB_ID_FLASHLIGHT = 17; protected const int ATTRIB_ID_SLIDER_FACTOR = 19; - protected const int ATTRIB_ID_AIM_DIFFICULT_STRAIN_COUNT = 21; - protected const int ATTRIB_ID_SPEED_DIFFICULT_STRAIN_COUNT = 23; protected const int ATTRIB_ID_SPEED_NOTE_COUNT = 21; + protected const int ATTRIB_ID_SPEED_DIFFICULT_STRAIN_COUNT = 23; + protected const int ATTRIB_ID_AIM_DIFFICULT_STRAIN_COUNT = 25; /// /// The mods which were applied to the beatmap. From b78ef81bf1e5e4be82dac4302936d842f9d9bb6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 11 Sep 2024 15:54:07 +0200 Subject: [PATCH 2266/2493] Fix Flashlight not appearing on top of bubbles from Bubbles mod Inadvertently regressed in 44d0dc6113a408a15a18025325e55646a2147b14. --- osu.Game/Rulesets/Mods/ModFlashlight.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/Mods/ModFlashlight.cs b/osu.Game/Rulesets/Mods/ModFlashlight.cs index c924915bd0..64c193d25f 100644 --- a/osu.Game/Rulesets/Mods/ModFlashlight.cs +++ b/osu.Game/Rulesets/Mods/ModFlashlight.cs @@ -83,8 +83,6 @@ namespace osu.Game.Rulesets.Mods flashlight.RelativeSizeAxes = Axes.Both; flashlight.Colour = Color4.Black; - // Flashlight mods should always draw above any other mod adding overlays. - flashlight.Depth = float.MinValue; flashlight.Combo.BindTo(Combo); flashlight.GetPlayfieldScale = () => drawableRuleset.Playfield.Scale; @@ -95,6 +93,9 @@ namespace osu.Game.Rulesets.Mods // workaround for 1px gaps on the edges of the playfield which would sometimes show with "gameplay" screen scaling active. Padding = new MarginPadding(-1), Child = flashlight, + // Flashlight mods should always draw above any other mod adding overlays. + // NegativeInfinity is not used to allow one more thing drawn on top (used in replay analysis overlay in osu!). + Depth = float.MinValue, }); } From 4a39873e2aac3a6fa71a3be06407d9afb7df8922 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 11 Sep 2024 15:54:30 +0200 Subject: [PATCH 2267/2493] Fix replay analysis overlay not rotating with Barrel Roll enabled Closes https://github.com/ppy/osu/issues/29839. --- osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs | 3 ++- osu.Game/Rulesets/Mods/ModBarrelRoll.cs | 8 ++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs b/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs index 16edc654a7..4192d678dd 100644 --- a/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs @@ -44,7 +44,8 @@ namespace osu.Game.Rulesets.Osu.UI { if (replayPlayer != null) { - PlayfieldAdjustmentContainer.Add(new ReplayAnalysisOverlay(replayPlayer.Score.Replay)); + ReplayAnalysisOverlay analysisOverlay; + PlayfieldAdjustmentContainer.Add(analysisOverlay = new ReplayAnalysisOverlay(replayPlayer.Score.Replay)); replayPlayer.AddSettings(new ReplayAnalysisSettings(Config)); cursorHideEnabled = Config.GetBindable(OsuRulesetSetting.ReplayCursorHideEnabled); diff --git a/osu.Game/Rulesets/Mods/ModBarrelRoll.cs b/osu.Game/Rulesets/Mods/ModBarrelRoll.cs index 0c301d293f..4f90496308 100644 --- a/osu.Game/Rulesets/Mods/ModBarrelRoll.cs +++ b/osu.Game/Rulesets/Mods/ModBarrelRoll.cs @@ -40,9 +40,11 @@ namespace osu.Game.Rulesets.Mods public override string SettingDescription => $"{SpinSpeed.Value:N2} rpm {Direction.Value.GetDescription().ToLowerInvariant()}"; + private PlayfieldAdjustmentContainer playfieldAdjustmentContainer = null!; + public void Update(Playfield playfield) { - playfield.Rotation = CurrentRotation = (Direction.Value == RotationDirection.Counterclockwise ? -1 : 1) * 360 * (float)(playfield.Time.Current / 60000 * SpinSpeed.Value); + playfieldAdjustmentContainer.Rotation = CurrentRotation = (Direction.Value == RotationDirection.Counterclockwise ? -1 : 1) * 360 * (float)(playfield.Time.Current / 60000 * SpinSpeed.Value); } public void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset) @@ -52,7 +54,9 @@ namespace osu.Game.Rulesets.Mods var playfieldSize = drawableRuleset.Playfield.DrawSize; float minSide = MathF.Min(playfieldSize.X, playfieldSize.Y); float maxSide = MathF.Max(playfieldSize.X, playfieldSize.Y); - drawableRuleset.Playfield.Scale = new Vector2(minSide / maxSide); + + playfieldAdjustmentContainer = drawableRuleset.PlayfieldAdjustmentContainer; + playfieldAdjustmentContainer.Scale = new Vector2(minSide / maxSide); } } } From f38ae5f239ba215268cfc6bbbf4aa2263fc9845b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 11 Sep 2024 15:55:02 +0200 Subject: [PATCH 2268/2493] Fix replay analysis overlay being affected by visibility impairing mods Closes https://github.com/ppy/osu/issues/29748. --- osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs b/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs index 4192d678dd..ab69b67051 100644 --- a/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs @@ -46,6 +46,7 @@ namespace osu.Game.Rulesets.Osu.UI { ReplayAnalysisOverlay analysisOverlay; PlayfieldAdjustmentContainer.Add(analysisOverlay = new ReplayAnalysisOverlay(replayPlayer.Score.Replay)); + Overlays.Add(analysisOverlay.CreateProxy().With(p => p.Depth = float.NegativeInfinity)); replayPlayer.AddSettings(new ReplayAnalysisSettings(Config)); cursorHideEnabled = Config.GetBindable(OsuRulesetSetting.ReplayCursorHideEnabled); From 3398497d4b7ef41db750ba2b9db048528836ba18 Mon Sep 17 00:00:00 2001 From: StanR Date: Thu, 12 Sep 2024 01:04:07 +0500 Subject: [PATCH 2269/2493] Remove kickslider changes --- osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs index 9e851f11a6..ac38bc14e1 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs @@ -134,8 +134,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators // bpm change was from a slider, this is easier typically than circle -> circle // unintentional side effect is that bursts with kicksliders at the ends might have lower difficulty than bursts without sliders - // therefore we're checking for quick sliders and don't lower the difficulty for them since they don't really make tapping easier (no time to adjust) - if (prevObj.BaseObject is Slider && prevObj.TravelTime > prevDelta * 1.5) + if (prevObj.BaseObject is Slider) effectiveRatio *= 0.2; // repeated island polartiy (2 -> 4, 3 -> 5) From 77c3cb65045876d2443a8c1aff442347eab81cc6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 12 Sep 2024 14:38:51 +0900 Subject: [PATCH 2270/2493] 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 7b45b9dec4..d5bdfd91b5 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -10,7 +10,7 @@ true - + diff --git a/osu.iOS.props b/osu.iOS.props index 1d76deddac..da1cec395f 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -17,6 +17,6 @@ -all - + From 652a59061164dadb2db1302d9c896e5cefecdec9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 13 Sep 2024 09:59:20 +0200 Subject: [PATCH 2271/2493] Attempt to address design concerns --- .../UserInterface/TestSceneFormControls.cs | 2 -- .../Graphics/UserInterfaceV2/FormCheckBox.cs | 33 +++++++++++++------ 2 files changed, 23 insertions(+), 12 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneFormControls.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneFormControls.cs index 9c05a34010..be2ba860d3 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneFormControls.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneFormControls.cs @@ -58,8 +58,6 @@ namespace osu.Game.Tests.Visual.UserInterface { Caption = EditorSetupStrings.LetterboxDuringBreaks, HintText = EditorSetupStrings.LetterboxDuringBreaksDescription, - OnText = "Letterbox", - OffText = "Do not letterbox", }, new FormCheckBox { diff --git a/osu.Game/Graphics/UserInterfaceV2/FormCheckBox.cs b/osu.Game/Graphics/UserInterfaceV2/FormCheckBox.cs index 587aa921f5..6054e898fe 100644 --- a/osu.Game/Graphics/UserInterfaceV2/FormCheckBox.cs +++ b/osu.Game/Graphics/UserInterfaceV2/FormCheckBox.cs @@ -14,7 +14,9 @@ using osu.Framework.Input.Events; using osu.Framework.Localisation; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; +using osu.Game.Localisation; using osu.Game.Overlays; +using osuTK; namespace osu.Game.Graphics.UserInterfaceV2 { @@ -30,8 +32,6 @@ namespace osu.Game.Graphics.UserInterfaceV2 public LocalisableString Caption { get; init; } public LocalisableString HintText { get; init; } - public LocalisableString OnText { get; init; } = "On"; - public LocalisableString OffText { get; init; } = "Off"; private Box background = null!; private FormFieldCaption caption = null!; @@ -74,17 +74,30 @@ namespace osu.Game.Graphics.UserInterfaceV2 Anchor = Anchor.TopLeft, Origin = Anchor.TopLeft, }, - text = new OsuSpriteText + new FillFlowContainer { RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, - }, - checkbox = new Nub - { - Anchor = Anchor.CentreRight, - Origin = Anchor.CentreRight, - Current = Current, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(7), + Children = new Drawable[] + { + checkbox = new Nub + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Current = Current, + Margin = new MarginPadding { Top = 2, }, + }, + text = new OsuSpriteText + { + RelativeSizeAxes = Axes.X, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + }, + } } }, }, @@ -141,7 +154,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 checkbox.Colour = Current.Disabled ? colourProvider.Foreground1 : colourProvider.Content1; text.Colour = Current.Disabled ? colourProvider.Foreground1 : colourProvider.Content1; - text.Text = Current.Value ? OnText : OffText; + text.Text = Current.Value ? CommonStrings.Enabled : CommonStrings.Disabled; if (!Current.Disabled) { From 1292a34b9d9ab0d932c6d56976a1e3b38f789b8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 13 Sep 2024 10:53:43 +0200 Subject: [PATCH 2272/2493] Add failing test coverage for object nudging --- .../Editing/TestSceneComposerSelection.cs | 48 ++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneComposerSelection.cs b/osu.Game.Tests/Visual/Editing/TestSceneComposerSelection.cs index 3884a3108f..ff93a8d83f 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneComposerSelection.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneComposerSelection.cs @@ -7,6 +7,7 @@ using NUnit.Framework; using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.UserInterface; using osu.Framework.Testing; +using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets; @@ -78,7 +79,7 @@ namespace osu.Game.Tests.Visual.Editing } [Test] - public void TestNudgeSelection() + public void TestNudgeSelectionTime() { HitCircle[] addedObjects = null!; @@ -99,6 +100,51 @@ namespace osu.Game.Tests.Visual.Editing AddAssert("objects reverted to original position", () => addedObjects[0].StartTime == 100); } + [Test] + public void TestNudgeSelectionPosition() + { + HitCircle addedObject = null!; + + AddStep("add hitobjects", () => EditorBeatmap.AddRange(new[] + { + addedObject = new HitCircle { StartTime = 200, Position = new Vector2(100) }, + })); + + AddStep("select object", () => EditorBeatmap.SelectedHitObjects.Add(addedObject)); + + AddStep("nudge up", () => + { + InputManager.PressKey(Key.ControlLeft); + InputManager.Key(Key.Up); + InputManager.ReleaseKey(Key.ControlLeft); + }); + AddAssert("object position moved up", () => addedObject.Position.Y, () => Is.EqualTo(99).Within(Precision.FLOAT_EPSILON)); + + AddStep("nudge down", () => + { + InputManager.PressKey(Key.ControlLeft); + InputManager.Key(Key.Down); + InputManager.ReleaseKey(Key.ControlLeft); + }); + AddAssert("object position moved down", () => addedObject.Position.Y, () => Is.EqualTo(100).Within(Precision.FLOAT_EPSILON)); + + AddStep("nudge left", () => + { + InputManager.PressKey(Key.ControlLeft); + InputManager.Key(Key.Left); + InputManager.ReleaseKey(Key.ControlLeft); + }); + AddAssert("object position moved left", () => addedObject.Position.X, () => Is.EqualTo(99).Within(Precision.FLOAT_EPSILON)); + + AddStep("nudge right", () => + { + InputManager.PressKey(Key.ControlLeft); + InputManager.Key(Key.Right); + InputManager.ReleaseKey(Key.ControlLeft); + }); + AddAssert("object position moved right", () => addedObject.Position.X, () => Is.EqualTo(100).Within(Precision.FLOAT_EPSILON)); + } + [Test] public void TestRotateHotkeys() { From 2ccb4a48eb68600559e0fbdd58712ab726211927 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 13 Sep 2024 11:18:50 +0200 Subject: [PATCH 2273/2493] Add test coverage for seeking between objects in editor --- .../Visual/Editing/TestSceneEditorSeeking.cs | 39 ++++++++++++++++++- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorSeeking.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorSeeking.cs index da4f159cae..06facc546d 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorSeeking.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorSeeking.cs @@ -1,11 +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.Linq; using NUnit.Framework; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets; using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.Osu.Objects; using osuTK.Input; namespace osu.Game.Tests.Visual.Editing @@ -135,9 +137,42 @@ namespace osu.Game.Tests.Visual.Editing pressAndCheckTime(Key.Up, 0); } - private void pressAndCheckTime(Key key, double expectedTime) + [Test] + public void TestSeekBetweenObjects() { - AddStep($"press {key}", () => InputManager.Key(key)); + AddStep("add objects", () => + { + EditorBeatmap.Clear(); + EditorBeatmap.AddRange(new[] + { + new HitCircle { StartTime = 1000, }, + new HitCircle { StartTime = 2250, }, + new HitCircle { StartTime = 3600, }, + }); + }); + AddStep("seek to 0", () => EditorClock.Seek(0)); + + pressAndCheckTime(Key.Right, 1000, Key.ControlLeft); + pressAndCheckTime(Key.Right, 2250, Key.ControlLeft); + pressAndCheckTime(Key.Right, 3600, Key.ControlLeft); + pressAndCheckTime(Key.Right, 3600, Key.ControlLeft); + pressAndCheckTime(Key.Left, 2250, Key.ControlLeft); + pressAndCheckTime(Key.Left, 1000, Key.ControlLeft); + pressAndCheckTime(Key.Left, 1000, Key.ControlLeft); + } + + private void pressAndCheckTime(Key key, double expectedTime, params Key[] modifiers) + { + AddStep($"press {key} with {(modifiers.Any() ? string.Join(',', modifiers) : "no modifiers")}", () => + { + foreach (var modifier in modifiers) + InputManager.PressKey(modifier); + + InputManager.Key(key); + + foreach (var modifier in modifiers) + InputManager.ReleaseKey(modifier); + }); AddUntilStep($"time is {expectedTime}", () => EditorClock.CurrentTime, () => Is.EqualTo(expectedTime).Within(1)); } } From 929ea87975520450ead9e385d9560d105c2f1063 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 13 Sep 2024 14:53:59 +0200 Subject: [PATCH 2274/2493] Revert to checkbox on right --- .../Graphics/UserInterfaceV2/FormCheckBox.cs | 30 +++++-------------- 1 file changed, 8 insertions(+), 22 deletions(-) diff --git a/osu.Game/Graphics/UserInterfaceV2/FormCheckBox.cs b/osu.Game/Graphics/UserInterfaceV2/FormCheckBox.cs index 6054e898fe..797ff09800 100644 --- a/osu.Game/Graphics/UserInterfaceV2/FormCheckBox.cs +++ b/osu.Game/Graphics/UserInterfaceV2/FormCheckBox.cs @@ -16,7 +16,6 @@ using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Localisation; using osu.Game.Overlays; -using osuTK; namespace osu.Game.Graphics.UserInterfaceV2 { @@ -74,31 +73,18 @@ namespace osu.Game.Graphics.UserInterfaceV2 Anchor = Anchor.TopLeft, Origin = Anchor.TopLeft, }, - new FillFlowContainer + text = new OsuSpriteText { RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(7), - Children = new Drawable[] - { - checkbox = new Nub - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Current = Current, - Margin = new MarginPadding { Top = 2, }, - }, - text = new OsuSpriteText - { - RelativeSizeAxes = Axes.X, - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - }, - } - } + }, + checkbox = new Nub + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + Current = Current, + }, }, }, }; From f71ce8869e8cccd50651f3cbe52355093b0c09e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 13 Sep 2024 14:54:07 +0200 Subject: [PATCH 2275/2493] Limit width of test scene controls To better reflect what the widths should be in actual usage. --- osu.Game.Tests/Visual/UserInterface/TestSceneFormControls.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneFormControls.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneFormControls.cs index be2ba860d3..eb8a8b3fe9 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneFormControls.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneFormControls.cs @@ -26,7 +26,10 @@ namespace osu.Game.Tests.Visual.UserInterface RelativeSizeAxes = Axes.Both, Child = new FillFlowContainer { - RelativeSizeAxes = Axes.Both, + RelativeSizeAxes = Axes.Y, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Width = 400, Direction = FillDirection.Vertical, Spacing = new Vector2(5), Padding = new MarginPadding(10), From 7f71ef4547981d9d3d432264087fa6a5813cbc46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 13 Sep 2024 15:14:09 +0200 Subject: [PATCH 2276/2493] Only allow seek to next/previous object via keybinding if there is no selection --- osu.Game/Screens/Edit/Editor.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 9bb91af806..a3549fdec5 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -721,10 +721,16 @@ namespace osu.Game.Screens.Edit switch (e.Action) { case GlobalAction.EditorSeekToPreviousHitObject: + if (editorBeatmap.SelectedHitObjects.Any()) + return false; + seekHitObject(-1); return true; case GlobalAction.EditorSeekToNextHitObject: + if (editorBeatmap.SelectedHitObjects.Any()) + return false; + seekHitObject(1); return true; From a4f6d4a300e2da5ba12d271965272f15634b8437 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 13 Sep 2024 15:58:41 +0200 Subject: [PATCH 2277/2493] Backpopulate missing ranked/submitted dates using new local metadata cache People keep asking why https://github.com/ppy/osu/pull/29553 didn't fix their databases (as stated in the PR, it didn't intend to), so this should do it for them. --- .../LocalCachedBeatmapMetadataSource.cs | 17 ++- .../Database/BackgroundDataStoreProcessor.cs | 104 ++++++++++++++++++ 2 files changed, 119 insertions(+), 2 deletions(-) diff --git a/osu.Game/Beatmaps/LocalCachedBeatmapMetadataSource.cs b/osu.Game/Beatmaps/LocalCachedBeatmapMetadataSource.cs index 96817571f6..eaa4d8ebfb 100644 --- a/osu.Game/Beatmaps/LocalCachedBeatmapMetadataSource.cs +++ b/osu.Game/Beatmaps/LocalCachedBeatmapMetadataSource.cs @@ -3,6 +3,7 @@ using System; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Threading.Tasks; using Microsoft.Data.Sqlite; @@ -78,7 +79,7 @@ namespace osu.Game.Beatmaps // cached database exists on disk. && storage.Exists(cache_database_name); - public bool TryLookup(BeatmapInfo beatmapInfo, out OnlineBeatmapMetadata? onlineMetadata) + public bool TryLookup(BeatmapInfo beatmapInfo, [NotNullWhen(true)] out OnlineBeatmapMetadata? onlineMetadata) { Debug.Assert(beatmapInfo.BeatmapSet != null); @@ -98,7 +99,7 @@ namespace osu.Game.Beatmaps try { - using (var db = new SqliteConnection(string.Concat(@"Data Source=", storage.GetFullPath(@"online.db", true)))) + using (var db = getConnection()) { db.Open(); @@ -125,6 +126,9 @@ namespace osu.Game.Beatmaps return false; } + private SqliteConnection getConnection() => + new SqliteConnection(string.Concat(@"Data Source=", storage.GetFullPath(@"online.db", true))); + private void prepareLocalCache() { bool isRefetch = storage.Exists(cache_database_name); @@ -191,6 +195,15 @@ namespace osu.Game.Beatmaps }); } + public int GetCacheVersion() + { + using (var connection = getConnection()) + { + connection.Open(); + return getCacheVersion(connection); + } + } + private int getCacheVersion(SqliteConnection connection) { using (var cmd = connection.CreateCommand()) diff --git a/osu.Game/Database/BackgroundDataStoreProcessor.cs b/osu.Game/Database/BackgroundDataStoreProcessor.cs index 16ff766ea4..59ef9a3ae1 100644 --- a/osu.Game/Database/BackgroundDataStoreProcessor.cs +++ b/osu.Game/Database/BackgroundDataStoreProcessor.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -11,6 +12,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Logging; +using osu.Framework.Platform; using osu.Game.Beatmaps; using osu.Game.Extensions; using osu.Game.Online.API; @@ -61,6 +63,9 @@ namespace osu.Game.Database [Resolved] private IAPIProvider api { get; set; } = null!; + [Resolved] + private Storage storage { get; set; } = null!; + protected virtual int TimeToSleepDuringGameplay => 30000; protected override void LoadComplete() @@ -78,6 +83,7 @@ namespace osu.Game.Database processScoresWithMissingStatistics(); convertLegacyTotalScoreToStandardised(); upgradeScoreRanks(); + backpopulateMissingSubmissionAndRankDates(); }, TaskCreationOptions.LongRunning).ContinueWith(t => { if (t.Exception?.InnerException is ObjectDisposedException) @@ -443,6 +449,104 @@ namespace osu.Game.Database completeNotification(notification, processedCount, scoreIds.Count, failedCount); } + private void backpopulateMissingSubmissionAndRankDates() + { + var localMetadataSource = new LocalCachedBeatmapMetadataSource(storage); + + if (!localMetadataSource.Available) + { + Logger.Log("Cannot backpopulate missing submission/rank dates because the local metadata cache is missing."); + return; + } + + try + { + if (localMetadataSource.GetCacheVersion() < 2) + { + Logger.Log("Cannot backpopulate missing submission/rank dates because the local metadata cache is too old."); + return; + } + } + catch (Exception ex) + { + Logger.Log($"Error when trying to query version of local metadata cache: {ex}"); + return; + } + + Logger.Log("Querying for beatmap sets that contain missing submission/rank date..."); + + HashSet beatmapSetIds = realmAccess.Run(r => new HashSet( + r.All() + .Where(b => b.StatusInt > 0 && (b.DateRanked == null || b.DateSubmitted == null)) + .AsEnumerable() + .Select(b => b.ID))); + + Logger.Log($"Found {beatmapSetIds.Count} beatmap sets with missing submission/rank date."); + + if (beatmapSetIds.Count == 0) + return; + + var notification = showProgressNotification(beatmapSetIds.Count, "Populating missing submission and rank dates", "beatmap sets now have correct submission and rank dates."); + + int processedCount = 0; + int failedCount = 0; + + foreach (var id in beatmapSetIds) + { + if (notification?.State == ProgressNotificationState.Cancelled) + break; + + updateNotificationProgress(notification, processedCount, beatmapSetIds.Count); + + sleepIfRequired(); + + try + { + // Can't use async overload because we're not on the update thread. + // ReSharper disable once MethodHasAsyncOverload + bool succeeded = realmAccess.Write(r => + { + BeatmapSetInfo beatmapSet = r.Find(id)!; + + // we want any ranked representative of the set. + // the reason for checking ranked status of the difficulty is that it can be locally modified, + // at which point the lookup will fail - but there might still be another unmodified difficulty on which it will work. + if (beatmapSet.Beatmaps.FirstOrDefault(b => b.Status >= BeatmapOnlineStatus.Ranked) is not BeatmapInfo beatmap) + return false; + + bool lookupSucceeded = localMetadataSource.TryLookup(beatmap, out var result); + + if (lookupSucceeded) + { + Debug.Assert(result != null); + beatmapSet.DateRanked = result.DateRanked; + beatmapSet.DateSubmitted = result.DateSubmitted; + return true; + } + + Logger.Log($"Could not find {beatmapSet.GetDisplayString()} in local cache while backpopulating missing submission/rank date"); + return false; + }); + + if (succeeded) + ++processedCount; + else + ++failedCount; + } + catch (ObjectDisposedException) + { + throw; + } + catch (Exception e) + { + Logger.Log($"Failed to update ranked/submitted dates for beatmap set {id}: {e}"); + ++failedCount; + } + } + + completeNotification(notification, processedCount, beatmapSetIds.Count, failedCount); + } + private void updateNotificationProgress(ProgressNotification? notification, int processedCount, int totalCount) { if (notification == null) From 562a5006eae0d9f964d7891daa0f203834e0dc3e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 14 Sep 2024 02:19:52 +0900 Subject: [PATCH 2278/2493] Change log output to only output when matches are found (in line with other methods) --- osu.Game/Database/BackgroundDataStoreProcessor.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Database/BackgroundDataStoreProcessor.cs b/osu.Game/Database/BackgroundDataStoreProcessor.cs index 59ef9a3ae1..0fa785e494 100644 --- a/osu.Game/Database/BackgroundDataStoreProcessor.cs +++ b/osu.Game/Database/BackgroundDataStoreProcessor.cs @@ -481,11 +481,11 @@ namespace osu.Game.Database .AsEnumerable() .Select(b => b.ID))); - Logger.Log($"Found {beatmapSetIds.Count} beatmap sets with missing submission/rank date."); - if (beatmapSetIds.Count == 0) return; + Logger.Log($"Found {beatmapSetIds.Count} beatmap sets with missing submission/rank date."); + var notification = showProgressNotification(beatmapSetIds.Count, "Populating missing submission and rank dates", "beatmap sets now have correct submission and rank dates."); int processedCount = 0; From 1204136af81596dffdfd7f4a8afc7d31d8788fea Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 14 Sep 2024 02:46:19 +0900 Subject: [PATCH 2279/2493] Update icon file with new design --- osu.Desktop/beatmap.ico | Bin 59403 -> 356968 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/osu.Desktop/beatmap.ico b/osu.Desktop/beatmap.ico index 410ccfd73d6e42edbf3fc6ccc01bc9e00d6cd9a9..58ab198bc273205c90a73d1efbdb16c0751f811f 100644 GIT binary patch literal 356968 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

U-SDh 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 z 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 I+Vs``0KQHqH2?qr literal 59403 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 DfNejV From eb102479f4ee1548803fa6e03f46fb08e2529053 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 14 Sep 2024 02:47:19 +0900 Subject: [PATCH 2280/2493] Use beatmap icon for `.osr` and `.osk` for now --- osu.Desktop/Windows/WindowsAssociationManager.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Desktop/Windows/WindowsAssociationManager.cs b/osu.Desktop/Windows/WindowsAssociationManager.cs index 92cffd0987..c8066cabda 100644 --- a/osu.Desktop/Windows/WindowsAssociationManager.cs +++ b/osu.Desktop/Windows/WindowsAssociationManager.cs @@ -42,8 +42,8 @@ namespace osu.Desktop.Windows { new FileAssociation(@".osz", WindowsAssociationManagerStrings.OsuBeatmap, Icons.Beatmap), new FileAssociation(@".olz", WindowsAssociationManagerStrings.OsuBeatmap, Icons.Beatmap), - new FileAssociation(@".osr", WindowsAssociationManagerStrings.OsuReplay, Icons.Lazer), - new FileAssociation(@".osk", WindowsAssociationManagerStrings.OsuSkin, Icons.Lazer), + new FileAssociation(@".osr", WindowsAssociationManagerStrings.OsuReplay, Icons.Beatmap), + new FileAssociation(@".osk", WindowsAssociationManagerStrings.OsuSkin, Icons.Beatmap), }; private static readonly UriAssociation[] uri_associations = From 385eb5eed5a4842e0eeb4e4b4dc450d77ec7c407 Mon Sep 17 00:00:00 2001 From: kongehund <63306696+kongehund@users.noreply.github.com> Date: Sat, 14 Sep 2024 16:32:51 +0200 Subject: [PATCH 2281/2493] Rewrite GetConvexHull --- osu.Game/Utils/GeometryUtils.cs | 76 +++++++++++++++++++++++---------- 1 file changed, 53 insertions(+), 23 deletions(-) diff --git a/osu.Game/Utils/GeometryUtils.cs b/osu.Game/Utils/GeometryUtils.cs index d4c1dc2db7..4c90421aca 100644 --- a/osu.Game/Utils/GeometryUtils.cs +++ b/osu.Game/Utils/GeometryUtils.cs @@ -152,42 +152,72 @@ namespace osu.Game.Utils /// The points to calculate a convex hull. public static List GetConvexHull(IEnumerable points) { - List p = points.ToList(); + // Naming convention implies positive y upwards. - if (p.Count < 3) - return p; + bool isCCW(Vector2 a, Vector2 b, Vector2 c) => crossProduct(b - a, c - a) > 0; - p.Sort((a, b) => a.X == b.X ? a.Y.CompareTo(b.Y) : a.X.CompareTo(b.X)); + float crossProduct(Vector2 v1, Vector2 v2) => v1.X * v2.Y - v1.Y * v2.X; - List upper = new List(); - List lower = new List(); + var pointsList = points.ToList(); - // Build the lower hull - for (int i = 0; i < p.Count; i++) + pointsList.Sort(delegate (Vector2 point1, Vector2 point2) { - while (lower.Count >= 2 && cross(lower[^2], lower[^1], p[i]) <= 0) - lower.RemoveAt(lower.Count - 1); + if (point1.X == point2.X) + return point1.Y.CompareTo(point2.Y); + return point1.X.CompareTo(point2.X); + }); - lower.Add(p[i]); + if (pointsList.Count < 3) + return pointsList; + + var convexHullUpper = new List + { + pointsList[0], + pointsList[1] + }; + var convexHullLower = new List + { + pointsList[pointsList.Count - 1], + pointsList[pointsList.Count - 2] + }; + + for (int i_points = 2; i_points < pointsList.Count; i_points++) + { + Vector2 c = pointsList[i_points]; + for (int i_hull = convexHullUpper.Count - 1; i_hull > 0; i_hull--) + { + Vector2 a = convexHullUpper[^2]; + Vector2 b = convexHullUpper[^1]; + if (isCCW(a, b, c)) + convexHullUpper.Remove(b); + else + break; + } + convexHullUpper.Add(c); } - // Build the upper hull - for (int i = p.Count - 1; i >= 0; i--) + for (int i_points = pointsList.Count - 3; i_points >= 0; i_points--) { - while (upper.Count >= 2 && cross(upper[^2], upper[^1], p[i]) <= 0) - upper.RemoveAt(upper.Count - 1); - - upper.Add(p[i]); + Vector2 c = pointsList[i_points]; + for (int i_hull = convexHullLower.Count - 1; i_hull > 0; i_hull--) + { + Vector2 a = convexHullLower[^2]; + Vector2 b = convexHullLower[^1]; + if (isCCW(a, b, c)) + convexHullLower.Remove(b); + else + break; + } + convexHullLower.Add(c); } - // Remove the last point of each half because it's a duplicate of the first point of the other half - lower.RemoveAt(lower.Count - 1); - upper.RemoveAt(upper.Count - 1); + convexHullUpper.RemoveAt(convexHullUpper.Count - 1); + convexHullLower.RemoveAt(convexHullLower.Count - 1); - lower.AddRange(upper); - return lower; + convexHullUpper.AddRange(convexHullLower); + var convexHull = convexHullUpper; - float cross(Vector2 o, Vector2 a, Vector2 b) => (a.X - o.X) * (b.Y - o.Y) - (a.Y - o.Y) * (b.X - o.X); + return convexHull; } public static List GetConvexHull(IEnumerable hitObjects) => From 30096c1c7198168c667f85873a60461410df296a Mon Sep 17 00:00:00 2001 From: OliBomby Date: Sat, 14 Sep 2024 17:26:04 +0200 Subject: [PATCH 2282/2493] clean up code --- osu.Game/Utils/GeometryUtils.cs | 74 ++++++++++++--------------------- 1 file changed, 27 insertions(+), 47 deletions(-) diff --git a/osu.Game/Utils/GeometryUtils.cs b/osu.Game/Utils/GeometryUtils.cs index 4c90421aca..8572ac6609 100644 --- a/osu.Game/Utils/GeometryUtils.cs +++ b/osu.Game/Utils/GeometryUtils.cs @@ -152,72 +152,52 @@ namespace osu.Game.Utils /// The points to calculate a convex hull. public static List GetConvexHull(IEnumerable points) { - // Naming convention implies positive y upwards. - - bool isCCW(Vector2 a, Vector2 b, Vector2 c) => crossProduct(b - a, c - a) > 0; - - float crossProduct(Vector2 v1, Vector2 v2) => v1.X * v2.Y - v1.Y * v2.X; - - var pointsList = points.ToList(); - - pointsList.Sort(delegate (Vector2 point1, Vector2 point2) - { - if (point1.X == point2.X) - return point1.Y.CompareTo(point2.Y); - return point1.X.CompareTo(point2.X); - }); + var pointsList = points.OrderBy(p => p.X).ThenBy(p => p.Y).ToList(); if (pointsList.Count < 3) return pointsList; - var convexHullUpper = new List + var convexHullLower = new List { pointsList[0], pointsList[1] }; - var convexHullLower = new List + var convexHullUpper = new List { - pointsList[pointsList.Count - 1], - pointsList[pointsList.Count - 2] + pointsList[^1], + pointsList[^2] }; - for (int i_points = 2; i_points < pointsList.Count; i_points++) + // Build the lower hull. + for (int i = 2; i < pointsList.Count; i++) { - Vector2 c = pointsList[i_points]; - for (int i_hull = convexHullUpper.Count - 1; i_hull > 0; i_hull--) - { - Vector2 a = convexHullUpper[^2]; - Vector2 b = convexHullUpper[^1]; - if (isCCW(a, b, c)) - convexHullUpper.Remove(b); - else - break; - } - convexHullUpper.Add(c); - } + Vector2 c = pointsList[i]; + while (convexHullLower.Count > 1 && isClockwise(convexHullLower[^2], convexHullLower[^1], c)) + convexHullLower.RemoveAt(convexHullLower.Count - 1); - for (int i_points = pointsList.Count - 3; i_points >= 0; i_points--) - { - Vector2 c = pointsList[i_points]; - for (int i_hull = convexHullLower.Count - 1; i_hull > 0; i_hull--) - { - Vector2 a = convexHullLower[^2]; - Vector2 b = convexHullLower[^1]; - if (isCCW(a, b, c)) - convexHullLower.Remove(b); - else - break; - } convexHullLower.Add(c); } - convexHullUpper.RemoveAt(convexHullUpper.Count - 1); + // Build the upper hull. + for (int i = pointsList.Count - 3; i >= 0; i--) + { + Vector2 c = pointsList[i]; + while (convexHullUpper.Count > 1 && isClockwise(convexHullUpper[^2], convexHullUpper[^1], c)) + convexHullUpper.RemoveAt(convexHullUpper.Count - 1); + + convexHullUpper.Add(c); + } + convexHullLower.RemoveAt(convexHullLower.Count - 1); + convexHullUpper.RemoveAt(convexHullUpper.Count - 1); - convexHullUpper.AddRange(convexHullLower); - var convexHull = convexHullUpper; + convexHullLower.AddRange(convexHullUpper); - return convexHull; + return convexHullLower; + + float crossProduct(Vector2 v1, Vector2 v2) => v1.X * v2.Y - v1.Y * v2.X; + + bool isClockwise(Vector2 a, Vector2 b, Vector2 c) => crossProduct(b - a, c - a) >= 0; } public static List GetConvexHull(IEnumerable hitObjects) => From 5e8174faa881d5a54398ddd4c0226f9a18e40f92 Mon Sep 17 00:00:00 2001 From: StanR Date: Sat, 14 Sep 2024 22:03:01 +0500 Subject: [PATCH 2283/2493] Reduce ratio for island size 1 --- .../Difficulty/Evaluators/RhythmEvaluator.cs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs index ac38bc14e1..bd99799ee0 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs @@ -66,7 +66,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators private const int history_time_max = 5 * 1000; // 5 seconds of calculatingRhythmBonus max. private const int history_objects_max = 32; - private const double rhythm_multiplier = 1.2; + private const double rhythm_multiplier = 1.3; ///

/// Calculates a rhythm multiplier for the difficulty of the tap associated with historic data of the current . @@ -113,7 +113,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators double prevDelta = prevObj.StrainTime; double lastDelta = lastObj.StrainTime; - double currRatio = 1.0 + 5.8 * Math.Min(0.5, Math.Pow(Math.Sin(Math.PI / (Math.Min(prevDelta, currDelta) / Math.Max(prevDelta, currDelta))), 2)); // fancy function to calculate rhythmbonuses. + double currRatio = 1.0 + 6.0 * Math.Min(0.5, Math.Pow(Math.Sin(Math.PI / (Math.Min(prevDelta, currDelta) / Math.Max(prevDelta, currDelta))), 2)); // fancy function to calculate rhythmbonuses. double windowPenalty = Math.Min(1, Math.Max(0, Math.Abs(prevDelta - currDelta) - deltaDifferenceEpsilon) / deltaDifferenceEpsilon); @@ -137,7 +137,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators if (prevObj.BaseObject is Slider) effectiveRatio *= 0.2; - // repeated island polartiy (2 -> 4, 3 -> 5) + // repeated island polarity (2 -> 4, 3 -> 5) if (island.IsSimilarPolarity(previousIsland)) effectiveRatio *= 0.3; @@ -145,6 +145,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators if (lastDelta > prevDelta + deltaDifferenceEpsilon && prevDelta > currDelta + deltaDifferenceEpsilon) effectiveRatio *= 0.125; + // singletaps are easier to control + if (island.Deltas.Count == 1) + effectiveRatio *= 0.7; + if (!islandCounts.TryAdd(island, 1)) { // only add island to island counts if they're going one after another From db626f168ad294b90ea0fbb989b0fc681557b35c Mon Sep 17 00:00:00 2001 From: StanR Date: Sun, 15 Sep 2024 01:12:41 +0500 Subject: [PATCH 2284/2493] Refactor --- .../Difficulty/Evaluators/RhythmEvaluator.cs | 40 +++++++++---------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs index bd99799ee0..fb1374eccf 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using System.Linq; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Osu.Difficulty.Preprocessing; using osu.Game.Rulesets.Osu.Objects; @@ -12,7 +11,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators { public static class RhythmEvaluator { - private readonly struct Island : IEquatable + private struct Island : IEquatable { private readonly double deltaDifferenceEpsilon; @@ -21,41 +20,39 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators deltaDifferenceEpsilon = epsilon; } - public Island(int firstDelta, double epsilon) + public Island(int delta, double epsilon) { deltaDifferenceEpsilon = epsilon; - AddDelta(firstDelta); + Delta = Math.Max(delta, OsuDifficultyHitObject.MIN_DELTA_TIME); } - public List Deltas { get; } = new List(); + public int Delta { get; private set; } + public int DeltaCount { get; private set; } public void AddDelta(int delta) { - double epsilon = deltaDifferenceEpsilon; - int existingDelta = Deltas.FirstOrDefault(x => Math.Abs(x - delta) >= epsilon); + if (Delta == default) + Delta = Math.Max(delta, OsuDifficultyHitObject.MIN_DELTA_TIME); - Deltas.Add(existingDelta == default ? delta : existingDelta); + DeltaCount++; } - public double AverageDelta() => Deltas.Count > 0 ? Math.Max(Deltas.Average(), OsuDifficultyHitObject.MIN_DELTA_TIME) : 0; - public bool IsSimilarPolarity(Island other) { // consider islands to be of similar polarity only if they're having the same average delta (we don't want to consider 3 singletaps similar to a triple) - return Math.Abs(AverageDelta() - other.AverageDelta()) < deltaDifferenceEpsilon && - Deltas.Count % 2 == other.Deltas.Count % 2; + return DeltaCount % 2 == other.DeltaCount % 2 && + Math.Abs(Delta - other.Delta) < deltaDifferenceEpsilon; } public override int GetHashCode() { - // we need to compare all deltas and they must be in the exact same order we added them - string joinedDeltas = string.Join(string.Empty, Deltas); - return joinedDeltas.GetHashCode(); + return HashCode.Combine(Delta, DeltaCount); } public bool Equals(Island other) { - return other.GetHashCode() == GetHashCode(); + return Math.Abs(Delta - other.Delta) < deltaDifferenceEpsilon && + DeltaCount == other.DeltaCount; } public override bool Equals(object? obj) @@ -113,7 +110,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators double prevDelta = prevObj.StrainTime; double lastDelta = lastObj.StrainTime; - double currRatio = 1.0 + 6.0 * Math.Min(0.5, Math.Pow(Math.Sin(Math.PI / (Math.Min(prevDelta, currDelta) / Math.Max(prevDelta, currDelta))), 2)); // fancy function to calculate rhythmbonuses. + // calculate how much current delta difference deserves a rhythm bonus + // this function is meant to reduce rhythm bonus for deltas that are multiples of each other (i.e 100 and 200) + double deltaDifferenceRatio = Math.Min(prevDelta, currDelta) / Math.Max(prevDelta, currDelta); + double currRatio = 1.0 + 6.0 * Math.Min(0.5, Math.Pow(Math.Sin(Math.PI / deltaDifferenceRatio), 2)); double windowPenalty = Math.Min(1, Math.Max(0, Math.Abs(prevDelta - currDelta) - deltaDifferenceEpsilon) / deltaDifferenceEpsilon); @@ -121,7 +121,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators if (firstDeltaSwitch) { - if (!(Math.Abs(prevDelta - currDelta) > deltaDifferenceEpsilon)) + if (Math.Abs(prevDelta - currDelta) < deltaDifferenceEpsilon) { // island is still progressing island.AddDelta((int)currDelta); @@ -146,7 +146,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators effectiveRatio *= 0.125; // singletaps are easier to control - if (island.Deltas.Count == 1) + if (island.DeltaCount == 1) effectiveRatio *= 0.7; if (!islandCounts.TryAdd(island, 1)) @@ -156,7 +156,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators islandCounts[island]++; // repeated island (ex: triplet -> triplet) - double power = logistic(island.AverageDelta(), 4, 0.165, 10); + double power = logistic(island.Delta, 4, 0.165, 10); effectiveRatio *= Math.Min(3.0 / islandCounts[island], Math.Pow(1.0 / islandCounts[island], power)); } From 738d4bcb804e2641a73a774cdd6d329cae91a054 Mon Sep 17 00:00:00 2001 From: StanR Date: Sun, 15 Sep 2024 01:49:52 +0500 Subject: [PATCH 2285/2493] Reduce ratio if we're starting island counting after a slider --- .../Difficulty/Evaluators/RhythmEvaluator.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs index fb1374eccf..5732d924f9 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs @@ -98,6 +98,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators OsuDifficultyHitObject prevObj = (OsuDifficultyHitObject)current.Previous(rhythmStart); OsuDifficultyHitObject lastObj = (OsuDifficultyHitObject)current.Previous(rhythmStart + 1); + // we go from the furthest object back to the current one for (int i = rhythmStart; i > 0; i--) { OsuDifficultyHitObject currObj = (OsuDifficultyHitObject)current.Previous(i - 1); @@ -171,15 +172,20 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators previousIsland = island; if (prevDelta + deltaDifferenceEpsilon < currDelta) // we're slowing down, stop counting - firstDeltaSwitch = false; // if we're speeding up, this stays true and we keep counting island size. + firstDeltaSwitch = false; // if we're speeding up, this stays true and we keep counting island size. island = new Island((int)currDelta, deltaDifferenceEpsilon); } } - else if (prevDelta > currDelta + deltaDifferenceEpsilon) // we want to be speeding up. + else if (prevDelta > currDelta + deltaDifferenceEpsilon) // we're speeding up { // Begin counting island until we change speed again. firstDeltaSwitch = true; + + // reduce ratio if we're starting after a slider + if (prevObj.BaseObject is Slider) + effectiveRatio *= 0.3; + startRatio = effectiveRatio; island = new Island((int)currDelta, deltaDifferenceEpsilon); From 863a74f9803f787872ddfec69c903d7e81be7d27 Mon Sep 17 00:00:00 2001 From: StanR Date: Sun, 15 Sep 2024 02:08:37 +0500 Subject: [PATCH 2286/2493] Balancing --- .../Difficulty/Evaluators/RhythmEvaluator.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs index 5732d924f9..c01530194b 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs @@ -63,7 +63,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators private const int history_time_max = 5 * 1000; // 5 seconds of calculatingRhythmBonus max. private const int history_objects_max = 32; - private const double rhythm_multiplier = 1.3; + private const double rhythm_multiplier = 1.4; /// /// Calculates a rhythm multiplier for the difficulty of the tap associated with historic data of the current . @@ -114,7 +114,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators // calculate how much current delta difference deserves a rhythm bonus // this function is meant to reduce rhythm bonus for deltas that are multiples of each other (i.e 100 and 200) double deltaDifferenceRatio = Math.Min(prevDelta, currDelta) / Math.Max(prevDelta, currDelta); - double currRatio = 1.0 + 6.0 * Math.Min(0.5, Math.Pow(Math.Sin(Math.PI / deltaDifferenceRatio), 2)); + double currRatio = 1.0 + 5.5 * Math.Min(0.5, Math.Pow(Math.Sin(Math.PI / deltaDifferenceRatio), 2)); double windowPenalty = Math.Min(1, Math.Max(0, Math.Abs(prevDelta - currDelta) - deltaDifferenceEpsilon) / deltaDifferenceEpsilon); From 145731bdefe9b35fc89a685bbd1eca76c537d850 Mon Sep 17 00:00:00 2001 From: StanR Date: Sun, 15 Sep 2024 02:48:41 +0500 Subject: [PATCH 2287/2493] More balancing --- .../Difficulty/Evaluators/RhythmEvaluator.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs index c01530194b..c4d78c7904 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs @@ -63,7 +63,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators private const int history_time_max = 5 * 1000; // 5 seconds of calculatingRhythmBonus max. private const int history_objects_max = 32; - private const double rhythm_multiplier = 1.4; + private const double rhythm_multiplier = 1.32; /// /// Calculates a rhythm multiplier for the difficulty of the tap associated with historic data of the current . @@ -114,7 +114,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators // calculate how much current delta difference deserves a rhythm bonus // this function is meant to reduce rhythm bonus for deltas that are multiples of each other (i.e 100 and 200) double deltaDifferenceRatio = Math.Min(prevDelta, currDelta) / Math.Max(prevDelta, currDelta); - double currRatio = 1.0 + 5.5 * Math.Min(0.5, Math.Pow(Math.Sin(Math.PI / deltaDifferenceRatio), 2)); + double currRatio = 1.0 + 6.0 * Math.Min(0.5, Math.Pow(Math.Sin(Math.PI / deltaDifferenceRatio), 2)); double windowPenalty = Math.Min(1, Math.Max(0, Math.Abs(prevDelta - currDelta) - deltaDifferenceEpsilon) / deltaDifferenceEpsilon); From bee18b03e73ade94c43c8657ae9fa7d81887a06f Mon Sep 17 00:00:00 2001 From: StanR Date: Mon, 16 Sep 2024 00:49:36 +0500 Subject: [PATCH 2288/2493] Reduce history max overall instead of using clock rate --- .../Difficulty/Evaluators/RhythmEvaluator.cs | 15 ++++++--------- .../Difficulty/OsuDifficultyCalculator.cs | 2 +- osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs | 6 ++---- 3 files changed, 9 insertions(+), 14 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs index c4d78c7904..09e9011596 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs @@ -61,14 +61,14 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators } } - private const int history_time_max = 5 * 1000; // 5 seconds of calculatingRhythmBonus max. - private const int history_objects_max = 32; + private const int history_time_max = 4 * 1000; // 5 seconds of calculatingRhythmBonus max. + private const int history_objects_max = 24; private const double rhythm_multiplier = 1.32; /// /// Calculates a rhythm multiplier for the difficulty of the tap associated with historic data of the current . /// - public static double EvaluateDifficultyOf(DifficultyHitObject current, double clockRate) + public static double EvaluateDifficultyOf(DifficultyHitObject current) { if (current.BaseObject is Spinner) return 0; @@ -81,18 +81,15 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators var previousIsland = new Island(deltaDifferenceEpsilon); Dictionary islandCounts = new Dictionary(); - int historyTimeMaxAdjusted = (int)Math.Ceiling(history_time_max / clockRate); - int historyObjectsMaxAdjusted = (int)Math.Ceiling(history_objects_max / clockRate); - double startRatio = 0; // store the ratio of the current start of an island to buff for tighter rhythms bool firstDeltaSwitch = false; - int historicalNoteCount = Math.Min(current.Index, historyObjectsMaxAdjusted); + int historicalNoteCount = Math.Min(current.Index, history_objects_max); int rhythmStart = 0; - while (rhythmStart < historicalNoteCount - 2 && current.StartTime - current.Previous(rhythmStart).StartTime < historyTimeMaxAdjusted) + while (rhythmStart < historicalNoteCount - 2 && current.StartTime - current.Previous(rhythmStart).StartTime < history_time_max) rhythmStart++; OsuDifficultyHitObject prevObj = (OsuDifficultyHitObject)current.Previous(rhythmStart); @@ -103,7 +100,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators { OsuDifficultyHitObject currObj = (OsuDifficultyHitObject)current.Previous(i - 1); - double currHistoricalDecay = (historyTimeMaxAdjusted - (current.StartTime - currObj.StartTime)) / historyTimeMaxAdjusted; // scales note 0 to 1 from history to now + double currHistoricalDecay = (history_time_max - (current.StartTime - currObj.StartTime)) / history_time_max; // scales note 0 to 1 from history to now currHistoricalDecay = Math.Min((double)(historicalNoteCount - i) / historicalNoteCount, currHistoricalDecay); // either we're limited by time or limited by object count. diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs index 835f4ee196..e93475ecff 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs @@ -134,7 +134,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty { new Aim(mods, true), new Aim(mods, false), - new Speed(mods, clockRate) + new Speed(mods) }; if (mods.Any(h => h is OsuModFlashlight)) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs index d0586662ff..f7f081b7ea 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs @@ -17,7 +17,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills public class Speed : OsuStrainSkill { private double skillMultiplier => 1.375; - private readonly double clockRate; private double strainDecayBase => 0.3; private double currentStrain; @@ -28,10 +27,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills private readonly List objectStrains = new List(); - public Speed(Mod[] mods, double clockRate) + public Speed(Mod[] mods) : base(mods) { - this.clockRate = clockRate; } private double strainDecay(double ms) => Math.Pow(strainDecayBase, ms / 1000); @@ -43,7 +41,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills currentStrain *= strainDecay(((OsuDifficultyHitObject)current).StrainTime); currentStrain += SpeedEvaluator.EvaluateDifficultyOf(current) * skillMultiplier; - currentRhythm = RhythmEvaluator.EvaluateDifficultyOf(current, clockRate); + currentRhythm = RhythmEvaluator.EvaluateDifficultyOf(current); double totalStrain = currentStrain * currentRhythm; From c9ce7d29e66f8b25d4d7fad371cb78f6b88121bc Mon Sep 17 00:00:00 2001 From: StanR Date: Mon, 16 Sep 2024 01:04:46 +0500 Subject: [PATCH 2289/2493] Adjust multipliers --- .../Difficulty/Evaluators/RhythmEvaluator.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs index 09e9011596..9eae3c7a10 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs @@ -63,7 +63,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators private const int history_time_max = 4 * 1000; // 5 seconds of calculatingRhythmBonus max. private const int history_objects_max = 24; - private const double rhythm_multiplier = 1.32; + private const double rhythm_overall_multiplier = 1.2; + private const double rhythm_ratio_multiplier = 10.0; /// /// Calculates a rhythm multiplier for the difficulty of the tap associated with historic data of the current . @@ -111,7 +112,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators // calculate how much current delta difference deserves a rhythm bonus // this function is meant to reduce rhythm bonus for deltas that are multiples of each other (i.e 100 and 200) double deltaDifferenceRatio = Math.Min(prevDelta, currDelta) / Math.Max(prevDelta, currDelta); - double currRatio = 1.0 + 6.0 * Math.Min(0.5, Math.Pow(Math.Sin(Math.PI / deltaDifferenceRatio), 2)); + double currRatio = 1.0 + rhythm_ratio_multiplier * Math.Min(0.5, Math.Pow(Math.Sin(Math.PI / deltaDifferenceRatio), 2)); double windowPenalty = Math.Min(1, Math.Max(0, Math.Abs(prevDelta - currDelta) - deltaDifferenceEpsilon) / deltaDifferenceEpsilon); @@ -192,7 +193,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators prevObj = currObj; } - return Math.Sqrt(4 + rhythmComplexitySum * rhythm_multiplier) / 2.0; //produces multiplier that can be applied to strain. range [1, infinity) (not really though) + return Math.Sqrt(4 + rhythmComplexitySum * rhythm_overall_multiplier) / 2.0; // produces multiplier that can be applied to strain. range [1, infinity) (not really though) } private static double logistic(double x, double maxValue, double multiplier, double offset) => (maxValue / (1 + Math.Pow(Math.E, offset - (multiplier * x)))); From d34e8ea69e78f80940b2cade87b9c0dba6f066b0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 16 Sep 2024 16:15:09 +0900 Subject: [PATCH 2290/2493] 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 d5bdfd91b5..c7ce707562 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -10,7 +10,7 @@ true - + diff --git a/osu.iOS.props b/osu.iOS.props index da1cec395f..bb20125282 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -17,6 +17,6 @@ -all - + From 785a7255074bb374ccf92b3d6ec68d5c7a3b6117 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 17 Sep 2024 15:12:02 +0900 Subject: [PATCH 2291/2493] Fix osu!catch fruit rotation being applied too late --- .../Objects/Drawables/DrawableFruit.cs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs index 52c53523e6..7bac6b588e 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; -using osu.Framework.Graphics; using osu.Game.Rulesets.Catch.Skinning.Default; using osu.Game.Skinning; @@ -28,11 +27,10 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables _ => new FruitPiece()); } - protected override void UpdateInitialTransforms() + protected override void OnApply() { - base.UpdateInitialTransforms(); - - ScalingContainer.RotateTo((RandomSingle(1) - 0.5f) * 40); + base.OnApply(); + ScalingContainer.Rotation = (RandomSingle(1) - 0.5f) * 40; } } } From a99dbfa768e56bcad9e6e8ebff295d42f482a4f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 17 Sep 2024 08:21:58 +0200 Subject: [PATCH 2292/2493] Add failing test step demonstrating incorrect end drag marker position --- .../Editor/TestSceneSliderSelectionBlueprint.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSelectionBlueprint.cs index c2589f11ef..fa8db51e09 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSelectionBlueprint.cs @@ -218,6 +218,9 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor AddAssert("tail positioned correctly", () => Precision.AlmostEquals(blueprint.TailOverlay.CirclePiece.ScreenSpaceDrawQuad.Centre, drawableObject.TailCircle.ScreenSpaceDrawQuad.Centre)); + + AddAssert("end drag marker positioned correctly", + () => Precision.AlmostEquals(blueprint.TailOverlay.EndDragMarker!.ToScreenSpace(blueprint.TailOverlay.EndDragMarker.OriginPosition), drawableObject.TailCircle.ScreenSpaceDrawQuad.Centre, 2)); } private void moveMouseToControlPoint(int index) From 3e63fe399f75f31d35803564e40559f59d4a213a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 17 Sep 2024 08:22:37 +0200 Subject: [PATCH 2293/2493] Enable NRT in test scene --- .../Editor/TestSceneSliderSelectionBlueprint.cs | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSelectionBlueprint.cs index fa8db51e09..f0f969b15b 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSelectionBlueprint.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 NUnit.Framework; using osu.Framework.Utils; using osu.Game.Beatmaps; @@ -22,9 +20,9 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor { public partial class TestSceneSliderSelectionBlueprint : SelectionBlueprintTestScene { - private Slider slider; - private DrawableSlider drawableObject; - private TestSliderBlueprint blueprint; + private Slider slider = null!; + private DrawableSlider drawableObject = null!; + private TestSliderBlueprint blueprint = null!; [SetUp] public void Setup() => Schedule(() => @@ -233,14 +231,14 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor } private void checkControlPointSelected(int index, bool selected) - => AddAssert($"control point {index} {(selected ? "selected" : "not selected")}", () => blueprint.ControlPointVisualiser.Pieces[index].IsSelected.Value == selected); + => AddAssert($"control point {index} {(selected ? "selected" : "not selected")}", () => blueprint.ControlPointVisualiser!.Pieces[index].IsSelected.Value == selected); private partial class TestSliderBlueprint : SliderSelectionBlueprint { public new SliderBodyPiece BodyPiece => base.BodyPiece; public new TestSliderCircleOverlay HeadOverlay => (TestSliderCircleOverlay)base.HeadOverlay; public new TestSliderCircleOverlay TailOverlay => (TestSliderCircleOverlay)base.TailOverlay; - public new PathControlPointVisualiser ControlPointVisualiser => base.ControlPointVisualiser; + public new PathControlPointVisualiser? ControlPointVisualiser => base.ControlPointVisualiser; public TestSliderBlueprint(Slider slider) : base(slider) From 67a7f608f155aa7c1e5abcd4f17c31ef23028f9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 17 Sep 2024 08:23:46 +0200 Subject: [PATCH 2294/2493] Fix slider end drag marker being in incorrect position for stacked sliders Closes https://github.com/ppy/osu/issues/29884. --- .../Edit/Blueprints/Sliders/SliderCircleOverlay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderCircleOverlay.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderCircleOverlay.cs index 247ceb4078..9c2998466a 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderCircleOverlay.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderCircleOverlay.cs @@ -70,7 +70,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders if (endDragMarkerContainer != null) { - endDragMarkerContainer.Position = circle.Position; + endDragMarkerContainer.Position = circle.Position + slider.StackOffset; endDragMarkerContainer.Scale = CirclePiece.Scale * 1.2f; var diff = slider.Path.PositionAt(1) - slider.Path.PositionAt(0.99f); endDragMarkerContainer.Rotation = float.RadiansToDegrees(MathF.Atan2(diff.Y, diff.X)); From 2ccdad41e793d9bb982eabeedf6f5be4a08367b3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 17 Sep 2024 15:27:16 +0900 Subject: [PATCH 2295/2493] Also fix banana showers --- .../Objects/Drawables/DrawableBanana.cs | 32 ++++++++++++++----- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBanana.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBanana.cs index 26e304cf3f..9a4bc45bda 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBanana.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBanana.cs @@ -1,10 +1,12 @@ // 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.Graphics; +using osu.Framework.Utils; using osu.Game.Rulesets.Catch.Skinning.Default; using osu.Game.Skinning; +using osuTK; namespace osu.Game.Rulesets.Catch.Objects.Drawables { @@ -36,23 +38,37 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables StartTimeBindable.BindValueChanged(_ => UpdateComboColour()); } - protected override void UpdateInitialTransforms() + private float startScale; + private float endScale; + + private float startAngle; + private float endAngle; + + protected override void OnApply() { - base.UpdateInitialTransforms(); + base.OnApply(); const float end_scale = 0.6f; const float random_scale_range = 1.6f; - ScalingContainer.ScaleTo(HitObject.Scale * (end_scale + random_scale_range * RandomSingle(3))) - .Then().ScaleTo(HitObject.Scale * end_scale, HitObject.TimePreempt); + startScale = end_scale + random_scale_range * RandomSingle(3); + endScale = end_scale; - ScalingContainer.RotateTo(getRandomAngle(1)) - .Then() - .RotateTo(getRandomAngle(2), HitObject.TimePreempt); + startAngle = getRandomAngle(1); + endAngle = getRandomAngle(2); float getRandomAngle(int series) => 180 * (RandomSingle(series) * 2 - 1); } + protected override void Update() + { + base.Update(); + + double preemptProgress = Math.Min(1, (Time.Current - (HitObject.StartTime - InitialLifetimeOffset)) / HitObject.TimePreempt); + ScalingContainer.Scale = new Vector2(HitObject.Scale * (float)Interpolation.Lerp(startScale, endScale, preemptProgress)); + ScalingContainer.Rotation = (float)Interpolation.Lerp(startAngle, endAngle, preemptProgress); + } + public override void PlaySamples() { base.PlaySamples(); From 3f4422429da16292cfc8a8b48797be1197507393 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 17 Sep 2024 15:37:54 +0900 Subject: [PATCH 2296/2493] *Also* fix droplets --- .../Objects/Drawables/DrawableDroplet.cs | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableDroplet.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableDroplet.cs index 8f32cdcc31..c92fd7cbba 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableDroplet.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableDroplet.cs @@ -2,7 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; -using osu.Framework.Graphics; +using osu.Framework.Utils; using osu.Game.Rulesets.Catch.Skinning.Default; using osu.Game.Skinning; @@ -28,15 +28,22 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables _ => new DropletPiece()); } - protected override void UpdateInitialTransforms() + private float startRotation; + + protected override void OnApply() { - base.UpdateInitialTransforms(); + base.OnApply(); // roughly matches osu-stable - float startRotation = RandomSingle(1) * 20; - double duration = HitObject.TimePreempt + 2000; + startRotation = RandomSingle(1) * 20; + } - ScalingContainer.RotateTo(startRotation).RotateTo(startRotation + 720, duration); + protected override void Update() + { + base.Update(); + + double preemptProgress = (Time.Current - (HitObject.StartTime - InitialLifetimeOffset)) / (HitObject.TimePreempt + 2000); + ScalingContainer.Rotation = (float)Interpolation.Lerp(startRotation, startRotation + 720, preemptProgress); } } } From c1c0d49bfeecba12db92f48d8f3c586ab820c3e8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 17 Sep 2024 16:15:45 +0900 Subject: [PATCH 2297/2493] Add comments and fix bananas stopping still if not caught --- .../Objects/Drawables/DrawableBanana.cs | 7 ++++++- .../Objects/Drawables/DrawableDroplet.cs | 2 ++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBanana.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBanana.cs index 9a4bc45bda..f6ecdce616 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBanana.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBanana.cs @@ -64,7 +64,12 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables { base.Update(); - double preemptProgress = Math.Min(1, (Time.Current - (HitObject.StartTime - InitialLifetimeOffset)) / HitObject.TimePreempt); + double preemptProgress = (Time.Current - (HitObject.StartTime - InitialLifetimeOffset)) / HitObject.TimePreempt; + + // Clamp scale and rotation at the point of bananas being caught, else let them freely extrapolate. + if (Result.IsHit) + preemptProgress = Math.Min(1, preemptProgress); + ScalingContainer.Scale = new Vector2(HitObject.Scale * (float)Interpolation.Lerp(startScale, endScale, preemptProgress)); ScalingContainer.Rotation = (float)Interpolation.Lerp(startAngle, endAngle, preemptProgress); } diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableDroplet.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableDroplet.cs index c92fd7cbba..73442a502b 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableDroplet.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableDroplet.cs @@ -42,6 +42,8 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables { base.Update(); + // No clamping for droplets. They should be considered indefinitely spinning regardless of time. + // They also never end up on the plate, so they shouldn't stop spinning when caught. double preemptProgress = (Time.Current - (HitObject.StartTime - InitialLifetimeOffset)) / (HitObject.TimePreempt + 2000); ScalingContainer.Rotation = (float)Interpolation.Lerp(startRotation, startRotation + 720, preemptProgress); } From f8fff4074ddecafbd79076662a11df71b7cc7610 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 17 Sep 2024 16:18:29 +0900 Subject: [PATCH 2298/2493] Fix rotation not being updated correctly on start time change --- osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBanana.cs | 5 +++-- .../Objects/Drawables/DrawableDroplet.cs | 6 +++--- osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs | 6 ++++-- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBanana.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBanana.cs index f6ecdce616..10e483b577 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBanana.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBanana.cs @@ -44,10 +44,11 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables private float startAngle; private float endAngle; - protected override void OnApply() + protected override void UpdateInitialTransforms() { - base.OnApply(); + base.UpdateInitialTransforms(); + // Important to have this in UpdateInitialTransforms() to it is re-triggered by RefreshStateTransforms(). const float end_scale = 0.6f; const float random_scale_range = 1.6f; diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableDroplet.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableDroplet.cs index 73442a502b..fadd630116 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableDroplet.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableDroplet.cs @@ -30,11 +30,11 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables private float startRotation; - protected override void OnApply() + protected override void UpdateInitialTransforms() { - base.OnApply(); + base.UpdateInitialTransforms(); - // roughly matches osu-stable + // Important to have this in UpdateInitialTransforms() to it is re-triggered by RefreshStateTransforms(). startRotation = RandomSingle(1) * 20; } diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs index 7bac6b588e..877fae9d67 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs @@ -27,9 +27,11 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables _ => new FruitPiece()); } - protected override void OnApply() + protected override void UpdateInitialTransforms() { - base.OnApply(); + base.UpdateInitialTransforms(); + + // Important to have this in UpdateInitialTransforms() to it is re-triggered by RefreshStateTransforms(). ScalingContainer.Rotation = (RandomSingle(1) - 0.5f) * 40; } } From 1b17231da47daba7743d868e7ba8f6bb281a3b72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 17 Sep 2024 11:35:16 +0200 Subject: [PATCH 2299/2493] Implement "form" slider bar control --- .../UserInterface/TestSceneFormControls.cs | 14 + .../Graphics/UserInterface/OsuSliderBar.cs | 6 +- .../Graphics/UserInterfaceV2/FormSliderBar.cs | 330 ++++++++++++++++++ .../Graphics/UserInterfaceV2/FormTextBox.cs | 2 +- 4 files changed, 348 insertions(+), 4 deletions(-) create mode 100644 osu.Game/Graphics/UserInterfaceV2/FormSliderBar.cs diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneFormControls.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneFormControls.cs index eb8a8b3fe9..6dd7275abf 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneFormControls.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneFormControls.cs @@ -1,12 +1,14 @@ // 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 osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; using osu.Game.Graphics.Cursor; using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Localisation; +using osu.Game.Resources.Localisation.Web; using osuTK; namespace osu.Game.Tests.Visual.UserInterface @@ -68,6 +70,18 @@ namespace osu.Game.Tests.Visual.UserInterface HintText = EditorSetupStrings.LetterboxDuringBreaksDescription, Current = { Disabled = true }, }, + new FormSliderBar + { + Caption = BeatmapsetsStrings.ShowStatsDrain, + HintText = EditorSetupStrings.DrainRateDescription, + Current = new BindableFloat + { + MinValue = 0, + MaxValue = 10, + Value = 5, + Precision = 0.1f, + } + }, }, }, } diff --git a/osu.Game/Graphics/UserInterface/OsuSliderBar.cs b/osu.Game/Graphics/UserInterface/OsuSliderBar.cs index 9cb6356cab..334fe343ae 100644 --- a/osu.Game/Graphics/UserInterface/OsuSliderBar.cs +++ b/osu.Game/Graphics/UserInterface/OsuSliderBar.cs @@ -46,7 +46,7 @@ namespace osu.Game.Graphics.UserInterface protected override void LoadComplete() { base.LoadComplete(); - CurrentNumber.BindValueChanged(current => TooltipText = getTooltipText(current.NewValue), true); + CurrentNumber.BindValueChanged(current => TooltipText = GetDisplayableValue(current.NewValue), true); } protected override void OnUserChange(T value) @@ -55,7 +55,7 @@ namespace osu.Game.Graphics.UserInterface playSample(value); - TooltipText = getTooltipText(value); + TooltipText = GetDisplayableValue(value); } private void playSample(T value) @@ -83,7 +83,7 @@ namespace osu.Game.Graphics.UserInterface channel.Play(); } - private LocalisableString getTooltipText(T value) + public LocalisableString GetDisplayableValue(T value) { if (CurrentNumber.IsInteger) return int.CreateTruncating(value).ToString("N0"); diff --git a/osu.Game/Graphics/UserInterfaceV2/FormSliderBar.cs b/osu.Game/Graphics/UserInterfaceV2/FormSliderBar.cs new file mode 100644 index 0000000000..91ce9da2d2 --- /dev/null +++ b/osu.Game/Graphics/UserInterfaceV2/FormSliderBar.cs @@ -0,0 +1,330 @@ +// 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.Globalization; +using System.Numerics; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +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.Graphics.UserInterface; +using osu.Framework.Input; +using osu.Framework.Input.Events; +using osu.Framework.Localisation; +using osu.Game.Graphics.UserInterface; +using osu.Game.Overlays; + +namespace osu.Game.Graphics.UserInterfaceV2 +{ + public partial class FormSliderBar : CompositeDrawable, IHasCurrentValue + where T : struct, INumber, IMinMaxValue + { + public Bindable Current + { + get => current.Current; + set => current.Current = value; + } + + private bool instantaneous; + + /// + /// Whether changes to the slider should instantaneously transfer to the text box (and vice versa). + /// If , the transfer will happen on text box commit (explicit, or implicit via focus loss), or on slider drag end. + /// + public bool Instantaneous + { + get => instantaneous; + set + { + instantaneous = value; + slider.TransferValueOnCommit = !instantaneous; + } + } + + private readonly BindableNumberWithCurrent current = new BindableNumberWithCurrent(); + + public LocalisableString Caption { get; init; } + public LocalisableString HintText { get; init; } + + private Box background = null!; + private Box flashLayer = null!; + private FormTextBox.InnerTextBox textBox = null!; + private Slider slider = null!; + private FormFieldCaption caption = null!; + private IFocusManager focusManager = null!; + + [Resolved] + private OverlayColourProvider colourProvider { get; set; } = null!; + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + RelativeSizeAxes = Axes.X; + Height = 50; + + Masking = true; + CornerRadius = 5; + + InternalChildren = new Drawable[] + { + background = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background5, + }, + flashLayer = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Colour4.Transparent, + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding(9), + Children = new Drawable[] + { + caption = new FormFieldCaption + { + Anchor = Anchor.TopLeft, + Origin = Anchor.TopLeft, + Caption = Caption, + TooltipText = HintText, + }, + textBox = new FormNumberBox.InnerNumberBox + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + RelativeSizeAxes = Axes.X, + Width = 0.5f, + CommitOnFocusLost = true, + SelectAllOnFocus = true, + AllowDecimals = true, + OnInputError = () => + { + flashLayer.Colour = ColourInfo.GradientVertical(colours.Red3.Opacity(0), colours.Red3); + flashLayer.FadeOutFromOne(200, Easing.OutQuint); + } + }, + slider = new Slider + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + RelativeSizeAxes = Axes.X, + Width = 0.5f, + Current = Current, + } + }, + }, + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + focusManager = GetContainingFocusManager()!; + + textBox.Focused.BindValueChanged(_ => updateState()); + textBox.OnCommit += textCommitted; + textBox.Current.BindValueChanged(textChanged); + + current.BindValueChanged(_ => + { + updateState(); + updateTextBoxFromSlider(); + }, true); + } + + private bool updatingFromTextBox; + + private void textChanged(ValueChangedEvent change) + { + if (!instantaneous) return; + + tryUpdateSliderFromTextBox(); + } + + private void textCommitted(TextBox t, bool isNew) + { + tryUpdateSliderFromTextBox(); + + // If the attempted update above failed, restore text box to match the slider. + Current.TriggerChange(); + + flashLayer.Colour = ColourInfo.GradientVertical(colourProvider.Dark2.Opacity(0), colourProvider.Dark2); + flashLayer.FadeOutFromOne(800, Easing.OutQuint); + } + + private void tryUpdateSliderFromTextBox() + { + updatingFromTextBox = true; + + try + { + switch (Current) + { + case Bindable bindableInt: + bindableInt.Value = int.Parse(textBox.Current.Value); + break; + + case Bindable bindableDouble: + bindableDouble.Value = double.Parse(textBox.Current.Value); + break; + + default: + Current.Parse(textBox.Current.Value, CultureInfo.CurrentCulture); + break; + } + } + catch + { + // ignore parsing failures. + // sane state will eventually be restored by a commit (either explicit, or implicit via focus loss). + } + + updatingFromTextBox = false; + } + + protected override bool OnHover(HoverEvent e) + { + updateState(); + return true; + } + + protected override void OnHoverLost(HoverLostEvent e) + { + base.OnHoverLost(e); + updateState(); + } + + protected override bool OnClick(ClickEvent e) + { + focusManager.ChangeFocus(textBox); + return true; + } + + private void updateState() + { + textBox.Alpha = 1; + + background.Colour = Current.Disabled ? colourProvider.Background4 : colourProvider.Background5; + caption.Colour = Current.Disabled ? colourProvider.Foreground1 : colourProvider.Content2; + textBox.Colour = Current.Disabled ? colourProvider.Foreground1 : colourProvider.Content1; + + BorderThickness = IsHovered || textBox.Focused.Value ? 2 : 0; + BorderColour = textBox.Focused.Value ? colourProvider.Highlight1 : colourProvider.Light4; + + if (textBox.Focused.Value) + background.Colour = ColourInfo.GradientVertical(colourProvider.Background5, colourProvider.Dark3); + else if (IsHovered) + background.Colour = ColourInfo.GradientVertical(colourProvider.Background5, colourProvider.Dark4); + else + background.Colour = colourProvider.Background5; + } + + private void updateTextBoxFromSlider() + { + if (updatingFromTextBox) return; + + textBox.Text = slider.GetDisplayableValue(Current.Value).ToString(); + } + + private partial class Slider : OsuSliderBar + { + private Box leftBox = null!; + private Box rightBox = null!; + private Circle nub = null!; + private const float nub_width = 10; + + [Resolved] + private OverlayColourProvider colourProvider { get; set; } = null!; + + [BackgroundDependencyLoader] + private void load() + { + Height = 40; + RelativeSizeAxes = Axes.X; + RangePadding = nub_width / 2; + Children = new Drawable[] + { + new Container + { + RelativeSizeAxes = Axes.Both, + Masking = true, + CornerRadius = 5, + Children = new Drawable[] + { + leftBox = new Box + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + }, + rightBox = new Box + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + }, + }, + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Horizontal = RangePadding, }, + Child = nub = new Circle + { + Width = nub_width, + RelativeSizeAxes = Axes.Y, + RelativePositionAxes = Axes.X, + Origin = Anchor.TopCentre, + } + }, + new HoverClickSounds() + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + updateState(); + } + + protected override void UpdateAfterChildren() + { + base.UpdateAfterChildren(); + leftBox.Width = Math.Clamp(RangePadding + nub.DrawPosition.X, 0, Math.Max(0, DrawWidth)) / DrawWidth; + rightBox.Width = Math.Clamp(DrawWidth - nub.DrawPosition.X - RangePadding, 0, Math.Max(0, DrawWidth)) / DrawWidth; + } + + protected override bool OnHover(HoverEvent e) + { + updateState(); + return base.OnHover(e); + } + + protected override void OnHoverLost(HoverLostEvent e) + { + updateState(); + base.OnHoverLost(e); + } + + private void updateState() + { + rightBox.Colour = colourProvider.Background6; + leftBox.Colour = IsHovered ? colourProvider.Highlight1.Opacity(0.5f) : colourProvider.Dark2; + nub.Colour = IsHovered ? colourProvider.Highlight1 : colourProvider.Light4; + } + + protected override void UpdateValue(float value) + { + nub.MoveToX(value, 250, Easing.OutQuint); + } + } + } +} diff --git a/osu.Game/Graphics/UserInterfaceV2/FormTextBox.cs b/osu.Game/Graphics/UserInterfaceV2/FormTextBox.cs index 044576c635..741bff6db6 100644 --- a/osu.Game/Graphics/UserInterfaceV2/FormTextBox.cs +++ b/osu.Game/Graphics/UserInterfaceV2/FormTextBox.cs @@ -122,7 +122,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 if (!current.Disabled && !ReadOnly) { - flashLayer.Colour = ColourInfo.GradientVertical(colourProvider.Dark1.Opacity(0), colourProvider.Dark2); + flashLayer.Colour = ColourInfo.GradientVertical(colourProvider.Dark2.Opacity(0), colourProvider.Dark2); flashLayer.FadeOutFromOne(800, Easing.OutQuint); } }; From e0f92bab6a7d981642eca24c3ee2e8d73b19446c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 17 Sep 2024 13:07:52 +0200 Subject: [PATCH 2300/2493] Add test case covering failure --- .../Editor/TestSceneManiaSelectionHandler.cs | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaSelectionHandler.cs b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaSelectionHandler.cs index b48f579ec0..4285ef2029 100644 --- a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaSelectionHandler.cs +++ b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaSelectionHandler.cs @@ -6,6 +6,7 @@ using NUnit.Framework; using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Rulesets.Mania.Objects; +using osu.Game.Rulesets.Mania.UI; using osu.Game.Screens.Edit.Compose.Components; using osu.Game.Tests.Beatmaps; using osu.Game.Tests.Visual; @@ -92,5 +93,30 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor AddAssert("second object flipped", () => second.StartTime, () => Is.EqualTo(250)); AddAssert("third object flipped", () => third.StartTime, () => Is.EqualTo(1250)); } + + [Test] + public void TestOffScreenObjectsRemainSelectedOnColumnChange() + { + AddStep("create objects", () => + { + for (int i = 0; i < 20; ++i) + EditorBeatmap.Add(new Note { StartTime = 1000 * i, Column = 0 }); + }); + + AddStep("select everything", () => EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects)); + AddStep("start drag", () => + { + InputManager.MoveMouseTo(this.ChildrenOfType().First()); + InputManager.PressButton(MouseButton.Left); + }); + AddStep("end drag", () => + { + InputManager.MoveMouseTo(this.ChildrenOfType().Last()); + InputManager.ReleaseButton(MouseButton.Left); + }); + + AddAssert("all objects in last column", () => EditorBeatmap.HitObjects.All(ho => ((ManiaHitObject)ho).Column == 3)); + AddAssert("all objects remain selected", () => EditorBeatmap.SelectedHitObjects.SequenceEqual(EditorBeatmap.HitObjects)); + } } } From 20b1d762699ac7cb0bebf705ed7708f4ebc20ef2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 17 Sep 2024 13:07:57 +0200 Subject: [PATCH 2301/2493] Ensure selection is preserved when moving selection between columns Closes https://github.com/ppy/osu/issues/29793. I believe that the sequence of events that makes this happens is as follows: - User selects a range of objects. Some of those objects are off-screen, and thus would be presumed to be not alive - except the blueprint container forces them to remain alive, because they're part of the selection. - User moves the selection to another column, which is implemented by temporarily removing the objects from the playfield, changing their column, and re-adding them. This sort of pattern is supposed to kick off the `HitObjectUsageTransferred` flow in `HitObjectUsageEventBuffer` - and it does... for objects that are *currently visible on screen* and thus would be alive regardless of `SetKeepAlive()`. However, this does not hold for objects that are off-screen - nothing ensures they are kept alive again after re-adding, and thus they inadvertently become dead. - Thus, this doesn't kick off the `BlueprintContainer` flows associated with transferring objects to another column, and instead fires the removal flows, which ensure that the off-screen objects that were being moved are instead deselected. I tried a few other options but found no better resolution than this - calling `SetKeepAlive()` directly would require making it public, which seems like a bad idea. There's really no good way to generically handle this either, because it is the ruleset that decides that its way of implementing this operation will be a removal and re-add of objects, so... --- osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs b/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs index 9ae2112b30..7e0991a4d4 100644 --- a/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs +++ b/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs @@ -104,8 +104,10 @@ namespace osu.Game.Rulesets.Mania.Edit int minColumn = int.MaxValue; int maxColumn = int.MinValue; + var selectedObjects = EditorBeatmap.SelectedHitObjects.OfType().ToArray(); + // find min/max in an initial pass before actually performing the movement. - foreach (var obj in EditorBeatmap.SelectedHitObjects.OfType()) + foreach (var obj in selectedObjects) { if (obj.Column < minColumn) minColumn = obj.Column; @@ -121,6 +123,13 @@ namespace osu.Game.Rulesets.Mania.Edit ((ManiaHitObject)h).Column += columnDelta; maniaPlayfield.Add(h); }); + + // `HitObjectUsageEventBuffer`'s usage transferal flows and the playfield's `SetKeepAlive()` functionality do not combine well with this operation's usage pattern, + // leading to selections being sometimes partially dropped if some of the objects being moved are off screen + // (check blame for detailed explanation). + // thus, ensure that selection is preserved manually. + EditorBeatmap.SelectedHitObjects.Clear(); + EditorBeatmap.SelectedHitObjects.AddRange(selectedObjects); } } } From 76c5e743d7ce834701c07fc15df3a19339a3db32 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 18 Sep 2024 13:49:27 +0900 Subject: [PATCH 2302/2493] Remove opacity from old style dropdown menus These aren't used in many places, but we've since moved away from opacity in UI elements like this, so let's just nuke it here for legibility. Addresses https://github.com/ppy/osu/discussions/29906. --- osu.Game/Graphics/UserInterface/OsuDropdown.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Graphics/UserInterface/OsuDropdown.cs b/osu.Game/Graphics/UserInterface/OsuDropdown.cs index 71ae149cf6..dc42216c55 100644 --- a/osu.Game/Graphics/UserInterface/OsuDropdown.cs +++ b/osu.Game/Graphics/UserInterface/OsuDropdown.cs @@ -75,7 +75,7 @@ namespace osu.Game.Graphics.UserInterface [BackgroundDependencyLoader(true)] private void load(OverlayColourProvider? colourProvider, OsuColour colours, AudioManager audio) { - BackgroundColour = colourProvider?.Background5 ?? Color4.Black.Opacity(0.5f); + BackgroundColour = colourProvider?.Background5 ?? Color4.Black; HoverColour = colourProvider?.Light4 ?? colours.PinkDarker; SelectionColour = colourProvider?.Background3 ?? colours.PinkDarker.Opacity(0.5f); @@ -397,7 +397,7 @@ namespace osu.Game.Graphics.UserInterface { bool hovered = Enabled.Value && IsHovered; var hoveredColour = colourProvider?.Light4 ?? colours.PinkDarker; - var unhoveredColour = colourProvider?.Background5 ?? Color4.Black.Opacity(0.5f); + var unhoveredColour = colourProvider?.Background5 ?? Color4.Black; Colour = Color4.White; Alpha = Enabled.Value ? 1 : 0.3f; From fd6b3b6b36bd7d88e4aa7e16393f2cbf6f8343d8 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Tue, 17 Sep 2024 22:25:18 -0700 Subject: [PATCH 2303/2493] Fix searching by clicking title/artist in beatmap overlay not following original language setting --- osu.Game/Online/Chat/MessageFormatter.cs | 2 ++ osu.Game/OsuGame.cs | 15 +++++++++++---- .../BeatmapSet/BeatmapSetHeaderContent.cs | 4 ++-- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/osu.Game/Online/Chat/MessageFormatter.cs b/osu.Game/Online/Chat/MessageFormatter.cs index 77454c4775..0f444ccde9 100644 --- a/osu.Game/Online/Chat/MessageFormatter.cs +++ b/osu.Game/Online/Chat/MessageFormatter.cs @@ -340,6 +340,8 @@ namespace osu.Game.Online.Chat Spectate, OpenUserProfile, SearchBeatmapSet, + SearchBeatmapTitle, + SearchBeatmapArtist, OpenWiki, Custom, OpenChangelog, diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 0ef6a94679..ffb145d7de 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -445,10 +445,17 @@ namespace osu.Game break; case LinkAction.SearchBeatmapSet: - if (link.Argument is RomanisableString romanisable) - SearchBeatmapSet(romanisable.GetPreferred(Localisation.CurrentParameters.Value.PreferOriginalScript)); - else - SearchBeatmapSet(argString); + SearchBeatmapSet(argString); + break; + + case LinkAction.SearchBeatmapTitle: + string title = ((RomanisableString)link.Argument).GetPreferred(Localisation.CurrentParameters.Value.PreferOriginalScript); + SearchBeatmapSet($@"title=""""{title}"""""); + break; + + case LinkAction.SearchBeatmapArtist: + string artist = ((RomanisableString)link.Argument).GetPreferred(Localisation.CurrentParameters.Value.PreferOriginalScript); + SearchBeatmapSet($@"artist=""""{artist}"""""); break; case LinkAction.FilterBeatmapSetGenre: diff --git a/osu.Game/Overlays/BeatmapSet/BeatmapSetHeaderContent.cs b/osu.Game/Overlays/BeatmapSet/BeatmapSetHeaderContent.cs index f9e0c6c380..6ea16a9997 100644 --- a/osu.Game/Overlays/BeatmapSet/BeatmapSetHeaderContent.cs +++ b/osu.Game/Overlays/BeatmapSet/BeatmapSetHeaderContent.cs @@ -242,7 +242,7 @@ namespace osu.Game.Overlays.BeatmapSet title.Clear(); artist.Clear(); - title.AddLink(titleText, LinkAction.SearchBeatmapSet, $@"title=""""{titleText}"""""); + title.AddLink(titleText, LinkAction.SearchBeatmapTitle, titleText); title.AddArbitraryDrawable(Empty().With(d => d.Width = 5)); title.AddArbitraryDrawable(externalLink = new ExternalLinkButton()); @@ -259,7 +259,7 @@ namespace osu.Game.Overlays.BeatmapSet title.AddArbitraryDrawable(new SpotlightBeatmapBadge()); } - artist.AddLink(artistText, LinkAction.SearchBeatmapSet, $@"artist=""""{artistText}"""""); + artist.AddLink(artistText, LinkAction.SearchBeatmapArtist, artistText); if (setInfo.NewValue.TrackId != null) { From 2d993645af3c2cd7f0ee8d7f2dcdb065a0dfb0c3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 18 Sep 2024 15:03:55 +0900 Subject: [PATCH 2304/2493] Add test coverage of judgements not being synced when resuming a replay --- .../Visual/Gameplay/TestSceneSpectator.cs | 11 +++++--- osu.Game/Rulesets/Scoring/ScoreProcessor.cs | 2 ++ .../Visual/Spectator/TestSpectatorClient.cs | 26 +++++++++++++++++-- 3 files changed, 33 insertions(+), 6 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs index 0de2b6a980..d8817e563c 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs @@ -19,6 +19,7 @@ using osu.Game.Rulesets.UI; using osu.Game.Scoring; using osu.Game.Screens; using osu.Game.Screens.Play; +using osu.Game.Screens.Play.HUD.JudgementCounter; using osu.Game.Tests.Beatmaps.IO; using osu.Game.Tests.Gameplay; using osu.Game.Tests.Visual.Multiplayer; @@ -167,14 +168,16 @@ namespace osu.Game.Tests.Visual.Gameplay public void TestSpectatingDuringGameplay() { start(); - sendFrames(300); + sendFrames(300, initialResultCount: 100); loadSpectatingScreen(); waitForPlayerCurrent(); - sendFrames(300); + sendFrames(300, initialResultCount: 100); AddUntilStep("playing from correct point in time", () => player.ChildrenOfType().First().FrameStableClock.CurrentTime, () => Is.GreaterThan(30000)); + AddAssert("check judgement counts are correct", () => player.ChildrenOfType().Single().Counters.Sum(c => c.ResultCount.Value), + () => Is.GreaterThanOrEqualTo(100)); } [Test] @@ -405,9 +408,9 @@ namespace osu.Game.Tests.Visual.Gameplay private void checkPaused(bool state) => AddUntilStep($"game is {(state ? "paused" : "playing")}", () => player.ChildrenOfType().First().IsPaused.Value == state); - private void sendFrames(int count = 10, double startTime = 0) + private void sendFrames(int count = 10, double startTime = 0, int initialResultCount = 0) { - AddStep("send frames", () => spectatorClient.SendFramesFromUser(streamingUser.Id, count, startTime)); + AddStep("send frames", () => spectatorClient.SendFramesFromUser(streamingUser.Id, count, startTime, initialResultCount)); } private void loadSpectatingScreen() diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index 44ddb8c187..9752918dfb 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -181,6 +181,8 @@ namespace osu.Game.Rulesets.Scoring } } + public IReadOnlyDictionary Statistics => ScoreResultCounts; + private bool beatmapApplied; protected readonly Dictionary ScoreResultCounts = new Dictionary(); diff --git a/osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs b/osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs index 5aef85fa13..c27e7f15ca 100644 --- a/osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs +++ b/osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs @@ -13,6 +13,8 @@ using osu.Game.Online.API; using osu.Game.Online.Spectator; using osu.Game.Replays.Legacy; using osu.Game.Rulesets; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Replays; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; @@ -99,13 +101,24 @@ namespace osu.Game.Tests.Visual.Spectator /// The user to send frames for. /// The total number of frames to send. /// The time to start gameplay frames from. - public void SendFramesFromUser(int userId, int count, double startTime = 0) + /// Add a number of misses to frame header data for testing purposes. + public void SendFramesFromUser(int userId, int count, double startTime = 0, int initialResultCount = 0) { var frames = new List(); int currentFrameIndex = userNextFrameDictionary[userId]; int lastFrameIndex = currentFrameIndex + count - 1; + var scoreProcessor = new ScoreProcessor(rulesetStore.GetRuleset(0)!.CreateInstance()); + + for (int i = 0; i < initialResultCount; i++) + { + scoreProcessor.ApplyResult(new JudgementResult(new HitObject(), new Judgement()) + { + Type = HitResult.Miss, + }); + } + for (; currentFrameIndex <= lastFrameIndex; currentFrameIndex++) { // This is done in the next frame so that currentFrameIndex is updated to the correct value. @@ -130,7 +143,16 @@ namespace osu.Game.Tests.Visual.Spectator Combo = currentFrameIndex, TotalScore = (long)(currentFrameIndex * 123478 * RNG.NextDouble(0.99, 1.01)), Accuracy = RNG.NextDouble(0.98, 1), - }, new ScoreProcessor(rulesetStore.GetRuleset(0)!.CreateInstance()), frames.ToArray()); + Statistics = scoreProcessor.Statistics.ToDictionary(), + }, scoreProcessor, frames.ToArray()); + + if (initialResultCount > 0) + { + foreach (var f in frames) + f.Header = bundle.Header; + } + + scoreProcessor.ResetFromReplayFrame(frames.Last()); ((ISpectatorClient)this).UserSentFrames(userId, bundle); frames.Clear(); From c46e9cbce3d098dafe1cf5331d533fc00cf2aa1e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 18 Sep 2024 14:35:18 +0900 Subject: [PATCH 2305/2493] Tidy up `JudgementCounter` classes --- .../HUD/JudgementCounter/JudgementCount.cs | 18 ++++++++++++++++++ .../JudgementCountController.cs | 9 --------- .../HUD/JudgementCounter/JudgementCounter.cs | 7 +++---- .../JudgementCounterDisplay.cs | 2 +- 4 files changed, 22 insertions(+), 14 deletions(-) create mode 100644 osu.Game/Screens/Play/HUD/JudgementCounter/JudgementCount.cs diff --git a/osu.Game/Screens/Play/HUD/JudgementCounter/JudgementCount.cs b/osu.Game/Screens/Play/HUD/JudgementCounter/JudgementCount.cs new file mode 100644 index 0000000000..ad70e519a2 --- /dev/null +++ b/osu.Game/Screens/Play/HUD/JudgementCounter/JudgementCount.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.Bindables; +using osu.Framework.Localisation; +using osu.Game.Rulesets.Scoring; + +namespace osu.Game.Screens.Play.HUD.JudgementCounter +{ + public struct JudgementCount + { + public LocalisableString DisplayName { get; set; } + + public HitResult[] Types { get; set; } + + public BindableInt ResultCount { get; set; } + } +} diff --git a/osu.Game/Screens/Play/HUD/JudgementCounter/JudgementCountController.cs b/osu.Game/Screens/Play/HUD/JudgementCounter/JudgementCountController.cs index 8134c97bac..5a53a9edd3 100644 --- a/osu.Game/Screens/Play/HUD/JudgementCounter/JudgementCountController.cs +++ b/osu.Game/Screens/Play/HUD/JudgementCounter/JudgementCountController.cs @@ -67,14 +67,5 @@ namespace osu.Game.Screens.Play.HUD.JudgementCounter else count.ResultCount.Value++; } - - public struct JudgementCount - { - public LocalisableString DisplayName { get; set; } - - public HitResult[] Types { get; set; } - - public BindableInt ResultCount { get; set; } - } } } diff --git a/osu.Game/Screens/Play/HUD/JudgementCounter/JudgementCounter.cs b/osu.Game/Screens/Play/HUD/JudgementCounter/JudgementCounter.cs index 45ed8d749b..d69416f34a 100644 --- a/osu.Game/Screens/Play/HUD/JudgementCounter/JudgementCounter.cs +++ b/osu.Game/Screens/Play/HUD/JudgementCounter/JudgementCounter.cs @@ -9,7 +9,6 @@ using osu.Framework.Graphics.Containers; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; -using osu.Game.Rulesets; using osu.Game.Rulesets.Scoring; namespace osu.Game.Screens.Play.HUD.JudgementCounter @@ -19,16 +18,16 @@ namespace osu.Game.Screens.Play.HUD.JudgementCounter public BindableBool ShowName = new BindableBool(); public Bindable Direction = new Bindable(); - public readonly JudgementCountController.JudgementCount Result; + public readonly JudgementCount Result; - public JudgementCounter(JudgementCountController.JudgementCount result) => Result = result; + public JudgementCounter(JudgementCount result) => Result = result; public OsuSpriteText ResultName = null!; private FillFlowContainer flowContainer = null!; private JudgementRollingCounter counter = null!; [BackgroundDependencyLoader] - private void load(OsuColour colours, IBindable ruleset) + private void load(OsuColour colours) { AutoSizeAxes = Axes.Both; diff --git a/osu.Game/Screens/Play/HUD/JudgementCounter/JudgementCounterDisplay.cs b/osu.Game/Screens/Play/HUD/JudgementCounter/JudgementCounterDisplay.cs index 25e5464205..bc953435b7 100644 --- a/osu.Game/Screens/Play/HUD/JudgementCounter/JudgementCounterDisplay.cs +++ b/osu.Game/Screens/Play/HUD/JudgementCounter/JudgementCounterDisplay.cs @@ -126,7 +126,7 @@ namespace osu.Game.Screens.Play.HUD.JudgementCounter } } - private JudgementCounter createCounter(JudgementCountController.JudgementCount info) => + private JudgementCounter createCounter(JudgementCount info) => new JudgementCounter(info) { State = { Value = Visibility.Hidden }, From 8f49876fe7458924801338d04545fbf39a34755d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 18 Sep 2024 14:35:44 +0900 Subject: [PATCH 2306/2493] Re-sync judgement counter display after replay frame reset --- .../JudgementCountController.cs | 33 ++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/HUD/JudgementCounter/JudgementCountController.cs b/osu.Game/Screens/Play/HUD/JudgementCounter/JudgementCountController.cs index 5a53a9edd3..7e9f3cba08 100644 --- a/osu.Game/Screens/Play/HUD/JudgementCounter/JudgementCountController.cs +++ b/osu.Game/Screens/Play/HUD/JudgementCounter/JudgementCountController.cs @@ -6,7 +6,6 @@ using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; -using osu.Framework.Localisation; using osu.Game.Rulesets; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Scoring; @@ -53,8 +52,40 @@ namespace osu.Game.Screens.Play.HUD.JudgementCounter { base.LoadComplete(); + scoreProcessor.OnResetFromReplayFrame += updateAllCounts; scoreProcessor.NewJudgement += judgement => updateCount(judgement, false); scoreProcessor.JudgementReverted += judgement => updateCount(judgement, true); + + updateAllCounts(); + } + + private void updateAllCounts() + { + // This flow is made to handle cases of watching from the middle of a replay / spectating session. + // + // Once we get an initial state, we can rely on `NewJudgement` and `JudgementReverted`, so + // as a preemptive optimisation, only do a full re-sync if we have all-zero counts. + bool hasCounts = false; + + foreach (var r in results) + { + if (r.Value.ResultCount.Value > 0) + { + hasCounts = true; + break; + } + } + + if (hasCounts) + return; + + foreach (var kvp in scoreProcessor.Statistics) + { + if (!results.TryGetValue(kvp.Key, out var count)) + continue; + + count.ResultCount.Value = kvp.Value; + } } private void updateCount(JudgementResult judgement, bool revert) From aae98e6906450bbd4518e4ec33af8049a0508df4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 18 Sep 2024 16:11:47 +0900 Subject: [PATCH 2307/2493] Add failing test showing crash at song select on selection edge case --- .../Navigation/TestSceneScreenNavigation.cs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs index f02c2fd4f0..6cd89dcd0c 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs @@ -144,6 +144,22 @@ namespace osu.Game.Tests.Visual.Navigation exitViaEscapeAndConfirm(); } + [Test] + public void TestEnterGameplayWhileFilteringToNoSelection() + { + TestPlaySongSelect songSelect = null; + + PushAndConfirm(() => songSelect = new TestPlaySongSelect()); + AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely()); + AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault); + + AddStep("force selection", () => + { + songSelect.FinaliseSelection(); + songSelect.FilterControl.CurrentTextSearch.Value = "test"; + }); + } + [Test] public void TestSongSelectBackActionHandling() { From c192a6a1d54b69936c6e741e49c7b0524c0a2c8e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 18 Sep 2024 15:50:55 +0900 Subject: [PATCH 2308/2493] Fix song select crashes due to attempting to clear selection after load has already begun --- osu.Game/Screens/Select/BeatmapCarousel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 525884c413..57978b7bbd 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -1036,7 +1036,7 @@ namespace osu.Game.Screens.Select itemsCache.Validate(); // update and let external consumers know about selection loss. - if (BeatmapSetsLoaded) + if (BeatmapSetsLoaded && AllowSelection) { bool selectionLost = selectedBeatmapSet != null && selectedBeatmapSet.State.Value != CarouselItemState.Selected; From 743d50924105a557a0d08424e7c274ee1996d580 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 18 Sep 2024 16:40:27 +0900 Subject: [PATCH 2309/2493] Also ensure filter is applied when returning to song select --- .../Navigation/TestSceneScreenNavigation.cs | 6 +++++ osu.Game/Screens/Select/SongSelect.cs | 23 ++++++++++++++++++- 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs index 6cd89dcd0c..eda7ce925a 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs @@ -150,6 +150,7 @@ namespace osu.Game.Tests.Visual.Navigation TestPlaySongSelect songSelect = null; PushAndConfirm(() => songSelect = new TestPlaySongSelect()); + AddUntilStep("wait for song select", () => songSelect.BeatmapSetsLoaded); AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely()); AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault); @@ -158,6 +159,11 @@ namespace osu.Game.Tests.Visual.Navigation songSelect.FinaliseSelection(); songSelect.FilterControl.CurrentTextSearch.Value = "test"; }); + + AddUntilStep("wait for player", () => !songSelect.IsCurrentScreen()); + AddStep("return to song select", () => songSelect.MakeCurrent()); + + AddUntilStep("wait for selection lost", () => songSelect.Beatmap.IsDefault); } [Test] diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index 6da72ee660..18608d61e9 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -127,6 +127,8 @@ namespace osu.Game.Screens.Select private Sample sampleChangeDifficulty = null!; private Sample sampleChangeBeatmap = null!; + private bool pendingFilterApplication; + private Container carouselContainer = null!; protected BeatmapDetailArea BeatmapDetails { get; private set; } = null!; @@ -328,7 +330,20 @@ namespace osu.Game.Screens.Select GetRecommendedBeatmap = s => recommender?.GetRecommendedBeatmap(s), }, c => carouselContainer.Child = c); - FilterControl.FilterChanged = Carousel.Filter; + FilterControl.FilterChanged = criteria => + { + // If a filter operation is applied when we're in a state that doesn't allow selection, + // we might end up in an unexpected state. This is because currently carousel panels are in charge + // of updating the global selection (which is very hard to deal with). + // + // For now let's just avoid filtering when selection isn't allowed locally. + // This should be nuked from existence when we get around to fixing the complexity of song select <-> beatmap carousel. + // The debounce part of BeatmapCarousel's filtering should probably also be removed and handled locally. + if (Carousel.AllowSelection) + Carousel.Filter(criteria); + else + pendingFilterApplication = true; + }; if (ShowSongSelectFooter) { @@ -701,6 +716,12 @@ namespace osu.Game.Screens.Select Carousel.AllowSelection = true; + if (pendingFilterApplication) + { + Carousel.Filter(FilterControl.CreateCriteria()); + pendingFilterApplication = false; + } + BeatmapDetails.Refresh(); beginLooping(); From ac507a3ba568e40396a642d13032dbc1e8d6c314 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 18 Sep 2024 17:21:43 +0900 Subject: [PATCH 2310/2493] Remove unused parameter in `applyActiveCriteria` --- osu.Game/Screens/Select/BeatmapCarousel.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 57978b7bbd..d9359cfec3 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -706,7 +706,7 @@ namespace osu.Game.Screens.Select private bool beatmapsSplitOut; - private void applyActiveCriteria(bool debounce, bool alwaysResetScrollPosition = true) + private void applyActiveCriteria(bool debounce) { PendingFilter?.Cancel(); PendingFilter = null; @@ -735,8 +735,7 @@ namespace osu.Game.Screens.Select root.Filter(activeCriteria); itemsCache.Invalidate(); - if (alwaysResetScrollPosition || !Scroll.UserScrolling) - ScrollToSelected(true); + ScrollToSelected(true); FilterApplied?.Invoke(); } From 95e26e6fd8a548d1d0443e52e838ca68d9bc7319 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 18 Sep 2024 11:23:00 +0200 Subject: [PATCH 2311/2493] Make slider bar instantaneous by default (and fix broken implementation) --- .../UserInterface/TestSceneFormControls.cs | 16 +++++++++++++--- .../Graphics/UserInterfaceV2/FormSliderBar.cs | 8 ++++++-- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneFormControls.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneFormControls.cs index 6dd7275abf..369fe1a40c 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneFormControls.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneFormControls.cs @@ -8,7 +8,6 @@ using osu.Framework.Graphics.Cursor; using osu.Game.Graphics.Cursor; using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Localisation; -using osu.Game.Resources.Localisation.Web; using osuTK; namespace osu.Game.Tests.Visual.UserInterface @@ -72,8 +71,7 @@ namespace osu.Game.Tests.Visual.UserInterface }, new FormSliderBar { - Caption = BeatmapsetsStrings.ShowStatsDrain, - HintText = EditorSetupStrings.DrainRateDescription, + Caption = "Instantaneous slider", Current = new BindableFloat { MinValue = 0, @@ -82,6 +80,18 @@ namespace osu.Game.Tests.Visual.UserInterface Precision = 0.1f, } }, + new FormSliderBar + { + Caption = "Non-instantaneous slider", + Current = new BindableFloat + { + MinValue = 0, + MaxValue = 10, + Value = 5, + Precision = 0.1f, + }, + Instantaneous = false, + }, }, }, } diff --git a/osu.Game/Graphics/UserInterfaceV2/FormSliderBar.cs b/osu.Game/Graphics/UserInterfaceV2/FormSliderBar.cs index 91ce9da2d2..e4c814e71d 100644 --- a/osu.Game/Graphics/UserInterfaceV2/FormSliderBar.cs +++ b/osu.Game/Graphics/UserInterfaceV2/FormSliderBar.cs @@ -7,6 +7,7 @@ using System.Numerics; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; @@ -29,7 +30,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 set => current.Current = value; } - private bool instantaneous; + private bool instantaneous = true; /// /// Whether changes to the slider should instantaneously transfer to the text box (and vice versa). @@ -41,7 +42,9 @@ namespace osu.Game.Graphics.UserInterfaceV2 set { instantaneous = value; - slider.TransferValueOnCommit = !instantaneous; + + if (slider.IsNotNull()) + slider.TransferValueOnCommit = !instantaneous; } } @@ -116,6 +119,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 RelativeSizeAxes = Axes.X, Width = 0.5f, Current = Current, + TransferValueOnCommit = !instantaneous, } }, }, From 0bab755be316d46ef82700f7cd9e0f916202db46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 18 Sep 2024 11:25:23 +0200 Subject: [PATCH 2312/2493] Add missing xmldoc --- osu.Game/Graphics/UserInterfaceV2/FormCheckBox.cs | 7 +++++++ osu.Game/Graphics/UserInterfaceV2/FormSliderBar.cs | 7 +++++++ osu.Game/Graphics/UserInterfaceV2/FormTextBox.cs | 11 +++++++++++ 3 files changed, 25 insertions(+) diff --git a/osu.Game/Graphics/UserInterfaceV2/FormCheckBox.cs b/osu.Game/Graphics/UserInterfaceV2/FormCheckBox.cs index 797ff09800..d4cd86010f 100644 --- a/osu.Game/Graphics/UserInterfaceV2/FormCheckBox.cs +++ b/osu.Game/Graphics/UserInterfaceV2/FormCheckBox.cs @@ -29,7 +29,14 @@ namespace osu.Game.Graphics.UserInterfaceV2 private readonly BindableWithCurrent current = new BindableWithCurrent(); + /// + /// Caption describing this slider bar, displayed on top of the controls. + /// public LocalisableString Caption { get; init; } + + /// + /// Hint text containing an extended description of this slider bar, displayed in a tooltip when hovering the caption. + /// public LocalisableString HintText { get; init; } private Box background = null!; diff --git a/osu.Game/Graphics/UserInterfaceV2/FormSliderBar.cs b/osu.Game/Graphics/UserInterfaceV2/FormSliderBar.cs index e4c814e71d..1d44c5d810 100644 --- a/osu.Game/Graphics/UserInterfaceV2/FormSliderBar.cs +++ b/osu.Game/Graphics/UserInterfaceV2/FormSliderBar.cs @@ -50,7 +50,14 @@ namespace osu.Game.Graphics.UserInterfaceV2 private readonly BindableNumberWithCurrent current = new BindableNumberWithCurrent(); + /// + /// Caption describing this slider bar, displayed on top of the controls. + /// public LocalisableString Caption { get; init; } + + /// + /// Hint text containing an extended description of this slider bar, displayed in a tooltip when hovering the caption. + /// public LocalisableString HintText { get; init; } private Box background = null!; diff --git a/osu.Game/Graphics/UserInterfaceV2/FormTextBox.cs b/osu.Game/Graphics/UserInterfaceV2/FormTextBox.cs index 741bff6db6..9bbb5cba99 100644 --- a/osu.Game/Graphics/UserInterfaceV2/FormTextBox.cs +++ b/osu.Game/Graphics/UserInterfaceV2/FormTextBox.cs @@ -59,8 +59,19 @@ namespace osu.Game.Graphics.UserInterfaceV2 private readonly BindableWithCurrent current = new BindableWithCurrent(); + /// + /// Caption describing this slider bar, displayed on top of the controls. + /// public LocalisableString Caption { get; init; } + + /// + /// Hint text containing an extended description of this slider bar, displayed in a tooltip when hovering the caption. + /// public LocalisableString HintText { get; init; } + + /// + /// Text displayed in the text box when its contents are empty. + /// public LocalisableString PlaceholderText { get; init; } private Box background = null!; From 093d9ab076129cf732e21849f5ee49a0185a451d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 18 Sep 2024 11:30:52 +0200 Subject: [PATCH 2313/2493] Keep slider bar looking active when dragging outside of its bounds --- .../Graphics/UserInterfaceV2/FormSliderBar.cs | 27 ++++++++++++++++--- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/osu.Game/Graphics/UserInterfaceV2/FormSliderBar.cs b/osu.Game/Graphics/UserInterfaceV2/FormSliderBar.cs index 1d44c5d810..fa6d44d4c5 100644 --- a/osu.Game/Graphics/UserInterfaceV2/FormSliderBar.cs +++ b/osu.Game/Graphics/UserInterfaceV2/FormSliderBar.cs @@ -143,6 +143,8 @@ namespace osu.Game.Graphics.UserInterfaceV2 textBox.OnCommit += textCommitted; textBox.Current.BindValueChanged(textChanged); + slider.IsDragging.BindValueChanged(_ => updateState()); + current.BindValueChanged(_ => { updateState(); @@ -226,12 +228,12 @@ namespace osu.Game.Graphics.UserInterfaceV2 caption.Colour = Current.Disabled ? colourProvider.Foreground1 : colourProvider.Content2; textBox.Colour = Current.Disabled ? colourProvider.Foreground1 : colourProvider.Content1; - BorderThickness = IsHovered || textBox.Focused.Value ? 2 : 0; + BorderThickness = IsHovered || textBox.Focused.Value || slider.IsDragging.Value ? 2 : 0; BorderColour = textBox.Focused.Value ? colourProvider.Highlight1 : colourProvider.Light4; if (textBox.Focused.Value) background.Colour = ColourInfo.GradientVertical(colourProvider.Background5, colourProvider.Dark3); - else if (IsHovered) + else if (IsHovered || slider.IsDragging.Value) background.Colour = ColourInfo.GradientVertical(colourProvider.Background5, colourProvider.Dark4); else background.Colour = colourProvider.Background5; @@ -246,6 +248,8 @@ namespace osu.Game.Graphics.UserInterfaceV2 private partial class Slider : OsuSliderBar { + public BindableBool IsDragging { get; set; } = new BindableBool(); + private Box leftBox = null!; private Box rightBox = null!; private Circle nub = null!; @@ -313,6 +317,21 @@ namespace osu.Game.Graphics.UserInterfaceV2 rightBox.Width = Math.Clamp(DrawWidth - nub.DrawPosition.X - RangePadding, 0, Math.Max(0, DrawWidth)) / DrawWidth; } + protected override bool OnDragStart(DragStartEvent e) + { + bool dragging = base.OnDragStart(e); + IsDragging.Value = dragging; + updateState(); + return dragging; + } + + protected override void OnDragEnd(DragEndEvent e) + { + base.OnDragEnd(e); + IsDragging.Value = false; + updateState(); + } + protected override bool OnHover(HoverEvent e) { updateState(); @@ -328,8 +347,8 @@ namespace osu.Game.Graphics.UserInterfaceV2 private void updateState() { rightBox.Colour = colourProvider.Background6; - leftBox.Colour = IsHovered ? colourProvider.Highlight1.Opacity(0.5f) : colourProvider.Dark2; - nub.Colour = IsHovered ? colourProvider.Highlight1 : colourProvider.Light4; + leftBox.Colour = IsHovered || IsDragged ? colourProvider.Highlight1.Opacity(0.5f) : colourProvider.Dark2; + nub.Colour = IsHovered || IsDragged ? colourProvider.Highlight1 : colourProvider.Light4; } protected override void UpdateValue(float value) From d506d8a1500f73a3b93473f155541073957b7a59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 18 Sep 2024 11:32:55 +0200 Subject: [PATCH 2314/2493] Implement `TabbableContentContainer` for slider control --- .../UserInterface/TestSceneFormControls.cs | 4 +++- .../Graphics/UserInterfaceV2/FormSliderBar.cs | 16 +++++++++++++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneFormControls.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneFormControls.cs index 369fe1a40c..b456da0f26 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneFormControls.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneFormControls.cs @@ -78,7 +78,8 @@ namespace osu.Game.Tests.Visual.UserInterface MaxValue = 10, Value = 5, Precision = 0.1f, - } + }, + TabbableContentContainer = this, }, new FormSliderBar { @@ -91,6 +92,7 @@ namespace osu.Game.Tests.Visual.UserInterface Precision = 0.1f, }, Instantaneous = false, + TabbableContentContainer = this, }, }, }, diff --git a/osu.Game/Graphics/UserInterfaceV2/FormSliderBar.cs b/osu.Game/Graphics/UserInterfaceV2/FormSliderBar.cs index fa6d44d4c5..84becb72c9 100644 --- a/osu.Game/Graphics/UserInterfaceV2/FormSliderBar.cs +++ b/osu.Game/Graphics/UserInterfaceV2/FormSliderBar.cs @@ -48,6 +48,19 @@ namespace osu.Game.Graphics.UserInterfaceV2 } } + private CompositeDrawable? tabbableContentContainer; + + public CompositeDrawable? TabbableContentContainer + { + set + { + tabbableContentContainer = value; + + if (textBox.IsNotNull()) + textBox.TabbableContentContainer = tabbableContentContainer; + } + } + private readonly BindableNumberWithCurrent current = new BindableNumberWithCurrent(); /// @@ -117,7 +130,8 @@ namespace osu.Game.Graphics.UserInterfaceV2 { flashLayer.Colour = ColourInfo.GradientVertical(colours.Red3.Opacity(0), colours.Red3); flashLayer.FadeOutFromOne(200, Easing.OutQuint); - } + }, + TabbableContentContainer = tabbableContentContainer, }, slider = new Slider { From 12bd516a570191cbb31d271082530eafca1206de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 18 Sep 2024 13:51:45 +0200 Subject: [PATCH 2315/2493] Shuffle playback order in global playlist by default RFC. Closes https://github.com/ppy/osu/issues/18169. Implements the given proposal of keeping the current stable order but adding a shuffle facility to the now playing overlay, and enabling it by default. There are more changes I want to make here but I'd like this to get discussion first, because I am likely to continue putting this sort of selection logic into `MusicController` and I just want to confirm nobody is going to have a problem with that. In particular this is not sharing the randomisation implementation with beatmap carousel because it doesn't generalise nicely (song select cares about the particular *beatmap difficulties* selected to rewind properly, while the music controller only cares about picking a *beatmap set*). --- .../Menus/TestSceneMusicActionHandling.cs | 2 + osu.Game/Overlays/MusicController.cs | 83 ++++++++++++++++++- osu.Game/Overlays/NowPlayingOverlay.cs | 12 +++ 3 files changed, 94 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Menus/TestSceneMusicActionHandling.cs b/osu.Game.Tests/Visual/Menus/TestSceneMusicActionHandling.cs index 03b3b94bd8..4454501a96 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneMusicActionHandling.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneMusicActionHandling.cs @@ -34,6 +34,8 @@ namespace osu.Game.Tests.Visual.Menus { Queue<(IWorkingBeatmap working, TrackChangeDirection changeDirection)> trackChangeQueue = null!; + AddStep("disable shuffle", () => Game.MusicController.Shuffle.Value = false); + // ensure we have at least two beatmaps available to identify the direction the music controller navigated to. AddRepeatStep("import beatmap", () => Game.BeatmapManager.Import(TestResources.CreateTestBeatmapSetInfo()), 5); diff --git a/osu.Game/Overlays/MusicController.cs b/osu.Game/Overlays/MusicController.cs index 63efdd5381..a7bca536df 100644 --- a/osu.Game/Overlays/MusicController.cs +++ b/osu.Game/Overlays/MusicController.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Audio; @@ -13,8 +14,10 @@ using osu.Framework.Graphics.Audio; using osu.Framework.Graphics.Containers; using osu.Framework.Logging; using osu.Framework.Threading; +using osu.Framework.Utils; using osu.Game.Audio.Effects; using osu.Game.Beatmaps; +using osu.Game.Configuration; using osu.Game.Database; using osu.Game.Rulesets.Mods; @@ -43,6 +46,8 @@ namespace osu.Game.Overlays /// public readonly BindableBool AllowTrackControl = new BindableBool(true); + public readonly BindableBool Shuffle = new BindableBool(true); + /// /// Fired when the global has changed. /// Includes direction information for display purposes. @@ -66,12 +71,18 @@ namespace osu.Game.Overlays private AudioFilter audioDuckFilter = null!; + private readonly Bindable randomSelectAlgorithm = new Bindable(); + private readonly List previousRandomSets = new List(); + private int randomHistoryDirection; + [BackgroundDependencyLoader] - private void load(AudioManager audio) + private void load(AudioManager audio, OsuConfigManager configManager) { AddInternal(audioDuckFilter = new AudioFilter(audio.TrackMixer)); audio.Tracks.AddAdjustment(AdjustableProperty.Volume, audioDuckVolume); sampleVolume = audio.VolumeSample.GetBoundCopy(); + + configManager.BindWith(OsuSetting.RandomSelectAlgorithm, randomSelectAlgorithm); } protected override void LoadComplete() @@ -238,8 +249,15 @@ namespace osu.Game.Overlays queuedDirection = TrackChangeDirection.Prev; - var playableSet = getBeatmapSets().AsEnumerable().TakeWhile(i => !i.Equals(current?.BeatmapSetInfo)).LastOrDefault(s => !s.Protected || allowProtectedTracks) + BeatmapSetInfo? playableSet; + + if (Shuffle.Value) + playableSet = getNextRandom(-1, allowProtectedTracks); + else + { + playableSet = getBeatmapSets().AsEnumerable().TakeWhile(i => !i.Equals(current?.BeatmapSetInfo)).LastOrDefault(s => !s.Protected || allowProtectedTracks) ?? getBeatmapSets().AsEnumerable().LastOrDefault(s => !s.Protected || allowProtectedTracks); + } if (playableSet != null) { @@ -327,8 +345,15 @@ namespace osu.Game.Overlays queuedDirection = TrackChangeDirection.Next; - var playableSet = getBeatmapSets().AsEnumerable().SkipWhile(i => !i.Equals(current?.BeatmapSetInfo) || (i.Protected && !allowProtectedTracks)).ElementAtOrDefault(1) + BeatmapSetInfo? playableSet; + + if (Shuffle.Value) + playableSet = getNextRandom(1, allowProtectedTracks); + else + { + playableSet = getBeatmapSets().AsEnumerable().SkipWhile(i => !i.Equals(current?.BeatmapSetInfo) || (i.Protected && !allowProtectedTracks)).ElementAtOrDefault(1) ?? getBeatmapSets().AsEnumerable().FirstOrDefault(i => !i.Protected || allowProtectedTracks); + } var playableBeatmap = playableSet?.Beatmaps.FirstOrDefault(); @@ -342,6 +367,58 @@ namespace osu.Game.Overlays return false; } + private BeatmapSetInfo? getNextRandom(int direction, bool allowProtectedTracks) + { + BeatmapSetInfo result; + + var possibleSets = getBeatmapSets().AsEnumerable().Where(s => !s.Protected || allowProtectedTracks).ToArray(); + + if (possibleSets.Length == 0) + return null; + + // condition below checks if the signs of `randomHistoryDirection` and `direction` are opposite and not zero. + // if that is the case, it means that the user had previously chosen next track `randomHistoryDirection` times and wants to go back, + // or that the user had previously chosen previous track `randomHistoryDirection` times and wants to go forward. + // in both cases, it means that we have a history of previous random selections that we can rewind. + if (randomHistoryDirection * direction < 0) + { + Debug.Assert(Math.Abs(randomHistoryDirection) == previousRandomSets.Count); + result = previousRandomSets[^1]; + previousRandomSets.RemoveAt(previousRandomSets.Count - 1); + randomHistoryDirection += direction; + return result; + } + + // if the early-return above didn't cover it, it means that we have no history to fall back on + // and need to actually choose something random. + switch (randomSelectAlgorithm.Value) + { + case RandomSelectAlgorithm.Random: + result = possibleSets[RNG.Next(possibleSets.Length)]; + break; + + case RandomSelectAlgorithm.RandomPermutation: + var notYetPlayedSets = possibleSets.Except(previousRandomSets).ToArray(); + + if (notYetPlayedSets.Length == 0) + { + notYetPlayedSets = possibleSets; + previousRandomSets.Clear(); + randomHistoryDirection = 0; + } + + result = notYetPlayedSets[RNG.Next(notYetPlayedSets.Length)]; + break; + + default: + throw new ArgumentOutOfRangeException(nameof(randomSelectAlgorithm), randomSelectAlgorithm.Value, "Unsupported random select algorithm"); + } + + previousRandomSets.Add(result); + randomHistoryDirection += direction; + return result; + } + private void restartTrack() { // if not scheduled, the previously track will be stopped one frame later (see ScheduleAfterChildren logic in GameBase). diff --git a/osu.Game/Overlays/NowPlayingOverlay.cs b/osu.Game/Overlays/NowPlayingOverlay.cs index 76c8c237d5..bcd15a2b7e 100644 --- a/osu.Game/Overlays/NowPlayingOverlay.cs +++ b/osu.Game/Overlays/NowPlayingOverlay.cs @@ -47,6 +47,7 @@ namespace osu.Game.Overlays private IconButton prevButton = null!; private IconButton playButton = null!; private IconButton nextButton = null!; + private MusicIconButton shuffleButton = null!; private IconButton playlistButton = null!; private ScrollingTextContainer title = null!, artist = null!; @@ -69,6 +70,7 @@ namespace osu.Game.Overlays private OsuColour colours { get; set; } = null!; private Bindable allowTrackControl = null!; + private BindableBool shuffle = new BindableBool(true); public NowPlayingOverlay() { @@ -162,6 +164,13 @@ namespace osu.Game.Overlays Action = () => musicController.NextTrack(), Icon = FontAwesome.Solid.StepForward, }, + shuffleButton = new MusicIconButton + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Action = shuffle.Toggle, + Icon = FontAwesome.Solid.Random, + } } }, playlistButton = new MusicIconButton @@ -227,6 +236,9 @@ namespace osu.Game.Overlays allowTrackControl = musicController.AllowTrackControl.GetBoundCopy(); allowTrackControl.BindValueChanged(_ => Scheduler.AddOnce(updateEnabledStates), true); + shuffle.BindTo(musicController.Shuffle); + shuffle.BindValueChanged(s => shuffleButton.FadeColour(s.NewValue ? colours.Yellow : Color4.White, 200, Easing.OutQuint), true); + musicController.TrackChanged += trackChanged; trackChanged(beatmap.Value); } From 2d3b027f85d7c30fe269ac8fa99bfca0dcd1555e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 18 Sep 2024 15:18:11 +0200 Subject: [PATCH 2316/2493] Add test case covering desired behaviour --- .../Editing/TestSceneComposerSelection.cs | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneComposerSelection.cs b/osu.Game.Tests/Visual/Editing/TestSceneComposerSelection.cs index 3884a3108f..765d7ee21e 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneComposerSelection.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneComposerSelection.cs @@ -215,6 +215,54 @@ namespace osu.Game.Tests.Visual.Editing AddAssert("2 hitobjects selected", () => EditorBeatmap.SelectedHitObjects.Count == 2 && !EditorBeatmap.SelectedHitObjects.Contains(addedObjects[1])); } + [Test] + public void TestMultiSelectWithDragBox() + { + var addedObjects = new[] + { + new HitCircle { StartTime = 100 }, + new HitCircle { StartTime = 200, Position = new Vector2(100) }, + new HitCircle { StartTime = 300, Position = new Vector2(512, 0) }, + new HitCircle { StartTime = 400, Position = new Vector2(412, 100) }, + }; + AddStep("add hitobjects", () => EditorBeatmap.AddRange(addedObjects)); + + AddStep("start dragging", () => + { + InputManager.MoveMouseTo(blueprintContainer.ScreenSpaceDrawQuad.Centre); + InputManager.PressButton(MouseButton.Left); + }); + AddStep("drag to left corner", () => InputManager.MoveMouseTo(blueprintContainer.ScreenSpaceDrawQuad.TopLeft - new Vector2(5))); + AddStep("end dragging", () => InputManager.ReleaseButton(MouseButton.Left)); + + AddAssert("2 hitobjects selected", () => EditorBeatmap.SelectedHitObjects, () => Has.Count.EqualTo(2)); + + AddStep("start dragging with control", () => + { + InputManager.MoveMouseTo(blueprintContainer.ScreenSpaceDrawQuad.Centre); + InputManager.PressButton(MouseButton.Left); + InputManager.PressKey(Key.ControlLeft); + }); + AddStep("drag to left corner", () => InputManager.MoveMouseTo(blueprintContainer.ScreenSpaceDrawQuad.TopRight + new Vector2(5, -5))); + AddStep("end dragging", () => + { + InputManager.ReleaseButton(MouseButton.Left); + InputManager.ReleaseKey(Key.ControlLeft); + }); + + AddAssert("4 hitobjects selected", () => EditorBeatmap.SelectedHitObjects, () => Has.Count.EqualTo(4)); + + AddStep("start dragging without control", () => + { + InputManager.MoveMouseTo(blueprintContainer.ScreenSpaceDrawQuad.Centre); + InputManager.PressButton(MouseButton.Left); + }); + AddStep("drag to left corner", () => InputManager.MoveMouseTo(blueprintContainer.ScreenSpaceDrawQuad.TopRight + new Vector2(5, -5))); + AddStep("end dragging", () => InputManager.ReleaseButton(MouseButton.Left)); + + AddAssert("2 hitobjects selected", () => EditorBeatmap.SelectedHitObjects, () => Has.Count.EqualTo(2)); + } + [Test] public void TestNearestSelection() { From f6195c551547e3b801563b86f9df17e4f4b81182 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 18 Sep 2024 15:02:04 +0200 Subject: [PATCH 2317/2493] Add to existing selection when dragging with control pressed Closes https://github.com/ppy/osu/issues/29023. --- .../Edit/Compose/Components/BlueprintContainer.cs | 14 +++++++++++--- .../Timeline/TimelineBlueprintContainer.cs | 5 ++++- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs index c66be90605..9776e64855 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs @@ -196,6 +196,11 @@ namespace osu.Game.Screens.Edit.Compose.Components DragBox.HandleDrag(e); DragBox.Show(); + + selectionBeforeDrag.Clear(); + if (e.ControlPressed) + selectionBeforeDrag.UnionWith(SelectedItems); + return true; } @@ -217,6 +222,7 @@ namespace osu.Game.Screens.Edit.Compose.Components } DragBox.Hide(); + selectionBeforeDrag.Clear(); } protected override void Update() @@ -227,7 +233,7 @@ namespace osu.Game.Screens.Edit.Compose.Components { lastDragEvent.Target = this; DragBox.HandleDrag(lastDragEvent); - UpdateSelectionFromDragBox(); + UpdateSelectionFromDragBox(selectionBeforeDrag); } } @@ -472,7 +478,7 @@ namespace osu.Game.Screens.Edit.Compose.Components /// /// Select all blueprints in a selection area specified by . /// - protected virtual void UpdateSelectionFromDragBox() + protected virtual void UpdateSelectionFromDragBox(HashSet selectionBeforeDrag) { var quad = DragBox.Box.ScreenSpaceDrawQuad; @@ -482,7 +488,7 @@ namespace osu.Game.Screens.Edit.Compose.Components { case SelectionState.Selected: // Selection is preserved even after blueprint becomes dead. - if (!quad.Contains(blueprint.ScreenSpaceSelectionPoint)) + if (!quad.Contains(blueprint.ScreenSpaceSelectionPoint) && !selectionBeforeDrag.Contains(blueprint.Item)) blueprint.Deselect(); break; @@ -535,6 +541,8 @@ namespace osu.Game.Screens.Edit.Compose.Components /// private bool wasDragStarted; + private readonly HashSet selectionBeforeDrag = new HashSet(); + /// /// Attempts to begin the movement of any selected blueprints. /// diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs index 740f0b6aac..a6af83d268 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs @@ -173,7 +173,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline protected sealed override DragBox CreateDragBox() => new TimelineDragBox(); - protected override void UpdateSelectionFromDragBox() + protected override void UpdateSelectionFromDragBox(HashSet selectionBeforeDrag) { Composer.BlueprintContainer.CommitIfPlacementActive(); @@ -191,6 +191,9 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline bool shouldBeSelected(HitObject hitObject) { + if (selectionBeforeDrag.Contains(hitObject)) + return true; + double midTime = (hitObject.StartTime + hitObject.GetEndTime()) / 2; return minTime <= midTime && midTime <= maxTime; } From 7f52ae883723a684f84f162b6a71b1f8e6dcdcea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 18 Sep 2024 15:30:56 +0200 Subject: [PATCH 2318/2493] Fix code quality inspection --- osu.Game/Overlays/NowPlayingOverlay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/NowPlayingOverlay.cs b/osu.Game/Overlays/NowPlayingOverlay.cs index bcd15a2b7e..e1e5aa9426 100644 --- a/osu.Game/Overlays/NowPlayingOverlay.cs +++ b/osu.Game/Overlays/NowPlayingOverlay.cs @@ -70,7 +70,7 @@ namespace osu.Game.Overlays private OsuColour colours { get; set; } = null!; private Bindable allowTrackControl = null!; - private BindableBool shuffle = new BindableBool(true); + private readonly BindableBool shuffle = new BindableBool(true); public NowPlayingOverlay() { From c185acdbae367902f84e803b12d62ebd95c0566d Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Wed, 18 Sep 2024 11:16:25 -0700 Subject: [PATCH 2319/2493] Use `GetLocalisedBindableString()` instead --- osu.Game/Online/Chat/MessageFormatter.cs | 2 -- osu.Game/OsuGame.cs | 18 ++++++++---------- .../BeatmapSet/BeatmapSetHeaderContent.cs | 4 ++-- 3 files changed, 10 insertions(+), 14 deletions(-) diff --git a/osu.Game/Online/Chat/MessageFormatter.cs b/osu.Game/Online/Chat/MessageFormatter.cs index 0f444ccde9..77454c4775 100644 --- a/osu.Game/Online/Chat/MessageFormatter.cs +++ b/osu.Game/Online/Chat/MessageFormatter.cs @@ -340,8 +340,6 @@ namespace osu.Game.Online.Chat Spectate, OpenUserProfile, SearchBeatmapSet, - SearchBeatmapTitle, - SearchBeatmapArtist, OpenWiki, Custom, OpenChangelog, diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index ffb145d7de..1af86b2d83 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -445,17 +445,15 @@ namespace osu.Game break; case LinkAction.SearchBeatmapSet: - SearchBeatmapSet(argString); - break; + if (link.Argument is LocalisableString localisable) + { + var localised = Localisation.GetLocalisedBindableString(localisable); + SearchBeatmapSet(localised.Value); + localised.UnbindAll(); + } + else + SearchBeatmapSet(argString); - case LinkAction.SearchBeatmapTitle: - string title = ((RomanisableString)link.Argument).GetPreferred(Localisation.CurrentParameters.Value.PreferOriginalScript); - SearchBeatmapSet($@"title=""""{title}"""""); - break; - - case LinkAction.SearchBeatmapArtist: - string artist = ((RomanisableString)link.Argument).GetPreferred(Localisation.CurrentParameters.Value.PreferOriginalScript); - SearchBeatmapSet($@"artist=""""{artist}"""""); break; case LinkAction.FilterBeatmapSetGenre: diff --git a/osu.Game/Overlays/BeatmapSet/BeatmapSetHeaderContent.cs b/osu.Game/Overlays/BeatmapSet/BeatmapSetHeaderContent.cs index 6ea16a9997..a50043f0f0 100644 --- a/osu.Game/Overlays/BeatmapSet/BeatmapSetHeaderContent.cs +++ b/osu.Game/Overlays/BeatmapSet/BeatmapSetHeaderContent.cs @@ -242,7 +242,7 @@ namespace osu.Game.Overlays.BeatmapSet title.Clear(); artist.Clear(); - title.AddLink(titleText, LinkAction.SearchBeatmapTitle, titleText); + title.AddLink(titleText, LinkAction.SearchBeatmapSet, LocalisableString.Interpolate($@"title=""""{titleText}""""")); title.AddArbitraryDrawable(Empty().With(d => d.Width = 5)); title.AddArbitraryDrawable(externalLink = new ExternalLinkButton()); @@ -259,7 +259,7 @@ namespace osu.Game.Overlays.BeatmapSet title.AddArbitraryDrawable(new SpotlightBeatmapBadge()); } - artist.AddLink(artistText, LinkAction.SearchBeatmapArtist, artistText); + artist.AddLink(artistText, LinkAction.SearchBeatmapSet, LocalisableString.Interpolate($@"artist=""""{artistText}""""")); if (setInfo.NewValue.TrackId != null) { From 0bad5e468455ef2667015a0c0fea148143459d6d Mon Sep 17 00:00:00 2001 From: StanR Date: Thu, 19 Sep 2024 04:38:01 +0500 Subject: [PATCH 2320/2493] Move slider-related ratio multiplier out of the delta switch block, add nerf for ratios with delta difference fractions that are too big, adjust consts --- .../Difficulty/Evaluators/RhythmEvaluator.cs | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs index 9eae3c7a10..80d75d5e67 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs @@ -63,8 +63,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators private const int history_time_max = 4 * 1000; // 5 seconds of calculatingRhythmBonus max. private const int history_objects_max = 24; - private const double rhythm_overall_multiplier = 1.2; - private const double rhythm_ratio_multiplier = 10.0; + private const double rhythm_overall_multiplier = 1.25; + private const double rhythm_ratio_multiplier = 11.0; /// /// Calculates a rhythm multiplier for the difficulty of the tap associated with historic data of the current . @@ -114,9 +114,22 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators double deltaDifferenceRatio = Math.Min(prevDelta, currDelta) / Math.Max(prevDelta, currDelta); double currRatio = 1.0 + rhythm_ratio_multiplier * Math.Min(0.5, Math.Pow(Math.Sin(Math.PI / deltaDifferenceRatio), 2)); + // reduce ratio bonus if delta difference is too big + double fraction = Math.Max(prevDelta / currDelta, currDelta / prevDelta); + double fractionMultiplier = Math.Clamp(2.0 - fraction / 8.0, 0.0, 1.0); + double windowPenalty = Math.Min(1, Math.Max(0, Math.Abs(prevDelta - currDelta) - deltaDifferenceEpsilon) / deltaDifferenceEpsilon); - double effectiveRatio = windowPenalty * currRatio; + double effectiveRatio = windowPenalty * currRatio * fractionMultiplier; + + // bpm change is into slider, this is easy acc window + if (currObj.BaseObject is Slider) + effectiveRatio *= 0.125; + + // bpm change was from a slider, this is easier typically than circle -> circle + // unintentional side effect is that bursts with kicksliders at the ends might have lower difficulty than bursts without sliders + if (prevObj.BaseObject is Slider) + effectiveRatio *= 0.2; if (firstDeltaSwitch) { @@ -127,15 +140,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators } else { - // bpm change is into slider, this is easy acc window - if (currObj.BaseObject is Slider) - effectiveRatio *= 0.125; - - // bpm change was from a slider, this is easier typically than circle -> circle - // unintentional side effect is that bursts with kicksliders at the ends might have lower difficulty than bursts without sliders - if (prevObj.BaseObject is Slider) - effectiveRatio *= 0.2; - // repeated island polarity (2 -> 4, 3 -> 5) if (island.IsSimilarPolarity(previousIsland)) effectiveRatio *= 0.3; @@ -180,10 +184,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators // Begin counting island until we change speed again. firstDeltaSwitch = true; - // reduce ratio if we're starting after a slider - if (prevObj.BaseObject is Slider) - effectiveRatio *= 0.3; - startRatio = effectiveRatio; island = new Island((int)currDelta, deltaDifferenceEpsilon); From d0519238a3a74f507da725035ce9cba5ad757cb0 Mon Sep 17 00:00:00 2001 From: Neku Date: Wed, 18 Sep 2024 22:57:37 +0200 Subject: [PATCH 2321/2493] Implement beat-synchronized animation in skip overlay --- osu.Game/Screens/Play/SkipOverlay.cs | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/osu.Game/Screens/Play/SkipOverlay.cs b/osu.Game/Screens/Play/SkipOverlay.cs index 29b2e5229b..c88724c8db 100644 --- a/osu.Game/Screens/Play/SkipOverlay.cs +++ b/osu.Game/Screens/Play/SkipOverlay.cs @@ -9,6 +9,7 @@ using osu.Framework; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; +using osu.Framework.Audio.Track; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; @@ -16,6 +17,7 @@ using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Framework.Utils; +using osu.Game.Beatmaps.ControlPoints; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; @@ -26,7 +28,7 @@ using osuTK.Graphics; namespace osu.Game.Screens.Play { - public partial class SkipOverlay : CompositeDrawable, IKeyBindingHandler + public partial class SkipOverlay : BeatSyncedContainer, IKeyBindingHandler { /// /// The total number of successful skips performed by this overlay. @@ -36,10 +38,9 @@ namespace osu.Game.Screens.Play private readonly double startTime; public Action RequestSkip; - private Button button; private ButtonContainer buttonContainer; - private Box remainingTimeBox; + private Circle remainingTimeBox; private FadeContainer fadeContainer; private double displayTime; @@ -51,7 +52,6 @@ namespace osu.Game.Screens.Play private IGameplayClock gameplayClock { get; set; } internal bool IsButtonVisible => fadeContainer.State == Visibility.Visible && buttonContainer.State.Value == Visibility.Visible; - public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; /// @@ -87,13 +87,13 @@ namespace osu.Game.Screens.Play Anchor = Anchor.Centre, Origin = Anchor.Centre, }, - remainingTimeBox = new Box + remainingTimeBox = new Circle { Height = 5, - RelativeSizeAxes = Axes.X, - Colour = colours.Yellow, Anchor = Anchor.BottomCentre, Origin = Anchor.BottomCentre, + Colour = colours.Yellow, + RelativeSizeAxes = Axes.X } } } @@ -210,6 +210,18 @@ namespace osu.Game.Screens.Play { } + protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes) + { + base.OnNewBeat(beatIndex, timingPoint, effectPoint, amplitudes); + + if (fadeOutBeginTime <= gameplayClock.CurrentTime) + return; + + float progress = (float)(gameplayClock.CurrentTime - displayTime) / (float)(fadeOutBeginTime - displayTime); + float newWidth = Math.Max(0, 1 - Math.Clamp(progress, 0, 1)); + remainingTimeBox.ResizeWidthTo(newWidth, timingPoint.BeatLength * 2, Easing.OutQuint); + } + public partial class FadeContainer : Container, IStateful { [CanBeNull] From fdd94aa8452ac5d70bd139d22f700d6e456947f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 19 Sep 2024 09:41:54 +0200 Subject: [PATCH 2322/2493] Remove pointless max The clamp should already ensure this. --- osu.Game/Screens/Play/SkipOverlay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/SkipOverlay.cs b/osu.Game/Screens/Play/SkipOverlay.cs index c88724c8db..362677ca5c 100644 --- a/osu.Game/Screens/Play/SkipOverlay.cs +++ b/osu.Game/Screens/Play/SkipOverlay.cs @@ -218,7 +218,7 @@ namespace osu.Game.Screens.Play return; float progress = (float)(gameplayClock.CurrentTime - displayTime) / (float)(fadeOutBeginTime - displayTime); - float newWidth = Math.Max(0, 1 - Math.Clamp(progress, 0, 1)); + float newWidth = 1 - Math.Clamp(progress, 0, 1); remainingTimeBox.ResizeWidthTo(newWidth, timingPoint.BeatLength * 2, Easing.OutQuint); } From fd45644d0f58f6eb90916aa820628b26918b60e2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 19 Sep 2024 16:54:00 +0900 Subject: [PATCH 2323/2493] Fix skin layout editor `PlayerAvatar` applying corner radius weirdly after scale Closes #29919. I've also made this handle resizing better, so now you can have non-square avatar displays. --- osu.Game/Screens/Play/HUD/PlayerAvatar.cs | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/PlayerAvatar.cs b/osu.Game/Screens/Play/HUD/PlayerAvatar.cs index 06d0f7bc9a..8e4406c2c1 100644 --- a/osu.Game/Screens/Play/HUD/PlayerAvatar.cs +++ b/osu.Game/Screens/Play/HUD/PlayerAvatar.cs @@ -39,14 +39,23 @@ namespace osu.Game.Screens.Play.HUD private IBindable? apiUser; + private readonly Container cornerContainer; + public PlayerAvatar() { Size = new Vector2(default_size); - InternalChild = avatar = new UpdateableAvatar(isInteractive: false) + InternalChild = cornerContainer = new Container { + Masking = true, RelativeSizeAxes = Axes.Both, - Masking = true + Child = avatar = new UpdateableAvatar(isInteractive: false) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + FillMode = FillMode.Fill, + } }; } @@ -66,7 +75,7 @@ namespace osu.Game.Screens.Play.HUD { base.LoadComplete(); - CornerRadius.BindValueChanged(e => avatar.CornerRadius = e.NewValue * default_size, true); + CornerRadius.BindValueChanged(e => cornerContainer.CornerRadius = e.NewValue * default_size, true); } public bool UsesFixedAnchor { get; set; } From ca8402d98021b6a83d5df5ec119c661bca38152c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 19 Sep 2024 19:06:46 +0900 Subject: [PATCH 2324/2493] Make animation slightly more snappy --- osu.Game/Graphics/UserInterfaceV2/FormSliderBar.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Graphics/UserInterfaceV2/FormSliderBar.cs b/osu.Game/Graphics/UserInterfaceV2/FormSliderBar.cs index 84becb72c9..ac3730598f 100644 --- a/osu.Game/Graphics/UserInterfaceV2/FormSliderBar.cs +++ b/osu.Game/Graphics/UserInterfaceV2/FormSliderBar.cs @@ -367,7 +367,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 protected override void UpdateValue(float value) { - nub.MoveToX(value, 250, Easing.OutQuint); + nub.MoveToX(value, 200, Easing.OutPow10); } } } From d5c2484109ccf55ae7169061696797d1b17e12bb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 19 Sep 2024 19:23:14 +0900 Subject: [PATCH 2325/2493] Always transfer updated counts once --- .../JudgementCountController.cs | 23 ++++--------------- 1 file changed, 5 insertions(+), 18 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/JudgementCounter/JudgementCountController.cs b/osu.Game/Screens/Play/HUD/JudgementCounter/JudgementCountController.cs index 7e9f3cba08..2562e26127 100644 --- a/osu.Game/Screens/Play/HUD/JudgementCounter/JudgementCountController.cs +++ b/osu.Game/Screens/Play/HUD/JudgementCounter/JudgementCountController.cs @@ -55,28 +55,13 @@ namespace osu.Game.Screens.Play.HUD.JudgementCounter scoreProcessor.OnResetFromReplayFrame += updateAllCounts; scoreProcessor.NewJudgement += judgement => updateCount(judgement, false); scoreProcessor.JudgementReverted += judgement => updateCount(judgement, true); - - updateAllCounts(); } + private bool hasUpdatedCounts; + private void updateAllCounts() { - // This flow is made to handle cases of watching from the middle of a replay / spectating session. - // - // Once we get an initial state, we can rely on `NewJudgement` and `JudgementReverted`, so - // as a preemptive optimisation, only do a full re-sync if we have all-zero counts. - bool hasCounts = false; - - foreach (var r in results) - { - if (r.Value.ResultCount.Value > 0) - { - hasCounts = true; - break; - } - } - - if (hasCounts) + if (hasUpdatedCounts) return; foreach (var kvp in scoreProcessor.Statistics) @@ -86,6 +71,8 @@ namespace osu.Game.Screens.Play.HUD.JudgementCounter count.ResultCount.Value = kvp.Value; } + + hasUpdatedCounts = true; } private void updateCount(JudgementResult judgement, bool revert) From 732a114b9594d2a861709591cd74d3438afdc375 Mon Sep 17 00:00:00 2001 From: StanR Date: Thu, 19 Sep 2024 15:53:18 +0500 Subject: [PATCH 2326/2493] Slight refactoring --- .../Difficulty/Evaluators/RhythmEvaluator.cs | 108 +++++++++--------- 1 file changed, 55 insertions(+), 53 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs index 80d75d5e67..87fc35adcd 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs @@ -11,57 +11,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators { public static class RhythmEvaluator { - private struct Island : IEquatable - { - private readonly double deltaDifferenceEpsilon; - - public Island(double epsilon) - { - deltaDifferenceEpsilon = epsilon; - } - - public Island(int delta, double epsilon) - { - deltaDifferenceEpsilon = epsilon; - Delta = Math.Max(delta, OsuDifficultyHitObject.MIN_DELTA_TIME); - } - - public int Delta { get; private set; } - public int DeltaCount { get; private set; } - - public void AddDelta(int delta) - { - if (Delta == default) - Delta = Math.Max(delta, OsuDifficultyHitObject.MIN_DELTA_TIME); - - DeltaCount++; - } - - public bool IsSimilarPolarity(Island other) - { - // consider islands to be of similar polarity only if they're having the same average delta (we don't want to consider 3 singletaps similar to a triple) - return DeltaCount % 2 == other.DeltaCount % 2 && - Math.Abs(Delta - other.Delta) < deltaDifferenceEpsilon; - } - - public override int GetHashCode() - { - return HashCode.Combine(Delta, DeltaCount); - } - - public bool Equals(Island other) - { - return Math.Abs(Delta - other.Delta) < deltaDifferenceEpsilon && - DeltaCount == other.DeltaCount; - } - - public override bool Equals(object? obj) - { - return obj?.GetHashCode() == GetHashCode(); - } - } - - private const int history_time_max = 4 * 1000; // 5 seconds of calculatingRhythmBonus max. + private const int history_time_max = 4 * 1000; // 4 seconds private const int history_objects_max = 24; private const double rhythm_overall_multiplier = 1.25; private const double rhythm_ratio_multiplier = 11.0; @@ -101,9 +51,11 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators { OsuDifficultyHitObject currObj = (OsuDifficultyHitObject)current.Previous(i - 1); - double currHistoricalDecay = (history_time_max - (current.StartTime - currObj.StartTime)) / history_time_max; // scales note 0 to 1 from history to now + // scales note 0 to 1 from history to now + double timeDecay = (history_time_max - (current.StartTime - currObj.StartTime)) / history_time_max; + double noteDecay = (double)(historicalNoteCount - i) / historicalNoteCount; - currHistoricalDecay = Math.Min((double)(historicalNoteCount - i) / historicalNoteCount, currHistoricalDecay); // either we're limited by time or limited by object count. + double currHistoricalDecay = Math.Min(noteDecay, timeDecay); // either we're limited by time or limited by object count. double currDelta = currObj.StrainTime; double prevDelta = prevObj.StrainTime; @@ -197,5 +149,55 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators } private static double logistic(double x, double maxValue, double multiplier, double offset) => (maxValue / (1 + Math.Pow(Math.E, offset - (multiplier * x)))); + + private struct Island : IEquatable + { + private readonly double deltaDifferenceEpsilon; + + public Island(double epsilon) + { + deltaDifferenceEpsilon = epsilon; + } + + public Island(int delta, double epsilon) + { + deltaDifferenceEpsilon = epsilon; + Delta = Math.Max(delta, OsuDifficultyHitObject.MIN_DELTA_TIME); + } + + public int Delta { get; private set; } + public int DeltaCount { get; private set; } + + public void AddDelta(int delta) + { + if (Delta == default) + Delta = Math.Max(delta, OsuDifficultyHitObject.MIN_DELTA_TIME); + + DeltaCount++; + } + + public bool IsSimilarPolarity(Island other) + { + // consider islands to be of similar polarity only if they're having the same average delta (we don't want to consider 3 singletaps similar to a triple) + return DeltaCount % 2 == other.DeltaCount % 2 && + Math.Abs(Delta - other.Delta) < deltaDifferenceEpsilon; + } + + public override int GetHashCode() + { + return HashCode.Combine(Delta, DeltaCount); + } + + public bool Equals(Island other) + { + return Math.Abs(Delta - other.Delta) < deltaDifferenceEpsilon && + DeltaCount == other.DeltaCount; + } + + public override bool Equals(object? obj) + { + return obj?.GetHashCode() == GetHashCode(); + } + } } } From 89509ea49ed608c2c587816c35833f1822490eb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 19 Sep 2024 14:03:49 +0200 Subject: [PATCH 2327/2493] Fix `DrawableOsuHitObject` not properly cleaning up dim application callbacks Should fix https://github.com/ppy/osu/issues/28629. First of all, to support the claim that this does fix the issue - reproduction is rather difficult, but I believe I found a way to maximise the chances of it reproducing by performing the following steps: 1. Apply the following diff: diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs index eacd2b3e75..4c00da031a 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs @@ -6,6 +6,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading; using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -95,6 +96,8 @@ public DrawableSlider([CanBeNull] Slider s = null) [BackgroundDependencyLoader] private void load() { + Thread.Sleep(100); + tailContainer = new Container { RelativeSizeAxes = Axes.Both }; AddRangeInternal(new Drawable[] 2. Download https://osu.ppy.sh/beatmapsets/1470790#osu/3023028 and open it in the editor. 3. Select all objects using Ctrl-A. Yes, it'll take a while, especially so with the patch above. 4. Rotate the selection by any amount using the right toolbox. 5. Press undo. The game should crash. If it doesn't spam redo and undo until it does. Now to explain what the fix is. In the issue thread I spent a considerable time hemming and hawing about which of the dimmable pieces was null, which was a complete miss and a failure at reading. Let's see the stack trace again: 2024-06-27 02:15:20 [error]: at osu.Game.Rulesets.Osu.Objects.Drawables.DrawableOsuHitObject.g__applyDim|15_0(Drawable piece) in /home/runner/work/osu-auth-client/osu-auth-client/osu/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs:line 101 Line 101, you say? What could be null here? https://github.com/ppy/osu/blob/bd8addfb5f71568479d2c037d1b6e811de6e7fe6/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs#L101 Okay, what's `InitialLifetimeOffset`, then? https://github.com/ppy/osu/blob/bd8addfb5f71568479d2c037d1b6e811de6e7fe6/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs#L108 Yes, that's right. It's `HitObject` that is null here. Now why does *that* happen? First, let's note that all stacks where this died went through `UpdateState()`, which means that the problematic `applyDim()` calls had to be `ApplyCustomUpdateState` event callbacks. Meaning that the pieces where `HitObject` was null were DHOs themselves. Recall that parent DHOs and child DHOs are pooled separately. Therefore, there is no guarantee that any parent and child DHOs will remain associated with each other for the entire duration of a gameplay session; it is quite the contrary, and nobody should rely on that. Unfortunately for us, adding a `applyDimToDrawableHitObject` callback to a child object's `ApplyCustomUpdateState` *implicitly creates* such an association, because it ends up allocating a closure that captures `this` (meaning the parent in this context). Therefore, this now creates a situation where a child DHO can attempt to read state from a former parent DHO which can be in an indeterminate state, and in fact, when this crashes, the former parent DHO is most likely not even in use - hence the null `HitObject`. Thus, the fix is to clear the association by unsubscribing from the event when nested objects are cleared. My hypothesis why the reproduction scenario is like it is, is that both the sleep and the increased pressure on the pool (by way of selecting all objects and therefore forcing the DHOs to be materialised beyond pool capacity) increases the likelihood of getting a crosslink. When pool pressure is low, it is much more likely that a parent DHO *will* get the same child DHO again on re-application, even though that is not guaranteed. Just as an additional detail, note that the sentry issue for this lists the "first seen" version as 2024.312.0, which is the release that included https://github.com/ppy/osu/pull/27401 which would be directly responsible for this mess. --- .../Objects/Drawables/DrawableOsuHitObject.cs | 35 +++++++++++++------ 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs index 5f5deca1ba..b3a68ec92d 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs @@ -91,20 +91,35 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables drawableObjectPiece.ApplyCustomUpdateState -= applyDimToDrawableHitObject; drawableObjectPiece.ApplyCustomUpdateState += applyDimToDrawableHitObject; } - else - applyDim(piece); - } - void applyDim(Drawable piece) - { - piece.FadeColour(new Color4(195, 195, 195, 255)); - using (piece.BeginDelayedSequence(InitialLifetimeOffset - OsuHitWindows.MISS_WINDOW)) - piece.FadeColour(Color4.White, 100); + // but at the end apply the transforms now regardless of whether this is a DHO or not. + // the above is just to ensure they don't get overwritten later. + applyDim(piece); } - - void applyDimToDrawableHitObject(DrawableHitObject dho, ArmedState _) => applyDim(dho); } + protected override void ClearNestedHitObjects() + { + base.ClearNestedHitObjects(); + + // any dimmable pieces that are DHOs will be pooled separately. + // `applyDimToDrawableHitObject` is a closure that implicitly captures `this`, + // and because of separate pooling of parent and child objects, there is no guarantee that the pieces will be associated with `this` again on re-use. + // therefore, clean up the subscription here to avoid crosstalk. + // not doing so can result in the callback attempting to read things from `this` when it is in a completely bogus state (not in use or similar). + foreach (var piece in DimmablePieces.OfType()) + piece.ApplyCustomUpdateState -= applyDimToDrawableHitObject; + } + + private void applyDim(Drawable piece) + { + piece.FadeColour(new Color4(195, 195, 195, 255)); + using (piece.BeginDelayedSequence(InitialLifetimeOffset - OsuHitWindows.MISS_WINDOW)) + piece.FadeColour(Color4.White, 100); + } + + private void applyDimToDrawableHitObject(DrawableHitObject dho, ArmedState _) => applyDim(dho); + protected sealed override double InitialLifetimeOffset => HitObject.TimePreempt; private OsuInputManager osuActionInputManager; From 202364be5edc8f5126f12894f614a33533bc454f Mon Sep 17 00:00:00 2001 From: StanR Date: Thu, 19 Sep 2024 17:52:55 +0500 Subject: [PATCH 2328/2493] Use `List` instead of `Dictionary` for island counting, minor adjustments --- .../Difficulty/Evaluators/RhythmEvaluator.cs | 35 +++++++++++++------ .../Difficulty/Skills/Speed.cs | 2 +- 2 files changed, 26 insertions(+), 11 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs index 87fc35adcd..d6ed60cc77 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs @@ -3,6 +3,8 @@ using System; using System.Collections.Generic; +using System.Linq; +using osu.Framework.Lists; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Osu.Difficulty.Preprocessing; using osu.Game.Rulesets.Osu.Objects; @@ -13,8 +15,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators { private const int history_time_max = 4 * 1000; // 4 seconds private const int history_objects_max = 24; - private const double rhythm_overall_multiplier = 1.25; - private const double rhythm_ratio_multiplier = 11.0; + private const double rhythm_overall_multiplier = 1.0; + private const double rhythm_ratio_multiplier = 14.0; /// /// Calculates a rhythm multiplier for the difficulty of the tap associated with historic data of the current . @@ -30,7 +32,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators var island = new Island(deltaDifferenceEpsilon); var previousIsland = new Island(deltaDifferenceEpsilon); - Dictionary islandCounts = new Dictionary(); + + // we can't use dictionary here because we need to compare island with a tolerance + // which is impossible to pass into the hash comparer + var islandCounts = new List<(Island Island, int Count)>(); double startRatio = 0; // store the ratio of the current start of an island to buff for tighter rhythms @@ -104,15 +109,21 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators if (island.DeltaCount == 1) effectiveRatio *= 0.7; - if (!islandCounts.TryAdd(island, 1)) + var islandCount = islandCounts.FirstOrDefault(x => x.Island.Equals(island)); + + if (islandCount != default) { // only add island to island counts if they're going one after another if (previousIsland.Equals(island)) - islandCounts[island]++; + islandCount.Count++; // repeated island (ex: triplet -> triplet) double power = logistic(island.Delta, 4, 0.165, 10); - effectiveRatio *= Math.Min(3.0 / islandCounts[island], Math.Pow(1.0 / islandCounts[island], power)); + effectiveRatio *= Math.Min(3.0 / islandCount.Count, Math.Pow(1.0 / islandCount.Count, power)); + } + else + { + islandCounts.Add((island, 1)); } // scale down the difficulty if the object is doubletappable @@ -150,7 +161,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators private static double logistic(double x, double maxValue, double multiplier, double offset) => (maxValue / (1 + Math.Pow(Math.E, offset - (multiplier * x)))); - private struct Island : IEquatable + private class Island : IEquatable { private readonly double deltaDifferenceEpsilon; @@ -163,6 +174,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators { deltaDifferenceEpsilon = epsilon; Delta = Math.Max(delta, OsuDifficultyHitObject.MIN_DELTA_TIME); + DeltaCount++; } public int Delta { get; private set; } @@ -188,15 +200,18 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators return HashCode.Combine(Delta, DeltaCount); } - public bool Equals(Island other) + public bool Equals(Island? other) { + if (other == null) + return false; + return Math.Abs(Delta - other.Delta) < deltaDifferenceEpsilon && DeltaCount == other.DeltaCount; } - public override bool Equals(object? obj) + public override string ToString() { - return obj?.GetHashCode() == GetHashCode(); + return $"{Delta}x{DeltaCount}"; } } } diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs index f7f081b7ea..37420ed026 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs @@ -16,7 +16,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills /// public class Speed : OsuStrainSkill { - private double skillMultiplier => 1.375; + private double skillMultiplier => 1.38; private double strainDecayBase => 0.3; private double currentStrain; From e986a7dfe117ff8b0ed1bbd691404bc5be1b7dcf Mon Sep 17 00:00:00 2001 From: StanR Date: Thu, 19 Sep 2024 19:15:54 +0500 Subject: [PATCH 2329/2493] Fix repetition nerf not updating the counts --- .../Difficulty/Evaluators/RhythmEvaluator.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs index d6ed60cc77..e34989f162 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.Linq; -using osu.Framework.Lists; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Osu.Difficulty.Preprocessing; using osu.Game.Rulesets.Osu.Objects; @@ -113,6 +112,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators if (islandCount != default) { + int countIndex = islandCounts.IndexOf(islandCount); + // only add island to island counts if they're going one after another if (previousIsland.Equals(island)) islandCount.Count++; @@ -120,6 +121,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators // repeated island (ex: triplet -> triplet) double power = logistic(island.Delta, 4, 0.165, 10); effectiveRatio *= Math.Min(3.0 / islandCount.Count, Math.Pow(1.0 / islandCount.Count, power)); + + islandCounts[countIndex] = (islandCount.Island, islandCount.Count); } else { From 8c72feda09feeac72b91542081cf745eaf5fbd97 Mon Sep 17 00:00:00 2001 From: PowerDaniex <140076282+u4vh3@users.noreply.github.com> Date: Wed, 10 Jul 2024 18:36:01 +0200 Subject: [PATCH 2330/2493] Add colour customization to the layout editor --- .../Configuration/SettingSourceAttribute.cs | 13 +++ osu.Game/Overlays/Settings/SettingsColour.cs | 79 +++++++++++++++++++ osu.Game/Overlays/SkinEditor/SkinEditor.cs | 5 ++ 3 files changed, 97 insertions(+) create mode 100644 osu.Game/Overlays/Settings/SettingsColour.cs diff --git a/osu.Game/Configuration/SettingSourceAttribute.cs b/osu.Game/Configuration/SettingSourceAttribute.cs index 1e425c88a6..3ba46144ca 100644 --- a/osu.Game/Configuration/SettingSourceAttribute.cs +++ b/osu.Game/Configuration/SettingSourceAttribute.cs @@ -186,6 +186,16 @@ namespace osu.Game.Configuration break; + case BindableColour4 bColour: + yield return new SettingsColour + { + LabelText = attr.Label, + TooltipText = attr.Description, + Current = bColour + }; + + break; + case IBindable bindable: var dropdownType = typeof(ModSettingsEnumDropdown<>).MakeGenericType(bindable.GetType().GetGenericArguments()[0]); var dropdown = (Drawable)Activator.CreateInstance(dropdownType)!; @@ -227,6 +237,9 @@ namespace osu.Game.Configuration case Bindable b: return b.Value; + case BindableColour4 c: + return c.Value.ToHex(); + case IBindable u: // An unknown (e.g. enum) generic type. var valueMethod = u.GetType().GetProperty(nameof(IBindable.Value)); diff --git a/osu.Game/Overlays/Settings/SettingsColour.cs b/osu.Game/Overlays/Settings/SettingsColour.cs new file mode 100644 index 0000000000..a58c20adea --- /dev/null +++ b/osu.Game/Overlays/Settings/SettingsColour.cs @@ -0,0 +1,79 @@ +// 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 osu.Framework.Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Cursor; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.UserInterface; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterfaceV2; + +namespace osu.Game.Overlays.Settings +{ + public partial class SettingsColour : SettingsItem + { + protected override Drawable CreateControl() => new ColourControl(); + + public partial class ColourControl : OsuClickableContainer, IHasPopover, IHasCurrentValue + { + private readonly BindableWithCurrent current = new BindableWithCurrent(); + + public Bindable Current + { + get => current.Current; + set => current.Current = value; + } + + private readonly Box fill; + private readonly OsuSpriteText colourHexCode; + + public ColourControl() + { + RelativeSizeAxes = Axes.X; + Height = 40; + CornerRadius = 20; + Masking = true; + Action = this.ShowPopover; + + Children = new Drawable[] + { + fill = new Box + { + RelativeSizeAxes = Axes.Both + }, + colourHexCode = new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Font = OsuFont.Default.With(size: 20) + } + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + Current.BindValueChanged(_ => updateColour(), true); + } + + private void updateColour() + { + fill.Colour = Current.Value; + colourHexCode.Text = Current.Value.ToHex(); + colourHexCode.Colour = OsuColour.ForegroundTextColourFor(Current.Value); + } + + public Popover GetPopover() => new OsuPopover(false) + { + Child = new OsuColourPicker + { + Current = { BindTarget = Current } + } + }; + } + } +} diff --git a/osu.Game/Overlays/SkinEditor/SkinEditor.cs b/osu.Game/Overlays/SkinEditor/SkinEditor.cs index d1e9676de7..b8e859bd63 100644 --- a/osu.Game/Overlays/SkinEditor/SkinEditor.cs +++ b/osu.Game/Overlays/SkinEditor/SkinEditor.cs @@ -33,6 +33,7 @@ using osu.Game.Screens.Edit; using osu.Game.Screens.Edit.Components; using osu.Game.Screens.Edit.Components.Menus; using osu.Game.Skinning; +using osu.Framework.Graphics.Cursor; namespace osu.Game.Overlays.SkinEditor { @@ -117,6 +118,9 @@ namespace osu.Game.Overlays.SkinEditor InternalChild = new OsuContextMenuContainer { + RelativeSizeAxes = Axes.Both, + Child = new PopoverContainer + { RelativeSizeAxes = Axes.Both, Child = new GridContainer { @@ -221,6 +225,7 @@ namespace osu.Game.Overlays.SkinEditor }, } } + } }; clipboardContent = clipboard.Content.GetBoundCopy(); From 6ec3f715d2a796413ab4e29b11d76413356725d8 Mon Sep 17 00:00:00 2001 From: PowerDaniex <140076282+u4vh3@users.noreply.github.com> Date: Wed, 10 Jul 2024 18:39:10 +0200 Subject: [PATCH 2331/2493] Fix formatting --- osu.Game/Overlays/SkinEditor/SkinEditor.cs | 182 ++++++++++----------- 1 file changed, 91 insertions(+), 91 deletions(-) diff --git a/osu.Game/Overlays/SkinEditor/SkinEditor.cs b/osu.Game/Overlays/SkinEditor/SkinEditor.cs index b8e859bd63..6f7781ee9c 100644 --- a/osu.Game/Overlays/SkinEditor/SkinEditor.cs +++ b/osu.Game/Overlays/SkinEditor/SkinEditor.cs @@ -120,112 +120,112 @@ namespace osu.Game.Overlays.SkinEditor { RelativeSizeAxes = Axes.Both, Child = new PopoverContainer - { - RelativeSizeAxes = Axes.Both, - Child = new GridContainer { RelativeSizeAxes = Axes.Both, - RowDimensions = new[] + Child = new GridContainer { - new Dimension(GridSizeMode.AutoSize), - new Dimension(GridSizeMode.AutoSize), - new Dimension(), - }, + RelativeSizeAxes = Axes.Both, + RowDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize), + new Dimension(GridSizeMode.AutoSize), + new Dimension(), + }, - Content = new[] - { - new Drawable[] + Content = new[] { - new Container + new Drawable[] { - Name = @"Menu container", - RelativeSizeAxes = Axes.X, - Depth = float.MinValue, - Height = MENU_HEIGHT, - Children = new Drawable[] + new Container { - new EditorMenuBar + Name = @"Menu container", + RelativeSizeAxes = Axes.X, + Depth = float.MinValue, + Height = MENU_HEIGHT, + Children = new Drawable[] { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - RelativeSizeAxes = Axes.Both, - Items = new[] + new EditorMenuBar { - new MenuItem(CommonStrings.MenuBarFile) - { - Items = new OsuMenuItem[] - { - new EditorMenuItem(Web.CommonStrings.ButtonsSave, MenuItemType.Standard, () => Save()), - new EditorMenuItem(CommonStrings.Export, MenuItemType.Standard, () => skins.ExportCurrentSkin()) { Action = { Disabled = !RuntimeInfo.IsDesktop } }, - new OsuMenuItemSpacer(), - new EditorMenuItem(CommonStrings.RevertToDefault, MenuItemType.Destructive, () => dialogOverlay?.Push(new RevertConfirmDialog(revert))), - new OsuMenuItemSpacer(), - new EditorMenuItem(CommonStrings.Exit, MenuItemType.Standard, () => skinEditorOverlay?.Hide()), - }, - }, - new MenuItem(CommonStrings.MenuBarEdit) - { - Items = new OsuMenuItem[] - { - undoMenuItem = new EditorMenuItem(CommonStrings.Undo, MenuItemType.Standard, Undo), - redoMenuItem = new EditorMenuItem(CommonStrings.Redo, MenuItemType.Standard, Redo), - new OsuMenuItemSpacer(), - cutMenuItem = new EditorMenuItem(CommonStrings.Cut, MenuItemType.Standard, Cut), - copyMenuItem = new EditorMenuItem(CommonStrings.Copy, MenuItemType.Standard, Copy), - pasteMenuItem = new EditorMenuItem(CommonStrings.Paste, MenuItemType.Standard, Paste), - cloneMenuItem = new EditorMenuItem(CommonStrings.Clone, MenuItemType.Standard, Clone), - } - }, - } - }, - headerText = new OsuTextFlowContainer - { - TextAnchor = Anchor.TopRight, - Padding = new MarginPadding(5), - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - AutoSizeAxes = Axes.X, - RelativeSizeAxes = Axes.Y, - }, - }, - }, - }, - new Drawable[] - { - new SkinEditorSceneLibrary - { - RelativeSizeAxes = Axes.X, - }, - }, - new Drawable[] - { - new GridContainer - { - RelativeSizeAxes = Axes.Both, - ColumnDimensions = new[] - { - new Dimension(GridSizeMode.AutoSize), - new Dimension(), - new Dimension(GridSizeMode.AutoSize), - }, - Content = new[] - { - new Drawable[] - { - componentsSidebar = new EditorSidebar(), - content = new Container - { - Depth = float.MaxValue, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, RelativeSizeAxes = Axes.Both, + Items = new[] + { + new MenuItem(CommonStrings.MenuBarFile) + { + Items = new OsuMenuItem[] + { + new EditorMenuItem(Web.CommonStrings.ButtonsSave, MenuItemType.Standard, () => Save()), + new EditorMenuItem(CommonStrings.Export, MenuItemType.Standard, () => skins.ExportCurrentSkin()) { Action = { Disabled = !RuntimeInfo.IsDesktop } }, + new OsuMenuItemSpacer(), + new EditorMenuItem(CommonStrings.RevertToDefault, MenuItemType.Destructive, () => dialogOverlay?.Push(new RevertConfirmDialog(revert))), + new OsuMenuItemSpacer(), + new EditorMenuItem(CommonStrings.Exit, MenuItemType.Standard, () => skinEditorOverlay?.Hide()), + }, + }, + new MenuItem(CommonStrings.MenuBarEdit) + { + Items = new OsuMenuItem[] + { + undoMenuItem = new EditorMenuItem(CommonStrings.Undo, MenuItemType.Standard, Undo), + redoMenuItem = new EditorMenuItem(CommonStrings.Redo, MenuItemType.Standard, Redo), + new OsuMenuItemSpacer(), + cutMenuItem = new EditorMenuItem(CommonStrings.Cut, MenuItemType.Standard, Cut), + copyMenuItem = new EditorMenuItem(CommonStrings.Copy, MenuItemType.Standard, Copy), + pasteMenuItem = new EditorMenuItem(CommonStrings.Paste, MenuItemType.Standard, Paste), + cloneMenuItem = new EditorMenuItem(CommonStrings.Clone, MenuItemType.Standard, Clone), + } + }, + } }, - settingsSidebar = new EditorSidebar(), + headerText = new OsuTextFlowContainer + { + TextAnchor = Anchor.TopRight, + Padding = new MarginPadding(5), + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + AutoSizeAxes = Axes.X, + RelativeSizeAxes = Axes.Y, + }, + }, + }, + }, + new Drawable[] + { + new SkinEditorSceneLibrary + { + RelativeSizeAxes = Axes.X, + }, + }, + new Drawable[] + { + new GridContainer + { + RelativeSizeAxes = Axes.Both, + ColumnDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize), + new Dimension(), + new Dimension(GridSizeMode.AutoSize), + }, + Content = new[] + { + new Drawable[] + { + componentsSidebar = new EditorSidebar(), + content = new Container + { + Depth = float.MaxValue, + RelativeSizeAxes = Axes.Both, + }, + settingsSidebar = new EditorSidebar(), + } } } - } - }, + }, + } } } - } }; clipboardContent = clipboard.Content.GetBoundCopy(); From e81e356d59afc04d13e6d1738ef1c1fe8445879e Mon Sep 17 00:00:00 2001 From: Daniel Cios Date: Tue, 6 Aug 2024 11:06:08 +0200 Subject: [PATCH 2332/2493] Add colour customisation to skin components --- .../SkinnableComponentStrings.cs | 20 +++++++++++++++++++ .../Screens/Play/HUD/ArgonSongProgress.cs | 4 ++++ osu.Game/Screens/Play/HUD/ArgonWedgePiece.cs | 7 ++++++- .../Screens/Play/HUD/DefaultSongProgress.cs | 5 +++++ .../Components/BeatmapAttributeText.cs | 2 ++ osu.Game/Skinning/Components/BoxElement.cs | 4 ++++ osu.Game/Skinning/Components/PlayerName.cs | 2 ++ osu.Game/Skinning/Components/TextElement.cs | 2 ++ .../Skinning/FontAdjustableSkinComponent.cs | 8 ++++++++ 9 files changed, 53 insertions(+), 1 deletion(-) diff --git a/osu.Game/Localisation/SkinComponents/SkinnableComponentStrings.cs b/osu.Game/Localisation/SkinComponents/SkinnableComponentStrings.cs index d5c8d5ccec..bd22527f67 100644 --- a/osu.Game/Localisation/SkinComponents/SkinnableComponentStrings.cs +++ b/osu.Game/Localisation/SkinComponents/SkinnableComponentStrings.cs @@ -59,6 +59,26 @@ namespace osu.Game.Localisation.SkinComponents /// public static LocalisableString ShowLabelDescription => new TranslatableString(getKey(@"show_label_description"), @"Whether the component's label should be shown."); + /// + /// "Colour" + /// + public static LocalisableString Colour => new TranslatableString(getKey(@"colour"), @"Colour"); + + /// + /// "The colour of the component." + /// + public static LocalisableString ColourDescription => new TranslatableString(getKey(@"colour_description"), @"The colour of the component."); + + /// + /// "Font colour" + /// + public static LocalisableString FontColour => new TranslatableString(getKey(@"font_colour"), @"Font colour"); + + /// + /// "The colour of the font." + /// + public static LocalisableString FontColourDescription => new TranslatableString(getKey(@"font_colour_description"), @"The colour of the font."); + private static string getKey(string key) => $@"{prefix}:{key}"; } } diff --git a/osu.Game/Screens/Play/HUD/ArgonSongProgress.cs b/osu.Game/Screens/Play/HUD/ArgonSongProgress.cs index ebebfebfb3..696369921a 100644 --- a/osu.Game/Screens/Play/HUD/ArgonSongProgress.cs +++ b/osu.Game/Screens/Play/HUD/ArgonSongProgress.cs @@ -9,6 +9,7 @@ using osu.Framework.Graphics.Containers; using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Localisation.HUD; +using osu.Game.Localisation.SkinComponents; using osu.Game.Rulesets.Objects; namespace osu.Game.Screens.Play.HUD @@ -28,6 +29,8 @@ namespace osu.Game.Screens.Play.HUD [SettingSource(typeof(SongProgressStrings), nameof(SongProgressStrings.ShowTime), nameof(SongProgressStrings.ShowTimeDescription))] public Bindable ShowTime { get; } = new BindableBool(true); + [SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.Colour), nameof(SkinnableComponentStrings.ColourDescription))] + public new BindableColour4 Colour { get; } = new BindableColour4(Colour4.White); [Resolved] private Player? player { get; set; } @@ -114,6 +117,7 @@ namespace osu.Game.Screens.Play.HUD base.Update(); content.Height = bar.Height + bar_height + info.Height; graphContainer.Height = bar.Height; + base.Colour = Colour.Value; } protected override void UpdateProgress(double progress, bool isIntro) diff --git a/osu.Game/Screens/Play/HUD/ArgonWedgePiece.cs b/osu.Game/Screens/Play/HUD/ArgonWedgePiece.cs index 3c2e3e05ea..837e9547f0 100644 --- a/osu.Game/Screens/Play/HUD/ArgonWedgePiece.cs +++ b/osu.Game/Screens/Play/HUD/ArgonWedgePiece.cs @@ -9,6 +9,7 @@ using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Game.Configuration; +using osu.Game.Localisation.SkinComponents; using osu.Game.Skinning; using osuTK; @@ -21,6 +22,9 @@ namespace osu.Game.Screens.Play.HUD [SettingSource("Inverted shear")] public BindableBool InvertShear { get; } = new BindableBool(); + [SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.Colour), nameof(SkinnableComponentStrings.ColourDescription))] + public new BindableColour4 Colour { get; } = new BindableColour4(Color4Extensions.FromHex("#66CCFF")); + public ArgonWedgePiece() { CornerRadius = 10f; @@ -37,7 +41,7 @@ namespace osu.Game.Screens.Play.HUD InternalChild = new Box { RelativeSizeAxes = Axes.Both, - Colour = ColourInfo.GradientVertical(Color4Extensions.FromHex("#66CCFF").Opacity(0.0f), Color4Extensions.FromHex("#66CCFF").Opacity(0.25f)), + Colour = ColourInfo.GradientVertical(Colour.Value.Opacity(0.0f), Colour.Value.Opacity(0.25f)), }; } @@ -46,6 +50,7 @@ namespace osu.Game.Screens.Play.HUD base.LoadComplete(); InvertShear.BindValueChanged(v => Shear = new Vector2(0.8f, 0f) * (v.NewValue ? -1 : 1), true); + Colour.BindValueChanged(c => InternalChild.Colour = ColourInfo.GradientVertical(Colour.Value.Opacity(0.0f), Colour.Value.Opacity(0.25f))); } } } diff --git a/osu.Game/Screens/Play/HUD/DefaultSongProgress.cs b/osu.Game/Screens/Play/HUD/DefaultSongProgress.cs index 6b2bb2b718..512edd7106 100644 --- a/osu.Game/Screens/Play/HUD/DefaultSongProgress.cs +++ b/osu.Game/Screens/Play/HUD/DefaultSongProgress.cs @@ -10,6 +10,7 @@ using osu.Framework.Utils; using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Localisation.HUD; +using osu.Game.Localisation.SkinComponents; using osu.Game.Rulesets.Objects; using osuTK; @@ -35,6 +36,8 @@ namespace osu.Game.Screens.Play.HUD [SettingSource(typeof(SongProgressStrings), nameof(SongProgressStrings.ShowTime), nameof(SongProgressStrings.ShowTimeDescription))] public Bindable ShowTime { get; } = new BindableBool(true); + [SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.Colour), nameof(SkinnableComponentStrings.ColourDescription))] + public new BindableColour4 Colour { get; } = new BindableColour4(Colour4.White); [Resolved] private Player? player { get; set; } @@ -114,6 +117,8 @@ namespace osu.Game.Screens.Play.HUD if (!Precision.AlmostEquals(Height, newHeight, 5f)) content.Height = newHeight; + + base.Colour = Colour.Value; } private void updateBarVisibility() diff --git a/osu.Game/Skinning/Components/BeatmapAttributeText.cs b/osu.Game/Skinning/Components/BeatmapAttributeText.cs index c467b2e946..06f0d9cea9 100644 --- a/osu.Game/Skinning/Components/BeatmapAttributeText.cs +++ b/osu.Game/Skinning/Components/BeatmapAttributeText.cs @@ -123,6 +123,8 @@ namespace osu.Game.Skinning.Components } protected override void SetFont(FontUsage font) => text.Font = font.With(size: 40); + + protected override void SetFontColour(Colour4 fontColour) => text.Colour = fontColour; } // WARNING: DO NOT ADD ANY VALUES TO THIS ENUM ANYWHERE ELSE THAN AT THE END. diff --git a/osu.Game/Skinning/Components/BoxElement.cs b/osu.Game/Skinning/Components/BoxElement.cs index 34d389728c..e49ec0cc4d 100644 --- a/osu.Game/Skinning/Components/BoxElement.cs +++ b/osu.Game/Skinning/Components/BoxElement.cs @@ -27,6 +27,9 @@ namespace osu.Game.Skinning.Components Precision = 0.01f }; + [SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.Colour), nameof(SkinnableComponentStrings.ColourDescription))] + public new BindableColour4 Colour { get; } = new BindableColour4(Colour4.White); + public BoxElement() { Size = new Vector2(400, 80); @@ -48,6 +51,7 @@ namespace osu.Game.Skinning.Components base.Update(); base.CornerRadius = CornerRadius.Value * Math.Min(DrawWidth, DrawHeight); + base.Colour = Colour.Value; } } } diff --git a/osu.Game/Skinning/Components/PlayerName.cs b/osu.Game/Skinning/Components/PlayerName.cs index 21bf615bc6..70672a1f58 100644 --- a/osu.Game/Skinning/Components/PlayerName.cs +++ b/osu.Game/Skinning/Components/PlayerName.cs @@ -53,5 +53,7 @@ namespace osu.Game.Skinning.Components } protected override void SetFont(FontUsage font) => text.Font = font.With(size: 40); + + protected override void SetFontColour(Colour4 fontColour) => text.Colour = fontColour; } } diff --git a/osu.Game/Skinning/Components/TextElement.cs b/osu.Game/Skinning/Components/TextElement.cs index 936f6a529b..9d66c58ae8 100644 --- a/osu.Game/Skinning/Components/TextElement.cs +++ b/osu.Game/Skinning/Components/TextElement.cs @@ -36,5 +36,7 @@ namespace osu.Game.Skinning.Components } protected override void SetFont(FontUsage font) => text.Font = font.With(size: 40); + + protected override void SetFontColour(Colour4 fontColour) => text.Colour = fontColour; } } diff --git a/osu.Game/Skinning/FontAdjustableSkinComponent.cs b/osu.Game/Skinning/FontAdjustableSkinComponent.cs index 8f3a1d41c6..e3052aee5c 100644 --- a/osu.Game/Skinning/FontAdjustableSkinComponent.cs +++ b/osu.Game/Skinning/FontAdjustableSkinComponent.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Bindables; +using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; using osu.Game.Configuration; @@ -20,11 +21,16 @@ namespace osu.Game.Skinning [SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.Font), nameof(SkinnableComponentStrings.FontDescription))] public Bindable Font { get; } = new Bindable(Typeface.Torus); + [SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.FontColour), nameof(SkinnableComponentStrings.FontColourDescription))] + public BindableColour4 FontColour { get; } = new BindableColour4(Colour4.White); + /// /// Implement to apply the user font selection to one or more components. /// protected abstract void SetFont(FontUsage font); + protected abstract void SetFontColour(Colour4 fontColour); + protected override void LoadComplete() { base.LoadComplete(); @@ -37,6 +43,8 @@ namespace osu.Game.Skinning FontUsage f = OsuFont.GetFont(e.NewValue, weight: fontWeight); SetFont(f); }, true); + + FontColour.BindValueChanged(e => SetFontColour(e.NewValue), true); } } } From 67f04f75a6c7a05c3135ea8328747669d4237624 Mon Sep 17 00:00:00 2001 From: Daniel Cios Date: Thu, 19 Sep 2024 15:26:27 +0200 Subject: [PATCH 2333/2493] Fix default color --- osu.Game/Overlays/Settings/SettingsColour.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Settings/SettingsColour.cs b/osu.Game/Overlays/Settings/SettingsColour.cs index a58c20adea..db248331d3 100644 --- a/osu.Game/Overlays/Settings/SettingsColour.cs +++ b/osu.Game/Overlays/Settings/SettingsColour.cs @@ -19,7 +19,7 @@ namespace osu.Game.Overlays.Settings public partial class ColourControl : OsuClickableContainer, IHasPopover, IHasCurrentValue { - private readonly BindableWithCurrent current = new BindableWithCurrent(); + private readonly BindableWithCurrent current = new BindableWithCurrent(Colour4.White); public Bindable Current { From c77afe2a132e22162be34d3703076c2f57a63f34 Mon Sep 17 00:00:00 2001 From: Daniel Cios Date: Thu, 19 Sep 2024 16:04:42 +0200 Subject: [PATCH 2334/2493] Add tests --- .../Settings/TestSceneSettingsSource.cs | 28 +++++-- .../UserInterface/TestSceneSettingsColour.cs | 75 +++++++++++++++++++ 2 files changed, 95 insertions(+), 8 deletions(-) create mode 100644 osu.Game.Tests/Visual/UserInterface/TestSceneSettingsColour.cs diff --git a/osu.Game.Tests/Visual/Settings/TestSceneSettingsSource.cs b/osu.Game.Tests/Visual/Settings/TestSceneSettingsSource.cs index 309438e51c..f589a3baa1 100644 --- a/osu.Game.Tests/Visual/Settings/TestSceneSettingsSource.cs +++ b/osu.Game.Tests/Visual/Settings/TestSceneSettingsSource.cs @@ -5,6 +5,7 @@ using NUnit.Framework; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; using osu.Framework.Localisation; using osu.Game.Configuration; using osu.Game.Overlays.Settings; @@ -19,16 +20,20 @@ namespace osu.Game.Tests.Visual.Settings { Children = new Drawable[] { - new FillFlowContainer + new PopoverContainer() { RelativeSizeAxes = Axes.Both, - Direction = FillDirection.Vertical, - Spacing = new Vector2(20), - Width = 0.5f, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Padding = new MarginPadding(50), - ChildrenEnumerable = new TestTargetClass().CreateSettingsControls() + Child = new FillFlowContainer + { + RelativeSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Spacing = new Vector2(20), + Width = 0.5f, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Padding = new MarginPadding(50), + ChildrenEnumerable = new TestTargetClass().CreateSettingsControls() + }, }, }; } @@ -66,6 +71,13 @@ namespace osu.Game.Tests.Visual.Settings [SettingSource("Sample number textbox", "Textbox number entry", SettingControlType = typeof(SettingsNumberBox))] public Bindable IntTextBoxBindable { get; } = new Bindable(); + + [SettingSource("Sample colour", "Change the colour", SettingControlType = typeof(SettingsColour))] + public BindableColour4 ColourBindable { get; } = new BindableColour4() + { + Default = Colour4.White, + Value = Colour4.Red + }; } private enum TestEnum diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneSettingsColour.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneSettingsColour.cs new file mode 100644 index 0000000000..d3de5a8319 --- /dev/null +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneSettingsColour.cs @@ -0,0 +1,75 @@ +// 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.Diagnostics.CodeAnalysis; +using System.Linq; +using NUnit.Framework; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; +using osu.Framework.Testing; +using osu.Framework.Utils; +using osu.Game.Graphics.UserInterfaceV2; +using osu.Game.Overlays.Settings; +using osuTK.Graphics; +using osuTK.Input; + +namespace osu.Game.Tests.Visual.UserInterface +{ + public partial class TestSceneSettingsColour : OsuManualInputManagerTestScene + { + private SettingsColour component; + + [Test] + public void TestColour() + { + createContent(); + + AddRepeatStep("set random colour", () => component.Current.Value = randomColour(), 4); + } + + [Test] + public void TestUserInteractions() + { + createContent(); + + AddStep("click colour", () => + { + InputManager.MoveMouseTo(component); + InputManager.Click(MouseButton.Left); + }); + + AddAssert("colour picker spawned", () => this.ChildrenOfType().Any()); + } + + private void createContent() + { + Child = new PopoverContainer + { + RelativeSizeAxes = Axes.Both, + Child = new FillFlowContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Width = 500, + AutoSizeAxes = Axes.Y, + Children = new Drawable[] + { + component = new SettingsColour + { + LabelText = "a sample component", + }, + }, + }, + }; + } + + private Colour4 randomColour() => new Color4( + RNG.NextSingle(), + RNG.NextSingle(), + RNG.NextSingle(), + 1); + } +} From 94c2f522ffa735bac22d91a07e0a92d04d9d5bff Mon Sep 17 00:00:00 2001 From: Daniel Cios Date: Thu, 19 Sep 2024 17:31:33 +0200 Subject: [PATCH 2335/2493] Fix spacing --- osu.Game/Screens/Play/HUD/ArgonSongProgress.cs | 1 + osu.Game/Screens/Play/HUD/DefaultSongProgress.cs | 1 + 2 files changed, 2 insertions(+) diff --git a/osu.Game/Screens/Play/HUD/ArgonSongProgress.cs b/osu.Game/Screens/Play/HUD/ArgonSongProgress.cs index 696369921a..3a4dc42484 100644 --- a/osu.Game/Screens/Play/HUD/ArgonSongProgress.cs +++ b/osu.Game/Screens/Play/HUD/ArgonSongProgress.cs @@ -29,6 +29,7 @@ namespace osu.Game.Screens.Play.HUD [SettingSource(typeof(SongProgressStrings), nameof(SongProgressStrings.ShowTime), nameof(SongProgressStrings.ShowTimeDescription))] public Bindable ShowTime { get; } = new BindableBool(true); + [SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.Colour), nameof(SkinnableComponentStrings.ColourDescription))] public new BindableColour4 Colour { get; } = new BindableColour4(Colour4.White); diff --git a/osu.Game/Screens/Play/HUD/DefaultSongProgress.cs b/osu.Game/Screens/Play/HUD/DefaultSongProgress.cs index 512edd7106..25d3c5588d 100644 --- a/osu.Game/Screens/Play/HUD/DefaultSongProgress.cs +++ b/osu.Game/Screens/Play/HUD/DefaultSongProgress.cs @@ -36,6 +36,7 @@ namespace osu.Game.Screens.Play.HUD [SettingSource(typeof(SongProgressStrings), nameof(SongProgressStrings.ShowTime), nameof(SongProgressStrings.ShowTimeDescription))] public Bindable ShowTime { get; } = new BindableBool(true); + [SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.Colour), nameof(SkinnableComponentStrings.ColourDescription))] public new BindableColour4 Colour { get; } = new BindableColour4(Colour4.White); From b86f246095fb79b9b67df4b0ec3b16155e997dd7 Mon Sep 17 00:00:00 2001 From: Daniel Cios Date: Thu, 19 Sep 2024 19:24:05 +0200 Subject: [PATCH 2336/2493] Fix code inspection failure --- osu.Game.Tests/Visual/Settings/TestSceneSettingsSource.cs | 4 ++-- .../Visual/UserInterface/TestSceneSettingsColour.cs | 1 - osu.Game/Overlays/Settings/SettingsColour.cs | 1 + 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Settings/TestSceneSettingsSource.cs b/osu.Game.Tests/Visual/Settings/TestSceneSettingsSource.cs index f589a3baa1..9544f77940 100644 --- a/osu.Game.Tests/Visual/Settings/TestSceneSettingsSource.cs +++ b/osu.Game.Tests/Visual/Settings/TestSceneSettingsSource.cs @@ -20,7 +20,7 @@ namespace osu.Game.Tests.Visual.Settings { Children = new Drawable[] { - new PopoverContainer() + new PopoverContainer { RelativeSizeAxes = Axes.Both, Child = new FillFlowContainer @@ -73,7 +73,7 @@ namespace osu.Game.Tests.Visual.Settings public Bindable IntTextBoxBindable { get; } = new Bindable(); [SettingSource("Sample colour", "Change the colour", SettingControlType = typeof(SettingsColour))] - public BindableColour4 ColourBindable { get; } = new BindableColour4() + public BindableColour4 ColourBindable { get; } = new BindableColour4 { Default = Colour4.White, Value = Colour4.Red diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneSettingsColour.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneSettingsColour.cs index d3de5a8319..75ddacc110 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneSettingsColour.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneSettingsColour.cs @@ -3,7 +3,6 @@ #nullable disable -using System.Diagnostics.CodeAnalysis; using System.Linq; using NUnit.Framework; using osu.Framework.Graphics; diff --git a/osu.Game/Overlays/Settings/SettingsColour.cs b/osu.Game/Overlays/Settings/SettingsColour.cs index db248331d3..7a091f1a54 100644 --- a/osu.Game/Overlays/Settings/SettingsColour.cs +++ b/osu.Game/Overlays/Settings/SettingsColour.cs @@ -1,5 +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 osu.Framework.Bindables; using osu.Framework.Extensions; using osu.Framework.Graphics; From 1a48b46536537eb050dc888b61c59e6c6e1eb851 Mon Sep 17 00:00:00 2001 From: Daniel Cios Date: Thu, 19 Sep 2024 21:50:59 +0200 Subject: [PATCH 2337/2493] Fix test failures --- .../UserInterface/TestSceneSettingsColour.cs | 27 ++++++++++--------- 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneSettingsColour.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneSettingsColour.cs index 75ddacc110..6bed5f91c5 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneSettingsColour.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneSettingsColour.cs @@ -45,24 +45,27 @@ namespace osu.Game.Tests.Visual.UserInterface private void createContent() { - Child = new PopoverContainer + AddStep("create component", () => { - RelativeSizeAxes = Axes.Both, - Child = new FillFlowContainer + Child = new PopoverContainer { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Width = 500, - AutoSizeAxes = Axes.Y, - Children = new Drawable[] + RelativeSizeAxes = Axes.Both, + Child = new FillFlowContainer { - component = new SettingsColour + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Width = 500, + AutoSizeAxes = Axes.Y, + Children = new Drawable[] { - LabelText = "a sample component", + component = new SettingsColour + { + LabelText = "a sample component", + }, }, }, - }, - }; + }; + }); } private Colour4 randomColour() => new Color4( From 59ab71f786f13e258479769f21065adfcaadd234 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Fri, 20 Sep 2024 01:06:52 +0200 Subject: [PATCH 2338/2493] Implement minimum enclosing circle --- osu.Game/Utils/GeometryUtils.cs | 133 ++++++++++++++++++++++++++++++++ 1 file changed, 133 insertions(+) diff --git a/osu.Game/Utils/GeometryUtils.cs b/osu.Game/Utils/GeometryUtils.cs index 8572ac6609..7e6db10a28 100644 --- a/osu.Game/Utils/GeometryUtils.cs +++ b/osu.Game/Utils/GeometryUtils.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Linq; using osu.Framework.Graphics; using osu.Framework.Graphics.Primitives; +using osu.Framework.Utils; using osu.Game.Rulesets.Objects.Types; using osuTK; @@ -218,5 +219,137 @@ namespace osu.Game.Utils return new[] { h.Position }; }); + + #region welzl_helpers + + // Function to check whether a point lies inside or on the boundaries of the circle + private static bool isInside((Vector2, float) c, Vector2 p) + { + return Precision.AlmostBigger(c.Item2, Vector2.Distance(c.Item1, p)); + } + + // Function to return a unique circle that intersects three points + private static (Vector2, float) circleFrom(Vector2 a, Vector2 b, Vector2 c) + { + if (Precision.AlmostEquals(0, (b.Y - a.Y) * (c.X - a.X) - (b.X - a.X) * (c.Y - a.Y))) + return circleFrom(a, b); + + // See: https://en.wikipedia.org/wiki/Circumscribed_circle#Cartesian_coordinates_2 + float d = 2 * (a.X * (b - c).Y + b.X * (c - a).Y + c.X * (a - b).Y); + float aSq = a.LengthSquared; + float bSq = b.LengthSquared; + float cSq = c.LengthSquared; + + var centre = new Vector2( + aSq * (b - c).Y + bSq * (c - a).Y + cSq * (a - b).Y, + aSq * (c - b).X + bSq * (a - c).X + cSq * (b - a).X) / d; + + return (centre, Vector2.Distance(a, centre)); + } + + // Function to return the smallest circle that intersects 2 points + private static (Vector2, float) circleFrom(Vector2 a, Vector2 b) + { + var centre = (a + b) / 2.0f; + return (centre, Vector2.Distance(a, b) / 2.0f); + } + + // Function to check whether a circle encloses the given points + private static bool isValidCircle((Vector2, float) c, ReadOnlySpan points) + { + // Iterating through all the points to check whether the points lie inside the circle or not + foreach (Vector2 p in points) + { + if (!isInside(c, p)) return false; + } + + return true; + } + + // Function to return the minimum enclosing circle for N <= 3 + private static (Vector2, float) minCircleTrivial(ReadOnlySpan points) + { + switch (points.Length) + { + case 0: + return (new Vector2(0, 0), 0); + + case 1: + return (points[0], 0); + + case 2: + return circleFrom(points[0], points[1]); + } + + // To check if MEC can be determined by 2 points only + for (int i = 0; i < 3; i++) + { + for (int j = i + 1; j < 3; j++) + { + var c = circleFrom(points[i], points[j]); + + if (isValidCircle(c, points)) + return c; + } + } + + return circleFrom(points[0], points[1], points[2]); + } + + // Returns the MEC using Welzl's algorithm + // Takes a set of input points P and a set R + // points on the circle boundary. + // n represents the number of points in P that are not yet processed. + private static (Vector2, float) welzlHelper(List points, ReadOnlySpan r, int n, Random random) + { + // Base case when all points processed or |R| = 3 + if (n == 0 || r.Length == 3) + return minCircleTrivial(r); + + // Pick a random point randomly + int idx = random.Next(n); + Vector2 p = points[idx]; + + // Put the picked point at the end of P since it's more efficient than + // deleting from the middle of the list + (points[idx], points[n - 1]) = (points[n - 1], points[idx]); + + // Get the MEC circle d from the set of points P - {p} + var d = welzlHelper(points, r, n - 1, random); + + // If d contains p, return d + if (isInside(d, p)) + return d; + + // Otherwise, must be on the boundary of the MEC + // Stackalloc to avoid allocations. It's safe to assume that the length of r will be at most 3 + Span r2 = stackalloc Vector2[r.Length + 1]; + r.CopyTo(r2); + r2[r.Length] = p; + + // Return the MEC for P - {p} and R U {p} + return welzlHelper(points, r2, n - 1, random); + } + + #endregion + + /// + /// Function to find the minimum enclosing circle for a collection of points. + /// + /// A tuple containing the circle center and radius. + public static (Vector2, float) MinimumEnclosingCircle(IEnumerable points) + { + // Using Welzl's algorithm to find the minimum enclosing circle + // https://www.geeksforgeeks.org/minimum-enclosing-circle-using-welzls-algorithm/ + List pCopy = points.ToList(); + return welzlHelper(pCopy, Array.Empty(), pCopy.Count, new Random()); + } + + /// + /// Function to find the minimum enclosing circle for a collection of hit objects. + /// + /// A tuple containing the circle center and radius. + public static (Vector2, float) MinimumEnclosingCircle(IEnumerable hitObjects) => + MinimumEnclosingCircle(enumerateStartAndEndPositions(hitObjects)); } } From ee006247516569cb9fdd380d66612f21290d48ee Mon Sep 17 00:00:00 2001 From: OliBomby Date: Fri, 20 Sep 2024 01:07:47 +0200 Subject: [PATCH 2339/2493] use minimum enclosing circle selection centre in rotation --- .../Edit/OsuSelectionRotationHandler.cs | 9 ++++----- .../SkinEditor/SkinSelectionRotationHandler.cs | 9 ++++----- .../Compose/Components/SelectionBoxRotationHandle.cs | 10 +++++++--- .../Compose/Components/SelectionRotationHandler.cs | 6 ++++++ 4 files changed, 21 insertions(+), 13 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionRotationHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionRotationHandler.cs index 62a39d3702..44d1543ae4 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionRotationHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionRotationHandler.cs @@ -47,7 +47,6 @@ namespace osu.Game.Rulesets.Osu.Edit private OsuHitObject[]? objectsInRotation; - private Vector2? defaultOrigin; private Dictionary? originalPositions; private Dictionary? originalPathControlPointPositions; @@ -61,7 +60,7 @@ namespace osu.Game.Rulesets.Osu.Edit changeHandler?.BeginChange(); objectsInRotation = selectedMovableObjects.ToArray(); - defaultOrigin = GeometryUtils.GetSurroundingQuad(objectsInRotation).Centre; + DefaultOrigin = GeometryUtils.MinimumEnclosingCircle(objectsInRotation).Item1; originalPositions = objectsInRotation.ToDictionary(obj => obj, obj => obj.Position); originalPathControlPointPositions = objectsInRotation.OfType().ToDictionary( obj => obj, @@ -73,9 +72,9 @@ namespace osu.Game.Rulesets.Osu.Edit if (!OperationInProgress.Value) throw new InvalidOperationException($"Cannot {nameof(Update)} a rotate operation without calling {nameof(Begin)} first!"); - Debug.Assert(objectsInRotation != null && originalPositions != null && originalPathControlPointPositions != null && defaultOrigin != null); + Debug.Assert(objectsInRotation != null && originalPositions != null && originalPathControlPointPositions != null && DefaultOrigin != null); - Vector2 actualOrigin = origin ?? defaultOrigin.Value; + Vector2 actualOrigin = origin ?? DefaultOrigin.Value; foreach (var ho in objectsInRotation) { @@ -103,7 +102,7 @@ namespace osu.Game.Rulesets.Osu.Edit objectsInRotation = null; originalPositions = null; originalPathControlPointPositions = null; - defaultOrigin = null; + DefaultOrigin = null; } private IEnumerable selectedMovableObjects => selectedItems.Cast() diff --git a/osu.Game/Overlays/SkinEditor/SkinSelectionRotationHandler.cs b/osu.Game/Overlays/SkinEditor/SkinSelectionRotationHandler.cs index 36b38543d1..9fd28a1cad 100644 --- a/osu.Game/Overlays/SkinEditor/SkinSelectionRotationHandler.cs +++ b/osu.Game/Overlays/SkinEditor/SkinSelectionRotationHandler.cs @@ -46,7 +46,6 @@ namespace osu.Game.Overlays.SkinEditor private Drawable[]? objectsInRotation; - private Vector2? defaultOrigin; private Dictionary? originalRotations; private Dictionary? originalPositions; @@ -60,7 +59,7 @@ namespace osu.Game.Overlays.SkinEditor 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; + DefaultOrigin = GeometryUtils.GetSurroundingQuad(objectsInRotation.SelectMany(d => d.ScreenSpaceDrawQuad.GetVertices().ToArray())).Centre; base.Begin(); } @@ -70,7 +69,7 @@ namespace osu.Game.Overlays.SkinEditor 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); + Debug.Assert(originalRotations != null && originalPositions != null && DefaultOrigin != null); if (objectsInRotation.Length == 1 && origin == null) { @@ -79,7 +78,7 @@ namespace osu.Game.Overlays.SkinEditor return; } - var actualOrigin = origin ?? defaultOrigin.Value; + var actualOrigin = origin ?? DefaultOrigin.Value; foreach (var drawableItem in objectsInRotation) { @@ -100,7 +99,7 @@ namespace osu.Game.Overlays.SkinEditor objectsInRotation = null; originalPositions = null; originalRotations = null; - defaultOrigin = null; + DefaultOrigin = null; base.Commit(); } diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionBoxRotationHandle.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionBoxRotationHandle.cs index c62e0e0d41..898efc8b5e 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionBoxRotationHandle.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionBoxRotationHandle.cs @@ -113,9 +113,13 @@ namespace osu.Game.Screens.Edit.Compose.Components private float convertDragEventToAngleOfRotation(DragEvent e) { - // Adjust coordinate system to the center of SelectionBox - float startAngle = MathF.Atan2(e.LastMousePosition.Y - selectionBox.DrawHeight / 2, e.LastMousePosition.X - selectionBox.DrawWidth / 2); - float endAngle = MathF.Atan2(e.MousePosition.Y - selectionBox.DrawHeight / 2, e.MousePosition.X - selectionBox.DrawWidth / 2); + // Adjust coordinate system to the center of the selection + Vector2 center = rotationHandler?.DefaultOrigin is not null + ? selectionBox.ToLocalSpace(rotationHandler.ToScreenSpace(rotationHandler.DefaultOrigin.Value)) + : selectionBox.DrawSize / 2; + + float startAngle = MathF.Atan2(e.LastMousePosition.Y - center.Y, e.LastMousePosition.X - center.X); + float endAngle = MathF.Atan2(e.MousePosition.Y - center.Y, e.MousePosition.X - center.X); return (endAngle - startAngle) * 180 / MathF.PI; } diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionRotationHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionRotationHandler.cs index 532daaf7fa..680acad114 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionRotationHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionRotationHandler.cs @@ -27,6 +27,12 @@ namespace osu.Game.Screens.Edit.Compose.Components /// public Bindable CanRotateAroundPlayfieldOrigin { get; private set; } = new BindableBool(); + /// + /// Implementation-defined origin point to rotate around when no explicit origin is provided. + /// This field is only assigned during a rotation operation. + /// + public Vector2? DefaultOrigin { get; protected set; } + /// /// Performs a single, instant, atomic rotation operation. /// From 8e11cda41a35919cfddf5b6e601686b4f549b335 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Fri, 20 Sep 2024 01:07:54 +0200 Subject: [PATCH 2340/2493] use minimum enclosing circle selection centre in scale --- osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs | 2 +- osu.Game/Overlays/SkinEditor/SkinSelectionScaleHandler.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs index 56c3ba9315..e9d5b3105a 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs @@ -84,10 +84,10 @@ namespace osu.Game.Rulesets.Osu.Edit OriginalSurroundingQuad = objectsInScale.Count == 1 && objectsInScale.First().Key is Slider slider ? GeometryUtils.GetSurroundingQuad(slider.Path.ControlPoints.Select(p => slider.Position + p.Position)) : GeometryUtils.GetSurroundingQuad(objectsInScale.Keys); - defaultOrigin = OriginalSurroundingQuad.Value.Centre; originalConvexHull = objectsInScale.Count == 1 && objectsInScale.First().Key is Slider slider2 ? GeometryUtils.GetConvexHull(slider2.Path.ControlPoints.Select(p => slider2.Position + p.Position)) : GeometryUtils.GetConvexHull(objectsInScale.Keys); + defaultOrigin = GeometryUtils.MinimumEnclosingCircle(originalConvexHull).Item1; } public override void Update(Vector2 scale, Vector2? origin = null, Axes adjustAxis = Axes.Both, float axisRotation = 0) diff --git a/osu.Game/Overlays/SkinEditor/SkinSelectionScaleHandler.cs b/osu.Game/Overlays/SkinEditor/SkinSelectionScaleHandler.cs index 977aaade99..6915769212 100644 --- a/osu.Game/Overlays/SkinEditor/SkinSelectionScaleHandler.cs +++ b/osu.Game/Overlays/SkinEditor/SkinSelectionScaleHandler.cs @@ -67,7 +67,7 @@ namespace osu.Game.Overlays.SkinEditor objectsInScale = selectedItems.Cast().ToDictionary(d => d, d => new OriginalDrawableState(d)); OriginalSurroundingQuad = ToLocalSpace(GeometryUtils.GetSurroundingQuad(objectsInScale.SelectMany(d => d.Key.ScreenSpaceDrawQuad.GetVertices().ToArray()))); - defaultOrigin = OriginalSurroundingQuad.Value.Centre; + defaultOrigin = ToLocalSpace(GeometryUtils.MinimumEnclosingCircle(objectsInScale.SelectMany(d => d.Key.ScreenSpaceDrawQuad.GetVertices().ToArray())).Item1); isFlippedX = false; isFlippedY = false; From ec575e9de4a8a5ffc87afe58ea954443a3aa0ba3 Mon Sep 17 00:00:00 2001 From: Daniel Cios Date: Fri, 20 Sep 2024 16:38:26 +0200 Subject: [PATCH 2341/2493] Rename Colour to AccentColour --- osu.Game/Screens/Play/HUD/ArgonSongProgress.cs | 4 ++-- osu.Game/Screens/Play/HUD/ArgonWedgePiece.cs | 6 +++--- osu.Game/Screens/Play/HUD/DefaultSongProgress.cs | 4 ++-- osu.Game/Skinning/Components/BoxElement.cs | 4 ++-- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/ArgonSongProgress.cs b/osu.Game/Screens/Play/HUD/ArgonSongProgress.cs index 3a4dc42484..1a18466743 100644 --- a/osu.Game/Screens/Play/HUD/ArgonSongProgress.cs +++ b/osu.Game/Screens/Play/HUD/ArgonSongProgress.cs @@ -31,7 +31,7 @@ namespace osu.Game.Screens.Play.HUD public Bindable ShowTime { get; } = new BindableBool(true); [SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.Colour), nameof(SkinnableComponentStrings.ColourDescription))] - public new BindableColour4 Colour { get; } = new BindableColour4(Colour4.White); + public BindableColour4 AccentColour { get; } = new BindableColour4(Colour4.White); [Resolved] private Player? player { get; set; } @@ -118,7 +118,7 @@ namespace osu.Game.Screens.Play.HUD base.Update(); content.Height = bar.Height + bar_height + info.Height; graphContainer.Height = bar.Height; - base.Colour = Colour.Value; + Colour = AccentColour.Value; } protected override void UpdateProgress(double progress, bool isIntro) diff --git a/osu.Game/Screens/Play/HUD/ArgonWedgePiece.cs b/osu.Game/Screens/Play/HUD/ArgonWedgePiece.cs index 837e9547f0..fb2e93b62b 100644 --- a/osu.Game/Screens/Play/HUD/ArgonWedgePiece.cs +++ b/osu.Game/Screens/Play/HUD/ArgonWedgePiece.cs @@ -23,7 +23,7 @@ namespace osu.Game.Screens.Play.HUD public BindableBool InvertShear { get; } = new BindableBool(); [SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.Colour), nameof(SkinnableComponentStrings.ColourDescription))] - public new BindableColour4 Colour { get; } = new BindableColour4(Color4Extensions.FromHex("#66CCFF")); + public BindableColour4 AccentColour { get; } = new BindableColour4(Color4Extensions.FromHex("#66CCFF")); public ArgonWedgePiece() { @@ -41,7 +41,7 @@ namespace osu.Game.Screens.Play.HUD InternalChild = new Box { RelativeSizeAxes = Axes.Both, - Colour = ColourInfo.GradientVertical(Colour.Value.Opacity(0.0f), Colour.Value.Opacity(0.25f)), + Colour = ColourInfo.GradientVertical(AccentColour.Value.Opacity(0.0f), AccentColour.Value.Opacity(0.25f)), }; } @@ -50,7 +50,7 @@ namespace osu.Game.Screens.Play.HUD base.LoadComplete(); InvertShear.BindValueChanged(v => Shear = new Vector2(0.8f, 0f) * (v.NewValue ? -1 : 1), true); - Colour.BindValueChanged(c => InternalChild.Colour = ColourInfo.GradientVertical(Colour.Value.Opacity(0.0f), Colour.Value.Opacity(0.25f))); + AccentColour.BindValueChanged(c => InternalChild.Colour = ColourInfo.GradientVertical(AccentColour.Value.Opacity(0.0f), AccentColour.Value.Opacity(0.25f))); } } } diff --git a/osu.Game/Screens/Play/HUD/DefaultSongProgress.cs b/osu.Game/Screens/Play/HUD/DefaultSongProgress.cs index 25d3c5588d..93d75a22ba 100644 --- a/osu.Game/Screens/Play/HUD/DefaultSongProgress.cs +++ b/osu.Game/Screens/Play/HUD/DefaultSongProgress.cs @@ -38,7 +38,7 @@ namespace osu.Game.Screens.Play.HUD public Bindable ShowTime { get; } = new BindableBool(true); [SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.Colour), nameof(SkinnableComponentStrings.ColourDescription))] - public new BindableColour4 Colour { get; } = new BindableColour4(Colour4.White); + public BindableColour4 AccentColour { get; } = new BindableColour4(Colour4.White); [Resolved] private Player? player { get; set; } @@ -119,7 +119,7 @@ namespace osu.Game.Screens.Play.HUD if (!Precision.AlmostEquals(Height, newHeight, 5f)) content.Height = newHeight; - base.Colour = Colour.Value; + Colour = AccentColour.Value; } private void updateBarVisibility() diff --git a/osu.Game/Skinning/Components/BoxElement.cs b/osu.Game/Skinning/Components/BoxElement.cs index e49ec0cc4d..633fb0c327 100644 --- a/osu.Game/Skinning/Components/BoxElement.cs +++ b/osu.Game/Skinning/Components/BoxElement.cs @@ -28,7 +28,7 @@ namespace osu.Game.Skinning.Components }; [SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.Colour), nameof(SkinnableComponentStrings.ColourDescription))] - public new BindableColour4 Colour { get; } = new BindableColour4(Colour4.White); + public BindableColour4 AccentColour { get; } = new BindableColour4(Colour4.White); public BoxElement() { @@ -51,7 +51,7 @@ namespace osu.Game.Skinning.Components base.Update(); base.CornerRadius = CornerRadius.Value * Math.Min(DrawWidth, DrawHeight); - base.Colour = Colour.Value; + Colour = AccentColour.Value; } } } From 73b6744a97ed3ca36db2c9ca99a1f451320f962c Mon Sep 17 00:00:00 2001 From: Daniel Cios Date: Fri, 20 Sep 2024 16:50:17 +0200 Subject: [PATCH 2342/2493] Rename FontColour to TextColour --- .../SkinComponents/SkinnableComponentStrings.cs | 8 ++++---- osu.Game/Skinning/Components/BeatmapAttributeText.cs | 2 +- osu.Game/Skinning/Components/PlayerName.cs | 2 +- osu.Game/Skinning/Components/TextElement.cs | 2 +- osu.Game/Skinning/FontAdjustableSkinComponent.cs | 8 ++++---- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/osu.Game/Localisation/SkinComponents/SkinnableComponentStrings.cs b/osu.Game/Localisation/SkinComponents/SkinnableComponentStrings.cs index bd22527f67..33fda23cb0 100644 --- a/osu.Game/Localisation/SkinComponents/SkinnableComponentStrings.cs +++ b/osu.Game/Localisation/SkinComponents/SkinnableComponentStrings.cs @@ -70,14 +70,14 @@ namespace osu.Game.Localisation.SkinComponents public static LocalisableString ColourDescription => new TranslatableString(getKey(@"colour_description"), @"The colour of the component."); /// - /// "Font colour" + /// "Text colour" /// - public static LocalisableString FontColour => new TranslatableString(getKey(@"font_colour"), @"Font colour"); + public static LocalisableString TextColour => new TranslatableString(getKey(@"text_colour"), @"Text colour"); /// - /// "The colour of the font." + /// "The colour of the text." /// - public static LocalisableString FontColourDescription => new TranslatableString(getKey(@"font_colour_description"), @"The colour of the font."); + public static LocalisableString TextColourDescription => new TranslatableString(getKey(@"text_colour_description"), @"The colour of the text."); private static string getKey(string key) => $@"{prefix}:{key}"; } diff --git a/osu.Game/Skinning/Components/BeatmapAttributeText.cs b/osu.Game/Skinning/Components/BeatmapAttributeText.cs index 06f0d9cea9..6e1d655cef 100644 --- a/osu.Game/Skinning/Components/BeatmapAttributeText.cs +++ b/osu.Game/Skinning/Components/BeatmapAttributeText.cs @@ -124,7 +124,7 @@ namespace osu.Game.Skinning.Components protected override void SetFont(FontUsage font) => text.Font = font.With(size: 40); - protected override void SetFontColour(Colour4 fontColour) => text.Colour = fontColour; + protected override void SetTextColour(Colour4 textColour) => text.Colour = textColour; } // WARNING: DO NOT ADD ANY VALUES TO THIS ENUM ANYWHERE ELSE THAN AT THE END. diff --git a/osu.Game/Skinning/Components/PlayerName.cs b/osu.Game/Skinning/Components/PlayerName.cs index 70672a1f58..5b6ded0cc5 100644 --- a/osu.Game/Skinning/Components/PlayerName.cs +++ b/osu.Game/Skinning/Components/PlayerName.cs @@ -54,6 +54,6 @@ namespace osu.Game.Skinning.Components protected override void SetFont(FontUsage font) => text.Font = font.With(size: 40); - protected override void SetFontColour(Colour4 fontColour) => text.Colour = fontColour; + protected override void SetTextColour(Colour4 textColour) => text.Colour = textColour; } } diff --git a/osu.Game/Skinning/Components/TextElement.cs b/osu.Game/Skinning/Components/TextElement.cs index 9d66c58ae8..6e875c5590 100644 --- a/osu.Game/Skinning/Components/TextElement.cs +++ b/osu.Game/Skinning/Components/TextElement.cs @@ -37,6 +37,6 @@ namespace osu.Game.Skinning.Components protected override void SetFont(FontUsage font) => text.Font = font.With(size: 40); - protected override void SetFontColour(Colour4 fontColour) => text.Colour = fontColour; + protected override void SetTextColour(Colour4 textColour) => text.Colour = textColour; } } diff --git a/osu.Game/Skinning/FontAdjustableSkinComponent.cs b/osu.Game/Skinning/FontAdjustableSkinComponent.cs index e3052aee5c..0821edf7fc 100644 --- a/osu.Game/Skinning/FontAdjustableSkinComponent.cs +++ b/osu.Game/Skinning/FontAdjustableSkinComponent.cs @@ -21,15 +21,15 @@ namespace osu.Game.Skinning [SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.Font), nameof(SkinnableComponentStrings.FontDescription))] public Bindable Font { get; } = new Bindable(Typeface.Torus); - [SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.FontColour), nameof(SkinnableComponentStrings.FontColourDescription))] - public BindableColour4 FontColour { get; } = new BindableColour4(Colour4.White); + [SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.TextColour), nameof(SkinnableComponentStrings.TextColourDescription))] + public BindableColour4 TextColour { get; } = new BindableColour4(Colour4.White); /// /// Implement to apply the user font selection to one or more components. /// protected abstract void SetFont(FontUsage font); - protected abstract void SetFontColour(Colour4 fontColour); + protected abstract void SetTextColour(Colour4 textColour); protected override void LoadComplete() { @@ -44,7 +44,7 @@ namespace osu.Game.Skinning SetFont(f); }, true); - FontColour.BindValueChanged(e => SetFontColour(e.NewValue), true); + TextColour.BindValueChanged(e => SetTextColour(e.NewValue), true); } } } From 8ceea9a5f7c80b6bc8d799380f9b70438f4dc65a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marvin=20Sch=C3=BCrz?= Date: Fri, 20 Sep 2024 17:19:38 +0200 Subject: [PATCH 2343/2493] Use scale origin when scaling sliders --- .../Edit/OsuSelectionScaleHandler.cs | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs index 56c3ba9315..ea16946dcb 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs @@ -105,9 +105,7 @@ namespace osu.Game.Rulesets.Osu.Edit // is not looking to change the duration of the slider but expand the whole pattern. if (objectsInScale.Count == 1 && objectsInScale.First().Key is Slider slider) { - var originalInfo = objectsInScale[slider]; - Debug.Assert(originalInfo.PathControlPointPositions != null && originalInfo.PathControlPointTypes != null); - scaleSlider(slider, scale, originalInfo.PathControlPointPositions, originalInfo.PathControlPointTypes, axisRotation); + scaleSlider(slider, scale, actualOrigin, objectsInScale[slider], axisRotation); } else { @@ -159,21 +157,25 @@ namespace osu.Game.Rulesets.Osu.Edit return scale; } - private void scaleSlider(Slider slider, Vector2 scale, Vector2[] originalPathPositions, PathType?[] originalPathTypes, float axisRotation = 0) + private void scaleSlider(Slider slider, Vector2 scale, Vector2 origin, OriginalHitObjectState originalInfo, float axisRotation = 0) { + Debug.Assert(originalInfo.PathControlPointPositions != null && originalInfo.PathControlPointTypes != null); + scale = Vector2.ComponentMax(scale, new Vector2(Precision.FLOAT_EPSILON)); // Maintain the path types in case they were defaulted to bezier at some point during scaling for (int i = 0; i < slider.Path.ControlPoints.Count; i++) { - slider.Path.ControlPoints[i].Position = GeometryUtils.GetScaledPosition(scale, Vector2.Zero, originalPathPositions[i], axisRotation); - slider.Path.ControlPoints[i].Type = originalPathTypes[i]; + slider.Path.ControlPoints[i].Position = GeometryUtils.GetScaledPosition(scale, Vector2.Zero, originalInfo.PathControlPointPositions[i], axisRotation); + slider.Path.ControlPoints[i].Type = originalInfo.PathControlPointTypes[i]; } // Snap the slider's length to the current beat divisor // to calculate the final resulting duration / bounding box before the final checks. slider.SnapTo(snapProvider); + slider.Position = GeometryUtils.GetScaledPosition(scale, origin, originalInfo.Position, axisRotation); + //if sliderhead or sliderend end up outside playfield, revert scaling. Quad scaledQuad = GeometryUtils.GetSurroundingQuad(new OsuHitObject[] { slider }); (bool xInBounds, bool yInBounds) = isQuadInBounds(scaledQuad); @@ -182,7 +184,9 @@ namespace osu.Game.Rulesets.Osu.Edit return; for (int i = 0; i < slider.Path.ControlPoints.Count; i++) - slider.Path.ControlPoints[i].Position = originalPathPositions[i]; + slider.Path.ControlPoints[i].Position = originalInfo.PathControlPointPositions[i]; + + slider.Position = originalInfo.Position; // Snap the slider's length again to undo the potentially-invalid length applied by the previous snap. slider.SnapTo(snapProvider); From 8bca8d60722039460f2446d5a083f151b4100e37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marvin=20Sch=C3=BCrz?= Date: Fri, 20 Sep 2024 17:38:49 +0200 Subject: [PATCH 2344/2493] Restore previous scale behavior when using scale popover --- osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs b/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs index dff370d259..ec347939e7 100644 --- a/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs +++ b/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs @@ -10,7 +10,10 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterfaceV2; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.UI; +using osu.Game.Screens.Edit; using osu.Game.Screens.Edit.Components.RadioButtons; using osuTK; @@ -35,6 +38,8 @@ namespace osu.Game.Rulesets.Osu.Edit private OsuCheckbox xCheckBox = null!; private OsuCheckbox yCheckBox = null!; + private BindableList selectedItems { get; } = new BindableList(); + public PreciseScalePopover(OsuSelectionScaleHandler scaleHandler, OsuGridToolboxGroup gridToolbox) { this.scaleHandler = scaleHandler; @@ -44,8 +49,10 @@ namespace osu.Game.Rulesets.Osu.Edit } [BackgroundDependencyLoader] - private void load() + private void load(EditorBeatmap editorBeatmap) { + selectedItems.BindTo(editorBeatmap.SelectedHitObjects); + Child = new FillFlowContainer { Width = 220, @@ -196,6 +203,7 @@ namespace osu.Game.Rulesets.Osu.Edit { ScaleOrigin.GridCentre => gridToolbox.StartPosition.Value, ScaleOrigin.PlayfieldCentre => OsuPlayfield.BASE_SIZE / 2, + ScaleOrigin.SelectionCentre when selectedItems.Count == 1 && selectedItems.First() is Slider slider => slider.Position, ScaleOrigin.SelectionCentre => null, _ => throw new ArgumentOutOfRangeException(nameof(scale)) }; From 59df9cbf0ff76e1bdf3d3b391600fe6444aeba71 Mon Sep 17 00:00:00 2001 From: Daniel Cios Date: Fri, 20 Sep 2024 18:07:26 +0200 Subject: [PATCH 2345/2493] Remove nullable disable --- .../Visual/UserInterface/TestSceneSettingsColour.cs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneSettingsColour.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneSettingsColour.cs index 6bed5f91c5..8d28116950 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneSettingsColour.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneSettingsColour.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.Linq; using NUnit.Framework; using osu.Framework.Graphics; @@ -19,14 +17,14 @@ namespace osu.Game.Tests.Visual.UserInterface { public partial class TestSceneSettingsColour : OsuManualInputManagerTestScene { - private SettingsColour component; + private SettingsColour? component; [Test] public void TestColour() { createContent(); - AddRepeatStep("set random colour", () => component.Current.Value = randomColour(), 4); + AddRepeatStep("set random colour", () => component!.Current.Value = randomColour(), 4); } [Test] @@ -36,7 +34,7 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep("click colour", () => { - InputManager.MoveMouseTo(component); + InputManager.MoveMouseTo(component!); InputManager.Click(MouseButton.Left); }); From 2dbbbe270daf475afeec30539af438d08de1956e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marvin=20Sch=C3=BCrz?= Date: Sat, 21 Sep 2024 13:37:41 +0200 Subject: [PATCH 2346/2493] Scale around center when pressing alt while dragging selection box scale handle --- .../Edit/Compose/Components/SelectionBoxScaleHandle.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionBoxScaleHandle.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionBoxScaleHandle.cs index 7b0943c1d0..42e7b8c219 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionBoxScaleHandle.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionBoxScaleHandle.cs @@ -50,14 +50,14 @@ namespace osu.Game.Screens.Edit.Compose.Components rawScale = convertDragEventToScaleMultiplier(e); - applyScale(shouldLockAspectRatio: isCornerAnchor(originalAnchor) && e.ShiftPressed); + applyScale(shouldLockAspectRatio: isCornerAnchor(originalAnchor) && e.ShiftPressed, ignoreAnchor: e.AltPressed); } protected override bool OnKeyDown(KeyDownEvent e) { if (IsDragged) { - applyScale(shouldLockAspectRatio: isCornerAnchor(originalAnchor) && e.ShiftPressed); + applyScale(shouldLockAspectRatio: isCornerAnchor(originalAnchor) && e.ShiftPressed, ignoreAnchor: e.AltPressed); return true; } @@ -69,7 +69,7 @@ namespace osu.Game.Screens.Edit.Compose.Components base.OnKeyUp(e); if (IsDragged) - applyScale(shouldLockAspectRatio: isCornerAnchor(originalAnchor) && e.ShiftPressed); + applyScale(shouldLockAspectRatio: isCornerAnchor(originalAnchor) && e.ShiftPressed, ignoreAnchor: e.AltPressed); } protected override void OnDragEnd(DragEndEvent e) @@ -100,13 +100,13 @@ namespace osu.Game.Screens.Edit.Compose.Components if ((originalAnchor & Anchor.y0) > 0) scale.Y = -scale.Y; } - private void applyScale(bool shouldLockAspectRatio) + private void applyScale(bool shouldLockAspectRatio, bool ignoreAnchor = false) { var newScale = shouldLockAspectRatio ? new Vector2((rawScale.X + rawScale.Y) * 0.5f) : rawScale; - var scaleOrigin = originalAnchor.Opposite().PositionOnQuad(scaleHandler!.OriginalSurroundingQuad!.Value); + Vector2? scaleOrigin = ignoreAnchor ? null : originalAnchor.Opposite().PositionOnQuad(scaleHandler!.OriginalSurroundingQuad!.Value); scaleHandler!.Update(newScale, scaleOrigin, getAdjustAxis()); } From 3180468db1001294266b2f59f1451802c6e2b1f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marvin=20Sch=C3=BCrz?= Date: Sat, 21 Sep 2024 14:22:17 +0200 Subject: [PATCH 2347/2493] Prevent the distance snap grid from being activated by alt key while dragging select box handle --- .../Edit/CatchHitObjectComposer.cs | 20 +++++++++++++++++++ .../Edit/OsuHitObjectComposer.cs | 2 ++ .../Edit/ComposerDistanceSnapProvider.cs | 17 +--------------- 3 files changed, 23 insertions(+), 16 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs b/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs index 83f48816f9..978aeba4ce 100644 --- a/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs +++ b/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs @@ -114,6 +114,26 @@ namespace osu.Game.Rulesets.Catch.Edit { } + protected override bool OnKeyDown(KeyDownEvent e) + { + if (e.Repeat) + return false; + + handleToggleViaKey(e); + return base.OnKeyDown(e); + } + + protected override void OnKeyUp(KeyUpEvent e) + { + handleToggleViaKey(e); + base.OnKeyUp(e); + } + + private void handleToggleViaKey(KeyboardEvent key) + { + DistanceSnapProvider.HandleToggleViaKey(key); + } + public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All) { var result = base.FindSnappedPositionAndTime(screenSpacePosition, snapType); diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs index 8fc2a9b7d3..c94dba6b23 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs @@ -369,6 +369,8 @@ namespace osu.Game.Rulesets.Osu.Edit gridSnapMomentary = shiftPressed; rectangularGridSnapToggle.Value = rectangularGridSnapToggle.Value == TernaryState.False ? TernaryState.True : TernaryState.False; } + + DistanceSnapProvider.HandleToggleViaKey(key); } private DistanceSnapGrid createDistanceSnapGrid(IEnumerable selectedHitObjects) diff --git a/osu.Game/Rulesets/Edit/ComposerDistanceSnapProvider.cs b/osu.Game/Rulesets/Edit/ComposerDistanceSnapProvider.cs index b9850a94a3..979492fd8b 100644 --- a/osu.Game/Rulesets/Edit/ComposerDistanceSnapProvider.cs +++ b/osu.Game/Rulesets/Edit/ComposerDistanceSnapProvider.cs @@ -195,22 +195,7 @@ namespace osu.Game.Rulesets.Edit new TernaryButton(DistanceSnapToggle, "Distance Snap", () => new SpriteIcon { Icon = OsuIcon.EditorDistanceSnap }) }; - protected override bool OnKeyDown(KeyDownEvent e) - { - if (e.Repeat) - return false; - - handleToggleViaKey(e); - return base.OnKeyDown(e); - } - - protected override void OnKeyUp(KeyUpEvent e) - { - handleToggleViaKey(e); - base.OnKeyUp(e); - } - - private void handleToggleViaKey(KeyboardEvent key) + public void HandleToggleViaKey(KeyboardEvent key) { bool altPressed = key.AltPressed; From 0077ba72ecac49a7b79916a76956c7dd02f89038 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marvin=20Sch=C3=BCrz?= Date: Sat, 21 Sep 2024 14:59:47 +0200 Subject: [PATCH 2348/2493] Freeze select box buttons in place as long as they are hovered --- .../Edit/Compose/Components/SelectionBox.cs | 26 +++++++++++++++++++ .../Compose/Components/SelectionBoxButton.cs | 9 +++++++ 2 files changed, 35 insertions(+) diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs index 0cc8a8273f..39f0011a12 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionBox.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.Primitives; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Events; @@ -284,8 +285,12 @@ namespace osu.Game.Screens.Edit.Compose.Components Action = action }; + button.OperationStarted += freezeButtonPosition; + button.HoverLost += unfreezeButtonPosition; + button.OperationStarted += operationStarted; button.OperationEnded += operationEnded; + buttons.Add(button); return button; @@ -357,8 +362,29 @@ namespace osu.Game.Screens.Edit.Compose.Components OperationStarted?.Invoke(); } + private Quad? frozenButtonsDrawQuad; + + private void freezeButtonPosition() + { + frozenButtonsDrawQuad = buttons.ScreenSpaceDrawQuad; + } + + private void unfreezeButtonPosition() + { + frozenButtonsDrawQuad = null; + } + private void ensureButtonsOnScreen() { + if (frozenButtonsDrawQuad != null) + { + buttons.Anchor = Anchor.TopLeft; + buttons.Origin = Anchor.TopLeft; + + buttons.Position = ToLocalSpace(frozenButtonsDrawQuad.Value.TopLeft) - new Vector2(button_padding); + return; + } + buttons.Position = Vector2.Zero; var thisQuad = ScreenSpaceDrawQuad; diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionBoxButton.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionBoxButton.cs index 6108d44c81..e355add40b 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionBoxButton.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionBoxButton.cs @@ -21,6 +21,8 @@ namespace osu.Game.Screens.Edit.Compose.Components public Action? Action; + public event Action? HoverLost; + public SelectionBoxButton(IconUsage iconUsage, string tooltip) { this.iconUsage = iconUsage; @@ -61,6 +63,13 @@ namespace osu.Game.Screens.Edit.Compose.Components icon.FadeColour(!IsHeld && IsHovered ? Color4.White : Color4.Black, TRANSFORM_DURATION, Easing.OutQuint); } + protected override void OnHoverLost(HoverLostEvent e) + { + base.OnHoverLost(e); + + HoverLost?.Invoke(); + } + public LocalisableString TooltipText { get; } } } From 1095f35025603ca1e948483e732d6d4346f6c51c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marvin=20Sch=C3=BCrz?= Date: Sat, 21 Sep 2024 15:25:37 +0200 Subject: [PATCH 2349/2493] Only store position instead of entire draw quad --- .../Screens/Edit/Compose/Components/SelectionBox.cs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs index 39f0011a12..4eae2b77f6 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs @@ -6,7 +6,6 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Events; @@ -362,26 +361,26 @@ namespace osu.Game.Screens.Edit.Compose.Components OperationStarted?.Invoke(); } - private Quad? frozenButtonsDrawQuad; + private Vector2? frozenButtonsPosition; private void freezeButtonPosition() { - frozenButtonsDrawQuad = buttons.ScreenSpaceDrawQuad; + frozenButtonsPosition = buttons.ScreenSpaceDrawQuad.TopLeft; } private void unfreezeButtonPosition() { - frozenButtonsDrawQuad = null; + frozenButtonsPosition = null; } private void ensureButtonsOnScreen() { - if (frozenButtonsDrawQuad != null) + if (frozenButtonsPosition != null) { buttons.Anchor = Anchor.TopLeft; buttons.Origin = Anchor.TopLeft; - buttons.Position = ToLocalSpace(frozenButtonsDrawQuad.Value.TopLeft) - new Vector2(button_padding); + buttons.Position = ToLocalSpace(frozenButtonsPosition.Value) - new Vector2(button_padding); return; } From 1b77b3912baf0d991da00c01cd986b0439398cb5 Mon Sep 17 00:00:00 2001 From: Givikap120 Date: Sun, 22 Sep 2024 15:01:58 +0300 Subject: [PATCH 2350/2493] initial commit --- .../Difficulty/CatchDifficultyCalculator.cs | 2 +- osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs | 3 +-- .../Difficulty/TaikoDifficultyCalculator.cs | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs index 0899212b6c..f78a6b4703 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs @@ -44,7 +44,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty StarRating = Math.Sqrt(skills[0].DifficultyValue()) * difficulty_multiplier, 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)), + MaxCombo = beatmap.GetMaxCombo(), }; return attributes; diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs index e93475ecff..c4fcd1f760 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs @@ -81,7 +81,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty double preempt = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.ApproachRate, 1800, 1200, 450) / clockRate; double drainRate = beatmap.Difficulty.DrainRate; - int maxCombo = beatmap.GetMaxCombo(); int hitCirclesCount = beatmap.HitObjects.Count(h => h is HitCircle); int sliderCount = beatmap.HitObjects.Count(h => h is Slider); @@ -104,7 +103,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty ApproachRate = preempt > 1200 ? (1800 - preempt) / 120 : (1200 - preempt) / 150 + 5, OverallDifficulty = (80 - hitWindowGreat) / 6, DrainRate = drainRate, - MaxCombo = maxCombo, + MaxCombo = beatmap.GetMaxCombo(), HitCircleCount = hitCirclesCount, SliderCount = sliderCount, SpinnerCount = spinnerCount, diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs index 28323693d0..6a1a047b7a 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs @@ -100,7 +100,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty ColourDifficulty = colourRating, PeakDifficulty = combinedRating, GreatHitWindow = hitWindows.WindowFor(HitResult.Great) / clockRate, - MaxCombo = beatmap.HitObjects.Count(h => h is Hit), + MaxCombo = beatmap.GetMaxCombo(), }; return attributes; From 2995df7903c8c22bb14eec4ebf79ca6284a4f98e Mon Sep 17 00:00:00 2001 From: Givikap120 Date: Sun, 22 Sep 2024 15:04:21 +0300 Subject: [PATCH 2351/2493] removed unnecessary includes --- osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs | 1 - osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs | 1 - 2 files changed, 2 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs index f78a6b4703..7d21409ee8 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using System.Linq; using osu.Game.Beatmaps; using osu.Game.Rulesets.Catch.Beatmaps; using osu.Game.Rulesets.Catch.Difficulty.Preprocessing; diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs index 6a1a047b7a..e3c550fbe9 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs @@ -14,7 +14,6 @@ using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing; using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour; using osu.Game.Rulesets.Taiko.Difficulty.Skills; using osu.Game.Rulesets.Taiko.Mods; -using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Rulesets.Taiko.Scoring; namespace osu.Game.Rulesets.Taiko.Difficulty From 881c9dfbba2753c76aabba81948ec200b507b8fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 23 Sep 2024 09:49:25 +0200 Subject: [PATCH 2352/2493] Fix score being cloned in async method causing random errors (again) Compare: https://github.com/ppy/osu/pull/24548. I don't have a reproduction scenario (judging from the stack trace of the crash it's likely to be nigh-impossible to concoct a reliable one), but there is some circumstantial evidence that this might help, namely that that previous fix above worked, and the pathway that's failing here is similarly async to the one that pull fixed. So I'm just gonna start with that and hope that it does the job. --- osu.Game/Screens/Play/SubmittingPlayer.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/SubmittingPlayer.cs b/osu.Game/Screens/Play/SubmittingPlayer.cs index aea3bf6d5c..24c5b2c3d4 100644 --- a/osu.Game/Screens/Play/SubmittingPlayer.cs +++ b/osu.Game/Screens/Play/SubmittingPlayer.cs @@ -234,9 +234,12 @@ namespace osu.Game.Screens.Play { if (LoadedBeatmapSuccessfully) { + // compare: https://github.com/ppy/osu/blob/ccf1acce56798497edfaf92d3ece933469edcf0a/osu.Game/Screens/Play/Player.cs#L848-L851 + var scoreCopy = Score.DeepClone(); + Task.Run(async () => { - await submitScore(Score.DeepClone()).ConfigureAwait(false); + await submitScore(scoreCopy).ConfigureAwait(false); spectatorClient.EndPlaying(GameplayState); }).FireAndForget(); } From e04b88a9b0093c0902e4f6ecf7e6ad2d64174a84 Mon Sep 17 00:00:00 2001 From: StanR Date: Mon, 23 Sep 2024 13:49:25 +0500 Subject: [PATCH 2353/2493] Replace speed buff with aim buff --- osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs | 2 +- osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs | 2 +- osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs index e34989f162..9c7191dec5 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs @@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators { private const int history_time_max = 4 * 1000; // 4 seconds private const int history_objects_max = 24; - private const double rhythm_overall_multiplier = 1.0; + private const double rhythm_overall_multiplier = 0.95; private const double rhythm_ratio_multiplier = 14.0; /// diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs index 3f6b22bbb1..7963e92c8d 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs @@ -23,7 +23,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills private double currentStrain; - private double skillMultiplier => 23.55; + private double skillMultiplier => 23.65; private double strainDecayBase => 0.15; private double strainDecay(double ms) => Math.Pow(strainDecayBase, ms / 1000); diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs index 37420ed026..f7f081b7ea 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs @@ -16,7 +16,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills /// public class Speed : OsuStrainSkill { - private double skillMultiplier => 1.38; + private double skillMultiplier => 1.375; private double strainDecayBase => 0.3; private double currentStrain; From 92b5650ff8dab72c298a396960cb5ef51e1a5d3f Mon Sep 17 00:00:00 2001 From: OliBomby Date: Mon, 23 Sep 2024 10:56:03 +0200 Subject: [PATCH 2354/2493] fix outdated comment --- .../Screens/Edit/Compose/Components/SelectionRotationHandler.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionRotationHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionRotationHandler.cs index 680acad114..af3b3d6489 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionRotationHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionRotationHandler.cs @@ -13,7 +13,7 @@ namespace osu.Game.Screens.Edit.Compose.Components public partial class SelectionRotationHandler : Component { /// - /// Whether there is any ongoing scale operation right now. + /// Whether there is any ongoing rotation operation right now. /// public Bindable OperationInProgress { get; private set; } = new BindableBool(); From 0f758ca25f6d68a0b4a0c57bc3ea0e730d854172 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 23 Sep 2024 11:08:31 +0200 Subject: [PATCH 2355/2493] Continue displaying storyboard even if fully dimmed in specific circumstances Closes https://github.com/ppy/osu/issues/9315. Closes https://github.com/ppy/osu/issues/29867. Notably, this does nothing about https://github.com/ppy/osu/issues/25075, but I'm not sure what to do with that one in the first place. --- osu.Game/Screens/Play/DimmableStoryboard.cs | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/DimmableStoryboard.cs b/osu.Game/Screens/Play/DimmableStoryboard.cs index 40cc0f66ad..84d99ea863 100644 --- a/osu.Game/Screens/Play/DimmableStoryboard.cs +++ b/osu.Game/Screens/Play/DimmableStoryboard.cs @@ -3,7 +3,9 @@ #nullable disable +using System; using System.Collections.Generic; +using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics.Containers; @@ -24,6 +26,21 @@ namespace osu.Game.Screens.Play private readonly Storyboard storyboard; private readonly IReadOnlyList mods; + /// + /// In certain circumstances, the storyboard cannot be hidden entirely even if it is fully dimmed. Such circumstances include: + /// + /// + /// cases where the storyboard has an overlay layer sprite, as it should continue to display fully dimmed + /// in front of the playfield (https://github.com/ppy/osu/issues/29867), + /// + /// + /// cases where the storyboard includes samples - as they are played back via drawable samples, + /// they must be present for the playback to occur (https://github.com/ppy/osu/issues/9315). + /// + /// + /// + private readonly Lazy storyboardMustAlwaysBePresent; + private DrawableStoryboard drawableStoryboard; /// @@ -38,6 +55,8 @@ namespace osu.Game.Screens.Play { this.storyboard = storyboard; this.mods = mods; + + storyboardMustAlwaysBePresent = new Lazy(() => storyboard.GetLayer(@"Overlay").Elements.Any() || storyboard.Layers.Any(l => l.Elements.OfType().Any())); } [BackgroundDependencyLoader] @@ -54,7 +73,7 @@ namespace osu.Game.Screens.Play base.LoadComplete(); } - protected override bool ShowDimContent => IgnoreUserSettings.Value || (ShowStoryboard.Value && DimLevel < 1); + protected override bool ShowDimContent => IgnoreUserSettings.Value || (ShowStoryboard.Value && (DimLevel < 1 || storyboardMustAlwaysBePresent.Value)); private void initializeStoryboard(bool async) { From a9ebfbe431e4616a7a0c3ea49182065839471014 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Mon, 23 Sep 2024 11:37:42 +0200 Subject: [PATCH 2356/2493] Assert default origin not null in rotation handle --- osu.Game.Tests/Visual/Editing/TestSceneComposeSelectBox.cs | 1 + .../Edit/Compose/Components/SelectionBoxRotationHandle.cs | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneComposeSelectBox.cs b/osu.Game.Tests/Visual/Editing/TestSceneComposeSelectBox.cs index 30f397f518..2bf07d8e27 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneComposeSelectBox.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneComposeSelectBox.cs @@ -84,6 +84,7 @@ namespace osu.Game.Tests.Visual.Editing targetContainer = getTargetContainer(); initialRotation = targetContainer!.Rotation; + DefaultOrigin = ToLocalSpace(targetContainer.ToScreenSpace(Vector2.Zero)); base.Begin(); } diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionBoxRotationHandle.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionBoxRotationHandle.cs index 898efc8b5e..03d600bfa2 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionBoxRotationHandle.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionBoxRotationHandle.cs @@ -77,6 +77,8 @@ namespace osu.Game.Screens.Edit.Compose.Components { base.OnDrag(e); + if (rotationHandler == null || !rotationHandler.OperationInProgress.Value) return; + rawCumulativeRotation += convertDragEventToAngleOfRotation(e); applyRotation(shouldSnap: e.ShiftPressed); @@ -114,9 +116,7 @@ namespace osu.Game.Screens.Edit.Compose.Components private float convertDragEventToAngleOfRotation(DragEvent e) { // Adjust coordinate system to the center of the selection - Vector2 center = rotationHandler?.DefaultOrigin is not null - ? selectionBox.ToLocalSpace(rotationHandler.ToScreenSpace(rotationHandler.DefaultOrigin.Value)) - : selectionBox.DrawSize / 2; + Vector2 center = selectionBox.ToLocalSpace(rotationHandler!.ToScreenSpace(rotationHandler!.DefaultOrigin!.Value)); float startAngle = MathF.Atan2(e.LastMousePosition.Y - center.Y, e.LastMousePosition.X - center.X); float endAngle = MathF.Atan2(e.MousePosition.Y - center.Y, e.MousePosition.X - center.X); From 0d06b122c1630e277864118b9cde787747902a21 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Mon, 23 Sep 2024 11:39:42 +0200 Subject: [PATCH 2357/2493] rename region --- osu.Game/Utils/GeometryUtils.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Utils/GeometryUtils.cs b/osu.Game/Utils/GeometryUtils.cs index 7e6db10a28..c933006cc5 100644 --- a/osu.Game/Utils/GeometryUtils.cs +++ b/osu.Game/Utils/GeometryUtils.cs @@ -220,7 +220,7 @@ namespace osu.Game.Utils return new[] { h.Position }; }); - #region welzl_helpers + #region Welzl helpers // Function to check whether a point lies inside or on the boundaries of the circle private static bool isInside((Vector2, float) c, Vector2 p) From 447d178e0104bc0fb03199a7c5af20918ea69cf2 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Mon, 23 Sep 2024 11:42:02 +0200 Subject: [PATCH 2358/2493] use named tuple members --- osu.Game/Utils/GeometryUtils.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Utils/GeometryUtils.cs b/osu.Game/Utils/GeometryUtils.cs index c933006cc5..51777f8ea0 100644 --- a/osu.Game/Utils/GeometryUtils.cs +++ b/osu.Game/Utils/GeometryUtils.cs @@ -223,9 +223,9 @@ namespace osu.Game.Utils #region Welzl helpers // Function to check whether a point lies inside or on the boundaries of the circle - private static bool isInside((Vector2, float) c, Vector2 p) + private static bool isInside((Vector2 Centre, float Radius) c, Vector2 p) { - return Precision.AlmostBigger(c.Item2, Vector2.Distance(c.Item1, p)); + return Precision.AlmostBigger(c.Radius, Vector2.Distance(c.Centre, p)); } // Function to return a unique circle that intersects three points @@ -336,7 +336,7 @@ namespace osu.Game.Utils /// /// Function to find the minimum enclosing circle for a collection of points. /// - /// A tuple containing the circle center and radius. + /// A tuple containing the circle centre and radius. public static (Vector2, float) MinimumEnclosingCircle(IEnumerable points) { // Using Welzl's algorithm to find the minimum enclosing circle @@ -348,7 +348,7 @@ namespace osu.Game.Utils /// /// Function to find the minimum enclosing circle for a collection of hit objects. /// - /// A tuple containing the circle center and radius. + /// A tuple containing the circle centre and radius. public static (Vector2, float) MinimumEnclosingCircle(IEnumerable hitObjects) => MinimumEnclosingCircle(enumerateStartAndEndPositions(hitObjects)); } From d0f12006a4e8755179fa9cd0faf979dab93ae526 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Mon, 23 Sep 2024 11:42:28 +0200 Subject: [PATCH 2359/2493] update wikipedia url --- osu.Game/Utils/GeometryUtils.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Utils/GeometryUtils.cs b/osu.Game/Utils/GeometryUtils.cs index 51777f8ea0..8395c3a090 100644 --- a/osu.Game/Utils/GeometryUtils.cs +++ b/osu.Game/Utils/GeometryUtils.cs @@ -234,7 +234,7 @@ namespace osu.Game.Utils if (Precision.AlmostEquals(0, (b.Y - a.Y) * (c.X - a.X) - (b.X - a.X) * (c.Y - a.Y))) return circleFrom(a, b); - // See: https://en.wikipedia.org/wiki/Circumscribed_circle#Cartesian_coordinates_2 + // See: https://en.wikipedia.org/wiki/Circumcircle#Cartesian_coordinates float d = 2 * (a.X * (b - c).Y + b.X * (c - a).Y + c.X * (a - b).Y); float aSq = a.LengthSquared; float bSq = b.LengthSquared; From 40cfaabc53cc310809d91a89baebd0e279894bc0 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Mon, 23 Sep 2024 11:43:36 +0200 Subject: [PATCH 2360/2493] verify n<=3 in minCircleTrivial --- osu.Game/Utils/GeometryUtils.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Utils/GeometryUtils.cs b/osu.Game/Utils/GeometryUtils.cs index 8395c3a090..93991efa22 100644 --- a/osu.Game/Utils/GeometryUtils.cs +++ b/osu.Game/Utils/GeometryUtils.cs @@ -269,6 +269,9 @@ namespace osu.Game.Utils // Function to return the minimum enclosing circle for N <= 3 private static (Vector2, float) minCircleTrivial(ReadOnlySpan points) { + if (points.Length > 3) + throw new ArgumentException("Number of points must be at most 3", nameof(points)); + switch (points.Length) { case 0: From 42549e81aa9c750a6603c2e403ff403040a90c93 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Mon, 23 Sep 2024 11:44:07 +0200 Subject: [PATCH 2361/2493] use RNG.Next --- osu.Game/Utils/GeometryUtils.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game/Utils/GeometryUtils.cs b/osu.Game/Utils/GeometryUtils.cs index 93991efa22..d4968749bf 100644 --- a/osu.Game/Utils/GeometryUtils.cs +++ b/osu.Game/Utils/GeometryUtils.cs @@ -303,14 +303,14 @@ namespace osu.Game.Utils // Takes a set of input points P and a set R // points on the circle boundary. // n represents the number of points in P that are not yet processed. - private static (Vector2, float) welzlHelper(List points, ReadOnlySpan r, int n, Random random) + private static (Vector2, float) welzlHelper(List points, ReadOnlySpan r, int n) { // Base case when all points processed or |R| = 3 if (n == 0 || r.Length == 3) return minCircleTrivial(r); // Pick a random point randomly - int idx = random.Next(n); + int idx = RNG.Next(n); Vector2 p = points[idx]; // Put the picked point at the end of P since it's more efficient than @@ -318,7 +318,7 @@ namespace osu.Game.Utils (points[idx], points[n - 1]) = (points[n - 1], points[idx]); // Get the MEC circle d from the set of points P - {p} - var d = welzlHelper(points, r, n - 1, random); + var d = welzlHelper(points, r, n - 1); // If d contains p, return d if (isInside(d, p)) @@ -331,7 +331,7 @@ namespace osu.Game.Utils r2[r.Length] = p; // Return the MEC for P - {p} and R U {p} - return welzlHelper(points, r2, n - 1, random); + return welzlHelper(points, r2, n - 1); } #endregion @@ -345,7 +345,7 @@ namespace osu.Game.Utils // Using Welzl's algorithm to find the minimum enclosing circle // https://www.geeksforgeeks.org/minimum-enclosing-circle-using-welzls-algorithm/ List pCopy = points.ToList(); - return welzlHelper(pCopy, Array.Empty(), pCopy.Count, new Random()); + return welzlHelper(pCopy, Array.Empty(), pCopy.Count); } /// From 4b349ba38738fbf862acccaeb43e6954782f6ba2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 23 Sep 2024 11:53:32 +0200 Subject: [PATCH 2362/2493] Use cache for beatmap lookups on spectate screen @peppy noticed recently that attempting to spectate just a few users was very likely to end up in requests very quickly being rejected with code 429 ("Too Many Requests"). I'm somewhat certain that the reason for that is that a significant number of players is wont to retry a lot in quick succession. That means that spectator server is going to note a lot of gameplay start and end messages in quick succession, too. And as it turns out, every gameplay start would end up triggering a new beatmap set fetch request: https://github.com/ppy/osu/blob/ccf1acce56798497edfaf92d3ece933469edcf0a/osu.Game/Screens/Spectate/SpectatorScreen.cs#L131-L134 https://github.com/ppy/osu/blob/ccf1acce56798497edfaf92d3ece933469edcf0a/osu.Game/Screens/Play/SoloSpectatorScreen.cs#L168-L172 https://github.com/ppy/osu/blob/ccf1acce56798497edfaf92d3ece933469edcf0a/osu.Game/Screens/Play/SoloSpectatorScreen.cs#L243-L256 To attempt to curtail that, use the beatmap cache instead, which should prevent these unnecessary requests from firing in the first place, therefore reducing the chance of the client getting throttled. This technically means that a different endpoint is used to fetch the data (`GET /beatmaps/?ids[]=` rather than `GET /beatmapsets/lookup?beatmap_id={id}`), but docs claim that both should return the same data, and it looks to work fine in practice. --- osu.Game/Screens/Play/SoloSpectatorScreen.cs | 26 +++++++++++--------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/osu.Game/Screens/Play/SoloSpectatorScreen.cs b/osu.Game/Screens/Play/SoloSpectatorScreen.cs index 95eb2d4376..269bc3bb92 100644 --- a/osu.Game/Screens/Play/SoloSpectatorScreen.cs +++ b/osu.Game/Screens/Play/SoloSpectatorScreen.cs @@ -3,6 +3,7 @@ using System.Diagnostics; using osu.Framework.Allocation; +using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; @@ -13,12 +14,11 @@ using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Drawables.Cards; using osu.Game.Configuration; +using osu.Game.Database; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Localisation; -using osu.Game.Online.API; -using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Spectator; using osu.Game.Overlays; @@ -34,7 +34,7 @@ namespace osu.Game.Screens.Play public partial class SoloSpectatorScreen : SpectatorScreen, IPreviewTrackOwner { [Resolved] - private IAPIProvider api { get; set; } = null!; + private BeatmapLookupCache beatmapLookupCache { get; set; } = null!; [Resolved] private PreviewTrackManager previewTrackManager { get; set; } = null!; @@ -60,7 +60,7 @@ namespace osu.Game.Screens.Play /// private SpectatorGameplayState? immediateSpectatorGameplayState; - private GetBeatmapSetRequest? onlineBeatmapRequest; + private ScheduledDelegate? beatmapFetchCallback; private APIBeatmapSet? beatmapSet; @@ -210,7 +210,7 @@ namespace osu.Game.Screens.Play private void clearDisplay() { watchButton.Enabled.Value = false; - onlineBeatmapRequest?.Cancel(); + beatmapFetchCallback?.Cancel(); beatmapPanelContainer.Clear(); previewTrackManager.StopAnyPlaying(this); } @@ -244,15 +244,17 @@ namespace osu.Game.Screens.Play { Debug.Assert(state.BeatmapID != null); - onlineBeatmapRequest = new GetBeatmapSetRequest(state.BeatmapID.Value, BeatmapSetLookupType.BeatmapId); - onlineBeatmapRequest.Success += beatmapSet => Schedule(() => + beatmapLookupCache.GetBeatmapAsync(state.BeatmapID.Value).ContinueWith(t => beatmapFetchCallback = Schedule(() => { - this.beatmapSet = beatmapSet; - beatmapPanelContainer.Child = new BeatmapCardNormal(this.beatmapSet, allowExpansion: false); - checkForAutomaticDownload(); - }); + var beatmap = t.GetResultSafely(); - api.Queue(onlineBeatmapRequest); + if (beatmap?.BeatmapSet == null) + return; + + beatmapSet = beatmap.BeatmapSet; + beatmapPanelContainer.Child = new BeatmapCardNormal(beatmapSet, allowExpansion: false); + checkForAutomaticDownload(); + })); } private void checkForAutomaticDownload() From 86817d0cfc9be71adbd9f6ceb7ff369e880becd4 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Mon, 23 Sep 2024 12:15:31 +0200 Subject: [PATCH 2363/2493] Add benchmark for minimum enclosing circle --- osu.Game.Benchmarks/BenchmarkGeometryUtils.cs | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 osu.Game.Benchmarks/BenchmarkGeometryUtils.cs diff --git a/osu.Game.Benchmarks/BenchmarkGeometryUtils.cs b/osu.Game.Benchmarks/BenchmarkGeometryUtils.cs new file mode 100644 index 0000000000..2ab4d3369a --- /dev/null +++ b/osu.Game.Benchmarks/BenchmarkGeometryUtils.cs @@ -0,0 +1,29 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using BenchmarkDotNet.Attributes; +using osu.Framework.Utils; +using osu.Game.Utils; +using osuTK; + +namespace osu.Game.Benchmarks +{ + public class BenchmarkGeometryUtils : BenchmarkTest + { + [Params(100, 1000, 2000, 4000, 8000, 10000)] + public int N; + + private Vector2[] points = null!; + + public override void SetUp() + { + points = new Vector2[N]; + + for (int i = 0; i < points.Length; ++i) + points[i] = new Vector2(RNG.Next(512), RNG.Next(384)); + } + + [Benchmark] + public void MinimumEnclosingCircle() => GeometryUtils.MinimumEnclosingCircle(points); + } +} From 203951780ed50a9ab3338548c9dd9bf32131f14e Mon Sep 17 00:00:00 2001 From: OliBomby Date: Mon, 23 Sep 2024 12:15:42 +0200 Subject: [PATCH 2364/2493] use collection expression instead of stackalloc --- osu.Game/Utils/GeometryUtils.cs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/osu.Game/Utils/GeometryUtils.cs b/osu.Game/Utils/GeometryUtils.cs index d4968749bf..e365a00862 100644 --- a/osu.Game/Utils/GeometryUtils.cs +++ b/osu.Game/Utils/GeometryUtils.cs @@ -325,13 +325,8 @@ namespace osu.Game.Utils return d; // Otherwise, must be on the boundary of the MEC - // Stackalloc to avoid allocations. It's safe to assume that the length of r will be at most 3 - Span r2 = stackalloc Vector2[r.Length + 1]; - r.CopyTo(r2); - r2[r.Length] = p; - // Return the MEC for P - {p} and R U {p} - return welzlHelper(points, r2, n - 1); + return welzlHelper(points, [..r, p], n - 1); } #endregion From eead6b9eaea9aad7c5ed1b9afe6ef067de0afd3b Mon Sep 17 00:00:00 2001 From: OliBomby Date: Mon, 23 Sep 2024 13:13:33 +0200 Subject: [PATCH 2365/2493] return to stackalloc because its faster --- osu.Game/Utils/GeometryUtils.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/osu.Game/Utils/GeometryUtils.cs b/osu.Game/Utils/GeometryUtils.cs index e365a00862..d4968749bf 100644 --- a/osu.Game/Utils/GeometryUtils.cs +++ b/osu.Game/Utils/GeometryUtils.cs @@ -325,8 +325,13 @@ namespace osu.Game.Utils return d; // Otherwise, must be on the boundary of the MEC + // Stackalloc to avoid allocations. It's safe to assume that the length of r will be at most 3 + Span r2 = stackalloc Vector2[r.Length + 1]; + r.CopyTo(r2); + r2[r.Length] = p; + // Return the MEC for P - {p} and R U {p} - return welzlHelper(points, [..r, p], n - 1); + return welzlHelper(points, r2, n - 1); } #endregion From bf245aa9d61d2fc1f3cffede114f0ecd2a34a7e6 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Mon, 23 Sep 2024 13:16:45 +0200 Subject: [PATCH 2366/2493] add a max depth to prevent stack overflow --- osu.Game/Utils/GeometryUtils.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game/Utils/GeometryUtils.cs b/osu.Game/Utils/GeometryUtils.cs index d4968749bf..c4c63903bb 100644 --- a/osu.Game/Utils/GeometryUtils.cs +++ b/osu.Game/Utils/GeometryUtils.cs @@ -305,8 +305,11 @@ namespace osu.Game.Utils // n represents the number of points in P that are not yet processed. private static (Vector2, float) welzlHelper(List points, ReadOnlySpan r, int n) { + const int max_depth = 4000; + // Base case when all points processed or |R| = 3 - if (n == 0 || r.Length == 3) + // To prevent stack overflow, we stop at a certain depth and give an approximate answer + if (n == 0 || r.Length == 3 || points.Count - n >= max_depth) return minCircleTrivial(r); // Pick a random point randomly From 41826d0606d37e8d1b46bcad8e774d0b69af9521 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 23 Sep 2024 13:17:46 +0200 Subject: [PATCH 2367/2493] Add failing test case to demonstrate failure --- .../VolumeAwareHitSampleInfoTest.cs | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 osu.Game.Rulesets.Taiko.Tests/VolumeAwareHitSampleInfoTest.cs diff --git a/osu.Game.Rulesets.Taiko.Tests/VolumeAwareHitSampleInfoTest.cs b/osu.Game.Rulesets.Taiko.Tests/VolumeAwareHitSampleInfoTest.cs new file mode 100644 index 0000000000..2b3a922067 --- /dev/null +++ b/osu.Game.Rulesets.Taiko.Tests/VolumeAwareHitSampleInfoTest.cs @@ -0,0 +1,27 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Game.Audio; +using osu.Game.Rulesets.Taiko.Skinning.Argon; + +namespace osu.Game.Rulesets.Taiko.Tests +{ + [TestFixture] + public class VolumeAwareHitSampleInfoTest + { + [Test] + public void TestVolumeAwareHitSampleInfoIsNotEqualToItsUnderlyingSample( + [Values(HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_CLAP)] + string sample, + [Values(HitSampleInfo.BANK_NORMAL, HitSampleInfo.BANK_SOFT)] + string bank, + [Values(30, 70, 100)] int volume) + { + var underlyingSample = new HitSampleInfo(sample, bank, volume: volume); + var volumeAwareSample = new VolumeAwareHitSampleInfo(underlyingSample); + + Assert.That(underlyingSample, Is.Not.EqualTo(volumeAwareSample)); + } + } +} From 08bded82fdfd1cf63d2a72b51cedb5be0de01c60 Mon Sep 17 00:00:00 2001 From: StanR Date: Mon, 23 Sep 2024 16:30:02 +0500 Subject: [PATCH 2368/2493] Remove GetHashCode --- .../Difficulty/Evaluators/RhythmEvaluator.cs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs index 9c7191dec5..abf9dbe725 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs @@ -198,11 +198,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators Math.Abs(Delta - other.Delta) < deltaDifferenceEpsilon; } - public override int GetHashCode() - { - return HashCode.Combine(Delta, DeltaCount); - } - public bool Equals(Island? other) { if (other == null) From e8a394f89485e61b37c61f095c7c9ed1c5c3b121 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 23 Sep 2024 13:27:36 +0200 Subject: [PATCH 2369/2493] Fix argon volume-aware hitsounds not correctly playing immediately after object placement Closes https://github.com/ppy/osu/issues/29832. The underlying reason for the incorrect sample playback was an equality comparer failure. Samples are contained in several pools which are managed by the playfield. In particular, the pools are keyed by `ISampleInfo` instances. This means that for correct operation, `ISampleInfo` has to implement `IEquatable` and also provide an appropriately correct `GetHashCode()` implementation. Different audible samples must not compare equal to each other when represented by `ISampleInfo`. As it turns out, `VolumeAwareHitSampleInfo` failed on this, due to not overriding equality members. Therefore, a `new HitSampleInfo(HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_NORMAL, volume: 70)` was allowed to compare equal to a `VolumeAwareHitSampleInfo` wrapping it, *even though they correspond to completely different sounds and go through entirely different lookup path sequences*. Therefore, to fix, provide more proper equality implementations for `VolumeAwareHitSampleInfo`. When testing note that this issue *only occurs immediately after placing an object*. Saving and re-entering editor makes this issue go away. I haven't looked too long into why, but the general gist of it is ordering; it appears that a `normal-hitnormal` pool exists at point of query of a new object placement, but does not seem to exist when entering editor afresh. That said I'm not sure that ordering aspect of this bug matters much if at all, since the two `IHitSampleInfo`s should never be allowed to alias with each other at all wrt equality. --- .../Argon/VolumeAwareHitSampleInfo.cs | 20 +++++++++++++++++++ osu.Game/Audio/HitSampleInfo.cs | 2 +- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/Skinning/Argon/VolumeAwareHitSampleInfo.cs b/osu.Game.Rulesets.Taiko/Skinning/Argon/VolumeAwareHitSampleInfo.cs index 3ca4b5a3c7..288ffde052 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Argon/VolumeAwareHitSampleInfo.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Argon/VolumeAwareHitSampleInfo.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 osu.Game.Audio; @@ -48,5 +49,24 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon return originalBank; } } + + public override bool Equals(HitSampleInfo? other) => other is VolumeAwareHitSampleInfo && base.Equals(other); + + /// + /// + /// This override attempts to match the override above, but in theory it is not strictly necessary. + /// Recall that must meet the following requirements: + /// + /// + /// "If two objects compare as equal, the method for each object must return the same value. + /// However, if two objects do not compare as equal, methods for the two objects do not have to return different values." + /// + /// + /// Making this override combine the value generated by the base implementation with a constant means + /// that and instances which have the same values of their members + /// will not have equal hash codes, which is slightly more efficient when these objects are used as dictionary keys. + /// + /// + public override int GetHashCode() => HashCode.Combine(base.GetHashCode(), 1); } } diff --git a/osu.Game/Audio/HitSampleInfo.cs b/osu.Game/Audio/HitSampleInfo.cs index f9c93d72ff..ce5e217532 100644 --- a/osu.Game/Audio/HitSampleInfo.cs +++ b/osu.Game/Audio/HitSampleInfo.cs @@ -96,7 +96,7 @@ namespace osu.Game.Audio public virtual HitSampleInfo With(Optional newName = default, Optional newBank = default, Optional newSuffix = default, Optional newVolume = default) => new HitSampleInfo(newName.GetOr(Name), newBank.GetOr(Bank), newSuffix.GetOr(Suffix), newVolume.GetOr(Volume)); - public bool Equals(HitSampleInfo? other) + public virtual bool Equals(HitSampleInfo? other) => other != null && Name == other.Name && Bank == other.Bank && Suffix == other.Suffix; public override bool Equals(object? obj) From d6c17f6ac0b08b5a5a4ba541b82fc300301d5918 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 23 Sep 2024 14:29:56 +0200 Subject: [PATCH 2370/2493] Implement "form" dropdown control --- .../UserInterface/TestSceneFormControls.cs | 6 + .../Graphics/UserInterfaceV2/FormDropdown.cs | 251 ++++++++++++++++++ 2 files changed, 257 insertions(+) create mode 100644 osu.Game/Graphics/UserInterfaceV2/FormDropdown.cs diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneFormControls.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneFormControls.cs index b456da0f26..89b4ae9f97 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneFormControls.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneFormControls.cs @@ -5,6 +5,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; +using osu.Game.Beatmaps; using osu.Game.Graphics.Cursor; using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Localisation; @@ -94,6 +95,11 @@ namespace osu.Game.Tests.Visual.UserInterface Instantaneous = false, TabbableContentContainer = this, }, + new FormEnumDropdown + { + Caption = EditorSetupStrings.EnableCountdown, + HintText = EditorSetupStrings.CountdownDescription, + }, }, }, } diff --git a/osu.Game/Graphics/UserInterfaceV2/FormDropdown.cs b/osu.Game/Graphics/UserInterfaceV2/FormDropdown.cs new file mode 100644 index 0000000000..d47b9ac73d --- /dev/null +++ b/osu.Game/Graphics/UserInterfaceV2/FormDropdown.cs @@ -0,0 +1,251 @@ +// 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.Extensions.ObjectExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.UserInterface; +using osu.Framework.Input.Events; +using osu.Framework.Localisation; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osu.Game.Overlays; +using osuTK; + +namespace osu.Game.Graphics.UserInterfaceV2 +{ + public partial class FormDropdown : OsuDropdown + { + /// + /// Caption describing this slider bar, displayed on top of the controls. + /// + public LocalisableString Caption { get; init; } + + /// + /// Hint text containing an extended description of this slider bar, displayed in a tooltip when hovering the caption. + /// + public LocalisableString HintText { get; init; } + + private FormDropdownHeader header = null!; + + [BackgroundDependencyLoader] + private void load() + { + RelativeSizeAxes = Axes.X; + + header.Caption = Caption; + header.HintText = HintText; + } + + protected override DropdownHeader CreateHeader() => header = new FormDropdownHeader + { + Dropdown = this, + }; + + protected override DropdownMenu CreateMenu() => new FormDropdownMenu(); + + private partial class FormDropdownHeader : DropdownHeader + { + public FormDropdown Dropdown { get; set; } = null!; + + protected override DropdownSearchBar CreateSearchBar() => SearchBar = new FormDropdownSearchBar(); + + private LocalisableString captionText; + private LocalisableString hintText; + private LocalisableString labelText; + + public LocalisableString Caption + { + get => captionText; + set + { + captionText = value; + + if (caption.IsNotNull()) + caption.Caption = value; + } + } + + public LocalisableString HintText + { + get => hintText; + set + { + hintText = value; + + if (caption.IsNotNull()) + caption.TooltipText = value; + } + } + + protected override LocalisableString Label + { + get => labelText; + set + { + labelText = value; + + if (label.IsNotNull()) + label.Text = labelText; + } + } + + protected new FormDropdownSearchBar SearchBar { get; set; } = null!; + + private FormFieldCaption caption = null!; + private OsuSpriteText label = null!; + private SpriteIcon chevron = null!; + + [Resolved] + private OverlayColourProvider colourProvider { get; set; } = null!; + + [BackgroundDependencyLoader] + private void load() + { + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.None; + Height = 50; + + Masking = true; + CornerRadius = 5; + + Foreground.AutoSizeAxes = Axes.None; + Foreground.RelativeSizeAxes = Axes.Both; + Foreground.Padding = new MarginPadding(9); + Foreground.Children = new Drawable[] + { + caption = new FormFieldCaption + { + Anchor = Anchor.TopLeft, + Origin = Anchor.TopLeft, + Caption = Caption, + TooltipText = HintText, + }, + label = new OsuSpriteText + { + RelativeSizeAxes = Axes.X, + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + }, + chevron = new SpriteIcon + { + Icon = FontAwesome.Solid.ChevronDown, + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + Size = new Vector2(16), + }, + }; + + AddInternal(new HoverClickSounds()); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + Dropdown.Current.BindDisabledChanged(_ => updateState()); + SearchBar.SearchTerm.BindValueChanged(_ => updateState(), true); + Dropdown.Menu.StateChanged += _ => + { + updateState(); + updateChevron(); + }; + SearchBar.TextBox.OnCommit += (_, _) => + { + Background.FlashColour(ColourInfo.GradientVertical(colourProvider.Background5, colourProvider.Dark2), 800, Easing.OutQuint); + }; + } + + protected override bool OnHover(HoverEvent e) + { + updateState(); + return true; + } + + protected override void OnHoverLost(HoverLostEvent e) + { + base.OnHoverLost(e); + updateState(); + } + + private void updateState() + { + label.Alpha = string.IsNullOrEmpty(SearchBar.SearchTerm.Value) ? 1 : 0; + + caption.Colour = Dropdown.Current.Disabled ? colourProvider.Foreground1 : colourProvider.Content2; + label.Colour = Dropdown.Current.Disabled ? colourProvider.Foreground1 : colourProvider.Content1; + chevron.Colour = Dropdown.Current.Disabled ? colourProvider.Foreground1 : colourProvider.Content1; + DisabledColour = Colour4.White; + + bool dropdownOpen = Dropdown.Menu.State == MenuState.Open; + + if (!Dropdown.Current.Disabled) + { + BorderThickness = IsHovered || dropdownOpen ? 2 : 0; + BorderColour = dropdownOpen ? colourProvider.Highlight1 : colourProvider.Light4; + + if (dropdownOpen) + Background.Colour = ColourInfo.GradientVertical(colourProvider.Background5, colourProvider.Dark3); + else if (IsHovered) + Background.Colour = ColourInfo.GradientVertical(colourProvider.Background5, colourProvider.Dark4); + else + Background.Colour = colourProvider.Background5; + } + else + { + Background.Colour = colourProvider.Background4; + } + } + + private void updateChevron() + { + bool open = Dropdown.Menu.State == MenuState.Open; + chevron.ScaleTo(open ? new Vector2(1f, -1f) : Vector2.One, 300, Easing.OutQuint); + } + } + + private partial class FormDropdownSearchBar : DropdownSearchBar + { + public FormTextBox.InnerTextBox TextBox { get; private set; } = null!; + + protected override void PopIn() => this.FadeIn(); + protected override void PopOut() => this.FadeOut(); + + protected override TextBox CreateTextBox() => TextBox = new FormTextBox.InnerTextBox(); + + [BackgroundDependencyLoader] + private void load() + { + TextBox.Anchor = Anchor.BottomLeft; + TextBox.Origin = Anchor.BottomLeft; + TextBox.RelativeSizeAxes = Axes.X; + TextBox.Margin = new MarginPadding(9); + } + } + + private partial class FormDropdownMenu : OsuDropdownMenu + { + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider) + { + ItemsContainer.Padding = new MarginPadding(9); + Margin = new MarginPadding { Top = 5 }; + + MaskingContainer.BorderThickness = 2; + MaskingContainer.BorderColour = colourProvider.Highlight1; + } + } + } + + public partial class FormEnumDropdown : FormDropdown + where T : struct, Enum + { + public FormEnumDropdown() + { + Items = Enum.GetValues(); + } + } +} From 4da78a8c009c4f2b0c5a254c923d194046b0c5c9 Mon Sep 17 00:00:00 2001 From: tsunyoku Date: Tue, 24 Sep 2024 10:06:07 +0100 Subject: [PATCH 2371/2493] make speed bonuses additive, scale distanceBonus --- .../Difficulty/Evaluators/SpeedEvaluator.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/SpeedEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/SpeedEvaluator.cs index 37fd11391c..4dbc3d9a23 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/SpeedEvaluator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/SpeedEvaluator.cs @@ -13,6 +13,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators private const double single_spacing_threshold = 125; // 1.25 circles distance between centers private const double min_speed_bonus = 75; // ~200BPM private const double speed_balancing_factor = 40; + private const double distance_multiplier = 0.95; /// /// Evaluates the difficulty of tapping the current object, based on: @@ -51,11 +52,11 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators strainTime /= Math.Clamp((strainTime / osuCurrObj.HitWindowGreat) / 0.93, 0.92, 1); // speedBonus will be 1.0 for BPM < 200 - double speedBonus = 1.0; + double speedBonus = 0.0; // Add additional scaling bonus for streams/bursts higher than 200bpm if (strainTime < min_speed_bonus) - speedBonus = 1 + 0.75 * Math.Pow((min_speed_bonus - strainTime) / speed_balancing_factor, 2); + speedBonus = 0.75 * Math.Pow((min_speed_bonus - strainTime) / speed_balancing_factor, 2); double travelDistance = osuPrevObj?.TravelDistance ?? 0; double distance = travelDistance + osuCurrObj.MinimumJumpDistance; @@ -64,10 +65,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators distance = Math.Min(distance, single_spacing_threshold); // Max distance bonus is 2 at single_spacing_threshold - double distanceBonus = 1 + Math.Pow(distance / single_spacing_threshold, 3.5); + double distanceBonus = Math.Pow(distance / single_spacing_threshold, 3.75) * distance_multiplier; // Base difficulty with all bonuses - double difficulty = speedBonus * distanceBonus * 1000 / strainTime; + double difficulty = (1 + speedBonus + distanceBonus) * 1000 / strainTime; // Apply penalty if there's doubletappable doubles return difficulty * doubletapness; From c857de3a9a45f691235a1ac9d4ddd5381e6e5042 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Tue, 24 Sep 2024 11:44:02 +0200 Subject: [PATCH 2372/2493] Revert "add a max depth to prevent stack overflow" This reverts commit bf245aa9d61d2fc1f3cffede114f0ecd2a34a7e6. --- osu.Game/Utils/GeometryUtils.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/osu.Game/Utils/GeometryUtils.cs b/osu.Game/Utils/GeometryUtils.cs index c4c63903bb..d4968749bf 100644 --- a/osu.Game/Utils/GeometryUtils.cs +++ b/osu.Game/Utils/GeometryUtils.cs @@ -305,11 +305,8 @@ namespace osu.Game.Utils // n represents the number of points in P that are not yet processed. private static (Vector2, float) welzlHelper(List points, ReadOnlySpan r, int n) { - const int max_depth = 4000; - // Base case when all points processed or |R| = 3 - // To prevent stack overflow, we stop at a certain depth and give an approximate answer - if (n == 0 || r.Length == 3 || points.Count - n >= max_depth) + if (n == 0 || r.Length == 3) return minCircleTrivial(r); // Pick a random point randomly From 86432078dd04ea69de005fac9f98c1353a59905d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 24 Sep 2024 11:53:02 +0200 Subject: [PATCH 2373/2493] Remove usage of switch expression syntax It's not universally accepted here and a `when` crept in that can be bypassed entirely using rather clean baseline language constructs, so why bother at this point. --- .../Edit/PreciseScalePopover.cs | 27 +++++++++++++------ 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs b/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs index ec347939e7..33b0c14185 100644 --- a/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs +++ b/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs @@ -198,15 +198,26 @@ namespace osu.Game.Rulesets.Osu.Edit updateAxisCheckBoxesEnabled(); } - private Vector2? getOriginPosition(PreciseScaleInfo scale) => - scale.Origin switch + private Vector2? getOriginPosition(PreciseScaleInfo scale) + { + switch (scale.Origin) { - ScaleOrigin.GridCentre => gridToolbox.StartPosition.Value, - ScaleOrigin.PlayfieldCentre => OsuPlayfield.BASE_SIZE / 2, - ScaleOrigin.SelectionCentre when selectedItems.Count == 1 && selectedItems.First() is Slider slider => slider.Position, - ScaleOrigin.SelectionCentre => null, - _ => throw new ArgumentOutOfRangeException(nameof(scale)) - }; + case ScaleOrigin.GridCentre: + return gridToolbox.StartPosition.Value; + + case ScaleOrigin.PlayfieldCentre: + return OsuPlayfield.BASE_SIZE / 2; + + case ScaleOrigin.SelectionCentre: + if (selectedItems.Count == 1 && selectedItems.First() is Slider slider) + return slider.Position; + + return null; + + default: + throw new ArgumentOutOfRangeException(nameof(scale)); + } + } private Axes getAdjustAxis(PreciseScaleInfo scale) => scale.XAxis ? scale.YAxis ? Axes.Both : Axes.X : Axes.Y; From 3031b68552cc9b2caa498a92ca4c72c4711a8871 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Tue, 24 Sep 2024 11:56:04 +0200 Subject: [PATCH 2374/2493] add TestMinimumEnclosingCircle --- osu.Game.Tests/Utils/GeometryUtilsTest.cs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/osu.Game.Tests/Utils/GeometryUtilsTest.cs b/osu.Game.Tests/Utils/GeometryUtilsTest.cs index ded4656ac1..f73175bb5b 100644 --- a/osu.Game.Tests/Utils/GeometryUtilsTest.cs +++ b/osu.Game.Tests/Utils/GeometryUtilsTest.cs @@ -29,5 +29,23 @@ namespace osu.Game.Tests.Utils Assert.That(hull, Is.EquivalentTo(expectedPoints)); } + + [TestCase(new int[] { }, 0, 0, 0)] + [TestCase(new[] { 0, 0 }, 0, 0, 0)] + [TestCase(new[] { 0, 0, 1, 1, 1, -1, 2, 0 }, 1, 0, 1)] + [TestCase(new[] { 0, 0, 1, 1, 1, -1, 2, 0, 1, 0 }, 1, 0, 1)] + [TestCase(new[] { 0, 0, 1, 1, 2, -1, 2, 0, 1, 0, 4, 10 }, 3, 4.5f, 5.5901699f)] + public void TestMinimumEnclosingCircle(int[] values, float x, float y, float r) + { + var points = new Vector2[values.Length / 2]; + for (int i = 0; i < values.Length; i += 2) + points[i / 2] = new Vector2(values[i], values[i + 1]); + + (var centre, float radius) = GeometryUtils.MinimumEnclosingCircle(points); + + Assert.That(centre.X, Is.EqualTo(x).Within(0.0001)); + Assert.That(centre.Y, Is.EqualTo(y).Within(0.0001)); + Assert.That(radius, Is.EqualTo(r).Within(0.0001)); + } } } From ac9c1508b1367ab24c7c479636508a89c8bfb3b7 Mon Sep 17 00:00:00 2001 From: James Wilson Date: Tue, 24 Sep 2024 11:22:46 +0100 Subject: [PATCH 2375/2493] update incorrect code comment Co-authored-by: StanR --- osu.Game.Rulesets.Osu/Difficulty/Evaluators/SpeedEvaluator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/SpeedEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/SpeedEvaluator.cs index 4dbc3d9a23..6a62fa32f3 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/SpeedEvaluator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/SpeedEvaluator.cs @@ -51,7 +51,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators // 0.93 is derived from making sure 260bpm OD8 streams aren't nerfed harshly, whilst 0.92 limits the effect of the cap. strainTime /= Math.Clamp((strainTime / osuCurrObj.HitWindowGreat) / 0.93, 0.92, 1); - // speedBonus will be 1.0 for BPM < 200 + // speedBonus will be 0.0 for BPM < 200 double speedBonus = 0.0; // Add additional scaling bonus for streams/bursts higher than 200bpm From 98d9b5eec8a1b3ba58e6373dc8c5192a3646b622 Mon Sep 17 00:00:00 2001 From: tsunyoku Date: Tue, 24 Sep 2024 11:23:34 +0100 Subject: [PATCH 2376/2493] correct `distanceBonus` code comment --- osu.Game.Rulesets.Osu/Difficulty/Evaluators/SpeedEvaluator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/SpeedEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/SpeedEvaluator.cs index 6a62fa32f3..ec0bec9b08 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/SpeedEvaluator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/SpeedEvaluator.cs @@ -64,7 +64,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators // Cap distance at single_spacing_threshold distance = Math.Min(distance, single_spacing_threshold); - // Max distance bonus is 2 at single_spacing_threshold + // Max distance bonus is 1 * `distance_multiplier` at single_spacing_threshold double distanceBonus = Math.Pow(distance / single_spacing_threshold, 3.75) * distance_multiplier; // Base difficulty with all bonuses From ce5c666c346ec61a42c16eb8e6b422d01cb95881 Mon Sep 17 00:00:00 2001 From: tsunyoku Date: Tue, 24 Sep 2024 11:28:15 +0100 Subject: [PATCH 2377/2493] bump global multiplier --- osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index 67d88b6b01..33c33d82ab 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty { public class OsuPerformanceCalculator : PerformanceCalculator { - public const double PERFORMANCE_BASE_MULTIPLIER = 1.14; // This is being adjusted to keep the final pp value scaled around what it used to be when changing things. + public const double PERFORMANCE_BASE_MULTIPLIER = 1.15; // This is being adjusted to keep the final pp value scaled around what it used to be when changing things. private double accuracy; private int scoreMaxCombo; From b54b4063bece8eac19e4364773c2ab842fafc636 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marvin=20Sch=C3=BCrz?= Date: Tue, 24 Sep 2024 12:40:28 +0200 Subject: [PATCH 2378/2493] Rename parameter --- .../Edit/Compose/Components/SelectionBoxScaleHandle.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionBoxScaleHandle.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionBoxScaleHandle.cs index 42e7b8c219..3b7e29cf3d 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionBoxScaleHandle.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionBoxScaleHandle.cs @@ -50,14 +50,14 @@ namespace osu.Game.Screens.Edit.Compose.Components rawScale = convertDragEventToScaleMultiplier(e); - applyScale(shouldLockAspectRatio: isCornerAnchor(originalAnchor) && e.ShiftPressed, ignoreAnchor: e.AltPressed); + applyScale(shouldLockAspectRatio: isCornerAnchor(originalAnchor) && e.ShiftPressed, useDefaultOrigin: e.AltPressed); } protected override bool OnKeyDown(KeyDownEvent e) { if (IsDragged) { - applyScale(shouldLockAspectRatio: isCornerAnchor(originalAnchor) && e.ShiftPressed, ignoreAnchor: e.AltPressed); + applyScale(shouldLockAspectRatio: isCornerAnchor(originalAnchor) && e.ShiftPressed, useDefaultOrigin: e.AltPressed); return true; } @@ -69,7 +69,7 @@ namespace osu.Game.Screens.Edit.Compose.Components base.OnKeyUp(e); if (IsDragged) - applyScale(shouldLockAspectRatio: isCornerAnchor(originalAnchor) && e.ShiftPressed, ignoreAnchor: e.AltPressed); + applyScale(shouldLockAspectRatio: isCornerAnchor(originalAnchor) && e.ShiftPressed, useDefaultOrigin: e.AltPressed); } protected override void OnDragEnd(DragEndEvent e) @@ -100,13 +100,13 @@ namespace osu.Game.Screens.Edit.Compose.Components if ((originalAnchor & Anchor.y0) > 0) scale.Y = -scale.Y; } - private void applyScale(bool shouldLockAspectRatio, bool ignoreAnchor = false) + private void applyScale(bool shouldLockAspectRatio, bool useDefaultOrigin = false) { var newScale = shouldLockAspectRatio ? new Vector2((rawScale.X + rawScale.Y) * 0.5f) : rawScale; - Vector2? scaleOrigin = ignoreAnchor ? null : originalAnchor.Opposite().PositionOnQuad(scaleHandler!.OriginalSurroundingQuad!.Value); + Vector2? scaleOrigin = useDefaultOrigin ? null : originalAnchor.Opposite().PositionOnQuad(scaleHandler!.OriginalSurroundingQuad!.Value); scaleHandler!.Update(newScale, scaleOrigin, getAdjustAxis()); } From 4c2ebdb2dbb3250b4f05d98bfda869e341916519 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 24 Sep 2024 12:53:54 +0200 Subject: [PATCH 2379/2493] Simplify accent colour assignment in argon wedge piece --- osu.Game/Screens/Play/HUD/ArgonWedgePiece.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/ArgonWedgePiece.cs b/osu.Game/Screens/Play/HUD/ArgonWedgePiece.cs index fb2e93b62b..46a658cd1c 100644 --- a/osu.Game/Screens/Play/HUD/ArgonWedgePiece.cs +++ b/osu.Game/Screens/Play/HUD/ArgonWedgePiece.cs @@ -41,7 +41,6 @@ namespace osu.Game.Screens.Play.HUD InternalChild = new Box { RelativeSizeAxes = Axes.Both, - Colour = ColourInfo.GradientVertical(AccentColour.Value.Opacity(0.0f), AccentColour.Value.Opacity(0.25f)), }; } @@ -50,7 +49,7 @@ namespace osu.Game.Screens.Play.HUD base.LoadComplete(); InvertShear.BindValueChanged(v => Shear = new Vector2(0.8f, 0f) * (v.NewValue ? -1 : 1), true); - AccentColour.BindValueChanged(c => InternalChild.Colour = ColourInfo.GradientVertical(AccentColour.Value.Opacity(0.0f), AccentColour.Value.Opacity(0.25f))); + AccentColour.BindValueChanged(c => InternalChild.Colour = ColourInfo.GradientVertical(AccentColour.Value.Opacity(0.0f), AccentColour.Value.Opacity(0.25f)), true); } } } From 5eb23d3a7157ce32e41e8c5cbcedfc1771c15aac Mon Sep 17 00:00:00 2001 From: tsunyoku Date: Tue, 24 Sep 2024 12:24:54 +0100 Subject: [PATCH 2380/2493] balancing attempts --- osu.Game.Rulesets.Osu/Difficulty/Evaluators/SpeedEvaluator.cs | 4 ++-- osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/SpeedEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/SpeedEvaluator.cs index ec0bec9b08..da1cb4b3b3 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/SpeedEvaluator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/SpeedEvaluator.cs @@ -13,7 +13,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators private const double single_spacing_threshold = 125; // 1.25 circles distance between centers private const double min_speed_bonus = 75; // ~200BPM private const double speed_balancing_factor = 40; - private const double distance_multiplier = 0.95; + private const double distance_multiplier = 0.94; /// /// Evaluates the difficulty of tapping the current object, based on: @@ -65,7 +65,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators distance = Math.Min(distance, single_spacing_threshold); // Max distance bonus is 1 * `distance_multiplier` at single_spacing_threshold - double distanceBonus = Math.Pow(distance / single_spacing_threshold, 3.75) * distance_multiplier; + double distanceBonus = Math.Pow(distance / single_spacing_threshold, 3.95) * distance_multiplier; // Base difficulty with all bonuses double difficulty = (1 + speedBonus + distanceBonus) * 1000 / strainTime; diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs index 1fbe03395c..48e421211f 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs @@ -23,7 +23,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills private double currentStrain; - private double skillMultiplier => 24.963; + private double skillMultiplier => 24.983; private double strainDecayBase => 0.15; private double strainDecay(double ms) => Math.Pow(strainDecayBase, ms / 1000); From 3ad734296473eae7fcfd91b3ed11b43fcc0d4774 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marvin=20Sch=C3=BCrz?= Date: Tue, 24 Sep 2024 13:35:56 +0200 Subject: [PATCH 2381/2493] Add tests for shift and alt modifiers in select box --- .../Editing/TestSceneComposerSelection.cs | 136 ++++++++++++++++++ 1 file changed, 136 insertions(+) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneComposerSelection.cs b/osu.Game.Tests/Visual/Editing/TestSceneComposerSelection.cs index 3884a3108f..3d7aef5a65 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneComposerSelection.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneComposerSelection.cs @@ -4,6 +4,7 @@ using System; using System.Linq; using NUnit.Framework; +using osu.Framework.Graphics; using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.UserInterface; using osu.Framework.Testing; @@ -36,6 +37,9 @@ namespace osu.Game.Tests.Visual.Editing private ContextMenuContainer contextMenuContainer => Editor.ChildrenOfType().First(); + private SelectionBoxScaleHandle getScaleHandle(Anchor anchor) + => Editor.ChildrenOfType().First(it => it.Anchor == anchor); + private void moveMouseToObject(Func targetFunc) { AddStep("move mouse to object", () => @@ -519,5 +523,137 @@ namespace osu.Game.Tests.Visual.Editing AddStep("release shift", () => InputManager.ReleaseKey(Key.ShiftLeft)); } + + [Test] + public void TestShiftModifierMaintainsAspectRatio() + { + HitCircle[] addedObjects = null!; + + float aspectRatioBeforeDrag = 0; + + float getAspectRatio() => (addedObjects[1].X - addedObjects[0].X) / (addedObjects[1].Y - addedObjects[0].Y); + + AddStep("add hitobjects", () => + { + EditorBeatmap.AddRange(addedObjects = new[] + { + new HitCircle { StartTime = 100, Position = new Vector2(150, 150) }, + new HitCircle { StartTime = 200, Position = new Vector2(250, 200) }, + }); + + aspectRatioBeforeDrag = getAspectRatio(); + }); + + AddStep("select objects", () => EditorBeatmap.SelectedHitObjects.AddRange(addedObjects)); + + AddStep("move mouse to handle", () => InputManager.MoveMouseTo(getScaleHandle(Anchor.BottomRight).ScreenSpaceDrawQuad.Centre)); + + AddStep("begin drag", () => InputManager.PressButton(MouseButton.Left)); + + AddStep("move mouse", () => InputManager.MoveMouseTo(InputManager.CurrentState.Mouse.Position + new Vector2(50))); + + AddStep("aspect ratio does not equal", () => Assert.AreNotEqual(aspectRatioBeforeDrag, getAspectRatio())); + + AddStep("press shift", () => InputManager.PressKey(Key.ShiftLeft)); + + AddStep("aspect ratio does equal", () => Assert.AreEqual(aspectRatioBeforeDrag, getAspectRatio())); + + AddStep("end drag", () => InputManager.ReleaseButton(MouseButton.Left)); + + AddStep("release shift", () => InputManager.ReleaseKey(Key.ShiftLeft)); + } + + [Test] + public void TestAltModifierScalesAroundCenter() + { + HitCircle[] addedObjects = null!; + + Vector2 centerBeforeDrag = Vector2.Zero; + + Vector2 getCenter() => (addedObjects[0].Position + addedObjects[1].Position) / 2; + + AddStep("add hitobjects", () => + { + EditorBeatmap.AddRange(addedObjects = new[] + { + new HitCircle { StartTime = 100, Position = new Vector2(150, 150) }, + new HitCircle { StartTime = 200, Position = new Vector2(250, 200) }, + }); + + centerBeforeDrag = getCenter(); + }); + + AddStep("select objects", () => EditorBeatmap.SelectedHitObjects.AddRange(addedObjects)); + + AddStep("move mouse to handle", () => InputManager.MoveMouseTo(getScaleHandle(Anchor.BottomRight).ScreenSpaceDrawQuad.Centre)); + + AddStep("begin drag", () => InputManager.PressButton(MouseButton.Left)); + + AddStep("move mouse", () => InputManager.MoveMouseTo(InputManager.CurrentState.Mouse.Position + new Vector2(50))); + + AddStep("center does not equal", () => Assert.AreNotEqual(centerBeforeDrag, getCenter())); + + AddStep("press alt", () => InputManager.PressKey(Key.AltLeft)); + + AddStep("center does equal", () => Assert.AreEqual(centerBeforeDrag, getCenter())); + + AddStep("end drag", () => InputManager.ReleaseButton(MouseButton.Left)); + + AddStep("release alt", () => InputManager.ReleaseKey(Key.AltLeft)); + } + + [Test] + public void TestShiftAndAltModifierKeys() + { + HitCircle[] addedObjects = null!; + + float aspectRatioBeforeDrag = 0; + + Vector2 centerBeforeDrag = Vector2.Zero; + + float getAspectRatio() => (addedObjects[1].X - addedObjects[0].X) / (addedObjects[1].Y - addedObjects[0].Y); + + Vector2 getCenter() => (addedObjects[0].Position + addedObjects[1].Position) / 2; + + AddStep("add hitobjects", () => + { + EditorBeatmap.AddRange(addedObjects = new[] + { + new HitCircle { StartTime = 100, Position = new Vector2(150, 150) }, + new HitCircle { StartTime = 200, Position = new Vector2(250, 200) }, + }); + + aspectRatioBeforeDrag = getAspectRatio(); + centerBeforeDrag = getCenter(); + }); + + AddStep("select objects", () => EditorBeatmap.SelectedHitObjects.AddRange(addedObjects)); + + AddStep("move mouse to handle", () => InputManager.MoveMouseTo(getScaleHandle(Anchor.BottomRight).ScreenSpaceDrawQuad.Centre)); + + AddStep("begin drag", () => InputManager.PressButton(MouseButton.Left)); + + AddStep("move mouse", () => InputManager.MoveMouseTo(InputManager.CurrentState.Mouse.Position + new Vector2(50))); + + AddStep("aspect ratio does not equal", () => Assert.AreNotEqual(aspectRatioBeforeDrag, getAspectRatio())); + + AddStep("center does not equal", () => Assert.AreNotEqual(centerBeforeDrag, getCenter())); + + AddStep("press shift", () => InputManager.PressKey(Key.ShiftLeft)); + + AddStep("aspect ratio does equal", () => Assert.AreEqual(aspectRatioBeforeDrag, getAspectRatio())); + + AddStep("center does not equal", () => Assert.AreNotEqual(centerBeforeDrag, getCenter())); + + AddStep("press alt", () => InputManager.PressKey(Key.AltLeft)); + + AddStep("center does equal", () => Assert.AreEqual(centerBeforeDrag, getCenter())); + + AddStep("end drag", () => InputManager.ReleaseButton(MouseButton.Left)); + + AddStep("release shift", () => InputManager.ReleaseKey(Key.ShiftLeft)); + + AddStep("release alt", () => InputManager.ReleaseKey(Key.AltLeft)); + } } } From 15c4b1dc8f81dd4db100854cde33f928db6307ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marvin=20Sch=C3=BCrz?= Date: Tue, 24 Sep 2024 13:45:03 +0200 Subject: [PATCH 2382/2493] Move mouse horizontally in test to make sure it doesn't accidentally maintain aspect ratio --- osu.Game.Tests/Visual/Editing/TestSceneComposerSelection.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneComposerSelection.cs b/osu.Game.Tests/Visual/Editing/TestSceneComposerSelection.cs index 3d7aef5a65..cbc9088d04 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneComposerSelection.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneComposerSelection.cs @@ -550,7 +550,7 @@ namespace osu.Game.Tests.Visual.Editing AddStep("begin drag", () => InputManager.PressButton(MouseButton.Left)); - AddStep("move mouse", () => InputManager.MoveMouseTo(InputManager.CurrentState.Mouse.Position + new Vector2(50))); + AddStep("move mouse", () => InputManager.MoveMouseTo(InputManager.CurrentState.Mouse.Position + new Vector2(50, 0))); AddStep("aspect ratio does not equal", () => Assert.AreNotEqual(aspectRatioBeforeDrag, getAspectRatio())); @@ -589,7 +589,7 @@ namespace osu.Game.Tests.Visual.Editing AddStep("begin drag", () => InputManager.PressButton(MouseButton.Left)); - AddStep("move mouse", () => InputManager.MoveMouseTo(InputManager.CurrentState.Mouse.Position + new Vector2(50))); + AddStep("move mouse", () => InputManager.MoveMouseTo(InputManager.CurrentState.Mouse.Position + new Vector2(50, 0))); AddStep("center does not equal", () => Assert.AreNotEqual(centerBeforeDrag, getCenter())); @@ -633,7 +633,7 @@ namespace osu.Game.Tests.Visual.Editing AddStep("begin drag", () => InputManager.PressButton(MouseButton.Left)); - AddStep("move mouse", () => InputManager.MoveMouseTo(InputManager.CurrentState.Mouse.Position + new Vector2(50))); + AddStep("move mouse", () => InputManager.MoveMouseTo(InputManager.CurrentState.Mouse.Position + new Vector2(50, 0))); AddStep("aspect ratio does not equal", () => Assert.AreNotEqual(aspectRatioBeforeDrag, getAspectRatio())); From 7f8b64bb6db05cacb52a1224a2d07c4fd4b9b5f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 27 Aug 2024 15:59:42 +0200 Subject: [PATCH 2383/2493] Redesign directory & file selector (and update usages accordingly) --- .../Settings/TestSceneDirectorySelector.cs | 5 ++ .../Visual/Settings/TestSceneFileSelector.cs | 34 +++++--- .../Screens/Setup/StablePathSelectScreen.cs | 3 + .../UserInterfaceV2/OsuDirectorySelector.cs | 43 ++++++++-- .../OsuDirectorySelectorBreadcrumbDisplay.cs | 79 ++++++++++++++++--- .../OsuDirectorySelectorDirectory.cs | 31 +------- .../OsuDirectorySelectorHiddenToggle.cs | 3 +- .../OsuDirectorySelectorParentDirectory.cs | 8 ++ .../UserInterfaceV2/OsuFileSelector.cs | 52 +++++++++--- .../FirstRunSetup/ScreenImportFromStable.cs | 8 ++ .../Maintenance/DirectorySelectScreen.cs | 7 +- .../Screens/Edit/Setup/LabelledFileChooser.cs | 9 +++ osu.Game/Screens/Import/FileImportScreen.cs | 12 +-- 13 files changed, 220 insertions(+), 74 deletions(-) diff --git a/osu.Game.Tests/Visual/Settings/TestSceneDirectorySelector.cs b/osu.Game.Tests/Visual/Settings/TestSceneDirectorySelector.cs index 3ef0ffc13a..03ecd4af61 100644 --- a/osu.Game.Tests/Visual/Settings/TestSceneDirectorySelector.cs +++ b/osu.Game.Tests/Visual/Settings/TestSceneDirectorySelector.cs @@ -9,6 +9,11 @@ namespace osu.Game.Tests.Visual.Settings { public partial class TestSceneDirectorySelector : ThemeComparisonTestScene { + public TestSceneDirectorySelector() + : base(false) + { + } + protected override Drawable CreateContent() => new OsuDirectorySelector { RelativeSizeAxes = Axes.Both diff --git a/osu.Game.Tests/Visual/Settings/TestSceneFileSelector.cs b/osu.Game.Tests/Visual/Settings/TestSceneFileSelector.cs index c70277987e..cf8a589152 100644 --- a/osu.Game.Tests/Visual/Settings/TestSceneFileSelector.cs +++ b/osu.Game.Tests/Visual/Settings/TestSceneFileSelector.cs @@ -1,37 +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 System; using NUnit.Framework; -using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Shapes; -using osu.Game.Graphics; using osu.Game.Graphics.UserInterfaceV2; +using osu.Game.Overlays; using osu.Game.Tests.Visual.UserInterface; namespace osu.Game.Tests.Visual.Settings { public partial class TestSceneFileSelector : ThemeComparisonTestScene { - [Resolved] - private OsuColour colours { get; set; } = null!; + public TestSceneFileSelector() + : base(false) + { + } [Test] public void TestJpgFilesOnly() { AddStep("create", () => { - ContentContainer.Children = new Drawable[] + var colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine); + + ContentContainer.Child = new DependencyProvidingContainer { - new Box + RelativeSizeAxes = Axes.Both, + CachedDependencies = new (Type, object)[] { - RelativeSizeAxes = Axes.Both, - Colour = colours.GreySeaFoam + (typeof(OverlayColourProvider), colourProvider) }, - new OsuFileSelector(validFileExtensions: new[] { ".jpg" }) + Children = new Drawable[] { - RelativeSizeAxes = Axes.Both, - }, + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background3 + }, + new OsuFileSelector(validFileExtensions: new[] { ".jpg" }) + { + RelativeSizeAxes = Axes.Both, + }, + } }; }); } diff --git a/osu.Game.Tournament/Screens/Setup/StablePathSelectScreen.cs b/osu.Game.Tournament/Screens/Setup/StablePathSelectScreen.cs index 74404e06f8..91b03ed085 100644 --- a/osu.Game.Tournament/Screens/Setup/StablePathSelectScreen.cs +++ b/osu.Game.Tournament/Screens/Setup/StablePathSelectScreen.cs @@ -27,6 +27,9 @@ namespace osu.Game.Tournament.Screens.Setup [Resolved] private MatchIPCInfo ipc { get; set; } = null!; + [Cached] + private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Blue); + private OsuDirectorySelector directorySelector = null!; private DialogOverlay? overlay; diff --git a/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelector.cs b/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelector.cs index 21f926ba42..4002480e7f 100644 --- a/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelector.cs +++ b/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelector.cs @@ -7,14 +7,18 @@ using System.IO; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.UserInterface; using osu.Game.Graphics.Containers; +using osu.Game.Overlays; namespace osu.Game.Graphics.UserInterfaceV2 { public partial class OsuDirectorySelector : DirectorySelector { - public const float ITEM_HEIGHT = 20; + public const float ITEM_HEIGHT = 16; + + private Box hiddenToggleBackground = null!; public OsuDirectorySelector(string initialPath = null) : base(initialPath) @@ -22,16 +26,45 @@ namespace osu.Game.Graphics.UserInterfaceV2 } [BackgroundDependencyLoader] - private void load() + private void load(OverlayColourProvider colourProvider) { - Padding = new MarginPadding(10); + AddInternal(new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background5, + Depth = float.MaxValue, + }); + + hiddenToggleBackground.Colour = colourProvider.Background4; } - protected override ScrollContainer CreateScrollContainer() => new OsuScrollContainer(); + protected override ScrollContainer CreateScrollContainer() => new OsuScrollContainer + { + Padding = new MarginPadding + { + Horizontal = 20, + Vertical = 15, + } + }; protected override DirectorySelectorBreadcrumbDisplay CreateBreadcrumb() => new OsuDirectorySelectorBreadcrumbDisplay(); - protected override Drawable CreateHiddenToggleButton() => new OsuDirectorySelectorHiddenToggle { Current = { BindTarget = ShowHiddenItems } }; + protected override Drawable CreateHiddenToggleButton() => new Container + { + RelativeSizeAxes = Axes.Y, + AutoSizeAxes = Axes.X, + Children = new Drawable[] + { + hiddenToggleBackground = new Box + { + RelativeSizeAxes = Axes.Both, + }, + new OsuDirectorySelectorHiddenToggle + { + Current = { BindTarget = ShowHiddenItems }, + }, + } + }; protected override DirectorySelectorDirectory CreateParentDirectoryItem(DirectoryInfo directory) => new OsuDirectorySelectorParentDirectory(directory); diff --git a/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelectorBreadcrumbDisplay.cs b/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelectorBreadcrumbDisplay.cs index 0917b9db97..5b52663198 100644 --- a/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelectorBreadcrumbDisplay.cs +++ b/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelectorBreadcrumbDisplay.cs @@ -6,28 +6,48 @@ using System.IO; using osu.Framework.Allocation; 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.Sprites; +using osu.Game.Graphics.UserInterface; +using osu.Game.Overlays; using osuTK; namespace osu.Game.Graphics.UserInterfaceV2 { internal partial class OsuDirectorySelectorBreadcrumbDisplay : DirectorySelectorBreadcrumbDisplay { - protected override Drawable CreateCaption() => new OsuSpriteText + public const float HEIGHT = 45; + public const float HORIZONTAL_PADDING = 20; + + protected override Drawable CreateCaption() => Empty().With(d => { - Text = "Current Directory: ", - Font = OsuFont.Default.With(size: OsuDirectorySelector.ITEM_HEIGHT), - }; + d.Origin = Anchor.CentreLeft; + d.Anchor = Anchor.CentreLeft; + d.Alpha = 0; + }); protected override DirectorySelectorDirectory CreateRootDirectoryItem() => new OsuBreadcrumbDisplayComputer(); protected override DirectorySelectorDirectory CreateDirectoryItem(DirectoryInfo directory, string displayName = null) => new OsuBreadcrumbDisplayDirectory(directory, displayName); - public OsuDirectorySelectorBreadcrumbDisplay() + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider) { - Padding = new MarginPadding(15); + ((FillFlowContainer)InternalChild).Padding = new MarginPadding + { + Horizontal = HORIZONTAL_PADDING, + Vertical = 10, + }; + + AddInternal(new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background4, + Depth = 1, + }); } private partial class OsuBreadcrumbDisplayComputer : OsuBreadcrumbDisplayDirectory @@ -40,26 +60,67 @@ namespace osu.Game.Graphics.UserInterfaceV2 } } - private partial class OsuBreadcrumbDisplayDirectory : OsuDirectorySelectorDirectory + private partial class OsuBreadcrumbDisplayDirectory : DirectorySelectorDirectory { public OsuBreadcrumbDisplayDirectory(DirectoryInfo directory, string displayName = null) : base(directory, displayName) { } + [Resolved] + private OverlayColourProvider colourProvider { get; set; } + [BackgroundDependencyLoader] private void load() { + Anchor = Anchor.CentreLeft; + Origin = Anchor.CentreLeft; + + Flow.AutoSizeAxes = Axes.X; + Flow.Height = 25; + Flow.Margin = new MarginPadding { Horizontal = 10, }; + + AddRangeInternal(new Drawable[] + { + new Background + { + Depth = 1 + }, + new HoverClickSounds(), + }); + Flow.Add(new SpriteIcon { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, Icon = FontAwesome.Solid.ChevronRight, - Size = new Vector2(FONT_SIZE / 2) + Size = new Vector2(FONT_SIZE / 2), + Margin = new MarginPadding { Left = 5, }, }); + Flow.Colour = colourProvider.Light3; } - protected override IconUsage? Icon => Directory.Name.Contains(Path.DirectorySeparatorChar) ? base.Icon : null; + protected override SpriteText CreateSpriteText() => new OsuSpriteText().With(t => t.Font = OsuFont.Default.With(weight: FontWeight.SemiBold)); + + protected override IconUsage? Icon => Directory.Name.Contains(Path.DirectorySeparatorChar) ? FontAwesome.Solid.Database : null; + + internal partial class Background : CompositeDrawable + { + [BackgroundDependencyLoader] + private void load(OverlayColourProvider overlayColourProvider) + { + RelativeSizeAxes = Axes.Both; + + Masking = true; + CornerRadius = 5; + + InternalChild = new Box + { + Colour = overlayColourProvider.Background3, + RelativeSizeAxes = Axes.Both, + }; + } + } } } } diff --git a/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelectorDirectory.cs b/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelectorDirectory.cs index 932017b03e..a1d76dd7e3 100644 --- a/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelectorDirectory.cs +++ b/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelectorDirectory.cs @@ -6,13 +6,10 @@ using System.IO; using osu.Framework.Allocation; 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.Sprites; using osu.Game.Graphics.UserInterface; -using osu.Game.Overlays; namespace osu.Game.Graphics.UserInterfaceV2 { @@ -24,43 +21,23 @@ namespace osu.Game.Graphics.UserInterfaceV2 } [BackgroundDependencyLoader] - private void load() + private void load(OsuColour colours) { Flow.AutoSizeAxes = Axes.X; Flow.Height = OsuDirectorySelector.ITEM_HEIGHT; AddRangeInternal(new Drawable[] { - new Background - { - Depth = 1 - }, new HoverClickSounds() }); + + Colour = colours.Orange1; } - protected override SpriteText CreateSpriteText() => new OsuSpriteText(); + protected override SpriteText CreateSpriteText() => new OsuSpriteText().With(t => t.Font = OsuFont.Default.With(weight: FontWeight.Bold)); protected override IconUsage? Icon => Directory.Name.Contains(Path.DirectorySeparatorChar) ? FontAwesome.Solid.Database : FontAwesome.Regular.Folder; - - internal partial class Background : CompositeDrawable - { - [BackgroundDependencyLoader(true)] - private void load(OverlayColourProvider overlayColourProvider, OsuColour colours) - { - RelativeSizeAxes = Axes.Both; - - Masking = true; - CornerRadius = 5; - - InternalChild = new Box - { - Colour = overlayColourProvider?.Background5 ?? colours.GreySeaFoamDarker, - RelativeSizeAxes = Axes.Both, - }; - } - } } } diff --git a/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelectorHiddenToggle.cs b/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelectorHiddenToggle.cs index 7665ed507f..ffedc9386f 100644 --- a/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelectorHiddenToggle.cs +++ b/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelectorHiddenToggle.cs @@ -16,7 +16,8 @@ namespace osu.Game.Graphics.UserInterfaceV2 { RelativeSizeAxes = Axes.None; AutoSizeAxes = Axes.None; - Size = new Vector2(100, 50); + Size = new Vector2(100, OsuDirectorySelectorBreadcrumbDisplay.HEIGHT); + Margin = new MarginPadding { Right = OsuDirectorySelectorBreadcrumbDisplay.HORIZONTAL_PADDING, }; Anchor = Anchor.CentreLeft; Origin = Anchor.CentreLeft; LabelTextFlowContainer.Anchor = Anchor.CentreLeft; diff --git a/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelectorParentDirectory.cs b/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelectorParentDirectory.cs index c0ac9f21ca..d274a0ecfe 100644 --- a/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelectorParentDirectory.cs +++ b/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelectorParentDirectory.cs @@ -2,7 +2,9 @@ // See the LICENCE file in the repository root for full licence text. using System.IO; +using osu.Framework.Allocation; using osu.Framework.Graphics.Sprites; +using osu.Game.Overlays; namespace osu.Game.Graphics.UserInterfaceV2 { @@ -14,5 +16,11 @@ namespace osu.Game.Graphics.UserInterfaceV2 : base(directory, "..") { } + + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider) + { + Colour = colourProvider.Content1; + } } } diff --git a/osu.Game/Graphics/UserInterfaceV2/OsuFileSelector.cs b/osu.Game/Graphics/UserInterfaceV2/OsuFileSelector.cs index 7097102335..feedeb8ff3 100644 --- a/osu.Game/Graphics/UserInterfaceV2/OsuFileSelector.cs +++ b/osu.Game/Graphics/UserInterfaceV2/OsuFileSelector.cs @@ -8,32 +8,64 @@ using System.Linq; using osu.Framework.Allocation; 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.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; +using osu.Game.Overlays; namespace osu.Game.Graphics.UserInterfaceV2 { public partial class OsuFileSelector : FileSelector { + private Box hiddenToggleBackground = null!; + public OsuFileSelector(string initialPath = null, string[] validFileExtensions = null) - : base(initialPath, validFileExtensions) { } [BackgroundDependencyLoader] - private void load() + private void load(OverlayColourProvider colourProvider) { - Padding = new MarginPadding(10); + AddInternal(new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background5, + Depth = float.MaxValue, + }); + + hiddenToggleBackground.Colour = colourProvider.Background4; } - protected override ScrollContainer CreateScrollContainer() => new OsuScrollContainer(); + protected override ScrollContainer CreateScrollContainer() => new OsuScrollContainer + { + Padding = new MarginPadding + { + Horizontal = 20, + Vertical = 15, + } + }; protected override DirectorySelectorBreadcrumbDisplay CreateBreadcrumb() => new OsuDirectorySelectorBreadcrumbDisplay(); - protected override Drawable CreateHiddenToggleButton() => new OsuDirectorySelectorHiddenToggle { Current = { BindTarget = ShowHiddenItems } }; + protected override Drawable CreateHiddenToggleButton() => new Container + { + RelativeSizeAxes = Axes.Y, + AutoSizeAxes = Axes.X, + Children = new Drawable[] + { + hiddenToggleBackground = new Box + { + RelativeSizeAxes = Axes.Both, + }, + new OsuDirectorySelectorHiddenToggle + { + Current = { BindTarget = ShowHiddenItems }, + }, + } + }; protected override DirectorySelectorDirectory CreateParentDirectoryItem(DirectoryInfo directory) => new OsuDirectorySelectorParentDirectory(directory); @@ -51,19 +83,17 @@ namespace osu.Game.Graphics.UserInterfaceV2 } [BackgroundDependencyLoader] - private void load() + private void load(OverlayColourProvider colourProvider) { Flow.AutoSizeAxes = Axes.X; Flow.Height = OsuDirectorySelector.ITEM_HEIGHT; AddRangeInternal(new Drawable[] { - new OsuDirectorySelectorDirectory.Background - { - Depth = 1 - }, new HoverClickSounds() }); + + Colour = colourProvider.Light3; } protected override IconUsage? Icon @@ -91,7 +121,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 } } - protected override SpriteText CreateSpriteText() => new OsuSpriteText(); + protected override SpriteText CreateSpriteText() => new OsuSpriteText().With(t => t.Font = OsuFont.Default.With(weight: FontWeight.SemiBold)); } } } diff --git a/osu.Game/Overlays/FirstRunSetup/ScreenImportFromStable.cs b/osu.Game/Overlays/FirstRunSetup/ScreenImportFromStable.cs index 983cb0bbb4..5eb38b6e11 100644 --- a/osu.Game/Overlays/FirstRunSetup/ScreenImportFromStable.cs +++ b/osu.Game/Overlays/FirstRunSetup/ScreenImportFromStable.cs @@ -314,6 +314,7 @@ namespace osu.Game.Overlays.FirstRunSetup private partial class DirectoryChooserPopover : OsuPopover { public DirectoryChooserPopover(Bindable currentDirectory) + : base(false) { Child = new Container { @@ -325,6 +326,13 @@ namespace osu.Game.Overlays.FirstRunSetup }, }; } + + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider) + { + Body.BorderColour = colourProvider.Highlight1; + Body.BorderThickness = 2; + } } } } diff --git a/osu.Game/Overlays/Settings/Sections/Maintenance/DirectorySelectScreen.cs b/osu.Game/Overlays/Settings/Sections/Maintenance/DirectorySelectScreen.cs index e87ca32bf6..f6f8d3b336 100644 --- a/osu.Game/Overlays/Settings/Sections/Maintenance/DirectorySelectScreen.cs +++ b/osu.Game/Overlays/Settings/Sections/Maintenance/DirectorySelectScreen.cs @@ -48,8 +48,11 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance /// protected virtual DirectoryInfo InitialPath => null; + [Cached] + private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple); + [BackgroundDependencyLoader] - private void load(OsuColour colours) + private void load() { InternalChild = new Container { @@ -64,7 +67,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance new Box { RelativeSizeAxes = Axes.Both, - Colour = colours.GreySeaFoamDark + Colour = colourProvider.Background4, }, new GridContainer { diff --git a/osu.Game/Screens/Edit/Setup/LabelledFileChooser.cs b/osu.Game/Screens/Edit/Setup/LabelledFileChooser.cs index 61f33c4bdc..a113ca5407 100644 --- a/osu.Game/Screens/Edit/Setup/LabelledFileChooser.cs +++ b/osu.Game/Screens/Edit/Setup/LabelledFileChooser.cs @@ -18,6 +18,7 @@ using osu.Framework.Localisation; using osu.Framework.Platform; using osu.Game.Database; using osu.Game.Graphics.UserInterfaceV2; +using osu.Game.Overlays; using osuTK; namespace osu.Game.Screens.Edit.Setup @@ -118,6 +119,7 @@ namespace osu.Game.Screens.Edit.Setup protected override string PopOutSampleName => "UI/overlay-big-pop-out"; public FileChooserPopover(string[] handledExtensions, Bindable currentFile, string? chooserPath) + : base(false) { Child = new Container { @@ -129,6 +131,13 @@ namespace osu.Game.Screens.Edit.Setup }, }; } + + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider) + { + Body.BorderColour = colourProvider.Highlight1; + Body.BorderThickness = 2; + } } } } diff --git a/osu.Game/Screens/Import/FileImportScreen.cs b/osu.Game/Screens/Import/FileImportScreen.cs index 6b7a269d12..1bdacae87f 100644 --- a/osu.Game/Screens/Import/FileImportScreen.cs +++ b/osu.Game/Screens/Import/FileImportScreen.cs @@ -15,6 +15,7 @@ using osu.Framework.Screens; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterfaceV2; +using osu.Game.Overlays; using osuTK; namespace osu.Game.Screens.Import @@ -36,8 +37,8 @@ namespace osu.Game.Screens.Import [Resolved] private OsuGameBase game { get; set; } - [Resolved] - private OsuColour colours { get; set; } + [Cached] + private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple); [BackgroundDependencyLoader(true)] private void load() @@ -52,11 +53,6 @@ namespace osu.Game.Screens.Import Size = new Vector2(0.9f, 0.8f), Children = new Drawable[] { - new Box - { - Colour = colours.GreySeaFoamDark, - RelativeSizeAxes = Axes.Both, - }, fileSelector = new OsuFileSelector(validFileExtensions: game.HandledExtensions.ToArray()) { RelativeSizeAxes = Axes.Both, @@ -72,7 +68,7 @@ namespace osu.Game.Screens.Import { new Box { - Colour = colours.GreySeaFoamDarker, + Colour = colourProvider.Background4, RelativeSizeAxes = Axes.Both }, new Container From 16fc413a4ac0a933dda126bf275b610cf1b1eef5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 27 Aug 2024 16:01:49 +0200 Subject: [PATCH 2384/2493] Apply NRT to directory & file selectors --- osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelector.cs | 6 ++---- .../OsuDirectorySelectorBreadcrumbDisplay.cs | 8 +++----- .../UserInterfaceV2/OsuDirectorySelectorDirectory.cs | 4 +--- osu.Game/Graphics/UserInterfaceV2/OsuFileSelector.cs | 7 +++---- 4 files changed, 9 insertions(+), 16 deletions(-) diff --git a/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelector.cs b/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelector.cs index 4002480e7f..85599a5d45 100644 --- a/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelector.cs +++ b/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelector.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.IO; using osu.Framework.Allocation; using osu.Framework.Graphics; @@ -20,7 +18,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 private Box hiddenToggleBackground = null!; - public OsuDirectorySelector(string initialPath = null) + public OsuDirectorySelector(string? initialPath = null) : base(initialPath) { } @@ -68,7 +66,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 protected override DirectorySelectorDirectory CreateParentDirectoryItem(DirectoryInfo directory) => new OsuDirectorySelectorParentDirectory(directory); - protected override DirectorySelectorDirectory CreateDirectoryItem(DirectoryInfo directory, string displayName = null) => new OsuDirectorySelectorDirectory(directory, displayName); + protected override DirectorySelectorDirectory CreateDirectoryItem(DirectoryInfo directory, string? displayName = null) => new OsuDirectorySelectorDirectory(directory, displayName); protected override void NotifySelectionError() => this.FlashColour(Colour4.Red, 300); } diff --git a/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelectorBreadcrumbDisplay.cs b/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelectorBreadcrumbDisplay.cs index 5b52663198..e91076498c 100644 --- a/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelectorBreadcrumbDisplay.cs +++ b/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelectorBreadcrumbDisplay.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.IO; using osu.Framework.Allocation; using osu.Framework.Graphics; @@ -31,7 +29,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 protected override DirectorySelectorDirectory CreateRootDirectoryItem() => new OsuBreadcrumbDisplayComputer(); - protected override DirectorySelectorDirectory CreateDirectoryItem(DirectoryInfo directory, string displayName = null) => new OsuBreadcrumbDisplayDirectory(directory, displayName); + protected override DirectorySelectorDirectory CreateDirectoryItem(DirectoryInfo directory, string? displayName = null) => new OsuBreadcrumbDisplayDirectory(directory, displayName); [BackgroundDependencyLoader] private void load(OverlayColourProvider colourProvider) @@ -62,13 +60,13 @@ namespace osu.Game.Graphics.UserInterfaceV2 private partial class OsuBreadcrumbDisplayDirectory : DirectorySelectorDirectory { - public OsuBreadcrumbDisplayDirectory(DirectoryInfo directory, string displayName = null) + public OsuBreadcrumbDisplayDirectory(DirectoryInfo? directory, string? displayName = null) : base(directory, displayName) { } [Resolved] - private OverlayColourProvider colourProvider { get; set; } + private OverlayColourProvider colourProvider { get; set; } = null!; [BackgroundDependencyLoader] private void load() diff --git a/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelectorDirectory.cs b/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelectorDirectory.cs index a1d76dd7e3..a36804658a 100644 --- a/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelectorDirectory.cs +++ b/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelectorDirectory.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.IO; using osu.Framework.Allocation; using osu.Framework.Graphics; @@ -15,7 +13,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 { internal partial class OsuDirectorySelectorDirectory : DirectorySelectorDirectory { - public OsuDirectorySelectorDirectory(DirectoryInfo directory, string displayName = null) + public OsuDirectorySelectorDirectory(DirectoryInfo directory, string? displayName = null) : base(directory, displayName) { } diff --git a/osu.Game/Graphics/UserInterfaceV2/OsuFileSelector.cs b/osu.Game/Graphics/UserInterfaceV2/OsuFileSelector.cs index feedeb8ff3..7ce5f63656 100644 --- a/osu.Game/Graphics/UserInterfaceV2/OsuFileSelector.cs +++ b/osu.Game/Graphics/UserInterfaceV2/OsuFileSelector.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.IO; using System.Linq; using osu.Framework.Allocation; @@ -22,7 +20,8 @@ namespace osu.Game.Graphics.UserInterfaceV2 { private Box hiddenToggleBackground = null!; - public OsuFileSelector(string initialPath = null, string[] validFileExtensions = null) + public OsuFileSelector(string? initialPath = null, string[]? validFileExtensions = null) + : base(initialPath, validFileExtensions) { } @@ -69,7 +68,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 protected override DirectorySelectorDirectory CreateParentDirectoryItem(DirectoryInfo directory) => new OsuDirectorySelectorParentDirectory(directory); - protected override DirectorySelectorDirectory CreateDirectoryItem(DirectoryInfo directory, string displayName = null) => new OsuDirectorySelectorDirectory(directory, displayName); + protected override DirectorySelectorDirectory CreateDirectoryItem(DirectoryInfo directory, string? displayName = null) => new OsuDirectorySelectorDirectory(directory, displayName); protected override DirectoryListingFile CreateFileItem(FileInfo file) => new OsuDirectoryListingFile(file); From 75dc822540c882ad2cfa867cba7ec687d3f3d814 Mon Sep 17 00:00:00 2001 From: StanR Date: Tue, 24 Sep 2024 17:57:31 +0500 Subject: [PATCH 2385/2493] Adjust some multipliers --- .../Difficulty/Evaluators/RhythmEvaluator.cs | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs index abf9dbe725..7bda1c29a3 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs @@ -13,9 +13,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators public static class RhythmEvaluator { private const int history_time_max = 4 * 1000; // 4 seconds - private const int history_objects_max = 24; - private const double rhythm_overall_multiplier = 0.95; - private const double rhythm_ratio_multiplier = 14.0; + private const int history_objects_max = 32; + private const double rhythm_overall_multiplier = 0.9; + private const double rhythm_ratio_multiplier = 11.5; /// /// Calculates a rhythm multiplier for the difficulty of the tap associated with historic data of the current . @@ -78,15 +78,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators double effectiveRatio = windowPenalty * currRatio * fractionMultiplier; - // bpm change is into slider, this is easy acc window - if (currObj.BaseObject is Slider) - effectiveRatio *= 0.125; - - // bpm change was from a slider, this is easier typically than circle -> circle - // unintentional side effect is that bursts with kicksliders at the ends might have lower difficulty than bursts without sliders - if (prevObj.BaseObject is Slider) - effectiveRatio *= 0.2; - if (firstDeltaSwitch) { if (Math.Abs(prevDelta - currDelta) < deltaDifferenceEpsilon) @@ -96,6 +87,15 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators } else { + // bpm change is into slider, this is easy acc window + if (currObj.BaseObject is Slider) + effectiveRatio *= 0.125; + + // bpm change was from a slider, this is easier typically than circle -> circle + // unintentional side effect is that bursts with kicksliders at the ends might have lower difficulty than bursts without sliders + if (prevObj.BaseObject is Slider) + effectiveRatio *= 0.15; + // repeated island polarity (2 -> 4, 3 -> 5) if (island.IsSimilarPolarity(previousIsland)) effectiveRatio *= 0.3; @@ -119,7 +119,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators islandCount.Count++; // repeated island (ex: triplet -> triplet) - double power = logistic(island.Delta, 4, 0.165, 10); + double power = logistic(island.Delta, 2, 0.165, 10); effectiveRatio *= Math.Min(3.0 / islandCount.Count, Math.Pow(1.0 / islandCount.Count, power)); islandCounts[countIndex] = (islandCount.Island, islandCount.Count); From 9f4e48dde78eb1b76c6131999aa21aeb4dc42843 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 24 Sep 2024 15:15:28 +0200 Subject: [PATCH 2386/2493] Actually use bindables rather than stick things in `Update()` --- osu.Game/Screens/Play/HUD/ArgonSongProgress.cs | 2 +- osu.Game/Screens/Play/HUD/DefaultSongProgress.cs | 3 +-- osu.Game/Skinning/Components/BoxElement.cs | 8 +++++++- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/ArgonSongProgress.cs b/osu.Game/Screens/Play/HUD/ArgonSongProgress.cs index 1a18466743..92ac863e98 100644 --- a/osu.Game/Screens/Play/HUD/ArgonSongProgress.cs +++ b/osu.Game/Screens/Play/HUD/ArgonSongProgress.cs @@ -98,6 +98,7 @@ namespace osu.Game.Screens.Play.HUD Interactive.BindValueChanged(_ => bar.Interactive = Interactive.Value, true); ShowGraph.BindValueChanged(_ => updateGraphVisibility(), true); ShowTime.BindValueChanged(_ => info.FadeTo(ShowTime.Value ? 1 : 0, 200, Easing.In), true); + AccentColour.BindValueChanged(_ => Colour = AccentColour.Value, true); } protected override void UpdateObjects(IEnumerable objects) @@ -118,7 +119,6 @@ namespace osu.Game.Screens.Play.HUD base.Update(); content.Height = bar.Height + bar_height + info.Height; graphContainer.Height = bar.Height; - Colour = AccentColour.Value; } protected override void UpdateProgress(double progress, bool isIntro) diff --git a/osu.Game/Screens/Play/HUD/DefaultSongProgress.cs b/osu.Game/Screens/Play/HUD/DefaultSongProgress.cs index 93d75a22ba..4e41901ee3 100644 --- a/osu.Game/Screens/Play/HUD/DefaultSongProgress.cs +++ b/osu.Game/Screens/Play/HUD/DefaultSongProgress.cs @@ -90,6 +90,7 @@ namespace osu.Game.Screens.Play.HUD Interactive.BindValueChanged(_ => updateBarVisibility(), true); ShowGraph.BindValueChanged(_ => updateGraphVisibility(), true); ShowTime.BindValueChanged(_ => updateTimeVisibility(), true); + AccentColour.BindValueChanged(_ => Colour = AccentColour.Value, true); base.LoadComplete(); } @@ -118,8 +119,6 @@ namespace osu.Game.Screens.Play.HUD if (!Precision.AlmostEquals(Height, newHeight, 5f)) content.Height = newHeight; - - Colour = AccentColour.Value; } private void updateBarVisibility() diff --git a/osu.Game/Skinning/Components/BoxElement.cs b/osu.Game/Skinning/Components/BoxElement.cs index 633fb0c327..7f052a8523 100644 --- a/osu.Game/Skinning/Components/BoxElement.cs +++ b/osu.Game/Skinning/Components/BoxElement.cs @@ -46,12 +46,18 @@ namespace osu.Game.Skinning.Components Masking = true; } + protected override void LoadComplete() + { + base.LoadComplete(); + + AccentColour.BindValueChanged(_ => Colour = AccentColour.Value, true); + } + protected override void Update() { base.Update(); base.CornerRadius = CornerRadius.Value * Math.Min(DrawWidth, DrawHeight); - Colour = AccentColour.Value; } } } From 872628b8b87c73d21579a2ffd7aa26058cd6c551 Mon Sep 17 00:00:00 2001 From: StanR Date: Tue, 24 Sep 2024 18:24:00 +0500 Subject: [PATCH 2387/2493] Some extra tweaking --- .../Difficulty/Evaluators/RhythmEvaluator.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs index 7bda1c29a3..edb67f37d9 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs @@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators { private const int history_time_max = 4 * 1000; // 4 seconds private const int history_objects_max = 32; - private const double rhythm_overall_multiplier = 0.9; + private const double rhythm_overall_multiplier = 0.92; private const double rhythm_ratio_multiplier = 11.5; /// @@ -119,7 +119,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators islandCount.Count++; // repeated island (ex: triplet -> triplet) - double power = logistic(island.Delta, 2, 0.165, 10); + double power = logistic(island.Delta, 2.75, 0.24, 14); effectiveRatio *= Math.Min(3.0 / islandCount.Count, Math.Pow(1.0 / islandCount.Count, power)); islandCounts[countIndex] = (islandCount.Island, islandCount.Count); From 99a80b399cbeb1ac3a95bbec1a1ca2639f920caf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marvin=20Sch=C3=BCrz?= Date: Tue, 24 Sep 2024 16:42:37 +0200 Subject: [PATCH 2388/2493] Animate SelectionBox buttons on unfreeze --- .../Edit/Compose/Components/SelectionBox.cs | 55 +++++++++++++++---- 1 file changed, 44 insertions(+), 11 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs index 4eae2b77f6..d685fe74b0 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionBox.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; @@ -370,10 +371,14 @@ namespace osu.Game.Screens.Edit.Compose.Components private void unfreezeButtonPosition() { - frozenButtonsPosition = null; + if (frozenButtonsPosition != null) + { + frozenButtonsPosition = null; + ensureButtonsOnScreen(true); + } } - private void ensureButtonsOnScreen() + private void ensureButtonsOnScreen(bool animated = false) { if (frozenButtonsPosition != null) { @@ -384,7 +389,8 @@ namespace osu.Game.Screens.Edit.Compose.Components return; } - buttons.Position = Vector2.Zero; + if (!animated && buttons.Transforms.Any()) + return; var thisQuad = ScreenSpaceDrawQuad; @@ -399,24 +405,51 @@ namespace osu.Game.Screens.Edit.Compose.Components float minHeight = buttons.ScreenSpaceDrawQuad.Height; + Anchor targetAnchor; + Anchor targetOrigin; + Vector2 targetPosition = Vector2.Zero; + if (topExcess < minHeight && bottomExcess < minHeight) { - buttons.Anchor = Anchor.BottomCentre; - buttons.Origin = Anchor.BottomCentre; - buttons.Y = Math.Min(0, ToLocalSpace(Parent!.ScreenSpaceDrawQuad.BottomLeft).Y - DrawHeight); + targetAnchor = Anchor.BottomCentre; + targetOrigin = Anchor.BottomCentre; + targetPosition.Y = Math.Min(0, ToLocalSpace(Parent!.ScreenSpaceDrawQuad.BottomLeft).Y - DrawHeight); } else if (topExcess > bottomExcess) { - buttons.Anchor = Anchor.TopCentre; - buttons.Origin = Anchor.BottomCentre; + targetAnchor = Anchor.TopCentre; + targetOrigin = Anchor.BottomCentre; } else { - buttons.Anchor = Anchor.BottomCentre; - buttons.Origin = Anchor.TopCentre; + targetAnchor = Anchor.BottomCentre; + targetOrigin = Anchor.TopCentre; } - buttons.X += ToLocalSpace(thisQuad.TopLeft - new Vector2(Math.Min(0, leftExcess)) + new Vector2(Math.Min(0, rightExcess))).X; + targetPosition.X += ToLocalSpace(thisQuad.TopLeft - new Vector2(Math.Min(0, leftExcess)) + new Vector2(Math.Min(0, rightExcess))).X; + + if (animated) + { + var originalPosition = ToLocalSpace(buttons.ScreenSpaceDrawQuad.TopLeft); + + buttons.Origin = targetOrigin; + buttons.Anchor = targetAnchor; + buttons.Position = targetPosition; + + var newPosition = ToLocalSpace(buttons.ScreenSpaceDrawQuad.TopLeft); + + var delta = newPosition - originalPosition; + + buttons.Position -= delta; + + buttons.MoveTo(targetPosition, 300, Easing.OutQuint); + } + else + { + buttons.Anchor = targetAnchor; + buttons.Origin = targetOrigin; + buttons.Position = targetPosition; + } } } } From 555d4ffe897e9a55c674064116b51a9bd08f106d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 24 Sep 2024 17:51:54 +0200 Subject: [PATCH 2389/2493] Add failing test case --- .../Ranking/TestSceneStatisticsPanel.cs | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs b/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs index acfa519c81..f46f76cbb8 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs @@ -5,14 +5,18 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Threading; using NUnit.Framework; +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.Beatmaps; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; using osu.Game.Online; using osu.Game.Rulesets; using osu.Game.Rulesets.Difficulty; @@ -23,6 +27,7 @@ using osu.Game.Screens.Ranking.Statistics; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI; +using osu.Game.Screens.Ranking.Statistics.User; using osu.Game.Tests.Resources; using osu.Game.Users; using osuTK; @@ -80,6 +85,69 @@ namespace osu.Game.Tests.Visual.Ranking loadPanel(null); } + [Test] + public void TestStatisticsShownCorrectlyIfUpdateDeliveredBeforeLoad() + { + UserStatisticsWatcher userStatisticsWatcher = null!; + ScoreInfo score = null!; + + AddStep("create user statistics watcher", () => Add(userStatisticsWatcher = new UserStatisticsWatcher())); + AddStep("set user statistics update", () => + { + score = TestResources.CreateTestScoreInfo(); + score.OnlineID = 1234; + ((Bindable)userStatisticsWatcher.LatestUpdate).Value = new UserStatisticsUpdate(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, + }); + }); + AddStep("load user statistics panel", () => Child = new DependencyProvidingContainer + { + CachedDependencies = [(typeof(UserStatisticsWatcher), userStatisticsWatcher)], + RelativeSizeAxes = Axes.Both, + Child = new UserStatisticsPanel(score) + { + RelativeSizeAxes = Axes.Both, + State = { Value = Visibility.Visible }, + Score = { Value = score, } + } + }); + AddUntilStep("overall ranking present", () => this.ChildrenOfType().Any()); + AddUntilStep("loading spinner not visible", () => this.ChildrenOfType().All(l => l.State.Value == Visibility.Hidden)); + } + private void loadPanel(ScoreInfo score) => AddStep("load panel", () => { Child = new UserStatisticsPanel(score) From 20e7ade3b0a48f670da7f6f1d4ddedda1f2ddc44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 24 Sep 2024 17:52:19 +0200 Subject: [PATCH 2390/2493] Fix statistics update not being shown on results screen if it arrives too fast As reported in https://discord.com/channels/188630481301012481/1097318920991559880/1288160137286258799. --- osu.Game/Screens/Ranking/Statistics/UserStatisticsPanel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Ranking/Statistics/UserStatisticsPanel.cs b/osu.Game/Screens/Ranking/Statistics/UserStatisticsPanel.cs index fa3bb1a375..4e9c07ab7b 100644 --- a/osu.Game/Screens/Ranking/Statistics/UserStatisticsPanel.cs +++ b/osu.Game/Screens/Ranking/Statistics/UserStatisticsPanel.cs @@ -37,7 +37,7 @@ namespace osu.Game.Screens.Ranking.Statistics { if (update.NewValue?.Score.MatchesOnlineID(achievedScore) == true) DisplayedUserStatisticsUpdate.Value = update.NewValue; - }); + }, true); } } From 2d95c0b0bbaf6d97c940414f2e6afc61aa15e656 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Tue, 24 Sep 2024 18:45:52 +0200 Subject: [PATCH 2391/2493] remove tail recursion form welzl --- osu.Game/Utils/GeometryUtils.cs | 54 ++++++++++++++++++--------------- 1 file changed, 29 insertions(+), 25 deletions(-) diff --git a/osu.Game/Utils/GeometryUtils.cs b/osu.Game/Utils/GeometryUtils.cs index d4968749bf..877f58769b 100644 --- a/osu.Game/Utils/GeometryUtils.cs +++ b/osu.Game/Utils/GeometryUtils.cs @@ -305,33 +305,37 @@ namespace osu.Game.Utils // n represents the number of points in P that are not yet processed. private static (Vector2, float) welzlHelper(List points, ReadOnlySpan r, int n) { - // Base case when all points processed or |R| = 3 - if (n == 0 || r.Length == 3) - return minCircleTrivial(r); - - // Pick a random point randomly - int idx = RNG.Next(n); - Vector2 p = points[idx]; - - // Put the picked point at the end of P since it's more efficient than - // deleting from the middle of the list - (points[idx], points[n - 1]) = (points[n - 1], points[idx]); - - // Get the MEC circle d from the set of points P - {p} - var d = welzlHelper(points, r, n - 1); - - // If d contains p, return d - if (isInside(d, p)) - return d; - - // Otherwise, must be on the boundary of the MEC - // Stackalloc to avoid allocations. It's safe to assume that the length of r will be at most 3 - Span r2 = stackalloc Vector2[r.Length + 1]; + Span r2 = stackalloc Vector2[3]; + int rLength = r.Length; r.CopyTo(r2); - r2[r.Length] = p; - // Return the MEC for P - {p} and R U {p} - return welzlHelper(points, r2, n - 1); + while (true) + { + // Base case when all points processed or |R| = 3 + if (n == 0 || rLength == 3) return minCircleTrivial(r2[..rLength]); + + // Pick a random point randomly + int idx = RNG.Next(n); + Vector2 p = points[idx]; + + // Put the picked point at the end of P since it's more efficient than + // deleting from the middle of the list + (points[idx], points[n - 1]) = (points[n - 1], points[idx]); + + // Get the MEC circle d from the set of points P - {p} + var d = welzlHelper(points, r2[..rLength], n - 1); + + // If d contains p, return d + if (isInside(d, p)) return d; + + // Otherwise, must be on the boundary of the MEC + // Stackalloc to avoid allocations. It's safe to assume that the length of r will be at most 3 + r2[rLength] = p; + rLength++; + + // Return the MEC for P - {p} and R U {p} + n--; + } } #endregion From 796fc948e138f839b083826fb69e6130e303c0c2 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Tue, 24 Sep 2024 20:15:03 +0200 Subject: [PATCH 2392/2493] Rewrite Welzl's algorithm to use no recursion --- osu.Game/Utils/GeometryUtils.cs | 104 ++++++++++++++++++-------------- 1 file changed, 59 insertions(+), 45 deletions(-) diff --git a/osu.Game/Utils/GeometryUtils.cs b/osu.Game/Utils/GeometryUtils.cs index 877f58769b..e9e79deb49 100644 --- a/osu.Game/Utils/GeometryUtils.cs +++ b/osu.Game/Utils/GeometryUtils.cs @@ -255,7 +255,7 @@ namespace osu.Game.Utils } // Function to check whether a circle encloses the given points - private static bool isValidCircle((Vector2, float) c, ReadOnlySpan points) + private static bool isValidCircle((Vector2, float) c, List points) { // Iterating through all the points to check whether the points lie inside the circle or not foreach (Vector2 p in points) @@ -267,12 +267,12 @@ namespace osu.Game.Utils } // Function to return the minimum enclosing circle for N <= 3 - private static (Vector2, float) minCircleTrivial(ReadOnlySpan points) + private static (Vector2, float) minCircleTrivial(List points) { - if (points.Length > 3) + if (points.Count > 3) throw new ArgumentException("Number of points must be at most 3", nameof(points)); - switch (points.Length) + switch (points.Count) { case 0: return (new Vector2(0, 0), 0); @@ -299,45 +299,6 @@ namespace osu.Game.Utils return circleFrom(points[0], points[1], points[2]); } - // Returns the MEC using Welzl's algorithm - // Takes a set of input points P and a set R - // points on the circle boundary. - // n represents the number of points in P that are not yet processed. - private static (Vector2, float) welzlHelper(List points, ReadOnlySpan r, int n) - { - Span r2 = stackalloc Vector2[3]; - int rLength = r.Length; - r.CopyTo(r2); - - while (true) - { - // Base case when all points processed or |R| = 3 - if (n == 0 || rLength == 3) return minCircleTrivial(r2[..rLength]); - - // Pick a random point randomly - int idx = RNG.Next(n); - Vector2 p = points[idx]; - - // Put the picked point at the end of P since it's more efficient than - // deleting from the middle of the list - (points[idx], points[n - 1]) = (points[n - 1], points[idx]); - - // Get the MEC circle d from the set of points P - {p} - var d = welzlHelper(points, r2[..rLength], n - 1); - - // If d contains p, return d - if (isInside(d, p)) return d; - - // Otherwise, must be on the boundary of the MEC - // Stackalloc to avoid allocations. It's safe to assume that the length of r will be at most 3 - r2[rLength] = p; - rLength++; - - // Return the MEC for P - {p} and R U {p} - n--; - } - } - #endregion /// @@ -348,8 +309,61 @@ namespace osu.Game.Utils { // Using Welzl's algorithm to find the minimum enclosing circle // https://www.geeksforgeeks.org/minimum-enclosing-circle-using-welzls-algorithm/ - List pCopy = points.ToList(); - return welzlHelper(pCopy, Array.Empty(), pCopy.Count); + List P = points.ToList(); + + var stack = new Stack<(Vector2?, int)>(); + var r = new List(3); + (Vector2, float) d = (Vector2.Zero, 0); + + stack.Push((null, P.Count)); + + while (stack.Count > 0) + { + // n represents the number of points in P that are not yet processed. + // p represents the point that was randomly picked to process. + (Vector2? p, int n) = stack.Pop(); + + if (!p.HasValue) + { + // Base case when all points processed or |R| = 3 + if (n == 0 || r.Count == 3) + { + d = minCircleTrivial(r); + continue; + } + + // Pick a random point randomly + int idx = RNG.Next(n); + p = P[idx]; + + // Put the picked point at the end of P since it's more efficient than + // deleting from the middle of the list + (P[idx], P[n - 1]) = (P[n - 1], P[idx]); + + // Schedule processing of p after we get the MEC circle d from the set of points P - {p} + stack.Push((p, n)); + // Get the MEC circle d from the set of points P - {p} + stack.Push((null, n - 1)); + } + else + { + // If d contains p, return d + if (isInside(d, p.Value)) + continue; + + // Remove points from R that were added in a deeper recursion + // |R| = |P| - |stack| - n + int removeCount = r.Count - (P.Count - stack.Count - n); + r.RemoveRange(r.Count - removeCount, removeCount); + + // Otherwise, must be on the boundary of the MEC + r.Add(p.Value); + // Return the MEC for P - {p} and R U {p} + stack.Push((null, n - 1)); + } + } + + return d; } /// From e3b4483872ab71fcc1e700dd1195294f650e5c1b Mon Sep 17 00:00:00 2001 From: OliBomby Date: Mon, 23 Sep 2024 16:33:36 +0200 Subject: [PATCH 2393/2493] Refactor PlacementBlueprint to not be hitobject specific --- .../CatchPlacementBlueprintTestScene.cs | 2 +- ...TestSceneBananaShowerPlacementBlueprint.cs | 4 +- .../TestSceneFruitPlacementBlueprint.cs | 2 +- .../TestSceneJuiceStreamPlacementBlueprint.cs | 2 +- .../Edit/BananaShowerCompositionTool.cs | 4 +- .../Blueprints/CatchPlacementBlueprint.cs | 2 +- .../Edit/CatchHitObjectComposer.cs | 4 +- .../Edit/FruitCompositionTool.cs | 4 +- .../Edit/JuiceStreamCompositionTool.cs | 4 +- .../ManiaPlacementBlueprintTestScene.cs | 2 +- .../TestSceneHoldNotePlacementBlueprint.cs | 2 +- .../Editor/TestSceneNotePlacementBlueprint.cs | 2 +- .../Blueprints/ManiaPlacementBlueprint.cs | 2 +- .../Edit/HoldNoteCompositionTool.cs | 4 +- .../Edit/ManiaHitObjectComposer.cs | 2 +- .../Edit/NoteCompositionTool.cs | 4 +- .../TestSceneHitCirclePlacementBlueprint.cs | 2 +- .../TestSceneSliderPlacementBlueprint.cs | 2 +- .../TestSceneSpinnerPlacementBlueprint.cs | 2 +- .../HitCircles/HitCirclePlacementBlueprint.cs | 2 +- .../Sliders/SliderPlacementBlueprint.cs | 2 +- .../Spinners/SpinnerPlacementBlueprint.cs | 2 +- .../Edit/HitCircleCompositionTool.cs | 4 +- .../Edit/OsuHitObjectComposer.cs | 2 +- .../Edit/SliderCompositionTool.cs | 4 +- .../Edit/SpinnerCompositionTool.cs | 4 +- .../Edit/Blueprints/HitPlacementBlueprint.cs | 2 +- .../Blueprints/TaikoSpanPlacementBlueprint.cs | 2 +- .../Edit/DrumRollCompositionTool.cs | 4 +- .../Edit/HitCompositionTool.cs | 4 +- .../Edit/SwellCompositionTool.cs | 4 +- .../Edit/TaikoHitObjectComposer.cs | 2 +- .../Editing/TestScenePlacementBlueprint.cs | 2 +- osu.Game/Rulesets/Edit/HitObjectComposer.cs | 4 +- .../Edit/HitObjectCompositionToolButton.cs | 4 +- .../Edit/HitObjectPlacementBlueprint.cs | 126 ++++++++++++++++++ osu.Game/Rulesets/Edit/PlacementBlueprint.cs | 122 +++-------------- ...tCompositionTool.cs => CompositionTool.cs} | 4 +- osu.Game/Rulesets/Edit/Tools/SelectTool.cs | 4 +- .../Components/ComposeBlueprintContainer.cs | 22 +-- .../Visual/PlacementBlueprintTestScene.cs | 8 +- 41 files changed, 212 insertions(+), 174 deletions(-) create mode 100644 osu.Game/Rulesets/Edit/HitObjectPlacementBlueprint.cs rename osu.Game/Rulesets/Edit/Tools/{HitObjectCompositionTool.cs => CompositionTool.cs} (84%) diff --git a/osu.Game.Rulesets.Catch.Tests/Editor/CatchPlacementBlueprintTestScene.cs b/osu.Game.Rulesets.Catch.Tests/Editor/CatchPlacementBlueprintTestScene.cs index aae759d934..0578010c25 100644 --- a/osu.Game.Rulesets.Catch.Tests/Editor/CatchPlacementBlueprintTestScene.cs +++ b/osu.Game.Rulesets.Catch.Tests/Editor/CatchPlacementBlueprintTestScene.cs @@ -71,7 +71,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor contentContainer.Playfield.HitObjectContainer.Add(hitObject); } - protected override SnapResult SnapForBlueprint(PlacementBlueprint blueprint) + protected override SnapResult SnapForBlueprint(HitObjectPlacementBlueprint blueprint) { var result = base.SnapForBlueprint(blueprint); result.Time = Math.Round(HitObjectContainer.TimeAtScreenSpacePosition(result.ScreenSpacePosition) / TIME_SNAP) * TIME_SNAP; diff --git a/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneBananaShowerPlacementBlueprint.cs b/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneBananaShowerPlacementBlueprint.cs index ed37ff4ef3..badd8e967d 100644 --- a/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneBananaShowerPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneBananaShowerPlacementBlueprint.cs @@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor { protected override DrawableHitObject CreateHitObject(HitObject hitObject) => new DrawableBananaShower((BananaShower)hitObject); - protected override PlacementBlueprint CreateBlueprint() => new BananaShowerPlacementBlueprint(); + protected override HitObjectPlacementBlueprint CreateBlueprint() => new BananaShowerPlacementBlueprint(); protected override void AddHitObject(DrawableHitObject hitObject) { @@ -71,7 +71,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor AddClickStep(MouseButton.Left); AddClickStep(MouseButton.Right); AddAssert("banana shower is not placed", () => LastObject == null); - AddAssert("state is waiting", () => CurrentBlueprint?.PlacementActive == PlacementBlueprint.PlacementState.Waiting); + AddAssert("state is waiting", () => CurrentBlueprint?.PlacementActive == HitObjectPlacementBlueprint.PlacementState.Waiting); } [Test] diff --git a/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneFruitPlacementBlueprint.cs b/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneFruitPlacementBlueprint.cs index 75d3c3753a..80cd948e26 100644 --- a/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneFruitPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneFruitPlacementBlueprint.cs @@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor { protected override DrawableHitObject CreateHitObject(HitObject hitObject) => new DrawableFruit((Fruit)hitObject); - protected override PlacementBlueprint CreateBlueprint() => new FruitPlacementBlueprint(); + protected override HitObjectPlacementBlueprint CreateBlueprint() => new FruitPlacementBlueprint(); [Test] public void TestFruitPlacementPosition() diff --git a/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneJuiceStreamPlacementBlueprint.cs b/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneJuiceStreamPlacementBlueprint.cs index d010bb02ad..8bd60078c6 100644 --- a/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneJuiceStreamPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneJuiceStreamPlacementBlueprint.cs @@ -114,7 +114,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor protected override DrawableHitObject CreateHitObject(HitObject hitObject) => new DrawableJuiceStream((JuiceStream)hitObject); - protected override PlacementBlueprint CreateBlueprint() => new JuiceStreamPlacementBlueprint(); + protected override HitObjectPlacementBlueprint CreateBlueprint() => new JuiceStreamPlacementBlueprint(); private void addMoveAndClickSteps(double time, float position, bool end = false) { diff --git a/osu.Game.Rulesets.Catch/Edit/BananaShowerCompositionTool.cs b/osu.Game.Rulesets.Catch/Edit/BananaShowerCompositionTool.cs index 31075db7d1..be93ba0242 100644 --- a/osu.Game.Rulesets.Catch/Edit/BananaShowerCompositionTool.cs +++ b/osu.Game.Rulesets.Catch/Edit/BananaShowerCompositionTool.cs @@ -10,7 +10,7 @@ using osu.Game.Rulesets.Edit.Tools; namespace osu.Game.Rulesets.Catch.Edit { - public class BananaShowerCompositionTool : HitObjectCompositionTool + public class BananaShowerCompositionTool : CompositionTool { public BananaShowerCompositionTool() : base(nameof(BananaShower)) @@ -19,6 +19,6 @@ namespace osu.Game.Rulesets.Catch.Edit public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Spinners); - public override PlacementBlueprint CreatePlacementBlueprint() => new BananaShowerPlacementBlueprint(); + public override HitObjectPlacementBlueprint CreatePlacementBlueprint() => new BananaShowerPlacementBlueprint(); } } diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/CatchPlacementBlueprint.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/CatchPlacementBlueprint.cs index 1a2990e4ac..aa862375c5 100644 --- a/osu.Game.Rulesets.Catch/Edit/Blueprints/CatchPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/CatchPlacementBlueprint.cs @@ -9,7 +9,7 @@ using osu.Game.Rulesets.UI.Scrolling; namespace osu.Game.Rulesets.Catch.Edit.Blueprints { - public partial class CatchPlacementBlueprint : PlacementBlueprint + public partial class CatchPlacementBlueprint : HitObjectPlacementBlueprint where THitObject : CatchHitObject, new() { protected new THitObject HitObject => (THitObject)base.HitObject; diff --git a/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs b/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs index 83f48816f9..8460e238f6 100644 --- a/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs +++ b/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs @@ -84,7 +84,7 @@ namespace osu.Game.Rulesets.Catch.Edit protected override BeatSnapGrid CreateBeatSnapGrid() => new CatchBeatSnapGrid(); - protected override IReadOnlyList CompositionTools => new HitObjectCompositionTool[] + protected override IReadOnlyList CompositionTools => new CompositionTool[] { new FruitCompositionTool(), new JuiceStreamCompositionTool(), @@ -168,7 +168,7 @@ namespace osu.Game.Rulesets.Catch.Edit if (EditorBeatmap.PlacementObject.Value is JuiceStream) { // Juice stream path is not subject to snapping. - if (BlueprintContainer.CurrentPlacement.PlacementActive is PlacementBlueprint.PlacementState.Active) + if (BlueprintContainer.CurrentPlacement.PlacementActive is HitObjectPlacementBlueprint.PlacementState.Active) return null; } diff --git a/osu.Game.Rulesets.Catch/Edit/FruitCompositionTool.cs b/osu.Game.Rulesets.Catch/Edit/FruitCompositionTool.cs index f776fe39c1..71c1e66903 100644 --- a/osu.Game.Rulesets.Catch/Edit/FruitCompositionTool.cs +++ b/osu.Game.Rulesets.Catch/Edit/FruitCompositionTool.cs @@ -10,7 +10,7 @@ using osu.Game.Rulesets.Edit.Tools; namespace osu.Game.Rulesets.Catch.Edit { - public class FruitCompositionTool : HitObjectCompositionTool + public class FruitCompositionTool : CompositionTool { public FruitCompositionTool() : base(nameof(Fruit)) @@ -19,6 +19,6 @@ namespace osu.Game.Rulesets.Catch.Edit public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles); - public override PlacementBlueprint CreatePlacementBlueprint() => new FruitPlacementBlueprint(); + public override HitObjectPlacementBlueprint CreatePlacementBlueprint() => new FruitPlacementBlueprint(); } } diff --git a/osu.Game.Rulesets.Catch/Edit/JuiceStreamCompositionTool.cs b/osu.Game.Rulesets.Catch/Edit/JuiceStreamCompositionTool.cs index cb66e2952e..7a567820f3 100644 --- a/osu.Game.Rulesets.Catch/Edit/JuiceStreamCompositionTool.cs +++ b/osu.Game.Rulesets.Catch/Edit/JuiceStreamCompositionTool.cs @@ -10,7 +10,7 @@ using osu.Game.Rulesets.Edit.Tools; namespace osu.Game.Rulesets.Catch.Edit { - public class JuiceStreamCompositionTool : HitObjectCompositionTool + public class JuiceStreamCompositionTool : CompositionTool { public JuiceStreamCompositionTool() : base(nameof(JuiceStream)) @@ -19,6 +19,6 @@ namespace osu.Game.Rulesets.Catch.Edit public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders); - public override PlacementBlueprint CreatePlacementBlueprint() => new JuiceStreamPlacementBlueprint(); + public override HitObjectPlacementBlueprint CreatePlacementBlueprint() => new JuiceStreamPlacementBlueprint(); } } diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/ManiaPlacementBlueprintTestScene.cs b/osu.Game.Rulesets.Mania.Tests/Editor/ManiaPlacementBlueprintTestScene.cs index d2a44122aa..5e633c3161 100644 --- a/osu.Game.Rulesets.Mania.Tests/Editor/ManiaPlacementBlueprintTestScene.cs +++ b/osu.Game.Rulesets.Mania.Tests/Editor/ManiaPlacementBlueprintTestScene.cs @@ -47,7 +47,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor }); } - protected override SnapResult SnapForBlueprint(PlacementBlueprint blueprint) + protected override SnapResult SnapForBlueprint(HitObjectPlacementBlueprint blueprint) { double time = column.TimeAtScreenSpacePosition(InputManager.CurrentState.Mouse.Position); var pos = column.ScreenSpacePositionAtTime(time); diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneHoldNotePlacementBlueprint.cs b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneHoldNotePlacementBlueprint.cs index b79bcb7682..2006879ab3 100644 --- a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneHoldNotePlacementBlueprint.cs +++ b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneHoldNotePlacementBlueprint.cs @@ -13,6 +13,6 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor public partial class TestSceneHoldNotePlacementBlueprint : ManiaPlacementBlueprintTestScene { protected override DrawableHitObject CreateHitObject(HitObject hitObject) => new DrawableHoldNote((HoldNote)hitObject); - protected override PlacementBlueprint CreateBlueprint() => new HoldNotePlacementBlueprint(); + protected override HitObjectPlacementBlueprint CreateBlueprint() => new HoldNotePlacementBlueprint(); } } diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneNotePlacementBlueprint.cs b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneNotePlacementBlueprint.cs index a446f13cbf..0cb9639cd1 100644 --- a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneNotePlacementBlueprint.cs +++ b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneNotePlacementBlueprint.cs @@ -64,6 +64,6 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor private Note getNote() => this.ChildrenOfType().FirstOrDefault()?.HitObject; protected override DrawableHitObject CreateHitObject(HitObject hitObject) => new DrawableNote((Note)hitObject); - protected override PlacementBlueprint CreateBlueprint() => new NotePlacementBlueprint(); + protected override HitObjectPlacementBlueprint CreateBlueprint() => new NotePlacementBlueprint(); } } diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs index 5e0512b5dc..a68bd5d6d6 100644 --- a/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs @@ -15,7 +15,7 @@ using osuTK.Input; namespace osu.Game.Rulesets.Mania.Edit.Blueprints { - public abstract partial class ManiaPlacementBlueprint : PlacementBlueprint + public abstract partial class ManiaPlacementBlueprint : HitObjectPlacementBlueprint where T : ManiaHitObject { protected new T HitObject => (T)base.HitObject; diff --git a/osu.Game.Rulesets.Mania/Edit/HoldNoteCompositionTool.cs b/osu.Game.Rulesets.Mania/Edit/HoldNoteCompositionTool.cs index 99e1ce04b1..592f8d9af7 100644 --- a/osu.Game.Rulesets.Mania/Edit/HoldNoteCompositionTool.cs +++ b/osu.Game.Rulesets.Mania/Edit/HoldNoteCompositionTool.cs @@ -9,7 +9,7 @@ using osu.Game.Rulesets.Mania.Edit.Blueprints; namespace osu.Game.Rulesets.Mania.Edit { - public class HoldNoteCompositionTool : HitObjectCompositionTool + public class HoldNoteCompositionTool : CompositionTool { public HoldNoteCompositionTool() : base("Hold") @@ -18,6 +18,6 @@ namespace osu.Game.Rulesets.Mania.Edit public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders); - public override PlacementBlueprint CreatePlacementBlueprint() => new HoldNotePlacementBlueprint(); + public override HitObjectPlacementBlueprint CreatePlacementBlueprint() => new HoldNotePlacementBlueprint(); } } diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs index 02a4f3a022..e3b4fa2fb7 100644 --- a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs +++ b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs @@ -46,7 +46,7 @@ namespace osu.Game.Rulesets.Mania.Edit protected override BeatSnapGrid CreateBeatSnapGrid() => new ManiaBeatSnapGrid(); - protected override IReadOnlyList CompositionTools => new HitObjectCompositionTool[] + protected override IReadOnlyList CompositionTools => new CompositionTool[] { new NoteCompositionTool(), new HoldNoteCompositionTool() diff --git a/osu.Game.Rulesets.Mania/Edit/NoteCompositionTool.cs b/osu.Game.Rulesets.Mania/Edit/NoteCompositionTool.cs index 08ee05ad3f..2e54d63525 100644 --- a/osu.Game.Rulesets.Mania/Edit/NoteCompositionTool.cs +++ b/osu.Game.Rulesets.Mania/Edit/NoteCompositionTool.cs @@ -10,7 +10,7 @@ using osu.Game.Rulesets.Mania.Objects; namespace osu.Game.Rulesets.Mania.Edit { - public class NoteCompositionTool : HitObjectCompositionTool + public class NoteCompositionTool : CompositionTool { public NoteCompositionTool() : base(nameof(Note)) @@ -19,6 +19,6 @@ namespace osu.Game.Rulesets.Mania.Edit public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles); - public override PlacementBlueprint CreatePlacementBlueprint() => new NotePlacementBlueprint(); + public override HitObjectPlacementBlueprint CreatePlacementBlueprint() => new NotePlacementBlueprint(); } } diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneHitCirclePlacementBlueprint.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneHitCirclePlacementBlueprint.cs index a49afd82f3..a105d860bf 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneHitCirclePlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneHitCirclePlacementBlueprint.cs @@ -14,6 +14,6 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor public partial class TestSceneHitCirclePlacementBlueprint : PlacementBlueprintTestScene { protected override DrawableHitObject CreateHitObject(HitObject hitObject) => new DrawableHitCircle((HitCircle)hitObject); - protected override PlacementBlueprint CreateBlueprint() => new HitCirclePlacementBlueprint(); + protected override HitObjectPlacementBlueprint CreateBlueprint() => new HitCirclePlacementBlueprint(); } } diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs index aa6a6f08d8..019565ae29 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs @@ -514,6 +514,6 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor private Slider? getSlider() => HitObjectContainer.Count > 0 ? ((DrawableSlider)HitObjectContainer[0]).HitObject : null; protected override DrawableHitObject CreateHitObject(HitObject hitObject) => new DrawableSlider((Slider)hitObject); - protected override PlacementBlueprint CreateBlueprint() => new SliderPlacementBlueprint(); + protected override HitObjectPlacementBlueprint CreateBlueprint() => new SliderPlacementBlueprint(); } } diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSpinnerPlacementBlueprint.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSpinnerPlacementBlueprint.cs index 0e8673319e..d7b5cc73be 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSpinnerPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSpinnerPlacementBlueprint.cs @@ -15,6 +15,6 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor { protected override DrawableHitObject CreateHitObject(HitObject hitObject) => new DrawableSpinner((Spinner)hitObject); - protected override PlacementBlueprint CreateBlueprint() => new SpinnerPlacementBlueprint(); + protected override HitObjectPlacementBlueprint CreateBlueprint() => new SpinnerPlacementBlueprint(); } } diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs index 20ad99baa2..78a0e36dc2 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs @@ -9,7 +9,7 @@ using osuTK.Input; namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles { - public partial class HitCirclePlacementBlueprint : PlacementBlueprint + public partial class HitCirclePlacementBlueprint : HitObjectPlacementBlueprint { public new HitCircle HitObject => (HitCircle)base.HitObject; diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs index 42945295b8..6ffe27dc13 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs @@ -21,7 +21,7 @@ using osuTK.Input; namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders { - public partial class SliderPlacementBlueprint : PlacementBlueprint + public partial class SliderPlacementBlueprint : HitObjectPlacementBlueprint { public new Slider HitObject => (Slider)base.HitObject; diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Spinners/SpinnerPlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Spinners/SpinnerPlacementBlueprint.cs index f59be0e0e9..17d2dcd75c 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Spinners/SpinnerPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Spinners/SpinnerPlacementBlueprint.cs @@ -13,7 +13,7 @@ using osuTK.Input; namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Spinners { - public partial class SpinnerPlacementBlueprint : PlacementBlueprint + public partial class SpinnerPlacementBlueprint : HitObjectPlacementBlueprint { public new Spinner HitObject => (Spinner)base.HitObject; diff --git a/osu.Game.Rulesets.Osu/Edit/HitCircleCompositionTool.cs b/osu.Game.Rulesets.Osu/Edit/HitCircleCompositionTool.cs index c41ae10b2e..d3116ede30 100644 --- a/osu.Game.Rulesets.Osu/Edit/HitCircleCompositionTool.cs +++ b/osu.Game.Rulesets.Osu/Edit/HitCircleCompositionTool.cs @@ -10,7 +10,7 @@ using osu.Game.Rulesets.Osu.Objects; namespace osu.Game.Rulesets.Osu.Edit { - public class HitCircleCompositionTool : HitObjectCompositionTool + public class HitCircleCompositionTool : CompositionTool { public HitCircleCompositionTool() : base(nameof(HitCircle)) @@ -19,6 +19,6 @@ namespace osu.Game.Rulesets.Osu.Edit public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles); - public override PlacementBlueprint CreatePlacementBlueprint() => new HitCirclePlacementBlueprint(); + public override HitObjectPlacementBlueprint CreatePlacementBlueprint() => new HitCirclePlacementBlueprint(); } } diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs index 8fc2a9b7d3..4368a338b2 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs @@ -41,7 +41,7 @@ namespace osu.Game.Rulesets.Osu.Edit protected override DrawableRuleset CreateDrawableRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList mods) => new DrawableOsuEditorRuleset(ruleset, beatmap, mods); - protected override IReadOnlyList CompositionTools => new HitObjectCompositionTool[] + protected override IReadOnlyList CompositionTools => new CompositionTool[] { new HitCircleCompositionTool(), new SliderCompositionTool(), diff --git a/osu.Game.Rulesets.Osu/Edit/SliderCompositionTool.cs b/osu.Game.Rulesets.Osu/Edit/SliderCompositionTool.cs index 617cc1c19b..d697a2ebe6 100644 --- a/osu.Game.Rulesets.Osu/Edit/SliderCompositionTool.cs +++ b/osu.Game.Rulesets.Osu/Edit/SliderCompositionTool.cs @@ -10,7 +10,7 @@ using osu.Game.Rulesets.Osu.Objects; namespace osu.Game.Rulesets.Osu.Edit { - public class SliderCompositionTool : HitObjectCompositionTool + public class SliderCompositionTool : CompositionTool { public SliderCompositionTool() : base(nameof(Slider)) @@ -26,6 +26,6 @@ namespace osu.Game.Rulesets.Osu.Edit public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders); - public override PlacementBlueprint CreatePlacementBlueprint() => new SliderPlacementBlueprint(); + public override HitObjectPlacementBlueprint CreatePlacementBlueprint() => new SliderPlacementBlueprint(); } } diff --git a/osu.Game.Rulesets.Osu/Edit/SpinnerCompositionTool.cs b/osu.Game.Rulesets.Osu/Edit/SpinnerCompositionTool.cs index c8160617c9..de1506e4a9 100644 --- a/osu.Game.Rulesets.Osu/Edit/SpinnerCompositionTool.cs +++ b/osu.Game.Rulesets.Osu/Edit/SpinnerCompositionTool.cs @@ -10,7 +10,7 @@ using osu.Game.Rulesets.Osu.Objects; namespace osu.Game.Rulesets.Osu.Edit { - public class SpinnerCompositionTool : HitObjectCompositionTool + public class SpinnerCompositionTool : CompositionTool { public SpinnerCompositionTool() : base(nameof(Spinner)) @@ -19,6 +19,6 @@ namespace osu.Game.Rulesets.Osu.Edit public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Spinners); - public override PlacementBlueprint CreatePlacementBlueprint() => new SpinnerPlacementBlueprint(); + public override HitObjectPlacementBlueprint CreatePlacementBlueprint() => new SpinnerPlacementBlueprint(); } } diff --git a/osu.Game.Rulesets.Taiko/Edit/Blueprints/HitPlacementBlueprint.cs b/osu.Game.Rulesets.Taiko/Edit/Blueprints/HitPlacementBlueprint.cs index 329fff5b42..7f45123bd6 100644 --- a/osu.Game.Rulesets.Taiko/Edit/Blueprints/HitPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Taiko/Edit/Blueprints/HitPlacementBlueprint.cs @@ -10,7 +10,7 @@ using osuTK.Input; namespace osu.Game.Rulesets.Taiko.Edit.Blueprints { - public partial class HitPlacementBlueprint : PlacementBlueprint + public partial class HitPlacementBlueprint : HitObjectPlacementBlueprint { private readonly HitPiece piece; diff --git a/osu.Game.Rulesets.Taiko/Edit/Blueprints/TaikoSpanPlacementBlueprint.cs b/osu.Game.Rulesets.Taiko/Edit/Blueprints/TaikoSpanPlacementBlueprint.cs index cd52398086..de3a4d96eb 100644 --- a/osu.Game.Rulesets.Taiko/Edit/Blueprints/TaikoSpanPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Taiko/Edit/Blueprints/TaikoSpanPlacementBlueprint.cs @@ -17,7 +17,7 @@ using osuTK.Input; namespace osu.Game.Rulesets.Taiko.Edit.Blueprints { - public partial class TaikoSpanPlacementBlueprint : PlacementBlueprint + public partial class TaikoSpanPlacementBlueprint : HitObjectPlacementBlueprint { private readonly HitPiece headPiece; private readonly HitPiece tailPiece; diff --git a/osu.Game.Rulesets.Taiko/Edit/DrumRollCompositionTool.cs b/osu.Game.Rulesets.Taiko/Edit/DrumRollCompositionTool.cs index f332441875..ba0fda6771 100644 --- a/osu.Game.Rulesets.Taiko/Edit/DrumRollCompositionTool.cs +++ b/osu.Game.Rulesets.Taiko/Edit/DrumRollCompositionTool.cs @@ -10,7 +10,7 @@ using osu.Game.Rulesets.Taiko.Objects; namespace osu.Game.Rulesets.Taiko.Edit { - public class DrumRollCompositionTool : HitObjectCompositionTool + public class DrumRollCompositionTool : CompositionTool { public DrumRollCompositionTool() : base(nameof(DrumRoll)) @@ -19,6 +19,6 @@ namespace osu.Game.Rulesets.Taiko.Edit public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders); - public override PlacementBlueprint CreatePlacementBlueprint() => new DrumRollPlacementBlueprint(); + public override HitObjectPlacementBlueprint CreatePlacementBlueprint() => new DrumRollPlacementBlueprint(); } } diff --git a/osu.Game.Rulesets.Taiko/Edit/HitCompositionTool.cs b/osu.Game.Rulesets.Taiko/Edit/HitCompositionTool.cs index fa50841893..f58defba83 100644 --- a/osu.Game.Rulesets.Taiko/Edit/HitCompositionTool.cs +++ b/osu.Game.Rulesets.Taiko/Edit/HitCompositionTool.cs @@ -10,7 +10,7 @@ using osu.Game.Rulesets.Taiko.Objects; namespace osu.Game.Rulesets.Taiko.Edit { - public class HitCompositionTool : HitObjectCompositionTool + public class HitCompositionTool : CompositionTool { public HitCompositionTool() : base(nameof(Hit)) @@ -19,6 +19,6 @@ namespace osu.Game.Rulesets.Taiko.Edit public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles); - public override PlacementBlueprint CreatePlacementBlueprint() => new HitPlacementBlueprint(); + public override HitObjectPlacementBlueprint CreatePlacementBlueprint() => new HitPlacementBlueprint(); } } diff --git a/osu.Game.Rulesets.Taiko/Edit/SwellCompositionTool.cs b/osu.Game.Rulesets.Taiko/Edit/SwellCompositionTool.cs index 4d4ee8effe..4ec623e29e 100644 --- a/osu.Game.Rulesets.Taiko/Edit/SwellCompositionTool.cs +++ b/osu.Game.Rulesets.Taiko/Edit/SwellCompositionTool.cs @@ -10,7 +10,7 @@ using osu.Game.Rulesets.Taiko.Objects; namespace osu.Game.Rulesets.Taiko.Edit { - public class SwellCompositionTool : HitObjectCompositionTool + public class SwellCompositionTool : CompositionTool { public SwellCompositionTool() : base(nameof(Swell)) @@ -19,6 +19,6 @@ namespace osu.Game.Rulesets.Taiko.Edit public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Spinners); - public override PlacementBlueprint CreatePlacementBlueprint() => new SwellPlacementBlueprint(); + public override HitObjectPlacementBlueprint CreatePlacementBlueprint() => new SwellPlacementBlueprint(); } } diff --git a/osu.Game.Rulesets.Taiko/Edit/TaikoHitObjectComposer.cs b/osu.Game.Rulesets.Taiko/Edit/TaikoHitObjectComposer.cs index 6020f6e04c..d97a854ff7 100644 --- a/osu.Game.Rulesets.Taiko/Edit/TaikoHitObjectComposer.cs +++ b/osu.Game.Rulesets.Taiko/Edit/TaikoHitObjectComposer.cs @@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Taiko.Edit { } - protected override IReadOnlyList CompositionTools => new HitObjectCompositionTool[] + protected override IReadOnlyList CompositionTools => new CompositionTool[] { new HitCompositionTool(), new DrumRollCompositionTool(), diff --git a/osu.Game.Tests/Visual/Editing/TestScenePlacementBlueprint.cs b/osu.Game.Tests/Visual/Editing/TestScenePlacementBlueprint.cs index fe74e1b346..6f3342f8ce 100644 --- a/osu.Game.Tests/Visual/Editing/TestScenePlacementBlueprint.cs +++ b/osu.Game.Tests/Visual/Editing/TestScenePlacementBlueprint.cs @@ -122,7 +122,7 @@ namespace osu.Game.Tests.Visual.Editing AddAssert("editor is still current", () => Editor.IsCurrentScreen()); AddAssert("slider not placed", () => EditorBeatmap.HitObjects.Count, () => Is.EqualTo(0)); AddAssert("no active placement", () => this.ChildrenOfType().Single().CurrentPlacement.PlacementActive, - () => Is.EqualTo(PlacementBlueprint.PlacementState.Waiting)); + () => Is.EqualTo(HitObjectPlacementBlueprint.PlacementState.Waiting)); } [Test] diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs index c2a7bec9f9..00de46b726 100644 --- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs @@ -323,7 +323,7 @@ namespace osu.Game.Rulesets.Edit /// /// A "select" tool is automatically added as the first tool. /// - protected abstract IReadOnlyList CompositionTools { get; } + protected abstract IReadOnlyList CompositionTools { get; } /// /// A collection of states which will be displayed to the user in the toolbox. @@ -466,7 +466,7 @@ namespace osu.Game.Rulesets.Edit private void setSelectTool() => toolboxCollection.Items.First().Select(); - private void toolSelected(HitObjectCompositionTool tool) + private void toolSelected(CompositionTool tool) { BlueprintContainer.CurrentTool = tool; diff --git a/osu.Game/Rulesets/Edit/HitObjectCompositionToolButton.cs b/osu.Game/Rulesets/Edit/HitObjectCompositionToolButton.cs index ba566ff5c0..641d60dbd3 100644 --- a/osu.Game/Rulesets/Edit/HitObjectCompositionToolButton.cs +++ b/osu.Game/Rulesets/Edit/HitObjectCompositionToolButton.cs @@ -9,9 +9,9 @@ namespace osu.Game.Rulesets.Edit { public class HitObjectCompositionToolButton : RadioButton { - public HitObjectCompositionTool Tool { get; } + public CompositionTool Tool { get; } - public HitObjectCompositionToolButton(HitObjectCompositionTool tool, Action? action) + public HitObjectCompositionToolButton(CompositionTool tool, Action? action) : base(tool.Name, action, tool.CreateIcon) { Tool = tool; diff --git a/osu.Game/Rulesets/Edit/HitObjectPlacementBlueprint.cs b/osu.Game/Rulesets/Edit/HitObjectPlacementBlueprint.cs new file mode 100644 index 0000000000..74025b4260 --- /dev/null +++ b/osu.Game/Rulesets/Edit/HitObjectPlacementBlueprint.cs @@ -0,0 +1,126 @@ +// 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 System.Threading; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Game.Audio; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; +using osu.Game.Screens.Edit; +using osu.Game.Screens.Edit.Compose; + +namespace osu.Game.Rulesets.Edit +{ + /// + /// A blueprint which governs the creation of a new to actualisation. + /// + public abstract partial class HitObjectPlacementBlueprint : PlacementBlueprint + { + /// + /// Whether the sample bank should be taken from the previous hit object. + /// + public bool AutomaticBankAssignment { get; set; } + + /// + /// The that is being placed. + /// + public readonly HitObject HitObject; + + [Resolved] + protected EditorClock EditorClock { get; private set; } = null!; + + [Resolved] + private EditorBeatmap beatmap { get; set; } = null!; + + private Bindable startTimeBindable = null!; + + private HitObject? getPreviousHitObject() => beatmap.HitObjects.TakeWhile(h => h.StartTime <= startTimeBindable.Value).LastOrDefault(); + + [Resolved] + private IPlacementHandler placementHandler { get; set; } = null!; + + protected HitObjectPlacementBlueprint(HitObject hitObject) + { + HitObject = hitObject; + + // adding the default hit sample should be the case regardless of the ruleset. + HitObject.Samples.Add(new HitSampleInfo(HitSampleInfo.HIT_NORMAL)); + } + + [BackgroundDependencyLoader] + private void load() + { + startTimeBindable = HitObject.StartTimeBindable.GetBoundCopy(); + startTimeBindable.BindValueChanged(_ => ApplyDefaultsToHitObject(), true); + } + + protected override void BeginPlacement(bool commitStart = false) + { + base.BeginPlacement(commitStart); + + placementHandler.BeginPlacement(HitObject); + } + + public override void EndPlacement(bool commit) + { + base.EndPlacement(commit); + + placementHandler.EndPlacement(HitObject, IsValidForPlacement && commit); + } + + /// + /// Updates the time and position of this based on the provided snap information. + /// + /// The snap result information. + public override void UpdateTimeAndPosition(SnapResult result) + { + if (PlacementActive == PlacementState.Waiting) + { + HitObject.StartTime = result.Time ?? EditorClock.CurrentTime; + + if (HitObject is IHasComboInformation comboInformation) + comboInformation.UpdateComboInformation(getPreviousHitObject() as IHasComboInformation); + } + + var lastHitObject = getPreviousHitObject(); + + if (AutomaticBankAssignment) + { + // Create samples based on the sample settings of the previous hit object + if (lastHitObject != null) + { + for (int i = 0; i < HitObject.Samples.Count; i++) + HitObject.Samples[i] = lastHitObject.CreateHitSampleInfo(HitObject.Samples[i].Name); + } + } + else + { + var lastHitNormal = lastHitObject?.Samples?.FirstOrDefault(o => o.Name == HitSampleInfo.HIT_NORMAL); + + if (lastHitNormal != null) + { + // Only inherit the volume from the previous hit object + for (int i = 0; i < HitObject.Samples.Count; i++) + HitObject.Samples[i] = HitObject.Samples[i].With(newVolume: lastHitNormal.Volume); + } + } + + if (HitObject is IHasRepeats hasRepeats) + { + // Make sure all the node samples are identical to the hit object's samples + for (int i = 0; i < hasRepeats.NodeSamples.Count; i++) + hasRepeats.NodeSamples[i] = HitObject.Samples.Select(o => o.With()).ToList(); + } + } + + /// + /// Invokes , + /// refreshing and parameters for the . + /// + protected void ApplyDefaultsToHitObject() => HitObject.ApplyDefaults(beatmap.ControlPointInfo, beatmap.Difficulty); + } +} diff --git a/osu.Game/Rulesets/Edit/PlacementBlueprint.cs b/osu.Game/Rulesets/Edit/PlacementBlueprint.cs index a50a7f4169..d2a54e8e03 100644 --- a/osu.Game/Rulesets/Edit/PlacementBlueprint.cs +++ b/osu.Game/Rulesets/Edit/PlacementBlueprint.cs @@ -1,29 +1,19 @@ // 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 System.Threading; -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.Game.Audio; -using osu.Game.Beatmaps; -using osu.Game.Beatmaps.ControlPoints; using osu.Game.Input.Bindings; using osu.Game.Rulesets.Objects; -using osu.Game.Rulesets.Objects.Types; -using osu.Game.Screens.Edit; -using osu.Game.Screens.Edit.Compose; using osuTK; using osuTK.Input; namespace osu.Game.Rulesets.Edit { /// - /// A blueprint which governs the creation of a new to actualisation. + /// A blueprint which governs the placement of something. /// public abstract partial class PlacementBlueprint : CompositeDrawable, IKeyBindingHandler { @@ -32,29 +22,6 @@ namespace osu.Game.Rulesets.Edit /// public PlacementState PlacementActive { get; private set; } - /// - /// Whether the sample bank should be taken from the previous hit object. - /// - public bool AutomaticBankAssignment { get; set; } - - /// - /// The that is being placed. - /// - public readonly HitObject HitObject; - - [Resolved] - protected EditorClock EditorClock { get; private set; } = null!; - - [Resolved] - private EditorBeatmap beatmap { get; set; } = null!; - - private Bindable startTimeBindable = null!; - - private HitObject? getPreviousHitObject() => beatmap.HitObjects.TakeWhile(h => h.StartTime <= startTimeBindable.Value).LastOrDefault(); - - [Resolved] - private IPlacementHandler placementHandler { get; set; } = null!; - /// /// Whether this blueprint is currently in a state that can be committed. /// @@ -64,13 +31,8 @@ namespace osu.Game.Rulesets.Edit /// protected virtual bool IsValidForPlacement => true; - protected PlacementBlueprint(HitObject hitObject) + protected PlacementBlueprint() { - HitObject = hitObject; - - // adding the default hit sample should be the case regardless of the ruleset. - HitObject.Samples.Add(new HitSampleInfo(HitSampleInfo.HIT_NORMAL)); - RelativeSizeAxes = Axes.Both; // This is required to allow the blueprint's position to be updated via OnMouseMove/Handle @@ -78,30 +40,22 @@ namespace osu.Game.Rulesets.Edit AlwaysPresent = true; } - [BackgroundDependencyLoader] - private void load() - { - startTimeBindable = HitObject.StartTimeBindable.GetBoundCopy(); - startTimeBindable.BindValueChanged(_ => ApplyDefaultsToHitObject(), true); - } - /// - /// Signals that the placement of has started. + /// Signals that the placement has started. /// - /// Whether this call is committing a value for HitObject.StartTime and continuing with further adjustments. - protected void BeginPlacement(bool commitStart = false) + /// Whether this call is committing a value and continuing with further adjustments. + protected virtual void BeginPlacement(bool commitStart = false) { - placementHandler.BeginPlacement(HitObject); if (commitStart) PlacementActive = PlacementState.Active; } /// /// Signals that the placement of has finished. - /// This will destroy this , and add the HitObject.StartTime to the . + /// This will destroy this , and commit the changes. /// - /// Whether the object should be committed. Note that a commit may fail if is false. - public void EndPlacement(bool commit) + /// Whether the changes should be committed. Note that a commit may fail if is false. + public virtual void EndPlacement(bool commit) { switch (PlacementActive) { @@ -114,10 +68,17 @@ namespace osu.Game.Rulesets.Edit break; } - placementHandler.EndPlacement(HitObject, IsValidForPlacement && commit); PlacementActive = PlacementState.Finished; } + /// + /// Updates the time and position of this based on the provided snap information. + /// + /// The snap result information. + public virtual void UpdateTimeAndPosition(SnapResult result) + { + } + public bool OnPressed(KeyBindingPressEvent e) { if (PlacementActive == PlacementState.Waiting) @@ -138,57 +99,6 @@ namespace osu.Game.Rulesets.Edit { } - /// - /// Updates the time and position of this based on the provided snap information. - /// - /// The snap result information. - public virtual void UpdateTimeAndPosition(SnapResult result) - { - if (PlacementActive == PlacementState.Waiting) - { - HitObject.StartTime = result.Time ?? EditorClock.CurrentTime; - - if (HitObject is IHasComboInformation comboInformation) - comboInformation.UpdateComboInformation(getPreviousHitObject() as IHasComboInformation); - } - - var lastHitObject = getPreviousHitObject(); - - if (AutomaticBankAssignment) - { - // Create samples based on the sample settings of the previous hit object - if (lastHitObject != null) - { - for (int i = 0; i < HitObject.Samples.Count; i++) - HitObject.Samples[i] = lastHitObject.CreateHitSampleInfo(HitObject.Samples[i].Name); - } - } - else - { - var lastHitNormal = lastHitObject?.Samples?.FirstOrDefault(o => o.Name == HitSampleInfo.HIT_NORMAL); - - if (lastHitNormal != null) - { - // Only inherit the volume from the previous hit object - for (int i = 0; i < HitObject.Samples.Count; i++) - HitObject.Samples[i] = HitObject.Samples[i].With(newVolume: lastHitNormal.Volume); - } - } - - if (HitObject is IHasRepeats hasRepeats) - { - // Make sure all the node samples are identical to the hit object's samples - for (int i = 0; i < hasRepeats.NodeSamples.Count; i++) - hasRepeats.NodeSamples[i] = HitObject.Samples.Select(o => o.With()).ToList(); - } - } - - /// - /// Invokes , - /// refreshing and parameters for the . - /// - protected void ApplyDefaultsToHitObject() => HitObject.ApplyDefaults(beatmap.ControlPointInfo, beatmap.Difficulty); - public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; protected override bool Handle(UIEvent e) diff --git a/osu.Game/Rulesets/Edit/Tools/HitObjectCompositionTool.cs b/osu.Game/Rulesets/Edit/Tools/CompositionTool.cs similarity index 84% rename from osu.Game/Rulesets/Edit/Tools/HitObjectCompositionTool.cs rename to osu.Game/Rulesets/Edit/Tools/CompositionTool.cs index ba1dc817bb..f509302daa 100644 --- a/osu.Game/Rulesets/Edit/Tools/HitObjectCompositionTool.cs +++ b/osu.Game/Rulesets/Edit/Tools/CompositionTool.cs @@ -6,13 +6,13 @@ using osu.Framework.Localisation; namespace osu.Game.Rulesets.Edit.Tools { - public abstract class HitObjectCompositionTool + public abstract class CompositionTool { public readonly string Name; public LocalisableString TooltipText { get; init; } - protected HitObjectCompositionTool(string name) + protected CompositionTool(string name) { Name = name; } diff --git a/osu.Game/Rulesets/Edit/Tools/SelectTool.cs b/osu.Game/Rulesets/Edit/Tools/SelectTool.cs index a272e9f480..7f8889bfca 100644 --- a/osu.Game/Rulesets/Edit/Tools/SelectTool.cs +++ b/osu.Game/Rulesets/Edit/Tools/SelectTool.cs @@ -9,7 +9,7 @@ using osu.Game.Graphics; namespace osu.Game.Rulesets.Edit.Tools { - public class SelectTool : HitObjectCompositionTool + public class SelectTool : CompositionTool { public SelectTool() : base("Select") @@ -18,6 +18,6 @@ namespace osu.Game.Rulesets.Edit.Tools public override Drawable CreateIcon() => new SpriteIcon { Icon = OsuIcon.EditorSelect }; - public override PlacementBlueprint CreatePlacementBlueprint() => null; + public override HitObjectPlacementBlueprint CreatePlacementBlueprint() => null; } } diff --git a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs index f1294ccc3c..f0296d45aa 100644 --- a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs @@ -41,6 +41,8 @@ namespace osu.Game.Screens.Edit.Compose.Components public PlacementBlueprint CurrentPlacement { get; private set; } + public HitObjectPlacementBlueprint CurrentHitObjectPlacement => CurrentPlacement as HitObjectPlacementBlueprint; + [Resolved(canBeNull: true)] private EditorScreenWithTimeline editorScreen { get; set; } @@ -164,13 +166,13 @@ namespace osu.Game.Screens.Edit.Compose.Components private void updatePlacementNewCombo() { - if (CurrentPlacement?.HitObject is IHasComboInformation c) + if (CurrentHitObjectPlacement?.HitObject is IHasComboInformation c) c.NewCombo = NewCombo.Value == TernaryState.True; } private void updatePlacementSamples() { - if (CurrentPlacement == null) return; + if (CurrentHitObjectPlacement == null) return; foreach (var kvp in SelectionHandler.SelectionSampleStates) sampleChanged(kvp.Key, kvp.Value.Value); @@ -181,9 +183,9 @@ namespace osu.Game.Screens.Edit.Compose.Components private void sampleChanged(string sampleName, TernaryState state) { - if (CurrentPlacement == null) return; + if (CurrentHitObjectPlacement == null) return; - var samples = CurrentPlacement.HitObject.Samples; + var samples = CurrentHitObjectPlacement.HitObject.Samples; var existingSample = samples.FirstOrDefault(s => s.Name == sampleName); @@ -196,19 +198,19 @@ namespace osu.Game.Screens.Edit.Compose.Components case TernaryState.True: if (existingSample == null) - samples.Add(CurrentPlacement.HitObject.CreateHitSampleInfo(sampleName)); + samples.Add(CurrentHitObjectPlacement.HitObject.CreateHitSampleInfo(sampleName)); break; } } private void bankChanged(string bankName, TernaryState state) { - if (CurrentPlacement == null) return; + if (CurrentHitObjectPlacement == null) return; if (bankName == EditorSelectionHandler.HIT_BANK_AUTO) - CurrentPlacement.AutomaticBankAssignment = state == TernaryState.True; + CurrentHitObjectPlacement.AutomaticBankAssignment = state == TernaryState.True; else if (state == TernaryState.True) - CurrentPlacement.HitObject.Samples = CurrentPlacement.HitObject.Samples.Select(s => s.With(newBank: bankName)).ToList(); + CurrentHitObjectPlacement.HitObject.Samples = CurrentHitObjectPlacement.HitObject.Samples.Select(s => s.With(newBank: bankName)).ToList(); } public readonly Bindable NewCombo = new Bindable { Description = "New Combo" }; @@ -386,12 +388,12 @@ namespace osu.Game.Screens.Edit.Compose.Components CurrentPlacement = null; } - private HitObjectCompositionTool currentTool; + private CompositionTool currentTool; /// /// The current placement tool. /// - public HitObjectCompositionTool CurrentTool + public CompositionTool CurrentTool { get => currentTool; diff --git a/osu.Game/Tests/Visual/PlacementBlueprintTestScene.cs b/osu.Game/Tests/Visual/PlacementBlueprintTestScene.cs index 0027e03492..c8d9ef8fc8 100644 --- a/osu.Game/Tests/Visual/PlacementBlueprintTestScene.cs +++ b/osu.Game/Tests/Visual/PlacementBlueprintTestScene.cs @@ -19,7 +19,7 @@ namespace osu.Game.Tests.Visual public abstract partial class PlacementBlueprintTestScene : OsuManualInputManagerTestScene, IPlacementHandler { protected readonly Container HitObjectContainer; - protected PlacementBlueprint CurrentBlueprint { get; private set; } + protected HitObjectPlacementBlueprint CurrentBlueprint { get; private set; } protected PlacementBlueprintTestScene() { @@ -87,14 +87,14 @@ namespace osu.Game.Tests.Visual CurrentBlueprint.UpdateTimeAndPosition(SnapForBlueprint(CurrentBlueprint)); } - protected virtual SnapResult SnapForBlueprint(PlacementBlueprint blueprint) => + protected virtual SnapResult SnapForBlueprint(HitObjectPlacementBlueprint blueprint) => new SnapResult(InputManager.CurrentState.Mouse.Position, null); public override void Add(Drawable drawable) { base.Add(drawable); - if (drawable is PlacementBlueprint blueprint) + if (drawable is HitObjectPlacementBlueprint blueprint) { blueprint.Show(); blueprint.UpdateTimeAndPosition(SnapForBlueprint(blueprint)); @@ -106,6 +106,6 @@ namespace osu.Game.Tests.Visual protected virtual Container CreateHitObjectContainer() => new Container { RelativeSizeAxes = Axes.Both }; protected abstract DrawableHitObject CreateHitObject(HitObject hitObject); - protected abstract PlacementBlueprint CreateBlueprint(); + protected abstract HitObjectPlacementBlueprint CreateBlueprint(); } } From d26e677bb7ceb9e35e7af22c77c9ccfe39672483 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Mon, 23 Sep 2024 17:34:45 +0200 Subject: [PATCH 2394/2493] fix warnings --- .../Editor/TestSceneBananaShowerPlacementBlueprint.cs | 2 +- osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs | 2 +- osu.Game.Tests/Visual/Editing/TestScenePlacementBlueprint.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneBananaShowerPlacementBlueprint.cs b/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneBananaShowerPlacementBlueprint.cs index badd8e967d..296d34d628 100644 --- a/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneBananaShowerPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneBananaShowerPlacementBlueprint.cs @@ -71,7 +71,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor AddClickStep(MouseButton.Left); AddClickStep(MouseButton.Right); AddAssert("banana shower is not placed", () => LastObject == null); - AddAssert("state is waiting", () => CurrentBlueprint?.PlacementActive == HitObjectPlacementBlueprint.PlacementState.Waiting); + AddAssert("state is waiting", () => CurrentBlueprint?.PlacementActive == PlacementBlueprint.PlacementState.Waiting); } [Test] diff --git a/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs b/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs index 8460e238f6..7323c7a91a 100644 --- a/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs +++ b/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs @@ -168,7 +168,7 @@ namespace osu.Game.Rulesets.Catch.Edit if (EditorBeatmap.PlacementObject.Value is JuiceStream) { // Juice stream path is not subject to snapping. - if (BlueprintContainer.CurrentPlacement.PlacementActive is HitObjectPlacementBlueprint.PlacementState.Active) + if (BlueprintContainer.CurrentPlacement.PlacementActive is PlacementBlueprint.PlacementState.Active) return null; } diff --git a/osu.Game.Tests/Visual/Editing/TestScenePlacementBlueprint.cs b/osu.Game.Tests/Visual/Editing/TestScenePlacementBlueprint.cs index 6f3342f8ce..fe74e1b346 100644 --- a/osu.Game.Tests/Visual/Editing/TestScenePlacementBlueprint.cs +++ b/osu.Game.Tests/Visual/Editing/TestScenePlacementBlueprint.cs @@ -122,7 +122,7 @@ namespace osu.Game.Tests.Visual.Editing AddAssert("editor is still current", () => Editor.IsCurrentScreen()); AddAssert("slider not placed", () => EditorBeatmap.HitObjects.Count, () => Is.EqualTo(0)); AddAssert("no active placement", () => this.ChildrenOfType().Single().CurrentPlacement.PlacementActive, - () => Is.EqualTo(HitObjectPlacementBlueprint.PlacementState.Waiting)); + () => Is.EqualTo(PlacementBlueprint.PlacementState.Waiting)); } [Test] From 3ab04d98f618df16362e5caf25c908df07012226 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 25 Sep 2024 16:45:37 +0900 Subject: [PATCH 2395/2493] Fix Realm-related iOS crashes by removing object references --- osu.Game/Beatmaps/BeatmapImporter.cs | 4 +++- osu.Game/Online/BeatmapDownloadTracker.cs | 4 +++- osu.Game/Online/ScoreDownloadTracker.cs | 10 +++++++--- osu.Game/Skinning/RealmBackedResourceStore.cs | 4 +++- osu.Game/Skinning/SkinManager.cs | 4 +++- 5 files changed, 19 insertions(+), 7 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapImporter.cs b/osu.Game/Beatmaps/BeatmapImporter.cs index 8acaebd1a8..63d8215d73 100644 --- a/osu.Game/Beatmaps/BeatmapImporter.cs +++ b/osu.Game/Beatmaps/BeatmapImporter.cs @@ -198,8 +198,10 @@ namespace osu.Game.Beatmaps if (beatmapSet.OnlineID > 0) { + int onlineId = beatmapSet.OnlineID; + // OnlineID should really be unique, but to avoid catastrophic failure let's iterate just to be sure. - foreach (var existingSetWithSameOnlineID in realm.All().Where(b => b.OnlineID == beatmapSet.OnlineID)) + foreach (var existingSetWithSameOnlineID in realm.All().Where(b => b.OnlineID == onlineId)) { existingSetWithSameOnlineID.DeletePending = true; existingSetWithSameOnlineID.OnlineID = -1; diff --git a/osu.Game/Online/BeatmapDownloadTracker.cs b/osu.Game/Online/BeatmapDownloadTracker.cs index 3db602c353..c1c3d17ff6 100644 --- a/osu.Game/Online/BeatmapDownloadTracker.cs +++ b/osu.Game/Online/BeatmapDownloadTracker.cs @@ -40,7 +40,9 @@ 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, _) => + int onlineId = TrackedItem.OnlineID; + + realmSubscription = realm.RegisterForNotifications(r => r.All().Where(s => s.OnlineID == onlineId && !s.DeletePending), (items, _) => { if (items.Any()) Schedule(() => UpdateState(DownloadState.LocallyAvailable)); diff --git a/osu.Game/Online/ScoreDownloadTracker.cs b/osu.Game/Online/ScoreDownloadTracker.cs index dfdac24d19..eb687a7023 100644 --- a/osu.Game/Online/ScoreDownloadTracker.cs +++ b/osu.Game/Online/ScoreDownloadTracker.cs @@ -46,10 +46,14 @@ namespace osu.Game.Online Downloader.DownloadBegan += downloadBegan; Downloader.DownloadFailed += downloadFailed; + long onlineId = TrackedItem.OnlineID; + long legacyOnlineId = TrackedItem.LegacyOnlineID; + string hash = TrackedItem.Hash; + realmSubscription = realm.RegisterForNotifications(r => r.All().Where(s => - ((s.OnlineID > 0 && s.OnlineID == TrackedItem.OnlineID) - || (s.LegacyOnlineID > 0 && s.LegacyOnlineID == TrackedItem.LegacyOnlineID) - || (!string.IsNullOrEmpty(s.Hash) && s.Hash == TrackedItem.Hash)) + ((s.OnlineID > 0 && s.OnlineID == onlineId) + || (s.LegacyOnlineID > 0 && s.LegacyOnlineID == legacyOnlineId) + || (!string.IsNullOrEmpty(s.Hash) && s.Hash == hash)) && !s.DeletePending), (items, _) => { if (items.Any()) diff --git a/osu.Game/Skinning/RealmBackedResourceStore.cs b/osu.Game/Skinning/RealmBackedResourceStore.cs index cce099a268..b1289bd0c5 100644 --- a/osu.Game/Skinning/RealmBackedResourceStore.cs +++ b/osu.Game/Skinning/RealmBackedResourceStore.cs @@ -29,7 +29,9 @@ namespace osu.Game.Skinning invalidateCache(); Debug.Assert(fileToStoragePathMapping != null); - realmSubscription = realm?.RegisterForNotifications(r => r.All().Where(s => s.ID == source.ID), skinChanged); + Guid id = source.ID; + + realmSubscription = realm?.RegisterForNotifications(r => r.All().Where(s => s.ID == id), skinChanged); } protected override void Dispose(bool disposing) diff --git a/osu.Game/Skinning/SkinManager.cs b/osu.Game/Skinning/SkinManager.cs index 4f816d88d2..cd431bd80c 100644 --- a/osu.Game/Skinning/SkinManager.cs +++ b/osu.Game/Skinning/SkinManager.cs @@ -131,9 +131,11 @@ namespace osu.Game.Skinning { Realm.Run(r => { + Guid currentSkinId = CurrentSkinInfo.Value.ID; + // choose from only user skins, removing the current selection to ensure a new one is chosen. var randomChoices = r.All() - .Where(s => !s.DeletePending && s.ID != CurrentSkinInfo.Value.ID) + .Where(s => !s.DeletePending && s.ID != currentSkinId) .ToArray(); if (randomChoices.Length == 0) From fd4891cf31de0f5e75b937fdf168a17c16e65a3b Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 25 Sep 2024 20:34:50 +0900 Subject: [PATCH 2396/2493] Fix similar Bindable-related crashes --- .../Utils/BindableValueAccessorTest.cs | 52 +++++++++++++++++++ .../Configuration/SettingSourceAttribute.cs | 7 +-- osu.Game/Rulesets/Mods/Mod.cs | 3 +- osu.Game/Utils/BindableValueAccessor.cs | 44 ++++++++++++++++ 4 files changed, 99 insertions(+), 7 deletions(-) create mode 100644 osu.Game.Tests/Utils/BindableValueAccessorTest.cs create mode 100644 osu.Game/Utils/BindableValueAccessor.cs diff --git a/osu.Game.Tests/Utils/BindableValueAccessorTest.cs b/osu.Game.Tests/Utils/BindableValueAccessorTest.cs new file mode 100644 index 0000000000..f09623dbfc --- /dev/null +++ b/osu.Game.Tests/Utils/BindableValueAccessorTest.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 NUnit.Framework; +using osu.Framework.Bindables; +using osu.Game.Utils; + +namespace osu.Game.Tests.Utils +{ + [TestFixture] + public class BindableValueAccessorTest + { + [Test] + public void GetValue() + { + const int value = 1337; + + BindableInt bindable = new BindableInt(value); + Assert.That(BindableValueAccessor.GetValue(bindable), Is.EqualTo(value)); + } + + [Test] + public void SetValue() + { + const int value = 1337; + + BindableInt bindable = new BindableInt(); + BindableValueAccessor.SetValue(bindable, value); + + Assert.That(bindable.Value, Is.EqualTo(value)); + } + + [Test] + public void GetInvalidBindable() + { + BindableList list = new BindableList(); + Assert.That(BindableValueAccessor.GetValue(list), Is.EqualTo(list)); + } + + [Test] + public void SetInvalidBindable() + { + const int value = 1337; + + BindableList list = new BindableList { value }; + BindableValueAccessor.SetValue(list, 2); + + Assert.That(list, Has.Exactly(1).Items); + Assert.That(list[0], Is.EqualTo(value)); + } + } +} diff --git a/osu.Game/Configuration/SettingSourceAttribute.cs b/osu.Game/Configuration/SettingSourceAttribute.cs index 1e425c88a6..580366a75a 100644 --- a/osu.Game/Configuration/SettingSourceAttribute.cs +++ b/osu.Game/Configuration/SettingSourceAttribute.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; -using System.Diagnostics; using System.Linq; using System.Reflection; using JetBrains.Annotations; @@ -15,6 +14,7 @@ using osu.Framework.Graphics; using osu.Framework.Localisation; using osu.Game.Graphics.UserInterface; using osu.Game.Overlays.Settings; +using osu.Game.Utils; namespace osu.Game.Configuration { @@ -228,10 +228,7 @@ namespace osu.Game.Configuration return b.Value; case IBindable u: - // An unknown (e.g. enum) generic type. - var valueMethod = u.GetType().GetProperty(nameof(IBindable.Value)); - Debug.Assert(valueMethod != null); - return valueMethod.GetValue(u)!; + return BindableValueAccessor.GetValue(u); default: // fall back for non-bindable cases. diff --git a/osu.Game/Rulesets/Mods/Mod.cs b/osu.Game/Rulesets/Mods/Mod.cs index b9a937b1a2..1b21216235 100644 --- a/osu.Game/Rulesets/Mods/Mod.cs +++ b/osu.Game/Rulesets/Mods/Mod.cs @@ -266,8 +266,7 @@ namespace osu.Game.Rulesets.Mods // TODO: special case for handling number types - PropertyInfo property = targetSetting.GetType().GetProperty(nameof(Bindable.Value))!; - property.SetValue(targetSetting, property.GetValue(sourceSetting)); + BindableValueAccessor.SetValue(targetSetting, BindableValueAccessor.GetValue(sourceSetting)); } } diff --git a/osu.Game/Utils/BindableValueAccessor.cs b/osu.Game/Utils/BindableValueAccessor.cs new file mode 100644 index 0000000000..dd097ada36 --- /dev/null +++ b/osu.Game/Utils/BindableValueAccessor.cs @@ -0,0 +1,44 @@ +// 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 System.Reflection; +using osu.Framework.Bindables; +using osu.Framework.Extensions.TypeExtensions; + +namespace osu.Game.Utils +{ + internal static class BindableValueAccessor + { + private static readonly MethodInfo get_method = typeof(BindableValueAccessor).GetMethod(nameof(getValue), BindingFlags.Static | BindingFlags.NonPublic)!; + private static readonly MethodInfo set_method = typeof(BindableValueAccessor).GetMethod(nameof(setValue), BindingFlags.Static | BindingFlags.NonPublic)!; + + public static object GetValue(IBindable bindable) + { + Type? bindableWithValueType = bindable.GetType().GetInterfaces().FirstOrDefault(isBindableT); + if (bindableWithValueType == null) + return bindable; + + return get_method.MakeGenericMethod(bindableWithValueType.GenericTypeArguments[0]).Invoke(null, [bindable])!; + } + + public static void SetValue(IBindable bindable, object value) + { + Type? bindableWithValueType = bindable.GetType().EnumerateBaseTypes().FirstOrDefault(isBindableT); + if (bindableWithValueType == null) + return; + + set_method.MakeGenericMethod(bindableWithValueType.GenericTypeArguments[0]).Invoke(null, [bindable, value]); + } + + private static bool isBindableT(Type type) + => type.IsGenericType + && (type.GetGenericTypeDefinition() == typeof(Bindable<>) + || type.GetGenericTypeDefinition() == typeof(IBindable<>)); + + private static object getValue(object bindable) => ((IBindable)bindable).Value!; + + private static object setValue(object bindable, object value) => ((Bindable)bindable).Value = (T)value; + } +} From 2fe229d62073a80c1a9f07155a510d9f80713584 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 25 Sep 2024 22:46:53 +0900 Subject: [PATCH 2397/2493] Inline condition --- osu.Game/Utils/BindableValueAccessor.cs | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/osu.Game/Utils/BindableValueAccessor.cs b/osu.Game/Utils/BindableValueAccessor.cs index dd097ada36..a4cd356339 100644 --- a/osu.Game/Utils/BindableValueAccessor.cs +++ b/osu.Game/Utils/BindableValueAccessor.cs @@ -16,7 +16,7 @@ namespace osu.Game.Utils public static object GetValue(IBindable bindable) { - Type? bindableWithValueType = bindable.GetType().GetInterfaces().FirstOrDefault(isBindableT); + Type? bindableWithValueType = bindable.GetType().GetInterfaces().FirstOrDefault(t => t.IsGenericType && t.GetGenericTypeDefinition() == typeof(IBindable<>)); if (bindableWithValueType == null) return bindable; @@ -25,18 +25,13 @@ namespace osu.Game.Utils public static void SetValue(IBindable bindable, object value) { - Type? bindableWithValueType = bindable.GetType().EnumerateBaseTypes().FirstOrDefault(isBindableT); + Type? bindableWithValueType = bindable.GetType().EnumerateBaseTypes().SingleOrDefault(t => t.IsGenericType && t.GetGenericTypeDefinition() == typeof(Bindable<>)); if (bindableWithValueType == null) return; set_method.MakeGenericMethod(bindableWithValueType.GenericTypeArguments[0]).Invoke(null, [bindable, value]); } - private static bool isBindableT(Type type) - => type.IsGenericType - && (type.GetGenericTypeDefinition() == typeof(Bindable<>) - || type.GetGenericTypeDefinition() == typeof(IBindable<>)); - private static object getValue(object bindable) => ((IBindable)bindable).Value!; private static object setValue(object bindable, object value) => ((Bindable)bindable).Value = (T)value; From fe8b9536ffa085326b889dce768ed2b035c3a67f Mon Sep 17 00:00:00 2001 From: StanR Date: Wed, 25 Sep 2024 18:58:24 +0500 Subject: [PATCH 2398/2493] Bring back some old nerfs as balancing factor --- .../Difficulty/Evaluators/RhythmEvaluator.cs | 36 ++++++++++++------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs index edb67f37d9..08efb187fe 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs @@ -12,10 +12,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators { public static class RhythmEvaluator { - private const int history_time_max = 4 * 1000; // 4 seconds + private const int history_time_max = 5 * 1000; // 5 seconds private const int history_objects_max = 32; - private const double rhythm_overall_multiplier = 0.92; - private const double rhythm_ratio_multiplier = 11.5; + private const double rhythm_overall_multiplier = 0.95; + private const double rhythm_ratio_multiplier = 12.0; /// /// Calculates a rhythm multiplier for the difficulty of the tap associated with historic data of the current . @@ -94,19 +94,20 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators // bpm change was from a slider, this is easier typically than circle -> circle // unintentional side effect is that bursts with kicksliders at the ends might have lower difficulty than bursts without sliders if (prevObj.BaseObject is Slider) - effectiveRatio *= 0.15; + effectiveRatio *= 0.3; // repeated island polarity (2 -> 4, 3 -> 5) if (island.IsSimilarPolarity(previousIsland)) - effectiveRatio *= 0.3; + effectiveRatio *= 0.5; // previous increase happened a note ago, 1/1->1/2-1/4, dont want to buff this. if (lastDelta > prevDelta + deltaDifferenceEpsilon && prevDelta > currDelta + deltaDifferenceEpsilon) effectiveRatio *= 0.125; - // singletaps are easier to control - if (island.DeltaCount == 1) - effectiveRatio *= 0.7; + // repeated island size (ex: triplet -> triplet) + // TODO: remove this nerf since its staying here only for balancing purposes because of the flawed ratio calculation + if (previousIsland.DeltaCount == island.DeltaCount) + effectiveRatio *= 0.5; var islandCount = islandCounts.FirstOrDefault(x => x.Island.Equals(island)); @@ -150,6 +151,15 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators // Begin counting island until we change speed again. firstDeltaSwitch = true; + // bpm change is into slider, this is easy acc window + if (currObj.BaseObject is Slider) + effectiveRatio *= 0.6; + + // bpm change was from a slider, this is easier typically than circle -> circle + // unintentional side effect is that bursts with kicksliders at the ends might have lower difficulty than bursts without sliders + if (prevObj.BaseObject is Slider) + effectiveRatio *= 0.6; + startRatio = effectiveRatio; island = new Island((int)currDelta, deltaDifferenceEpsilon); @@ -180,12 +190,12 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators DeltaCount++; } - public int Delta { get; private set; } + public int Delta { get; private set; } = int.MaxValue; public int DeltaCount { get; private set; } public void AddDelta(int delta) { - if (Delta == default) + if (Delta == int.MaxValue) Delta = Math.Max(delta, OsuDifficultyHitObject.MIN_DELTA_TIME); DeltaCount++; @@ -193,9 +203,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators public bool IsSimilarPolarity(Island other) { - // consider islands to be of similar polarity only if they're having the same average delta (we don't want to consider 3 singletaps similar to a triple) - return DeltaCount % 2 == other.DeltaCount % 2 && - Math.Abs(Delta - other.Delta) < deltaDifferenceEpsilon; + // TODO: consider islands to be of similar polarity only if they're having the same average delta (we don't want to consider 3 singletaps similar to a triple) + // naively adding delta check here breaks _a lot_ of maps because of the flawed ratio calculation + return DeltaCount % 2 == other.DeltaCount % 2; } public bool Equals(Island? other) From df0966abb2475a53fed24acec5429c44dfb29bb8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 26 Sep 2024 01:11:23 +0900 Subject: [PATCH 2399/2493] Update velopack and switch to using async version of `WaitExitThenApplyUpdates` --- osu.Desktop/Updater/VelopackUpdateManager.cs | 15 ++++++++------- osu.Desktop/osu.Desktop.csproj | 2 +- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/osu.Desktop/Updater/VelopackUpdateManager.cs b/osu.Desktop/Updater/VelopackUpdateManager.cs index ae58a8793c..7a79284533 100644 --- a/osu.Desktop/Updater/VelopackUpdateManager.cs +++ b/osu.Desktop/Updater/VelopackUpdateManager.cs @@ -66,7 +66,7 @@ namespace osu.Desktop.Updater { Activated = () => { - restartToApplyUpdate(); + Task.Run(restartToApplyUpdate); return true; } }); @@ -88,7 +88,11 @@ namespace osu.Desktop.Updater { notification = new UpdateProgressNotification { - CompletionClickAction = restartToApplyUpdate, + CompletionClickAction = () => + { + Task.Run(restartToApplyUpdate); + return true; + }, }; Schedule(() => notificationOverlay.Post(notification)); @@ -127,13 +131,10 @@ namespace osu.Desktop.Updater return true; } - private bool restartToApplyUpdate() + private async Task restartToApplyUpdate() { - // TODO: Migrate this to async flow whenever available (see https://github.com/ppy/osu/pull/28743#discussion_r1740505665). - // Currently there's an internal Thread.Sleep(300) which will cause a stutter when the user clicks to restart. - updateManager.WaitExitThenApplyUpdates(pendingUpdate?.TargetFullRelease); + await updateManager.WaitExitThenApplyUpdatesAsync(pendingUpdate?.TargetFullRelease).ConfigureAwait(false); Schedule(() => game.AttemptExit()); - return true; } } } diff --git a/osu.Desktop/osu.Desktop.csproj b/osu.Desktop/osu.Desktop.csproj index bf5f26b352..3df8c16f08 100644 --- a/osu.Desktop/osu.Desktop.csproj +++ b/osu.Desktop/osu.Desktop.csproj @@ -26,7 +26,7 @@ - + From f4055d923f7fe28e0aed4947ca2d66cc5e8bd0c6 Mon Sep 17 00:00:00 2001 From: tsunyoku Date: Wed, 25 Sep 2024 18:14:15 +0100 Subject: [PATCH 2400/2493] increase aim skill multiplier --- osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs index 1fbe03395c..faf91e4652 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs @@ -23,7 +23,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills private double currentStrain; - private double skillMultiplier => 24.963; + private double skillMultiplier => 25.18; private double strainDecayBase => 0.15; private double strainDecay(double ms) => Math.Pow(strainDecayBase, ms / 1000); From 89e8baf1d35dabc6133d609e9af5e1da50cff976 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 26 Sep 2024 16:48:42 +0900 Subject: [PATCH 2401/2493] Add inline comments for iOS locals --- osu.Game/Beatmaps/BeatmapImporter.cs | 1 + osu.Game/Online/BeatmapDownloadTracker.cs | 1 + osu.Game/Online/ScoreDownloadTracker.cs | 1 + osu.Game/Skinning/RealmBackedResourceStore.cs | 1 + osu.Game/Skinning/SkinManager.cs | 1 + 5 files changed, 5 insertions(+) diff --git a/osu.Game/Beatmaps/BeatmapImporter.cs b/osu.Game/Beatmaps/BeatmapImporter.cs index 63d8215d73..94144e4695 100644 --- a/osu.Game/Beatmaps/BeatmapImporter.cs +++ b/osu.Game/Beatmaps/BeatmapImporter.cs @@ -198,6 +198,7 @@ namespace osu.Game.Beatmaps if (beatmapSet.OnlineID > 0) { + // Required local for iOS. Will cause runtime crash if inlined. int onlineId = beatmapSet.OnlineID; // OnlineID should really be unique, but to avoid catastrophic failure let's iterate just to be sure. diff --git a/osu.Game/Online/BeatmapDownloadTracker.cs b/osu.Game/Online/BeatmapDownloadTracker.cs index c1c3d17ff6..6a2163c3a2 100644 --- a/osu.Game/Online/BeatmapDownloadTracker.cs +++ b/osu.Game/Online/BeatmapDownloadTracker.cs @@ -40,6 +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 }; + // Required local for iOS. Will cause runtime crash if inlined. int onlineId = TrackedItem.OnlineID; realmSubscription = realm.RegisterForNotifications(r => r.All().Where(s => s.OnlineID == onlineId && !s.DeletePending), (items, _) => diff --git a/osu.Game/Online/ScoreDownloadTracker.cs b/osu.Game/Online/ScoreDownloadTracker.cs index eb687a7023..5f6ba15d05 100644 --- a/osu.Game/Online/ScoreDownloadTracker.cs +++ b/osu.Game/Online/ScoreDownloadTracker.cs @@ -46,6 +46,7 @@ namespace osu.Game.Online Downloader.DownloadBegan += downloadBegan; Downloader.DownloadFailed += downloadFailed; + // Required local for iOS. Will cause runtime crash if inlined. long onlineId = TrackedItem.OnlineID; long legacyOnlineId = TrackedItem.LegacyOnlineID; string hash = TrackedItem.Hash; diff --git a/osu.Game/Skinning/RealmBackedResourceStore.cs b/osu.Game/Skinning/RealmBackedResourceStore.cs index b1289bd0c5..f41bd89b7a 100644 --- a/osu.Game/Skinning/RealmBackedResourceStore.cs +++ b/osu.Game/Skinning/RealmBackedResourceStore.cs @@ -29,6 +29,7 @@ namespace osu.Game.Skinning invalidateCache(); Debug.Assert(fileToStoragePathMapping != null); + // Required local for iOS. Will cause runtime crash if inlined. Guid id = source.ID; realmSubscription = realm?.RegisterForNotifications(r => r.All().Where(s => s.ID == id), skinChanged); diff --git a/osu.Game/Skinning/SkinManager.cs b/osu.Game/Skinning/SkinManager.cs index cd431bd80c..9018c2e2c3 100644 --- a/osu.Game/Skinning/SkinManager.cs +++ b/osu.Game/Skinning/SkinManager.cs @@ -131,6 +131,7 @@ namespace osu.Game.Skinning { Realm.Run(r => { + // Required local for iOS. Will cause runtime crash if inlined. Guid currentSkinId = CurrentSkinInfo.Value.ID; // choose from only user skins, removing the current selection to ensure a new one is chosen. From b1a05f463e3bfe927dea17513953ea56cf890a38 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 26 Sep 2024 19:42:20 +0900 Subject: [PATCH 2402/2493] Reduce size of hidden toggle slightly --- .../UserInterfaceV2/OsuDirectorySelectorHiddenToggle.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelectorHiddenToggle.cs b/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelectorHiddenToggle.cs index ffedc9386f..521ebebf91 100644 --- a/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelectorHiddenToggle.cs +++ b/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelectorHiddenToggle.cs @@ -16,13 +16,15 @@ namespace osu.Game.Graphics.UserInterfaceV2 { RelativeSizeAxes = Axes.None; AutoSizeAxes = Axes.None; - Size = new Vector2(100, OsuDirectorySelectorBreadcrumbDisplay.HEIGHT); + Size = new Vector2(140, OsuDirectorySelectorBreadcrumbDisplay.HEIGHT); Margin = new MarginPadding { Right = OsuDirectorySelectorBreadcrumbDisplay.HORIZONTAL_PADDING, }; Anchor = Anchor.CentreLeft; Origin = Anchor.CentreLeft; LabelTextFlowContainer.Anchor = Anchor.CentreLeft; LabelTextFlowContainer.Origin = Anchor.CentreLeft; LabelText = @"Show hidden"; + + Scale = new Vector2(0.8f); } [BackgroundDependencyLoader(true)] From f4a4807449b6ef1d8504b655a16d77a77ed79e95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 28 Aug 2024 10:29:10 +0200 Subject: [PATCH 2403/2493] Implement "form" file picker --- .../UserInterface/TestSceneFormControls.cs | 5 + .../UserInterfaceV2/FormFileSelector.cs | 262 ++++++++++++++++++ 2 files changed, 267 insertions(+) create mode 100644 osu.Game/Graphics/UserInterfaceV2/FormFileSelector.cs diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneFormControls.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneFormControls.cs index 89b4ae9f97..2a0b0515a1 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneFormControls.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneFormControls.cs @@ -100,6 +100,11 @@ namespace osu.Game.Tests.Visual.UserInterface Caption = EditorSetupStrings.EnableCountdown, HintText = EditorSetupStrings.CountdownDescription, }, + new FormFileSelector + { + Caption = "Audio file", + PlaceholderText = "Select an audio file", + }, }, }, } diff --git a/osu.Game/Graphics/UserInterfaceV2/FormFileSelector.cs b/osu.Game/Graphics/UserInterfaceV2/FormFileSelector.cs new file mode 100644 index 0000000000..66f68f3e3b --- /dev/null +++ b/osu.Game/Graphics/UserInterfaceV2/FormFileSelector.cs @@ -0,0 +1,262 @@ +// 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.IO; +using System.Linq; +using System.Threading.Tasks; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Extensions; +using osu.Framework.Extensions.ObjectExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; +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.Framework.Platform; +using osu.Game.Database; +using osu.Game.Graphics.Sprites; +using osu.Game.Overlays; +using osuTK; + +namespace osu.Game.Graphics.UserInterfaceV2 +{ + public partial class FormFileSelector : CompositeDrawable, IHasCurrentValue, ICanAcceptFiles, IHasPopover + { + public Bindable Current + { + get => current.Current; + set => current.Current = value; + } + + private readonly BindableWithCurrent current = new BindableWithCurrent(); + + public IEnumerable HandledExtensions => handledExtensions; + + private readonly string[] handledExtensions; + + /// + /// The initial path to use when displaying the . + /// + /// + /// Uses a value before the first selection is made + /// to ensure that the first selection starts at . + /// + private string? initialChooserPath; + + private readonly Bindable popoverState = new Bindable(); + + /// + /// Caption describing this file selector, displayed on top of the controls. + /// + public LocalisableString Caption { get; init; } + + /// + /// Hint text containing an extended description of this file selector, displayed in a tooltip when hovering the caption. + /// + public LocalisableString HintText { get; init; } + + /// + /// Text displayed in the selector when no file is selected. + /// + public LocalisableString PlaceholderText { get; init; } + + private Box background = null!; + + private FormFieldCaption caption = null!; + private OsuSpriteText placeholderText = null!; + private OsuSpriteText filenameText = null!; + + [Resolved] + private OverlayColourProvider colourProvider { get; set; } = null!; + + [Resolved] + private OsuGameBase game { get; set; } = null!; + + public FormFileSelector(params string[] handledExtensions) + { + this.handledExtensions = handledExtensions; + } + + [BackgroundDependencyLoader] + private void load() + { + RelativeSizeAxes = Axes.X; + Height = 50; + + Masking = true; + CornerRadius = 5; + + InternalChildren = new Drawable[] + { + background = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background5, + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding(9), + Children = new Drawable[] + { + caption = new FormFieldCaption + { + Anchor = Anchor.TopLeft, + Origin = Anchor.TopLeft, + Caption = Caption, + TooltipText = HintText, + }, + placeholderText = new OsuSpriteText + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + RelativeSizeAxes = Axes.X, + Width = 1, + Text = PlaceholderText, + Colour = colourProvider.Foreground1, + }, + filenameText = new OsuSpriteText + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + RelativeSizeAxes = Axes.X, + Width = 1, + }, + new SpriteIcon + { + Anchor = Anchor.BottomRight, + Origin = Anchor.BottomRight, + Icon = FontAwesome.Solid.FolderOpen, + Size = new Vector2(16), + Colour = colourProvider.Light1, + } + }, + }, + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + popoverState.BindValueChanged(_ => updateState()); + current.BindValueChanged(_ => + { + updateState(); + onFileSelected(); + }); + current.BindDisabledChanged(_ => updateState(), true); + game.RegisterImportHandler(this); + } + + private void onFileSelected() + { + if (Current.Value != null) + this.HidePopover(); + + initialChooserPath = Current.Value?.DirectoryName; + placeholderText.Alpha = Current.Value == null ? 1 : 0; + filenameText.Text = Current.Value?.Name ?? string.Empty; + background.FlashColour(ColourInfo.GradientVertical(colourProvider.Background5, colourProvider.Dark2), 800, Easing.OutQuint); + } + + protected override bool OnClick(ClickEvent e) + { + this.ShowPopover(); + return true; + } + + protected override bool OnHover(HoverEvent e) + { + updateState(); + return true; + } + + protected override void OnHoverLost(HoverLostEvent e) + { + base.OnHoverLost(e); + updateState(); + } + + private void updateState() + { + caption.Colour = Current.Disabled ? colourProvider.Foreground1 : colourProvider.Content2; + filenameText.Colour = Current.Disabled ? colourProvider.Foreground1 : colourProvider.Content1; + + if (!Current.Disabled) + { + BorderThickness = IsHovered || popoverState.Value == Visibility.Visible ? 2 : 0; + BorderColour = popoverState.Value == Visibility.Visible ? colourProvider.Highlight1 : colourProvider.Light4; + + if (popoverState.Value == Visibility.Visible) + background.Colour = ColourInfo.GradientVertical(colourProvider.Background5, colourProvider.Dark3); + else if (IsHovered) + background.Colour = ColourInfo.GradientVertical(colourProvider.Background5, colourProvider.Dark4); + else + background.Colour = colourProvider.Background5; + } + else + { + background.Colour = colourProvider.Background4; + } + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (game.IsNotNull()) + game.UnregisterImportHandler(this); + } + + Task ICanAcceptFiles.Import(params string[] paths) + { + Schedule(() => Current.Value = new FileInfo(paths.First())); + return Task.CompletedTask; + } + + Task ICanAcceptFiles.Import(ImportTask[] tasks, ImportParameters parameters) => throw new NotImplementedException(); + + public Popover GetPopover() + { + var popover = new FileChooserPopover(handledExtensions, Current, initialChooserPath); + popoverState.UnbindBindings(); + popoverState.BindTo(popover.State); + return popover; + } + + private partial class FileChooserPopover : OsuPopover + { + protected override string PopInSampleName => "UI/overlay-big-pop-in"; + protected override string PopOutSampleName => "UI/overlay-big-pop-out"; + + public FileChooserPopover(string[] handledExtensions, Bindable currentFile, string? chooserPath) + : base(false) + { + Child = new Container + { + Size = new Vector2(600, 400), + Child = new OsuFileSelector(chooserPath, handledExtensions) + { + RelativeSizeAxes = Axes.Both, + CurrentFile = { BindTarget = currentFile } + }, + }; + } + + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider) + { + Body.BorderThickness = 2; + Body.BorderColour = colourProvider.Highlight1; + } + } + } +} From 9e9bfc3721db83e5926fb6ee9bcfa0f4a2a1f684 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 27 Sep 2024 12:48:50 +0900 Subject: [PATCH 2404/2493] Update velopack with zstd changes Closes https://github.com/ppy/osu/issues/29810. --- 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 3df8c16f08..342b28f5ef 100644 --- a/osu.Desktop/osu.Desktop.csproj +++ b/osu.Desktop/osu.Desktop.csproj @@ -26,7 +26,7 @@ - + From cbeeb4a2b4b5cc7485f3f44012a14103ad8f2987 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 27 Sep 2024 13:43:57 +0900 Subject: [PATCH 2405/2493] Add basic hover states for file selector elements --- .../UserInterfaceV2/FormFileSelector.cs | 19 +++++- .../OsuDirectorySelectorBreadcrumbDisplay.cs | 20 +------ .../OsuDirectorySelectorDirectory.cs | 6 +- .../UserInterfaceV2/OsuFileSelector.cs | 6 +- .../OsuFileSelectorBackgroundLayer.cs | 59 +++++++++++++++++++ 5 files changed, 79 insertions(+), 31 deletions(-) create mode 100644 osu.Game/Graphics/UserInterfaceV2/OsuFileSelectorBackgroundLayer.cs diff --git a/osu.Game/Graphics/UserInterfaceV2/FormFileSelector.cs b/osu.Game/Graphics/UserInterfaceV2/FormFileSelector.cs index 66f68f3e3b..55cc026d7c 100644 --- a/osu.Game/Graphics/UserInterfaceV2/FormFileSelector.cs +++ b/osu.Game/Graphics/UserInterfaceV2/FormFileSelector.cs @@ -24,6 +24,7 @@ using osu.Game.Database; using osu.Game.Graphics.Sprites; using osu.Game.Overlays; using osuTK; +using osuTK.Graphics; namespace osu.Game.Graphics.UserInterfaceV2 { @@ -254,8 +255,22 @@ namespace osu.Game.Graphics.UserInterfaceV2 [BackgroundDependencyLoader] private void load(OverlayColourProvider colourProvider) { - Body.BorderThickness = 2; - Body.BorderColour = colourProvider.Highlight1; + Add(new Container + { + RelativeSizeAxes = Axes.Both, + Masking = true, + BorderThickness = 2, + CornerRadius = 10, + BorderColour = colourProvider.Highlight1, + Children = new Drawable[] + { + new Box + { + Colour = Color4.Transparent, + RelativeSizeAxes = Axes.Both, + }, + } + }); } } } diff --git a/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelectorBreadcrumbDisplay.cs b/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelectorBreadcrumbDisplay.cs index e91076498c..3fd1fa998f 100644 --- a/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelectorBreadcrumbDisplay.cs +++ b/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelectorBreadcrumbDisplay.cs @@ -80,7 +80,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 AddRangeInternal(new Drawable[] { - new Background + new OsuFileSelectorBackgroundLayer(0.5f) { Depth = 1 }, @@ -101,24 +101,6 @@ namespace osu.Game.Graphics.UserInterfaceV2 protected override SpriteText CreateSpriteText() => new OsuSpriteText().With(t => t.Font = OsuFont.Default.With(weight: FontWeight.SemiBold)); protected override IconUsage? Icon => Directory.Name.Contains(Path.DirectorySeparatorChar) ? FontAwesome.Solid.Database : null; - - internal partial class Background : CompositeDrawable - { - [BackgroundDependencyLoader] - private void load(OverlayColourProvider overlayColourProvider) - { - RelativeSizeAxes = Axes.Both; - - Masking = true; - CornerRadius = 5; - - InternalChild = new Box - { - Colour = overlayColourProvider.Background3, - RelativeSizeAxes = Axes.Both, - }; - } - } } } } diff --git a/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelectorDirectory.cs b/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelectorDirectory.cs index a36804658a..4240eb73a4 100644 --- a/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelectorDirectory.cs +++ b/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelectorDirectory.cs @@ -7,7 +7,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.UserInterface; using osu.Game.Graphics.Sprites; -using osu.Game.Graphics.UserInterface; namespace osu.Game.Graphics.UserInterfaceV2 { @@ -24,10 +23,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 Flow.AutoSizeAxes = Axes.X; Flow.Height = OsuDirectorySelector.ITEM_HEIGHT; - AddRangeInternal(new Drawable[] - { - new HoverClickSounds() - }); + AddInternal(new OsuFileSelectorBackgroundLayer()); Colour = colours.Orange1; } diff --git a/osu.Game/Graphics/UserInterfaceV2/OsuFileSelector.cs b/osu.Game/Graphics/UserInterfaceV2/OsuFileSelector.cs index 7ce5f63656..f54bfeebba 100644 --- a/osu.Game/Graphics/UserInterfaceV2/OsuFileSelector.cs +++ b/osu.Game/Graphics/UserInterfaceV2/OsuFileSelector.cs @@ -11,7 +11,6 @@ using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.UserInterface; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; -using osu.Game.Graphics.UserInterface; using osu.Game.Overlays; namespace osu.Game.Graphics.UserInterfaceV2 @@ -87,10 +86,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 Flow.AutoSizeAxes = Axes.X; Flow.Height = OsuDirectorySelector.ITEM_HEIGHT; - AddRangeInternal(new Drawable[] - { - new HoverClickSounds() - }); + AddInternal(new OsuFileSelectorBackgroundLayer()); Colour = colourProvider.Light3; } diff --git a/osu.Game/Graphics/UserInterfaceV2/OsuFileSelectorBackgroundLayer.cs b/osu.Game/Graphics/UserInterfaceV2/OsuFileSelectorBackgroundLayer.cs new file mode 100644 index 0000000000..ee5e7f014d --- /dev/null +++ b/osu.Game/Graphics/UserInterfaceV2/OsuFileSelectorBackgroundLayer.cs @@ -0,0 +1,59 @@ +// 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.Shapes; +using osu.Framework.Input.Events; +using osu.Game.Graphics.UserInterface; +using osu.Game.Overlays; + +namespace osu.Game.Graphics.UserInterfaceV2 +{ + internal partial class OsuFileSelectorBackgroundLayer : CompositeDrawable + { + private Box background = null!; + + private readonly float defaultAlpha; + + public OsuFileSelectorBackgroundLayer(float defaultAlpha = 0f) + { + Depth = float.MaxValue; + + this.defaultAlpha = defaultAlpha; + } + + [BackgroundDependencyLoader] + private void load(OverlayColourProvider overlayColourProvider) + { + RelativeSizeAxes = Axes.Both; + + Masking = true; + CornerRadius = 5; + + InternalChildren = new Drawable[] + { + new HoverClickSounds(), + background = new Box + { + Alpha = defaultAlpha, + Colour = overlayColourProvider.Background3, + RelativeSizeAxes = Axes.Both, + }, + }; + } + + protected override bool OnHover(HoverEvent e) + { + background.FadeTo(1, 200, Easing.OutQuint); + return base.OnHover(e); + } + + protected override void OnHoverLost(HoverLostEvent e) + { + base.OnHoverLost(e); + background.FadeTo(defaultAlpha, 500, Easing.OutQuint); + } + } +} From eacd9b9756583950a78b91affcccd92f74d2162c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 27 Sep 2024 13:57:05 +0900 Subject: [PATCH 2406/2493] Move dependent files to namespace --- .../BackgroundLayer.cs} | 6 +++--- .../HiddenFilesToggleCheckbox.cs} | 6 +++--- .../OsuDirectorySelectorBreadcrumbDisplay.cs | 4 ++-- .../{ => FileSelection}/OsuDirectorySelectorDirectory.cs | 4 ++-- .../OsuDirectorySelectorParentDirectory.cs | 2 +- osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelector.cs | 3 ++- osu.Game/Graphics/UserInterfaceV2/OsuFileSelector.cs | 5 +++-- 7 files changed, 16 insertions(+), 14 deletions(-) rename osu.Game/Graphics/UserInterfaceV2/{OsuFileSelectorBackgroundLayer.cs => FileSelection/BackgroundLayer.cs} (88%) rename osu.Game/Graphics/UserInterfaceV2/{OsuDirectorySelectorHiddenToggle.cs => FileSelection/HiddenFilesToggleCheckbox.cs} (88%) rename osu.Game/Graphics/UserInterfaceV2/{ => FileSelection}/OsuDirectorySelectorBreadcrumbDisplay.cs (97%) rename osu.Game/Graphics/UserInterfaceV2/{ => FileSelection}/OsuDirectorySelectorDirectory.cs (91%) rename osu.Game/Graphics/UserInterfaceV2/{ => FileSelection}/OsuDirectorySelectorParentDirectory.cs (92%) diff --git a/osu.Game/Graphics/UserInterfaceV2/OsuFileSelectorBackgroundLayer.cs b/osu.Game/Graphics/UserInterfaceV2/FileSelection/BackgroundLayer.cs similarity index 88% rename from osu.Game/Graphics/UserInterfaceV2/OsuFileSelectorBackgroundLayer.cs rename to osu.Game/Graphics/UserInterfaceV2/FileSelection/BackgroundLayer.cs index ee5e7f014d..cd3199c6f5 100644 --- a/osu.Game/Graphics/UserInterfaceV2/OsuFileSelectorBackgroundLayer.cs +++ b/osu.Game/Graphics/UserInterfaceV2/FileSelection/BackgroundLayer.cs @@ -9,15 +9,15 @@ using osu.Framework.Input.Events; using osu.Game.Graphics.UserInterface; using osu.Game.Overlays; -namespace osu.Game.Graphics.UserInterfaceV2 +namespace osu.Game.Graphics.UserInterfaceV2.FileSelection { - internal partial class OsuFileSelectorBackgroundLayer : CompositeDrawable + internal partial class BackgroundLayer : CompositeDrawable { private Box background = null!; private readonly float defaultAlpha; - public OsuFileSelectorBackgroundLayer(float defaultAlpha = 0f) + public BackgroundLayer(float defaultAlpha = 0f) { Depth = float.MaxValue; diff --git a/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelectorHiddenToggle.cs b/osu.Game/Graphics/UserInterfaceV2/FileSelection/HiddenFilesToggleCheckbox.cs similarity index 88% rename from osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelectorHiddenToggle.cs rename to osu.Game/Graphics/UserInterfaceV2/FileSelection/HiddenFilesToggleCheckbox.cs index 521ebebf91..07d84a0095 100644 --- a/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelectorHiddenToggle.cs +++ b/osu.Game/Graphics/UserInterfaceV2/FileSelection/HiddenFilesToggleCheckbox.cs @@ -8,11 +8,11 @@ using osu.Game.Overlays; using osuTK; using osuTK.Graphics; -namespace osu.Game.Graphics.UserInterfaceV2 +namespace osu.Game.Graphics.UserInterfaceV2.FileSelection { - internal partial class OsuDirectorySelectorHiddenToggle : OsuCheckbox + internal partial class HiddenFilesToggleCheckbox : OsuCheckbox { - public OsuDirectorySelectorHiddenToggle() + public HiddenFilesToggleCheckbox() { RelativeSizeAxes = Axes.None; AutoSizeAxes = Axes.None; diff --git a/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelectorBreadcrumbDisplay.cs b/osu.Game/Graphics/UserInterfaceV2/FileSelection/OsuDirectorySelectorBreadcrumbDisplay.cs similarity index 97% rename from osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelectorBreadcrumbDisplay.cs rename to osu.Game/Graphics/UserInterfaceV2/FileSelection/OsuDirectorySelectorBreadcrumbDisplay.cs index 3fd1fa998f..aeeda82bfb 100644 --- a/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelectorBreadcrumbDisplay.cs +++ b/osu.Game/Graphics/UserInterfaceV2/FileSelection/OsuDirectorySelectorBreadcrumbDisplay.cs @@ -13,7 +13,7 @@ using osu.Game.Graphics.UserInterface; using osu.Game.Overlays; using osuTK; -namespace osu.Game.Graphics.UserInterfaceV2 +namespace osu.Game.Graphics.UserInterfaceV2.FileSelection { internal partial class OsuDirectorySelectorBreadcrumbDisplay : DirectorySelectorBreadcrumbDisplay { @@ -80,7 +80,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 AddRangeInternal(new Drawable[] { - new OsuFileSelectorBackgroundLayer(0.5f) + new BackgroundLayer(0.5f) { Depth = 1 }, diff --git a/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelectorDirectory.cs b/osu.Game/Graphics/UserInterfaceV2/FileSelection/OsuDirectorySelectorDirectory.cs similarity index 91% rename from osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelectorDirectory.cs rename to osu.Game/Graphics/UserInterfaceV2/FileSelection/OsuDirectorySelectorDirectory.cs index 4240eb73a4..0da4e1929f 100644 --- a/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelectorDirectory.cs +++ b/osu.Game/Graphics/UserInterfaceV2/FileSelection/OsuDirectorySelectorDirectory.cs @@ -8,7 +8,7 @@ using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.UserInterface; using osu.Game.Graphics.Sprites; -namespace osu.Game.Graphics.UserInterfaceV2 +namespace osu.Game.Graphics.UserInterfaceV2.FileSelection { internal partial class OsuDirectorySelectorDirectory : DirectorySelectorDirectory { @@ -23,7 +23,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 Flow.AutoSizeAxes = Axes.X; Flow.Height = OsuDirectorySelector.ITEM_HEIGHT; - AddInternal(new OsuFileSelectorBackgroundLayer()); + AddInternal(new BackgroundLayer()); Colour = colours.Orange1; } diff --git a/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelectorParentDirectory.cs b/osu.Game/Graphics/UserInterfaceV2/FileSelection/OsuDirectorySelectorParentDirectory.cs similarity index 92% rename from osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelectorParentDirectory.cs rename to osu.Game/Graphics/UserInterfaceV2/FileSelection/OsuDirectorySelectorParentDirectory.cs index d274a0ecfe..e5e1e0b7f3 100644 --- a/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelectorParentDirectory.cs +++ b/osu.Game/Graphics/UserInterfaceV2/FileSelection/OsuDirectorySelectorParentDirectory.cs @@ -6,7 +6,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics.Sprites; using osu.Game.Overlays; -namespace osu.Game.Graphics.UserInterfaceV2 +namespace osu.Game.Graphics.UserInterfaceV2.FileSelection { internal partial class OsuDirectorySelectorParentDirectory : OsuDirectorySelectorDirectory { diff --git a/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelector.cs b/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelector.cs index 85599a5d45..65ffdcaa5b 100644 --- a/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelector.cs +++ b/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelector.cs @@ -8,6 +8,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.UserInterface; using osu.Game.Graphics.Containers; +using osu.Game.Graphics.UserInterfaceV2.FileSelection; using osu.Game.Overlays; namespace osu.Game.Graphics.UserInterfaceV2 @@ -57,7 +58,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 { RelativeSizeAxes = Axes.Both, }, - new OsuDirectorySelectorHiddenToggle + new HiddenFilesToggleCheckbox { Current = { BindTarget = ShowHiddenItems }, }, diff --git a/osu.Game/Graphics/UserInterfaceV2/OsuFileSelector.cs b/osu.Game/Graphics/UserInterfaceV2/OsuFileSelector.cs index f54bfeebba..c7b559d9ed 100644 --- a/osu.Game/Graphics/UserInterfaceV2/OsuFileSelector.cs +++ b/osu.Game/Graphics/UserInterfaceV2/OsuFileSelector.cs @@ -11,6 +11,7 @@ using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.UserInterface; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterfaceV2.FileSelection; using osu.Game.Overlays; namespace osu.Game.Graphics.UserInterfaceV2 @@ -58,7 +59,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 { RelativeSizeAxes = Axes.Both, }, - new OsuDirectorySelectorHiddenToggle + new HiddenFilesToggleCheckbox { Current = { BindTarget = ShowHiddenItems }, }, @@ -86,7 +87,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 Flow.AutoSizeAxes = Axes.X; Flow.Height = OsuDirectorySelector.ITEM_HEIGHT; - AddInternal(new OsuFileSelectorBackgroundLayer()); + AddInternal(new BackgroundLayer()); Colour = colourProvider.Light3; } From b2983e25629e407c3045a65bded33bebe43931cd Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 27 Sep 2024 14:21:16 +0900 Subject: [PATCH 2407/2493] Update shader preloader with missing shader usages --- osu.Game/Screens/Loader.cs | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Loader.cs b/osu.Game/Screens/Loader.cs index 57e3998646..f64ae196a0 100644 --- a/osu.Game/Screens/Loader.cs +++ b/osu.Game/Screens/Loader.cs @@ -118,13 +118,20 @@ namespace osu.Game.Screens { loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.TEXTURE)); loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.BLUR)); + loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.TEXTURE)); - loadTargets.Add(manager.Load(@"CursorTrail", FragmentShaderDescriptor.TEXTURE)); - - loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_2, "TriangleBorder")); - loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_2, "FastCircle")); + loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_2, @"TriangleBorder")); + loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_2, @"FastCircle")); + loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_2, @"CircularProgress")); + loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_2, @"ArgonBarPath")); + loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_2, @"ArgonBarPathBackground")); + loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_2, @"SaturationSelectorBackground")); + loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_2, @"HueSelectorBackground")); loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_3, FragmentShaderDescriptor.TEXTURE)); + + loadTargets.Add(manager.Load(@"CursorTrail", FragmentShaderDescriptor.TEXTURE)); + loadTargets.Add(manager.Load(@"LogoAnimation", @"LogoAnimation")); } protected virtual bool AllLoaded => loadTargets.All(s => s.IsLoaded); From 4205a21c0c595be177cd46b00f7e9b23d1361c69 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 27 Sep 2024 16:11:24 +0900 Subject: [PATCH 2408/2493] Add one more shader usage --- osu.Game/Screens/Loader.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Screens/Loader.cs b/osu.Game/Screens/Loader.cs index f64ae196a0..4a59b180f5 100644 --- a/osu.Game/Screens/Loader.cs +++ b/osu.Game/Screens/Loader.cs @@ -127,6 +127,8 @@ namespace osu.Game.Screens loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_2, @"ArgonBarPathBackground")); loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_2, @"SaturationSelectorBackground")); loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_2, @"HueSelectorBackground")); + // Ruleset local shader usage (should probably move somewhere else). + loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_2, @"SpinnerGlow")); loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_3, FragmentShaderDescriptor.TEXTURE)); From 5be63ee304e54852723cd4b043459b4aa733eb9a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 27 Sep 2024 16:16:17 +0900 Subject: [PATCH 2409/2493] Reorganise with ruleset shader separated out --- osu.Game/Screens/Loader.cs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/Loader.cs b/osu.Game/Screens/Loader.cs index 4a59b180f5..d71ee05b27 100644 --- a/osu.Game/Screens/Loader.cs +++ b/osu.Game/Screens/Loader.cs @@ -118,7 +118,7 @@ namespace osu.Game.Screens { loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.TEXTURE)); loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.BLUR)); - loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.TEXTURE)); + loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_3, FragmentShaderDescriptor.TEXTURE)); loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_2, @"TriangleBorder")); loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_2, @"FastCircle")); @@ -127,13 +127,11 @@ namespace osu.Game.Screens loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_2, @"ArgonBarPathBackground")); loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_2, @"SaturationSelectorBackground")); loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_2, @"HueSelectorBackground")); + loadTargets.Add(manager.Load(@"LogoAnimation", @"LogoAnimation")); + // Ruleset local shader usage (should probably move somewhere else). loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_2, @"SpinnerGlow")); - - loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_3, FragmentShaderDescriptor.TEXTURE)); - loadTargets.Add(manager.Load(@"CursorTrail", FragmentShaderDescriptor.TEXTURE)); - loadTargets.Add(manager.Load(@"LogoAnimation", @"LogoAnimation")); } protected virtual bool AllLoaded => loadTargets.All(s => s.IsLoaded); From 21796900e2eeed8e8b9d707b285eca5a4184f6fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 27 Sep 2024 09:26:08 +0200 Subject: [PATCH 2410/2493] Fix code quality naming issue --- osu.Game/Utils/GeometryUtils.cs | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/osu.Game/Utils/GeometryUtils.cs b/osu.Game/Utils/GeometryUtils.cs index e9e79deb49..eac86a9c02 100644 --- a/osu.Game/Utils/GeometryUtils.cs +++ b/osu.Game/Utils/GeometryUtils.cs @@ -309,21 +309,21 @@ namespace osu.Game.Utils { // Using Welzl's algorithm to find the minimum enclosing circle // https://www.geeksforgeeks.org/minimum-enclosing-circle-using-welzls-algorithm/ - List P = points.ToList(); + List p = points.ToList(); var stack = new Stack<(Vector2?, int)>(); var r = new List(3); (Vector2, float) d = (Vector2.Zero, 0); - stack.Push((null, P.Count)); + stack.Push((null, p.Count)); while (stack.Count > 0) { - // n represents the number of points in P that are not yet processed. - // p represents the point that was randomly picked to process. - (Vector2? p, int n) = stack.Pop(); + // `n` represents the number of points in P that are not yet processed. + // `point` represents the point that was randomly picked to process. + (Vector2? point, int n) = stack.Pop(); - if (!p.HasValue) + if (!point.HasValue) { // Base case when all points processed or |R| = 3 if (n == 0 || r.Count == 3) @@ -334,30 +334,30 @@ namespace osu.Game.Utils // Pick a random point randomly int idx = RNG.Next(n); - p = P[idx]; + point = p[idx]; // Put the picked point at the end of P since it's more efficient than // deleting from the middle of the list - (P[idx], P[n - 1]) = (P[n - 1], P[idx]); + (p[idx], p[n - 1]) = (p[n - 1], p[idx]); // Schedule processing of p after we get the MEC circle d from the set of points P - {p} - stack.Push((p, n)); + stack.Push((point, n)); // Get the MEC circle d from the set of points P - {p} stack.Push((null, n - 1)); } else { // If d contains p, return d - if (isInside(d, p.Value)) + if (isInside(d, point.Value)) continue; // Remove points from R that were added in a deeper recursion // |R| = |P| - |stack| - n - int removeCount = r.Count - (P.Count - stack.Count - n); + int removeCount = r.Count - (p.Count - stack.Count - n); r.RemoveRange(r.Count - removeCount, removeCount); // Otherwise, must be on the boundary of the MEC - r.Add(p.Value); + r.Add(point.Value); // Return the MEC for P - {p} and R U {p} stack.Push((null, n - 1)); } From cb51e12d1393dab1a9c00ec542889b64fa5d73a2 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 27 Sep 2024 16:24:51 +0900 Subject: [PATCH 2411/2493] Fix iOS CI build --- .github/workflows/ci.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4abd55e3f4..6fbb74dfba 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -135,5 +135,8 @@ jobs: - name: Install .NET Workloads run: dotnet workload install maui-ios + - name: Select Xcode 16 + run: sudo xcode-select -s /Applications/Xcode_16.app/Contents/Developer + - name: Build run: dotnet build -c Debug osu.iOS From 1dd6082aa9ea6c06647ddadfaddd1ae1720b9fba Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 27 Sep 2024 16:56:22 +0900 Subject: [PATCH 2412/2493] Rename method to be more appropriate --- .../HUD/JudgementCounter/JudgementCountController.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/JudgementCounter/JudgementCountController.cs b/osu.Game/Screens/Play/HUD/JudgementCounter/JudgementCountController.cs index 2562e26127..c00cb3487b 100644 --- a/osu.Game/Screens/Play/HUD/JudgementCounter/JudgementCountController.cs +++ b/osu.Game/Screens/Play/HUD/JudgementCounter/JudgementCountController.cs @@ -52,16 +52,16 @@ namespace osu.Game.Screens.Play.HUD.JudgementCounter { base.LoadComplete(); - scoreProcessor.OnResetFromReplayFrame += updateAllCounts; + scoreProcessor.OnResetFromReplayFrame += updateAllCountsFromReplayFrame; scoreProcessor.NewJudgement += judgement => updateCount(judgement, false); scoreProcessor.JudgementReverted += judgement => updateCount(judgement, true); } - private bool hasUpdatedCounts; + private bool hasUpdatedCountsFromReplayFrame; - private void updateAllCounts() + private void updateAllCountsFromReplayFrame() { - if (hasUpdatedCounts) + if (hasUpdatedCountsFromReplayFrame) return; foreach (var kvp in scoreProcessor.Statistics) @@ -72,7 +72,7 @@ namespace osu.Game.Screens.Play.HUD.JudgementCounter count.ResultCount.Value = kvp.Value; } - hasUpdatedCounts = true; + hasUpdatedCountsFromReplayFrame = true; } private void updateCount(JudgementResult judgement, bool revert) From 1c23fd31d74265710b9a7b2be28de0242e8b23aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 27 Sep 2024 10:27:54 +0200 Subject: [PATCH 2413/2493] Ensure `Slider.updateNestedPositions()` actually updates the position of all nesteds --- osu.Game.Rulesets.Osu/Objects/Slider.cs | 27 ++++++++++++++----- osu.Game.Rulesets.Osu/Objects/SliderRepeat.cs | 2 ++ osu.Game.Rulesets.Osu/Objects/SliderTick.cs | 1 + 3 files changed, 24 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs index 2b3bb18844..e484efb408 100644 --- a/osu.Game.Rulesets.Osu/Objects/Slider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs @@ -204,6 +204,7 @@ namespace osu.Game.Rulesets.Osu.Objects SpanStartTime = e.SpanStartTime, StartTime = e.Time, Position = Position + Path.PositionAt(e.PathProgress), + PathProgress = e.PathProgress, StackHeight = StackHeight, }); break; @@ -236,6 +237,7 @@ namespace osu.Game.Rulesets.Osu.Objects StartTime = StartTime + (e.SpanIndex + 1) * SpanDuration, Position = Position + Path.PositionAt(e.PathProgress), StackHeight = StackHeight, + PathProgress = e.PathProgress, }); break; } @@ -248,14 +250,27 @@ namespace osu.Game.Rulesets.Osu.Objects { endPositionCache.Invalidate(); - if (HeadCircle != null) - HeadCircle.Position = Position; + foreach (var nested in NestedHitObjects) + { + switch (nested) + { + case SliderHeadCircle headCircle: + headCircle.Position = Position; + break; - if (TailCircle != null) - TailCircle.Position = EndPosition; + case SliderTailCircle tailCircle: + tailCircle.Position = EndPosition; + break; - if (LastRepeat != null) - LastRepeat.Position = RepeatCount % 2 == 0 ? Position : Position + Path.PositionAt(1); + case SliderRepeat repeat: + repeat.Position = Position + Path.PositionAt(repeat.PathProgress); + break; + + case SliderTick tick: + tick.Position = Position + Path.PositionAt(tick.PathProgress); + break; + } + } } protected void UpdateNestedSamples() diff --git a/osu.Game.Rulesets.Osu/Objects/SliderRepeat.cs b/osu.Game.Rulesets.Osu/Objects/SliderRepeat.cs index e95cfd369d..1bbd1e8070 100644 --- a/osu.Game.Rulesets.Osu/Objects/SliderRepeat.cs +++ b/osu.Game.Rulesets.Osu/Objects/SliderRepeat.cs @@ -5,6 +5,8 @@ namespace osu.Game.Rulesets.Osu.Objects { public class SliderRepeat : SliderEndCircle { + public double PathProgress { get; set; } + public SliderRepeat(Slider slider) : base(slider) { diff --git a/osu.Game.Rulesets.Osu/Objects/SliderTick.cs b/osu.Game.Rulesets.Osu/Objects/SliderTick.cs index 74ec4d6eb3..219c2be00b 100644 --- a/osu.Game.Rulesets.Osu/Objects/SliderTick.cs +++ b/osu.Game.Rulesets.Osu/Objects/SliderTick.cs @@ -13,6 +13,7 @@ namespace osu.Game.Rulesets.Osu.Objects { public int SpanIndex { get; set; } public double SpanStartTime { get; set; } + public double PathProgress { get; set; } protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty) { From 89f47c46544e7f1928f8b281230fbefd74904619 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 27 Sep 2024 10:28:32 +0200 Subject: [PATCH 2414/2493] Fix random mod needlessly trying to fix nested object positions on its own --- .../OsuHitObjectGenerationUtils_Reposition.cs | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs b/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs index a9ae313a31..f7a0543739 100644 --- a/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs +++ b/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs @@ -232,8 +232,6 @@ namespace osu.Game.Rulesets.Osu.Utils slider.Position = workingObject.PositionModified = new Vector2(newX, newY); workingObject.EndPositionModified = slider.EndPosition; - shiftNestedObjects(slider, workingObject.PositionModified - workingObject.PositionOriginal); - return workingObject.PositionModified - previousPosition; } @@ -307,22 +305,6 @@ namespace osu.Game.Rulesets.Osu.Utils return new RectangleF(left, top, right - left, bottom - top); } - /// - /// Shifts all nested s and s by the specified shift. - /// - /// whose nested s and s should be shifted - /// The the 's nested s and s should be shifted by - private static void shiftNestedObjects(Slider slider, Vector2 shift) - { - foreach (var hitObject in slider.NestedHitObjects.Where(o => o is SliderTick || o is SliderRepeat)) - { - if (!(hitObject is OsuHitObject osuHitObject)) - continue; - - osuHitObject.Position += shift; - } - } - /// /// Clamp a position to playfield, keeping a specified distance from the edges. /// From 92ee86e3dd210e2b05877e2477ee27d54a086be3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 27 Sep 2024 17:40:06 +0900 Subject: [PATCH 2415/2493] 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 c7ce707562..6b42258b49 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -10,7 +10,7 @@ true - + diff --git a/osu.iOS.props b/osu.iOS.props index bb20125282..8acd1deff1 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -17,6 +17,6 @@ -all - + From 371cee1617854c76f8cfd98427aae75bdf460ac1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 27 Sep 2024 17:41:27 +0900 Subject: [PATCH 2416/2493] Consume framework change to avoid weird unbind flow --- osu.Game/OsuGame.cs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 1af86b2d83..44ba78762a 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -446,11 +446,7 @@ namespace osu.Game case LinkAction.SearchBeatmapSet: if (link.Argument is LocalisableString localisable) - { - var localised = Localisation.GetLocalisedBindableString(localisable); - SearchBeatmapSet(localised.Value); - localised.UnbindAll(); - } + SearchBeatmapSet(Localisation.GetLocalisedString(localisable)); else SearchBeatmapSet(argString); From e7c44512066454c21726a4c24b6e93233571e25b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 27 Sep 2024 18:20:16 +0900 Subject: [PATCH 2417/2493] Reduce brightness of hover effect --- osu.Game/Overlays/Mods/ModCustomisationHeader.cs | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModCustomisationHeader.cs b/osu.Game/Overlays/Mods/ModCustomisationHeader.cs index 1d40fb3f5c..54fbd37dbe 100644 --- a/osu.Game/Overlays/Mods/ModCustomisationHeader.cs +++ b/osu.Game/Overlays/Mods/ModCustomisationHeader.cs @@ -29,7 +29,7 @@ namespace osu.Game.Overlays.Mods [Resolved] private OverlayColourProvider colourProvider { get; set; } = null!; - public readonly Bindable ExpandedState = new Bindable(ModCustomisationPanelState.Collapsed); + public readonly Bindable ExpandedState = new Bindable(); private readonly ModCustomisationPanel panel; @@ -54,7 +54,7 @@ namespace osu.Game.Overlays.Mods hoverBackground = new Box { RelativeSizeAxes = Axes.Both, - Colour = OsuColour.Gray(80).Opacity(180), + Colour = OsuColour.Gray(50), Blending = BlendingParameters.Additive, Alpha = 0, }, @@ -134,16 +134,13 @@ namespace osu.Game.Overlays.Mods if (panel.ExpandedState.Value == ModCustomisationPanelState.Collapsed) panel.ExpandedState.Value = ModCustomisationPanelState.Expanded; - hoverBackground.FadeIn(200); - + hoverBackground.FadeTo(0.4f, 200, Easing.OutQuint); return base.OnHover(e); } protected override void OnHoverLost(HoverLostEvent e) { - if (Enabled.Value) - hoverBackground.FadeOut(200); - + hoverBackground.FadeOut(200, Easing.OutQuint); base.OnHoverLost(e); } } From eb725ec1fb19c2348c9a8c8442644ef4f8cc33ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 27 Sep 2024 12:13:11 +0200 Subject: [PATCH 2418/2493] Nudge test coverage to also cover discovered fail case --- .../Visual/Editing/TestSceneComposerSelection.cs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneComposerSelection.cs b/osu.Game.Tests/Visual/Editing/TestSceneComposerSelection.cs index 765d7ee21e..13d5a7e3b2 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneComposerSelection.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneComposerSelection.cs @@ -244,11 +244,8 @@ namespace osu.Game.Tests.Visual.Editing InputManager.PressKey(Key.ControlLeft); }); AddStep("drag to left corner", () => InputManager.MoveMouseTo(blueprintContainer.ScreenSpaceDrawQuad.TopRight + new Vector2(5, -5))); - AddStep("end dragging", () => - { - InputManager.ReleaseButton(MouseButton.Left); - InputManager.ReleaseKey(Key.ControlLeft); - }); + AddStep("end dragging", () => InputManager.ReleaseButton(MouseButton.Left)); + AddStep("release control", () => InputManager.ReleaseKey(Key.ControlLeft)); AddAssert("4 hitobjects selected", () => EditorBeatmap.SelectedHitObjects, () => Has.Count.EqualTo(4)); From d60733175563068265fb76baf649646e6843c726 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 27 Sep 2024 12:15:08 +0200 Subject: [PATCH 2419/2493] Fix control-drag selection expansion deselecting object if control is released over one of the blueprints --- .../Screens/Edit/Compose/Components/BlueprintContainer.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs index 9776e64855..30c1258f93 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs @@ -432,7 +432,7 @@ namespace osu.Game.Screens.Edit.Compose.Components private bool endClickSelection(MouseButtonEvent e) { // If already handled a selection, double-click, or drag, we don't want to perform a mouse up / click action. - if (clickSelectionHandled || doubleClickHandled || isDraggingBlueprint) return true; + if (clickSelectionHandled || doubleClickHandled || isDraggingBlueprint || wasDragStarted) return true; if (e.Button != MouseButton.Left) return false; @@ -448,7 +448,7 @@ namespace osu.Game.Screens.Edit.Compose.Components return false; } - if (!wasDragStarted && selectedBlueprintAlreadySelectedOnMouseDown && SelectedItems.Count == 1) + if (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 f6c5f975ee8e1389f2c8f708b99cdacaabf056e1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 27 Sep 2024 20:08:26 +0900 Subject: [PATCH 2420/2493] Add failing test showing url decoding is not being performed --- .../Visual/Editing/TestSceneOpenEditorTimestamp.cs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneOpenEditorTimestamp.cs b/osu.Game.Tests/Visual/Editing/TestSceneOpenEditorTimestamp.cs index 971eb223eb..955ded97af 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneOpenEditorTimestamp.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneOpenEditorTimestamp.cs @@ -100,6 +100,20 @@ namespace osu.Game.Tests.Visual.Editing assertOnScreenAt(EditorScreenMode.Compose, 0); } + [Test] + public void TestUrlDecodingOfArgs() + { + setUpEditor(new OsuRuleset().RulesetInfo); + AddAssert("is osu! ruleset", () => editorBeatmap.BeatmapInfo.Ruleset.Equals(new OsuRuleset().RulesetInfo)); + + AddStep("jump to encoded link", () => Game.HandleLink("osu://edit/00:14:142%20(1)")); + + AddUntilStep("wait for seek", () => editorClock.SeekingOrStopped.Value); + + AddAssert("time is correct", () => editorClock.CurrentTime, () => Is.EqualTo(14_142)); + AddAssert("selected object is correct", () => editorBeatmap.SelectedHitObjects.Single().StartTime, () => Is.EqualTo(14_142)); + } + private void addStepClickLink(string timestamp, string step = "", bool waitForSeek = true) { AddStep($"{step} {timestamp}", () => From 9647a1be7d928fec2cd76b48533ee5180e529851 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 27 Sep 2024 20:08:38 +0900 Subject: [PATCH 2421/2493] Ensure editor timestamp args are URL decoded --- osu.Game/Online/Chat/MessageFormatter.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Online/Chat/MessageFormatter.cs b/osu.Game/Online/Chat/MessageFormatter.cs index 77454c4775..f354eea027 100644 --- a/osu.Game/Online/Chat/MessageFormatter.cs +++ b/osu.Game/Online/Chat/MessageFormatter.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; +using System.Web; using osu.Game.Online.API.Requests.Responses; using osu.Game.Rulesets.Edit; @@ -234,7 +235,7 @@ namespace osu.Game.Online.Chat return new LinkDetails(LinkAction.External, url); } - return new LinkDetails(linkType, args[2]); + return new LinkDetails(linkType, HttpUtility.UrlDecode(args[2])); case "osump": return new LinkDetails(LinkAction.JoinMultiplayerMatch, args[1]); From 132931f6054d24b116798937ce3f087299523d7b Mon Sep 17 00:00:00 2001 From: Fina <75299710+Finadoggie@users.noreply.github.com> Date: Fri, 27 Sep 2024 14:04:48 -0700 Subject: [PATCH 2422/2493] Create variable to check if using slideracc --- osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index a4ebcd15a4..946a0535db 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -15,6 +15,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty { public const double PERFORMANCE_BASE_MULTIPLIER = 1.14; // This is being adjusted to keep the final pp value scaled around what it used to be when changing things. + private bool usingClassicSliderAccuracy; + private double accuracy; private int scoreMaxCombo; private int countGreat; @@ -33,6 +35,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty { var osuAttributes = (OsuDifficultyAttributes)attributes; + usingClassicSliderAccuracy = score.Mods.OfType().All(m => m.NoSliderHeadAccuracy.Value); accuracy = score.Accuracy; scoreMaxCombo = score.MaxCombo; countGreat = score.Statistics.GetValueOrDefault(HitResult.Great); @@ -192,7 +195,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty // This percentage only considers HitCircles of any value - in this part of the calculation we focus on hitting the timing hit window. double betterAccuracyPercentage; int amountHitObjectsWithAccuracy = attributes.HitCircleCount; - if (score.Mods.OfType().All(m => !m.NoSliderHeadAccuracy.Value)) + if (!usingClassicSliderAccuracy) amountHitObjectsWithAccuracy += attributes.SliderCount; if (amountHitObjectsWithAccuracy > 0) From 7a849c7e206e599d00735823b10407febf4d1aa3 Mon Sep 17 00:00:00 2001 From: Finadoggie <75299710+Finadoggie@users.noreply.github.com> Date: Fri, 27 Sep 2024 14:11:12 -0700 Subject: [PATCH 2423/2493] Fix formatting --- osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index 946a0535db..148d2a8130 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -36,6 +36,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty var osuAttributes = (OsuDifficultyAttributes)attributes; usingClassicSliderAccuracy = score.Mods.OfType().All(m => m.NoSliderHeadAccuracy.Value); + accuracy = score.Accuracy; scoreMaxCombo = score.MaxCombo; countGreat = score.Statistics.GetValueOrDefault(HitResult.Great); From 9b1ae2fe26f53b6f0c529a6f191a1ba55a5b4a9f Mon Sep 17 00:00:00 2001 From: Finadoggie <75299710+Finadoggie@users.noreply.github.com> Date: Fri, 27 Sep 2024 14:21:15 -0700 Subject: [PATCH 2424/2493] fix the code such that it actually works when testing it --- osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index 148d2a8130..dc2113ed40 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -35,7 +35,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty { var osuAttributes = (OsuDifficultyAttributes)attributes; - usingClassicSliderAccuracy = score.Mods.OfType().All(m => m.NoSliderHeadAccuracy.Value); + usingClassicSliderAccuracy = !(score.Mods.OfType().All(m => !m.NoSliderHeadAccuracy.Value)); accuracy = score.Accuracy; scoreMaxCombo = score.MaxCombo; From f473f4398c90e477686b48307ae9f9ec26e3a906 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Sat, 28 Sep 2024 22:37:16 +0300 Subject: [PATCH 2425/2493] Fix text in FormFileSelector bleeding through the border --- osu.Game/Graphics/UserInterfaceV2/FormFileSelector.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Graphics/UserInterfaceV2/FormFileSelector.cs b/osu.Game/Graphics/UserInterfaceV2/FormFileSelector.cs index 55cc026d7c..42bf9c7b9f 100644 --- a/osu.Game/Graphics/UserInterfaceV2/FormFileSelector.cs +++ b/osu.Game/Graphics/UserInterfaceV2/FormFileSelector.cs @@ -244,6 +244,9 @@ namespace osu.Game.Graphics.UserInterfaceV2 Child = new Container { Size = new Vector2(600, 400), + // simplest solution to avoid underlying text to bleed through the bottom border + // https://github.com/ppy/osu/pull/30005#issuecomment-2378884430 + Padding = new MarginPadding { Bottom = 1 }, Child = new OsuFileSelector(chooserPath, handledExtensions) { RelativeSizeAxes = Axes.Both, From 3fac9baa9f97e1918d3bfb3760bf3c157be2fb86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 30 Sep 2024 08:38:11 +0200 Subject: [PATCH 2426/2493] Add test steps demonstrating failure case --- .../Visual/SongSelect/TestScenePlaySongSelect.cs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs index 6b8fa94336..aae0648157 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs @@ -175,6 +175,20 @@ namespace osu.Game.Tests.Visual.SongSelect increaseModSpeed(); AddAssert("adaptive speed still active", () => songSelect!.Mods.Value.First() is ModAdaptiveSpeed); + OsuModDoubleTime dtWithAdjustPitch = new OsuModDoubleTime + { + SpeedChange = { Value = 1.05 }, + AdjustPitch = { Value = true }, + }; + changeMods(dtWithAdjustPitch); + + decreaseModSpeed(); + AddAssert("no mods selected", () => songSelect!.Mods.Value.Count == 0); + + decreaseModSpeed(); + AddAssert("half time activated at 0.95x", () => songSelect!.Mods.Value.OfType().Single().SpeedChange.Value, () => Is.EqualTo(0.95).Within(0.005)); + AddAssert("half time has adjust pitch active", () => songSelect!.Mods.Value.OfType().Single().AdjustPitch.Value, () => Is.True); + void increaseModSpeed() => AddStep("increase mod speed", () => { InputManager.PressKey(Key.ControlLeft); From 23b8354af4b5564b94f4d17282f517f71f8db398 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 30 Sep 2024 08:46:45 +0200 Subject: [PATCH 2427/2493] Add more test steps demonstrating another failure case --- .../Visual/SongSelect/TestScenePlaySongSelect.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs index aae0648157..3a95aca6b9 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs @@ -189,6 +189,15 @@ namespace osu.Game.Tests.Visual.SongSelect AddAssert("half time activated at 0.95x", () => songSelect!.Mods.Value.OfType().Single().SpeedChange.Value, () => Is.EqualTo(0.95).Within(0.005)); AddAssert("half time has adjust pitch active", () => songSelect!.Mods.Value.OfType().Single().AdjustPitch.Value, () => Is.True); + AddStep("turn off adjust pitch", () => songSelect!.Mods.Value.OfType().Single().AdjustPitch.Value = false); + + increaseModSpeed(); + AddAssert("no mods selected", () => songSelect!.Mods.Value.Count == 0); + + increaseModSpeed(); + AddAssert("double time activated at 1.05x", () => songSelect!.Mods.Value.OfType().Single().SpeedChange.Value, () => Is.EqualTo(1.05).Within(0.005)); + AddAssert("double time has adjust pitch inactive", () => songSelect!.Mods.Value.OfType().Single().AdjustPitch.Value, () => Is.False); + void increaseModSpeed() => AddStep("increase mod speed", () => { InputManager.PressKey(Key.ControlLeft); From 5e5bb49cd8d8726223fca0eacd531fc797fd4c94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 30 Sep 2024 08:47:02 +0200 Subject: [PATCH 2428/2493] Fix rate change hotkeys sometimes losing track of adjust pitch setting Fixes https://osu.ppy.sh/community/forums/topics/1983327. The cause of the bug is a bit convoluted, and stems from the fact that the mod select overlay controls all of the game-global mod instances if present. `ModSpeedHotkeyHandler` would store the last spotted instance of a rate adjust mod - which in this case is a problem, because on deselection of a mod, the mod select overlay resets its settings to defaults: https://github.com/ppy/osu/blob/a258059d4338b999b8e065e48b952d14a6d14fb8/osu.Game/Overlays/Mods/ModSelectOverlay.cs#L424-L425 A way to defend against this is a clone, but this reveals another issue, in that the existing code was *relying* on the reference to the mod remaining the same in any other case, to read the latest valid settings of the mod. This basically only mattered in the edge case wherein Double Time would swap places with Half Time and vice versa (think [0.95,1.05] range). Therefore, track mod settings too explicitly to ensure that the stored clone is as up-to-date as possible. --- osu.Game/Screens/Select/ModSpeedHotkeyHandler.cs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Select/ModSpeedHotkeyHandler.cs b/osu.Game/Screens/Select/ModSpeedHotkeyHandler.cs index af64002bcf..c4cd44705e 100644 --- a/osu.Game/Screens/Select/ModSpeedHotkeyHandler.cs +++ b/osu.Game/Screens/Select/ModSpeedHotkeyHandler.cs @@ -27,6 +27,7 @@ namespace osu.Game.Screens.Select private OnScreenDisplay? onScreenDisplay { get; set; } private ModRateAdjust? lastActiveRateAdjustMod; + private ModSettingChangeTracker? settingChangeTracker; protected override void LoadComplete() { @@ -34,10 +35,19 @@ namespace osu.Game.Screens.Select selectedMods.BindValueChanged(val => { - lastActiveRateAdjustMod = val.NewValue.OfType().SingleOrDefault() ?? lastActiveRateAdjustMod; + storeLastActiveRateAdjustMod(); + + settingChangeTracker?.Dispose(); + settingChangeTracker = new ModSettingChangeTracker(val.NewValue); + settingChangeTracker.SettingChanged += _ => storeLastActiveRateAdjustMod(); }, true); } + private void storeLastActiveRateAdjustMod() + { + lastActiveRateAdjustMod = (ModRateAdjust?)selectedMods.Value.OfType().SingleOrDefault()?.DeepClone() ?? lastActiveRateAdjustMod; + } + public bool ChangeSpeed(double delta, IEnumerable availableMods) { double targetSpeed = (selectedMods.Value.OfType().SingleOrDefault()?.SpeedChange.Value ?? 1) + delta; From 31d1ba494938201f6f9fd85ef75b52d576ac2ab8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 30 Sep 2024 09:30:06 +0200 Subject: [PATCH 2429/2493] Remove unused member --- .../Utils/OsuHitObjectGenerationUtils_Reposition.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs b/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs index f7a0543739..7073abbc89 100644 --- a/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs +++ b/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs @@ -413,7 +413,6 @@ namespace osu.Game.Rulesets.Osu.Utils private class WorkingObject { public float RotationOriginal { get; } - public Vector2 PositionOriginal { get; } public Vector2 PositionModified { get; set; } public Vector2 EndPositionModified { get; set; } @@ -424,7 +423,7 @@ namespace osu.Game.Rulesets.Osu.Utils { PositionInfo = positionInfo; RotationOriginal = HitObject is Slider slider ? getSliderRotation(slider) : 0; - PositionModified = PositionOriginal = HitObject.Position; + PositionModified = HitObject.Position; EndPositionModified = HitObject.EndPosition; } } From e91c8fb4bd0a74b9530b37a0c68fa94f5d478ce1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 30 Sep 2024 11:02:00 +0200 Subject: [PATCH 2430/2493] Properly disable comment box on beatmaps that cannot be commented on Closes https://github.com/ppy/osu/issues/30052. Compare: - https://github.com/ppy/osu-web/blob/83816dbe24ad2927273cba968f2fcd2694a121a9/resources/js/components/comment-editor.tsx#L54-L60 - https://github.com/ppy/osu-web/blob/83816dbe24ad2927273cba968f2fcd2694a121a9/resources/js/components/comment-editor.tsx#L47-L52 --- .../UserInterface/TestSceneCommentEditor.cs | 34 ++++++++++++-- .../API/Requests/Responses/CommentableMeta.cs | 9 ++++ osu.Game/Overlays/Comments/CommentEditor.cs | 47 +++++++++++++++---- .../Overlays/Comments/CommentsContainer.cs | 8 ++-- osu.Game/Overlays/Comments/DrawableComment.cs | 2 +- .../Overlays/Comments/ReplyCommentEditor.cs | 7 +-- 6 files changed, 89 insertions(+), 18 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneCommentEditor.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneCommentEditor.cs index e1d40882be..ac6ca218c4 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneCommentEditor.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneCommentEditor.cs @@ -12,6 +12,7 @@ using osu.Framework.Localisation; using osu.Framework.Testing; using osu.Game.Graphics.UserInterface; using osu.Game.Online.API; +using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays; using osu.Game.Overlays.Comments; using osuTK; @@ -133,6 +134,34 @@ namespace osu.Game.Tests.Visual.UserInterface assertLoggedInState(); } + [Test] + public void TestCommentsDisabled() + { + AddStep("no reason for disable", () => commentEditor.CommentableMeta.Value = new CommentableMeta + { + CurrentUserAttributes = new CommentableMeta.CommentableCurrentUserAttributes(), + }); + AddAssert("textbox enabled", () => commentEditor.ChildrenOfType().Single().ReadOnly, () => Is.False); + + AddStep("specific reason for disable", () => commentEditor.CommentableMeta.Value = new CommentableMeta + { + CurrentUserAttributes = new CommentableMeta.CommentableCurrentUserAttributes + { + CanNewCommentReason = "This comment section is disabled. For reasons.", + } + }); + AddAssert("textbox disabled", () => commentEditor.ChildrenOfType().Single().ReadOnly, () => Is.True); + + AddStep("entire commentable meta missing", () => commentEditor.CommentableMeta.Value = null); + AddAssert("textbox enabled", () => commentEditor.ChildrenOfType().Single().ReadOnly, () => Is.False); + + AddStep("current user attributes missing", () => commentEditor.CommentableMeta.Value = new CommentableMeta + { + CurrentUserAttributes = null, + }); + AddAssert("textbox enabled", () => commentEditor.ChildrenOfType().Single().ReadOnly, () => Is.True); + } + [Test] public void TestCancelAction() { @@ -167,8 +196,7 @@ namespace osu.Game.Tests.Visual.UserInterface protected override LocalisableString GetButtonText(bool isLoggedIn) => isLoggedIn ? @"Commit" : "You're logged out!"; - protected override LocalisableString GetPlaceholderText(bool isLoggedIn) => - isLoggedIn ? @"This text box is empty" : "Still empty, but now you can't type in it."; + protected override LocalisableString GetPlaceholderText() => @"This text box is empty"; } private partial class TestCancellableCommentEditor : CancellableCommentEditor @@ -189,7 +217,7 @@ namespace osu.Game.Tests.Visual.UserInterface } protected override LocalisableString GetButtonText(bool isLoggedIn) => @"Save"; - protected override LocalisableString GetPlaceholderText(bool isLoggedIn) => @"Multiline textboxes soon"; + protected override LocalisableString GetPlaceholderText() => @"Multiline textboxes soon"; } } } diff --git a/osu.Game/Online/API/Requests/Responses/CommentableMeta.cs b/osu.Game/Online/API/Requests/Responses/CommentableMeta.cs index 1084f1c900..4b4595fef6 100644 --- a/osu.Game/Online/API/Requests/Responses/CommentableMeta.cs +++ b/osu.Game/Online/API/Requests/Responses/CommentableMeta.cs @@ -24,5 +24,14 @@ namespace osu.Game.Online.API.Requests.Responses [JsonProperty("url")] public string Url { get; set; } = string.Empty; + + [JsonProperty("current_user_attributes")] + public CommentableCurrentUserAttributes? CurrentUserAttributes { get; set; } + + public struct CommentableCurrentUserAttributes + { + [JsonProperty("can_new_comment_reason")] + public string? CanNewCommentReason { get; set; } + } } } diff --git a/osu.Game/Overlays/Comments/CommentEditor.cs b/osu.Game/Overlays/Comments/CommentEditor.cs index 02bcbb9d05..b75e5aa8d8 100644 --- a/osu.Game/Overlays/Comments/CommentEditor.cs +++ b/osu.Game/Overlays/Comments/CommentEditor.cs @@ -14,6 +14,8 @@ using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Online.API; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Resources.Localisation.Web; using osuTK; using osuTK.Graphics; @@ -21,6 +23,8 @@ namespace osu.Game.Overlays.Comments { public abstract partial class CommentEditor : CompositeDrawable { + public Bindable CommentableMeta { get; set; } = new Bindable(); + private const int side_padding = 8; protected abstract LocalisableString FooterText { get; } @@ -53,8 +57,7 @@ namespace osu.Game.Overlays.Comments /// /// Returns the placeholder text for the comment box. /// - /// Whether the current user is logged in. - protected abstract LocalisableString GetPlaceholderText(bool isLoggedIn); + protected abstract LocalisableString GetPlaceholderText(); protected bool ShowLoadingSpinner { @@ -168,7 +171,8 @@ namespace osu.Game.Overlays.Comments { base.LoadComplete(); Current.BindValueChanged(_ => updateCommitButtonState(), true); - apiState.BindValueChanged(updateStateForLoggedIn, true); + apiState.BindValueChanged(_ => updateEnabledState()); + CommentableMeta.BindValueChanged(_ => updateEnabledState(), true); } protected abstract void OnCommit(string text); @@ -176,16 +180,25 @@ namespace osu.Game.Overlays.Comments private void updateCommitButtonState() => commitButton.Enabled.Value = loadingSpinner.State.Value == Visibility.Hidden && !string.IsNullOrEmpty(Current.Value); - private void updateStateForLoggedIn(ValueChangedEvent state) => Schedule(() => + private void updateEnabledState() => Schedule(() => { - bool isAvailable = state.NewValue > APIState.Offline; + bool isOnline = apiState.Value > APIState.Offline; + var canNewCommentReason = CommentEditor.canNewCommentReason(CommentableMeta.Value); + bool commentsDisabled = canNewCommentReason != null; + bool canComment = isOnline && !commentsDisabled; - TextBox.PlaceholderText = GetPlaceholderText(isAvailable); - TextBox.ReadOnly = !isAvailable; + if (!isOnline) + TextBox.PlaceholderText = AuthorizationStrings.RequireLogin; + else if (canNewCommentReason != null) + TextBox.PlaceholderText = canNewCommentReason.Value; + else + TextBox.PlaceholderText = GetPlaceholderText(); + TextBox.ReadOnly = !canComment; - if (isAvailable) + if (isOnline) { commitButton.Show(); + commitButton.Enabled.Value = !commentsDisabled; logInButton.Hide(); } else @@ -195,6 +208,24 @@ namespace osu.Game.Overlays.Comments } }); + // https://github.com/ppy/osu-web/blob/83816dbe24ad2927273cba968f2fcd2694a121a9/resources/js/components/comment-editor.tsx#L54-L60 + // careful here, logic is VERY finicky. + private static LocalisableString? canNewCommentReason(CommentableMeta? meta) + { + if (meta == null) + return null; + + if (meta.CurrentUserAttributes != null) + { + if (meta.CurrentUserAttributes.Value.CanNewCommentReason is string reason) + return reason; + + return null; + } + + return AuthorizationStrings.CommentStoreDisabled; + } + private partial class EditorTextBox : OsuTextBox { protected override float LeftRightPadding => side_padding; diff --git a/osu.Game/Overlays/Comments/CommentsContainer.cs b/osu.Game/Overlays/Comments/CommentsContainer.cs index 2e5f13aa99..921c1682f5 100644 --- a/osu.Game/Overlays/Comments/CommentsContainer.cs +++ b/osu.Game/Overlays/Comments/CommentsContainer.cs @@ -20,6 +20,7 @@ using System.Collections.Generic; using JetBrains.Annotations; using osu.Framework.Localisation; using osu.Framework.Logging; +using osu.Game.Extensions; using osu.Game.Graphics.Sprites; using osu.Game.Resources.Localisation.Web; using osu.Game.Users.Drawables; @@ -49,6 +50,7 @@ namespace osu.Game.Overlays.Comments private int currentPage; private FillFlowContainer pinnedContent; + private NewCommentEditor newCommentEditor; private FillFlowContainer content; private DeletedCommentsCounter deletedCommentsCounter; private CommentsShowMoreButton moreButton; @@ -114,7 +116,7 @@ namespace osu.Game.Overlays.Comments Padding = new MarginPadding { Left = 60 }, RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Child = new NewCommentEditor + Child = newCommentEditor = new NewCommentEditor { OnPost = prependPostedComments } @@ -242,6 +244,7 @@ namespace osu.Game.Overlays.Comments protected void OnSuccess(CommentBundle response) { commentCounter.Current.Value = response.Total; + newCommentEditor.CommentableMeta.Value = response.CommentableMeta.SingleOrDefault(m => m.Id == id.Value && m.Type == type.Value.ToString().ToSnakeCase().ToLowerInvariant()); if (!response.Comments.Any()) { @@ -413,8 +416,7 @@ namespace osu.Game.Overlays.Comments protected override LocalisableString GetButtonText(bool isLoggedIn) => isLoggedIn ? CommonStrings.ButtonsPost : CommentsStrings.GuestButtonNew; - protected override LocalisableString GetPlaceholderText(bool isLoggedIn) => - isLoggedIn ? CommentsStrings.PlaceholderNew : AuthorizationStrings.RequireLogin; + protected override LocalisableString GetPlaceholderText() => CommentsStrings.PlaceholderNew; protected override void OnCommit(string text) { diff --git a/osu.Game/Overlays/Comments/DrawableComment.cs b/osu.Game/Overlays/Comments/DrawableComment.cs index 296f90872e..d664a44be9 100644 --- a/osu.Game/Overlays/Comments/DrawableComment.cs +++ b/osu.Game/Overlays/Comments/DrawableComment.cs @@ -428,7 +428,7 @@ namespace osu.Game.Overlays.Comments if (replyEditorContainer.Count == 0) { replyEditorContainer.Show(); - replyEditorContainer.Add(new ReplyCommentEditor(Comment) + replyEditorContainer.Add(new ReplyCommentEditor(Comment, Meta) { OnPost = comments => { diff --git a/osu.Game/Overlays/Comments/ReplyCommentEditor.cs b/osu.Game/Overlays/Comments/ReplyCommentEditor.cs index d5ae4f92ab..8350887ec0 100644 --- a/osu.Game/Overlays/Comments/ReplyCommentEditor.cs +++ b/osu.Game/Overlays/Comments/ReplyCommentEditor.cs @@ -2,6 +2,7 @@ // 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.Localisation; @@ -26,12 +27,12 @@ namespace osu.Game.Overlays.Comments protected override LocalisableString GetButtonText(bool isLoggedIn) => isLoggedIn ? CommonStrings.ButtonsReply : CommentsStrings.GuestButtonReply; - protected override LocalisableString GetPlaceholderText(bool isLoggedIn) => - isLoggedIn ? CommentsStrings.PlaceholderReply : AuthorizationStrings.RequireLogin; + protected override LocalisableString GetPlaceholderText() => CommentsStrings.PlaceholderReply; - public ReplyCommentEditor(Comment parent) + public ReplyCommentEditor(Comment parent, IEnumerable meta) { parentComment = parent; + CommentableMeta.Value = meta.SingleOrDefault(m => m.Id == parent.CommentableId && m.Type == parent.CommentableType); } protected override void LoadComplete() From 4723efaf41b56f86ed4817ff81aa873938074bd8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 30 Sep 2024 12:52:51 +0200 Subject: [PATCH 2431/2493] Add failing test coverage for incorrect distance snapping --- ...tSceneHitObjectComposerDistanceSnapping.cs | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs b/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs index cf8c3c6ef1..700aafb62d 100644 --- a/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs +++ b/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs @@ -227,6 +227,42 @@ namespace osu.Game.Tests.Editing assertSnappedDistance(400, 400); } + [Test] + public void TestUnsnappedObject() + { + var slider = new Slider + { + StartTime = 0, + Path = new SliderPath + { + ControlPoints = + { + new PathControlPoint(), + // simulate object snapped to 1/3rds + // this object's end time will be 2000 / 3 = 666.66... ms + new PathControlPoint(new Vector2(200 / 3f, 0)), + } + } + }; + + AddStep("add slider", () => composer.EditorBeatmap.Add(slider)); + AddStep("set snap to 1/4", () => BeatDivisor.Value = 4); + + // with default beat length of 1000ms and snap at 1/4, the valid snap times are 500ms, 750ms, and 1000ms + // with default settings, the snapped distance will be a tenth of the difference of the time delta + + // (500 - 666.66...) / 10 = -16.66... = -100 / 6 + assertSnappedDistance(0, -100 / 6f, slider); + assertSnappedDistance(7, -100 / 6f, slider); + + // (750 - 666.66...) / 10 = 8.33... = 100 / 12 + assertSnappedDistance(9, 100 / 12f, slider); + assertSnappedDistance(33, 100 / 12f, slider); + + // (1000 - 666.66...) / 10 = 33.33... = 100 / 3 + assertSnappedDistance(34, 100 / 3f, slider); + } + [Test] public void TestUseCurrentSnap() { From 75fc57c34bb4efd1a05bfb1fda7bbe14471b499b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 30 Sep 2024 12:26:08 +0200 Subject: [PATCH 2432/2493] Fix distance spacing grid displaying incorrectly for unsnapped objects with duration --- .../Sliders/SliderPlacementBlueprint.cs | 2 +- .../Sliders/SliderSelectionBlueprint.cs | 2 +- ...tSceneHitObjectComposerDistanceSnapping.cs | 2 +- .../Editing/TestSceneDistanceSnapGrid.cs | 2 +- .../Edit/ComposerDistanceSnapProvider.cs | 30 ++++++++++++++----- .../Rulesets/Edit/IDistanceSnapProvider.cs | 9 +++++- .../Rulesets/Objects/SliderPathExtensions.cs | 2 +- .../Components/CircularDistanceSnapGrid.cs | 4 +-- 8 files changed, 37 insertions(+), 16 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs index 6ffe27dc13..cb57c8e6e0 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs @@ -401,7 +401,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders if (state == SliderPlacementState.Drawing) HitObject.Path.ExpectedDistance.Value = (float)HitObject.Path.CalculatedDistance; else - HitObject.Path.ExpectedDistance.Value = distanceSnapProvider?.FindSnappedDistance(HitObject, (float)HitObject.Path.CalculatedDistance) ?? (float)HitObject.Path.CalculatedDistance; + HitObject.Path.ExpectedDistance.Value = distanceSnapProvider?.FindSnappedDistance(HitObject, (float)HitObject.Path.CalculatedDistance, DistanceSnapTarget.Start) ?? (float)HitObject.Path.CalculatedDistance; bodyPiece.UpdateFrom(HitObject); headCirclePiece.UpdateFrom(HitObject.HeadCircle); diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs index 1debb09099..cd66f8d796 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs @@ -269,7 +269,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders { double minDistance = distanceSnapProvider?.GetBeatSnapDistanceAt(HitObject, false) * oldVelocityMultiplier ?? 1; // Add a small amount to the proposed distance to make it easier to snap to the full length of the slider. - proposedDistance = distanceSnapProvider?.FindSnappedDistance(HitObject, (float)proposedDistance + 1) ?? proposedDistance; + proposedDistance = distanceSnapProvider?.FindSnappedDistance(HitObject, (float)proposedDistance + 1, DistanceSnapTarget.Start) ?? proposedDistance; proposedDistance = MathHelper.Clamp(proposedDistance, minDistance, HitObject.Path.CalculatedDistance); } diff --git a/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs b/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs index 700aafb62d..2503d5a954 100644 --- a/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs +++ b/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs @@ -298,7 +298,7 @@ namespace osu.Game.Tests.Editing => AddAssert($"distance = {distance} -> duration = {expectedDuration} (snapped)", () => composer.DistanceSnapProvider.FindSnappedDuration(referenceObject ?? new HitObject(), distance), () => Is.EqualTo(expectedDuration).Within(Precision.FLOAT_EPSILON)); private void assertSnappedDistance(float distance, float expectedDistance, HitObject? referenceObject = null) - => AddAssert($"distance = {distance} -> distance = {expectedDistance} (snapped)", () => composer.DistanceSnapProvider.FindSnappedDistance(referenceObject ?? new HitObject(), distance), () => Is.EqualTo(expectedDistance).Within(Precision.FLOAT_EPSILON)); + => AddAssert($"distance = {distance} -> distance = {expectedDistance} (snapped)", () => composer.DistanceSnapProvider.FindSnappedDistance(referenceObject ?? new HitObject(), distance, DistanceSnapTarget.End), () => Is.EqualTo(expectedDistance).Within(Precision.FLOAT_EPSILON)); private partial class TestHitObjectComposer : OsuHitObjectComposer { diff --git a/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs b/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs index f2a015402a..c1a788cd22 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs @@ -199,7 +199,7 @@ namespace osu.Game.Tests.Visual.Editing public double FindSnappedDuration(HitObject referenceObject, float distance) => 0; - public float FindSnappedDistance(HitObject referenceObject, float distance) => 0; + public float FindSnappedDistance(HitObject referenceObject, float distance, DistanceSnapTarget target) => 0; } } } diff --git a/osu.Game/Rulesets/Edit/ComposerDistanceSnapProvider.cs b/osu.Game/Rulesets/Edit/ComposerDistanceSnapProvider.cs index 979492fd8b..7ed692ad3d 100644 --- a/osu.Game/Rulesets/Edit/ComposerDistanceSnapProvider.cs +++ b/osu.Game/Rulesets/Edit/ComposerDistanceSnapProvider.cs @@ -280,22 +280,36 @@ namespace osu.Game.Rulesets.Edit public virtual double FindSnappedDuration(HitObject referenceObject, float distance) => beatSnapProvider.SnapTime(referenceObject.StartTime + DistanceToDuration(referenceObject, distance), referenceObject.StartTime) - referenceObject.StartTime; - public virtual float FindSnappedDistance(HitObject referenceObject, float distance) + public virtual float FindSnappedDistance(HitObject referenceObject, float distance, DistanceSnapTarget target) { - double startTime = referenceObject.StartTime; + double referenceTime; - double actualDuration = startTime + DistanceToDuration(referenceObject, distance); + switch (target) + { + case DistanceSnapTarget.Start: + referenceTime = referenceObject.StartTime; + break; - double snappedEndTime = beatSnapProvider.SnapTime(actualDuration, startTime); + case DistanceSnapTarget.End: + referenceTime = referenceObject.GetEndTime(); + break; - double beatLength = beatSnapProvider.GetBeatLengthAtTime(startTime); + default: + throw new ArgumentOutOfRangeException(nameof(target), target, $"Unknown {nameof(DistanceSnapTarget)} value"); + } + + double actualDuration = referenceTime + DistanceToDuration(referenceObject, distance); + + double snappedTime = beatSnapProvider.SnapTime(actualDuration, referenceTime); + + double beatLength = beatSnapProvider.GetBeatLengthAtTime(referenceTime); // we don't want to exceed the actual duration and snap to a point in the future. // as we are snapping to beat length via SnapTime (which will round-to-nearest), check for snapping in the forward direction and reverse it. - if (snappedEndTime > actualDuration + 1) - snappedEndTime -= beatLength; + if (snappedTime > actualDuration + 1) + snappedTime -= beatLength; - return DurationToDistance(referenceObject, snappedEndTime - startTime); + return DurationToDistance(referenceObject, snappedTime - referenceTime); } #endregion diff --git a/osu.Game/Rulesets/Edit/IDistanceSnapProvider.cs b/osu.Game/Rulesets/Edit/IDistanceSnapProvider.cs index 380038eadf..17fae9e8b2 100644 --- a/osu.Game/Rulesets/Edit/IDistanceSnapProvider.cs +++ b/osu.Game/Rulesets/Edit/IDistanceSnapProvider.cs @@ -58,10 +58,17 @@ namespace osu.Game.Rulesets.Edit /// /// An object to be used as a reference point for this operation. /// The distance to convert. + /// Whether the distance measured should be from the start or the end of . /// /// A value that represents snapped to the closest beat of the timing point. /// The distance will always be less than or equal to the provided . /// - float FindSnappedDistance(HitObject referenceObject, float distance); + float FindSnappedDistance(HitObject referenceObject, float distance, DistanceSnapTarget target); + } + + public enum DistanceSnapTarget + { + Start, + End, } } diff --git a/osu.Game/Rulesets/Objects/SliderPathExtensions.cs b/osu.Game/Rulesets/Objects/SliderPathExtensions.cs index c03d3646da..a631274f74 100644 --- a/osu.Game/Rulesets/Objects/SliderPathExtensions.cs +++ b/osu.Game/Rulesets/Objects/SliderPathExtensions.cs @@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Objects public static void SnapTo(this THitObject hitObject, IDistanceSnapProvider? snapProvider) where THitObject : HitObject, IHasPath { - hitObject.Path.ExpectedDistance.Value = snapProvider?.FindSnappedDistance(hitObject, (float)hitObject.Path.CalculatedDistance) ?? hitObject.Path.CalculatedDistance; + hitObject.Path.ExpectedDistance.Value = snapProvider?.FindSnappedDistance(hitObject, (float)hitObject.Path.CalculatedDistance, DistanceSnapTarget.Start) ?? hitObject.Path.CalculatedDistance; } /// diff --git a/osu.Game/Screens/Edit/Compose/Components/CircularDistanceSnapGrid.cs b/osu.Game/Screens/Edit/Compose/Components/CircularDistanceSnapGrid.cs index 92fe52148c..bd750dac76 100644 --- a/osu.Game/Screens/Edit/Compose/Components/CircularDistanceSnapGrid.cs +++ b/osu.Game/Screens/Edit/Compose/Components/CircularDistanceSnapGrid.cs @@ -59,7 +59,7 @@ namespace osu.Game.Screens.Edit.Compose.Components // Picture the scenario where the user has just placed an object on a 1/2 snap, then changes to // 1/3 snap and expects to be able to place the next object on a valid 1/3 snap, regardless of the // fact that the 1/2 snap reference object is not valid for 1/3 snapping. - float offset = SnapProvider.FindSnappedDistance(ReferenceObject, 0); + float offset = SnapProvider.FindSnappedDistance(ReferenceObject, 0, DistanceSnapTarget.End); for (int i = 0; i < requiredCircles; i++) { @@ -104,7 +104,7 @@ namespace osu.Game.Screens.Edit.Compose.Components ? SnapProvider.DurationToDistance(ReferenceObject, editorClock.CurrentTime - ReferenceObject.GetEndTime()) // When interacting with the resolved snap provider, the distance spacing multiplier should first be removed // to allow for snapping at a non-multiplied ratio. - : SnapProvider.FindSnappedDistance(ReferenceObject, travelLength / distanceSpacingMultiplier); + : SnapProvider.FindSnappedDistance(ReferenceObject, travelLength / distanceSpacingMultiplier, DistanceSnapTarget.End); double snappedTime = StartTime + SnapProvider.DistanceToDuration(ReferenceObject, snappedDistance); From 11fc1f9a1c632b0ecac600d80e87cfe3345fd6f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 30 Sep 2024 13:32:19 +0200 Subject: [PATCH 2433/2493] Fix distance snap grid using wrong colour when reference object is unsnapped --- osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs b/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs index 8aa2fa9f45..7003d632ca 100644 --- a/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs +++ b/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs @@ -155,7 +155,7 @@ namespace osu.Game.Screens.Edit.Compose.Components { var timingPoint = Beatmap.ControlPointInfo.TimingPointAt(StartTime); double beatLength = timingPoint.BeatLength / beatDivisor.Value; - int beatIndex = (int)Math.Round((StartTime - timingPoint.Time) / beatLength); + int beatIndex = (int)Math.Floor((StartTime - timingPoint.Time) / beatLength); var colour = BindableBeatDivisor.GetColourFor(BindableBeatDivisor.GetDivisorForBeatIndex(beatIndex + placementIndex + 1, beatDivisor.Value), Colours); From 155d6e57be1805c83ff12316e515d8bb4d06ec00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 30 Sep 2024 14:05:20 +0200 Subject: [PATCH 2434/2493] Isolate tests properly --- .../Visual/UserInterface/TestSceneCommentEditor.cs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneCommentEditor.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneCommentEditor.cs index ac6ca218c4..721e231577 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneCommentEditor.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneCommentEditor.cs @@ -29,9 +29,10 @@ namespace osu.Game.Tests.Visual.UserInterface private TestCancellableCommentEditor cancellableCommentEditor = null!; private DummyAPIAccess dummyAPI => (DummyAPIAccess)API; - [SetUp] - public void SetUp() => Schedule(() => - Add(new FillFlowContainer + [SetUpSteps] + public void SetUpSteps() + { + AddStep("create content", () => Child = new FillFlowContainer { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -44,7 +45,8 @@ namespace osu.Game.Tests.Visual.UserInterface commentEditor = new TestCommentEditor(), cancellableCommentEditor = new TestCancellableCommentEditor() } - })); + }); + } [Test] public void TestCommitViaKeyboard() From 74a9899fc078528fc70c17fef32370b8cb54283f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 30 Sep 2024 14:05:23 +0200 Subject: [PATCH 2435/2493] Fix doubled-up enabled state management of commit button --- osu.Game/Overlays/Comments/CommentEditor.cs | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/osu.Game/Overlays/Comments/CommentEditor.cs b/osu.Game/Overlays/Comments/CommentEditor.cs index b75e5aa8d8..ccb912253a 100644 --- a/osu.Game/Overlays/Comments/CommentEditor.cs +++ b/osu.Game/Overlays/Comments/CommentEditor.cs @@ -68,7 +68,7 @@ namespace osu.Game.Overlays.Comments else loadingSpinner.Hide(); - updateCommitButtonState(); + updateState(); } } @@ -170,17 +170,15 @@ namespace osu.Game.Overlays.Comments protected override void LoadComplete() { base.LoadComplete(); - Current.BindValueChanged(_ => updateCommitButtonState(), true); - apiState.BindValueChanged(_ => updateEnabledState()); - CommentableMeta.BindValueChanged(_ => updateEnabledState(), true); + Current.BindValueChanged(_ => updateState()); + apiState.BindValueChanged(_ => Scheduler.AddOnce(updateState)); + CommentableMeta.BindValueChanged(_ => Scheduler.AddOnce(updateState)); + updateState(); } protected abstract void OnCommit(string text); - private void updateCommitButtonState() => - commitButton.Enabled.Value = loadingSpinner.State.Value == Visibility.Hidden && !string.IsNullOrEmpty(Current.Value); - - private void updateEnabledState() => Schedule(() => + private void updateState() { bool isOnline = apiState.Value > APIState.Offline; var canNewCommentReason = CommentEditor.canNewCommentReason(CommentableMeta.Value); @@ -198,7 +196,7 @@ namespace osu.Game.Overlays.Comments if (isOnline) { commitButton.Show(); - commitButton.Enabled.Value = !commentsDisabled; + commitButton.Enabled.Value = !commentsDisabled && loadingSpinner.State.Value == Visibility.Hidden && !string.IsNullOrEmpty(Current.Value); logInButton.Hide(); } else @@ -206,7 +204,7 @@ namespace osu.Game.Overlays.Comments commitButton.Hide(); logInButton.Show(); } - }); + } // https://github.com/ppy/osu-web/blob/83816dbe24ad2927273cba968f2fcd2694a121a9/resources/js/components/comment-editor.tsx#L54-L60 // careful here, logic is VERY finicky. From 48b03a328b3debf30c64fafb618e981c90fd0524 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 30 Sep 2024 14:26:30 +0200 Subject: [PATCH 2436/2493] Ensure sliders are snapped when changing path types Closes https://github.com/ppy/osu/issues/29915. Uses behaviour suggested in https://github.com/ppy/osu/issues/29915#issuecomment-2361843011. --- .../Sliders/Components/PathControlPointVisualiser.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs index df369dcef5..d90aab5788 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs @@ -353,6 +353,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components { changeHandler?.BeginChange(); + double originalDistance = hitObject.Path.Distance; + foreach (var p in Pieces.Where(p => p.IsSelected.Value)) { var pointsInSegment = hitObject.Path.PointsInSegment(p.ControlPoint); @@ -375,6 +377,11 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components EnsureValidPathTypes(); + if (hitObject.Path.Distance < originalDistance) + hitObject.SnapTo(distanceSnapProvider); + else + hitObject.Path.ExpectedDistance.Value = originalDistance; + changeHandler?.EndChange(); } From 493dcc7a1cc987c0e02b7a9d1272792ba84ae76a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 30 Sep 2024 14:32:11 +0200 Subject: [PATCH 2437/2493] Fix test being dodgy Hitobjects are in an indeterminate state until defaults are applied. Adding the object to the beatmap will do this. --- .../Editing/TestSceneHitObjectComposerDistanceSnapping.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs b/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs index cf8c3c6ef1..d16199b0f5 100644 --- a/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs +++ b/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs @@ -112,6 +112,7 @@ namespace osu.Game.Tests.Editing { SliderVelocityMultiplier = slider_velocity }; + AddStep("add to beatmap", () => composer.EditorBeatmap.Add(referenceObject)); assertSnapDistance(base_distance * slider_velocity, referenceObject, true); assertSnappedDistance(base_distance * slider_velocity + 10, base_distance * slider_velocity, referenceObject); From 0409edccce27fc7dfb11fb39ce4a1a6722d7ca6a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 1 Oct 2024 15:14:40 +0900 Subject: [PATCH 2438/2493] Add button to centre editor grid to current hit object --- .../Edit/OsuGridToolboxGroup.cs | 24 +++++++++++++++++++ osu.Game/Rulesets/Edit/ExpandableButton.cs | 2 +- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs b/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs index 73ecb2fe7c..0fe9d85635 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs @@ -14,6 +14,7 @@ using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osu.Game.Input.Bindings; using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu.UI; using osu.Game.Screens.Edit; using osu.Game.Screens.Edit.Components.RadioButtons; @@ -90,6 +91,8 @@ namespace osu.Game.Rulesets.Osu.Edit private ExpandableSlider gridLinesRotationSlider = null!; private EditorRadioButtonCollection gridTypeButtons = null!; + private ExpandableButton useSelectedObjectPositionButton = null!; + public OsuGridToolboxGroup() : base("grid") { @@ -112,6 +115,19 @@ namespace osu.Game.Rulesets.Osu.Edit Current = StartPositionY, KeyboardStep = 1, }, + useSelectedObjectPositionButton = new ExpandableButton + { + ExpandedLabelText = "Centre on selected object", + Action = () => + { + if (editorBeatmap.SelectedHitObjects.Count != 1) + return; + + StartPosition.Value = ((IHasPosition)editorBeatmap.SelectedHitObjects.Single()).Position; + updateEnabledStates(); + }, + RelativeSizeAxes = Axes.X, + }, spacingSlider = new ExpandableSlider { Current = Spacing, @@ -211,6 +227,14 @@ namespace osu.Game.Rulesets.Osu.Edit break; } }, true); + + editorBeatmap.BeatmapReprocessed += updateEnabledStates; + editorBeatmap.SelectedHitObjects.BindCollectionChanged((_, _) => updateEnabledStates(), true); + } + + private void updateEnabledStates() + { + useSelectedObjectPositionButton.Enabled.Value = editorBeatmap.SelectedHitObjects.Count == 1 && StartPosition.Value != ((IHasPosition)editorBeatmap.SelectedHitObjects.Single()).Position; } private void nextGridSize() diff --git a/osu.Game/Rulesets/Edit/ExpandableButton.cs b/osu.Game/Rulesets/Edit/ExpandableButton.cs index a708f76845..9139802d68 100644 --- a/osu.Game/Rulesets/Edit/ExpandableButton.cs +++ b/osu.Game/Rulesets/Edit/ExpandableButton.cs @@ -11,7 +11,7 @@ using osu.Game.Graphics.UserInterfaceV2; namespace osu.Game.Rulesets.Edit { - internal partial class ExpandableButton : RoundedButton, IExpandable + public partial class ExpandableButton : RoundedButton, IExpandable { private float actualHeight; From 8d2f2517a312d5482f8a4719d3f6a4c008a34fa4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 1 Oct 2024 10:01:31 +0200 Subject: [PATCH 2439/2493] Specify type explicitly --- osu.Game/Overlays/Comments/CommentEditor.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Comments/CommentEditor.cs b/osu.Game/Overlays/Comments/CommentEditor.cs index ccb912253a..c456592383 100644 --- a/osu.Game/Overlays/Comments/CommentEditor.cs +++ b/osu.Game/Overlays/Comments/CommentEditor.cs @@ -181,7 +181,7 @@ namespace osu.Game.Overlays.Comments private void updateState() { bool isOnline = apiState.Value > APIState.Offline; - var canNewCommentReason = CommentEditor.canNewCommentReason(CommentableMeta.Value); + LocalisableString? canNewCommentReason = CommentEditor.canNewCommentReason(CommentableMeta.Value); bool commentsDisabled = canNewCommentReason != null; bool canComment = isOnline && !commentsDisabled; From a2d9302f4ad472b23c102794a43a4cc9879aa5bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 1 Oct 2024 10:12:38 +0200 Subject: [PATCH 2440/2493] Move shuffle button to left side --- osu.Game/Overlays/NowPlayingOverlay.cs | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/osu.Game/Overlays/NowPlayingOverlay.cs b/osu.Game/Overlays/NowPlayingOverlay.cs index e1e5aa9426..f4da9a92dc 100644 --- a/osu.Game/Overlays/NowPlayingOverlay.cs +++ b/osu.Game/Overlays/NowPlayingOverlay.cs @@ -164,15 +164,16 @@ namespace osu.Game.Overlays Action = () => musicController.NextTrack(), Icon = FontAwesome.Solid.StepForward, }, - shuffleButton = new MusicIconButton - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Action = shuffle.Toggle, - Icon = FontAwesome.Solid.Random, - } } }, + shuffleButton = new MusicIconButton + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.Centre, + Position = new Vector2(bottom_black_area_height / 2, 0), + Action = shuffle.Toggle, + Icon = FontAwesome.Solid.Random, + }, playlistButton = new MusicIconButton { Origin = Anchor.Centre, From 2a214f7c9fb82de43c0b36ef4aab3d4cea301dca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 1 Oct 2024 10:19:59 +0200 Subject: [PATCH 2441/2493] Fix incorrect implementation of next track choice `SkipWhile()` in this context does not correctly ensure that `ElementAtOrDefault(1)` is not a protected track. An explicit `Where()` does. Spotted accidentally when I noticed that skipping to next track can select a protected track, but skipping to previous cannot. --- osu.Game/Overlays/MusicController.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/MusicController.cs b/osu.Game/Overlays/MusicController.cs index a7bca536df..600c014a95 100644 --- a/osu.Game/Overlays/MusicController.cs +++ b/osu.Game/Overlays/MusicController.cs @@ -351,7 +351,9 @@ namespace osu.Game.Overlays playableSet = getNextRandom(1, allowProtectedTracks); else { - playableSet = getBeatmapSets().AsEnumerable().SkipWhile(i => !i.Equals(current?.BeatmapSetInfo) || (i.Protected && !allowProtectedTracks)).ElementAtOrDefault(1) + playableSet = getBeatmapSets().AsEnumerable().SkipWhile(i => !i.Equals(current?.BeatmapSetInfo)) + .Where(i => !i.Protected || allowProtectedTracks) + .ElementAtOrDefault(1) ?? getBeatmapSets().AsEnumerable().FirstOrDefault(i => !i.Protected || allowProtectedTracks); } From 307cc5581dad4b4f03a80ccb39a93a6b100c59ec Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 1 Oct 2024 18:01:06 +0900 Subject: [PATCH 2442/2493] Fix usage of locked context without locking Raised in the latest Rider EAP, seems valid enough. --- osu.Game/Beatmaps/WorkingBeatmap.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/WorkingBeatmap.cs b/osu.Game/Beatmaps/WorkingBeatmap.cs index 25159996f3..07bf4c028a 100644 --- a/osu.Game/Beatmaps/WorkingBeatmap.cs +++ b/osu.Game/Beatmaps/WorkingBeatmap.cs @@ -183,7 +183,14 @@ namespace osu.Game.Beatmaps #region Beatmap - public virtual bool BeatmapLoaded => beatmapLoadTask?.IsCompleted ?? false; + public virtual bool BeatmapLoaded + { + get + { + lock (beatmapFetchLock) + return beatmapLoadTask?.IsCompleted ?? false; + } + } public IBeatmap Beatmap { From 44b982864c27bce354d8f7a7de34418b6eaa74f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 1 Oct 2024 11:12:56 +0200 Subject: [PATCH 2443/2493] Fix test failures --- .../TestSceneHitObjectComposerDistanceSnapping.cs | 13 ++++++++----- .../Rulesets/Edit/ComposerDistanceSnapProvider.cs | 1 + 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs b/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs index cf8c3c6ef1..156675a066 100644 --- a/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs +++ b/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs @@ -230,23 +230,26 @@ namespace osu.Game.Tests.Editing [Test] public void TestUseCurrentSnap() { + ExpandableButton getCurrentSnapButton() => composer.ChildrenOfType().Single(g => g.Name == "snapping") + .ChildrenOfType().Single(); + AddStep("add objects to beatmap", () => { editorBeatmap.Add(new HitCircle { StartTime = 1000 }); editorBeatmap.Add(new HitCircle { Position = new Vector2(100), StartTime = 2000 }); }); - AddStep("hover use current snap button", () => InputManager.MoveMouseTo(composer.ChildrenOfType().Single())); - AddUntilStep("use current snap expanded", () => composer.ChildrenOfType().Single().Expanded.Value, () => Is.True); + AddStep("hover use current snap button", () => InputManager.MoveMouseTo(getCurrentSnapButton())); + AddUntilStep("use current snap expanded", () => getCurrentSnapButton().Expanded.Value, () => Is.True); AddStep("seek before first object", () => EditorClock.Seek(0)); - AddUntilStep("use current snap not available", () => composer.ChildrenOfType().Single().Enabled.Value, () => Is.False); + AddUntilStep("use current snap not available", () => getCurrentSnapButton().Enabled.Value, () => Is.False); AddStep("seek to between objects", () => EditorClock.Seek(1500)); - AddUntilStep("use current snap available", () => composer.ChildrenOfType().Single().Enabled.Value, () => Is.True); + AddUntilStep("use current snap available", () => getCurrentSnapButton().Enabled.Value, () => Is.True); AddStep("seek after last object", () => EditorClock.Seek(2500)); - AddUntilStep("use current snap not available", () => composer.ChildrenOfType().Single().Enabled.Value, () => Is.False); + AddUntilStep("use current snap not available", () => getCurrentSnapButton().Enabled.Value, () => Is.False); } private void assertSnapDistance(float expectedDistance, HitObject? referenceObject, bool includeSliderVelocity) diff --git a/osu.Game/Rulesets/Edit/ComposerDistanceSnapProvider.cs b/osu.Game/Rulesets/Edit/ComposerDistanceSnapProvider.cs index 979492fd8b..d8f493405f 100644 --- a/osu.Game/Rulesets/Edit/ComposerDistanceSnapProvider.cs +++ b/osu.Game/Rulesets/Edit/ComposerDistanceSnapProvider.cs @@ -74,6 +74,7 @@ namespace osu.Game.Rulesets.Edit toolboxContainer.Add(toolboxGroup = new EditorToolboxGroup("snapping") { + Name = "snapping", Alpha = DistanceSpacingMultiplier.Disabled ? 0 : 1, Children = new Drawable[] { From 9dad38c457aa68b9d4cbccb56226d3a3b33ddd0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 1 Oct 2024 11:27:45 +0200 Subject: [PATCH 2444/2493] Fix button being interactable when collapsed Did not match the expandable button in distance snap toolbox. --- .../Edit/OsuGridToolboxGroup.cs | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs b/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs index 0fe9d85635..4fa8852770 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs @@ -202,12 +202,6 @@ namespace osu.Game.Rulesets.Osu.Edit gridLinesRotationSlider.ExpandedLabelText = $"Rotation: {rotation.NewValue:#,0.##}"; }, true); - expandingContainer?.Expanded.BindValueChanged(v => - { - gridTypeButtons.FadeTo(v.NewValue ? 1f : 0f, 500, Easing.OutQuint); - gridTypeButtons.BypassAutoSizeAxes = !v.NewValue ? Axes.Y : Axes.None; - }, true); - GridType.BindValueChanged(v => { GridLinesRotation.Disabled = v.NewValue == PositionSnapGridType.Circle; @@ -229,12 +223,20 @@ namespace osu.Game.Rulesets.Osu.Edit }, true); editorBeatmap.BeatmapReprocessed += updateEnabledStates; - editorBeatmap.SelectedHitObjects.BindCollectionChanged((_, _) => updateEnabledStates(), true); + editorBeatmap.SelectedHitObjects.BindCollectionChanged((_, _) => updateEnabledStates()); + expandingContainer?.Expanded.BindValueChanged(v => + { + gridTypeButtons.FadeTo(v.NewValue ? 1f : 0f, 500, Easing.OutQuint); + gridTypeButtons.BypassAutoSizeAxes = !v.NewValue ? Axes.Y : Axes.None; + updateEnabledStates(); + }, true); } private void updateEnabledStates() { - useSelectedObjectPositionButton.Enabled.Value = editorBeatmap.SelectedHitObjects.Count == 1 && StartPosition.Value != ((IHasPosition)editorBeatmap.SelectedHitObjects.Single()).Position; + useSelectedObjectPositionButton.Enabled.Value = expandingContainer?.Expanded.Value == true + && editorBeatmap.SelectedHitObjects.Count == 1 + && StartPosition.Value != ((IHasPosition)editorBeatmap.SelectedHitObjects.Single()).Position; } private void nextGridSize() From ae75bfd9667b0692dec761b81fdaa570e46dd2aa Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 1 Oct 2024 18:41:53 +0900 Subject: [PATCH 2445/2493] Rename keyboard mapping methods to make more sense now that everything's on the left --- osu.Game/Rulesets/Edit/HitObjectComposer.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs index 00de46b726..316e8e55e8 100644 --- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs @@ -363,7 +363,7 @@ namespace osu.Game.Rulesets.Edit if (e.ControlPressed || e.AltPressed || e.SuperPressed) return false; - if (checkLeftToggleFromKey(e.Key, out int leftIndex)) + if (checkToolboxMappingFromKey(e.Key, out int leftIndex)) { var item = toolboxCollection.Items.ElementAtOrDefault(leftIndex); @@ -375,7 +375,7 @@ namespace osu.Game.Rulesets.Edit } } - if (checkRightToggleFromKey(e.Key, out int rightIndex)) + if (checkToggleMappingFromKey(e.Key, out int rightIndex)) { var item = e.ShiftPressed ? sampleBankTogglesCollection.ElementAtOrDefault(rightIndex) @@ -391,7 +391,7 @@ namespace osu.Game.Rulesets.Edit return base.OnKeyDown(e); } - private bool checkLeftToggleFromKey(Key key, out int index) + private bool checkToolboxMappingFromKey(Key key, out int index) { if (key < Key.Number1 || key > Key.Number9) { @@ -403,7 +403,7 @@ namespace osu.Game.Rulesets.Edit return true; } - private bool checkRightToggleFromKey(Key key, out int index) + private bool checkToggleMappingFromKey(Key key, out int index) { switch (key) { From 24d534929d3b6f64da7a1aa53cbf01b5715857cb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 1 Oct 2024 18:44:27 +0900 Subject: [PATCH 2446/2493] Less `var` please --- .../Components/PathControlPointVisualiser.cs | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs index 21d63a0ea3..bb8ee11e49 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs @@ -107,7 +107,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components if (segment.Count == 0) return; - var first = segment[0]; + PathControlPoint first = segment[0]; if (first.Type != PathType.PERFECT_CURVE) return; @@ -273,10 +273,10 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components if (selectedPieces.Length != 1) return false; - var selectedPiece = selectedPieces.Single(); - var selectedPoint = selectedPiece.ControlPoint; + PathControlPointPiece selectedPiece = selectedPieces.Single(); + PathControlPoint selectedPoint = selectedPiece.ControlPoint; - var validTypes = path_types; + PathType?[] validTypes = path_types; if (selectedPoint == controlPoints[0]) validTypes = validTypes.Where(t => t != null).ToArray(); @@ -313,7 +313,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components if (Pieces.All(p => !p.IsSelected.Value)) return false; - var type = path_types[e.Key - Key.Number1]; + PathType? type = path_types[e.Key - Key.Number1]; // The first control point can never be inherit type if (Pieces[0].IsSelected.Value && type == null) @@ -355,7 +355,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components foreach (var p in Pieces.Where(p => p.IsSelected.Value)) { - var pointsInSegment = hitObject.Path.PointsInSegment(p.ControlPoint); + List pointsInSegment = hitObject.Path.PointsInSegment(p.ControlPoint); int indexInSegment = pointsInSegment.IndexOf(p.ControlPoint); if (type?.Type == SplineType.PerfectCurve) @@ -405,14 +405,14 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components public void DragInProgress(DragEvent e) { Vector2[] oldControlPoints = hitObject.Path.ControlPoints.Select(cp => cp.Position).ToArray(); - var oldPosition = hitObject.Position; + Vector2 oldPosition = hitObject.Position; double oldStartTime = hitObject.StartTime; if (selectedControlPoints.Contains(hitObject.Path.ControlPoints[0])) { // Special handling for selections containing head control point - the position of the hit object changes which means the snapped position and time have to be taken into account Vector2 newHeadPosition = Parent!.ToScreenSpace(e.MousePosition + (dragStartPositions[0] - dragStartPositions[draggedControlPointIndex])); - var result = positionSnapProvider?.FindSnappedPositionAndTime(newHeadPosition); + SnapResult result = positionSnapProvider?.FindSnappedPositionAndTime(newHeadPosition); Vector2 movementDelta = Parent!.ToLocalSpace(result?.ScreenSpacePosition ?? newHeadPosition) - hitObject.Position; @@ -421,7 +421,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components for (int i = 1; i < hitObject.Path.ControlPoints.Count; i++) { - var controlPoint = hitObject.Path.ControlPoints[i]; + PathControlPoint controlPoint = hitObject.Path.ControlPoints[i]; // Since control points are relative to the position of the hit object, all points that are _not_ selected // need to be offset _back_ by the delta corresponding to the movement of the head point. // All other selected control points (if any) will move together with the head point @@ -432,13 +432,13 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components } else { - var result = positionSnapProvider?.FindSnappedPositionAndTime(Parent!.ToScreenSpace(e.MousePosition), SnapType.GlobalGrids); + SnapResult result = positionSnapProvider?.FindSnappedPositionAndTime(Parent!.ToScreenSpace(e.MousePosition), SnapType.GlobalGrids); Vector2 movementDelta = Parent!.ToLocalSpace(result?.ScreenSpacePosition ?? Parent!.ToScreenSpace(e.MousePosition)) - dragStartPositions[draggedControlPointIndex] - hitObject.Position; for (int i = 0; i < controlPoints.Count; ++i) { - var controlPoint = controlPoints[i]; + PathControlPoint controlPoint = controlPoints[i]; if (selectedControlPoints.Contains(controlPoint)) controlPoint.Position = dragStartPositions[i] + movementDelta; } @@ -490,7 +490,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components for (int i = 0; i < path_types.Length; ++i) { - var type = path_types[i]; + PathType? type = path_types[i]; // special inherit case if (type == null) From 162558e0b8253eb4588117dbc270d78510068f3c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 1 Oct 2024 18:48:09 +0900 Subject: [PATCH 2447/2493] Use `record` `struct` See https://www.jetbrains.com/help/rider/UsageOfDefaultStructEquality.html. --- osu.Game/Graphics/UserInterface/Hotkey.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Graphics/UserInterface/Hotkey.cs b/osu.Game/Graphics/UserInterface/Hotkey.cs index 0b5176a02e..8b3014bdc5 100644 --- a/osu.Game/Graphics/UserInterface/Hotkey.cs +++ b/osu.Game/Graphics/UserInterface/Hotkey.cs @@ -11,7 +11,7 @@ using osu.Game.Input.Bindings; namespace osu.Game.Graphics.UserInterface { - public readonly struct Hotkey + public readonly record struct Hotkey { public KeyCombination[]? KeyCombinations { get; init; } public GlobalAction? GlobalAction { get; init; } From 4f16ecdf1b5bd1dffff3ee00b13c977058ec0558 Mon Sep 17 00:00:00 2001 From: CloneWith Date: Tue, 1 Oct 2024 20:22:46 +0800 Subject: [PATCH 2448/2493] Add progress tooltip for ArgonSongProgressBar --- .../Screens/Play/HUD/ArgonSongProgressBar.cs | 27 ++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/HUD/ArgonSongProgressBar.cs b/osu.Game/Screens/Play/HUD/ArgonSongProgressBar.cs index 7a7870a775..87f5c495ff 100644 --- a/osu.Game/Screens/Play/HUD/ArgonSongProgressBar.cs +++ b/osu.Game/Screens/Play/HUD/ArgonSongProgressBar.cs @@ -6,16 +6,21 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Shapes; +using osu.Framework.Input; using osu.Framework.Input.Events; +using osu.Framework.Localisation; using osu.Framework.Utils; +using osu.Game.Extensions; using osu.Game.Graphics; using osuTK; namespace osu.Game.Screens.Play.HUD { - public partial class ArgonSongProgressBar : SongProgressBar + public partial class ArgonSongProgressBar : SongProgressBar, IHasTooltip { + // Parent will handle restricting the area of valid input. public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; @@ -33,6 +38,13 @@ namespace osu.Game.Screens.Play.HUD private double trackTime => (EndTime - StartTime) * Progress; + private float relativePositionX; + + private InputManager? inputManager; + + public LocalisableString TooltipText => $"{(relativePositionX > 0 ? Math.Round(EndTime * relativePositionX / DrawWidth, 2) : relativePositionX > DrawWidth ? EndTime : 0).ToEditorFormattedString()}" + + $" - {(relativePositionX > 0 ? Math.Round(relativePositionX / DrawWidth * 100, 2) : relativePositionX > DrawWidth ? 100 : 0)}%"; + public ArgonSongProgressBar(float barHeight) { RelativeSizeAxes = Axes.X; @@ -74,6 +86,7 @@ namespace osu.Game.Screens.Play.HUD private void load(OsuColour colours) { catchUpColour = colours.BlueDark; + inputManager = GetContainingInputManager(); } protected override void LoadComplete() @@ -102,6 +115,18 @@ namespace osu.Game.Screens.Play.HUD { base.Update(); + if (inputManager != null) + { + // Update the cursor position in time + var cursorPosition = inputManager.CurrentState.Mouse.Position; + relativePositionX = ToLocalSpace(cursorPosition).X; + } + else + { + // If null (e.g. before the game starts), try getting the input manager again + inputManager = GetContainingInputManager(); + } + playfieldBar.Length = (float)Interpolation.Lerp(playfieldBar.Length, Progress, Math.Clamp(Time.Elapsed / 40, 0, 1)); audioBar.Length = (float)Interpolation.Lerp(audioBar.Length, AudioProgress, Math.Clamp(Time.Elapsed / 40, 0, 1)); From 5af05f1cc987f198a128f071efb345c052bf46fc Mon Sep 17 00:00:00 2001 From: CloneWith Date: Tue, 1 Oct 2024 20:53:15 +0800 Subject: [PATCH 2449/2493] Use play length for timestamp calculation --- osu.Game/Screens/Play/HUD/ArgonSongProgressBar.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/HUD/ArgonSongProgressBar.cs b/osu.Game/Screens/Play/HUD/ArgonSongProgressBar.cs index 87f5c495ff..ea1ebce5f7 100644 --- a/osu.Game/Screens/Play/HUD/ArgonSongProgressBar.cs +++ b/osu.Game/Screens/Play/HUD/ArgonSongProgressBar.cs @@ -42,7 +42,7 @@ namespace osu.Game.Screens.Play.HUD private InputManager? inputManager; - public LocalisableString TooltipText => $"{(relativePositionX > 0 ? Math.Round(EndTime * relativePositionX / DrawWidth, 2) : relativePositionX > DrawWidth ? EndTime : 0).ToEditorFormattedString()}" + public LocalisableString TooltipText => $"{(relativePositionX > 0 ? (EndTime - StartTime) * relativePositionX / DrawWidth : relativePositionX > DrawWidth ? EndTime : 0).ToEditorFormattedString()}" + $" - {(relativePositionX > 0 ? Math.Round(relativePositionX / DrawWidth * 100, 2) : relativePositionX > DrawWidth ? 100 : 0)}%"; public ArgonSongProgressBar(float barHeight) From 5dd28d53521b31bc77c55f7d4fd4b6da80b7252a Mon Sep 17 00:00:00 2001 From: CloneWith Date: Tue, 1 Oct 2024 21:08:31 +0800 Subject: [PATCH 2450/2493] Fix extra newline --- osu.Game/Screens/Play/HUD/ArgonSongProgressBar.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Screens/Play/HUD/ArgonSongProgressBar.cs b/osu.Game/Screens/Play/HUD/ArgonSongProgressBar.cs index ea1ebce5f7..28a2e1030f 100644 --- a/osu.Game/Screens/Play/HUD/ArgonSongProgressBar.cs +++ b/osu.Game/Screens/Play/HUD/ArgonSongProgressBar.cs @@ -20,7 +20,6 @@ namespace osu.Game.Screens.Play.HUD { public partial class ArgonSongProgressBar : SongProgressBar, IHasTooltip { - // Parent will handle restricting the area of valid input. public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; From 87835f2481f5945262dc4e94810e6ae6c9577005 Mon Sep 17 00:00:00 2001 From: StanR Date: Wed, 2 Oct 2024 19:47:22 +0500 Subject: [PATCH 2451/2493] Uncap speed od accuracy scaling --- osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index 67d88b6b01..2941aeb936 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -177,7 +177,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty double relevantAccuracy = attributes.SpeedNoteCount == 0 ? 0 : (relevantCountGreat * 6.0 + relevantCountOk * 2.0 + relevantCountMeh) / (attributes.SpeedNoteCount * 6.0); // Scale the speed value with accuracy and OD. - speedValue *= (0.95 + Math.Pow(attributes.OverallDifficulty, 2) / 750) * Math.Pow((accuracy + relevantAccuracy) / 2.0, (14.5 - Math.Max(attributes.OverallDifficulty, 8)) / 2); + speedValue *= (0.95 + Math.Pow(attributes.OverallDifficulty, 2) / 750) * Math.Pow((accuracy + relevantAccuracy) / 2.0, (14.5 - attributes.OverallDifficulty) / 2); // Scale the speed value with # of 50s to punish doubletapping. speedValue *= Math.Pow(0.99, countMeh < totalHits / 500.0 ? 0 : countMeh - totalHits / 500.0); From 1039d4e45de192aedd8e97ed05addd068df8dacc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 3 Oct 2024 16:21:27 +0900 Subject: [PATCH 2452/2493] Increase chat font size again --- osu.Game/Online/Chat/StandAloneChatDisplay.cs | 1 - osu.Game/Overlays/Chat/ChatLine.cs | 8 ++++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/osu.Game/Online/Chat/StandAloneChatDisplay.cs b/osu.Game/Online/Chat/StandAloneChatDisplay.cs index e100b5fe5b..187191d232 100644 --- a/osu.Game/Online/Chat/StandAloneChatDisplay.cs +++ b/osu.Game/Online/Chat/StandAloneChatDisplay.cs @@ -205,7 +205,6 @@ namespace osu.Game.Online.Chat protected partial class StandAloneMessage : ChatLine { - protected override float FontSize => 13; protected override float Spacing => 5; protected override float UsernameWidth => 90; diff --git a/osu.Game/Overlays/Chat/ChatLine.cs b/osu.Game/Overlays/Chat/ChatLine.cs index 3f8862de36..e386f2ac09 100644 --- a/osu.Game/Overlays/Chat/ChatLine.cs +++ b/osu.Game/Overlays/Chat/ChatLine.cs @@ -48,7 +48,7 @@ namespace osu.Game.Overlays.Chat public IReadOnlyCollection DrawableContentFlow => drawableContentFlow; - protected virtual float FontSize => 12; + private const float font_size = 13; protected virtual float Spacing => 15; @@ -183,13 +183,13 @@ namespace osu.Game.Overlays.Chat Anchor = Anchor.TopLeft, Origin = Anchor.TopLeft, Spacing = new Vector2(-1, 0), - Font = OsuFont.GetFont(size: FontSize, weight: FontWeight.SemiBold, fixedWidth: true), + Font = OsuFont.GetFont(size: font_size, weight: FontWeight.SemiBold, fixedWidth: true), AlwaysPresent = true, }, drawableUsername = new DrawableChatUsername(message.Sender) { Width = UsernameWidth, - FontSize = FontSize, + FontSize = font_size, AutoSizeAxes = Axes.Y, Origin = Anchor.TopRight, Anchor = Anchor.TopRight, @@ -258,7 +258,7 @@ namespace osu.Game.Overlays.Chat private void styleMessageContent(SpriteText text) { text.Shadow = false; - text.Font = text.Font.With(size: FontSize, italics: Message.IsAction, weight: isMention ? FontWeight.SemiBold : FontWeight.Medium); + text.Font = text.Font.With(size: font_size, italics: Message.IsAction, weight: isMention ? FontWeight.SemiBold : FontWeight.Medium); Color4 messageColour = colourProvider?.Content1 ?? Colour4.White; From df730e6b6f1fb98e902a1df561272af9ba5fb058 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 28 Aug 2024 11:45:32 +0200 Subject: [PATCH 2453/2493] Implement "form" colour palette control --- .../UserInterface/TestSceneFormControls.cs | 11 + .../UserInterfaceV2/FormColourPalette.cs | 226 ++++++++++++++++++ 2 files changed, 237 insertions(+) create mode 100644 osu.Game/Graphics/UserInterfaceV2/FormColourPalette.cs diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneFormControls.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneFormControls.cs index 2a0b0515a1..518c3fc693 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneFormControls.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneFormControls.cs @@ -105,6 +105,17 @@ namespace osu.Game.Tests.Visual.UserInterface Caption = "Audio file", PlaceholderText = "Select an audio file", }, + new FormColourPalette + { + Caption = "Combo colours", + Colours = + { + Colour4.Red, + Colour4.Green, + Colour4.Blue, + Colour4.Yellow, + } + }, }, }, } diff --git a/osu.Game/Graphics/UserInterfaceV2/FormColourPalette.cs b/osu.Game/Graphics/UserInterfaceV2/FormColourPalette.cs new file mode 100644 index 0000000000..a2c1c1622e --- /dev/null +++ b/osu.Game/Graphics/UserInterfaceV2/FormColourPalette.cs @@ -0,0 +1,226 @@ +// 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.Specialized; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.UserInterface; +using osu.Framework.Input.Events; +using osu.Framework.Localisation; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osu.Game.Overlays; +using osu.Game.Resources.Localisation.Web; +using osuTK; + +namespace osu.Game.Graphics.UserInterfaceV2 +{ + public partial class FormColourPalette : CompositeDrawable + { + public BindableList Colours { get; } = new BindableList(); + + public LocalisableString Caption { get; init; } + public LocalisableString HintText { get; init; } + + private Box background = null!; + private FormFieldCaption caption = null!; + private FillFlowContainer flow = null!; + + [Resolved] + private OverlayColourProvider colourProvider { get; set; } = null!; + + [BackgroundDependencyLoader] + private void load() + { + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + + Masking = true; + CornerRadius = 5; + + RoundedButton button; + + InternalChildren = new Drawable[] + { + background = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background5, + }, + new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding(9), + Spacing = new Vector2(7), + Direction = FillDirection.Vertical, + Children = new Drawable[] + { + caption = new FormFieldCaption + { + Caption = Caption, + TooltipText = HintText, + }, + flow = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Full, + Spacing = new Vector2(5), + Child = button = new RoundedButton + { + Action = () => Colours.Add(Colour4.White), + Size = new Vector2(70, 25), + Text = "+", + } + } + }, + }, + }; + + flow.SetLayoutPosition(button, float.MaxValue); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + Colours.BindCollectionChanged((_, args) => + { + if (args.Action != NotifyCollectionChangedAction.Replace) + updateColours(); + }, true); + updateState(); + } + + protected override bool OnHover(HoverEvent e) + { + updateState(); + return true; + } + + protected override void OnHoverLost(HoverLostEvent e) + { + base.OnHoverLost(e); + updateState(); + } + + private void updateState() + { + background.Colour = colourProvider.Background5; + caption.Colour = colourProvider.Content2; + + BorderThickness = IsHovered ? 2 : 0; + + if (IsHovered) + BorderColour = colourProvider.Light4; + } + + private void updateColours() + { + flow.RemoveAll(d => d is ColourButton, true); + + for (int i = 0; i < Colours.Count; ++i) + { + // copy to avoid accesses to modified closure. + int colourIndex = i; + var colourButton = new ColourButton { Current = { Value = Colours[colourIndex] } }; + colourButton.Current.BindValueChanged(colour => Colours[colourIndex] = colour.NewValue); + colourButton.DeleteRequested = () => Colours.RemoveAt(flow.IndexOf(colourButton)); + flow.Add(colourButton); + } + } + + private partial class ColourButton : OsuClickableContainer, IHasPopover, IHasContextMenu + { + public Bindable Current { get; } = new Bindable(); + public Action? DeleteRequested { get; set; } + + private Box background = null!; + private OsuSpriteText hexCode = null!; + + [BackgroundDependencyLoader] + private void load() + { + Size = new Vector2(70, 25); + + Masking = true; + CornerRadius = 3; + Action = this.ShowPopover; + + Children = new Drawable[] + { + background = new Box + { + RelativeSizeAxes = Axes.Both, + }, + hexCode = new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }, + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + Current.BindValueChanged(_ => updateState(), true); + } + + public Popover GetPopover() => new ColourPickerPopover + { + Current = { BindTarget = Current } + }; + + public MenuItem[] ContextMenuItems => new MenuItem[] + { + new OsuMenuItem(CommonStrings.ButtonsDelete, MenuItemType.Destructive, () => DeleteRequested?.Invoke()) + }; + + private void updateState() + { + background.Colour = Current.Value; + hexCode.Text = Current.Value.ToHex(); + hexCode.Colour = OsuColour.ForegroundTextColourFor(Current.Value); + } + } + + private partial class ColourPickerPopover : OsuPopover, IHasCurrentValue + { + public Bindable Current + { + get => current.Current; + set => current.Current = value; + } + + private readonly BindableWithCurrent current = new BindableWithCurrent(); + + public ColourPickerPopover() + : base(false) + { + } + + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider) + { + Child = new OsuColourPicker + { + Current = { BindTarget = Current } + }; + + Body.BorderThickness = 2; + Body.BorderColour = colourProvider.Highlight1; + Content.Padding = new MarginPadding(2); + } + } + } +} From b15608343b8d201c77ffeafd5081faead2257d09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 28 Aug 2024 12:17:39 +0200 Subject: [PATCH 2454/2493] Replace setup screen controls with new "form" controls --- .../Edit/Setup/ManiaDifficultySection.cs | 69 ++++++----- .../Edit/Setup/OsuDifficultySection.cs | 88 +++++++------- .../Edit/Setup/TaikoDifficultySection.cs | 52 ++++----- .../Visual/Editing/TestSceneDesignSection.cs | 6 +- .../Editing/TestSceneMetadataSection.cs | 12 +- .../UserInterfaceV2/FormColourPalette.cs | 2 +- .../UserInterfaceV2/FormFileSelector.cs | 6 +- osu.Game/Screens/Edit/Setup/ColoursSection.cs | 8 +- osu.Game/Screens/Edit/Setup/DesignSection.cs | 60 +++++----- .../Screens/Edit/Setup/DifficultySection.cs | 76 ++++++------ .../Edit/Setup/LabelledRomanisedTextBox.cs | 22 ---- .../Screens/Edit/Setup/MetadataSection.cs | 60 +++++----- .../Screens/Edit/Setup/ResourcesSection.cs | 35 ++---- osu.Game/Screens/Edit/Setup/SetupScreen.cs | 110 +++++++++++------- osu.Game/Screens/Edit/Setup/SetupSection.cs | 17 +-- 15 files changed, 306 insertions(+), 317 deletions(-) delete mode 100644 osu.Game/Screens/Edit/Setup/LabelledRomanisedTextBox.cs diff --git a/osu.Game.Rulesets.Mania/Edit/Setup/ManiaDifficultySection.cs b/osu.Game.Rulesets.Mania/Edit/Setup/ManiaDifficultySection.cs index 7168504309..ed1de591f6 100644 --- a/osu.Game.Rulesets.Mania/Edit/Setup/ManiaDifficultySection.cs +++ b/osu.Game.Rulesets.Mania/Edit/Setup/ManiaDifficultySection.cs @@ -19,12 +19,12 @@ namespace osu.Game.Rulesets.Mania.Edit.Setup { public override LocalisableString Title => EditorSetupStrings.DifficultyHeader; - private LabelledSliderBar keyCountSlider { get; set; } = null!; - private LabelledSwitchButton specialStyle { get; set; } = null!; - private LabelledSliderBar healthDrainSlider { get; set; } = null!; - private LabelledSliderBar overallDifficultySlider { get; set; } = null!; - private LabelledSliderBar baseVelocitySlider { get; set; } = null!; - private LabelledSliderBar tickRateSlider { get; set; } = null!; + private FormSliderBar keyCountSlider { get; set; } = null!; + private FormCheckBox specialStyle { get; set; } = null!; + private FormSliderBar healthDrainSlider { get; set; } = null!; + private FormSliderBar overallDifficultySlider { get; set; } = null!; + private FormSliderBar baseVelocitySlider { get; set; } = null!; + private FormSliderBar tickRateSlider { get; set; } = null!; [Resolved] private Editor? editor { get; set; } @@ -37,77 +37,76 @@ namespace osu.Game.Rulesets.Mania.Edit.Setup { Children = new Drawable[] { - keyCountSlider = new LabelledSliderBar + keyCountSlider = new FormSliderBar { - Label = BeatmapsetsStrings.ShowStatsCsMania, - FixedLabelWidth = LABEL_WIDTH, - Description = "The number of columns in the beatmap", + Caption = BeatmapsetsStrings.ShowStatsCsMania, + HintText = "The number of columns in the beatmap", Current = new BindableFloat(Beatmap.Difficulty.CircleSize) { Default = BeatmapDifficulty.DEFAULT_DIFFICULTY, MinValue = 0, MaxValue = 10, Precision = 1, - } + }, + TabbableContentContainer = this, }, - specialStyle = new LabelledSwitchButton + specialStyle = new FormCheckBox { - Label = "Use special (N+1) style", - FixedLabelWidth = LABEL_WIDTH, - Description = "Changes one column to act as a classic \"scratch\" or \"special\" column, which can be moved around by the user's skin (to the left/right/centre). Generally used in 6K (5+1) or 8K (7+1) configurations.", + Caption = "Use special (N+1) style", + HintText = "Changes one column to act as a classic \"scratch\" or \"special\" column, which can be moved around by the user's skin (to the left/right/centre). Generally used in 6K (5+1) or 8K (7+1) configurations.", Current = { Value = Beatmap.BeatmapInfo.SpecialStyle } }, - healthDrainSlider = new LabelledSliderBar + healthDrainSlider = new FormSliderBar { - Label = BeatmapsetsStrings.ShowStatsDrain, - FixedLabelWidth = LABEL_WIDTH, - Description = EditorSetupStrings.DrainRateDescription, + Caption = BeatmapsetsStrings.ShowStatsDrain, + HintText = EditorSetupStrings.DrainRateDescription, Current = new BindableFloat(Beatmap.Difficulty.DrainRate) { Default = BeatmapDifficulty.DEFAULT_DIFFICULTY, MinValue = 0, MaxValue = 10, Precision = 0.1f, - } + }, + TabbableContentContainer = this, }, - overallDifficultySlider = new LabelledSliderBar + overallDifficultySlider = new FormSliderBar { - Label = BeatmapsetsStrings.ShowStatsAccuracy, - FixedLabelWidth = LABEL_WIDTH, - Description = EditorSetupStrings.OverallDifficultyDescription, + Caption = BeatmapsetsStrings.ShowStatsAccuracy, + HintText = EditorSetupStrings.OverallDifficultyDescription, Current = new BindableFloat(Beatmap.Difficulty.OverallDifficulty) { Default = BeatmapDifficulty.DEFAULT_DIFFICULTY, MinValue = 0, MaxValue = 10, Precision = 0.1f, - } + }, + TabbableContentContainer = this, }, - baseVelocitySlider = new LabelledSliderBar + baseVelocitySlider = new FormSliderBar { - Label = EditorSetupStrings.BaseVelocity, - FixedLabelWidth = LABEL_WIDTH, - Description = EditorSetupStrings.BaseVelocityDescription, + Caption = EditorSetupStrings.BaseVelocity, + HintText = EditorSetupStrings.BaseVelocityDescription, Current = new BindableDouble(Beatmap.Difficulty.SliderMultiplier) { Default = 1.4, MinValue = 0.4, MaxValue = 3.6, Precision = 0.01f, - } + }, + TabbableContentContainer = this, }, - tickRateSlider = new LabelledSliderBar + tickRateSlider = new FormSliderBar { - Label = EditorSetupStrings.TickRate, - FixedLabelWidth = LABEL_WIDTH, - Description = EditorSetupStrings.TickRateDescription, + Caption = EditorSetupStrings.TickRate, + HintText = EditorSetupStrings.TickRateDescription, Current = new BindableDouble(Beatmap.Difficulty.SliderTickRate) { Default = 1, MinValue = 1, MaxValue = 4, Precision = 1, - } + }, + TabbableContentContainer = this, }, }; diff --git a/osu.Game.Rulesets.Osu/Edit/Setup/OsuDifficultySection.cs b/osu.Game.Rulesets.Osu/Edit/Setup/OsuDifficultySection.cs index b61faa0ae9..7008c87d41 100644 --- a/osu.Game.Rulesets.Osu/Edit/Setup/OsuDifficultySection.cs +++ b/osu.Game.Rulesets.Osu/Edit/Setup/OsuDifficultySection.cs @@ -16,13 +16,13 @@ namespace osu.Game.Rulesets.Osu.Edit.Setup { public partial class OsuDifficultySection : SetupSection { - private LabelledSliderBar circleSizeSlider { get; set; } = null!; - private LabelledSliderBar healthDrainSlider { get; set; } = null!; - private LabelledSliderBar approachRateSlider { get; set; } = null!; - private LabelledSliderBar overallDifficultySlider { get; set; } = null!; - private LabelledSliderBar baseVelocitySlider { get; set; } = null!; - private LabelledSliderBar tickRateSlider { get; set; } = null!; - private LabelledSliderBar stackLeniency { get; set; } = null!; + private FormSliderBar circleSizeSlider { get; set; } = null!; + private FormSliderBar healthDrainSlider { get; set; } = null!; + private FormSliderBar approachRateSlider { get; set; } = null!; + private FormSliderBar overallDifficultySlider { get; set; } = null!; + private FormSliderBar baseVelocitySlider { get; set; } = null!; + private FormSliderBar tickRateSlider { get; set; } = null!; + private FormSliderBar stackLeniency { get; set; } = null!; public override LocalisableString Title => EditorSetupStrings.DifficultyHeader; @@ -31,103 +31,103 @@ namespace osu.Game.Rulesets.Osu.Edit.Setup { Children = new Drawable[] { - circleSizeSlider = new LabelledSliderBar + circleSizeSlider = new FormSliderBar { - Label = BeatmapsetsStrings.ShowStatsCs, - FixedLabelWidth = LABEL_WIDTH, - Description = EditorSetupStrings.CircleSizeDescription, + Caption = BeatmapsetsStrings.ShowStatsCs, + HintText = EditorSetupStrings.CircleSizeDescription, Current = new BindableFloat(Beatmap.Difficulty.CircleSize) { Default = BeatmapDifficulty.DEFAULT_DIFFICULTY, MinValue = 0, MaxValue = 10, Precision = 0.1f, - } + }, + TabbableContentContainer = this, }, - healthDrainSlider = new LabelledSliderBar + healthDrainSlider = new FormSliderBar { - Label = BeatmapsetsStrings.ShowStatsDrain, - FixedLabelWidth = LABEL_WIDTH, - Description = EditorSetupStrings.DrainRateDescription, + Caption = BeatmapsetsStrings.ShowStatsDrain, + HintText = EditorSetupStrings.DrainRateDescription, Current = new BindableFloat(Beatmap.Difficulty.DrainRate) { Default = BeatmapDifficulty.DEFAULT_DIFFICULTY, MinValue = 0, MaxValue = 10, Precision = 0.1f, - } + }, + TabbableContentContainer = this, }, - approachRateSlider = new LabelledSliderBar + approachRateSlider = new FormSliderBar { - Label = BeatmapsetsStrings.ShowStatsAr, - FixedLabelWidth = LABEL_WIDTH, - Description = EditorSetupStrings.ApproachRateDescription, + Caption = BeatmapsetsStrings.ShowStatsAr, + HintText = EditorSetupStrings.ApproachRateDescription, Current = new BindableFloat(Beatmap.Difficulty.ApproachRate) { Default = BeatmapDifficulty.DEFAULT_DIFFICULTY, MinValue = 0, MaxValue = 10, Precision = 0.1f, - } + }, + TabbableContentContainer = this, }, - overallDifficultySlider = new LabelledSliderBar + overallDifficultySlider = new FormSliderBar { - Label = BeatmapsetsStrings.ShowStatsAccuracy, - FixedLabelWidth = LABEL_WIDTH, - Description = EditorSetupStrings.OverallDifficultyDescription, + Caption = BeatmapsetsStrings.ShowStatsAccuracy, + HintText = EditorSetupStrings.OverallDifficultyDescription, Current = new BindableFloat(Beatmap.Difficulty.OverallDifficulty) { Default = BeatmapDifficulty.DEFAULT_DIFFICULTY, MinValue = 0, MaxValue = 10, Precision = 0.1f, - } + }, + TabbableContentContainer = this, }, - baseVelocitySlider = new LabelledSliderBar + baseVelocitySlider = new FormSliderBar { - Label = EditorSetupStrings.BaseVelocity, - FixedLabelWidth = LABEL_WIDTH, - Description = EditorSetupStrings.BaseVelocityDescription, + Caption = EditorSetupStrings.BaseVelocity, + HintText = EditorSetupStrings.BaseVelocityDescription, Current = new BindableDouble(Beatmap.Difficulty.SliderMultiplier) { Default = 1.4, MinValue = 0.4, MaxValue = 3.6, Precision = 0.01f, - } + }, + TabbableContentContainer = this, }, - tickRateSlider = new LabelledSliderBar + tickRateSlider = new FormSliderBar { - Label = EditorSetupStrings.TickRate, - FixedLabelWidth = LABEL_WIDTH, - Description = EditorSetupStrings.TickRateDescription, + Caption = EditorSetupStrings.TickRate, + HintText = EditorSetupStrings.TickRateDescription, Current = new BindableDouble(Beatmap.Difficulty.SliderTickRate) { Default = 1, MinValue = 1, MaxValue = 4, Precision = 1, - } + }, + TabbableContentContainer = this, }, - stackLeniency = new LabelledSliderBar + stackLeniency = new FormSliderBar { - Label = "Stack Leniency", - FixedLabelWidth = LABEL_WIDTH, - Description = "In play mode, osu! automatically stacks notes which occur at the same location. Increasing this value means it is more likely to snap notes of further time-distance.", + Caption = "Stack Leniency", + HintText = "In play mode, osu! automatically stacks notes which occur at the same location. Increasing this value means it is more likely to snap notes of further time-distance.", Current = new BindableFloat(Beatmap.BeatmapInfo.StackLeniency) { Default = 0.7f, MinValue = 0, MaxValue = 1, Precision = 0.1f - } + }, + TabbableContentContainer = this, }, }; - foreach (var item in Children.OfType>()) + foreach (var item in Children.OfType>()) item.Current.ValueChanged += _ => updateValues(); - foreach (var item in Children.OfType>()) + foreach (var item in Children.OfType>()) item.Current.ValueChanged += _ => updateValues(); } diff --git a/osu.Game.Rulesets.Taiko/Edit/Setup/TaikoDifficultySection.cs b/osu.Game.Rulesets.Taiko/Edit/Setup/TaikoDifficultySection.cs index 2aaa16ee0b..e191169929 100644 --- a/osu.Game.Rulesets.Taiko/Edit/Setup/TaikoDifficultySection.cs +++ b/osu.Game.Rulesets.Taiko/Edit/Setup/TaikoDifficultySection.cs @@ -16,10 +16,10 @@ namespace osu.Game.Rulesets.Taiko.Edit.Setup { public partial class TaikoDifficultySection : SetupSection { - private LabelledSliderBar healthDrainSlider { get; set; } = null!; - private LabelledSliderBar overallDifficultySlider { get; set; } = null!; - private LabelledSliderBar baseVelocitySlider { get; set; } = null!; - private LabelledSliderBar tickRateSlider { get; set; } = null!; + private FormSliderBar healthDrainSlider { get; set; } = null!; + private FormSliderBar overallDifficultySlider { get; set; } = null!; + private FormSliderBar baseVelocitySlider { get; set; } = null!; + private FormSliderBar tickRateSlider { get; set; } = null!; public override LocalisableString Title => EditorSetupStrings.DifficultyHeader; @@ -28,64 +28,64 @@ namespace osu.Game.Rulesets.Taiko.Edit.Setup { Children = new Drawable[] { - healthDrainSlider = new LabelledSliderBar + healthDrainSlider = new FormSliderBar { - Label = BeatmapsetsStrings.ShowStatsDrain, - FixedLabelWidth = LABEL_WIDTH, - Description = EditorSetupStrings.DrainRateDescription, + Caption = BeatmapsetsStrings.ShowStatsDrain, + HintText = EditorSetupStrings.DrainRateDescription, Current = new BindableFloat(Beatmap.Difficulty.DrainRate) { Default = BeatmapDifficulty.DEFAULT_DIFFICULTY, MinValue = 0, MaxValue = 10, Precision = 0.1f, - } + }, + TabbableContentContainer = this, }, - overallDifficultySlider = new LabelledSliderBar + overallDifficultySlider = new FormSliderBar { - Label = BeatmapsetsStrings.ShowStatsAccuracy, - FixedLabelWidth = LABEL_WIDTH, - Description = EditorSetupStrings.OverallDifficultyDescription, + Caption = BeatmapsetsStrings.ShowStatsAccuracy, + HintText = EditorSetupStrings.OverallDifficultyDescription, Current = new BindableFloat(Beatmap.Difficulty.OverallDifficulty) { Default = BeatmapDifficulty.DEFAULT_DIFFICULTY, MinValue = 0, MaxValue = 10, Precision = 0.1f, - } + }, + TabbableContentContainer = this, }, - baseVelocitySlider = new LabelledSliderBar + baseVelocitySlider = new FormSliderBar { - Label = EditorSetupStrings.BaseVelocity, - FixedLabelWidth = LABEL_WIDTH, - Description = EditorSetupStrings.BaseVelocityDescription, + Caption = EditorSetupStrings.BaseVelocity, + HintText = EditorSetupStrings.BaseVelocityDescription, Current = new BindableDouble(Beatmap.Difficulty.SliderMultiplier) { Default = 1.4, MinValue = 0.4, MaxValue = 3.6, Precision = 0.01f, - } + }, + TabbableContentContainer = this, }, - tickRateSlider = new LabelledSliderBar + tickRateSlider = new FormSliderBar() { - Label = EditorSetupStrings.TickRate, - FixedLabelWidth = LABEL_WIDTH, - Description = EditorSetupStrings.TickRateDescription, + Caption = EditorSetupStrings.TickRate, + HintText = EditorSetupStrings.TickRateDescription, Current = new BindableDouble(Beatmap.Difficulty.SliderTickRate) { Default = 1, MinValue = 1, MaxValue = 4, Precision = 1, - } + }, + TabbableContentContainer = this, }, }; - foreach (var item in Children.OfType>()) + foreach (var item in Children.OfType>()) item.Current.ValueChanged += _ => updateValues(); - foreach (var item in Children.OfType>()) + foreach (var item in Children.OfType>()) item.Current.ValueChanged += _ => updateValues(); } diff --git a/osu.Game.Tests/Visual/Editing/TestSceneDesignSection.cs b/osu.Game.Tests/Visual/Editing/TestSceneDesignSection.cs index 9a66e1676d..143547dfc9 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneDesignSection.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneDesignSection.cs @@ -99,11 +99,11 @@ namespace osu.Game.Tests.Visual.Editing private partial class TestDesignSection : DesignSection { - public new LabelledSwitchButton EnableCountdown => base.EnableCountdown; + public new FormCheckBox EnableCountdown => base.EnableCountdown; public new FillFlowContainer CountdownSettings => base.CountdownSettings; - public new LabelledEnumDropdown CountdownSpeed => base.CountdownSpeed; - public new LabelledNumberBox CountdownOffset => base.CountdownOffset; + public new FormEnumDropdown CountdownSpeed => base.CountdownSpeed; + public new FormTextBox CountdownOffset => base.CountdownOffset; } } } diff --git a/osu.Game.Tests/Visual/Editing/TestSceneMetadataSection.cs b/osu.Game.Tests/Visual/Editing/TestSceneMetadataSection.cs index 8b6f31d599..653e3e7ff9 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneMetadataSection.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneMetadataSection.cs @@ -212,7 +212,7 @@ namespace osu.Game.Tests.Visual.Editing private void assertRomanisedArtist(string expected, bool editable) { AddAssert($"romanised artist is {expected}", () => metadataSection.RomanisedArtistTextBox.Current.Value, () => Is.EqualTo(expected)); - AddAssert($"romanised artist is {(editable ? "" : "not ")}editable", () => metadataSection.RomanisedArtistTextBox.ReadOnly == !editable); + AddAssert($"romanised artist is {(editable ? "" : "not ")}editable", () => metadataSection.RomanisedArtistTextBox.Current.Disabled == !editable); } private void assertTitle(string expected) @@ -221,16 +221,16 @@ namespace osu.Game.Tests.Visual.Editing private void assertRomanisedTitle(string expected, bool editable) { AddAssert($"romanised title is {expected}", () => metadataSection.RomanisedTitleTextBox.Current.Value, () => Is.EqualTo(expected)); - AddAssert($"romanised title is {(editable ? "" : "not ")}editable", () => metadataSection.RomanisedTitleTextBox.ReadOnly == !editable); + AddAssert($"romanised title is {(editable ? "" : "not ")}editable", () => metadataSection.RomanisedTitleTextBox.Current.Disabled == !editable); } private partial class TestMetadataSection : MetadataSection { - public new LabelledTextBox ArtistTextBox => base.ArtistTextBox; - public new LabelledTextBox RomanisedArtistTextBox => base.RomanisedArtistTextBox; + public new FormTextBox ArtistTextBox => base.ArtistTextBox; + public new FormTextBox RomanisedArtistTextBox => base.RomanisedArtistTextBox; - public new LabelledTextBox TitleTextBox => base.TitleTextBox; - public new LabelledTextBox RomanisedTitleTextBox => base.RomanisedTitleTextBox; + public new FormTextBox TitleTextBox => base.TitleTextBox; + public new FormTextBox RomanisedTitleTextBox => base.RomanisedTitleTextBox; } } } diff --git a/osu.Game/Graphics/UserInterfaceV2/FormColourPalette.cs b/osu.Game/Graphics/UserInterfaceV2/FormColourPalette.cs index a2c1c1622e..672df1d0a5 100644 --- a/osu.Game/Graphics/UserInterfaceV2/FormColourPalette.cs +++ b/osu.Game/Graphics/UserInterfaceV2/FormColourPalette.cs @@ -133,7 +133,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 int colourIndex = i; var colourButton = new ColourButton { Current = { Value = Colours[colourIndex] } }; colourButton.Current.BindValueChanged(colour => Colours[colourIndex] = colour.NewValue); - colourButton.DeleteRequested = () => Colours.RemoveAt(flow.IndexOf(colourButton)); + colourButton.DeleteRequested = () => Colours.RemoveAt(colourIndex); flow.Add(colourButton); } } diff --git a/osu.Game/Graphics/UserInterfaceV2/FormFileSelector.cs b/osu.Game/Graphics/UserInterfaceV2/FormFileSelector.cs index 42bf9c7b9f..f5b6cb3e64 100644 --- a/osu.Game/Graphics/UserInterfaceV2/FormFileSelector.cs +++ b/osu.Game/Graphics/UserInterfaceV2/FormFileSelector.cs @@ -148,12 +148,12 @@ namespace osu.Game.Graphics.UserInterfaceV2 base.LoadComplete(); popoverState.BindValueChanged(_ => updateState()); + current.BindDisabledChanged(_ => updateState()); current.BindValueChanged(_ => { updateState(); onFileSelected(); - }); - current.BindDisabledChanged(_ => updateState(), true); + }, true); game.RegisterImportHandler(this); } @@ -189,7 +189,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 private void updateState() { caption.Colour = Current.Disabled ? colourProvider.Foreground1 : colourProvider.Content2; - filenameText.Colour = Current.Disabled ? colourProvider.Foreground1 : colourProvider.Content1; + filenameText.Colour = Current.Disabled || Current.Value == null ? colourProvider.Foreground1 : colourProvider.Content1; if (!Current.Disabled) { diff --git a/osu.Game/Screens/Edit/Setup/ColoursSection.cs b/osu.Game/Screens/Edit/Setup/ColoursSection.cs index a5d79b5b52..01ca114e4f 100644 --- a/osu.Game/Screens/Edit/Setup/ColoursSection.cs +++ b/osu.Game/Screens/Edit/Setup/ColoursSection.cs @@ -13,18 +13,16 @@ namespace osu.Game.Screens.Edit.Setup { public override LocalisableString Title => EditorSetupStrings.ColoursHeader; - private LabelledColourPalette comboColours = null!; + private FormColourPalette comboColours = null!; [BackgroundDependencyLoader] private void load() { Children = new Drawable[] { - comboColours = new LabelledColourPalette + comboColours = new FormColourPalette { - Label = EditorSetupStrings.HitCircleSliderCombos, - FixedLabelWidth = LABEL_WIDTH, - ColourNamePrefix = EditorSetupStrings.ComboColourPrefix + Caption = EditorSetupStrings.HitCircleSliderCombos, } }; diff --git a/osu.Game/Screens/Edit/Setup/DesignSection.cs b/osu.Game/Screens/Edit/Setup/DesignSection.cs index b05a073146..e3c01fc9bf 100644 --- a/osu.Game/Screens/Edit/Setup/DesignSection.cs +++ b/osu.Game/Screens/Edit/Setup/DesignSection.cs @@ -17,75 +17,75 @@ namespace osu.Game.Screens.Edit.Setup { internal partial class DesignSection : SetupSection { - protected LabelledSwitchButton EnableCountdown = null!; + protected FormCheckBox EnableCountdown = null!; protected FillFlowContainer CountdownSettings = null!; - protected LabelledEnumDropdown CountdownSpeed = null!; - protected LabelledNumberBox CountdownOffset = null!; + protected FormEnumDropdown CountdownSpeed = null!; + protected FormNumberBox CountdownOffset = null!; - private LabelledSwitchButton widescreenSupport = null!; - private LabelledSwitchButton epilepsyWarning = null!; - private LabelledSwitchButton letterboxDuringBreaks = null!; - private LabelledSwitchButton samplesMatchPlaybackRate = null!; + private FormCheckBox widescreenSupport = null!; + private FormCheckBox epilepsyWarning = null!; + private FormCheckBox letterboxDuringBreaks = null!; + private FormCheckBox samplesMatchPlaybackRate = null!; public override LocalisableString Title => EditorSetupStrings.DesignHeader; [BackgroundDependencyLoader] private void load() { - Children = new[] + Children = new Drawable[] { - EnableCountdown = new LabelledSwitchButton + EnableCountdown = new FormCheckBox { - Label = EditorSetupStrings.EnableCountdown, + Caption = EditorSetupStrings.EnableCountdown, + HintText = EditorSetupStrings.CountdownDescription, Current = { Value = Beatmap.BeatmapInfo.Countdown != CountdownType.None }, - Description = EditorSetupStrings.CountdownDescription }, CountdownSettings = new FillFlowContainer { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Spacing = new Vector2(10), + Spacing = new Vector2(5), Direction = FillDirection.Vertical, Children = new Drawable[] { - CountdownSpeed = new LabelledEnumDropdown + CountdownSpeed = new FormEnumDropdown { - Label = EditorSetupStrings.CountdownSpeed, + Caption = EditorSetupStrings.CountdownSpeed, Current = { Value = Beatmap.BeatmapInfo.Countdown != CountdownType.None ? Beatmap.BeatmapInfo.Countdown : CountdownType.Normal }, Items = Enum.GetValues().Where(type => type != CountdownType.None) }, - CountdownOffset = new LabelledNumberBox + CountdownOffset = new FormNumberBox { - Label = EditorSetupStrings.CountdownOffset, + Caption = EditorSetupStrings.CountdownOffset, + HintText = EditorSetupStrings.CountdownOffsetDescription, Current = { Value = Beatmap.BeatmapInfo.CountdownOffset.ToString() }, - Description = EditorSetupStrings.CountdownOffsetDescription, + TabbableContentContainer = this, } } }, - Empty(), - widescreenSupport = new LabelledSwitchButton + widescreenSupport = new FormCheckBox { - Label = EditorSetupStrings.WidescreenSupport, - Description = EditorSetupStrings.WidescreenSupportDescription, + Caption = EditorSetupStrings.WidescreenSupport, + HintText = EditorSetupStrings.WidescreenSupportDescription, Current = { Value = Beatmap.BeatmapInfo.WidescreenStoryboard } }, - epilepsyWarning = new LabelledSwitchButton + epilepsyWarning = new FormCheckBox { - Label = EditorSetupStrings.EpilepsyWarning, - Description = EditorSetupStrings.EpilepsyWarningDescription, + Caption = EditorSetupStrings.EpilepsyWarning, + HintText = EditorSetupStrings.EpilepsyWarningDescription, Current = { Value = Beatmap.BeatmapInfo.EpilepsyWarning } }, - letterboxDuringBreaks = new LabelledSwitchButton + letterboxDuringBreaks = new FormCheckBox { - Label = EditorSetupStrings.LetterboxDuringBreaks, - Description = EditorSetupStrings.LetterboxDuringBreaksDescription, + Caption = EditorSetupStrings.LetterboxDuringBreaks, + HintText = EditorSetupStrings.LetterboxDuringBreaksDescription, Current = { Value = Beatmap.BeatmapInfo.LetterboxInBreaks } }, - samplesMatchPlaybackRate = new LabelledSwitchButton + samplesMatchPlaybackRate = new FormCheckBox { - Label = EditorSetupStrings.SamplesMatchPlaybackRate, - Description = EditorSetupStrings.SamplesMatchPlaybackRateDescription, + Caption = EditorSetupStrings.SamplesMatchPlaybackRate, + HintText = EditorSetupStrings.SamplesMatchPlaybackRateDescription, Current = { Value = Beatmap.BeatmapInfo.SamplesMatchPlaybackRate } } }; diff --git a/osu.Game/Screens/Edit/Setup/DifficultySection.cs b/osu.Game/Screens/Edit/Setup/DifficultySection.cs index b9ba2d9cb7..a27a7258c7 100644 --- a/osu.Game/Screens/Edit/Setup/DifficultySection.cs +++ b/osu.Game/Screens/Edit/Setup/DifficultySection.cs @@ -15,12 +15,12 @@ namespace osu.Game.Screens.Edit.Setup { public partial class DifficultySection : SetupSection { - private LabelledSliderBar circleSizeSlider { get; set; } = null!; - private LabelledSliderBar healthDrainSlider { get; set; } = null!; - private LabelledSliderBar approachRateSlider { get; set; } = null!; - private LabelledSliderBar overallDifficultySlider { get; set; } = null!; - private LabelledSliderBar baseVelocitySlider { get; set; } = null!; - private LabelledSliderBar tickRateSlider { get; set; } = null!; + private FormSliderBar circleSizeSlider { get; set; } = null!; + private FormSliderBar healthDrainSlider { get; set; } = null!; + private FormSliderBar approachRateSlider { get; set; } = null!; + private FormSliderBar overallDifficultySlider { get; set; } = null!; + private FormSliderBar baseVelocitySlider { get; set; } = null!; + private FormSliderBar tickRateSlider { get; set; } = null!; public override LocalisableString Title => EditorSetupStrings.DifficultyHeader; @@ -29,90 +29,90 @@ namespace osu.Game.Screens.Edit.Setup { Children = new Drawable[] { - circleSizeSlider = new LabelledSliderBar + circleSizeSlider = new FormSliderBar { - Label = BeatmapsetsStrings.ShowStatsCs, - FixedLabelWidth = LABEL_WIDTH, - Description = EditorSetupStrings.CircleSizeDescription, + Caption = BeatmapsetsStrings.ShowStatsCs, + HintText = EditorSetupStrings.CircleSizeDescription, Current = new BindableFloat(Beatmap.Difficulty.CircleSize) { Default = BeatmapDifficulty.DEFAULT_DIFFICULTY, MinValue = 0, MaxValue = 10, Precision = 0.1f, - } + }, + TabbableContentContainer = this, }, - healthDrainSlider = new LabelledSliderBar + healthDrainSlider = new FormSliderBar { - Label = BeatmapsetsStrings.ShowStatsDrain, - FixedLabelWidth = LABEL_WIDTH, - Description = EditorSetupStrings.DrainRateDescription, + Caption = BeatmapsetsStrings.ShowStatsDrain, + HintText = EditorSetupStrings.DrainRateDescription, Current = new BindableFloat(Beatmap.Difficulty.DrainRate) { Default = BeatmapDifficulty.DEFAULT_DIFFICULTY, MinValue = 0, MaxValue = 10, Precision = 0.1f, - } + }, + TabbableContentContainer = this, }, - approachRateSlider = new LabelledSliderBar + approachRateSlider = new FormSliderBar { - Label = BeatmapsetsStrings.ShowStatsAr, - FixedLabelWidth = LABEL_WIDTH, - Description = EditorSetupStrings.ApproachRateDescription, + Caption = BeatmapsetsStrings.ShowStatsAr, + HintText = EditorSetupStrings.ApproachRateDescription, Current = new BindableFloat(Beatmap.Difficulty.ApproachRate) { Default = BeatmapDifficulty.DEFAULT_DIFFICULTY, MinValue = 0, MaxValue = 10, Precision = 0.1f, - } + }, + TabbableContentContainer = this, }, - overallDifficultySlider = new LabelledSliderBar + overallDifficultySlider = new FormSliderBar { - Label = BeatmapsetsStrings.ShowStatsAccuracy, - FixedLabelWidth = LABEL_WIDTH, - Description = EditorSetupStrings.OverallDifficultyDescription, + Caption = BeatmapsetsStrings.ShowStatsAccuracy, + HintText = EditorSetupStrings.OverallDifficultyDescription, Current = new BindableFloat(Beatmap.Difficulty.OverallDifficulty) { Default = BeatmapDifficulty.DEFAULT_DIFFICULTY, MinValue = 0, MaxValue = 10, Precision = 0.1f, - } + }, + TabbableContentContainer = this, }, - baseVelocitySlider = new LabelledSliderBar + baseVelocitySlider = new FormSliderBar { - Label = EditorSetupStrings.BaseVelocity, - FixedLabelWidth = LABEL_WIDTH, - Description = EditorSetupStrings.BaseVelocityDescription, + Caption = EditorSetupStrings.BaseVelocity, + HintText = EditorSetupStrings.BaseVelocityDescription, Current = new BindableDouble(Beatmap.Difficulty.SliderMultiplier) { Default = 1.4, MinValue = 0.4, MaxValue = 3.6, Precision = 0.01f, - } + }, + TabbableContentContainer = this, }, - tickRateSlider = new LabelledSliderBar + tickRateSlider = new FormSliderBar { - Label = EditorSetupStrings.TickRate, - FixedLabelWidth = LABEL_WIDTH, - Description = EditorSetupStrings.TickRateDescription, + Caption = EditorSetupStrings.TickRate, + HintText = EditorSetupStrings.TickRateDescription, Current = new BindableDouble(Beatmap.Difficulty.SliderTickRate) { Default = 1, MinValue = 1, MaxValue = 4, Precision = 1, - } + }, + TabbableContentContainer = this, }, }; - foreach (var item in Children.OfType>()) + foreach (var item in Children.OfType>()) item.Current.ValueChanged += _ => updateValues(); - foreach (var item in Children.OfType>()) + foreach (var item in Children.OfType>()) item.Current.ValueChanged += _ => updateValues(); } diff --git a/osu.Game/Screens/Edit/Setup/LabelledRomanisedTextBox.cs b/osu.Game/Screens/Edit/Setup/LabelledRomanisedTextBox.cs deleted file mode 100644 index 85c697bf14..0000000000 --- a/osu.Game/Screens/Edit/Setup/LabelledRomanisedTextBox.cs +++ /dev/null @@ -1,22 +0,0 @@ -// 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.Beatmaps; -using osu.Game.Graphics.UserInterface; -using osu.Game.Graphics.UserInterfaceV2; - -namespace osu.Game.Screens.Edit.Setup -{ - internal partial class LabelledRomanisedTextBox : LabelledTextBox - { - protected override OsuTextBox CreateTextBox() => new RomanisedTextBox(); - - private partial class RomanisedTextBox : OsuTextBox - { - protected override bool AllowIme => false; - - protected override bool CanAddCharacter(char character) - => MetadataUtils.IsRomanised(character); - } - } -} diff --git a/osu.Game/Screens/Edit/Setup/MetadataSection.cs b/osu.Game/Screens/Edit/Setup/MetadataSection.cs index 19071dc806..20c0a74d84 100644 --- a/osu.Game/Screens/Edit/Setup/MetadataSection.cs +++ b/osu.Game/Screens/Edit/Setup/MetadataSection.cs @@ -14,16 +14,16 @@ namespace osu.Game.Screens.Edit.Setup { public partial class MetadataSection : SetupSection { - protected LabelledTextBox ArtistTextBox = null!; - protected LabelledTextBox RomanisedArtistTextBox = null!; + protected FormTextBox ArtistTextBox = null!; + protected FormTextBox RomanisedArtistTextBox = null!; - protected LabelledTextBox TitleTextBox = null!; - protected LabelledTextBox RomanisedTitleTextBox = null!; + protected FormTextBox TitleTextBox = null!; + protected FormTextBox RomanisedTitleTextBox = null!; - private LabelledTextBox creatorTextBox = null!; - private LabelledTextBox difficultyTextBox = null!; - private LabelledTextBox sourceTextBox = null!; - private LabelledTextBox tagsTextBox = null!; + private FormTextBox creatorTextBox = null!; + private FormTextBox difficultyTextBox = null!; + private FormTextBox sourceTextBox = null!; + private FormTextBox tagsTextBox = null!; public override LocalisableString Title => EditorSetupStrings.MetadataHeader; @@ -34,33 +34,26 @@ namespace osu.Game.Screens.Edit.Setup Children = new[] { - ArtistTextBox = createTextBox(EditorSetupStrings.Artist, + ArtistTextBox = createTextBox(EditorSetupStrings.Artist, !string.IsNullOrEmpty(metadata.ArtistUnicode) ? metadata.ArtistUnicode : metadata.Artist), - RomanisedArtistTextBox = createTextBox(EditorSetupStrings.RomanisedArtist, + RomanisedArtistTextBox = createTextBox(EditorSetupStrings.RomanisedArtist, !string.IsNullOrEmpty(metadata.Artist) ? metadata.Artist : MetadataUtils.StripNonRomanisedCharacters(metadata.ArtistUnicode)), - - Empty(), - - TitleTextBox = createTextBox(EditorSetupStrings.Title, + TitleTextBox = createTextBox(EditorSetupStrings.Title, !string.IsNullOrEmpty(metadata.TitleUnicode) ? metadata.TitleUnicode : metadata.Title), - RomanisedTitleTextBox = createTextBox(EditorSetupStrings.RomanisedTitle, + RomanisedTitleTextBox = createTextBox(EditorSetupStrings.RomanisedTitle, !string.IsNullOrEmpty(metadata.Title) ? metadata.Title : MetadataUtils.StripNonRomanisedCharacters(metadata.ArtistUnicode)), - - Empty(), - - creatorTextBox = createTextBox(EditorSetupStrings.Creator, metadata.Author.Username), - difficultyTextBox = createTextBox(EditorSetupStrings.DifficultyName, Beatmap.BeatmapInfo.DifficultyName), - sourceTextBox = createTextBox(BeatmapsetsStrings.ShowInfoSource, metadata.Source), - tagsTextBox = createTextBox(BeatmapsetsStrings.ShowInfoTags, metadata.Tags) + creatorTextBox = createTextBox(EditorSetupStrings.Creator, metadata.Author.Username), + difficultyTextBox = createTextBox(EditorSetupStrings.DifficultyName, Beatmap.BeatmapInfo.DifficultyName), + sourceTextBox = createTextBox(BeatmapsetsStrings.ShowInfoSource, metadata.Source), + tagsTextBox = createTextBox(BeatmapsetsStrings.ShowInfoTags, metadata.Tags) }; } private TTextBox createTextBox(LocalisableString label, string initialValue) - where TTextBox : LabelledTextBox, new() + where TTextBox : FormTextBox, new() => new TTextBox { - Label = label, - FixedLabelWidth = LABEL_WIDTH, + Caption = label, Current = { Value = initialValue }, TabbableContentContainer = this }; @@ -75,13 +68,13 @@ namespace osu.Game.Screens.Edit.Setup ArtistTextBox.Current.BindValueChanged(artist => transferIfRomanised(artist.NewValue, RomanisedArtistTextBox)); TitleTextBox.Current.BindValueChanged(title => transferIfRomanised(title.NewValue, RomanisedTitleTextBox)); - foreach (var item in Children.OfType()) + foreach (var item in Children.OfType()) item.OnCommit += onCommit; updateReadOnlyState(); } - private void transferIfRomanised(string value, LabelledTextBox target) + private void transferIfRomanised(string value, FormTextBox target) { if (MetadataUtils.IsRomanised(value)) target.Current.Value = value; @@ -119,5 +112,18 @@ namespace osu.Game.Screens.Edit.Setup Beatmap.SaveState(); } + + private partial class FormRomanisedTextBox : FormTextBox + { + internal override InnerTextBox CreateTextBox() => new RomanisedTextBox(); + + private partial class RomanisedTextBox : InnerTextBox + { + protected override bool AllowIme => false; + + protected override bool CanAddCharacter(char character) + => MetadataUtils.IsRomanised(character); + } + } } } diff --git a/osu.Game/Screens/Edit/Setup/ResourcesSection.cs b/osu.Game/Screens/Edit/Setup/ResourcesSection.cs index f6d20319cb..3ce9f01b2b 100644 --- a/osu.Game/Screens/Edit/Setup/ResourcesSection.cs +++ b/osu.Game/Screens/Edit/Setup/ResourcesSection.cs @@ -7,6 +7,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Localisation; using osu.Game.Beatmaps; +using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Overlays; using osu.Game.Localisation; @@ -14,8 +15,8 @@ namespace osu.Game.Screens.Edit.Setup { internal partial class ResourcesSection : SetupSection { - private LabelledFileChooser audioTrackChooser = null!; - private LabelledFileChooser backgroundChooser = null!; + private FormFileSelector audioTrackChooser = null!; + private FormFileSelector backgroundChooser = null!; public override LocalisableString Title => EditorSetupStrings.ResourcesHeader; @@ -35,24 +36,22 @@ namespace osu.Game.Screens.Edit.Setup private Editor? editor { get; set; } [Resolved] - private SetupScreenHeader header { get; set; } = null!; + private SetupScreenHeaderBackground headerBackground { get; set; } = null!; [BackgroundDependencyLoader] private void load() { Children = new Drawable[] { - backgroundChooser = new LabelledFileChooser(".jpg", ".jpeg", ".png") + backgroundChooser = new FormFileSelector(".jpg", ".jpeg", ".png") { - Label = GameplaySettingsStrings.BackgroundHeader, - FixedLabelWidth = LABEL_WIDTH, - TabbableContentContainer = this + Caption = GameplaySettingsStrings.BackgroundHeader, + PlaceholderText = EditorSetupStrings.ClickToSelectBackground, }, - audioTrackChooser = new LabelledFileChooser(".mp3", ".ogg") + audioTrackChooser = new FormFileSelector(".mp3", ".ogg") { - Label = EditorSetupStrings.AudioTrack, - FixedLabelWidth = LABEL_WIDTH, - TabbableContentContainer = this + Caption = EditorSetupStrings.AudioTrack, + PlaceholderText = EditorSetupStrings.ClickToSelectTrack, }, }; @@ -64,8 +63,6 @@ namespace osu.Game.Screens.Edit.Setup backgroundChooser.Current.BindValueChanged(backgroundChanged); audioTrackChooser.Current.BindValueChanged(audioTrackChanged); - - updatePlaceholderText(); } public bool ChangeBackgroundImage(FileInfo source) @@ -92,7 +89,7 @@ namespace osu.Game.Screens.Edit.Setup editorBeatmap.SaveState(); working.Value.Metadata.BackgroundFile = destination.Name; - header.Background.UpdateBackground(); + headerBackground.UpdateBackground(); editor?.ApplyToBackground(bg => bg.RefreshBackground()); @@ -132,22 +129,12 @@ namespace osu.Game.Screens.Edit.Setup { if (file.NewValue == null || !ChangeBackgroundImage(file.NewValue)) backgroundChooser.Current.Value = file.OldValue; - - updatePlaceholderText(); } private void audioTrackChanged(ValueChangedEvent file) { if (file.NewValue == null || !ChangeAudioTrack(file.NewValue)) audioTrackChooser.Current.Value = file.OldValue; - - updatePlaceholderText(); - } - - private void updatePlaceholderText() - { - audioTrackChooser.Text = audioTrackChooser.Current.Value?.Name ?? EditorSetupStrings.ClickToSelectTrack; - backgroundChooser.Text = backgroundChooser.Current.Value?.Name ?? EditorSetupStrings.ClickToSelectBackground; } } } diff --git a/osu.Game/Screens/Edit/Setup/SetupScreen.cs b/osu.Game/Screens/Edit/Setup/SetupScreen.cs index 17bbc7daa2..4b9a7a858f 100644 --- a/osu.Game/Screens/Edit/Setup/SetupScreen.cs +++ b/osu.Game/Screens/Edit/Setup/SetupScreen.cs @@ -1,55 +1,97 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Collections.Generic; +using System.Linq; using osu.Framework.Allocation; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Screens; using osu.Game.Graphics.Containers; using osu.Game.Overlays; +using osuTK; namespace osu.Game.Screens.Edit.Setup { public partial class SetupScreen : EditorScreen { - [Cached] - private SectionsContainer sections { get; } = new SetupScreenSectionsContainer(); - - [Cached] - private SetupScreenHeader header = new SetupScreenHeader(); - public SetupScreen() : base(EditorScreenMode.SongSetup) { } + [Cached] + private SetupScreenHeaderBackground background = new SetupScreenHeaderBackground { RelativeSizeAxes = Axes.Both, }; + [BackgroundDependencyLoader] private void load(EditorBeatmap beatmap, OverlayColourProvider colourProvider) { var ruleset = beatmap.BeatmapInfo.Ruleset.CreateInstance(); - List sectionsEnumerable = - [ - new ResourcesSection(), - new MetadataSection() - ]; - - sectionsEnumerable.AddRange(ruleset.CreateEditorSetupSections()); - sectionsEnumerable.Add(new DesignSection()); - - Add(new Box + Children = new Drawable[] { - Colour = colourProvider.Background3, - RelativeSizeAxes = Axes.Both, - }); - - Add(sections.With(s => - { - s.RelativeSizeAxes = Axes.Both; - s.ChildrenEnumerable = sectionsEnumerable; - s.FixedHeader = header; - })); + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background3, + }, + new GridContainer + { + RelativeSizeAxes = Axes.Both, + RowDimensions = + [ + new Dimension(GridSizeMode.Absolute, 110), + new Dimension() + ], + Content = new[] + { + new Drawable[] + { + background, + }, + new Drawable[] + { + new OsuScrollContainer + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding(15), + Child = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Full, + Spacing = new Vector2(28), + Children = new Drawable[] + { + new FillFlowContainer + { + Width = 450, + AutoSizeAxes = Axes.Y, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Spacing = new Vector2(25), + Children = new Drawable[] + { + new ResourcesSection(), + new MetadataSection(), + } + }, + new FillFlowContainer + { + Width = 450, + AutoSizeAxes = Axes.Y, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Spacing = new Vector2(25), + ChildrenEnumerable = ruleset.CreateEditorSetupSections().Append(new DesignSection()), + }, + } + } + } + } + } + } + }; } public override void OnExiting(ScreenExitEvent e) @@ -62,19 +104,5 @@ namespace osu.Game.Screens.Edit.Setup // (and potentially block the exit procedure for save). GetContainingFocusManager()?.TriggerFocusContention(this); } - - private partial class SetupScreenSectionsContainer : SectionsContainer - { - protected override UserTrackingScrollContainer CreateScrollContainer() - { - var scrollContainer = base.CreateScrollContainer(); - - // Workaround for masking issues (see https://github.com/ppy/osu-framework/issues/1675#issuecomment-910023157) - // Note that this actually causes the full scroll range to be reduced by 2px at the bottom, but it's not really noticeable. - scrollContainer.Margin = new MarginPadding { Top = 2 }; - - return scrollContainer; - } - } } } diff --git a/osu.Game/Screens/Edit/Setup/SetupSection.cs b/osu.Game/Screens/Edit/Setup/SetupSection.cs index 5f676798f1..d3b231de25 100644 --- a/osu.Game/Screens/Edit/Setup/SetupSection.cs +++ b/osu.Game/Screens/Edit/Setup/SetupSection.cs @@ -6,7 +6,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Localisation; using osu.Game.Graphics; -using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterfaceV2; using osuTK; @@ -37,30 +37,23 @@ namespace osu.Game.Screens.Edit.Setup RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; - Padding = new MarginPadding - { - Vertical = 10, - Horizontal = 100 - }; - InternalChild = new FillFlowContainer { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Spacing = new Vector2(20), + Spacing = new Vector2(10), Direction = FillDirection.Vertical, Children = new Drawable[] { - new OsuSpriteText + new SectionHeader(Title) { - Font = OsuFont.GetFont(weight: FontWeight.Bold), - Text = Title + Margin = new MarginPadding { Left = 9, }, }, flow = new FillFlowContainer { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Spacing = new Vector2(10), + Spacing = new Vector2(5), Direction = FillDirection.Vertical, } } From 09441a53c283662e0646962a2f8c311133179e32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 3 Oct 2024 11:10:55 +0200 Subject: [PATCH 2455/2493] Fix "form" file selector displaying commit animation on initial show --- osu.Game/Graphics/UserInterfaceV2/FormFileSelector.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Graphics/UserInterfaceV2/FormFileSelector.cs b/osu.Game/Graphics/UserInterfaceV2/FormFileSelector.cs index f5b6cb3e64..3b822a1b2f 100644 --- a/osu.Game/Graphics/UserInterfaceV2/FormFileSelector.cs +++ b/osu.Game/Graphics/UserInterfaceV2/FormFileSelector.cs @@ -154,6 +154,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 updateState(); onFileSelected(); }, true); + FinishTransforms(true); game.RegisterImportHandler(this); } From cde348bfb811accf8d8508afd35ae2e7bb310068 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 3 Oct 2024 11:10:58 +0200 Subject: [PATCH 2456/2493] Fix "form" textbox not dropping border if disabled when hovered --- osu.Game/Graphics/UserInterfaceV2/FormTextBox.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Graphics/UserInterfaceV2/FormTextBox.cs b/osu.Game/Graphics/UserInterfaceV2/FormTextBox.cs index 9bbb5cba99..973419310c 100644 --- a/osu.Game/Graphics/UserInterfaceV2/FormTextBox.cs +++ b/osu.Game/Graphics/UserInterfaceV2/FormTextBox.cs @@ -202,6 +202,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 } else { + BorderThickness = 0; background.Colour = colourProvider.Background4; } } From a567c6369d7e74a32ed47ed326643651930f3953 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 3 Oct 2024 11:11:02 +0200 Subject: [PATCH 2457/2493] Autoselect contents of "form" number box --- osu.Game/Graphics/UserInterfaceV2/FormNumberBox.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Graphics/UserInterfaceV2/FormNumberBox.cs b/osu.Game/Graphics/UserInterfaceV2/FormNumberBox.cs index 66f1a45210..8ce6c85fa9 100644 --- a/osu.Game/Graphics/UserInterfaceV2/FormNumberBox.cs +++ b/osu.Game/Graphics/UserInterfaceV2/FormNumberBox.cs @@ -10,6 +10,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 internal override InnerTextBox CreateTextBox() => new InnerNumberBox { AllowDecimals = AllowDecimals, + SelectAllOnFocus = true, }; internal partial class InnerNumberBox : InnerTextBox From 19356d0487dd1f8f7796e5c0787ee578841c5917 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 3 Oct 2024 19:33:48 +0900 Subject: [PATCH 2458/2493] Match corner radius of "new" button --- osu.Game/Graphics/UserInterfaceV2/FormColourPalette.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Graphics/UserInterfaceV2/FormColourPalette.cs b/osu.Game/Graphics/UserInterfaceV2/FormColourPalette.cs index a2c1c1622e..e3ae38ac65 100644 --- a/osu.Game/Graphics/UserInterfaceV2/FormColourPalette.cs +++ b/osu.Game/Graphics/UserInterfaceV2/FormColourPalette.cs @@ -152,7 +152,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 Size = new Vector2(70, 25); Masking = true; - CornerRadius = 3; + CornerRadius = 12.5f; Action = this.ShowPopover; Children = new Drawable[] From ddfa877b12d2093a1d69a7c1198557cbd16eef4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 3 Oct 2024 13:08:04 +0200 Subject: [PATCH 2459/2493] Fix code quality --- osu.Game.Rulesets.Taiko/Edit/Setup/TaikoDifficultySection.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/Edit/Setup/TaikoDifficultySection.cs b/osu.Game.Rulesets.Taiko/Edit/Setup/TaikoDifficultySection.cs index e191169929..8fce59e791 100644 --- a/osu.Game.Rulesets.Taiko/Edit/Setup/TaikoDifficultySection.cs +++ b/osu.Game.Rulesets.Taiko/Edit/Setup/TaikoDifficultySection.cs @@ -67,7 +67,7 @@ namespace osu.Game.Rulesets.Taiko.Edit.Setup }, TabbableContentContainer = this, }, - tickRateSlider = new FormSliderBar() + tickRateSlider = new FormSliderBar { Caption = EditorSetupStrings.TickRate, HintText = EditorSetupStrings.TickRateDescription, From 8a650deab66e30d115ac4542b04ff9bf635b3d04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 3 Oct 2024 13:09:35 +0200 Subject: [PATCH 2460/2493] Fix tests --- .../Editor/TestSceneManiaEditorSaving.cs | 4 ++-- osu.Game.Tests/Visual/Editing/TestSceneDesignSection.cs | 5 +++++ osu.Game.Tests/Visual/Editing/TestSceneMetadataSection.cs | 8 ++++++-- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaEditorSaving.cs b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaEditorSaving.cs index 9765648f44..d9ba721646 100644 --- a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaEditorSaving.cs +++ b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaEditorSaving.cs @@ -20,10 +20,10 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor [Test] public void TestKeyCountChange() { - LabelledSliderBar keyCount = null!; + FormSliderBar keyCount = null!; AddStep("go to setup screen", () => InputManager.Key(Key.F4)); - AddUntilStep("retrieve key count slider", () => keyCount = Editor.ChildrenOfType().Single().ChildrenOfType>().First(), () => Is.Not.Null); + AddUntilStep("retrieve key count slider", () => keyCount = Editor.ChildrenOfType().Single().ChildrenOfType>().First(), () => Is.Not.Null); AddAssert("key count is 5", () => keyCount.Current.Value, () => Is.EqualTo(5)); AddStep("change key count to 8", () => { diff --git a/osu.Game.Tests/Visual/Editing/TestSceneDesignSection.cs b/osu.Game.Tests/Visual/Editing/TestSceneDesignSection.cs index 143547dfc9..a4f250675e 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneDesignSection.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneDesignSection.cs @@ -7,12 +7,14 @@ using System; using System.Globalization; using System.Linq; using NUnit.Framework; +using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.UserInterface; using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Graphics.UserInterfaceV2; +using osu.Game.Overlays; using osu.Game.Rulesets.Osu; using osu.Game.Screens.Edit; using osu.Game.Screens.Edit.Setup; @@ -25,6 +27,9 @@ namespace osu.Game.Tests.Visual.Editing private TestDesignSection designSection; private EditorBeatmap editorBeatmap { get; set; } + [Cached] + private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine); + [SetUpSteps] public void SetUp() { diff --git a/osu.Game.Tests/Visual/Editing/TestSceneMetadataSection.cs b/osu.Game.Tests/Visual/Editing/TestSceneMetadataSection.cs index 653e3e7ff9..167230f5a0 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneMetadataSection.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneMetadataSection.cs @@ -11,6 +11,7 @@ using osu.Framework.Input; using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Graphics.UserInterfaceV2; +using osu.Game.Overlays; using osu.Game.Rulesets.Osu; using osu.Game.Screens.Edit; using osu.Game.Screens.Edit.Setup; @@ -20,6 +21,9 @@ namespace osu.Game.Tests.Visual.Editing { public partial class TestSceneMetadataSection : OsuManualInputManagerTestScene { + [Cached] + private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine); + [Cached] private EditorBeatmap editorBeatmap = new EditorBeatmap(new Beatmap { @@ -212,7 +216,7 @@ namespace osu.Game.Tests.Visual.Editing private void assertRomanisedArtist(string expected, bool editable) { AddAssert($"romanised artist is {expected}", () => metadataSection.RomanisedArtistTextBox.Current.Value, () => Is.EqualTo(expected)); - AddAssert($"romanised artist is {(editable ? "" : "not ")}editable", () => metadataSection.RomanisedArtistTextBox.Current.Disabled == !editable); + AddAssert($"romanised artist is {(editable ? "" : "not ")}editable", () => metadataSection.RomanisedArtistTextBox.ReadOnly == !editable); } private void assertTitle(string expected) @@ -221,7 +225,7 @@ namespace osu.Game.Tests.Visual.Editing private void assertRomanisedTitle(string expected, bool editable) { AddAssert($"romanised title is {expected}", () => metadataSection.RomanisedTitleTextBox.Current.Value, () => Is.EqualTo(expected)); - AddAssert($"romanised title is {(editable ? "" : "not ")}editable", () => metadataSection.RomanisedTitleTextBox.Current.Disabled == !editable); + AddAssert($"romanised title is {(editable ? "" : "not ")}editable", () => metadataSection.RomanisedTitleTextBox.ReadOnly == !editable); } private partial class TestMetadataSection : MetadataSection From 99eb26b7d55a4a88c5ea567cb70b6d0f4b0b3ea8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 3 Oct 2024 13:53:21 +0200 Subject: [PATCH 2461/2493] Redo the layout of sections based on discord feedback See https://discord.com/channels/188630481301012481/188630652340404224/1291358770064130140 and everything after. --- osu.Game.Rulesets.Catch/CatchRuleset.cs | 24 +++++++++++++++++-- osu.Game.Rulesets.Mania/ManiaRuleset.cs | 5 +++- osu.Game.Rulesets.Osu/OsuRuleset.cs | 24 +++++++++++++++++-- osu.Game.Rulesets.Taiko/TaikoRuleset.cs | 5 +++- osu.Game/Rulesets/Ruleset.cs | 24 +++++++++++++++++-- osu.Game/Screens/Edit/Setup/DesignSection.cs | 2 +- .../Screens/Edit/Setup/ResourcesSection.cs | 2 +- osu.Game/Screens/Edit/Setup/SetupScreen.cs | 18 ++++---------- osu.Game/Screens/Edit/Setup/SetupSection.cs | 1 - 9 files changed, 80 insertions(+), 25 deletions(-) diff --git a/osu.Game.Rulesets.Catch/CatchRuleset.cs b/osu.Game.Rulesets.Catch/CatchRuleset.cs index 7eaf4f2b18..9f48da599e 100644 --- a/osu.Game.Rulesets.Catch/CatchRuleset.cs +++ b/osu.Game.Rulesets.Catch/CatchRuleset.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Bindings; using osu.Framework.Localisation; @@ -31,6 +32,7 @@ using osu.Game.Scoring; using osu.Game.Screens.Edit.Setup; using osu.Game.Screens.Ranking.Statistics; using osu.Game.Skinning; +using osuTK; namespace osu.Game.Rulesets.Catch { @@ -223,10 +225,28 @@ namespace osu.Game.Rulesets.Catch public override HitObjectComposer CreateHitObjectComposer() => new CatchHitObjectComposer(this); - public override IEnumerable CreateEditorSetupSections() => + public override IEnumerable CreateEditorSetupSections() => [ + new MetadataSection(), new DifficultySection(), - new ColoursSection(), + new FillFlowContainer + { + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(25), + Children = new Drawable[] + { + new ResourcesSection + { + RelativeSizeAxes = Axes.X, + }, + new ColoursSection + { + RelativeSizeAxes = Axes.X, + } + } + }, + new DesignSection(), ]; public override IBeatmapVerifier CreateBeatmapVerifier() => new CatchBeatmapVerifier(); diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs index c01fa508fe..cdc7b0a951 100644 --- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs @@ -419,9 +419,12 @@ namespace osu.Game.Rulesets.Mania return new ManiaFilterCriteria(); } - public override IEnumerable CreateEditorSetupSections() => + public override IEnumerable CreateEditorSetupSections() => [ + new MetadataSection(), new ManiaDifficultySection(), + new ResourcesSection(), + new DesignSection(), ]; public int GetKeyCount(IBeatmapInfo beatmapInfo, IReadOnlyList? mods = null) diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index be48ef9acc..9f2a5b2066 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Linq; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Bindings; using osu.Framework.Localisation; @@ -39,6 +40,7 @@ using osu.Game.Scoring; using osu.Game.Screens.Edit.Setup; using osu.Game.Screens.Ranking.Statistics; using osu.Game.Skinning; +using osuTK; namespace osu.Game.Rulesets.Osu { @@ -336,10 +338,28 @@ namespace osu.Game.Rulesets.Osu }; } - public override IEnumerable CreateEditorSetupSections() => + public override IEnumerable CreateEditorSetupSections() => [ + new MetadataSection(), new OsuDifficultySection(), - new ColoursSection(), + new FillFlowContainer + { + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(25), + Children = new Drawable[] + { + new ResourcesSection + { + RelativeSizeAxes = Axes.X, + }, + new ColoursSection + { + RelativeSizeAxes = Axes.X, + } + } + }, + new DesignSection(), ]; /// diff --git a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs index 2447a4a247..70e429a344 100644 --- a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs @@ -190,9 +190,12 @@ namespace osu.Game.Rulesets.Taiko public override HitObjectComposer CreateHitObjectComposer() => new TaikoHitObjectComposer(this); - public override IEnumerable CreateEditorSetupSections() => + public override IEnumerable CreateEditorSetupSections() => [ + new MetadataSection(), new TaikoDifficultySection(), + new ResourcesSection(), + new DesignSection(), ]; public override IBeatmapVerifier CreateBeatmapVerifier() => new TaikoBeatmapVerifier(); diff --git a/osu.Game/Rulesets/Ruleset.cs b/osu.Game/Rulesets/Ruleset.cs index 5af1fd386c..d4989642c2 100644 --- a/osu.Game/Rulesets/Ruleset.cs +++ b/osu.Game/Rulesets/Ruleset.cs @@ -8,6 +8,7 @@ using System.Linq; using osu.Framework.Extensions; using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Bindings; using osu.Framework.IO.Stores; @@ -30,6 +31,7 @@ using osu.Game.Screens.Edit.Setup; using osu.Game.Screens.Ranking.Statistics; using osu.Game.Skinning; using osu.Game.Users; +using osuTK; namespace osu.Game.Rulesets { @@ -396,10 +398,28 @@ namespace osu.Game.Rulesets /// /// Can be overridden to add ruleset-specific sections to the editor beatmap setup screen. /// - public virtual IEnumerable CreateEditorSetupSections() => + public virtual IEnumerable CreateEditorSetupSections() => [ + new MetadataSection(), new DifficultySection(), - new ColoursSection(), + new FillFlowContainer + { + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(25), + Children = new Drawable[] + { + new ResourcesSection + { + RelativeSizeAxes = Axes.X, + }, + new ColoursSection + { + RelativeSizeAxes = Axes.X, + } + } + }, + new DesignSection(), ]; /// diff --git a/osu.Game/Screens/Edit/Setup/DesignSection.cs b/osu.Game/Screens/Edit/Setup/DesignSection.cs index e3c01fc9bf..7def5394e6 100644 --- a/osu.Game/Screens/Edit/Setup/DesignSection.cs +++ b/osu.Game/Screens/Edit/Setup/DesignSection.cs @@ -15,7 +15,7 @@ using osu.Game.Localisation; namespace osu.Game.Screens.Edit.Setup { - internal partial class DesignSection : SetupSection + public partial class DesignSection : SetupSection { protected FormCheckBox EnableCountdown = null!; diff --git a/osu.Game/Screens/Edit/Setup/ResourcesSection.cs b/osu.Game/Screens/Edit/Setup/ResourcesSection.cs index 3ce9f01b2b..6fec7078a8 100644 --- a/osu.Game/Screens/Edit/Setup/ResourcesSection.cs +++ b/osu.Game/Screens/Edit/Setup/ResourcesSection.cs @@ -13,7 +13,7 @@ using osu.Game.Localisation; namespace osu.Game.Screens.Edit.Setup { - internal partial class ResourcesSection : SetupSection + public partial class ResourcesSection : SetupSection { private FormFileSelector audioTrackChooser = null!; private FormFileSelector backgroundChooser = null!; diff --git a/osu.Game/Screens/Edit/Setup/SetupScreen.cs b/osu.Game/Screens/Edit/Setup/SetupScreen.cs index 4b9a7a858f..1af54d55d6 100644 --- a/osu.Game/Screens/Edit/Setup/SetupScreen.cs +++ b/osu.Game/Screens/Edit/Setup/SetupScreen.cs @@ -65,25 +65,15 @@ namespace osu.Game.Screens.Edit.Setup { new FillFlowContainer { - Width = 450, + Width = 925, AutoSizeAxes = Axes.Y, Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, Spacing = new Vector2(25), - Children = new Drawable[] + ChildrenEnumerable = ruleset.CreateEditorSetupSections().Select(section => section.With(s => { - new ResourcesSection(), - new MetadataSection(), - } - }, - new FillFlowContainer - { - Width = 450, - AutoSizeAxes = Axes.Y, - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - Spacing = new Vector2(25), - ChildrenEnumerable = ruleset.CreateEditorSetupSections().Append(new DesignSection()), + s.Width = 450; + })), }, } } diff --git a/osu.Game/Screens/Edit/Setup/SetupSection.cs b/osu.Game/Screens/Edit/Setup/SetupSection.cs index d3b231de25..bd1eb51b48 100644 --- a/osu.Game/Screens/Edit/Setup/SetupSection.cs +++ b/osu.Game/Screens/Edit/Setup/SetupSection.cs @@ -34,7 +34,6 @@ namespace osu.Game.Screens.Edit.Setup [BackgroundDependencyLoader] private void load() { - RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; InternalChild = new FillFlowContainer From 2c0a7d4c182e0b9cbb79d9ffae0d5afba97b57df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 3 Oct 2024 14:32:46 +0200 Subject: [PATCH 2462/2493] Adjust slider bar padding https://discord.com/channels/188630481301012481/188630652340404224/1291374650256916482 --- osu.Game/Graphics/UserInterfaceV2/FormSliderBar.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/osu.Game/Graphics/UserInterfaceV2/FormSliderBar.cs b/osu.Game/Graphics/UserInterfaceV2/FormSliderBar.cs index ac3730598f..78c197dee6 100644 --- a/osu.Game/Graphics/UserInterfaceV2/FormSliderBar.cs +++ b/osu.Game/Graphics/UserInterfaceV2/FormSliderBar.cs @@ -107,7 +107,12 @@ namespace osu.Game.Graphics.UserInterfaceV2 new Container { RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding(9), + Padding = new MarginPadding + { + Vertical = 9, + Left = 9, + Right = 5, + }, Children = new Drawable[] { caption = new FormFieldCaption From 090c8ee602407334d694fa5cfce25fc61459bda1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 3 Oct 2024 14:35:03 +0200 Subject: [PATCH 2463/2493] Make colour palette things circular again --- osu.Game/Graphics/UserInterfaceV2/FormColourPalette.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Graphics/UserInterfaceV2/FormColourPalette.cs b/osu.Game/Graphics/UserInterfaceV2/FormColourPalette.cs index 00bac0c346..9575ebaa3b 100644 --- a/osu.Game/Graphics/UserInterfaceV2/FormColourPalette.cs +++ b/osu.Game/Graphics/UserInterfaceV2/FormColourPalette.cs @@ -77,7 +77,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 Child = button = new RoundedButton { Action = () => Colours.Add(Colour4.White), - Size = new Vector2(70, 25), + Size = new Vector2(70), Text = "+", } } @@ -149,10 +149,10 @@ namespace osu.Game.Graphics.UserInterfaceV2 [BackgroundDependencyLoader] private void load() { - Size = new Vector2(70, 25); + Size = new Vector2(70); Masking = true; - CornerRadius = 12.5f; + CornerRadius = 35; Action = this.ShowPopover; Children = new Drawable[] From 1bab2236fe40c4b87f39ebac16bbfafda370b898 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 3 Oct 2024 15:01:23 +0200 Subject: [PATCH 2464/2493] Ensure columns collapse into one correctly if no space --- osu.Game.Rulesets.Catch/CatchRuleset.cs | 2 +- osu.Game.Rulesets.Osu/OsuRuleset.cs | 2 +- osu.Game/Screens/Edit/Setup/SetupScreen.cs | 50 ++++++++++++++-------- 3 files changed, 35 insertions(+), 19 deletions(-) diff --git a/osu.Game.Rulesets.Catch/CatchRuleset.cs b/osu.Game.Rulesets.Catch/CatchRuleset.cs index 9f48da599e..5bd7a0ff00 100644 --- a/osu.Game.Rulesets.Catch/CatchRuleset.cs +++ b/osu.Game.Rulesets.Catch/CatchRuleset.cs @@ -233,7 +233,7 @@ namespace osu.Game.Rulesets.Catch { AutoSizeAxes = Axes.Y, Direction = FillDirection.Vertical, - Spacing = new Vector2(25), + Spacing = new Vector2(SetupScreen.SPACING), Children = new Drawable[] { new ResourcesSection diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index 9f2a5b2066..2f928aaefa 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -346,7 +346,7 @@ namespace osu.Game.Rulesets.Osu { AutoSizeAxes = Axes.Y, Direction = FillDirection.Vertical, - Spacing = new Vector2(25), + Spacing = new Vector2(SetupScreen.SPACING), Children = new Drawable[] { new ResourcesSection diff --git a/osu.Game/Screens/Edit/Setup/SetupScreen.cs b/osu.Game/Screens/Edit/Setup/SetupScreen.cs index 1af54d55d6..38720f6333 100644 --- a/osu.Game/Screens/Edit/Setup/SetupScreen.cs +++ b/osu.Game/Screens/Edit/Setup/SetupScreen.cs @@ -15,6 +15,10 @@ namespace osu.Game.Screens.Edit.Setup { public partial class SetupScreen : EditorScreen { + public const float COLUMN_WIDTH = 450; + public const float SPACING = 28; + public const float MAX_WIDTH = 2 * COLUMN_WIDTH + SPACING; + public SetupScreen() : base(EditorScreenMode.SongSetup) { @@ -23,6 +27,9 @@ namespace osu.Game.Screens.Edit.Setup [Cached] private SetupScreenHeaderBackground background = new SetupScreenHeaderBackground { RelativeSizeAxes = Axes.Both, }; + private OsuScrollContainer scroll = null!; + private FillFlowContainer flow = null!; + [BackgroundDependencyLoader] private void load(EditorBeatmap beatmap, OverlayColourProvider colourProvider) { @@ -51,31 +58,24 @@ namespace osu.Game.Screens.Edit.Setup }, new Drawable[] { - new OsuScrollContainer + scroll = new OsuScrollContainer { RelativeSizeAxes = Axes.Both, Padding = new MarginPadding(15), - Child = new FillFlowContainer + Child = flow = new FillFlowContainer { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Direction = FillDirection.Full, - Spacing = new Vector2(28), - Children = new Drawable[] + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Spacing = new Vector2(25), + ChildrenEnumerable = ruleset.CreateEditorSetupSections().Select(section => section.With(s => { - new FillFlowContainer - { - Width = 925, - AutoSizeAxes = Axes.Y, - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - Spacing = new Vector2(25), - ChildrenEnumerable = ruleset.CreateEditorSetupSections().Select(section => section.With(s => - { - s.Width = 450; - })), - }, - } + s.Width = 450; + s.Anchor = Anchor.TopCentre; + s.Origin = Anchor.TopCentre; + })), } } } @@ -84,6 +84,22 @@ namespace osu.Game.Screens.Edit.Setup }; } + protected override void UpdateAfterChildren() + { + base.UpdateAfterChildren(); + + if (scroll.DrawWidth > MAX_WIDTH) + { + flow.RelativeSizeAxes = Axes.None; + flow.Width = MAX_WIDTH; + } + else + { + flow.RelativeSizeAxes = Axes.X; + flow.Width = 1; + } + } + public override void OnExiting(ScreenExitEvent e) { base.OnExiting(e); From 1280d7ea15d57a4197c80e712965ca87c0cdaa74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 3 Oct 2024 15:05:15 +0200 Subject: [PATCH 2465/2493] Fix tests again --- osu.Game.Tests/Visual/Editing/TestSceneDesignSection.cs | 2 +- osu.Game.Tests/Visual/Editing/TestSceneMetadataSection.cs | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneDesignSection.cs b/osu.Game.Tests/Visual/Editing/TestSceneDesignSection.cs index a4f250675e..4dd27a7b6e 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneDesignSection.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneDesignSection.cs @@ -47,7 +47,7 @@ namespace osu.Game.Tests.Visual.Editing { (typeof(EditorBeatmap), editorBeatmap) }, - Child = designSection = new TestDesignSection() + Child = designSection = new TestDesignSection { RelativeSizeAxes = Axes.X } }); } diff --git a/osu.Game.Tests/Visual/Editing/TestSceneMetadataSection.cs b/osu.Game.Tests/Visual/Editing/TestSceneMetadataSection.cs index 167230f5a0..743529d40c 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneMetadataSection.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneMetadataSection.cs @@ -6,6 +6,7 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; +using osu.Framework.Graphics; using osu.Framework.Graphics.UserInterface; using osu.Framework.Input; using osu.Framework.Testing; @@ -205,7 +206,7 @@ namespace osu.Game.Tests.Visual.Editing } private void createSection() - => AddStep("create metadata section", () => Child = metadataSection = new TestMetadataSection()); + => AddStep("create metadata section", () => Child = metadataSection = new TestMetadataSection { RelativeSizeAxes = Axes.X }); private void assertArtistMetadata(string expected) => AddAssert($"artist metadata is {expected}", () => editorBeatmap.Metadata.Artist, () => Is.EqualTo(expected)); From 29418226c05eda74d72875441656683e65989b45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 3 Oct 2024 15:32:25 +0200 Subject: [PATCH 2466/2493] Do not add checkbox padding to the left of menu items if no item actually needs it RFC. As per https://discord.com/channels/188630481301012481/188630652340404224/1291346164976980009. The diff is extremely dumb but I tried a few smarter methods and they're either not fully correct or don't work. Primary problem is that menu items are mutable externally and there's no hook provided by the framework to know that items changed. One could probably be made but I'd prefer that this change be examined visually first before I engage in too much ceremony and start changing framework around. --- .../Graphics/UserInterface/DrawableOsuMenuItem.cs | 6 ++++++ osu.Game/Graphics/UserInterface/OsuMenu.cs | 11 +++++++++++ 2 files changed, 17 insertions(+) diff --git a/osu.Game/Graphics/UserInterface/DrawableOsuMenuItem.cs b/osu.Game/Graphics/UserInterface/DrawableOsuMenuItem.cs index 20de8e3c9f..cd44bd8fb9 100644 --- a/osu.Game/Graphics/UserInterface/DrawableOsuMenuItem.cs +++ b/osu.Game/Graphics/UserInterface/DrawableOsuMenuItem.cs @@ -6,6 +6,7 @@ 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; @@ -26,6 +27,8 @@ namespace osu.Game.Graphics.UserInterface public const int TEXT_SIZE = 17; public const int TRANSITION_LENGTH = 80; + public BindableBool ShowCheckbox { get; } = new BindableBool(); + private TextContainer text; private HotkeyDisplay hotkey; private HoverClickSounds hoverClickSounds; @@ -72,6 +75,7 @@ namespace osu.Game.Graphics.UserInterface { base.LoadComplete(); + ShowCheckbox.BindValueChanged(_ => updateState()); Item.Action.BindDisabledChanged(_ => updateState(), true); FinishTransforms(); } @@ -138,6 +142,8 @@ namespace osu.Game.Graphics.UserInterface text.BoldText.FadeOut(TRANSITION_LENGTH, Easing.OutQuint); text.NormalText.FadeIn(TRANSITION_LENGTH, Easing.OutQuint); } + + text.CheckboxContainer.Alpha = ShowCheckbox.Value ? 1 : 0; } protected sealed override Drawable CreateContent() => text = CreateTextContainer(); diff --git a/osu.Game/Graphics/UserInterface/OsuMenu.cs b/osu.Game/Graphics/UserInterface/OsuMenu.cs index 2b9a26166f..719100e138 100644 --- a/osu.Game/Graphics/UserInterface/OsuMenu.cs +++ b/osu.Game/Graphics/UserInterface/OsuMenu.cs @@ -3,6 +3,7 @@ #nullable disable +using System.Linq; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; @@ -42,6 +43,16 @@ namespace osu.Game.Graphics.UserInterface sampleClose = audio.Samples.Get(@"UI/dropdown-close"); } + protected override void Update() + { + base.Update(); + + bool showCheckboxes = Items.Any(i => i is StatefulMenuItem); + + foreach (var drawableItem in ItemsContainer.OfType()) + drawableItem.ShowCheckbox.Value = showCheckboxes; + } + protected override void AnimateOpen() { if (!TopLevelMenu && !wasOpened) From 114e53f8b2afb595081bb9894e84639faa9b13ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 4 Oct 2024 09:46:10 +0200 Subject: [PATCH 2467/2493] Add failing test --- .../Visual/Editing/TestSceneColoursSection.cs | 123 ++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 osu.Game.Tests/Visual/Editing/TestSceneColoursSection.cs diff --git a/osu.Game.Tests/Visual/Editing/TestSceneColoursSection.cs b/osu.Game.Tests/Visual/Editing/TestSceneColoursSection.cs new file mode 100644 index 0000000000..5a3329bbc9 --- /dev/null +++ b/osu.Game.Tests/Visual/Editing/TestSceneColoursSection.cs @@ -0,0 +1,123 @@ +// 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.Graphics; +using osu.Framework.Testing; +using osu.Game.Beatmaps; +using osu.Game.Graphics.UserInterfaceV2; +using osu.Game.Overlays; +using osu.Game.Rulesets.Osu; +using osu.Game.Screens.Edit; +using osu.Game.Screens.Edit.Setup; +using osu.Game.Skinning; +using osuTK.Graphics; + +namespace osu.Game.Tests.Visual.Editing +{ + [HeadlessTest] + public partial class TestSceneColoursSection : OsuManualInputManagerTestScene + { + [Test] + public void TestNoBeatmapSkinColours() + { + LegacyBeatmapSkin skin = null!; + ColoursSection coloursSection = null!; + + AddStep("create beatmap skin", () => skin = new LegacyBeatmapSkin(new BeatmapInfo(), null)); + AddStep("create colours section", () => Child = new DependencyProvidingContainer + { + RelativeSizeAxes = Axes.Both, + CachedDependencies = + [ + (typeof(EditorBeatmap), new EditorBeatmap(new Beatmap + { + BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo } + }, skin)), + (typeof(OverlayColourProvider), new OverlayColourProvider(OverlayColourScheme.Aquamarine)) + ], + Child = coloursSection = new ColoursSection + { + RelativeSizeAxes = Axes.X, + } + }); + AddAssert("beatmap skin has no colours", () => skin.Configuration.CustomComboColours, () => Is.Empty); + AddAssert("section displays default combo colours", + () => coloursSection.ChildrenOfType().Single().Colours, + () => Is.EquivalentTo(new Colour4[] + { + SkinConfiguration.DefaultComboColours[1], + SkinConfiguration.DefaultComboColours[2], + SkinConfiguration.DefaultComboColours[3], + SkinConfiguration.DefaultComboColours[0], + })); + + AddStep("add a colour", () => coloursSection.ChildrenOfType().Single().Colours.Add(Colour4.Aqua)); + AddAssert("beatmap skin has colours", + () => skin.Configuration.CustomComboColours, + () => Is.EquivalentTo(new[] + { + SkinConfiguration.DefaultComboColours[1], + SkinConfiguration.DefaultComboColours[2], + SkinConfiguration.DefaultComboColours[3], + Color4.Aqua, + SkinConfiguration.DefaultComboColours[0], + })); + } + + [Test] + public void TestExistingColours() + { + LegacyBeatmapSkin skin = null!; + ColoursSection coloursSection = null!; + + AddStep("create beatmap skin", () => + { + skin = new LegacyBeatmapSkin(new BeatmapInfo(), null); + skin.Configuration.CustomComboColours = new List + { + Color4.Azure, + Color4.Beige, + Color4.Chartreuse + }; + }); + AddStep("create colours section", () => Child = new DependencyProvidingContainer + { + RelativeSizeAxes = Axes.Both, + CachedDependencies = + [ + (typeof(EditorBeatmap), new EditorBeatmap(new Beatmap + { + BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo } + }, skin)), + (typeof(OverlayColourProvider), new OverlayColourProvider(OverlayColourScheme.Aquamarine)) + ], + Child = coloursSection = new ColoursSection + { + RelativeSizeAxes = Axes.X, + } + }); + AddAssert("section displays combo colours", + () => coloursSection.ChildrenOfType().Single().Colours, + () => Is.EquivalentTo(new[] + { + Colour4.Beige, + Colour4.Chartreuse, + Colour4.Azure, + })); + + AddStep("add a colour", () => coloursSection.ChildrenOfType().Single().Colours.Add(Colour4.Aqua)); + AddAssert("beatmap skin has colours", + () => skin.Configuration.CustomComboColours, + () => Is.EquivalentTo(new[] + { + Color4.Azure, + Color4.Beige, + Color4.Aqua, + Color4.Chartreuse + })); + } + } +} From 6e5a38c6c8f974f3ad89b629990dd8e81acd9616 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 4 Oct 2024 10:01:11 +0200 Subject: [PATCH 2468/2493] Initialise colours section with default combo colours if none present Closes https://github.com/ppy/osu/issues/30100. --- osu.Game/Screens/Edit/Setup/ColoursSection.cs | 44 ++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Setup/ColoursSection.cs b/osu.Game/Screens/Edit/Setup/ColoursSection.cs index 01ca114e4f..ee76ec1f6d 100644 --- a/osu.Game/Screens/Edit/Setup/ColoursSection.cs +++ b/osu.Game/Screens/Edit/Setup/ColoursSection.cs @@ -6,6 +6,7 @@ using osu.Framework.Graphics; using osu.Framework.Localisation; using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Localisation; +using osu.Game.Skinning; namespace osu.Game.Screens.Edit.Setup { @@ -25,9 +26,50 @@ namespace osu.Game.Screens.Edit.Setup Caption = EditorSetupStrings.HitCircleSliderCombos, } }; + } + private bool syncingColours = false; + + protected override void LoadComplete() + { if (Beatmap.BeatmapSkin != null) - comboColours.Colours.BindTo(Beatmap.BeatmapSkin.ComboColours); + comboColours.Colours.AddRange(Beatmap.BeatmapSkin.ComboColours); + + if (comboColours.Colours.Count == 0) + { + // compare ctor of `EditorBeatmapSkin` + for (int i = 0; i < SkinConfiguration.DefaultComboColours.Count; ++i) + comboColours.Colours.Add(SkinConfiguration.DefaultComboColours[(i + 1) % SkinConfiguration.DefaultComboColours.Count]); + } + + comboColours.Colours.BindCollectionChanged((_, _) => + { + if (Beatmap.BeatmapSkin != null) + { + if (syncingColours) + return; + + syncingColours = true; + + Beatmap.BeatmapSkin.ComboColours.Clear(); + Beatmap.BeatmapSkin.ComboColours.AddRange(comboColours.Colours); + + syncingColours = false; + } + }); + + Beatmap.BeatmapSkin?.ComboColours.BindCollectionChanged((_, _) => + { + if (syncingColours) + return; + + syncingColours = true; + + comboColours.Colours.Clear(); + comboColours.Colours.AddRange(Beatmap.BeatmapSkin?.ComboColours); + + syncingColours = false; + }); } } } From 2d7fdaf89271da7fc93516b82c5f301f9a2510cd Mon Sep 17 00:00:00 2001 From: CloneWith Date: Fri, 4 Oct 2024 16:42:48 +0800 Subject: [PATCH 2469/2493] Override OnMouseMove for cursor position fetching --- .../Screens/Play/HUD/ArgonSongProgressBar.cs | 26 +++++++------------ 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/ArgonSongProgressBar.cs b/osu.Game/Screens/Play/HUD/ArgonSongProgressBar.cs index 28a2e1030f..3c9553ee55 100644 --- a/osu.Game/Screens/Play/HUD/ArgonSongProgressBar.cs +++ b/osu.Game/Screens/Play/HUD/ArgonSongProgressBar.cs @@ -8,7 +8,6 @@ using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Shapes; -using osu.Framework.Input; using osu.Framework.Input.Events; using osu.Framework.Localisation; using osu.Framework.Utils; @@ -39,8 +38,6 @@ namespace osu.Game.Screens.Play.HUD private float relativePositionX; - private InputManager? inputManager; - public LocalisableString TooltipText => $"{(relativePositionX > 0 ? (EndTime - StartTime) * relativePositionX / DrawWidth : relativePositionX > DrawWidth ? EndTime : 0).ToEditorFormattedString()}" + $" - {(relativePositionX > 0 ? Math.Round(relativePositionX / DrawWidth * 100, 2) : relativePositionX > DrawWidth ? 100 : 0)}%"; @@ -85,7 +82,6 @@ namespace osu.Game.Screens.Play.HUD private void load(OsuColour colours) { catchUpColour = colours.BlueDark; - inputManager = GetContainingInputManager(); } protected override void LoadComplete() @@ -96,6 +92,16 @@ namespace osu.Game.Screens.Play.HUD playfieldBar.TransformTo(nameof(playfieldBar.AccentColour), mainColour, 200, Easing.In); } + protected override bool OnMouseMove(MouseMoveEvent e) + { + base.OnMouseMove(e); + + var cursorPosition = e.ScreenSpaceMousePosition; + relativePositionX = ToLocalSpace(cursorPosition).X; + + return true; + } + protected override bool OnHover(HoverEvent e) { if (Interactive) @@ -114,18 +120,6 @@ namespace osu.Game.Screens.Play.HUD { base.Update(); - if (inputManager != null) - { - // Update the cursor position in time - var cursorPosition = inputManager.CurrentState.Mouse.Position; - relativePositionX = ToLocalSpace(cursorPosition).X; - } - else - { - // If null (e.g. before the game starts), try getting the input manager again - inputManager = GetContainingInputManager(); - } - playfieldBar.Length = (float)Interpolation.Lerp(playfieldBar.Length, Progress, Math.Clamp(Time.Elapsed / 40, 0, 1)); audioBar.Length = (float)Interpolation.Lerp(audioBar.Length, AudioProgress, Math.Clamp(Time.Elapsed / 40, 0, 1)); From fd5655455a6611488db0636c61973272d91bea27 Mon Sep 17 00:00:00 2001 From: CloneWith Date: Fri, 4 Oct 2024 16:56:16 +0800 Subject: [PATCH 2470/2493] Adjust tooltip text format --- osu.Game/Screens/Play/HUD/ArgonSongProgressBar.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/ArgonSongProgressBar.cs b/osu.Game/Screens/Play/HUD/ArgonSongProgressBar.cs index 3c9553ee55..a33c965417 100644 --- a/osu.Game/Screens/Play/HUD/ArgonSongProgressBar.cs +++ b/osu.Game/Screens/Play/HUD/ArgonSongProgressBar.cs @@ -11,7 +11,6 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; using osu.Framework.Localisation; using osu.Framework.Utils; -using osu.Game.Extensions; using osu.Game.Graphics; using osuTK; @@ -38,8 +37,9 @@ namespace osu.Game.Screens.Play.HUD private float relativePositionX; - public LocalisableString TooltipText => $"{(relativePositionX > 0 ? (EndTime - StartTime) * relativePositionX / DrawWidth : relativePositionX > DrawWidth ? EndTime : 0).ToEditorFormattedString()}" - + $" - {(relativePositionX > 0 ? Math.Round(relativePositionX / DrawWidth * 100, 2) : relativePositionX > DrawWidth ? 100 : 0)}%"; + public LocalisableString TooltipText => $"{formatTime(TimeSpan.FromSeconds(relativePositionX > 0 ? Math.Round((EndTime - StartTime) * relativePositionX / DrawWidth / 1000) + : relativePositionX > DrawWidth ? Math.Round(EndTime / 1000) : 0))}" + + $" - {(relativePositionX > 0 ? Math.Round(relativePositionX / DrawWidth * 100, 1) : relativePositionX > DrawWidth ? 100 : 0)}%"; public ArgonSongProgressBar(float barHeight) { @@ -191,5 +191,7 @@ namespace osu.Game.Screens.Play.HUD set => fill.Colour = value; } } + + private string formatTime(TimeSpan timeSpan) => $"{(timeSpan < TimeSpan.Zero ? "-" : "")}{Math.Floor(timeSpan.Duration().TotalMinutes)}:{timeSpan.Duration().Seconds:D2}"; } } From 7cd724f342adca027eee034ce352a1a4dfc1a458 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 4 Oct 2024 11:09:14 +0200 Subject: [PATCH 2471/2493] Move setup screen background preview to appropriate form control See https://discord.com/channels/188630481301012481/188630652340404224/1291361342971707463. --- .../UserInterfaceV2/FormFileSelector.cs | 20 +++++++- .../Screens/Edit/Setup/ResourcesSection.cs | 11 +++- osu.Game/Screens/Edit/Setup/SetupScreen.cs | 50 ++++++------------- .../Edit/Setup/SetupScreenHeaderBackground.cs | 3 +- 4 files changed, 43 insertions(+), 41 deletions(-) diff --git a/osu.Game/Graphics/UserInterfaceV2/FormFileSelector.cs b/osu.Game/Graphics/UserInterfaceV2/FormFileSelector.cs index 3b822a1b2f..81023417a5 100644 --- a/osu.Game/Graphics/UserInterfaceV2/FormFileSelector.cs +++ b/osu.Game/Graphics/UserInterfaceV2/FormFileSelector.cs @@ -68,6 +68,8 @@ namespace osu.Game.Graphics.UserInterfaceV2 /// public LocalisableString PlaceholderText { get; init; } + public Container PreviewContainer { get; private set; } = null!; + private Box background = null!; private FormFieldCaption caption = null!; @@ -89,7 +91,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 private void load() { RelativeSizeAxes = Axes.X; - Height = 50; + AutoSizeAxes = Axes.Y; Masking = true; CornerRadius = 5; @@ -101,9 +103,23 @@ namespace osu.Game.Graphics.UserInterfaceV2 RelativeSizeAxes = Axes.Both, Colour = colourProvider.Background5, }, + PreviewContainer = new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding + { + Horizontal = 1.5f, + Top = 1.5f, + Bottom = 50 + }, + }, new Container { - RelativeSizeAxes = Axes.Both, + RelativeSizeAxes = Axes.X, + Height = 50, + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, Padding = new MarginPadding(9), Children = new Drawable[] { diff --git a/osu.Game/Screens/Edit/Setup/ResourcesSection.cs b/osu.Game/Screens/Edit/Setup/ResourcesSection.cs index 6fec7078a8..845c21b598 100644 --- a/osu.Game/Screens/Edit/Setup/ResourcesSection.cs +++ b/osu.Game/Screens/Edit/Setup/ResourcesSection.cs @@ -35,12 +35,17 @@ namespace osu.Game.Screens.Edit.Setup [Resolved] private Editor? editor { get; set; } - [Resolved] - private SetupScreenHeaderBackground headerBackground { get; set; } = null!; + private SetupScreenHeaderBackground headerBackground = null!; [BackgroundDependencyLoader] private void load() { + headerBackground = new SetupScreenHeaderBackground + { + RelativeSizeAxes = Axes.X, + Height = 110, + }; + Children = new Drawable[] { backgroundChooser = new FormFileSelector(".jpg", ".jpeg", ".png") @@ -55,6 +60,8 @@ namespace osu.Game.Screens.Edit.Setup }, }; + backgroundChooser.PreviewContainer.Add(headerBackground); + if (!string.IsNullOrEmpty(working.Value.Metadata.BackgroundFile)) backgroundChooser.Current.Value = new FileInfo(working.Value.Metadata.BackgroundFile); diff --git a/osu.Game/Screens/Edit/Setup/SetupScreen.cs b/osu.Game/Screens/Edit/Setup/SetupScreen.cs index 38720f6333..f8c4998263 100644 --- a/osu.Game/Screens/Edit/Setup/SetupScreen.cs +++ b/osu.Game/Screens/Edit/Setup/SetupScreen.cs @@ -24,9 +24,6 @@ namespace osu.Game.Screens.Edit.Setup { } - [Cached] - private SetupScreenHeaderBackground background = new SetupScreenHeaderBackground { RelativeSizeAxes = Axes.Both, }; - private OsuScrollContainer scroll = null!; private FillFlowContainer flow = null!; @@ -42,43 +39,24 @@ namespace osu.Game.Screens.Edit.Setup RelativeSizeAxes = Axes.Both, Colour = colourProvider.Background3, }, - new GridContainer + scroll = new OsuScrollContainer { RelativeSizeAxes = Axes.Both, - RowDimensions = - [ - new Dimension(GridSizeMode.Absolute, 110), - new Dimension() - ], - Content = new[] + Padding = new MarginPadding(15), + Child = flow = new FillFlowContainer { - new Drawable[] + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Full, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Spacing = new Vector2(25), + ChildrenEnumerable = ruleset.CreateEditorSetupSections().Select(section => section.With(s => { - background, - }, - new Drawable[] - { - scroll = new OsuScrollContainer - { - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding(15), - Child = flow = new FillFlowContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Direction = FillDirection.Full, - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - Spacing = new Vector2(25), - ChildrenEnumerable = ruleset.CreateEditorSetupSections().Select(section => section.With(s => - { - s.Width = 450; - s.Anchor = Anchor.TopCentre; - s.Origin = Anchor.TopCentre; - })), - } - } - } + s.Width = 450; + s.Anchor = Anchor.TopCentre; + s.Origin = Anchor.TopCentre; + })), } } }; diff --git a/osu.Game/Screens/Edit/Setup/SetupScreenHeaderBackground.cs b/osu.Game/Screens/Edit/Setup/SetupScreenHeaderBackground.cs index 033e5361bb..5f3e6eb469 100644 --- a/osu.Game/Screens/Edit/Setup/SetupScreenHeaderBackground.cs +++ b/osu.Game/Screens/Edit/Setup/SetupScreenHeaderBackground.cs @@ -29,7 +29,8 @@ namespace osu.Game.Screens.Edit.Setup InternalChild = content = new Container { RelativeSizeAxes = Axes.Both, - Masking = true + Masking = true, + CornerRadius = 3.5f, }; } From 61103cc712672f5f9069fa5f34ce0cfd2b5cdf55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 4 Oct 2024 11:18:09 +0200 Subject: [PATCH 2472/2493] Remove redundant initialiser --- osu.Game/Screens/Edit/Setup/ColoursSection.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Setup/ColoursSection.cs b/osu.Game/Screens/Edit/Setup/ColoursSection.cs index ee76ec1f6d..8de7f86523 100644 --- a/osu.Game/Screens/Edit/Setup/ColoursSection.cs +++ b/osu.Game/Screens/Edit/Setup/ColoursSection.cs @@ -28,7 +28,7 @@ namespace osu.Game.Screens.Edit.Setup }; } - private bool syncingColours = false; + private bool syncingColours; protected override void LoadComplete() { From e136568a18785f45d42a3d0590c5cdc1ea05215b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 4 Oct 2024 11:25:21 +0200 Subject: [PATCH 2473/2493] Remove linq usage to kill allocations --- osu.Game/Graphics/UserInterface/OsuMenu.cs | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/osu.Game/Graphics/UserInterface/OsuMenu.cs b/osu.Game/Graphics/UserInterface/OsuMenu.cs index 719100e138..6e7dad2b5f 100644 --- a/osu.Game/Graphics/UserInterface/OsuMenu.cs +++ b/osu.Game/Graphics/UserInterface/OsuMenu.cs @@ -3,7 +3,6 @@ #nullable disable -using System.Linq; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; @@ -47,10 +46,19 @@ namespace osu.Game.Graphics.UserInterface { base.Update(); - bool showCheckboxes = Items.Any(i => i is StatefulMenuItem); + bool showCheckboxes = false; - foreach (var drawableItem in ItemsContainer.OfType()) - drawableItem.ShowCheckbox.Value = showCheckboxes; + foreach (var drawableItem in ItemsContainer) + { + if (drawableItem.Item is StatefulMenuItem) + showCheckboxes = true; + } + + foreach (var drawableItem in ItemsContainer) + { + if (drawableItem is DrawableOsuMenuItem osuItem) + osuItem.ShowCheckbox.Value = showCheckboxes; + } } protected override void AnimateOpen() From ff2777a3b941e7d7efc4fdc2298739342762f515 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 4 Oct 2024 18:43:39 +0900 Subject: [PATCH 2474/2493] When adding a new combo colour, use the last colour as a starting point Also opens the popover automatically because you always want to edit it. --- .../Graphics/UserInterfaceV2/FormColourPalette.cs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/osu.Game/Graphics/UserInterfaceV2/FormColourPalette.cs b/osu.Game/Graphics/UserInterfaceV2/FormColourPalette.cs index 9575ebaa3b..fad58841e3 100644 --- a/osu.Game/Graphics/UserInterfaceV2/FormColourPalette.cs +++ b/osu.Game/Graphics/UserInterfaceV2/FormColourPalette.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Specialized; +using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions; @@ -19,6 +20,7 @@ using osu.Game.Graphics.UserInterface; using osu.Game.Overlays; using osu.Game.Resources.Localisation.Web; using osuTK; +using osuTK.Graphics; namespace osu.Game.Graphics.UserInterfaceV2 { @@ -76,7 +78,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 Spacing = new Vector2(5), Child = button = new RoundedButton { - Action = () => Colours.Add(Colour4.White), + Action = addNewColour, Size = new Vector2(70), Text = "+", } @@ -112,6 +114,16 @@ namespace osu.Game.Graphics.UserInterfaceV2 updateState(); } + private void addNewColour() + { + Color4 startingColour = Colours.Count > 0 + ? Colours.Last() + : Colour4.White; + + Colours.Add(startingColour); + flow.OfType().Last().TriggerClick(); + } + private void updateState() { background.Colour = colourProvider.Background5; From 45a6a743a226becf932488075fd91617f4a7f440 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 4 Oct 2024 13:12:25 +0200 Subject: [PATCH 2475/2493] Fix improper handling of decimal separator in "form" number boxes / sliders Spotted in passing in https://discord.com/channels/188630481301012481/1097318920991559880/1291693852981329981. --- osu.Game/Graphics/UserInterfaceV2/FormNumberBox.cs | 4 +++- osu.Game/Graphics/UserInterfaceV2/FormSliderBar.cs | 13 ++++++++++--- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/osu.Game/Graphics/UserInterfaceV2/FormNumberBox.cs b/osu.Game/Graphics/UserInterfaceV2/FormNumberBox.cs index 8ce6c85fa9..c3256e0038 100644 --- a/osu.Game/Graphics/UserInterfaceV2/FormNumberBox.cs +++ b/osu.Game/Graphics/UserInterfaceV2/FormNumberBox.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 System.Globalization; + namespace osu.Game.Graphics.UserInterfaceV2 { public partial class FormNumberBox : FormTextBox @@ -18,7 +20,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 public bool AllowDecimals { get; init; } protected override bool CanAddCharacter(char character) - => char.IsAsciiDigit(character) || (AllowDecimals && character == '.'); + => char.IsAsciiDigit(character) || (AllowDecimals && CultureInfo.CurrentCulture.NumberFormat.NumberDecimalSeparator.Contains(character)); } } } diff --git a/osu.Game/Graphics/UserInterfaceV2/FormSliderBar.cs b/osu.Game/Graphics/UserInterfaceV2/FormSliderBar.cs index 78c197dee6..a29e33a421 100644 --- a/osu.Game/Graphics/UserInterfaceV2/FormSliderBar.cs +++ b/osu.Game/Graphics/UserInterfaceV2/FormSliderBar.cs @@ -17,6 +17,7 @@ using osu.Framework.Input; using osu.Framework.Input.Events; using osu.Framework.Localisation; using osu.Game.Graphics.UserInterface; +using osu.Game.Localisation; using osu.Game.Overlays; namespace osu.Game.Graphics.UserInterfaceV2 @@ -83,8 +84,10 @@ namespace osu.Game.Graphics.UserInterfaceV2 [Resolved] private OverlayColourProvider colourProvider { get; set; } = null!; + private readonly Bindable currentLanguage = new Bindable(); + [BackgroundDependencyLoader] - private void load(OsuColour colours) + private void load(OsuColour colours, OsuGame? game) { RelativeSizeAxes = Axes.X; Height = 50; @@ -150,6 +153,9 @@ namespace osu.Game.Graphics.UserInterfaceV2 }, }, }; + + if (game != null) + currentLanguage.BindTo(game.CurrentLanguage); } protected override void LoadComplete() @@ -164,10 +170,11 @@ namespace osu.Game.Graphics.UserInterfaceV2 slider.IsDragging.BindValueChanged(_ => updateState()); + currentLanguage.BindValueChanged(_ => Schedule(updateValueDisplay)); current.BindValueChanged(_ => { updateState(); - updateTextBoxFromSlider(); + updateValueDisplay(); }, true); } @@ -258,7 +265,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 background.Colour = colourProvider.Background5; } - private void updateTextBoxFromSlider() + private void updateValueDisplay() { if (updatingFromTextBox) return; From 86c3e3e987740745ddebed4680dc1e3ff380ffa7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 4 Oct 2024 13:59:04 +0200 Subject: [PATCH 2476/2493] Replace `FormSliderBar.Instantaneous` with `TransferValueOnCommit` Rather than control the propagation of the value between the slider and the textbox, add a property that controls the propagation of the value between the bindables inside the form control to external bindables. This will help alleviate issues where the external bindable update incurs overheads due to having heavy change callbacks attached. --- .../UserInterface/TestSceneFormControls.cs | 15 +--- .../UserInterface/TestSceneFormSliderBar.cs | 63 ++++++++++++++ .../Graphics/UserInterfaceV2/FormSliderBar.cs | 86 ++++++++++++------- 3 files changed, 118 insertions(+), 46 deletions(-) create mode 100644 osu.Game.Tests/Visual/UserInterface/TestSceneFormSliderBar.cs diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneFormControls.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneFormControls.cs index 518c3fc693..c6fd65b973 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneFormControls.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneFormControls.cs @@ -72,7 +72,7 @@ namespace osu.Game.Tests.Visual.UserInterface }, new FormSliderBar { - Caption = "Instantaneous slider", + Caption = "Slider", Current = new BindableFloat { MinValue = 0, @@ -82,19 +82,6 @@ namespace osu.Game.Tests.Visual.UserInterface }, TabbableContentContainer = this, }, - new FormSliderBar - { - Caption = "Non-instantaneous slider", - Current = new BindableFloat - { - MinValue = 0, - MaxValue = 10, - Value = 5, - Precision = 0.1f, - }, - Instantaneous = false, - TabbableContentContainer = this, - }, new FormEnumDropdown { Caption = EditorSetupStrings.EnableCountdown, diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneFormSliderBar.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneFormSliderBar.cs new file mode 100644 index 0000000000..97835a993d --- /dev/null +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneFormSliderBar.cs @@ -0,0 +1,63 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.Extensions.ObjectExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterfaceV2; +using osu.Game.Overlays; +using osuTK; + +namespace osu.Game.Tests.Visual.UserInterface +{ + public partial class TestSceneFormSliderBar : OsuTestScene + { + [Cached] + private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine); + + [Test] + public void TestTransferValueOnCommit() + { + OsuSpriteText text; + FormSliderBar slider = null!; + + AddStep("create content", () => + { + Child = new FillFlowContainer + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Width = 0.5f, + Direction = FillDirection.Vertical, + Spacing = new Vector2(10), + Children = new Drawable[] + { + text = new OsuSpriteText(), + slider = new FormSliderBar + { + Caption = "Slider", + Current = new BindableFloat + { + MinValue = 0, + MaxValue = 10, + Precision = 0.1f, + Default = 5f, + } + }, + } + }; + slider.Current.BindValueChanged(_ => text.Text = $"Current value is: {slider.Current.Value}", true); + }); + AddToggleStep("toggle transfer value on commit", b => + { + if (slider.IsNotNull()) + slider.TransferValueOnCommit = b; + }); + } + } +} diff --git a/osu.Game/Graphics/UserInterfaceV2/FormSliderBar.cs b/osu.Game/Graphics/UserInterfaceV2/FormSliderBar.cs index a29e33a421..da28437eee 100644 --- a/osu.Game/Graphics/UserInterfaceV2/FormSliderBar.cs +++ b/osu.Game/Graphics/UserInterfaceV2/FormSliderBar.cs @@ -28,27 +28,23 @@ namespace osu.Game.Graphics.UserInterfaceV2 public Bindable Current { get => current.Current; - set => current.Current = value; - } - - private bool instantaneous = true; - - /// - /// Whether changes to the slider should instantaneously transfer to the text box (and vice versa). - /// If , the transfer will happen on text box commit (explicit, or implicit via focus loss), or on slider drag end. - /// - public bool Instantaneous - { - get => instantaneous; set { - instantaneous = value; - - if (slider.IsNotNull()) - slider.TransferValueOnCommit = !instantaneous; + current.Current = value; + currentNumberInstantaneous.Default = current.Default; } } + private readonly BindableNumberWithCurrent current = new BindableNumberWithCurrent(); + + private readonly BindableNumber currentNumberInstantaneous = new BindableNumber(); + + /// + /// Whether changes to the value should instantaneously transfer to outside bindables. + /// If , the transfer will happen on text box commit (explicit, or implicit via focus loss), or on slider commit. + /// + public bool TransferValueOnCommit { get; set; } + private CompositeDrawable? tabbableContentContainer; public CompositeDrawable? TabbableContentContainer @@ -62,8 +58,6 @@ namespace osu.Game.Graphics.UserInterfaceV2 } } - private readonly BindableNumberWithCurrent current = new BindableNumberWithCurrent(); - /// /// Caption describing this slider bar, displayed on top of the controls. /// @@ -147,8 +141,8 @@ namespace osu.Game.Graphics.UserInterfaceV2 Origin = Anchor.CentreRight, RelativeSizeAxes = Axes.X, Width = 0.5f, - Current = Current, - TransferValueOnCommit = !instantaneous, + Current = currentNumberInstantaneous, + OnCommit = () => current.Value = currentNumberInstantaneous.Value, } }, }, @@ -170,9 +164,28 @@ namespace osu.Game.Graphics.UserInterfaceV2 slider.IsDragging.BindValueChanged(_ => updateState()); - currentLanguage.BindValueChanged(_ => Schedule(updateValueDisplay)); - current.BindValueChanged(_ => + current.ValueChanged += e => currentNumberInstantaneous.Value = e.NewValue; + current.MinValueChanged += v => currentNumberInstantaneous.MinValue = v; + current.MaxValueChanged += v => currentNumberInstantaneous.MaxValue = v; + current.PrecisionChanged += v => currentNumberInstantaneous.Precision = v; + current.DisabledChanged += disabled => { + if (disabled) + { + // revert any changes before disabling to make sure we are in a consistent state. + currentNumberInstantaneous.Value = current.Value; + } + + currentNumberInstantaneous.Disabled = disabled; + }; + + current.CopyTo(currentNumberInstantaneous); + currentLanguage.BindValueChanged(_ => Schedule(updateValueDisplay)); + currentNumberInstantaneous.BindValueChanged(e => + { + if (!TransferValueOnCommit) + current.Value = e.NewValue; + updateState(); updateValueDisplay(); }, true); @@ -182,17 +195,15 @@ namespace osu.Game.Graphics.UserInterfaceV2 private void textChanged(ValueChangedEvent change) { - if (!instantaneous) return; - tryUpdateSliderFromTextBox(); } private void textCommitted(TextBox t, bool isNew) { tryUpdateSliderFromTextBox(); - // If the attempted update above failed, restore text box to match the slider. - Current.TriggerChange(); + currentNumberInstantaneous.TriggerChange(); + current.Value = currentNumberInstantaneous.Value; flashLayer.Colour = ColourInfo.GradientVertical(colourProvider.Dark2.Opacity(0), colourProvider.Dark2); flashLayer.FadeOutFromOne(800, Easing.OutQuint); @@ -204,7 +215,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 try { - switch (Current) + switch (currentNumberInstantaneous) { case Bindable bindableInt: bindableInt.Value = int.Parse(textBox.Current.Value); @@ -215,7 +226,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 break; default: - Current.Parse(textBox.Current.Value, CultureInfo.CurrentCulture); + currentNumberInstantaneous.Parse(textBox.Current.Value, CultureInfo.CurrentCulture); break; } } @@ -250,9 +261,9 @@ namespace osu.Game.Graphics.UserInterfaceV2 { textBox.Alpha = 1; - background.Colour = Current.Disabled ? colourProvider.Background4 : colourProvider.Background5; - caption.Colour = Current.Disabled ? colourProvider.Foreground1 : colourProvider.Content2; - textBox.Colour = Current.Disabled ? colourProvider.Foreground1 : colourProvider.Content1; + background.Colour = currentNumberInstantaneous.Disabled ? colourProvider.Background4 : colourProvider.Background5; + caption.Colour = currentNumberInstantaneous.Disabled ? colourProvider.Foreground1 : colourProvider.Content2; + textBox.Colour = currentNumberInstantaneous.Disabled ? colourProvider.Foreground1 : colourProvider.Content1; BorderThickness = IsHovered || textBox.Focused.Value || slider.IsDragging.Value ? 2 : 0; BorderColour = textBox.Focused.Value ? colourProvider.Highlight1 : colourProvider.Light4; @@ -269,12 +280,13 @@ namespace osu.Game.Graphics.UserInterfaceV2 { if (updatingFromTextBox) return; - textBox.Text = slider.GetDisplayableValue(Current.Value).ToString(); + textBox.Text = slider.GetDisplayableValue(currentNumberInstantaneous.Value).ToString(); } private partial class Slider : OsuSliderBar { public BindableBool IsDragging { get; set; } = new BindableBool(); + public Action? OnCommit { get; set; } private Box leftBox = null!; private Box rightBox = null!; @@ -381,6 +393,16 @@ namespace osu.Game.Graphics.UserInterfaceV2 { nub.MoveToX(value, 200, Easing.OutPow10); } + + protected override bool Commit() + { + bool result = base.Commit(); + + if (result) + OnCommit?.Invoke(); + + return result; + } } } } From 7816c41b94e367f90f7c3c7ecb142c0e0d116272 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 4 Oct 2024 14:03:38 +0200 Subject: [PATCH 2477/2493] Only transfer difficulty slider values on commit Closes https://github.com/ppy/osu/issues/30112. --- .../Edit/Setup/ManiaDifficultySection.cs | 5 +++++ osu.Game.Rulesets.Osu/Edit/Setup/OsuDifficultySection.cs | 7 +++++++ .../Edit/Setup/TaikoDifficultySection.cs | 4 ++++ osu.Game/Screens/Edit/Setup/DifficultySection.cs | 6 ++++++ 4 files changed, 22 insertions(+) diff --git a/osu.Game.Rulesets.Mania/Edit/Setup/ManiaDifficultySection.cs b/osu.Game.Rulesets.Mania/Edit/Setup/ManiaDifficultySection.cs index ed1de591f6..a23988362a 100644 --- a/osu.Game.Rulesets.Mania/Edit/Setup/ManiaDifficultySection.cs +++ b/osu.Game.Rulesets.Mania/Edit/Setup/ManiaDifficultySection.cs @@ -48,6 +48,7 @@ namespace osu.Game.Rulesets.Mania.Edit.Setup MaxValue = 10, Precision = 1, }, + TransferValueOnCommit = true, TabbableContentContainer = this, }, specialStyle = new FormCheckBox @@ -67,6 +68,7 @@ namespace osu.Game.Rulesets.Mania.Edit.Setup MaxValue = 10, Precision = 0.1f, }, + TransferValueOnCommit = true, TabbableContentContainer = this, }, overallDifficultySlider = new FormSliderBar @@ -80,6 +82,7 @@ namespace osu.Game.Rulesets.Mania.Edit.Setup MaxValue = 10, Precision = 0.1f, }, + TransferValueOnCommit = true, TabbableContentContainer = this, }, baseVelocitySlider = new FormSliderBar @@ -93,6 +96,7 @@ namespace osu.Game.Rulesets.Mania.Edit.Setup MaxValue = 3.6, Precision = 0.01f, }, + TransferValueOnCommit = true, TabbableContentContainer = this, }, tickRateSlider = new FormSliderBar @@ -106,6 +110,7 @@ namespace osu.Game.Rulesets.Mania.Edit.Setup MaxValue = 4, Precision = 1, }, + TransferValueOnCommit = true, TabbableContentContainer = this, }, }; diff --git a/osu.Game.Rulesets.Osu/Edit/Setup/OsuDifficultySection.cs b/osu.Game.Rulesets.Osu/Edit/Setup/OsuDifficultySection.cs index 7008c87d41..7a01646b35 100644 --- a/osu.Game.Rulesets.Osu/Edit/Setup/OsuDifficultySection.cs +++ b/osu.Game.Rulesets.Osu/Edit/Setup/OsuDifficultySection.cs @@ -42,6 +42,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Setup MaxValue = 10, Precision = 0.1f, }, + TransferValueOnCommit = true, TabbableContentContainer = this, }, healthDrainSlider = new FormSliderBar @@ -55,6 +56,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Setup MaxValue = 10, Precision = 0.1f, }, + TransferValueOnCommit = true, TabbableContentContainer = this, }, approachRateSlider = new FormSliderBar @@ -68,6 +70,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Setup MaxValue = 10, Precision = 0.1f, }, + TransferValueOnCommit = true, TabbableContentContainer = this, }, overallDifficultySlider = new FormSliderBar @@ -81,6 +84,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Setup MaxValue = 10, Precision = 0.1f, }, + TransferValueOnCommit = true, TabbableContentContainer = this, }, baseVelocitySlider = new FormSliderBar @@ -94,6 +98,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Setup MaxValue = 3.6, Precision = 0.01f, }, + TransferValueOnCommit = true, TabbableContentContainer = this, }, tickRateSlider = new FormSliderBar @@ -107,6 +112,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Setup MaxValue = 4, Precision = 1, }, + TransferValueOnCommit = true, TabbableContentContainer = this, }, stackLeniency = new FormSliderBar @@ -120,6 +126,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Setup MaxValue = 1, Precision = 0.1f }, + TransferValueOnCommit = true, TabbableContentContainer = this, }, }; diff --git a/osu.Game.Rulesets.Taiko/Edit/Setup/TaikoDifficultySection.cs b/osu.Game.Rulesets.Taiko/Edit/Setup/TaikoDifficultySection.cs index 8fce59e791..52f7176b3f 100644 --- a/osu.Game.Rulesets.Taiko/Edit/Setup/TaikoDifficultySection.cs +++ b/osu.Game.Rulesets.Taiko/Edit/Setup/TaikoDifficultySection.cs @@ -39,6 +39,7 @@ namespace osu.Game.Rulesets.Taiko.Edit.Setup MaxValue = 10, Precision = 0.1f, }, + TransferValueOnCommit = true, TabbableContentContainer = this, }, overallDifficultySlider = new FormSliderBar @@ -52,6 +53,7 @@ namespace osu.Game.Rulesets.Taiko.Edit.Setup MaxValue = 10, Precision = 0.1f, }, + TransferValueOnCommit = true, TabbableContentContainer = this, }, baseVelocitySlider = new FormSliderBar @@ -65,6 +67,7 @@ namespace osu.Game.Rulesets.Taiko.Edit.Setup MaxValue = 3.6, Precision = 0.01f, }, + TransferValueOnCommit = true, TabbableContentContainer = this, }, tickRateSlider = new FormSliderBar @@ -78,6 +81,7 @@ namespace osu.Game.Rulesets.Taiko.Edit.Setup MaxValue = 4, Precision = 1, }, + TransferValueOnCommit = true, TabbableContentContainer = this, }, }; diff --git a/osu.Game/Screens/Edit/Setup/DifficultySection.cs b/osu.Game/Screens/Edit/Setup/DifficultySection.cs index a27a7258c7..88241451cf 100644 --- a/osu.Game/Screens/Edit/Setup/DifficultySection.cs +++ b/osu.Game/Screens/Edit/Setup/DifficultySection.cs @@ -40,6 +40,7 @@ namespace osu.Game.Screens.Edit.Setup MaxValue = 10, Precision = 0.1f, }, + TransferValueOnCommit = true, TabbableContentContainer = this, }, healthDrainSlider = new FormSliderBar @@ -53,6 +54,7 @@ namespace osu.Game.Screens.Edit.Setup MaxValue = 10, Precision = 0.1f, }, + TransferValueOnCommit = true, TabbableContentContainer = this, }, approachRateSlider = new FormSliderBar @@ -66,6 +68,7 @@ namespace osu.Game.Screens.Edit.Setup MaxValue = 10, Precision = 0.1f, }, + TransferValueOnCommit = true, TabbableContentContainer = this, }, overallDifficultySlider = new FormSliderBar @@ -79,6 +82,7 @@ namespace osu.Game.Screens.Edit.Setup MaxValue = 10, Precision = 0.1f, }, + TransferValueOnCommit = true, TabbableContentContainer = this, }, baseVelocitySlider = new FormSliderBar @@ -92,6 +96,7 @@ namespace osu.Game.Screens.Edit.Setup MaxValue = 3.6, Precision = 0.01f, }, + TransferValueOnCommit = true, TabbableContentContainer = this, }, tickRateSlider = new FormSliderBar @@ -105,6 +110,7 @@ namespace osu.Game.Screens.Edit.Setup MaxValue = 4, Precision = 1, }, + TransferValueOnCommit = true, TabbableContentContainer = this, }, }; From 7cfc389d0391a91debe52b9a5ff01b6b3b469bce Mon Sep 17 00:00:00 2001 From: James Wilson Date: Fri, 4 Oct 2024 13:37:05 +0100 Subject: [PATCH 2478/2493] remove double-negative on `usingClassicSliderHeadAccuracy` --- osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index dc2113ed40..a5c2fa09c0 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -35,7 +35,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty { var osuAttributes = (OsuDifficultyAttributes)attributes; - usingClassicSliderAccuracy = !(score.Mods.OfType().All(m => !m.NoSliderHeadAccuracy.Value)); + usingClassicSliderAccuracy = score.Mods.OfType().Any(m => m.NoSliderHeadAccuracy.Value); accuracy = score.Accuracy; scoreMaxCombo = score.MaxCombo; From d4a00d75e80de49a72a8fdf20e623e437f4bbeb5 Mon Sep 17 00:00:00 2001 From: StanR Date: Fri, 4 Oct 2024 17:42:15 +0500 Subject: [PATCH 2479/2493] Update osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs Co-authored-by: James Wilson --- osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs index 08efb187fe..d10d2c5c05 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs @@ -131,7 +131,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators } // scale down the difficulty if the object is doubletappable - double doubletapness = prevObj.GetDoubletapness((OsuDifficultyHitObject?)prevObj.Next(0)); + double doubletapness = prevObj.GetDoubletapness(currObj); effectiveRatio *= 1 - doubletapness * 0.75; rhythmComplexitySum += Math.Sqrt(effectiveRatio * startRatio) * currHistoricalDecay; From 94aecae0cee7be5611fa6f4e5bae25bed55126a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 4 Oct 2024 14:48:48 +0200 Subject: [PATCH 2480/2493] Fix tests --- .../OsuDifficultyCalculatorTest.cs | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs index e35cf10d95..17b51085da 100644 --- a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs +++ b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs @@ -15,22 +15,22 @@ namespace osu.Game.Rulesets.Osu.Tests { protected override string ResourceAssembly => "osu.Game.Rulesets.Osu.Tests"; - [TestCase(6.710442985146793d, 239, "diffcalc-test")] - [TestCase(1.4386882251130073d, 54, "zero-length-sliders")] - [TestCase(0.42506480230838789d, 4, "very-fast-slider")] - [TestCase(0.14102693012101306d, 2, "nan-slider")] + [TestCase(6.7154251995274938d, 239, "diffcalc-test")] + [TestCase(1.4430610657612626d, 54, "zero-length-sliders")] + [TestCase(0.42630400627180914d, 4, "very-fast-slider")] + [TestCase(0.14143808967817237d, 2, "nan-slider")] public void Test(double expectedStarRating, int expectedMaxCombo, string name) => base.Test(expectedStarRating, expectedMaxCombo, name); - [TestCase(8.9742952703071666d, 239, "diffcalc-test")] - [TestCase(1.743180218215227d, 54, "zero-length-sliders")] - [TestCase(0.55071082800473514d, 4, "very-fast-slider")] + [TestCase(8.9808183779700208d, 239, "diffcalc-test")] + [TestCase(1.7483507893412422d, 54, "zero-length-sliders")] + [TestCase(0.55231632896800109d, 4, "very-fast-slider")] public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name) => Test(expectedStarRating, expectedMaxCombo, name, new OsuModDoubleTime()); - [TestCase(6.710442985146793d, 239, "diffcalc-test")] - [TestCase(1.4386882251130073d, 54, "zero-length-sliders")] - [TestCase(0.42506480230838789d, 4, "very-fast-slider")] + [TestCase(6.7154251995274938d, 239, "diffcalc-test")] + [TestCase(1.4430610657612626d, 54, "zero-length-sliders")] + [TestCase(0.42630400627180914d, 4, "very-fast-slider")] public void TestClassicMod(double expectedStarRating, int expectedMaxCombo, string name) => Test(expectedStarRating, expectedMaxCombo, name, new OsuModClassic()); From d7ba4ce7f2be89c74b5dc13a07ab094f1cc5408f Mon Sep 17 00:00:00 2001 From: CloneWith Date: Fri, 4 Oct 2024 23:30:50 +0800 Subject: [PATCH 2481/2493] Refactor progress tooltip updating method * Rewrite calculating logic * Remove useless variables --- .../Screens/Play/HUD/ArgonSongProgressBar.cs | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/ArgonSongProgressBar.cs b/osu.Game/Screens/Play/HUD/ArgonSongProgressBar.cs index a33c965417..4f0f94b606 100644 --- a/osu.Game/Screens/Play/HUD/ArgonSongProgressBar.cs +++ b/osu.Game/Screens/Play/HUD/ArgonSongProgressBar.cs @@ -37,9 +37,7 @@ namespace osu.Game.Screens.Play.HUD private float relativePositionX; - public LocalisableString TooltipText => $"{formatTime(TimeSpan.FromSeconds(relativePositionX > 0 ? Math.Round((EndTime - StartTime) * relativePositionX / DrawWidth / 1000) - : relativePositionX > DrawWidth ? Math.Round(EndTime / 1000) : 0))}" - + $" - {(relativePositionX > 0 ? Math.Round(relativePositionX / DrawWidth * 100, 1) : relativePositionX > DrawWidth ? 100 : 0)}%"; + public LocalisableString TooltipText => updateTooltip(); public ArgonSongProgressBar(float barHeight) { @@ -84,6 +82,19 @@ namespace osu.Game.Screens.Play.HUD catchUpColour = colours.BlueDark; } + private LocalisableString updateTooltip() + { + // clamping in case the cursor lays out of the progress bar horizontally + double progress = Math.Clamp(relativePositionX, 0, DrawWidth) / DrawWidth; + + TimeSpan currentSpan = TimeSpan.FromMilliseconds(Math.Round((EndTime - StartTime) * progress)); + int currentSeconds = currentSpan.Duration().Seconds; + // merging hours and minutes, e.g. 1:15:55 -> 75:55 + int currentMinutes = (int)Math.Floor(currentSpan.Duration().TotalMinutes); + + return $"{currentMinutes}:{currentSeconds:D2} - {progress:P1}"; + } + protected override void LoadComplete() { base.LoadComplete(); @@ -191,7 +202,5 @@ namespace osu.Game.Screens.Play.HUD set => fill.Colour = value; } } - - private string formatTime(TimeSpan timeSpan) => $"{(timeSpan < TimeSpan.Zero ? "-" : "")}{Math.Floor(timeSpan.Duration().TotalMinutes)}:{timeSpan.Duration().Seconds:D2}"; } } From 232381c9fbfa2b0a00c8840f703634e982f3ba54 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Sat, 5 Oct 2024 23:00:04 +0900 Subject: [PATCH 2482/2493] Rollback iOS workload to last known working version --- .github/workflows/ci.yml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6fbb74dfba..fc6e231c4b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -133,10 +133,7 @@ jobs: dotnet-version: "8.0.x" - name: Install .NET Workloads - run: dotnet workload install maui-ios - - - name: Select Xcode 16 - run: sudo xcode-select -s /Applications/Xcode_16.app/Contents/Developer + run: dotnet workload install ios --from-rollback-file https://raw.githubusercontent.com/ppy/osu-framework/refs/heads/master/workloads.json - name: Build run: dotnet build -c Debug osu.iOS From 72ac2eeb1da5fcb12147c8f5038a2fe8f94d9db4 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Sun, 6 Oct 2024 07:54:56 +0900 Subject: [PATCH 2483/2493] Update osu!framework package --- 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 6b42258b49..d1f91abd1a 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -10,7 +10,7 @@ true - + diff --git a/osu.iOS.props b/osu.iOS.props index 8acd1deff1..8e2c3b4bd0 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -17,6 +17,6 @@ -all - + From 2bcbaed5b8e17efee1f095116322362a7f0440cd Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 7 Oct 2024 14:11:31 +0900 Subject: [PATCH 2484/2493] 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 6b42258b49..f943ee727c 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -10,7 +10,7 @@ true - + diff --git a/osu.iOS.props b/osu.iOS.props index 8acd1deff1..51b7bfbf91 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -17,6 +17,6 @@ -all - + From b5cc45bdda059665d8c0a5c9c791342e57f321cc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 7 Oct 2024 14:26:42 +0900 Subject: [PATCH 2485/2493] Simplify format code (and adjust formatting slightly) --- .../Screens/Play/HUD/ArgonSongProgressBar.cs | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/ArgonSongProgressBar.cs b/osu.Game/Screens/Play/HUD/ArgonSongProgressBar.cs index 4f0f94b606..7d9cd25c4e 100644 --- a/osu.Game/Screens/Play/HUD/ArgonSongProgressBar.cs +++ b/osu.Game/Screens/Play/HUD/ArgonSongProgressBar.cs @@ -37,7 +37,20 @@ namespace osu.Game.Screens.Play.HUD private float relativePositionX; - public LocalisableString TooltipText => updateTooltip(); + public LocalisableString TooltipText + { + get + { + double progress = Math.Clamp(relativePositionX, 0, DrawWidth) / DrawWidth; + + TimeSpan currentSpan = TimeSpan.FromMilliseconds(Math.Round((EndTime - StartTime) * progress)); + + int seconds = currentSpan.Duration().Seconds; + int minutes = (int)Math.Floor(currentSpan.Duration().TotalMinutes); + + return $"{minutes}:{seconds:D2} ({progress:P0})"; + } + } public ArgonSongProgressBar(float barHeight) { @@ -82,19 +95,6 @@ namespace osu.Game.Screens.Play.HUD catchUpColour = colours.BlueDark; } - private LocalisableString updateTooltip() - { - // clamping in case the cursor lays out of the progress bar horizontally - double progress = Math.Clamp(relativePositionX, 0, DrawWidth) / DrawWidth; - - TimeSpan currentSpan = TimeSpan.FromMilliseconds(Math.Round((EndTime - StartTime) * progress)); - int currentSeconds = currentSpan.Duration().Seconds; - // merging hours and minutes, e.g. 1:15:55 -> 75:55 - int currentMinutes = (int)Math.Floor(currentSpan.Duration().TotalMinutes); - - return $"{currentMinutes}:{currentSeconds:D2} - {progress:P1}"; - } - protected override void LoadComplete() { base.LoadComplete(); From 6e4eed657ccf520e110f64c9aa4772a647a68b34 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 7 Oct 2024 14:32:31 +0900 Subject: [PATCH 2486/2493] Fix weird mouse position handling and don't return `true` to event --- osu.Game/Screens/Play/HUD/ArgonSongProgressBar.cs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/ArgonSongProgressBar.cs b/osu.Game/Screens/Play/HUD/ArgonSongProgressBar.cs index 7d9cd25c4e..ace21fa955 100644 --- a/osu.Game/Screens/Play/HUD/ArgonSongProgressBar.cs +++ b/osu.Game/Screens/Play/HUD/ArgonSongProgressBar.cs @@ -35,13 +35,13 @@ namespace osu.Game.Screens.Play.HUD private double trackTime => (EndTime - StartTime) * Progress; - private float relativePositionX; + private float lastMouseX; public LocalisableString TooltipText { get { - double progress = Math.Clamp(relativePositionX, 0, DrawWidth) / DrawWidth; + double progress = Math.Clamp(lastMouseX, 0, DrawWidth) / DrawWidth; TimeSpan currentSpan = TimeSpan.FromMilliseconds(Math.Round((EndTime - StartTime) * progress)); @@ -107,10 +107,8 @@ namespace osu.Game.Screens.Play.HUD { base.OnMouseMove(e); - var cursorPosition = e.ScreenSpaceMousePosition; - relativePositionX = ToLocalSpace(cursorPosition).X; - - return true; + lastMouseX = e.MousePosition.X; + return false; } protected override bool OnHover(HoverEvent e) From c7f2564c0ace0f0a7e184d1dfcba232ef8f357c8 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 7 Oct 2024 15:50:59 +0900 Subject: [PATCH 2487/2493] Make diffcalc workflow recreate comment on completion --- .github/workflows/diffcalc.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/diffcalc.yml b/.github/workflows/diffcalc.yml index 9f129a697c..7fb0709dec 100644 --- a/.github/workflows/diffcalc.yml +++ b/.github/workflows/diffcalc.yml @@ -361,8 +361,7 @@ jobs: uses: thollander/actions-comment-pull-request@fabd468d3a1a0b97feee5f6b9e499eab0dd903f6 # v2.5.0 with: comment_tag: ${{ env.EXECUTION_ID }} - mode: upsert - create_if_not_exists: false + mode: recreate message: | Target: ${{ needs.generator.outputs.TARGET }} Spreadsheet: ${{ needs.generator.outputs.SPREADSHEET_LINK }} @@ -372,8 +371,7 @@ jobs: uses: thollander/actions-comment-pull-request@fabd468d3a1a0b97feee5f6b9e499eab0dd903f6 # v2.5.0 with: comment_tag: ${{ env.EXECUTION_ID }} - mode: upsert - create_if_not_exists: false + mode: recreate message: | Difficulty calculation failed: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} From 9508b0ecab62a5e93b41b3b240e5617a6b82dc98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 7 Oct 2024 09:32:23 +0200 Subject: [PATCH 2488/2493] Fix tests --- .../OsuDifficultyCalculatorTest.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs index 17b51085da..efda3fa369 100644 --- a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs +++ b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs @@ -15,21 +15,21 @@ namespace osu.Game.Rulesets.Osu.Tests { protected override string ResourceAssembly => "osu.Game.Rulesets.Osu.Tests"; - [TestCase(6.7154251995274938d, 239, "diffcalc-test")] - [TestCase(1.4430610657612626d, 54, "zero-length-sliders")] + [TestCase(6.7171144000821119d, 239, "diffcalc-test")] + [TestCase(1.4485749025771304d, 54, "zero-length-sliders")] [TestCase(0.42630400627180914d, 4, "very-fast-slider")] [TestCase(0.14143808967817237d, 2, "nan-slider")] public void Test(double expectedStarRating, int expectedMaxCombo, string name) => base.Test(expectedStarRating, expectedMaxCombo, name); - [TestCase(8.9808183779700208d, 239, "diffcalc-test")] - [TestCase(1.7483507893412422d, 54, "zero-length-sliders")] + [TestCase(8.9825709931204205d, 239, "diffcalc-test")] + [TestCase(1.7550169162648608d, 54, "zero-length-sliders")] [TestCase(0.55231632896800109d, 4, "very-fast-slider")] public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name) => Test(expectedStarRating, expectedMaxCombo, name, new OsuModDoubleTime()); - [TestCase(6.7154251995274938d, 239, "diffcalc-test")] - [TestCase(1.4430610657612626d, 54, "zero-length-sliders")] + [TestCase(6.7171144000821119d, 239, "diffcalc-test")] + [TestCase(1.4485749025771304d, 54, "zero-length-sliders")] [TestCase(0.42630400627180914d, 4, "very-fast-slider")] public void TestClassicMod(double expectedStarRating, int expectedMaxCombo, string name) => Test(expectedStarRating, expectedMaxCombo, name, new OsuModClassic()); From 11fc811e2fed2f2f9ebf9eb7114e7552f9d7cfd5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 7 Oct 2024 16:44:10 +0900 Subject: [PATCH 2489/2493] Fix delete dialogs having generic "Caution" header text Regressed in https://github.com/ppy/osu/pull/28363. --- .../Editors/Components/DeleteRoundDialog.cs | 4 +--- .../Editors/Components/DeleteTeamDialog.cs | 4 +--- .../Collections/DeleteCollectionDialog.cs | 2 +- osu.Game/Localisation/DialogStrings.cs | 7 ++++++- osu.Game/Online/Chat/ExternalLinkOpener.cs | 2 +- .../Overlays/Dialog/DangerousActionDialog.cs | 4 ++-- osu.Game/Overlays/Dialog/DeletionDialog.cs | 20 +++++++++++++++++++ .../Overlays/Mods/DeleteModPresetDialog.cs | 2 +- .../MassDeleteConfirmationDialog.cs | 2 ++ .../DeleteDifficultyConfirmationDialog.cs | 2 +- .../Screens/Select/BeatmapDeleteDialog.cs | 2 +- .../Screens/Select/LocalScoreDeleteDialog.cs | 5 +---- osu.Game/Screens/Select/SkinDeleteDialog.cs | 2 +- 13 files changed, 39 insertions(+), 19 deletions(-) create mode 100644 osu.Game/Overlays/Dialog/DeletionDialog.cs diff --git a/osu.Game.Tournament/Screens/Editors/Components/DeleteRoundDialog.cs b/osu.Game.Tournament/Screens/Editors/Components/DeleteRoundDialog.cs index 769412bf94..6fff5111bd 100644 --- a/osu.Game.Tournament/Screens/Editors/Components/DeleteRoundDialog.cs +++ b/osu.Game.Tournament/Screens/Editors/Components/DeleteRoundDialog.cs @@ -2,18 +2,16 @@ // See the LICENCE file in the repository root for full licence text. using System; -using osu.Framework.Graphics.Sprites; using osu.Game.Overlays.Dialog; using osu.Game.Tournament.Models; namespace osu.Game.Tournament.Screens.Editors.Components { - public partial class DeleteRoundDialog : DangerousActionDialog + public partial class DeleteRoundDialog : DeletionDialog { public DeleteRoundDialog(TournamentRound round, Action action) { HeaderText = round.Name.Value.Length > 0 ? $@"Delete round ""{round.Name.Value}""?" : @"Delete unnamed round?"; - Icon = FontAwesome.Solid.Trash; DangerousAction = action; } } diff --git a/osu.Game.Tournament/Screens/Editors/Components/DeleteTeamDialog.cs b/osu.Game.Tournament/Screens/Editors/Components/DeleteTeamDialog.cs index 65fb47cf94..cf1dffba0c 100644 --- a/osu.Game.Tournament/Screens/Editors/Components/DeleteTeamDialog.cs +++ b/osu.Game.Tournament/Screens/Editors/Components/DeleteTeamDialog.cs @@ -2,20 +2,18 @@ // See the LICENCE file in the repository root for full licence text. using System; -using osu.Framework.Graphics.Sprites; using osu.Game.Overlays.Dialog; using osu.Game.Tournament.Models; namespace osu.Game.Tournament.Screens.Editors.Components { - public partial class DeleteTeamDialog : DangerousActionDialog + public partial class DeleteTeamDialog : DeletionDialog { public DeleteTeamDialog(TournamentTeam team, Action action) { HeaderText = team.FullName.Value.Length > 0 ? $@"Delete team ""{team.FullName.Value}""?" : team.Acronym.Value.Length > 0 ? $@"Delete team ""{team.Acronym.Value}""?" : @"Delete unnamed team?"; - Icon = FontAwesome.Solid.Trash; DangerousAction = action; } } diff --git a/osu.Game/Collections/DeleteCollectionDialog.cs b/osu.Game/Collections/DeleteCollectionDialog.cs index 9edc213077..80a7fa4bc0 100644 --- a/osu.Game/Collections/DeleteCollectionDialog.cs +++ b/osu.Game/Collections/DeleteCollectionDialog.cs @@ -8,7 +8,7 @@ using osu.Game.Overlays.Dialog; namespace osu.Game.Collections { - public partial class DeleteCollectionDialog : DangerousActionDialog + public partial class DeleteCollectionDialog : DeletionDialog { public DeleteCollectionDialog(Live collection, Action deleteAction) { diff --git a/osu.Game/Localisation/DialogStrings.cs b/osu.Game/Localisation/DialogStrings.cs index 043a3f5b4c..a7634575b8 100644 --- a/osu.Game/Localisation/DialogStrings.cs +++ b/osu.Game/Localisation/DialogStrings.cs @@ -12,7 +12,12 @@ namespace osu.Game.Localisation /// /// "Caution" /// - public static LocalisableString Caution => new TranslatableString(getKey(@"header_text"), @"Caution"); + public static LocalisableString CautionHeaderText => new TranslatableString(getKey(@"header_text"), @"Caution"); + + /// + /// "Are you sure you want to delete the following:" + /// + public static LocalisableString DeletionHeaderText => new TranslatableString(getKey(@"deletion_header_text"), @"Are you sure you want to delete the following:"); /// /// "Yes. Go for it." diff --git a/osu.Game/Online/Chat/ExternalLinkOpener.cs b/osu.Game/Online/Chat/ExternalLinkOpener.cs index 90fec5fafd..1c48a4fe6d 100644 --- a/osu.Game/Online/Chat/ExternalLinkOpener.cs +++ b/osu.Game/Online/Chat/ExternalLinkOpener.cs @@ -46,7 +46,7 @@ namespace osu.Game.Online.Chat { public ExternalLinkDialog(string url, Action openExternalLinkAction, Action copyExternalLinkAction) { - HeaderText = DialogStrings.Caution; + HeaderText = DialogStrings.CautionHeaderText; BodyText = $"Are you sure you want to open the following link in a web browser?\n\n{url}"; Icon = FontAwesome.Solid.ExclamationTriangle; diff --git a/osu.Game/Overlays/Dialog/DangerousActionDialog.cs b/osu.Game/Overlays/Dialog/DangerousActionDialog.cs index 31160d1832..287b0fa2c6 100644 --- a/osu.Game/Overlays/Dialog/DangerousActionDialog.cs +++ b/osu.Game/Overlays/Dialog/DangerousActionDialog.cs @@ -30,9 +30,9 @@ namespace osu.Game.Overlays.Dialog protected DangerousActionDialog() { - HeaderText = DialogStrings.Caution; + HeaderText = DialogStrings.CautionHeaderText; - Icon = FontAwesome.Regular.TrashAlt; + Icon = FontAwesome.Solid.ExclamationTriangle; Buttons = new PopupDialogButton[] { diff --git a/osu.Game/Overlays/Dialog/DeletionDialog.cs b/osu.Game/Overlays/Dialog/DeletionDialog.cs new file mode 100644 index 0000000000..26a29068a9 --- /dev/null +++ b/osu.Game/Overlays/Dialog/DeletionDialog.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.Graphics.Sprites; +using osu.Game.Localisation; + +namespace osu.Game.Overlays.Dialog +{ + /// + /// A dialog which provides confirmation for deletion of something. + /// + public abstract partial class DeletionDialog : DangerousActionDialog + { + protected DeletionDialog() + { + HeaderText = DialogStrings.DeletionHeaderText; + Icon = FontAwesome.Solid.Trash; + } + } +} diff --git a/osu.Game/Overlays/Mods/DeleteModPresetDialog.cs b/osu.Game/Overlays/Mods/DeleteModPresetDialog.cs index 9788764453..5651ecb34c 100644 --- a/osu.Game/Overlays/Mods/DeleteModPresetDialog.cs +++ b/osu.Game/Overlays/Mods/DeleteModPresetDialog.cs @@ -7,7 +7,7 @@ using osu.Game.Rulesets.Mods; namespace osu.Game.Overlays.Mods { - public partial class DeleteModPresetDialog : DangerousActionDialog + public partial class DeleteModPresetDialog : DeletionDialog { public DeleteModPresetDialog(Live modPreset) { diff --git a/osu.Game/Overlays/Settings/Sections/Maintenance/MassDeleteConfirmationDialog.cs b/osu.Game/Overlays/Settings/Sections/Maintenance/MassDeleteConfirmationDialog.cs index 7ead815fe9..a7a7ee2590 100644 --- a/osu.Game/Overlays/Settings/Sections/Maintenance/MassDeleteConfirmationDialog.cs +++ b/osu.Game/Overlays/Settings/Sections/Maintenance/MassDeleteConfirmationDialog.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; using osu.Game.Overlays.Dialog; @@ -12,6 +13,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance public MassDeleteConfirmationDialog(Action deleteAction, LocalisableString deleteContent) { BodyText = deleteContent; + Icon = FontAwesome.Solid.Trash; DangerousAction = deleteAction; } } diff --git a/osu.Game/Screens/Edit/DeleteDifficultyConfirmationDialog.cs b/osu.Game/Screens/Edit/DeleteDifficultyConfirmationDialog.cs index 8556949528..1aeb1d8a40 100644 --- a/osu.Game/Screens/Edit/DeleteDifficultyConfirmationDialog.cs +++ b/osu.Game/Screens/Edit/DeleteDifficultyConfirmationDialog.cs @@ -7,7 +7,7 @@ using osu.Game.Overlays.Dialog; namespace osu.Game.Screens.Edit { - public partial class DeleteDifficultyConfirmationDialog : DangerousActionDialog + public partial class DeleteDifficultyConfirmationDialog : DeletionDialog { public DeleteDifficultyConfirmationDialog(BeatmapInfo beatmapInfo, Action deleteAction) { diff --git a/osu.Game/Screens/Select/BeatmapDeleteDialog.cs b/osu.Game/Screens/Select/BeatmapDeleteDialog.cs index e98af8cca2..8bc40dbd9e 100644 --- a/osu.Game/Screens/Select/BeatmapDeleteDialog.cs +++ b/osu.Game/Screens/Select/BeatmapDeleteDialog.cs @@ -7,7 +7,7 @@ using osu.Game.Overlays.Dialog; namespace osu.Game.Screens.Select { - public partial class BeatmapDeleteDialog : DangerousActionDialog + public partial class BeatmapDeleteDialog : DeletionDialog { private readonly BeatmapSetInfo beatmapSet; diff --git a/osu.Game/Screens/Select/LocalScoreDeleteDialog.cs b/osu.Game/Screens/Select/LocalScoreDeleteDialog.cs index cd98872b65..ec2b8437e1 100644 --- a/osu.Game/Screens/Select/LocalScoreDeleteDialog.cs +++ b/osu.Game/Screens/Select/LocalScoreDeleteDialog.cs @@ -4,11 +4,10 @@ using osu.Framework.Allocation; using osu.Game.Overlays.Dialog; using osu.Game.Scoring; -using osu.Framework.Graphics.Sprites; namespace osu.Game.Screens.Select { - public partial class LocalScoreDeleteDialog : DangerousActionDialog + public partial class LocalScoreDeleteDialog : DeletionDialog { private readonly ScoreInfo score; @@ -21,8 +20,6 @@ namespace osu.Game.Screens.Select private void load(ScoreManager scoreManager) { BodyText = $"{score.User} ({score.DisplayAccuracy}, {score.Rank})"; - - Icon = FontAwesome.Regular.TrashAlt; DangerousAction = () => scoreManager.Delete(score); } } diff --git a/osu.Game/Screens/Select/SkinDeleteDialog.cs b/osu.Game/Screens/Select/SkinDeleteDialog.cs index 6612ae837a..cd14b5b6d2 100644 --- a/osu.Game/Screens/Select/SkinDeleteDialog.cs +++ b/osu.Game/Screens/Select/SkinDeleteDialog.cs @@ -7,7 +7,7 @@ using osu.Game.Overlays.Dialog; namespace osu.Game.Screens.Select { - public partial class SkinDeleteDialog : DangerousActionDialog + public partial class SkinDeleteDialog : DeletionDialog { private readonly Skin skin; From 3da59f44b59aa7a098d72769bfca46ff737ae842 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 7 Oct 2024 10:28:44 +0200 Subject: [PATCH 2490/2493] Fix clicking "centre on selected object" button not updating slider state --- osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs b/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs index 4fa8852770..a8331ab4aa 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs @@ -188,6 +188,12 @@ namespace osu.Game.Rulesets.Osu.Edit StartPosition.Value = new Vector2(StartPosition.Value.X, y.NewValue); }, true); + StartPosition.BindValueChanged(pos => + { + StartPositionX.Value = pos.NewValue.X; + StartPositionY.Value = pos.NewValue.Y; + }); + Spacing.BindValueChanged(spacing => { spacingSlider.ContractedLabelText = $"S: {spacing.NewValue:N0}"; From 04c65ad91969e2835e4f382a026a53df07abc215 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 7 Oct 2024 10:29:18 +0200 Subject: [PATCH 2491/2493] Fix "centre on selected object" sometimes remaining disabled after moving grid --- osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs b/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs index a8331ab4aa..dff2347ca5 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs @@ -192,6 +192,7 @@ namespace osu.Game.Rulesets.Osu.Edit { StartPositionX.Value = pos.NewValue.X; StartPositionY.Value = pos.NewValue.Y; + updateEnabledStates(); }); Spacing.BindValueChanged(spacing => From 2b5ddddf4f9d24b000e01d1ef7c8929beaaa3b18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 7 Oct 2024 10:29:45 +0200 Subject: [PATCH 2492/2493] Fix "centre on selected object" button not respecting precision of allowable grid positions --- osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs b/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs index dff2347ca5..972224a230 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.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.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -10,6 +11,7 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; +using osu.Framework.Utils; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osu.Game.Input.Bindings; @@ -123,7 +125,8 @@ namespace osu.Game.Rulesets.Osu.Edit if (editorBeatmap.SelectedHitObjects.Count != 1) return; - StartPosition.Value = ((IHasPosition)editorBeatmap.SelectedHitObjects.Single()).Position; + var position = ((IHasPosition)editorBeatmap.SelectedHitObjects.Single()).Position; + StartPosition.Value = new Vector2(MathF.Round(position.X), MathF.Round(position.Y)); updateEnabledStates(); }, RelativeSizeAxes = Axes.X, @@ -243,7 +246,7 @@ namespace osu.Game.Rulesets.Osu.Edit { useSelectedObjectPositionButton.Enabled.Value = expandingContainer?.Expanded.Value == true && editorBeatmap.SelectedHitObjects.Count == 1 - && StartPosition.Value != ((IHasPosition)editorBeatmap.SelectedHitObjects.Single()).Position; + && !Precision.AlmostEquals(StartPosition.Value, ((IHasPosition)editorBeatmap.SelectedHitObjects.Single()).Position, 0.5f); } private void nextGridSize() From 639caf167df19a6390922e4cdd3cd4799c09d979 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 7 Oct 2024 18:27:28 +0900 Subject: [PATCH 2493/2493] Save master state in workflow --- .github/workflows/diffcalc.yml | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/.github/workflows/diffcalc.yml b/.github/workflows/diffcalc.yml index 7fb0709dec..ebf22b8a0e 100644 --- a/.github/workflows/diffcalc.yml +++ b/.github/workflows/diffcalc.yml @@ -104,6 +104,25 @@ env: EXECUTION_ID: execution-${{ github.run_id }}-${{ github.run_number }}-${{ github.run_attempt }} jobs: + master-environment: + name: Save master environment + runs-on: ubuntu-latest + outputs: + HEAD: ${{ steps.get-head.outputs.HEAD }} + steps: + - name: Checkout osu + uses: actions/checkout@v4 + with: + ref: master + sparse-checkout: | + README.md + + - name: Get HEAD ref + id: get-head + run: | + ref=$(git log -1 --format='%H') + echo "HEAD=https://github.com/${{ github.repository }}/commit/${ref}" >> "${GITHUB_OUTPUT}" + check-permissions: name: Check permissions runs-on: ubuntu-latest @@ -121,7 +140,7 @@ jobs: create-comment: name: Create PR comment - needs: check-permissions + needs: [ master-environment, check-permissions ] runs-on: ubuntu-latest if: ${{ github.event_name == 'issue_comment' && github.event.issue.pull_request }} steps: @@ -158,7 +177,7 @@ jobs: environment: name: Setup environment - needs: directory + needs: [ master-environment, directory ] runs-on: self-hosted env: VARS_JSON: ${{ toJSON(vars) }} @@ -182,6 +201,10 @@ jobs: fi done + - name: Add master environment + run: | + sed -i "s;^OSU_A=.*$;OSU_A=${{ needs.master-environment.outputs.HEAD }};" "${{ needs.directory.outputs.GENERATOR_ENV }}" + - name: Add pull-request environment if: ${{ github.event_name == 'issue_comment' && github.event.issue.pull_request }} run: |